From 49c811ceb6f4215ab4b5faad25a4b6931b1afcfc Mon Sep 17 00:00:00 2001 From: Kris Austin Date: Fri, 18 Sep 2026 18:54:43 -0500 Subject: [PATCH] fix: H2D extruder sync does nothing on the Motion ability page (#14931) --- src/slic3r/GUI/Tab.cpp | 21 ++++++--- .../test_config_variant_expansion.cpp | 44 +++++++++++++++++++ 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 768d2023cc..a7e2d3ad3e 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -8386,17 +8386,19 @@ void Tab::sync_excluder() Preset & printer_preset = m_preset_bundle->printers.get_edited_preset(); auto nozzle_volumes = m_preset_bundle->project_config.option("nozzle_volume_type"); auto extruders = printer_preset.config.option("extruder_type"); + // Motion ability options hold a (normal, silent) pair per variant, so switch_excluder indexes that page with stride 2. + const int stride = m_active_page->title().StartsWith("Motion ability") ? 2 : 1; auto get_index_for_extruder = - [this, &extruders, variant_keys = extruder_variant_keys[m_type >= Preset::TYPE_COUNT ? Preset::TYPE_PRINT : m_type]](int extruder_id, NozzleVolumeType nozzle_type) { + [this, &extruders, stride, variant_keys = extruder_variant_keys[m_type >= Preset::TYPE_COUNT ? Preset::TYPE_PRINT : m_type]](int extruder_id, NozzleVolumeType nozzle_type) { return m_config->get_index_for_extruder(extruder_id + 1, variant_keys.first, - ExtruderType(extruders->values[extruder_id]), nozzle_type, variant_keys.second); + ExtruderType(extruders->values[extruder_id]), nozzle_type, variant_keys.second, stride); }; int active_index = get_current_active_extruder(); auto active_nozzle = get_actual_nozzle_volume_type(active_index); int from_index = get_index_for_extruder(active_index, active_nozzle); int dest_index = get_index_for_extruder(1 - active_index, active_nozzle); - auto from_str = std::to_string(from_index); - auto dest_str = std::to_string(dest_index); + if (from_index < 0 || dest_index < 0) // no variant column for this nozzle on one of the extruders + return; auto dirty_options = m_presets->current_dirty_options(true); DynamicConfig config_origin, config_to_apply; for (int i = 0; i < dirty_options.size(); ++i) { @@ -8409,16 +8411,21 @@ void Tab::sync_excluder() if (field == nullptr || line == nullptr) continue; ++n; - bool dirty = opt.substr(n) == from_str; + auto is_from_slot = [&](const std::string &dirty_opt) { + int slot = std::atoi(dirty_opt.c_str() + n); + return slot >= from_index && slot < from_index + stride; + }; + bool dirty = is_from_slot(opt); while (i + 1 < dirty_options.size() && dirty_options[i + 1].compare(0, n, opt, 0, n) == 0) { - dirty |= dirty_options[i + 1].substr(n) == from_str; + dirty |= is_from_slot(dirty_options[i + 1]); ++i; } if (dirty) { auto key = opt.substr(0, n - 1); auto option = dynamic_cast(m_config->option(key)); auto option2 = dynamic_cast(option->clone()); - option2->set_at(option, dest_index, from_index); + for (int s = 0; s < stride; ++s) + option2->set_at(option, dest_index + s, from_index + s); if (*option == *option2) { delete option2; continue; diff --git a/tests/libslic3r/test_config_variant_expansion.cpp b/tests/libslic3r/test_config_variant_expansion.cpp index d8e09539bb..274946cdca 100644 --- a/tests/libslic3r/test_config_variant_expansion.cpp +++ b/tests/libslic3r/test_config_variant_expansion.cpp @@ -594,3 +594,47 @@ TEST_CASE("update_values_from_multi_to_multi_2 sizes the destination row to the CHECK(object_config.update_values_from_multi_to_multi_2(src_variants, {}, dst, keys) == -1); } } + +TEST_CASE("get_index_for_extruder returns -1 when no variant column matches the extruder", "[Config]") +{ + DynamicPrintConfig config; + config.option("print_extruder_id", true)->values = {1}; + config.option("print_extruder_variant", true)->values = {"Direct Drive Standard"}; + + SECTION("the extruder that owns the column resolves to its slot") { + REQUIRE(config.get_index_for_extruder(1, "print_extruder_id", etDirectDrive, nvtStandard, "print_extruder_variant") == 0); + } + + SECTION("an extruder with no matching column resolves to -1") { + REQUIRE(config.get_index_for_extruder(2, "print_extruder_id", etDirectDrive, nvtStandard, "print_extruder_variant") == -1); + } +} + +// stride scales the returned slot so callers can address stride-2 options (machine_max_*, a +// Normal/Silent pair per column) by their pair's base slot. The printer Tab's extruder sync +// relies on this to copy the right slots on the Motion ability page. +TEST_CASE("get_index_for_extruder scales the variant column by the requested stride", "[Config]") +{ + DynamicPrintConfig config; + config.option("printer_extruder_id", true)->values = {1, 2}; + config.option("printer_extruder_variant", true)->values = {"Direct Drive Standard", + "Direct Drive High Flow"}; + + // extruder 1 resolves to column 0, extruder 2 to column 1 + const int col0_stride1 = config.get_index_for_extruder(1, "printer_extruder_id", etDirectDrive, nvtStandard, + "printer_extruder_variant", 1); + const int col1_stride1 = config.get_index_for_extruder(2, "printer_extruder_id", etDirectDrive, nvtHighFlow, + "printer_extruder_variant", 1); + REQUIRE(col0_stride1 == 0); + REQUIRE(col1_stride1 == 1); + + // stride 2 returns exactly twice the stride-1 index (the pair's base slot) + const int col0_stride2 = config.get_index_for_extruder(1, "printer_extruder_id", etDirectDrive, nvtStandard, + "printer_extruder_variant", 2); + const int col1_stride2 = config.get_index_for_extruder(2, "printer_extruder_id", etDirectDrive, nvtHighFlow, + "printer_extruder_variant", 2); + REQUIRE(col0_stride2 == col0_stride1 * 2); + REQUIRE(col1_stride2 == col1_stride1 * 2); + REQUIRE(col0_stride2 == 0); + REQUIRE(col1_stride2 == 2); +}