Migrate the IDEX/IQEX config keys testers already have

The parallel printing options shipped to testers as is_ixex and ixex_*,
became is_imex and imex_* when the feature stopped being called iXex, and
the two clearance keys were renamed once more to say what they measure:
nozzle to carriage edge on the collision side, not the carriage's full
width. Nothing translated any of it, so loading an existing printer profile
dropped every one of these values - the keys are unknown and cleared.
is_ixex is the one that matters most, because without it the others migrate
into a feature that stays switched off, leaving settings that look
configured and do nothing.

Per-plate mode is persisted twice and only one path went through
handle_legacy. Plate metadata in a 3MF is matched by exact attribute name
and written with set_key_value, so a project saved between the per-plate
mode landing and the rename loaded every plate back on Primary and sliced
single-carriage with no warning. The loader now accepts the old attribute
name.

ixex_primary_col and ixex_primary_row are dropped rather than mapped: the
primary is a role inside the mode's active-tools string now, not a grid
coordinate, and they were never in an option list, so no saved file carries
them.

The test drives the full era-1 key list and asserts the enum values rather
than non-nullness, since a forward-compatible substitution would otherwise
hide a failed deserialize behind a default. handle_legacy had no test before
this.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Clifford Garwood
2026-09-18 01:32:27 -04:00
co-authored by Claude Opus 5
parent f7a08b0868
commit b6f690a65c
3 changed files with 89 additions and 1 deletions
+5 -1
View File
@@ -311,6 +311,10 @@ static constexpr const char* OTHER_LAYERS_PRINT_SEQUENCE_ATTR = "other_layers_pr
static constexpr const char* OTHER_LAYERS_PRINT_SEQUENCE_NUMS_ATTR = "other_layers_print_sequence_nums";
static constexpr const char* SPIRAL_VASE_MODE = "spiral_mode";
static constexpr const char* IMEX_PARALLEL_MODE_ATTR = "imex_parallel_mode";
// The same attribute before the feature was renamed IMEX. Plate metadata is matched by exact
// string and written with set_key_value, so it never passes through handle_legacy: without this
// a project saved in that window loads every plate back on the Primary mode, silently.
static constexpr const char* IXEX_PARALLEL_MODE_ATTR_LEGACY = "ixex_parallel_mode";
static constexpr const char* IMEX_HEAD_FILAMENT_MAP_ATTR = "imex_head_filament_map";
static constexpr const char* FILAMENT_MAP_MODE_ATTR = "filament_map_mode";
static constexpr const char* FILAMENT_MAP_ATTR = "filament_maps";
@@ -4484,7 +4488,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
std::istringstream(value) >> std::boolalpha >> spiral_mode;
m_curr_plater->config.set_key_value("spiral_mode", new ConfigOptionBool(spiral_mode));
}
else if (key == IMEX_PARALLEL_MODE_ATTR) {
else if (key == IMEX_PARALLEL_MODE_ATTR || key == IXEX_PARALLEL_MODE_ATTR_LEGACY) {
m_curr_plater->config.set_key_value("imex_parallel_mode", new ConfigOptionString(value));
}
else if (key == IMEX_HEAD_FILAMENT_MAP_ATTR) {
+18
View File
@@ -9331,6 +9331,24 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va
else if (opt_key == "wall_direction" && value == "auto") {
value = "ccw";
}
// Orca: the IDEX/IQEX parallel printing keys shipped to testers as ixex_* before the feature
// was renamed IMEX, and the two clearance keys were renamed again to say what they measure:
// nozzle to carriage edge on the collision side, not the carriage's full width. Without this
// an existing printer profile loses every one of these values silently.
// is_ixex is the master gate: without it every other key below migrates into a feature that
// stays switched off, which is worse than losing them all, because the settings then look
// configured. ixex_primary_col/_row are the only era-1 keys with no modern counterpart (the
// primary is a role in the mode's tools string now); they were never in an option list, so no
// saved file carries them, and the has() check at the end of this function drops them anyway.
else if (opt_key == "is_ixex") {
opt_key = "is_imex";
} else if (opt_key.compare(0, 5, "ixex_") == 0) {
opt_key = "imex_" + opt_key.substr(5);
if (opt_key == "imex_carriage_width_x")
opt_key = "imex_nozzle_clearance_x";
else if (opt_key == "imex_carriage_width_y")
opt_key = "imex_nozzle_clearance_y";
}
// Ignore the following obsolete configuration keys:
static std::set<std::string> ignore = {
+66
View File
@@ -1298,3 +1298,69 @@ TEST_CASE("min_object_distance yields no floor when an FFF config lacks the opti
CHECK_THAT(min_object_distance(c), Catch::Matchers::WithinAbs(12., 1e-9));
}
}
TEST_CASE("handle_legacy migrates every IDEX/IQEX key testers already have", "[Config]") {
// Three eras of saved profiles: the feature shipped as iXex (is_ixex + ixex_*), the clearance
// pair was renamed to say what it measures, then the whole prefix became imex_. Driven from the
// full era-1 key list, because the gap this test was rewritten to catch was a key the earlier
// hand-picked version simply did not mention.
ConfigSubstitutionContext ctxt(ForwardCompatibilitySubstitutionRule::Enable);
DynamicPrintConfig era1;
era1.set_deserialize("is_ixex", "1", ctxt);
era1.set_deserialize("ixex_gantry_count", "2", ctxt);
era1.set_deserialize("ixex_tools_per_gantry", "2", ctxt);
era1.set_deserialize("ixex_carriage_width_x", "12.5", ctxt);
era1.set_deserialize("ixex_carriage_width_y", "7.25", ctxt);
era1.set_deserialize("ixex_carriage_margin", "1.5", ctxt);
era1.set_deserialize("ixex_tool_layout", "front-right", ctxt);
era1.set_deserialize("ixex_viz_theme", "deuteranopia", ctxt);
era1.set_deserialize("ixex_parallel_mode", "copy", ctxt);
era1.set_deserialize("ixex_mode_names", "\"Duplicate\";\"Mirror\"", ctxt);
era1.set_deserialize("ixex_mode_active_tools", "\"0:P,1:C\";\"0:P,1:M\"", ctxt);
era1.set_deserialize("ixex_mode_gcodes", "\"M118 copy\";\"M118 mirror\"", ctxt);
// The gate first: every other value is inert without it, and a populated but disabled config is
// worse than an empty one because it looks configured.
REQUIRE(era1.option("is_imex") != nullptr);
CHECK(era1.opt_bool("is_imex"));
CHECK(era1.opt_int("imex_gantry_count") == 2);
CHECK(era1.opt_int("imex_tools_per_gantry") == 2);
CHECK(era1.opt_float("imex_nozzle_clearance_x") == Catch::Approx(12.5));
CHECK(era1.opt_float("imex_nozzle_clearance_y") == Catch::Approx(7.25));
CHECK(era1.opt_float("imex_carriage_margin") == Catch::Approx(1.5));
CHECK(era1.opt_string("imex_parallel_mode") == "copy");
// Both enums changed type from coString, and a forward-compatible substitution would hand back
// a default rather than fail, so assert the value and not merely that the option exists.
CHECK(era1.opt_enum<ImexToolLayout>("imex_tool_layout") == ImexToolLayout::FrontRight);
CHECK(era1.opt_enum<ImexVizTheme>("imex_viz_theme") == ImexVizTheme::Deuteranopia);
// The three lists carry escaped, semicolon-separated values - the likeliest place for a silent
// change - so check both elements survive in order.
CHECK(era1.opt_string("imex_mode_names", 0u) == "Duplicate");
CHECK(era1.opt_string("imex_mode_names", 1u) == "Mirror");
CHECK(era1.opt_string("imex_mode_active_tools", 0u) == "0:P,1:C");
CHECK(era1.opt_string("imex_mode_gcodes", 1u) == "M118 mirror");
// Era 2: renamed clearance keys, still under the old prefix.
DynamicPrintConfig era2;
era2.set_deserialize("ixex_nozzle_clearance_x", "3.5", ctxt);
CHECK(era2.opt_float("imex_nozzle_clearance_x") == Catch::Approx(3.5));
// Era 3 (current) is untouched by the branch.
DynamicPrintConfig era3;
era3.set_deserialize("imex_nozzle_clearance_x", "9.0", ctxt);
CHECK(era3.opt_float("imex_nozzle_clearance_x") == Catch::Approx(9.0));
// The two keys with no modern counterpart are dropped and reported, not mapped onto a key that
// does not exist.
ConfigSubstitutionContext obsolete_ctxt(ForwardCompatibilitySubstitutionRule::Enable);
DynamicPrintConfig obsolete;
obsolete.set_deserialize("ixex_primary_col", "1", obsolete_ctxt);
obsolete.set_deserialize("ixex_primary_row", "0", obsolete_ctxt);
CHECK(obsolete.option("imex_primary_col") == nullptr);
CHECK(obsolete.option("imex_primary_row") == nullptr);
CHECK(obsolete_ctxt.unrecogized_keys.size() == 2);
}