Add multi-variant support to the app

This commit is contained in:
SoftFever
2026-09-28 19:08:35 +08:00
parent 2ccabb5695
commit 71bb400e46
27 changed files with 846 additions and 195 deletions
+53
View File
@@ -715,3 +715,56 @@ TEST_CASE("Multi-extruder slice stays in bounds with a short max_layer_height",
REQUIRE_FALSE(print.objects().front()->layers().empty());
}
// A filament can define several variants (Standard, High Flow). Each filament prints with its
// variant of the extruder's variant string, or with its own first variant when it defines none, on a
// printer listing a single variant as on one listing several.
TEST_CASE("Each filament prints with its variant of the extruder's variant string", "[MultiFilament]")
{
auto [variant_list, nozzle_volume_type, filament, temperature, resolved] = GENERATE(table<std::string, NozzleVolumeType, int, int, std::string>({
{ "Direct Drive Standard", nvtStandard, 1, 211, "211,223" },
{ "Direct Drive Standard", nvtStandard, 2, 223, "211,223" },
{ "Direct Drive High Flow", nvtHighFlow, 1, 239, "239,223" },
{ "Direct Drive High Flow", nvtHighFlow, 2, 223, "239,223" }, // filament 2 defines no High Flow variant
{ "Direct Drive Standard,Direct Drive High Flow", nvtHighFlow, 1, 239, "239,223" },
{ "Direct Drive Standard,Direct Drive High Flow", nvtHighFlow, 2, 223, "239,223" },
}));
DYNAMIC_SECTION(variant_list << " printer, " << get_nozzle_volume_type_string(nozzle_volume_type) << " nozzle, filament " << filament) {
DynamicPrintConfig config = multifilament_config(2, {
{ "extruder_variant_list", variant_list },
// filament 1 defines Standard (211) and High Flow (239), filament 2 Standard (223)
{ "filament_extruder_variant", "Direct Drive Standard;Direct Drive High Flow;Direct Drive Standard" },
{ "filament_self_index", "1,1,2" },
{ "nozzle_temperature", "211,239,223" },
{ "nozzle_temperature_initial_layer", "211,239,223" },
{ "sparse_infill_filament_id", filament },
{ "internal_solid_filament_id", filament },
{ "top_surface_filament_id", filament },
{ "bottom_surface_filament_id", filament },
{ "outer_wall_filament_id", filament },
{ "inner_wall_filament_id", filament },
{ "enable_prime_tower", 0 },
{ "skirt_loops", 0 },
{ "brim_type", "no_brim" },
// custom G-code indexes the per-filament arrays by filament
{ "machine_start_gcode", "; start temperature {nozzle_temperature_initial_layer[initial_extruder]}" },
});
config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type", true)->values = { nozzle_volume_type };
const std::string gcode = slice({ cube(20) }, config);
std::set<int> temperatures;
std::istringstream stream(gcode);
for (std::string line; std::getline(stream, line);) {
if (line.rfind("M104 ", 0) != 0 && line.rfind("M109 ", 0) != 0)
continue;
const size_t s = line.find(" S");
if (s != std::string::npos && std::stoi(line.substr(s + 2)) > 0)
temperatures.insert(std::stoi(line.substr(s + 2)));
}
CHECK(temperatures == std::set<int>{ temperature });
CHECK(gcode.find("; start temperature " + std::to_string(temperature) + "\n") != std::string::npos);
// The config the slice ran with holds one value per filament, as the readers that index
// it by filament (the wipe tower, the filament compatibility check) expect.
CHECK(gcode.find("; nozzle_temperature = " + resolved + "\n") != std::string::npos);
}
}
@@ -120,13 +120,25 @@ TEST_CASE("get_config_index_base resolves (volume type, extruder type, id) to a
REQUIRE(get_config_index_base(nvtHighFlow, etDirectDrive, 2, variant_list, variant_ids) == 3);
}
SECTION("no matching column falls back to slot 0") {
REQUIRE(get_config_index_base(nvtStandard, etDirectDrive, 3, variant_list, variant_ids) == 0);
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("Hybrid is not a preset variant string and falls back to slot 0") {
REQUIRE(get_config_index_base(nvtHybrid, etDirectDrive, 2, variant_list, variant_ids) == 0);
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<std::string> mixed_list = {"Direct Drive Standard", "Direct Drive High Flow", "Direct Drive Standard"};
const std::vector<int> 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);
}
}
@@ -512,6 +524,73 @@ TEST_CASE("update_values_to_printer_extruders_for_multiple_filaments resolves pe
Catch::Matchers::Approx(std::vector<double>({-1., -1.})));
REQUIRE(config.option<ConfigOptionFloats>("filament_max_volumetric_speed")->values == std::vector<double>({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<ConfigOptionEnumsGeneric>("extruder_type", true)->values = {etDirectDrive};
config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type", true)->values = {nozzle_volume_type};
config.option<ConfigOptionStrings>("extruder_variant_list", true)->values = {variant};
config.option<ConfigOptionFloats>("filament_diameter", true)->values = {1.75, 1.75};
config.option<ConfigOptionInts>("filament_self_index", true)->values = {1, 1, 2};
config.option<ConfigOptionStrings>("filament_extruder_variant", true)->values = {"Direct Drive Standard", "Direct Drive High Flow",
"Direct Drive Standard"};
config.option<ConfigOptionFloats>("filament_max_volumetric_speed", true)->values = {12., 20., 13.};
config.option<ConfigOptionInts>("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<NozzleVolumeType, std::string, std::vector<double>>({
{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<std::vector<NozzleVolumeType>> 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<ConfigOptionFloats>("filament_max_volumetric_speed")->values == speeds);
REQUIRE(config.option<ConfigOptionInts>("filament_self_index")->values == std::vector<int>({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<ConfigOptionInts>("filament_map", true)->values = {1};
std::vector<std::vector<NozzleVolumeType>> 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<ConfigOptionFloats>("filament_max_volumetric_speed")->values == std::vector<double>({12., 13.}));
REQUIRE(config.option<ConfigOptionInts>("filament_self_index")->values == std::vector<int>({1, 2}));
}
}
TEST_CASE("has_multi_variant_filament is true only when a filament defines more than one variant", "[Config]")
{
DynamicPrintConfig config;
config.option<ConfigOptionFloats>("filament_diameter", true)->values = {1.75, 1.75};
SECTION("one variant per filament reports false") {
config.option<ConfigOptionStrings>("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<ConfigOptionStrings>("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
@@ -10,11 +10,13 @@ TEST_CASE("convert_to_nvt_type maps extruder variant strings to nozzle volume ty
REQUIRE(convert_to_nvt_type("Direct Drive Standard") == nvtStandard);
REQUIRE(convert_to_nvt_type("Direct Drive High Flow") == nvtHighFlow);
REQUIRE(convert_to_nvt_type("Direct Drive TPU High Flow") == nvtTPUHighFlow);
REQUIRE(convert_to_nvt_type("Direct Drive E3D High Flow") == nvtE3DHighFlow);
}
SECTION("Bowden variants") {
REQUIRE(convert_to_nvt_type("Bowden Standard") == nvtStandard);
REQUIRE(convert_to_nvt_type("Bowden High Flow") == nvtHighFlow);
REQUIRE(convert_to_nvt_type("Bowden E3D High Flow") == nvtE3DHighFlow);
}
SECTION("Unparsable strings fall back to hybrid") {
@@ -29,3 +31,49 @@ TEST_CASE("convert_to_nvt_type maps extruder variant strings to nozzle volume ty
REQUIRE(convert_to_nvt_type(" Bowden Standard") == nvtStandard);
}
}
TEST_CASE("E3D High Flow is nozzle volume type 5, after the reserved 4", "[Config]")
{
REQUIRE(int(nvtE3DHighFlow) == 5);
REQUIRE(get_valid_nozzle_volume_type() == std::set<NozzleVolumeType>{nvtStandard, nvtHighFlow, nvtTPUHighFlow, nvtE3DHighFlow});
REQUIRE(get_nozzle_volume_type_string(nvtE3DHighFlow) == "E3D High Flow");
REQUIRE(get_extruder_variant_string(etDirectDrive, nvtE3DHighFlow) == "Direct Drive E3D High Flow");
REQUIRE(get_extruder_variant_string(etBowden, nvtE3DHighFlow) == "Bowden E3D High Flow");
SECTION("nozzle_volume_type round-trips it by name") {
DynamicPrintConfig config;
config.set_deserialize_strict("nozzle_volume_type", "Standard,E3D High Flow");
REQUIRE(config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type")->values == std::vector<int>{nvtStandard, nvtE3DHighFlow});
REQUIRE(config.opt_serialize("nozzle_volume_type") == "Standard,E3D High Flow");
}
SECTION("its variant is found by value, not mistaken for High Flow") {
const std::vector<std::string> variant_list = {"Direct Drive Standard", "Direct Drive High Flow", "Direct Drive E3D High Flow"};
const std::vector<int> variant_ids = {1, 1, 1};
REQUIRE(get_config_index_base(nvtE3DHighFlow, etDirectDrive, 1, variant_list, variant_ids) == 2);
REQUIRE(get_config_index_base(nvtHighFlow, etDirectDrive, 1, variant_list, variant_ids) == 1);
}
}
TEST_CASE("get_extruder_supported_nozzle_volume_types reads the extruder's variant list", "[Config]")
{
DynamicPrintConfig config;
// H2C: the left extruder offers E3D High Flow, the right (nozzle rack) extruder does not.
config.set_deserialize_strict("extruder_type", "Direct Drive,Direct Drive");
config.option<ConfigOptionStrings>("extruder_variant_list", true)->values = {
"Direct Drive Standard,Direct Drive High Flow,Direct Drive E3D High Flow",
"Direct Drive Standard,Direct Drive High Flow"};
REQUIRE(get_extruder_supported_nozzle_volume_types(config, 0) == std::set<NozzleVolumeType>{nvtStandard, nvtHighFlow, nvtE3DHighFlow});
REQUIRE(get_extruder_supported_nozzle_volume_types(config, 1) == std::set<NozzleVolumeType>{nvtStandard, nvtHighFlow});
SECTION("an E3D-only list does not report plain High Flow") {
config.option<ConfigOptionStrings>("extruder_variant_list")->values[1] = "Direct Drive Standard,Direct Drive E3D High Flow";
REQUIRE(get_extruder_supported_nozzle_volume_types(config, 1) == std::set<NozzleVolumeType>{nvtStandard, nvtE3DHighFlow});
}
SECTION("an extruder past the profile's lists gives the empty, unknown set") {
REQUIRE(get_extruder_supported_nozzle_volume_types(config, 2).empty());
REQUIRE(get_extruder_supported_nozzle_volume_types(config, -1).empty());
}
}