Merge branch 'main' into feat/plugin-feature

Resolve five conflicts, all of which needed both sides rather than a pick:

- BackgroundSlicingProcess: ours was a pure tabs->spaces reformat of base, so
  keep main's per-filament volume/nozzle map read-back (its only change here).
- GUI_App: main's #12506 else-if attached to an `if` this branch deleted;
  re-expressed onto the same-agent early-return path (the agent factory caches
  per id, so pointer equality is the same predicate).
- MainFrame: both sides relocated Sync Presets independently; keep main's
  push_notification plus the branch's Plugins menu items.
- Tab: the "TODO: Orca: Support hybrid" blocks were unchanged base, not a branch
  decision; take main's enabled Hybrid to match the already auto-merged siblings.
- test_config: union of both sides' cases (6 plugin + 9 multi-nozzle).
This commit is contained in:
SoftFever
2026-07-15 17:18:46 +08:00
986 changed files with 210610 additions and 18572 deletions

View File

@@ -535,3 +535,247 @@ TEST_CASE_METHOD(PluginResolverFixture,
CHECK(manifest == std::vector<std::string>{"x;;slicing-pipeline"});
}
TEST_CASE("H2C/A2L-era multi-nozzle and pre-heat config keys exist", "[config]") {
// Foundation keys backing H2C 6-nozzle cluster grouping, the pre-heat/pre-cool time
// model, and wipe-tower nozzle-change handling. Defaults must keep existing
// single-nozzle printers behaving identically.
Slic3r::DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
// Printer / per-extruder options
REQUIRE(config.option<ConfigOptionIntsNullable>("extruder_max_nozzle_count") != nullptr);
REQUIRE(config.option<ConfigOptionIntsNullable>("extruder_max_nozzle_count")->values == std::vector<int>{1});
REQUIRE(config.option<ConfigOptionBool>("enable_pre_heating") != nullptr);
REQUIRE(config.option<ConfigOptionBool>("enable_pre_heating")->value == false);
REQUIRE(config.option<ConfigOptionFloatsNullable>("hotend_cooling_rate") != nullptr);
REQUIRE(config.option<ConfigOptionFloatsNullable>("hotend_heating_rate") != nullptr);
REQUIRE(config.option<ConfigOptionFloat>("machine_hotend_change_time") != nullptr);
REQUIRE(config.option<ConfigOptionFloat>("machine_prepare_compensation_time") != nullptr);
// Filament pre-cooling / ramming / nozzle-change (nc) options
REQUIRE(config.option<ConfigOptionIntsNullable>("filament_pre_cooling_temperature") != nullptr);
REQUIRE(config.option<ConfigOptionIntsNullable>("filament_pre_cooling_temperature_nc") != nullptr);
REQUIRE(config.option<ConfigOptionFloatsNullable>("filament_preheat_temperature_delta") != nullptr);
REQUIRE(config.option<ConfigOptionFloatsNullable>("filament_retract_length_nc") != nullptr);
REQUIRE(config.option<ConfigOptionFloats>("filament_change_length_nc") != nullptr);
REQUIRE(config.option<ConfigOptionFloats>("filament_prime_volume_nc") != nullptr);
REQUIRE(config.option<ConfigOptionFloatsNullable>("filament_ramming_travel_time") != nullptr);
REQUIRE(config.option<ConfigOptionFloatsNullable>("filament_ramming_travel_time_nc") != nullptr);
REQUIRE(config.option<ConfigOptionFloatsNullable>("filament_ramming_volumetric_speed") != nullptr);
REQUIRE(config.option<ConfigOptionFloatsNullable>("filament_ramming_volumetric_speed_nc") != nullptr);
// Spot-check defaults that must not alter existing behavior.
REQUIRE(config.option<ConfigOptionFloatsNullable>("filament_retract_length_nc")->values == std::vector<double>{10.});
REQUIRE(config.option<ConfigOptionFloats>("filament_prime_volume_nc")->values == std::vector<double>{60.});
REQUIRE(config.option<ConfigOptionIntsNullable>("filament_pre_cooling_temperature_nc")->values == std::vector<int>{0});
REQUIRE(config.option<ConfigOptionFloatsNullable>("filament_ramming_volumetric_speed")->values == std::vector<double>{-1});
}
SCENARIO("ConfigOptionVector::set_to_index with stride=1 copies values correctly", "[Config][set_to_index]") {
GIVEN("A destination vector and a source vector with 3 values") {
Slic3r::ConfigOptionFloats dest({0.0});
Slic3r::ConfigOptionFloats src({10.0, 20.0, 30.0});
std::vector<int> variant_index = {0, 1, 2};
int stride = 1;
WHEN("set_to_index is called with stride=1") {
dest.set_to_index(&src, variant_index, stride);
THEN("The destination contains the source values") {
REQUIRE(dest.values.size() == 3);
REQUIRE(dest.values[0] == 10.0);
REQUIRE(dest.values[1] == 20.0);
REQUIRE(dest.values[2] == 30.0);
}
}
}
GIVEN("A destination vector and a source vector with subset mapping") {
Slic3r::ConfigOptionFloats dest({0.0});
Slic3r::ConfigOptionFloats src({100.0, 200.0, 300.0});
std::vector<int> variant_index = {1, 2};
int stride = 1;
WHEN("set_to_index maps only indices 1 and 2") {
dest.set_to_index(&src, variant_index, stride);
THEN("Only the mapped values are copied, default fills the others") {
REQUIRE(dest.values.size() == 2);
REQUIRE(dest.values[0] == 200.0);
REQUIRE(dest.values[1] == 300.0);
}
}
}
}
SCENARIO("ConfigOptionVector::set_to_index with stride=2 copies grouped values correctly", "[Config][set_to_index]") {
GIVEN("A destination vector and a source vector with stride=2 (e.g., nozzle groups)") {
// Source has 4 groups of 2 values each: (10,11), (20,21), (30,31), (40,41)
Slic3r::ConfigOptionFloats dest({0.0});
Slic3r::ConfigOptionFloats src({10.0, 11.0, 20.0, 21.0, 30.0, 31.0, 40.0, 41.0});
int stride = 2;
WHEN("set_to_index maps groups 0, 1, 3") {
std::vector<int> variant_index = {0, 1, 3};
dest.set_to_index(&src, variant_index, stride);
THEN("The destination has 3 groups (6 values) mapped correctly") {
REQUIRE(dest.values.size() == 6);
// Group 0: (10, 11)
REQUIRE(dest.values[0] == 10.0);
REQUIRE(dest.values[1] == 11.0);
// Group 1: (20, 21)
REQUIRE(dest.values[2] == 20.0);
REQUIRE(dest.values[3] == 21.0);
// Group 3: (40, 41)
REQUIRE(dest.values[4] == 40.0);
REQUIRE(dest.values[5] == 41.0);
}
}
}
GIVEN("A destination and a single-group source") {
Slic3r::ConfigOptionFloats dest({0.0});
// Source has 1 group of 2 values
Slic3r::ConfigOptionFloats src({50.0, 60.0});
int stride = 2;
WHEN("set_to_index maps group 0 from a single-group source") {
std::vector<int> variant_index = {0};
dest.set_to_index(&src, variant_index, stride);
THEN("The destination contains the single group correctly") {
REQUIRE(dest.values.size() == 2);
REQUIRE(dest.values[0] == 50.0);
REQUIRE(dest.values[1] == 60.0);
}
}
}
}
SCENARIO("ConfigOptionVector::set_to_index handles empty dest_index", "[Config][set_to_index]") {
GIVEN("A destination and source with stride=2") {
Slic3r::ConfigOptionFloats dest({0.0});
Slic3r::ConfigOptionFloats src({10.0, 11.0, 20.0, 21.0});
std::vector<int> variant_index = {};
int stride = 2;
WHEN("set_to_index is called with an empty index vector") {
dest.set_to_index(&src, variant_index, stride);
THEN("The destination is resized to 0") {
REQUIRE(dest.values.size() == 0);
}
}
}
}
SCENARIO("ConfigOptionVector::set_to_index handles nil values in source", "[Config][set_to_index]") {
GIVEN("A source with a nil group (stride=2)") {
Slic3r::ConfigOptionFloatsNullable dest({0.0});
Slic3r::ConfigOptionFloatsNullable src({10.0, 11.0,
Slic3r::ConfigOptionFloatsNullable::nil_value(), Slic3r::ConfigOptionFloatsNullable::nil_value(),
30.0, 31.0});
int stride = 2;
WHEN("set_to_index maps all groups including the nil one") {
std::vector<int> variant_index = {0, 1, 2};
dest.set_to_index(&src, variant_index, stride);
THEN("Non-nil groups are copied and the nil group keeps the default") {
REQUIRE(dest.values.size() == 6);
// Group 0: (10, 11) — copied
REQUIRE(dest.values[0] == 10.0);
REQUIRE(dest.values[1] == 11.0);
// Group 1: nil — keeps default (the front value = 10.0)
REQUIRE(dest.values[2] == 10.0);
REQUIRE(dest.values[3] == 10.0);
// Group 2: (30, 31) — copied
REQUIRE(dest.values[4] == 30.0);
REQUIRE(dest.values[5] == 31.0);
}
}
}
}
SCENARIO("ConfigOptionVector::set_to_index handles out-of-bounds dest_index", "[Config][set_to_index]") {
GIVEN("A source with only 2 groups (4 values) but dest_index references group 3") {
Slic3r::ConfigOptionFloats dest({0.0});
Slic3r::ConfigOptionFloats src({10.0, 11.0, 20.0, 21.0}); // 2 groups of stride 2
int stride = 2;
WHEN("set_to_index maps group 3 which is out of bounds") {
std::vector<int> variant_index = {0, 3}; // group 3 is out of range
dest.set_to_index(&src, variant_index, stride);
THEN("Group 0 is copied, group 3 falls back to default without crashing") {
REQUIRE(dest.values.size() == 4);
// Group 0: (10, 11) — copied
REQUIRE(dest.values[0] == 10.0);
REQUIRE(dest.values[1] == 11.0);
// Group 3: out of bounds — keeps default (10.0 = src.values.front())
REQUIRE(dest.values[2] == 10.0);
REQUIRE(dest.values[3] == 10.0);
}
}
}
}
SCENARIO("ConfigOptionVector::set_to_index handles negative dest_index values", "[Config][set_to_index]") {
GIVEN("A destination and source with a negative entry in dest_index") {
// The dest is initially empty, so resize fills all slots with src.values.front().
Slic3r::ConfigOptionFloats dest;
Slic3r::ConfigOptionFloats src({100.0, 101.0, 200.0, 201.0});
int stride = 2;
WHEN("set_to_index maps group 0 and a negative index") {
std::vector<int> variant_index = {-1, 0};
dest.set_to_index(&src, variant_index, stride);
THEN("The negative index is skipped, the valid group is copied") {
REQUIRE(dest.values.size() == 4);
// Position 0 (variant_index[0] = -1): skipped, keeps default fill
// from resize (src.values.front() = 100.0, applied to all new elements)
REQUIRE(dest.values[0] == 100.0);
REQUIRE(dest.values[1] == 100.0);
// Position 1 (variant_index[1] = 0): copied from group 0 of src
REQUIRE(dest.values[2] == 100.0);
REQUIRE(dest.values[3] == 101.0);
}
}
}
}
SCENARIO("ConfigOptionVector::set_to_index handles single-element groups with stride=1", "[Config][set_to_index]") {
GIVEN("A destination re-mapping one variant index with a stride=1 source") {
// Simulates the PrintObject.cpp code path: stride=1, variant_index={1}
Slic3r::ConfigOptionFloats dest({99.0, 99.0, 99.0, 99.0}); // pre-sized for 4 extruders
Slic3r::ConfigOptionFloats src({0.5, 0.6, 0.7, 0.8}); // 4 extruder values
std::vector<int> variant_index = {1}; // only extruder 1 is active
int stride = 1;
WHEN("set_to_index is called") {
dest.set_to_index(&src, variant_index, stride);
THEN("Only the mapped value is copied, rest are defaulted") {
REQUIRE(dest.values.size() == 1);
REQUIRE(dest.values[0] == 0.6);
}
}
}
}
SCENARIO("ConfigOptionVector::set_to_index throws on incompatible type", "[Config][set_to_index]") {
GIVEN("A Floats destination and an Ints source") {
Slic3r::ConfigOptionFloats dest({0.0});
Slic3r::ConfigOptionInts src({1, 2, 3});
std::vector<int> variant_index = {0};
int stride = 1;
WHEN("set_to_index is called with mismatched types") {
THEN("A ConfigurationError is thrown") {
REQUIRE_THROWS_AS(dest.set_to_index(&src, variant_index, stride), Slic3r::ConfigurationError);
}
}
}
}