From 0c3d7c6ed1978c7f863c68b27b649b92ad9f23e6 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Sat, 22 Aug 2026 21:14:08 +0800 Subject: [PATCH] Expand mixed slots in by-object filament bookkeeping --- src/libslic3r/Print.cpp | 29 ++++++++-- tests/fff_print/test_mixed_filament.cpp | 76 +++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 6 deletions(-) diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index b50a6b9d43..3cd42eb96e 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -5,6 +5,7 @@ #include "Brim.hpp" #include "ClipperUtils.hpp" #include "Extruder.hpp" +#include "FilamentMixer.hpp" #include "Flow.hpp" #include "Geometry/ConvexHull.hpp" #include "I18N.hpp" @@ -2601,28 +2602,38 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) if (this->config().print_sequence == PrintSequence::ByObject) { // Order object instances for sequential print. print_object_instances_ordering = sort_object_instances_by_model_order(*this); + // A mixed slot is virtual; only its components reach a nozzle. These per-object orderings + // are unsorted (no resolve_mixed_filaments), so expand the slots here for the grouping, the + // unprintable sets and the slice-used lists. No-op without mixed filaments. + // Orca: the slice-used lists stay sourced from these expanded lists rather than from the + // sorted orderings (which may add the wipe-tower filament or seed dontcare layers + // differently), so prints without mixed filaments keep their used-filament set; the + // first-layer set therefore lists every component of a mixed slot, not just the one layer 0 + // resolves to. + const auto &is_mixed = m_config.filament_is_mixed.values; + const auto &comp_strs = m_config.filament_mixed_components.values; + const bool has_mixed = has_any_mixed_filament(is_mixed); std::vector first_layer_used_filaments; - std::vector used_mixed_filaments; std::vector> all_filaments; for (print_object_instance_sequential_active = print_object_instances_ordering.begin(); print_object_instance_sequential_active != print_object_instances_ordering.end(); ++print_object_instance_sequential_active) { tool_ordering = ToolOrdering(*(*print_object_instance_sequential_active)->print_object, initial_extruder_id); for (size_t idx = 0; idx < tool_ordering.layer_tools().size(); ++idx) { - auto& layer_filament = tool_ordering.layer_tools()[idx].extruders; + auto layer_filament = tool_ordering.layer_tools()[idx].extruders; + if (has_mixed) + layer_filament = expand_mixed_filaments(layer_filament, is_mixed, comp_strs); all_filaments.emplace_back(layer_filament); if (idx == 0) first_layer_used_filaments.insert(first_layer_used_filaments.end(), layer_filament.begin(), layer_filament.end()); } - used_mixed_filaments.insert(used_mixed_filaments.end(), - tool_ordering.used_mixed_filaments().begin(), tool_ordering.used_mixed_filaments().end()); } sort_remove_duplicates(first_layer_used_filaments); - sort_remove_duplicates(used_mixed_filaments); auto used_filaments = collect_sorted_used_filaments(all_filaments); this->set_slice_used_filaments(first_layer_used_filaments,used_filaments); - this->set_slice_used_mixed_filaments(used_mixed_filaments); auto physical_unprintables = this->get_physical_unprintable_filaments(used_filaments); auto geometric_unprintables = this->get_geometric_unprintable_filaments(); + if (has_mixed) + expand_mixed_slots_in_unprintables(geometric_unprintables, is_mixed, comp_strs); auto filament_unprintable_volumes = this->get_filament_unprintable_flow(used_filaments); // Selector (per-layer regroup) prints skip the static grouping: their print-wide result // is stitched from the per-object plans after the ordering loop below. @@ -2674,6 +2685,7 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) std::vector> nozzle_map_per_layer; std::vector> stitched_layer_filaments; print_object_instance_sequential_active = print_object_instances_ordering.begin(); + std::vector used_mixed_filaments; for (; print_object_instance_sequential_active != print_object_instances_ordering.end(); ++print_object_instance_sequential_active) { const PrintObject *print_object = (*print_object_instance_sequential_active)->print_object; if (dynamic_reorder) { @@ -2705,10 +2717,15 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) if (!tool_ordering.layer_tools().empty()) seq_mixed_resolution[print_object->id()] = tool_ordering.layer_tools().front().mixed_filament_resolution; } + // Only sorted orderings have run resolve_mixed_filaments, so only they know which + // mixed slots actually print. + append(used_mixed_filaments, tool_ordering.used_mixed_filaments()); if ((initial_extruder_id = tool_ordering.first_extruder()) != static_cast(-1)) { append(printExtruders, tool_ordering.tools_for_layer(layers_to_print.front().first).extruders); } } + sort_remove_duplicates(used_mixed_filaments); + this->set_slice_used_mixed_filaments(used_mixed_filaments); if (dynamic_reorder && m_objects.size() > 1) { // Stitch the per-object plans into one print-wide selector result. A single-object // sequential print publishes (and writes back) from its own ordering instead: the diff --git a/tests/fff_print/test_mixed_filament.cpp b/tests/fff_print/test_mixed_filament.cpp index 4f4d914cc2..84a3270605 100644 --- a/tests/fff_print/test_mixed_filament.cpp +++ b/tests/fff_print/test_mixed_filament.cpp @@ -1,6 +1,7 @@ #include #include "libslic3r/GCode/ToolOrdering.hpp" +#include "libslic3r/MultiNozzleUtils.hpp" #include "libslic3r/Print.hpp" #include "test_helpers.hpp" @@ -135,3 +136,78 @@ TEST_CASE("Whole-layer mixing emits only the nominal layer height", "[MixedFilam CHECK(gc.find(";HEIGHT:0.12") == std::string::npos); CHECK(gc.find(";HEIGHT:0.08") == std::string::npos); } + +TEST_CASE("By-object prints without mixed filaments keep their used-filament set", "[MixedFilament]") +{ + // Regression guard for the mixed gate: with no mixed slot the by-object bookkeeping must + // be untouched by this change. Object 2 prints with filament 2, so both filaments are used + // and no mixed filament is reported. + DynamicPrintConfig config = multifilament_config(2, {{"print_sequence", "by object"}}); + const std::vector> overrides{ {}, { {"extruder", "2"} } }; + + Print print; + Model model; + init_print(std::vector{cube(20), cube(20)}, print, model, config, &overrides); + REQUIRE(print.objects().size() == 2); + print.process(); + + CHECK(print.get_slice_used_filaments(false) == std::vector{0, 1}); + CHECK(print.get_slice_used_filaments(true) == std::vector{0, 1}); + CHECK(print.get_slice_used_mixed_filaments().empty()); +} + +TEST_CASE("By-layer prints record a mixed slot's components and the slot itself", "[MixedFilament]") +{ + // Control for the by-object case below: the by-layer path publishes the physical + // components (0-based 0 and 1) as used filaments and the mixed slot (config index 2) as + // a used mixed filament. By-object prints must report exactly the same. + Print print; + Model model; + init_print({cube(20)}, print, model, mixed_config(false)); + print.process(); + + CHECK(print.get_slice_used_filaments(false) == std::vector{0, 1}); + CHECK(print.get_slice_used_mixed_filaments() == std::vector{2}); +} + +TEST_CASE("By-object prints expand a mixed slot to its components in the slice bookkeeping", "[MixedFilament]") +{ + // Sequential prints build their filament lists from unsorted per-object orderings, which + // still carry the virtual slot (config index 2). The slice-used sets and the published + // grouping result must see the physical components 0 and 1 instead, and the slot itself + // must still be reported as a used mixed filament — exactly what the by-layer path yields. + DynamicPrintConfig config = mixed_config(false); + config.set_deserialize_strict({{"print_sequence", "by object"}}); + + Print print; + Model model; + init_print({cube(20), cube(20)}, print, model, config); + REQUIRE(print.objects().size() == 2); + print.process(); + + const std::vector components{0, 1}; + CHECK(print.get_slice_used_filaments(false) == components); + CHECK(print.get_slice_used_filaments(true) == components); + CHECK(print.get_slice_used_mixed_filaments() == std::vector{2}); + + auto group_result = print.get_layered_nozzle_group_result(); + REQUIRE(group_result != nullptr); + CHECK(group_result->get_used_filaments() == components); +} + +TEST_CASE("By-object G-code lists a mixed slot's components in the filament header", "[MixedFilament]") +{ + DynamicPrintConfig config = mixed_config(false); + config.set_deserialize_strict({{"print_sequence", "by object"}}); + + Print print; + Model model; + init_print({cube(20), cube(20)}, print, model, config); + const std::string gc = Slic3r::Test::gcode(print); + + REQUIRE(!gc.empty()); + // The header names the filaments that must be loaded (components 1 and 2, 1-based), + // never the virtual slot 3. + CHECK(gc.find("; filament: 1,2\n") != std::string::npos); + CHECK(gc.find("; filament: 3") == std::string::npos); +}