Files
OrcaSlicer/tests/libslic3r/test_config_variant_expansion.cpp
T
HanifKoh 84657ff11e Add Missing Includes Across the Remaining Sources and Tests (#16071)
* Ignore Clipper, libpng, mcut and Boost.Polygon Internals in clang-tidy

Each only works through a wrapper or umbrella header: libslic3r/clipper.hpp or clipper_z.hpp configure Clipper before including it, png.h pulls in libpng's config headers, and Boost.Polygon's headers only compile through polygon.hpp or voronoi.hpp.

* Ignore minilzo's Config Headers in clang-tidy

lzoconf.h and lzodefs.h are internal to minilzo.h, which is what the code includes.

* Add Missing Includes Across the Remaining Sources and Tests

Covers src/slic3r/Utils, src/slic3r/plugin, src/slic3r/Config, src/libvgcode, src/dev-utils, src/OrcaSlicer.cpp and tests/, the directories left after src/slic3r/GUI and src/libslic3r. Generated with clang-tidy misc-include-cleaner. libvgcode's own headers are included by relative path as in the rest of that library, and Catch2 and pybind11 with angle brackets as elsewhere in the repo.

* Make the GUI and Test Headers Compile on Their Own

Each now includes, or forward-declares, what it uses instead of relying on what its includers happened to include first. Headers that only compile on one platform, or that nothing built includes, are left alone.

* Keep Windows and nanosvg Setup Ahead of the Added Includes

OrcaSlicer.cpp and several tests set _WIN32_WINNT, WIN32_LEAN_AND_MEAN or NOMINMAX before including Windows.h, and the profile validator defines NANOSVG_IMPLEMENTATION before any libslic3r header. The added includes had landed above those blocks, which broke the Windows build.

* Add the GUI Includes the First Pass Missed

Covers headers that only became editable once they compiled on their own, and wx symbols whose suggested header changed as the clang-tidy ignore list grew after the src/slic3r/GUI pass.

* Keep the Added Test Includes Below the NOMINMAX Guard

test_marchingsquares.cpp and test_texture_displacement.cpp had includes inside #ifndef NOMINMAX, which the tests inherit as defined on Windows from libslic3r, so those were skipped there. .clang-tidy also ignores the MSVC STL and UCRT internals, Boost.Multiprecision's fwd.hpp and CPython's Windows include directory, as in #16068.
2026-10-03 13:45:21 +08:00

802 lines
47 KiB
C++

#include <catch2/catch_all.hpp>
#include "libslic3r/Config.hpp"
#include <string>
#include <vector>
#include <set>
#include <cmath>
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers.hpp>
#include <catch2/matchers/catch_matchers_vector.hpp>
#include <catch2/generators/catch_generators.hpp>
#include <catch2/catch_approx.hpp>
#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<ConfigOptionStrings>("extruder_nozzle_stats", true)->values = {"Standard#1", "Standard#3|High Flow#2"};
config.option<ConfigOptionEnumsGeneric>("extruder_type", true)->values = {etDirectDrive, etDirectDrive};
config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type", true)->values = {nvtStandard, nvtHybrid};
config.option<ConfigOptionStrings>("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<ConfigOptionInts>("print_extruder_id", true)->values = {1, 1, 2, 2};
config.option<ConfigOptionStrings>("print_extruder_variant", true)->values = {"Direct Drive Standard", "Direct Drive High Flow",
"Direct Drive Standard", "Direct Drive High Flow"};
config.option<ConfigOptionFloats>("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<ConfigOptionEnumsGeneric>("extruder_type")->values[1] = etBowden;
DynamicPrintConfig filament;
filament.option<ConfigOptionStrings>("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<std::vector<NozzleVolumeType>> 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<int> slot_index{2, 0};
ConfigOptionFloats resolved(machine);
REQUIRE(resolved.apply_override(&filament, slot_index));
REQUIRE(resolved.values == std::vector<double>({30., 42.}));
}
SECTION("an index past the machine slots keeps the slot's own value") {
std::vector<int> slot_index{5, 0};
ConfigOptionFloats resolved(machine);
REQUIRE(resolved.apply_override(&filament, slot_index));
REQUIRE(resolved.values == std::vector<double>({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<int> slot_index{2, -1, 0};
ConfigOptionFloats resolved(machine);
REQUIRE(!resolved.apply_override(&all_nil, slot_index));
REQUIRE(resolved.values == std::vector<double>({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<int> slot_index{0, -1, -1, -1, 0};
ConfigOptionFloats resolved(per_extruder);
REQUIRE(!resolved.apply_override(&all_nil, slot_index));
REQUIRE(resolved.values == std::vector<double>({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<ConfigOptionFloats>("nozzle_diameter", true)->values = {0.4, 0.4};
// Both extruders resolve to the same default variant, so there is only one column.
config.option<ConfigOptionStrings>("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<ConfigOptionFloats>("nozzle_diameter", true)->values = {0.4, 0.4};
config.option<ConfigOptionStrings>("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<ConfigOptionFloats>("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<std::string> variant_list = {"Direct Drive Standard", "Direct Drive High Flow",
"Direct Drive Standard", "Direct Drive High Flow"};
const std::vector<int> 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<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);
}
}
TEST_CASE("support interface pattern registry includes spiral inset", "[Config]")
{
const auto &values = ConfigOptionEnum<SupportMaterialInterfacePattern>::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<std::vector<NozzleVolumeType>> 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<ConfigOptionStrings>("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<ConfigOptionStrings>("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<NozzleVolumeType>{nvtStandard});
REQUIRE(nozzle_volume_types[1] == std::vector<NozzleVolumeType>{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<ConfigOptionStrings>("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<NozzleVolumeType>{nvtStandard});
REQUIRE(nozzle_volume_types[1] == std::vector<NozzleVolumeType>({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<std::vector<NozzleVolumeType>> 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<int> 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<int>({0, 2, 3}));
REQUIRE(config.option<ConfigOptionFloats>("outer_wall_speed")->values == std::vector<double>({30., 50., 500.}));
REQUIRE(config.option<ConfigOptionInts>("print_extruder_id")->values == std::vector<int>({1, 2, 2}));
REQUIRE(config.option<ConfigOptionStrings>("print_extruder_variant")->values ==
std::vector<std::string>({"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<ConfigOptionInts>("printer_extruder_id", true)->values = {1, 1, 2, 2};
config.option<ConfigOptionStrings>("printer_extruder_variant", true)->values = {"Direct Drive Standard", "Direct Drive High Flow",
"Direct Drive Standard", "Direct Drive High Flow"};
config.option<ConfigOptionFloats>("machine_max_speed_x", true)->values = {100., 50., 110., 55., 120., 60., 130., 65.};
std::vector<std::vector<NozzleVolumeType>> nozzle_volume_types;
int extruder_count = 2;
int count = config.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types);
std::vector<int> 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<int>({0, 2, 3}));
REQUIRE(config.option<ConfigOptionFloats>("machine_max_speed_x")->values ==
std::vector<double>({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<ConfigOptionStrings>("filament_extruder_variant", true)->values = {"Direct Drive Standard", "Direct Drive High Flow"};
filament_config.option<ConfigOptionFloats>("filament_max_volumetric_speed", true)->values = {12., 20.};
std::vector<std::vector<NozzleVolumeType>> 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<int> 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<int>({0}));
REQUIRE(filament_config.option<ConfigOptionFloats>("filament_max_volumetric_speed")->values == std::vector<double>({12.}));
}
SECTION("a High Flow filament volume type selects the High Flow column") {
std::vector<int> 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<int>({1}));
REQUIRE(filament_config.option<ConfigOptionFloats>("filament_max_volumetric_speed")->values == std::vector<double>({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<ConfigOptionStrings>("extruder_nozzle_stats", true)->values = {"", "Standard#3|High Flow#3"};
config.option<ConfigOptionEnumsGeneric>("extruder_type", true)->values = {etDirectDrive, etDirectDrive};
config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type", true)->values = {nvtStandard, nvtHybrid};
config.option<ConfigOptionStrings>("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<std::vector<NozzleVolumeType>> 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<int> 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<int>({0, 2, 3}));
REQUIRE(config.option<ConfigOptionFloats>("outer_wall_speed")->values == std::vector<double>({30., 50., 500.}));
}
SECTION("without Hybrid or extra slots the expansion matches the per-extruder resolution") {
DynamicPrintConfig config;
config.option<ConfigOptionEnumsGeneric>("extruder_type", true)->values = {etDirectDrive, etDirectDrive};
config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type", true)->values = {nvtStandard, nvtHighFlow};
config.option<ConfigOptionStrings>("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<int> 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<int>({0, 3}));
std::vector<std::vector<NozzleVolumeType>> 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<int> 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<ConfigOptionFloats>("outer_wall_speed")->values == std::vector<double>({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<ConfigOptionInts>("print_extruder_id", true)->values = {1};
config.option<ConfigOptionStrings>("print_extruder_variant", true)->values = {"Direct Drive Standard"};
config.option<ConfigOptionFloats>("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<ConfigOptionEnumsGeneric>("extruder_type", true)->values = {etDirectDrive, etDirectDrive};
config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type", true)->values = {nvtStandard, nvtStandard};
config.option<ConfigOptionStrings>("extruder_variant_list", true)->values = {"Direct Drive Standard", "Direct Drive Standard"};
add_degenerate_print_columns(config);
std::vector<std::vector<NozzleVolumeType>> nozzle_volume_types;
int extruder_count = 2;
int count = config.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types);
std::vector<int> 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<int>({0, 1}));
REQUIRE(config.option<ConfigOptionInts>("print_extruder_id")->values == std::vector<int>({1, 2}));
REQUIRE(config.option<ConfigOptionStrings>("print_extruder_variant")->values ==
std::vector<std::string>({"Direct Drive Standard", "Direct Drive Standard"}));
// width-1 data arrays replicate their only column into every slot
REQUIRE(config.option<ConfigOptionFloats>("outer_wall_speed")->values == std::vector<double>({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<std::vector<NozzleVolumeType>> 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<int> 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<int>({0, 2, 3}));
REQUIRE(config.option<ConfigOptionInts>("print_extruder_id")->values == std::vector<int>({1, 2, 2}));
REQUIRE(config.option<ConfigOptionStrings>("print_extruder_variant")->values ==
std::vector<std::string>({"Direct Drive Standard", "Direct Drive Standard", "Direct Drive High Flow"}));
REQUIRE(config.option<ConfigOptionFloats>("outer_wall_speed")->values == std::vector<double>({30., 30., 30.}));
}
SECTION("a single-extruder single-column layout is not treated as degenerate") {
DynamicPrintConfig config;
config.option<ConfigOptionEnumsGeneric>("extruder_type", true)->values = {etDirectDrive};
config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type", true)->values = {nvtStandard};
config.option<ConfigOptionStrings>("extruder_variant_list", true)->values = {"Direct Drive Standard"};
add_degenerate_print_columns(config);
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(config, extruder_count, count, nozzle_volume_types,
print_options_with_variant, "print_extruder_id", "print_extruder_variant");
REQUIRE(config.option<ConfigOptionInts>("print_extruder_id")->values == std::vector<int>({1}));
REQUIRE(config.option<ConfigOptionFloats>("outer_wall_speed")->values == std::vector<double>({30.}));
}
SECTION("a second expansion leaves the synthesized layout unchanged") {
DynamicPrintConfig config;
config.option<ConfigOptionEnumsGeneric>("extruder_type", true)->values = {etDirectDrive, etDirectDrive};
config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type", true)->values = {nvtStandard, nvtStandard};
config.option<ConfigOptionStrings>("extruder_variant_list", true)->values = {"Direct Drive Standard", "Direct Drive Standard"};
add_degenerate_print_columns(config);
std::vector<std::vector<NozzleVolumeType>> 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<ConfigOptionInts>("print_extruder_id")->values ==
once.option<ConfigOptionInts>("print_extruder_id")->values);
REQUIRE(config.option<ConfigOptionStrings>("print_extruder_variant")->values ==
once.option<ConfigOptionStrings>("print_extruder_variant")->values);
REQUIRE(config.option<ConfigOptionFloats>("outer_wall_speed")->values ==
once.option<ConfigOptionFloats>("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<ConfigOptionInts>("filament_self_index", true)->values = {1, 1, 2, 2};
config.option<ConfigOptionStrings>("filament_extruder_variant", true)->values = {"Direct Drive Standard", "Direct Drive High Flow",
"Direct Drive Standard", "Direct Drive High Flow"};
config.option<ConfigOptionFloats>("filament_max_volumetric_speed", true)->values = {12., 20., 13., 21.};
config.option<ConfigOptionFloats>("fan_max_speed", true)->values = {15., 60., 20., 80.};
config.option<ConfigOptionBools>("filament_multitool_ramming", true)->values = {false, true, false, true};
};
std::set<std::string> 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<ConfigOptionInts>("filament_map", true)->values = {2, 2};
config.option<ConfigOptionInts>("filament_volume_map", true)->values = {nvtStandard, nvtHighFlow};
std::vector<std::vector<NozzleVolumeType>> 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<ConfigOptionFloats>("filament_max_volumetric_speed")->values == std::vector<double>({12., 21.}));
REQUIRE(config.option<ConfigOptionFloats>("fan_max_speed")->values == std::vector<double>({15., 80.}));
REQUIRE(config.option<ConfigOptionBools>("filament_multitool_ramming")->values == std::vector<unsigned char>({0, 1}));
REQUIRE(config.option<ConfigOptionStrings>("filament_extruder_variant")->values ==
std::vector<std::string>({"Direct Drive Standard", "Direct Drive High Flow"}));
REQUIRE(config.option<ConfigOptionInts>("filament_self_index")->values == std::vector<int>({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<ConfigOptionInts>("filament_map", true)->values = {2, 2};
// the registered default is a single-element vector; it must not override slot resolution
config.option<ConfigOptionInts>("filament_volume_map", true)->values = {nvtStandard};
std::vector<std::vector<NozzleVolumeType>> 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<ConfigOptionFloats>("filament_max_volumetric_speed")->values == std::vector<double>({12., 13.}));
REQUIRE(config.option<ConfigOptionInts>("filament_self_index")->values == std::vector<int>({1, 2}));
}
SECTION("a single-filament explicit assignment on a Hybrid extruder is honored") {
DynamicPrintConfig config = make_hybrid_printer_config();
config.option<ConfigOptionInts>("filament_self_index", true)->values = {1, 1};
config.option<ConfigOptionStrings>("filament_extruder_variant", true)->values = {"Direct Drive Standard", "Direct Drive High Flow"};
config.option<ConfigOptionFloats>("filament_max_volumetric_speed", true)->values = {12., 20.};
config.option<ConfigOptionInts>("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<ConfigOptionInts>("filament_volume_map", true)->values = {nvtHighFlow};
std::vector<std::vector<NozzleVolumeType>> 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<ConfigOptionFloats>("filament_max_volumetric_speed")->values == std::vector<double>({20.}));
REQUIRE(config.option<ConfigOptionInts>("filament_self_index")->values == std::vector<int>({1}));
}
SECTION("without Hybrid or extra slots the volume map is not consulted") {
DynamicPrintConfig config;
config.option<ConfigOptionEnumsGeneric>("extruder_type", true)->values = {etDirectDrive, etDirectDrive};
config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type", true)->values = {nvtStandard, nvtHighFlow};
config.option<ConfigOptionStrings>("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<ConfigOptionInts>("filament_map", true)->values = {1, 2};
// sized to the filament count, but inert because no extruder exposes multiple volume types
config.option<ConfigOptionInts>("filament_volume_map", true)->values = {nvtHighFlow, nvtStandard};
std::vector<std::vector<NozzleVolumeType>> 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<ConfigOptionFloats>("filament_max_volumetric_speed")->values == std::vector<double>({12., 21.}));
REQUIRE(config.option<ConfigOptionInts>("filament_self_index")->values == std::vector<int>({1, 2}));
}
SECTION("a variant option shorter than the filament slots keeps its first value instead of zero") {
DynamicPrintConfig config;
config.option<ConfigOptionEnumsGeneric>("extruder_type", true)->values = {etDirectDrive, etDirectDrive};
config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type", true)->values = {nvtStandard, nvtHighFlow};
config.option<ConfigOptionStrings>("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<ConfigOptionInts>("filament_map", true)->values = {1, 2};
// no loaded preset carries the key, so only its single registered default is present
config.option<ConfigOptionFloatsNullable>("filament_cooling_before_tower", true)->values = {10.};
// only the first filament's two variant columns were loaded
config.option<ConfigOptionFloatsNullable>("filament_ramming_volumetric_speed", true)->values = {-1., -2.};
std::vector<std::vector<NozzleVolumeType>> 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<ConfigOptionFloatsNullable>("filament_cooling_before_tower")->values,
Catch::Matchers::Approx(std::vector<double>({10., 10.})));
REQUIRE_THAT(config.option<ConfigOptionFloatsNullable>("filament_ramming_volumetric_speed")->values,
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
// 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<std::string> src_variants{"Direct Drive Standard"};
const std::vector<std::string> dst_variants{"Direct Drive Standard", "Direct Drive High Flow",
"Direct Drive Standard", "Direct Drive High Flow"};
const std::set<std::string> keys{"outer_wall_speed"};
// The per-object override as authored on the single-variant printer.
const auto object_override = [] {
DynamicPrintConfig c;
c.option<ConfigOptionFloatsNullable>("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<ConfigOptionFloatsNullable>("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<ConfigOptionFloatsNullable>("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<ConfigOptionFloatsNullable>("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<ConfigOptionFloatsNullable>("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<ConfigOptionFloatsNullable>("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<ConfigOptionFloatsNullable>("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<ConfigOptionFloatsNullable>("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<ConfigOptionInts>("print_extruder_id", true)->values = {1};
config.option<ConfigOptionStrings>("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<ConfigOptionInts>("printer_extruder_id", true)->values = {1, 2};
config.option<ConfigOptionStrings>("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<ConfigOptionInts>("filament_self_index", true)->values = {1, 1, 2};
config.load_from_ini_string("pressure_advance = 0.021", ForwardCompatibilitySubstitutionRule::Disable);
REQUIRE(config.option<ConfigOptionFloats>("pressure_advance")->values == std::vector<double>({0.021, 0.021, 0.021}));
}