From a936081f52fc9fa55b75fb294b6a4006d09ae8dc Mon Sep 17 00:00:00 2001 From: SoftFever Date: Tue, 12 May 2026 21:52:51 +0800 Subject: [PATCH] collapse_printer_variants_to_extruders for bbl profiles --- src/libslic3r/Preset.cpp | 92 ++++++++++++++++++++++ tests/libslic3r/test_config.cpp | 130 ++++++++++++++++++++++++++++++++ 2 files changed, 222 insertions(+) diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 942be33eb9..b84c7d4c1d 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -225,9 +225,101 @@ static const std::unordered_map pre_family_model_map { }}; +// Orca: BBL printer profiles encode a per-(extruder × nozzle_volume_type) variant matrix, +// producing arrays of size n_extruders * n_variants_per_extruder for keys in +// printer_options_with_variant_1/_2. OrcaSlicer keeps one machine setting slot per +// physical extruder, so collapse these arrays in memory and intentionally keep the +// Standard flow variant for each physical extruder. Profile JSON on disk is untouched, +// so vendor profiles sync from Bambu Studio without edits. +// +// extruder_variant_list (one entry per physical extruder, each a comma-separated list of +// supported variants for that extruder) is the source of truth — a child profile may +// override printer_extruder_variant with the full matrix while inheriting an +// already-flattened printer_extruder_id from a parent this function previously processed. +static void collapse_printer_variants_to_extruders(DynamicPrintConfig& config) +{ + auto* variant_opt = config.option("printer_extruder_variant"); + auto* nd_opt = config.option("nozzle_diameter"); + if (!variant_opt || !nd_opt) + return; + + const size_t n_extruders = nd_opt->values.size(); + const size_t orig_variants = variant_opt->values.size(); + if (n_extruders == 0 || orig_variants <= n_extruders) + return; + + auto* variant_list_opt = config.option("extruder_variant_list"); + if (!variant_list_opt || variant_list_opt->values.size() != n_extruders) + return; + + // Use cumulative offsets, not name search: symmetric printers (H2D) may have + // identical variant strings on multiple extruders and a name search would collapse + // them all to the first match. + const std::string standard_flow = get_nozzle_volume_type_string(NozzleVolumeType::nvtStandard); + std::vector keep; + keep.reserve(n_extruders); + int cursor = 0; + for (size_t e = 0; e < n_extruders; ++e) { + std::vector variants; + boost::split(variants, variant_list_opt->values[e], boost::is_any_of(","), boost::token_compress_on); + int variant_count = 0; + int standard_offset = -1; + for (auto& v : variants) { + boost::trim(v); + if (v.empty()) + continue; + if (standard_offset < 0 && (v == standard_flow || boost::ends_with(v, " " + standard_flow))) + standard_offset = variant_count; + ++variant_count; + } + if (variant_count == 0) + return; + keep.push_back(cursor + (standard_offset >= 0 ? standard_offset : 0)); + cursor += variant_count; + } + if (size_t(cursor) != orig_variants) + return; + + auto pick_with_stride = [&](const std::string& key, int stride) { + ConfigOption* opt = config.option(key); + if (!opt || !opt->is_vector()) + return; + auto* vec = static_cast(opt); + // Skip arrays not sized to the full variant grid; replace_nil_and_resize (called + // later from extend_default_config_length) handles those. + if (vec->size() != orig_variants * size_t(stride)) + return; + // In-place compaction is safe because keep[] is monotonically non-decreasing + // (cursor only advances), so read_idx >= write_idx for every assignment. + for (size_t e = 0; e < keep.size(); ++e) { + for (int s = 0; s < stride; ++s) { + vec->set_at(vec, e * stride + s, keep[e] * stride + s); + } + } + vec->resize(keep.size() * stride); + }; + + for (const auto& key : printer_options_with_variant_1) + pick_with_stride(key, 1); + for (const auto& key : printer_options_with_variant_2) + pick_with_stride(key, 2); + + // Rebuild rather than flatten: printer_extruder_id may have arrived parent-flattened, + // stretched by an earlier replace_nil_and_resize, or fresh from JSON. + if (auto* id_opt = config.option("printer_extruder_id")) { + id_opt->values.clear(); + id_opt->values.reserve(n_extruders); + for (size_t e = 1; e <= n_extruders; ++e) + id_opt->values.push_back(int(e)); + } +} + // 中间版本兼容性处理,如果是nil值,先改成default值,再进行扩展 void extend_default_config_length(DynamicPrintConfig& config, const bool set_nil_to_default, const DynamicPrintConfig& defaults) { + // Orca: flatten BBL variant arrays to one slot per physical extruder before sizing. + collapse_printer_variants_to_extruders(config); + constexpr int default_param_length = 1; int filament_variant_length = default_param_length; int process_variant_length = default_param_length; diff --git a/tests/libslic3r/test_config.cpp b/tests/libslic3r/test_config.cpp index 2bee0d7d11..1422bdf580 100644 --- a/tests/libslic3r/test_config.cpp +++ b/tests/libslic3r/test_config.cpp @@ -1,5 +1,6 @@ #include +#include "libslic3r/Preset.hpp" #include "libslic3r/PrintConfig.hpp" #include "libslic3r/PrintConfigConstants.hpp" #include "libslic3r/LocalesUtils.hpp" @@ -315,6 +316,135 @@ SCENARIO("update_non_diff_values_to_base_config preserves child vectors when chi } } +SCENARIO("extend_default_config_length collapses selectable nozzle flow variants to Standard", + "[Config][Variant]") { + GIVEN("A single-extruder BBL-style profile with Standard and High Flow variants") { + Slic3r::DynamicPrintConfig config; + + config.set_key_value("nozzle_diameter", new Slic3r::ConfigOptionFloats({0.4})); + config.set_key_value("extruder_type", new Slic3r::ConfigOptionEnumsGeneric({Slic3r::etDirectDrive})); + config.set_key_value("extruder_variant_list", new Slic3r::ConfigOptionStrings({ + "Direct Drive Standard,Direct Drive High Flow" + })); + config.set_key_value("printer_extruder_id", new Slic3r::ConfigOptionInts({1, 1})); + config.set_key_value("printer_extruder_variant", new Slic3r::ConfigOptionStrings({ + "Direct Drive Standard", "Direct Drive High Flow" + })); + config.set_key_value("machine_max_speed_x", new Slic3r::ConfigOptionFloats({ + 500, 200, 200, 200 + })); + + WHEN("default config lengths are normalized") { + Slic3r::extend_default_config_length(config, false, {}); + + THEN("only the Standard nozzle flow variant remains") { + auto* variants = config.option("printer_extruder_variant"); + REQUIRE(variants->values == std::vector({ + "Direct Drive Standard" + })); + } + + THEN("Standard machine limits are kept") { + auto* speed_x = config.option("machine_max_speed_x"); + REQUIRE(speed_x->values == std::vector({500, 200})); + } + + THEN("printer extruder ids are rebuilt per physical extruder") { + auto* ids = config.option("printer_extruder_id"); + REQUIRE(ids->values == std::vector({1})); + } + } + } +} + +SCENARIO("extend_default_config_length collapses mixed physical extruder variants", + "[Config][Variant]") { + GIVEN("An X2D-style profile with one Direct Drive and one Bowden extruder") { + Slic3r::DynamicPrintConfig config; + + config.set_key_value("nozzle_diameter", new Slic3r::ConfigOptionFloats({0.4, 0.4})); + config.set_key_value("extruder_type", new Slic3r::ConfigOptionEnumsGeneric({ + Slic3r::etDirectDrive, Slic3r::etBowden + })); + config.set_key_value("extruder_variant_list", new Slic3r::ConfigOptionStrings({ + "Direct Drive Standard,Direct Drive High Flow", + "Bowden Standard,Bowden High Flow" + })); + config.set_key_value("printer_extruder_id", new Slic3r::ConfigOptionInts({1, 1, 2, 2})); + config.set_key_value("printer_extruder_variant", new Slic3r::ConfigOptionStrings({ + "Direct Drive Standard", "Direct Drive High Flow", + "Bowden Standard", "Bowden High Flow" + })); + config.set_key_value("retraction_length", new Slic3r::ConfigOptionFloats({ + 0.8, 0.8, 2.0, 2.0 + })); + config.set_key_value("machine_max_speed_e", new Slic3r::ConfigOptionFloats({ + 30, 30, 30, 30, 120, 120, 120, 120 + })); + + WHEN("default config lengths are normalized") { + Slic3r::extend_default_config_length(config, false, {}); + + THEN("one Standard variant is kept for each physical extruder") { + auto* variants = config.option("printer_extruder_variant"); + REQUIRE(variants->values == std::vector({ + "Direct Drive Standard", "Bowden Standard" + })); + } + + THEN("per-extruder retraction settings are aligned") { + auto* retraction = config.option("retraction_length"); + REQUIRE(retraction->values == std::vector({0.8, 2.0})); + } + + THEN("stride-2 machine limits are aligned") { + auto* speed_e = config.option("machine_max_speed_e"); + REQUIRE(speed_e->values == std::vector({30, 30, 120, 120})); + } + } + } +} + +SCENARIO("extend_default_config_length collapses symmetric physical extruder variants", + "[Config][Variant]") { + GIVEN("An H2D-style profile with matching Direct Drive extruder variant lists") { + Slic3r::DynamicPrintConfig config; + + config.set_key_value("nozzle_diameter", new Slic3r::ConfigOptionFloats({0.4, 0.4})); + config.set_key_value("extruder_type", new Slic3r::ConfigOptionEnumsGeneric({ + Slic3r::etDirectDrive, Slic3r::etDirectDrive + })); + config.set_key_value("extruder_variant_list", new Slic3r::ConfigOptionStrings({ + "Direct Drive Standard,Direct Drive High Flow", + "Direct Drive Standard,Direct Drive High Flow" + })); + config.set_key_value("printer_extruder_id", new Slic3r::ConfigOptionInts({1, 1, 2, 2})); + config.set_key_value("printer_extruder_variant", new Slic3r::ConfigOptionStrings({ + "Direct Drive Standard", "Direct Drive High Flow", + "Direct Drive Standard", "Direct Drive High Flow" + })); + config.set_key_value("machine_max_speed_e", new Slic3r::ConfigOptionFloats({ + 50, 50, 60, 60, 70, 70, 80, 80 + })); + + WHEN("default config lengths are normalized") { + Slic3r::extend_default_config_length(config, false, {}); + + THEN("the Standard variant is kept for each physical extruder, not the first matching name globally") { + auto* variants = config.option("printer_extruder_variant"); + REQUIRE(variants->values == std::vector({ + "Direct Drive Standard", "Direct Drive Standard" + })); + } + + THEN("stride-2 machine limits are kept from each extruder's Standard slot") { + auto* speed_e = config.option("machine_max_speed_e"); + REQUIRE(speed_e->values == std::vector({50, 50, 70, 70})); + } + } + } +} + // SCENARIO("DynamicPrintConfig JSON serialization", "[Config]") { // WHEN("DynamicPrintConfig is serialized and deserialized") { // auto now = std::chrono::high_resolution_clock::now();