refactor(imex): drop unused Split mode-type infrastructure

The mode-type tag (imex_mode_types config + imex_mode_type_for helper +
Split sentinel) was added in 0bb1cef as scaffolding for the Split rendering
work that landed in 4370cca and then got reverted in d9be71b. With Phase 2-4
gone, this scaffolding is now unused dead code — and the design we settled
on instead is to leave topology entirely implicit (parsed from active_tools_str)
rather than carrying a per-mode type tag the user would otherwise have to
manage explicitly.

The multi-color slicing block + bare T<n> suppression that were the actual
substance of the safeguards work stay in place:
- imex_multicolor_block_reason still allows multi-color exactly when 2+ tools
  are active on the primary's gantry — Felix's hypothetical IQEX paired-gantry
  case works through this path, no new mode type required.
- Slicer-side: bare T<n> stays suppressed at print-start in IMEX parallel modes;
  mid-print T<n> emits naturally for the legitimate IQEX 4-tool-active scenario.

Removed:
- ConfigOptionStrings imex_mode_types (PrintConfig.hpp/cpp + Preset.cpp key list)
- imex_mode_type_for helper + kImexModeType{Primary,Copy,Mirror,Split} sentinels
- mode_type parameter on imex_multicolor_block_reason and the Split short-circuit
- mode_type plumbing in Print::validate and PartPlate::has_imex_multimaterial_conflict
- Three unit tests for imex_mode_type_for + two Split-specific multicolor block tests

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Clifford GarwoodandClaude Opus 4.7 committed 2026-04-28 00:48:39 -04:00
1 parent d9be71b1ca
commit 66891899e9
8 files changed
+14 -134

No files matched your search

+3 -36
View File
@@ -48,30 +48,7 @@ bool imex_suppresses_bare_toolchange(const std::string& parallel_mode, unsigned
&& parallel_mode != kImexPrimaryMode;
}
std::string imex_mode_type_for(const std::string& mode_name,
const std::vector<std::string>& mode_names,
const std::vector<std::string>& mode_types)
{
if (mode_name.empty() || mode_name == kImexPrimaryMode)
return kImexModeTypePrimary;
// Look up the parallel-vector entry by index match.
for (size_t i = 0; i < mode_names.size(); ++i) {
if (mode_names[i] != mode_name) continue;
if (i < mode_types.size() && !mode_types[i].empty())
return mode_types[i];
break; // entry found but no type set → fall through to inference
}
// Legacy fallback: a mode literally named "copy" / "mirror" / "split" was authored
// before imex_mode_types existed. Take the name as the type.
if (mode_name == kImexModeTypeCopy ||
mode_name == kImexModeTypeMirror ||
mode_name == kImexModeTypeSplit)
return mode_name;
return kImexModeTypePrimary;
}
std::string imex_multicolor_block_reason(const std::string& parallel_mode,
const std::string& mode_type,
const std::string& active_tools_str,
int tools_per_gantry,
const std::vector<int>& used_filaments_0b,
@@ -84,10 +61,7 @@ std::string imex_multicolor_block_reason(const std::string& parallel_mode,
// MMU lane sharing detection: two or more used filaments routed to the same
// physical head means a mid-print MMU swap on that head, which IMEX parallel
// modes can't slave to the secondary gantry. Applies to all parallel mode
// types including Split — even Split can't replicate MMU lane swaps across
// gantries because the slaved gantry's physical heads can't independently
// toolchange.
// modes can't slave to the secondary gantry.
if (!pem.values.empty()) {
std::unordered_set<int> seen_physicals;
for (int filament : used_filaments_0b) {
@@ -103,13 +77,6 @@ std::string imex_multicolor_block_reason(const std::string& parallel_mode,
}
}
// Split modes are explicitly designed for per-gantry multi-color: each gantry
// gets half the bed and runs its own multi-tool sequence (the slaved gantry
// mirrors/copies the primary's sequence). The within-gantry-toolchange check
// below doesn't apply because Split's whole purpose is to enable that.
if (mode_type == kImexModeTypeSplit)
return {};
// Determine which physical head is the primary in this mode, then count how many
// tool roles are assigned to tools on the same gantry. Multi-color requires at
// least 2 (so there's a within-gantry toolchange topology).
@@ -130,8 +97,8 @@ std::string imex_multicolor_block_reason(const std::string& parallel_mode,
return "Multi-color prints in IDEX/IQEX parallel modes require at least two tools "
"assigned to the primary tool's gantry — there is no within-gantry "
"toolchange path otherwise. Either reduce the print to a single filament, "
"switch to Primary mode, or use a Split-type mode designed for per-gantry "
"multi-color printing.";
"switch to Primary mode, or define a parallel mode that pairs two tools on "
"the primary gantry.";
}
return {};
}
-23
View File
@@ -105,25 +105,6 @@ int imex_pem_tool_for(int filament_id, const std::string& parallel_mode, const C
// contains its own T<n> is handled separately by GCode.cpp's custom_gcode_changes_tool().
bool imex_suppresses_bare_toolchange(const std::string& parallel_mode, unsigned int toolchange_count);
// Mode-type sentinels used by imex_mode_types entries and returned by
// imex_mode_type_for(). Compared against to drive zone/ghost/validation behavior.
inline constexpr const char* kImexModeTypePrimary = "primary";
inline constexpr const char* kImexModeTypeCopy = "copy";
inline constexpr const char* kImexModeTypeMirror = "mirror";
inline constexpr const char* kImexModeTypeSplit = "split";
// Looks up the topology type ("primary" / "copy" / "mirror" / "split") for the named
// mode. Reads from the parallel imex_mode_types vector when present; falls back to
// inferring from the mode_name itself when the entry is missing or empty (legacy
// presets pre-dating imex_mode_types — a mode literally named "copy" is treated as
// a copy-type mode, etc.). Anything unknown defaults to "primary".
//
// `mode_name` should be the active mode being looked up. `mode_names` and `mode_types`
// are the parallel vectors from the printer config.
std::string imex_mode_type_for(const std::string& mode_name,
const std::vector<std::string>& mode_names,
const std::vector<std::string>& mode_types);
// Returns a user-facing block reason when multi-color printing is incompatible with
// the active IMEX parallel mode, or an empty string when the configuration is OK.
//
@@ -147,16 +128,12 @@ std::string imex_mode_type_for(const std::string& mode_name,
//
// Inputs:
// parallel_mode — m_imex_parallel_mode at slice time
// mode_type — imex_mode_type_for(parallel_mode, ...). "split" types skip the
// gantry-pair check (Split modes are explicitly designed for
// per-gantry multi-color); MMU sharing still blocks them.
// active_tools_str — the imex_mode_active_tools entry for the active mode, e.g.
// "0:P,1:C,2:M,3:M"
// tools_per_gantry — imex_tools_per_gantry from printer config (>= 1)
// used_filaments_0b — 0-based logical filament indices used by the print
// pem — physical_extruder_map (logical idx → physical head)
std::string imex_multicolor_block_reason(const std::string& parallel_mode,
const std::string& mode_type,
const std::string& active_tools_str,
int tools_per_gantry,
const std::vector<int>& used_filaments_0b,
+1 -1
View File
@@ -1044,7 +1044,7 @@ static std::vector<std::string> s_Preset_printer_options {
// IDEX/IQEX (independent X extruder) — printer capability and user-defined modes
"is_imex", "imex_gantry_count", "imex_tools_per_gantry", "imex_tool_layout",
"imex_nozzle_clearance_x", "imex_nozzle_clearance_y", "imex_carriage_margin", "imex_viz_theme",
"imex_mode_names", "imex_mode_active_tools", "imex_mode_gcodes", "imex_mode_types"
"imex_mode_names", "imex_mode_active_tools", "imex_mode_gcodes"
};
static std::vector<std::string> s_Preset_sla_print_options {
-3
View File
@@ -1216,15 +1216,12 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons*
break;
}
}
const std::string mode_type = imex_mode_type_for(
parallel_mode, mode_names, m_config.imex_mode_types.values);
std::vector<int> used_filaments_0b;
used_filaments_0b.reserve(extruders.size());
for (unsigned int e : extruders)
used_filaments_0b.push_back((int)e);
const std::string reason = imex_multicolor_block_reason(
parallel_mode,
mode_type,
active_tools_str,
m_config.imex_tools_per_gantry.value,
used_filaments_0b,
-14
View File
@@ -5667,20 +5667,6 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionStrings());
// Mode-type tag drives how zones, ghosts, and slice-time validation interpret a
// given mode entry. Parallel to imex_mode_names. Values: "primary", "copy",
// "mirror", "split". Empty / missing entries fall back to "primary" except for
// entries named "copy" or "mirror" (legacy compat with pre-type configs).
def = this->add("imex_mode_types", coStrings);
def->label = L("IDEX/IQEX Mode Types");
def->tooltip = L("Per-mode topology tag. \"copy\" / \"mirror\" produce one ghost per "
"non-primary tool at the corresponding mirrored/copied position. "
"\"split\" splits the bed in half along the gantry axis and aggregates "
"each gantry's tools into a single multi-color print region — used "
"for paired-gantry IQEX multi-color printing.");
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionStrings());
def = this->add("manual_filament_change", coBool);
def->label = L("Manual Filament Change");
def->tooltip = L("Enable this option to omit the custom Change filament G-code only at the beginning of the print. "
-1
View File
@@ -1368,7 +1368,6 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionStrings, imex_mode_names))
((ConfigOptionStrings, imex_mode_active_tools))
((ConfigOptionStrings, imex_mode_gcodes))
((ConfigOptionStrings, imex_mode_types))
((ConfigOptionBool, single_extruder_multi_material_priming))
((ConfigOptionBool, wipe_tower_no_sparse_layers))
((ConfigOptionString, change_filament_gcode))
+1 -7
View File
@@ -1236,7 +1236,6 @@ bool PartPlate::has_imex_multimaterial_conflict() const
// Resolve the active mode's tools string from the printer config.
auto* names_opt = printer_cfg.option<ConfigOptionStrings>("imex_mode_names");
auto* tools_opt = printer_cfg.option<ConfigOptionStrings>("imex_mode_active_tools");
auto* types_opt = printer_cfg.option<ConfigOptionStrings>("imex_mode_types");
auto* tpg_opt = printer_cfg.option<ConfigOptionInt>("imex_tools_per_gantry");
auto* pem_opt = printer_cfg.option<ConfigOptionInts>("physical_extruder_map");
if (!names_opt || !tools_opt || !tpg_opt || !pem_opt) return false;
@@ -1249,18 +1248,13 @@ bool PartPlate::has_imex_multimaterial_conflict() const
}
}
const std::vector<std::string> empty_types;
const std::string mode_type = imex_mode_type_for(
mode, names_opt->values,
types_opt ? types_opt->values : empty_types);
// Convert PartPlate's 1-based extruder list to the 0-based form the helper expects.
const std::vector<int> used_1b = get_extruders(true);
std::vector<int> used_0b;
used_0b.reserve(used_1b.size());
for (int e : used_1b) if (e > 0) used_0b.push_back(e - 1);
return !imex_multicolor_block_reason(mode, mode_type, active_tools_str, tpg_opt->value, used_0b, *pem_opt).empty();
return !imex_multicolor_block_reason(mode, active_tools_str, tpg_opt->value, used_0b, *pem_opt).empty();
}
void PartPlate::render_imex_zones(bool force_default_color)
+9 -49
View File
@@ -116,39 +116,16 @@ TEST_CASE("imex_suppresses_bare_toolchange — parallel mode allows mid-print to
REQUIRE_FALSE(imex_suppresses_bare_toolchange("backup_mode", 100));
}
TEST_CASE("imex_mode_type_for — primary sentinel always returns primary", "[IMEX]") {
REQUIRE(imex_mode_type_for("", {}, {}) == "primary");
REQUIRE(imex_mode_type_for("primary", {}, {}) == "primary");
}
TEST_CASE("imex_mode_type_for — explicit type from parallel array", "[IMEX]") {
std::vector<std::string> names = {"primary", "my-copy", "my-split"};
std::vector<std::string> types = {"primary", "copy", "split" };
REQUIRE(imex_mode_type_for("my-copy", names, types) == "copy");
REQUIRE(imex_mode_type_for("my-split", names, types) == "split");
}
TEST_CASE("imex_mode_type_for — legacy fallback when types vector missing", "[IMEX]") {
// Pre-imex_mode_types presets just had names. A mode literally named "copy" /
// "mirror" / "split" maps to itself; anything else defaults to primary.
std::vector<std::string> names = {"primary", "copy", "mirror", "split", "weird-name"};
std::vector<std::string> types; // empty → fallback to name-based inference
REQUIRE(imex_mode_type_for("copy", names, types) == "copy");
REQUIRE(imex_mode_type_for("mirror", names, types) == "mirror");
REQUIRE(imex_mode_type_for("split", names, types) == "split");
REQUIRE(imex_mode_type_for("weird-name", names, types) == "primary");
}
TEST_CASE("imex_multicolor_block_reason — non-IMEX prints never block", "[IMEX]") {
auto pem = make_pem({0, 1, 2, 3});
REQUIRE(imex_multicolor_block_reason("", "primary", "0:P,1:C", 2, {0, 1}, pem).empty());
REQUIRE(imex_multicolor_block_reason("primary", "primary", "0:P,1:C", 2, {0, 1}, pem).empty());
REQUIRE(imex_multicolor_block_reason("", "0:P,1:C", 2, {0, 1}, pem).empty());
REQUIRE(imex_multicolor_block_reason("primary", "0:P,1:C", 2, {0, 1}, pem).empty());
}
TEST_CASE("imex_multicolor_block_reason — single-color prints never block", "[IMEX]") {
auto pem = make_pem({0, 1, 2, 3});
REQUIRE(imex_multicolor_block_reason("copy", "copy", "0:P,1:C,2:M,3:M", 2, {0}, pem).empty());
REQUIRE(imex_multicolor_block_reason("mirror", "mirror", "0:P,1:C,2:M,3:M", 2, {}, pem).empty());
REQUIRE(imex_multicolor_block_reason("copy", "0:P,1:C,2:M,3:M", 2, {0}, pem).empty());
REQUIRE(imex_multicolor_block_reason("mirror", "0:P,1:C,2:M,3:M", 2, {}, pem).empty());
}
TEST_CASE("imex_multicolor_block_reason — IDEX (1 tool per gantry) blocks multi-color", "[IMEX]") {
@@ -156,7 +133,7 @@ TEST_CASE("imex_multicolor_block_reason — IDEX (1 tool per gantry) blocks mult
// on gantry 0, copy=T1 on gantry 1. Primary's gantry has only 1 active tool,
// so there's no within-gantry toolchange path. Block.
auto pem = make_pem({0, 1});
const std::string reason = imex_multicolor_block_reason("copy", "copy", "0:P,1:C", 1, {0, 1}, pem);
const std::string reason = imex_multicolor_block_reason("copy", "0:P,1:C", 1, {0, 1}, pem);
REQUIRE_FALSE(reason.empty());
REQUIRE_THAT(reason, Catch::Matchers::ContainsSubstring("primary tool's gantry"));
}
@@ -166,7 +143,7 @@ TEST_CASE("imex_multicolor_block_reason — IQEX 2-tool-active mode blocks multi
// gantries). Primary's gantry (gantry 0) has only T0 active → no within-gantry
// swap target. Block.
auto pem = make_pem({0, 1, 2, 3});
const std::string reason = imex_multicolor_block_reason("copy", "copy", "0:P,2:C", 2, {0, 2}, pem);
const std::string reason = imex_multicolor_block_reason("copy", "0:P,2:C", 2, {0, 2}, pem);
REQUIRE_FALSE(reason.empty());
REQUIRE_THAT(reason, Catch::Matchers::ContainsSubstring("primary tool's gantry"));
}
@@ -176,24 +153,7 @@ TEST_CASE("imex_multicolor_block_reason — IQEX 4-tool-active multi-color allow
// T2/T3 on gantry 1 mirroring. Primary's gantry has 2 tools active → within-
// gantry toolchange topology is present. Used filaments {0, 1} both on gantry 0.
auto pem = make_pem({0, 1, 2, 3});
REQUIRE(imex_multicolor_block_reason("copy", "copy", "0:P,1:C,2:M,3:M", 2, {0, 1}, pem).empty());
}
TEST_CASE("imex_multicolor_block_reason — Split mode allows multi-color regardless of gantry pair", "[IMEX]") {
// Split modes are explicitly designed for per-gantry multi-color, so the
// gantry-pair check is bypassed. T0+T2 with one tool per gantry would normally
// be blocked under copy/mirror semantics, but Split allows it.
auto pem = make_pem({0, 1, 2, 3});
REQUIRE(imex_multicolor_block_reason("split-copy", "split", "0:P,2:C", 2, {0, 2}, pem).empty());
}
TEST_CASE("imex_multicolor_block_reason — Split mode still blocks MMU sharing", "[IMEX]") {
// Even Split can't replicate an MMU lane swap on the slaved gantry — those
// physical heads aren't independently swappable.
auto pem = make_pem({0, 0, 0, 0, 1, 2, 3});
const std::string reason = imex_multicolor_block_reason("split-copy", "split", "0:P,2:C", 2, {0, 1}, pem);
REQUIRE_FALSE(reason.empty());
REQUIRE_THAT(reason, Catch::Matchers::ContainsSubstring("physical extruder map"));
REQUIRE(imex_multicolor_block_reason("copy", "0:P,1:C,2:M,3:M", 2, {0, 1}, pem).empty());
}
TEST_CASE("imex_multicolor_block_reason — MMU lane sharing blocks multi-color", "[IMEX]") {
@@ -201,14 +161,14 @@ TEST_CASE("imex_multicolor_block_reason — MMU lane sharing blocks multi-color"
// an MMU/AFC manifold. IMEX parallel modes can't slave the secondary gantry
// through an MMU lane swap, so block.
auto pem = make_pem({0, 0, 0, 0, 1, 2, 3});
const std::string reason = imex_multicolor_block_reason("copy", "copy", "0:P,1:C,2:M,3:M", 2, {0, 1}, pem);
const std::string reason = imex_multicolor_block_reason("copy", "0:P,1:C,2:M,3:M", 2, {0, 1}, pem);
REQUIRE_FALSE(reason.empty());
REQUIRE_THAT(reason, Catch::Matchers::ContainsSubstring("physical extruder map"));
}
TEST_CASE("imex_multicolor_block_reason — mode without primary blocks", "[IMEX]") {
auto pem = make_pem({0, 1, 2, 3});
const std::string reason = imex_multicolor_block_reason("copy", "copy", "1:C,2:M", 2, {0, 1}, pem);
const std::string reason = imex_multicolor_block_reason("copy", "1:C,2:M", 2, {0, 1}, pem);
REQUIRE_FALSE(reason.empty());
REQUIRE_THAT(reason, Catch::Matchers::ContainsSubstring("primary tool"));
}