Merge upstream main: rib wall prime tower, wipe tower sync, multi-extruder config fixes, plugin resolution fixes

This commit is contained in:
Clifford Garwood
2026-07-31 15:09:53 -04:00
27 changed files with 2377 additions and 1468 deletions
@@ -43,18 +43,33 @@ TEST_CASE("apply_override fills nil entries from the 0-based default index", "[C
REQUIRE(resolved.values == std::vector<double>({30., 42.}));
}
SECTION("an index past the machine slots falls back to the first slot") {
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) falls back to the first slot") {
std::vector<int> slot_index{-1, 0};
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(&filament, slot_index));
REQUIRE(resolved.values == std::vector<double>({10., 42.}));
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.}));
}
}
@@ -272,6 +287,102 @@ TEST_CASE("update_values_to_printer_extruders expands one slot per (extruder x v
}
}
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) {
@@ -500,6 +500,52 @@ TEST_CASE("Re-applying an unchanged config after slicing keeps the result valid"
REQUIRE(print.is_step_done(psSlicingFinished));
}
TEST_CASE("A degenerate process variant map on a custom multi-extruder printer slices to a stable result", "[Print][Regression]")
{
// Non-BBL multi-extruder printers get machine-scope variant columns synthesized on preset
// load (extend_extruder_variant), but nothing ships process-scope print_extruder_id /
// print_extruder_variant: presets and 3mf project configs carry the length-1 defaults. The
// apply-time expansion must synthesize the process columns from extruder_variant_list;
// otherwise the failed per-extruder lookups collapse the per-extruder retract overrides
// during slicing and the post-slice re-apply invalidates every fresh result, forever.
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_num_extruders(5);
config.option<ConfigOptionFloats>("nozzle_diameter", true)->values = {0.4, 0.4, 0.4, 0.4, 0.4};
// per-extruder machine values that a first-slot collapse would destroy
config.option<ConfigOptionPercents>("retract_before_wipe", true)->values = {100., 70., 70., 70., 100.};
config.option<ConfigOptionEnumsGeneric>("z_hop_types", true)->values = {zhtSlope, zhtNormal, zhtNormal, zhtNormal, zhtSlope};
// filament presets carry the nullable override twins (all-nil = "no override"); they are what
// routes the machine values through apply_override in the in-slice override recompute
config.option<ConfigOptionPercentsNullable>("filament_retract_before_wipe", true)->values =
std::vector<double>(5, ConfigOptionPercentsNullable::nil_value());
config.option<ConfigOptionEnumsGenericNullable>("filament_z_hop_types", true)->values =
std::vector<int>(5, ConfigOptionEnumsGenericNullable::nil_value());
config.option<ConfigOptionFloats>("filament_diameter", true)->values = std::vector<double>(5, 1.75);
config.option<ConfigOptionStrings>("filament_colour", true)->values = {"#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#00FFFF"};
config.option<ConfigOptionInts>("filament_map", true)->values = {1, 2, 3, 4, 1};
Model model;
model.add_object("cube", "", make_cube(20, 20, 20))->add_instance()->set_offset(Vec3d(100., 100., 0.));
Print print;
print.apply(model, config);
print.process();
REQUIRE(print.is_step_done(psSlicingFinished));
// BackgroundSlicingProcess reads the engine-computed maps back into the plate config after
// slicing; the next apply overlays that written-back state.
config.option<ConfigOptionInts>("filament_map", true)->values = print.get_filament_maps();
config.option<ConfigOptionInts>("filament_volume_map", true)->values = print.get_filament_volume_maps();
config.option<ConfigOptionInts>("filament_nozzle_map", true)->values = print.get_filament_nozzle_maps();
auto status = print.apply(model, config);
REQUIRE(status == PrintBase::APPLY_STATUS_UNCHANGED);
REQUIRE(print.is_step_done(psSlicingFinished));
// the per-extruder machine values must survive the in-slice override recompute
REQUIRE(print.config().retract_before_wipe.values == std::vector<double>({100., 70., 70., 70., 100.}));
REQUIRE(print.config().z_hop_types.values == std::vector<int>({zhtSlope, zhtNormal, zhtNormal, zhtNormal, zhtSlope}));
}
TEST_CASE("normalize_nozzle_map_per_layer makes per-filament assignments gap-free", "[MultiNozzle][H2C][Dynamic]")
{
SECTION("gaps inherit the last used nozzle, entries on used layers stay untouched") {
@@ -13,6 +13,7 @@
#include <boost/filesystem.hpp>
#include <nlohmann/json.hpp>
#include <chrono>
#include <fstream>
#include <string>
@@ -48,6 +49,18 @@ const char* const CLOUD_PLUGIN_SOURCE = R"PY(# /// script
# version = "1.0"
# ///
print('ok')
import orca
class stubscript(orca.script.ScriptPluginCapabilityBase):
def get_name(self):
return "stubscript"
def execute(self):
return orca.ExecutionResult.success("Stub orca script.")
@orca.plugin
class stubpackage(orca.base):
def register_capabilities(self):
orca.register_capability(stubscript)
)PY";
} // namespace
@@ -152,4 +165,31 @@ TEST_CASE("cloud metadata refresh preserves a plugin's stored config", "[PluginC
reloaded.load();
REQUIRE(reloaded.has_config(id));
CHECK(reloaded.get_config(id)->config == configured);
// A local package can remain after the cloud subscription disappears. The cloud identity is
// retained for diagnosis, but the orphaned state must suppress update availability until the
// plugin is returned by a later cloud refresh.
PluginDescriptor orphaned_record = cloud_record;
orphaned_record.cloud->orphaned = true;
orphaned_record.cloud->update_available = true;
manager.update_cloud_metadata({orphaned_record});
const PluginDescriptor orphaned = find_by_uuid();
REQUIRE(orphaned.cloud.has_value());
CHECK(orphaned.cloud->orphaned);
CHECK_FALSE(orphaned.has_error());
CHECK(orphaned.get_update_status() == PluginUpdateStatus::Normal);
// Orphaned is informational only: the local package must remain loadable and usable.
std::string load_error;
manager.load_plugin(uuid, /*skip_deps=*/true);
REQUIRE(manager.wait_for_plugin_load(uuid, std::chrono::seconds(120), load_error));
INFO("load error: " << load_error);
CHECK(load_error.empty());
CHECK(manager.is_plugin_loaded(uuid));
CHECK(manager.unload_plugin(uuid));
// Seeing the plugin in a subsequent cloud response clears the orphaned marker.
manager.update_cloud_metadata({cloud_record});
CHECK_FALSE(find_by_uuid().cloud->orphaned);
}