diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 2f8f0f90fd..f42591b6be 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -6101,7 +6101,17 @@ LayerResult GCode::process_layer( const bool island_level_ordering = print.config().print_sequence != PrintSequence::ByObject && single_object_instance_idx == size_t(-1) && print.config().print_order != PrintOrder::AsObjectList; - for (unsigned int filament_id : layer_tools.extruders) { + // A mixed-color slot is absent from layer_tools.extruders by design: resolve_mixed_filaments() + // replaced it with its physical components. Its geometry is still keyed under the slot in + // by_extruder though, and the sublayer emitter looks the plan up by slot id, so append the + // slots here. Appended (not merged) so the existing order is untouched, and empty for every + // configuration without sublayer splitting. + std::vector plan_filaments = layer_tools.extruders; + for (const auto &grp : layer_tools.mixed_sub_layer_groups) + if (std::find(plan_filaments.begin(), plan_filaments.end(), grp.mixed_slot_0based) == plan_filaments.end()) + plan_filaments.push_back(grp.mixed_slot_0based); + + for (unsigned int filament_id : plan_filaments) { auto objects_by_extruder_it = by_extruder.find(filament_id); if (objects_by_extruder_it == by_extruder.end()) continue; @@ -6282,8 +6292,22 @@ LayerResult GCode::process_layer( } if (print.config().print_sequence == PrintSequence::ByLayer && m_enable_exclude_object && print.config().support_object_skip_flush.value) { - std::vector filament_instances_id; - for (InstanceToPrint &instance : filament_to_print_instances[extruder_id].first) filament_instances_id.emplace_back(instance.label_object_id); + std::set all_label_ids; + for (InstanceToPrint &instance : filament_to_print_instances[extruder_id].first) + all_label_ids.insert(instance.label_object_id); + // This extruder may also be printing sub-layers on behalf of a mixed slot, whose + // instances live under the slot id. Their labels belong in the same skip set, or + // exclude-object would not skip that geometry. + for (const auto &grp : layer_tools.mixed_sub_layer_groups) + for (unsigned int comp : grp.components_0based) + if (comp == extruder_id) { + auto mit = filament_to_print_instances.find(grp.mixed_slot_0based); + if (mit != filament_to_print_instances.end()) + for (const InstanceToPrint &inst : mit->second.first) + all_label_ids.insert(inst.label_object_id); + break; + } + std::vector filament_instances_id(all_label_ids.begin(), all_label_ids.end()); m_filament_instances_code = _encode_label_ids_to_base64(filament_instances_id); } diff --git a/tests/fff_print/CMakeLists.txt b/tests/fff_print/CMakeLists.txt index 43afd4281d..fc46bb8fdc 100644 --- a/tests/fff_print/CMakeLists.txt +++ b/tests/fff_print/CMakeLists.txt @@ -15,6 +15,7 @@ add_executable(${_TEST_NAME}_tests test_perimeters.cpp test_print.cpp test_printobject.cpp + test_mixed_filament.cpp test_skirt_brim.cpp test_slicing_pipeline_hook.cpp test_support_material.cpp diff --git a/tests/fff_print/test_mixed_filament.cpp b/tests/fff_print/test_mixed_filament.cpp new file mode 100644 index 0000000000..4f4d914cc2 --- /dev/null +++ b/tests/fff_print/test_mixed_filament.cpp @@ -0,0 +1,137 @@ +#include + +#include "libslic3r/GCode/ToolOrdering.hpp" +#include "libslic3r/Print.hpp" + +#include "test_helpers.hpp" + +using namespace Slic3r; +using namespace Slic3r::Test; + +namespace { + +// Two physical filaments plus one mixed slot (config index 2, 1-based id 3) blending them 60/40. +// The mixed arrays are parallel to filament_colour and must be sized to the filament count. +// Note ConfigOptionBools deserializes on ',' while ConfigOptionStrings uses ';'. +DynamicPrintConfig mixed_config(bool sublayer_on, const char *ratios = "0.6,0.4") +{ + DynamicPrintConfig config = multifilament_config(3); + config.set_deserialize_strict({ + {"filament_is_mixed", "0,0,1"}, + {"filament_mixed_components", ";;1,2"}, + {"filament_mixed_sublayer_ratios", std::string(";;") + ratios}, + {"filament_mixed_gradient", "0,0,0"}, + {"filament_mixed_gradient_range", ";;"}, + {"filament_mixed_gradient_curve", ";;"}, + {"filament_mixed_gradient_per_part","0,0,0"}, + {"enable_mixed_color_sublayer", sublayer_on ? "1" : "0"}, + // Assign every region role to the mixed slot so it actually participates in slicing. + {"outer_wall_filament_id", "3"}, + {"inner_wall_filament_id", "3"}, + {"sparse_infill_filament_id", "3"}, + {"internal_solid_filament_id", "3"}, + {"top_surface_filament_id", "3"}, + {"bottom_surface_filament_id", "3"}, + }); + return config; +} + +// Total sub-layer groups and per-layer DRR resolutions across the whole tool ordering. +void count_mixed(ToolOrdering &to, size_t &groups, size_t &resolutions) +{ + groups = resolutions = 0; + for (const LayerTools < : to.layer_tools()) { + groups += lt.mixed_sub_layer_groups.size(); + resolutions += lt.mixed_filament_resolution.size(); + } +} + +} // namespace + +TEST_CASE("enable_mixed_color_sublayer reaches the Print config", "[MixedFilament]") +{ + Print print; + Model model; + init_print({cube(20)}, print, model, mixed_config(true)); + + // The option lives in PrintConfig; if it did not survive Print::apply the slicer would + // silently fall back to the whole-layer path. + CHECK(print.config().enable_mixed_color_sublayer.value == true); + REQUIRE(print.config().filament_is_mixed.values.size() == 3); + CHECK(print.config().filament_is_mixed.values[2] == true); + REQUIRE(print.config().filament_mixed_components.values.size() == 3); + CHECK(print.config().filament_mixed_components.values[2] == "1,2"); +} + +TEST_CASE("Mixed filament splits layers into sub-layers when the option is on", "[MixedFilament]") +{ + Print print; + Model model; + init_print({cube(20)}, print, model, mixed_config(true)); + print.process(); + + ToolOrdering &to = const_cast(print.tool_ordering()); + REQUIRE(!to.layer_tools().empty()); + + size_t groups = 0, resolutions = 0; + count_mixed(to, groups, resolutions); + + INFO("layers=" << to.layer_tools().size() << " groups=" << groups); + CHECK(groups > 0); +} + +TEST_CASE("Mixed filament alternates whole layers when the option is off", "[MixedFilament]") +{ + Print print; + Model model; + init_print({cube(20)}, print, model, mixed_config(false)); + print.process(); + + ToolOrdering &to = const_cast(print.tool_ordering()); + REQUIRE(!to.layer_tools().empty()); + + size_t groups = 0, resolutions = 0; + count_mixed(to, groups, resolutions); + + // With splitting off the slot is realized by the deficit round-robin scheduler instead: + // no sub-layer groups, but a per-layer resolution to one physical component. + INFO("layers=" << to.layer_tools().size() << " resolutions=" << resolutions); + CHECK(groups == 0); + CHECK(resolutions > 0); +} + +TEST_CASE("Sub-layer splitting emits the scaled sub-heights into G-code", "[MixedFilament]") +{ + // layer_height 0.2 split 60/40 gives sub-layers of 0.12 and 0.08. The emitter reports the + // sub-height (not the nominal layer height) in the HEIGHT tag and scales flow to match. + DynamicPrintConfig config = mixed_config(true); + config.set_deserialize_strict({{"layer_height", "0.2"}, {"initial_layer_print_height", "0.2"}}); + + Print print; + Model model; + init_print({cube(20)}, print, model, config); + print.process(); + const std::string gc = Slic3r::Test::gcode(print); + + REQUIRE(!gc.empty()); + INFO("gcode bytes=" << gc.size()); + CHECK(gc.find(";HEIGHT:0.12") != std::string::npos); + CHECK(gc.find(";HEIGHT:0.08") != std::string::npos); +} + +TEST_CASE("Whole-layer mixing emits only the nominal layer height", "[MixedFilament]") +{ + DynamicPrintConfig config = mixed_config(false); + config.set_deserialize_strict({{"layer_height", "0.2"}, {"initial_layer_print_height", "0.2"}}); + + Print print; + Model model; + init_print({cube(20)}, print, model, config); + print.process(); + const std::string gc = Slic3r::Test::gcode(print); + + REQUIRE(!gc.empty()); + // No sub-layer split, so the 60/40 sub-heights must never appear. + CHECK(gc.find(";HEIGHT:0.12") == std::string::npos); + CHECK(gc.find(";HEIGHT:0.08") == std::string::npos); +}