Files
OrcaSlicer/src/libslic3r/IMEXHelpers.hpp
T
Clifford GarwoodandClaude Opus 4.7 c2492ccc47 refactor(imex): extract imex_pem_tool_for helper + unit tests
The physical_extruder_map translation used by IMEX per-tool PA emission was
inlined identically at two sites in GCode.cpp (tool-change and second-layer
transition). Extract to IMEXHelpers so the routing rule ("parallel mode AND
populated pem → physical index, else -1") is testable in isolation and the
call sites read as intent rather than re-deriving the conditional.

Production change is behavior-preserving:
- Same predicate (`!mode.empty() && mode != "primary"`)
- Same empty-pem short-circuit returning -1
- Same get_at() dispatch on hit
- Both call sites replaced with a single call

Four unit tests in [IMEX] cover the routing matrix:
  - non-IMEX ("") and primary mode short-circuit
  - parallel mode + empty pem short-circuits (defense-in-depth; get_at would
    throw on empty values otherwise)
  - identity pem (non-MMU IDEX) routes filament to itself
  - MMU collapse routes multiple logical slots to one physical (7-slot profile
    with 4-lane MMU on physical 0 and direct drives on 1/2/3)

All IMEX + Variant regression suites pass post-refactor (133 assertions / 48
cases under libslic3r, 25 assertions / 6 cases under fff_print [PressureAdvance]).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 23:07:23 -04:00

121 lines
6.7 KiB
C++

#pragma once
#include <map>
#include <string>
#include <utility>
#include <vector>
#include <Eigen/Geometry>
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/Point.hpp"
namespace Slic3r {
class PresetBundle;
// Returns the effective physical_extruder_map given an optionally-explicit map and
// the printer's `printer_extruder_id`. If `explicit_pem` has size >= 2 the caller
// authored one, and it is returned verbatim. Otherwise the map is auto-derived
// from `printer_extruder_id` by converting each 1-indexed value to 0-indexed.
// Returns an empty ConfigOptionInts if neither source yields any values.
// Slice-time (PrintApply) and GUI ghost-color paths both call this so a printer
// profile without an explicit pem still gets a consistent mapping.
ConfigOptionInts effective_physical_extruder_map(const ConfigOptionInts* explicit_pem,
const ConfigOptionInts* printer_extruder_id);
// GUI overload: resolves the effective pem from a live PresetBundle using the
// project_config → printer preset fallback, then derives from printer_extruder_id
// if neither holds a user-authored map (size >= 2). Use this instead of open-coding
// the lookup at ghost-color, tooltip, click-gate, and cache-key call sites so they
// all agree on what the slicer will see.
ConfigOptionInts effective_physical_extruder_map(const PresetBundle& pb);
// Returns the physical extruder index to emit as a per-tool qualifier (PA / temperature)
// for the given filament slot, or -1 when the current IMEX state does not warrant a
// per-tool qualification: non-IMEX / primary mode, or the pem is empty / unpopulated.
// Used at tool-change and second-layer transition call sites where IMEX parallel modes
// route emission through the physical extruder, while ordinary tool changes emit bare
// firmware commands like any non-IMEX printer.
int imex_pem_tool_for(int filament_id, const std::string& parallel_mode, const ConfigOptionInts& pem);
// Returns the lowest 0-based logical filament index L such that pem[L] == physical.
// Returns -1 if no filament routes to `physical`.
// Degenerate case: an empty pem returns 0 when `physical == 0` (identity-on-head-0
// fallback for printers that never configured a pem) and -1 otherwise.
// With identity pem, the caller sees logical == physical behavior because
// position L holds value L.
int first_filament_for_physical_head(const ConfigOptionInts& pem, int physical);
// Returns true if the printer has at least one physical head fed by >= 2 logical
// filaments (i.e., an MMU/AFC is present on some head). Drives the plater's
// click-handler branch: true -> open popover; false -> retain cycle-through.
bool has_mmu(const ConfigOptionInts& pem);
// Returns true if any physical head OTHER than `primary_physical` is fed by
// >= 2 logical filaments. Primary is the head whose filament is already
// controlled by the left sidebar's object->filament assignment, so an MMU
// there needs no popover row; only non-primary MMU heads do.
// Returns false when pem is empty or size < 2.
bool has_non_primary_mmu(const ConfigOptionInts& pem, int primary_physical);
// Parses one mode's entry from `imex_mode_active_tools` and returns the
// 0-based physical T-index carrying the `:P` (Primary) role marker.
// Accepts two forms:
// "0:P,1:C,2:C" (explicit role suffix)
// "0" (backwards-compat: a bare index == Primary)
// Returns -1 on empty/malformed input or if no primary is found.
// The caller is responsible for indexing into imex_mode_active_tools by mode.
int imex_primary_tool_for_mode(const std::string& active_tools_for_mode);
// Parses the "phys:slot,phys:slot" serialization of imex_head_filament_map.
// Keys are physical T-indices (0-based); values are 1-based filament slots.
// Returns empty map on empty/malformed input.
std::map<int,int> parse_imex_head_filament_map(const std::string& s);
// Resolves which 0-based logical filament to use for a physical head.
// 1. If the plate map overrides `physical`, returns (slot - 1) — 1-based → 0-based.
// 2. Else falls back to first_filament_for_physical_head(pem, physical).
// Returns -1 if neither source yields a valid filament.
int resolve_filament_for_head(const std::map<int,int>& plate_map,
const ConfigOptionInts& pem,
int physical);
enum class ImexRole { Primary, Copy, Mirror };
// Parses `imex_mode_active_tools[mode]` into a list of (physical_head, role) pairs.
// Accepted token forms (comma-separated, whitespace-tolerant):
// "phys" — bare index, role defaults to Copy
// "phys:P" — Primary
// "phys:C" — Copy
// "phys:M" — Mirror
// "phys:???" — unknown role suffix, treated as Copy
// Malformed tokens (unparseable int, negative phys) are skipped.
// NOTE: the bare-token → Copy default differs from `imex_primary_tool_for_mode`,
// which separately scans for a Primary; callers needing the primary index should
// use that helper. This parser is for call sites that already have the primary
// in hand and need the full head/role list (e.g. ghost factory/updater).
std::vector<std::pair<int, ImexRole>> parse_imex_active_tools(const std::string& active_tools_for_mode);
// World-space transform composed as `head_xf * primary_instance_world` to place a ghost
// copy of the primary into `target`'s frame under `role`.
// `gantry_offset` = center_for(target) - center_for(primary) (XY, in mm).
// `primary_zone_center` = XY center of the primary head's zone in world coords. Only
// consulted for Mirror; Copy/Primary ignore it.
// Copy: pure translation by gantry_offset. Ghost tracks primary 1:1 during drag.
// Mirror: true reflection about the zone-boundary plane between primary and target.
// The plane is perpendicular to the primary-row gantry axis (X for all current
// IMEX printers) and passes through `primary_zone_center.x + gantry_offset.x / 2`.
// Ghost origin lands at the mirrored position within the target zone (matches
// where the mirror tool will actually print), and primary drag reflects across
// that plane so the ghost's X moves opposite the primary's X while Y tracks 1:1
// — i.e. the ghost stays a true mirror as you drag. Geometry is X-flipped
// regardless of gantry_offset direction so off-row Mirror targets (e.g. T3 on
// a 2x2) reflect across the same plane as on-row peers.
// Zero-length gantry_offset degenerates to identity.
// Primary: identity.
Transform3d imex_head_transform(int primary, int target, ImexRole role,
const Vec2d& gantry_offset,
const Vec2d& primary_zone_center = Vec2d::Zero());
} // namespace Slic3r