diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp index dce8673fa4..a7fe9a1640 100644 --- a/src/libslic3r/Format/bbs_3mf.cpp +++ b/src/libslic3r/Format/bbs_3mf.cpp @@ -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) { diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index eec423f3f9..9623426992 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -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 ignore = { diff --git a/tests/libslic3r/test_config.cpp b/tests/libslic3r/test_config.cpp index 720147a7fe..51ab05bb40 100644 --- a/tests/libslic3r/test_config.cpp +++ b/tests/libslic3r/test_config.cpp @@ -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("imex_tool_layout") == ImexToolLayout::FrontRight); + CHECK(era1.opt_enum("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); +}