#include #include "libslic3r/Config.hpp" #include #include #include #include #include #include #include #include #include #include "libslic3r/PrintConfig.hpp" #include "libslic3r/Preset.hpp" using namespace Slic3r; namespace { // A 2-extruder printer whose second extruder holds both a Standard and a High Flow nozzle // (nozzle_volume_type Hybrid), described by extruder_nozzle_stats. The variant lists carry one // column per (extruder x volume type) as composed from the presets. DynamicPrintConfig make_hybrid_printer_config() { DynamicPrintConfig config; config.option("extruder_nozzle_stats", true)->values = {"Standard#1", "Standard#3|High Flow#2"}; config.option("extruder_type", true)->values = {etDirectDrive, etDirectDrive}; config.option("nozzle_volume_type", true)->values = {nvtStandard, nvtHybrid}; config.option("extruder_variant_list", true)->values = {"Direct Drive Standard,Direct Drive High Flow", "Direct Drive Standard,Direct Drive High Flow"}; return config; } void add_print_variant_columns(DynamicPrintConfig &config) { config.option("print_extruder_id", true)->values = {1, 1, 2, 2}; config.option("print_extruder_variant", true)->values = {"Direct Drive Standard", "Direct Drive High Flow", "Direct Drive Standard", "Direct Drive High Flow"}; config.option("outer_wall_speed", true)->values = {30., 200., 50., 500.}; } } // namespace TEST_CASE("Filament cooling and pressure settings follow the selected nozzle variant", "[Config][FilamentVariants]") { struct Setting { const char *key; const char *standard; const char *high_flow; }; const Setting settings[] = { {"enable_pressure_advance", "0", "1"}, {"pressure_advance", "0.04", "0.02"}, {"fan_min_speed", "15", "10"}, {"fan_max_speed", "15", "60"}, {"additional_cooling_fan_speed", "70", "0"}, {"filament_minimal_purge_on_wipe_tower", "0", "15"}, {"filament_multitool_ramming", "0", "1"}, {"filament_multitool_ramming_volume", "0", "5"}, {"filament_multitool_ramming_flow", "10", "30"}, {"nozzle_temperature_range_low", "190", "210"}, {"nozzle_temperature_range_high", "255", "280"}, }; const int variant_index = GENERATE(0, 1, 2, 3); const bool load_preset = GENERATE(false, true); const bool high_flow = variant_index % 2 != 0; for (const Setting &setting : settings) { DYNAMIC_SECTION(setting.key << " variant=" << variant_index << " loaded=" << load_preset) { DynamicPrintConfig printer = make_hybrid_printer_config(); if (variant_index >= 2) printer.option("extruder_type")->values[1] = etBowden; DynamicPrintConfig filament; filament.option("filament_extruder_variant", true)->values = {"Direct Drive Standard", "Direct Drive High Flow", "Bowden Standard", "Bowden High Flow"}; // one value per variant of filament_extruder_variant const std::string values = std::string(setting.standard) + "," + setting.high_flow + "," + setting.standard + "," + setting.high_flow; REQUIRE(filament.option(setting.key, true)->deserialize(values)); if (load_preset) { extend_default_config_length(filament, false, DynamicPrintConfig::full_print_config()); DynamicPrintConfig expanded; REQUIRE(expanded.option(setting.key, true)->deserialize(values)); REQUIRE(*filament.option(setting.key) == *expanded.option(setting.key)); } std::vector> nozzle_types; const int count = printer.get_extruder_nozzle_volume_count(2, nozzle_types); filament.update_values_to_printer_extruders(printer, 2, count, nozzle_types, filament_options_with_variant, "", "filament_extruder_variant", 1, 2, high_flow ? nvtHighFlow : nvtStandard); DynamicPrintConfig expected; REQUIRE(expected.option(setting.key, true)->deserialize(high_flow ? setting.high_flow : setting.standard)); REQUIRE(*filament.option(setting.key) == *expected.option(setting.key)); } } } TEST_CASE("apply_override fills nil entries from the 0-based default index", "[Config]") { ConfigOptionFloats machine({10., 20., 30.}); ConfigOptionFloatsNullable filament; filament.values = {ConfigOptionFloatsNullable::nil_value(), 42.}; SECTION("a nil entry picks the slot addressed by its 0-based index") { std::vector slot_index{2, 0}; ConfigOptionFloats resolved(machine); REQUIRE(resolved.apply_override(&filament, slot_index)); REQUIRE(resolved.values == std::vector({30., 42.})); } SECTION("an index past the machine slots keeps the slot's own value") { std::vector slot_index{5, 0}; ConfigOptionFloats resolved(machine); REQUIRE(resolved.apply_override(&filament, slot_index)); REQUIRE(resolved.values == std::vector({10., 42.})); } SECTION("a negative index (unresolved slot) keeps the slot's own value") { ConfigOptionFloatsNullable all_nil; all_nil.values = {ConfigOptionFloatsNullable::nil_value(), ConfigOptionFloatsNullable::nil_value(), ConfigOptionFloatsNullable::nil_value()}; std::vector slot_index{2, -1, 0}; ConfigOptionFloats resolved(machine); REQUIRE(!resolved.apply_override(&all_nil, slot_index)); REQUIRE(resolved.values == std::vector({30., 20., 10.})); } SECTION("all-nil overrides keyed by unresolved slots leave the machine values intact") { // The failed-lookup map a degenerate print_extruder_id used to produce; the negative // slots must not collapse the machine array to its first value. ConfigOptionFloats per_extruder({100., 70., 70., 70., 100.}); ConfigOptionFloatsNullable all_nil; all_nil.values.assign(5, ConfigOptionFloatsNullable::nil_value()); std::vector slot_index{0, -1, -1, -1, 0}; ConfigOptionFloats resolved(per_extruder); REQUIRE(!resolved.apply_override(&all_nil, slot_index)); REQUIRE(resolved.values == std::vector({100., 70., 70., 70., 100.})); } } TEST_CASE("support_different_extruders is true only when the printer defines more than one variant column", "[Config]") { int extruder_count = 0; SECTION("a non-Bambu dual-nozzle printer with one variant column reports false") { DynamicPrintConfig config; config.option("nozzle_diameter", true)->values = {0.4, 0.4}; // Both extruders resolve to the same default variant, so there is only one column. config.option("extruder_variant_list", true)->values = {"Direct Drive Standard", "Direct Drive Standard"}; REQUIRE(config.support_different_extruders(extruder_count) == false); REQUIRE(extruder_count == 2); } SECTION("a Bambu H2D-style printer with distinct variants reports true") { DynamicPrintConfig config; config.option("nozzle_diameter", true)->values = {0.4, 0.4}; config.option("extruder_variant_list", true)->values = { "Direct Drive Standard,Direct Drive High Flow", "Direct Drive Standard,Direct Drive High Flow,Direct Drive TPU High Flow"}; REQUIRE(config.support_different_extruders(extruder_count) == true); REQUIRE(extruder_count == 2); } SECTION("a many-toolhead printer that never opts into variants reports false") { // A Snapmaker U1 has four identical toolheads and never defines extruder_variant_list, // so the config falls back to a single default variant token. DynamicPrintConfig config; config.option("nozzle_diameter", true)->values = {0.4, 0.4, 0.4, 0.4}; REQUIRE(config.support_different_extruders(extruder_count) == false); REQUIRE(extruder_count == 4); } } TEST_CASE("get_config_index_base resolves (volume type, extruder type, id) to a slot", "[Config]") { const std::vector variant_list = {"Direct Drive Standard", "Direct Drive High Flow", "Direct Drive Standard", "Direct Drive High Flow"}; const std::vector variant_ids = {1, 1, 2, 2}; SECTION("a matching (variant, id) pair yields its slot") { REQUIRE(get_config_index_base(nvtStandard, etDirectDrive, 1, variant_list, variant_ids) == 0); REQUIRE(get_config_index_base(nvtHighFlow, etDirectDrive, 1, variant_list, variant_ids) == 1); REQUIRE(get_config_index_base(nvtStandard, etDirectDrive, 2, variant_list, variant_ids) == 2); REQUIRE(get_config_index_base(nvtHighFlow, etDirectDrive, 2, variant_list, variant_ids) == 3); } SECTION("no matching variant falls back to the id's first variant") { REQUIRE(get_config_index_base(nvtStandard, etBowden, 1, variant_list, variant_ids) == 0); REQUIRE(get_config_index_base(nvtStandard, etBowden, 2, variant_list, variant_ids) == 2); } SECTION("an id without any variant falls back to variant index 0") { REQUIRE(get_config_index_base(nvtStandard, etDirectDrive, 3, variant_list, variant_ids) == 0); } SECTION("Hybrid is not a preset variant string and falls back to the id's first variant") { REQUIRE(get_config_index_base(nvtHybrid, etDirectDrive, 2, variant_list, variant_ids) == 2); } SECTION("a filament without a High Flow variant keeps its own variant, not the first filament's") { // filament 1 defines Standard and High Flow, filament 2 only Standard const std::vector mixed_list = {"Direct Drive Standard", "Direct Drive High Flow", "Direct Drive Standard"}; const std::vector mixed_ids = {1, 1, 2}; REQUIRE(get_config_index_base(nvtHighFlow, etDirectDrive, 1, mixed_list, mixed_ids) == 1); REQUIRE(get_config_index_base(nvtHighFlow, etDirectDrive, 2, mixed_list, mixed_ids) == 2); } } TEST_CASE("support interface pattern registry includes spiral inset", "[Config]") { const auto &values = ConfigOptionEnum::get_enum_values(); REQUIRE(values.at("spiralinset") == SupportMaterialInterfacePattern::smipSpiralInset); } TEST_CASE("get_extruder_nozzle_volume_count reads the per-extruder volume-type layout", "[Config]") { std::vector> nozzle_volume_types; SECTION("absent stats fall back to one slot per extruder") { DynamicPrintConfig config; REQUIRE(config.get_extruder_nozzle_volume_count(2, nozzle_volume_types) == 2); REQUIRE(nozzle_volume_types.size() == 2); REQUIRE(nozzle_volume_types[0].empty()); REQUIRE(nozzle_volume_types[1].empty()); } SECTION("stats sized differently from the extruder count are ignored") { DynamicPrintConfig config; config.option("extruder_nozzle_stats", true)->values = {"Standard#1"}; REQUIRE(config.get_extruder_nozzle_volume_count(2, nozzle_volume_types) == 2); REQUIRE(nozzle_volume_types[0].empty()); REQUIRE(nozzle_volume_types[1].empty()); } SECTION("single volume type per extruder counts one slot each") { DynamicPrintConfig config; config.option("extruder_nozzle_stats", true)->values = {"Standard#1", "High Flow#1"}; REQUIRE(config.get_extruder_nozzle_volume_count(2, nozzle_volume_types) == 2); REQUIRE(nozzle_volume_types[0] == std::vector{nvtStandard}); REQUIRE(nozzle_volume_types[1] == std::vector{nvtHighFlow}); } SECTION("a mixed-nozzle extruder contributes one slot per volume type, ascending enum order") { DynamicPrintConfig config; // list High Flow first in the token string: parsing must still order Standard before High Flow config.option("extruder_nozzle_stats", true)->values = {"Standard#3", "High Flow#3|Standard#3"}; REQUIRE(config.get_extruder_nozzle_volume_count(2, nozzle_volume_types) == 3); REQUIRE(nozzle_volume_types[0] == std::vector{nvtStandard}); REQUIRE(nozzle_volume_types[1] == std::vector({nvtStandard, nvtHighFlow})); } } TEST_CASE("update_values_to_printer_extruders expands one slot per (extruder x volume type)", "[Config]") { SECTION("Hybrid extruder yields three slots, extruder-ascending then volume-ascending") { DynamicPrintConfig config = make_hybrid_printer_config(); add_print_variant_columns(config); std::vector> nozzle_volume_types; int extruder_count = 2; int count = config.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types); REQUIRE(count == 3); std::vector variant_index = config.update_values_to_printer_extruders(config, extruder_count, count, nozzle_volume_types, print_options_with_variant, "print_extruder_id", "print_extruder_variant"); REQUIRE(variant_index == std::vector({0, 2, 3})); REQUIRE(config.option("outer_wall_speed")->values == std::vector({30., 50., 500.})); REQUIRE(config.option("print_extruder_id")->values == std::vector({1, 2, 2})); REQUIRE(config.option("print_extruder_variant")->values == std::vector({"Direct Drive Standard", "Direct Drive Standard", "Direct Drive High Flow"})); } SECTION("stride-2 options keep (normal, silent) pairs together per slot") { DynamicPrintConfig config = make_hybrid_printer_config(); config.option("printer_extruder_id", true)->values = {1, 1, 2, 2}; config.option("printer_extruder_variant", true)->values = {"Direct Drive Standard", "Direct Drive High Flow", "Direct Drive Standard", "Direct Drive High Flow"}; config.option("machine_max_speed_x", true)->values = {100., 50., 110., 55., 120., 60., 130., 65.}; std::vector> nozzle_volume_types; int extruder_count = 2; int count = config.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types); std::vector variant_index = config.update_values_to_printer_extruders(config, extruder_count, count, nozzle_volume_types, printer_options_with_variant_2, "printer_extruder_id", "printer_extruder_variant", 2); REQUIRE(variant_index == std::vector({0, 2, 3})); REQUIRE(config.option("machine_max_speed_x")->values == std::vector({100., 50., 120., 60., 130., 65.})); } SECTION("single-slot expansion on a Hybrid extruder resolves via the filament volume type") { DynamicPrintConfig printer_config = make_hybrid_printer_config(); DynamicPrintConfig filament_config; filament_config.option("filament_extruder_variant", true)->values = {"Direct Drive Standard", "Direct Drive High Flow"}; filament_config.option("filament_max_volumetric_speed", true)->values = {12., 20.}; std::vector> nozzle_volume_types; int extruder_count = 2; int count = printer_config.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types); SECTION("default filament volume type selects the Standard column") { std::vector variant_index = filament_config.update_values_to_printer_extruders(printer_config, extruder_count, count, nozzle_volume_types, filament_options_with_variant, "", "filament_extruder_variant", 1, 2); REQUIRE(variant_index == std::vector({0})); REQUIRE(filament_config.option("filament_max_volumetric_speed")->values == std::vector({12.})); } SECTION("a High Flow filament volume type selects the High Flow column") { std::vector variant_index = filament_config.update_values_to_printer_extruders(printer_config, extruder_count, count, nozzle_volume_types, filament_options_with_variant, "", "filament_extruder_variant", 1, 2, nvtHighFlow); REQUIRE(variant_index == std::vector({1})); REQUIRE(filament_config.option("filament_max_volumetric_speed")->values == std::vector({20.})); } } SECTION("an extruder without per-type stats does not overrun the slot table when another is Hybrid") { DynamicPrintConfig config; // e0 carries no per-type stats (empty entry), so the summed volume-type count (2) does // not exceed the extruder count even though the Hybrid e1 emits one slot per volume type. config.option("extruder_nozzle_stats", true)->values = {"", "Standard#3|High Flow#3"}; config.option("extruder_type", true)->values = {etDirectDrive, etDirectDrive}; config.option("nozzle_volume_type", true)->values = {nvtStandard, nvtHybrid}; config.option("extruder_variant_list", true)->values = {"Direct Drive Standard,Direct Drive High Flow", "Direct Drive Standard,Direct Drive High Flow"}; add_print_variant_columns(config); std::vector> nozzle_volume_types; int extruder_count = 2; int count = config.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types); REQUIRE(count == 2); REQUIRE(nozzle_volume_types[0].empty()); std::vector variant_index = config.update_values_to_printer_extruders(config, extruder_count, count, nozzle_volume_types, print_options_with_variant, "print_extruder_id", "print_extruder_variant"); // e0 resolves by its configured type; the Hybrid e1 emits one slot per stats volume type REQUIRE(variant_index == std::vector({0, 2, 3})); REQUIRE(config.option("outer_wall_speed")->values == std::vector({30., 50., 500.})); } SECTION("without Hybrid or extra slots the expansion matches the per-extruder resolution") { DynamicPrintConfig config; config.option("extruder_type", true)->values = {etDirectDrive, etDirectDrive}; config.option("nozzle_volume_type", true)->values = {nvtStandard, nvtHighFlow}; config.option("extruder_variant_list", true)->values = {"Direct Drive Standard,Direct Drive High Flow", "Direct Drive Standard,Direct Drive High Flow"}; add_print_variant_columns(config); // compute what the per-extruder loop resolves directly, before the arrays are rewritten std::vector expected_index; for (int e_index = 0; e_index < 2; e_index++) expected_index.push_back(config.get_index_for_extruder(e_index + 1, "print_extruder_id", etDirectDrive, e_index == 0 ? nvtStandard : nvtHighFlow, "print_extruder_variant")); REQUIRE(expected_index == std::vector({0, 3})); std::vector> nozzle_volume_types; int extruder_count = 2; int count = config.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types); REQUIRE(count == 2); std::vector variant_index = config.update_values_to_printer_extruders(config, extruder_count, count, nozzle_volume_types, print_options_with_variant, "print_extruder_id", "print_extruder_variant"); REQUIRE(variant_index == expected_index); REQUIRE(config.option("outer_wall_speed")->values == std::vector({30., 500.})); } } TEST_CASE("update_values_to_printer_extruders synthesizes degenerate process variant columns", "[Config]") { // Non-BBL process presets and 3mf project configs keep the length-1 defaults for // print_extruder_id/print_extruder_variant; only BBL system presets ship full-width columns. auto add_degenerate_print_columns = [](DynamicPrintConfig &config) { config.option("print_extruder_id", true)->values = {1}; config.option("print_extruder_variant", true)->values = {"Direct Drive Standard"}; config.option("outer_wall_speed", true)->values = {30.}; }; SECTION("a single-column pair on a multi-extruder machine expands to one column per extruder") { DynamicPrintConfig config; config.option("extruder_type", true)->values = {etDirectDrive, etDirectDrive}; config.option("nozzle_volume_type", true)->values = {nvtStandard, nvtStandard}; config.option("extruder_variant_list", true)->values = {"Direct Drive Standard", "Direct Drive Standard"}; add_degenerate_print_columns(config); std::vector> nozzle_volume_types; int extruder_count = 2; int count = config.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types); std::vector variant_index = config.update_values_to_printer_extruders(config, extruder_count, count, nozzle_volume_types, print_options_with_variant, "print_extruder_id", "print_extruder_variant"); REQUIRE(variant_index == std::vector({0, 1})); REQUIRE(config.option("print_extruder_id")->values == std::vector({1, 2})); REQUIRE(config.option("print_extruder_variant")->values == std::vector({"Direct Drive Standard", "Direct Drive Standard"})); // width-1 data arrays replicate their only column into every slot REQUIRE(config.option("outer_wall_speed")->values == std::vector({30., 30.})); } SECTION("a multi-variant list synthesizes one column per (extruder x variant)") { DynamicPrintConfig config = make_hybrid_printer_config(); add_degenerate_print_columns(config); std::vector> nozzle_volume_types; int extruder_count = 2; int count = config.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types); REQUIRE(count == 3); std::vector variant_index = config.update_values_to_printer_extruders(config, extruder_count, count, nozzle_volume_types, print_options_with_variant, "print_extruder_id", "print_extruder_variant"); // same slot resolution as the explicit BBL-style 4-column layout REQUIRE(variant_index == std::vector({0, 2, 3})); REQUIRE(config.option("print_extruder_id")->values == std::vector({1, 2, 2})); REQUIRE(config.option("print_extruder_variant")->values == std::vector({"Direct Drive Standard", "Direct Drive Standard", "Direct Drive High Flow"})); REQUIRE(config.option("outer_wall_speed")->values == std::vector({30., 30., 30.})); } SECTION("a single-extruder single-column layout is not treated as degenerate") { DynamicPrintConfig config; config.option("extruder_type", true)->values = {etDirectDrive}; config.option("nozzle_volume_type", true)->values = {nvtStandard}; config.option("extruder_variant_list", true)->values = {"Direct Drive Standard"}; add_degenerate_print_columns(config); std::vector> nozzle_volume_types; int extruder_count = 1; int count = config.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types); config.update_values_to_printer_extruders(config, extruder_count, count, nozzle_volume_types, print_options_with_variant, "print_extruder_id", "print_extruder_variant"); REQUIRE(config.option("print_extruder_id")->values == std::vector({1})); REQUIRE(config.option("outer_wall_speed")->values == std::vector({30.})); } SECTION("a second expansion leaves the synthesized layout unchanged") { DynamicPrintConfig config; config.option("extruder_type", true)->values = {etDirectDrive, etDirectDrive}; config.option("nozzle_volume_type", true)->values = {nvtStandard, nvtStandard}; config.option("extruder_variant_list", true)->values = {"Direct Drive Standard", "Direct Drive Standard"}; add_degenerate_print_columns(config); std::vector> nozzle_volume_types; int extruder_count = 2; int count = config.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types); config.update_values_to_printer_extruders(config, extruder_count, count, nozzle_volume_types, print_options_with_variant, "print_extruder_id", "print_extruder_variant"); DynamicPrintConfig once = config; config.update_values_to_printer_extruders(config, extruder_count, count, nozzle_volume_types, print_options_with_variant, "print_extruder_id", "print_extruder_variant"); REQUIRE(config.option("print_extruder_id")->values == once.option("print_extruder_id")->values); REQUIRE(config.option("print_extruder_variant")->values == once.option("print_extruder_variant")->values); REQUIRE(config.option("outer_wall_speed")->values == once.option("outer_wall_speed")->values); } } TEST_CASE("update_values_to_printer_extruders_for_multiple_filaments resolves per-filament slots", "[Config]") { auto make_filament_arrays = [](DynamicPrintConfig &config) { config.option("filament_self_index", true)->values = {1, 1, 2, 2}; config.option("filament_extruder_variant", true)->values = {"Direct Drive Standard", "Direct Drive High Flow", "Direct Drive Standard", "Direct Drive High Flow"}; config.option("filament_max_volumetric_speed", true)->values = {12., 20., 13., 21.}; config.option("fan_max_speed", true)->values = {15., 60., 20., 80.}; config.option("filament_multitool_ramming", true)->values = {false, true, false, true}; }; std::set filament_keys = filament_options_with_variant; filament_keys.insert("filament_self_index"); SECTION("filament_volume_map picks the concrete volume type on a Hybrid extruder") { DynamicPrintConfig config = make_hybrid_printer_config(); make_filament_arrays(config); config.option("filament_map", true)->values = {2, 2}; config.option("filament_volume_map", true)->values = {nvtStandard, nvtHighFlow}; std::vector> nozzle_volume_types; int extruder_count = 2; int count = config.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types); config.update_values_to_printer_extruders_for_multiple_filaments(config, extruder_count, count, filament_keys, "filament_self_index", "filament_extruder_variant"); REQUIRE(config.option("filament_max_volumetric_speed")->values == std::vector({12., 21.})); REQUIRE(config.option("fan_max_speed")->values == std::vector({15., 80.})); REQUIRE(config.option("filament_multitool_ramming")->values == std::vector({0, 1})); REQUIRE(config.option("filament_extruder_variant")->values == std::vector({"Direct Drive Standard", "Direct Drive High Flow"})); REQUIRE(config.option("filament_self_index")->values == std::vector({1, 2})); } SECTION("a volume map not sized to the filament count is ignored") { DynamicPrintConfig config = make_hybrid_printer_config(); make_filament_arrays(config); config.option("filament_map", true)->values = {2, 2}; // the registered default is a single-element vector; it must not override slot resolution config.option("filament_volume_map", true)->values = {nvtStandard}; std::vector> nozzle_volume_types; int extruder_count = 2; int count = config.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types); config.update_values_to_printer_extruders_for_multiple_filaments(config, extruder_count, count, filament_keys, "filament_self_index", "filament_extruder_variant"); // Hybrid resolves as Standard when no usable per-filament map exists REQUIRE(config.option("filament_max_volumetric_speed")->values == std::vector({12., 13.})); REQUIRE(config.option("filament_self_index")->values == std::vector({1, 2})); } SECTION("a single-filament explicit assignment on a Hybrid extruder is honored") { DynamicPrintConfig config = make_hybrid_printer_config(); config.option("filament_self_index", true)->values = {1, 1}; config.option("filament_extruder_variant", true)->values = {"Direct Drive Standard", "Direct Drive High Flow"}; config.option("filament_max_volumetric_speed", true)->values = {12., 20.}; config.option("filament_map", true)->values = {2}; // sized to the (single) filament count: the producers guarantee sizing, so a // single-filament map is as trustworthy as any other and the explicit High Flow // request must win over the Hybrid->Standard fallback config.option("filament_volume_map", true)->values = {nvtHighFlow}; std::vector> nozzle_volume_types; int extruder_count = 2; int count = config.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types); config.update_values_to_printer_extruders_for_multiple_filaments(config, extruder_count, count, filament_keys, "filament_self_index", "filament_extruder_variant"); REQUIRE(config.option("filament_max_volumetric_speed")->values == std::vector({20.})); REQUIRE(config.option("filament_self_index")->values == std::vector({1})); } SECTION("without Hybrid or extra slots the volume map is not consulted") { DynamicPrintConfig config; config.option("extruder_type", true)->values = {etDirectDrive, etDirectDrive}; config.option("nozzle_volume_type", true)->values = {nvtStandard, nvtHighFlow}; config.option("extruder_variant_list", true)->values = {"Direct Drive Standard,Direct Drive High Flow", "Direct Drive Standard,Direct Drive High Flow"}; make_filament_arrays(config); config.option("filament_map", true)->values = {1, 2}; // sized to the filament count, but inert because no extruder exposes multiple volume types config.option("filament_volume_map", true)->values = {nvtHighFlow, nvtStandard}; std::vector> nozzle_volume_types; int extruder_count = 2; int count = config.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types); REQUIRE(count == 2); config.update_values_to_printer_extruders_for_multiple_filaments(config, extruder_count, count, filament_keys, "filament_self_index", "filament_extruder_variant"); // filament 1 keeps its extruder's Standard column, filament 2 its extruder's High Flow column REQUIRE(config.option("filament_max_volumetric_speed")->values == std::vector({12., 21.})); REQUIRE(config.option("filament_self_index")->values == std::vector({1, 2})); } SECTION("a variant option shorter than the filament slots keeps its first value instead of zero") { DynamicPrintConfig config; config.option("extruder_type", true)->values = {etDirectDrive, etDirectDrive}; config.option("nozzle_volume_type", true)->values = {nvtStandard, nvtHighFlow}; config.option("extruder_variant_list", true)->values = {"Direct Drive Standard,Direct Drive High Flow", "Direct Drive Standard,Direct Drive High Flow"}; make_filament_arrays(config); config.option("filament_map", true)->values = {1, 2}; // no loaded preset carries the key, so only its single registered default is present config.option("filament_cooling_before_tower", true)->values = {10.}; // only the first filament's two variant columns were loaded config.option("filament_ramming_volumetric_speed", true)->values = {-1., -2.}; std::vector> nozzle_volume_types; int extruder_count = 2; int count = config.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types); config.update_values_to_printer_extruders_for_multiple_filaments(config, extruder_count, count, filament_keys, "filament_self_index", "filament_extruder_variant"); // filament 2 resolves to column 3 (its extruder's High Flow column), past the end of both vectors REQUIRE_THAT(config.option("filament_cooling_before_tower")->values, Catch::Matchers::Approx(std::vector({10., 10.}))); REQUIRE_THAT(config.option("filament_ramming_volumetric_speed")->values, Catch::Matchers::Approx(std::vector({-1., -1.}))); REQUIRE(config.option("filament_max_volumetric_speed")->values == std::vector({12., 21.})); } // A single extruder whose only variant is Standard or High Flow; filament 1 defines both // variants, filament 2 only Standard. auto make_single_variant_config = [](NozzleVolumeType nozzle_volume_type, const std::string &variant) { DynamicPrintConfig config; config.option("extruder_type", true)->values = {etDirectDrive}; config.option("nozzle_volume_type", true)->values = {nozzle_volume_type}; config.option("extruder_variant_list", true)->values = {variant}; config.option("filament_diameter", true)->values = {1.75, 1.75}; config.option("filament_self_index", true)->values = {1, 1, 2}; config.option("filament_extruder_variant", true)->values = {"Direct Drive Standard", "Direct Drive High Flow", "Direct Drive Standard"}; config.option("filament_max_volumetric_speed", true)->values = {12., 20., 13.}; config.option("filament_map", true)->values = {1, 1}; return config; }; SECTION("a single-variant printer picks each filament's variant of that variant string") { auto [nozzle_volume_type, variant, speeds] = GENERATE(table>({ {nvtStandard, "Direct Drive Standard", {12., 13.}}, // filament 2 defines no High Flow variant and keeps its first {nvtHighFlow, "Direct Drive High Flow", {20., 13.}}, })); DynamicPrintConfig config = make_single_variant_config(nozzle_volume_type, variant); int extruder_count = 1; REQUIRE_FALSE(config.support_different_extruders(extruder_count)); REQUIRE(config.has_multi_variant_filament()); std::vector> nozzle_volume_types; int count = config.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types); config.update_values_to_printer_extruders_for_multiple_filaments(config, extruder_count, count, filament_keys, "filament_self_index", "filament_extruder_variant"); REQUIRE(config.option("filament_max_volumetric_speed")->values == speeds); REQUIRE(config.option("filament_self_index")->values == std::vector({1, 2})); } SECTION("a filament map shorter than the filament count keeps every filament") { DynamicPrintConfig config = make_single_variant_config(nvtStandard, "Direct Drive Standard"); config.option("filament_map", true)->values = {1}; std::vector> nozzle_volume_types; int extruder_count = 1; int count = config.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types); config.update_values_to_printer_extruders_for_multiple_filaments(config, extruder_count, count, filament_keys, "filament_self_index", "filament_extruder_variant"); REQUIRE(config.option("filament_max_volumetric_speed")->values == std::vector({12., 13.})); REQUIRE(config.option("filament_self_index")->values == std::vector({1, 2})); } } TEST_CASE("has_multi_variant_filament is true only when a filament defines more than one variant", "[Config]") { DynamicPrintConfig config; config.option("filament_diameter", true)->values = {1.75, 1.75}; SECTION("one variant per filament reports false") { config.option("filament_extruder_variant", true)->values = {"Direct Drive Standard", "Bowden Standard"}; REQUIRE_FALSE(config.has_multi_variant_filament()); } SECTION("a filament with Standard and High Flow variants reports true") { config.option("filament_extruder_variant", true)->values = {"Direct Drive Standard", "Direct Drive High Flow", "Direct Drive Standard"}; REQUIRE(config.has_multi_variant_filament()); } } // update_values_from_multi_to_multi_2 walks the DESTINATION PRINTER's variant list while writing // into a row taken from the destination PRINT preset, whose arrays are sized to its own // print_extruder_variant. Those two widths disagree until the print preset is re-selected for the // new printer -- Tab::load_current_preset() runs this migration first -- so a project authored on // a single-variant printer, opened and switched to a wider one, wrote past the end of the row. TEST_CASE("update_values_from_multi_to_multi_2 sizes the destination row to the variant count", "[Config][VariantExpansion]") { const std::vector src_variants{"Direct Drive Standard"}; const std::vector dst_variants{"Direct Drive Standard", "Direct Drive High Flow", "Direct Drive Standard", "Direct Drive High Flow"}; const std::set keys{"outer_wall_speed"}; // The per-object override as authored on the single-variant printer. const auto object_override = [] { DynamicPrintConfig c; c.option("outer_wall_speed", true)->values = {42.}; return c; }; SECTION("a row narrower than the variant list is grown, not overrun") { DynamicPrintConfig object_config = object_override(); DynamicPrintConfig dst; dst.option("outer_wall_speed", true)->values = {200.}; REQUIRE(object_config.update_values_from_multi_to_multi_2(src_variants, dst_variants, dst, keys) == 0); const auto& out = object_config.option("outer_wall_speed")->values; REQUIRE(out.size() == dst_variants.size()); // Both "Direct Drive Standard" columns match the source variant, so they take the override. CHECK(out[0] == Catch::Approx(42.)); CHECK(out[2] == Catch::Approx(42.)); // The High Flow columns have no matching source variant: nil, so the destination keeps // tracking the print preset rather than being pinned to another variant's value. CHECK(std::isnan(out[1])); CHECK(std::isnan(out[3])); } // The regression guard: where the row already matches the variant list -- every case that was // not corrupting the heap -- the resize is a no-op and the output is unchanged. SECTION("a correctly sized row is untouched") { DynamicPrintConfig object_config = object_override(); DynamicPrintConfig dst; dst.option("outer_wall_speed", true)->values = {200., 500., 210., 510.}; REQUIRE(object_config.update_values_from_multi_to_multi_2(src_variants, dst_variants, dst, keys) == 0); const auto& out = object_config.option("outer_wall_speed")->values; REQUIRE(out.size() == 4); CHECK(out[0] == Catch::Approx(42.)); // matched -> override CHECK(out[1] == Catch::Approx(500.)); // unmatched -> preset value preserved CHECK(out[2] == Catch::Approx(42.)); CHECK(out[3] == Catch::Approx(510.)); } // is_nil(idx) indexes values[idx] with no bounds check, so a source shorter than its own // variant list read out of range before the guard was added. SECTION("a source shorter than its variant list is read in range") { DynamicPrintConfig object_config = object_override(); // one value... DynamicPrintConfig dst; dst.option("outer_wall_speed", true)->values = {200., 500.}; REQUIRE(object_config.update_values_from_multi_to_multi_2( {"Direct Drive Standard", "Direct Drive Standard"}, // ...but two source variants {"Direct Drive Standard", "Direct Drive High Flow"}, dst, keys) == 0); const auto& out = object_config.option("outer_wall_speed")->values; REQUIRE(out.size() == 2); CHECK(out[0] == Catch::Approx(42.)); CHECK(out[1] == Catch::Approx(500.)); } SECTION("an empty destination variant list is refused") { DynamicPrintConfig object_config = object_override(); DynamicPrintConfig dst; dst.option("outer_wall_speed", true)->values = {200.}; 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); } // A per-variant filament option read with a single value gives it to every filament variant. A project // exported by an older CLI holds a single value for an option no loaded preset defined, such as // filament_ironing_flow. TEST_CASE("A per-variant filament option read with a single value gives it to every filament variant", "[Config]") { // filament 1 defines Standard and High Flow, filament 2 Standard DynamicPrintConfig config; config.option("filament_self_index", true)->values = {1, 1, 2}; config.load_from_ini_string("pressure_advance = 0.021", ForwardCompatibilitySubstitutionRule::Disable); REQUIRE(config.option("pressure_advance")->values == std::vector({0.021, 0.021, 0.021})); }