diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 66c1e9620a..c21edd9ac6 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -1354,10 +1354,35 @@ StringObjectException Print::validate(std::vector *warnin for (unsigned int e : extruders) used_filaments_0b.push_back((int)e); + // A mixed filament is blended at the nozzle by its component toolheads, and a + // parallel mode is already using those toolheads to print copies or mirrors, so + // the two cannot run at once -- regardless of where the components are routed. + // + // Runs before BOTH rules below. Against the routing rule it would otherwise be + // reported as merely unrouted: mixed slots are kept at the tail of the filament + // arrays, past the physical filament count, so on a printer whose logical extruder + // count equals that count they fall outside physical_extruder_map and resolve to no + // head. Against the multi-color rule, a mixed slot plus any second filament trips + // the >1 gate and earns a lecture about gantry topology the user never configured. + // Being unsupported outright, this dominates both. + { + const auto& is_mixed = m_config.filament_is_mixed.values; + if (std::any_of(used_filaments_0b.begin(), used_filaments_0b.end(), + [&](int slot) { return slot >= 0 && slot < (int) is_mixed.size() && is_mixed[slot]; })) { + StringObjectException err; + // "Mixed filament" is the term the rest of the UI uses -- the button that + // creates one, and the sibling refusal for the wipe tower filament. + err.string = L("Mixed filaments are not supported in IMEX parallel modes. " + "Switch this plate to Primary mode."); + err.object = m_objects.front(); + return err; + } + } + // Multi-color has the more specific rule and its remedies are self-contained // (an MMU manifold sharing one head cannot be fixed by switching mode), so it runs - // first: validate() returns on the first error, and the routing block below would - // otherwise mask it with advice that leads to this error on the next slice. + // ahead of the routing block below, which would otherwise mask it with advice that + // leads to this error on the next slice. validate() returns on the first error. if (used_filaments_0b.size() > 1) { const std::string reason = imex_multicolor_block_reason( parallel_mode, @@ -1369,24 +1394,6 @@ StringObjectException Print::validate(std::vector *warnin return { reason }; } - // A blended filament is mixed at the nozzle by its component toolheads. A - // parallel mode is already using those toolheads to print copies or mirrors, so - // the two cannot run at once -- regardless of where the components are routed. - // Checked BEFORE the routing rule below, which would otherwise report a blended - // slot as simply unrouted: mixed slots sit past the end of physical_extruder_map - // (it has one entry per physical extruder), so they resolve to no head at all. - { - const auto& is_mixed = m_config.filament_is_mixed.values; - if (std::any_of(used_filaments_0b.begin(), used_filaments_0b.end(), - [&](int slot) { return slot >= 0 && slot < (int) is_mixed.size() && is_mixed[slot]; })) { - StringObjectException err; - err.string = L("Blended filaments are not supported in IMEX parallel modes. " - "Switch this plate to Primary mode."); - err.object = m_objects.front(); - return err; - } - } - // The IMEX Primary tool prints the sliced paths directly, so it can only use a // filament the printer's physical_extruder_map routes to it. The ghost filament // picker already enforces this for the secondary tools; the primary's filament @@ -1425,10 +1432,12 @@ StringObjectException Print::validate(std::vector *warnin std::string routed_list; for (int head : routed_heads) routed_list += (routed_list.empty() ? "T" : ", T") + std::to_string(head); - // Reachable when the profile declares IMEX modes but no physical_extruder_map: - // validate() reads the raw config, and the PrintConfig default is a single - // entry, so every slot past the first falls outside it. Without this the - // sentence ends in a dangling "on .". + // Print::apply() normalises physical_extruder_map through + // effective_physical_extruder_map (PrintApply.cpp) before validate() runs, so + // an unauthored map arrives here as the identity and every slot is in range. + // What is left is a printer with more filaments than logical extruders, where + // the tail slots fall outside the map. Narrow, but without this the sentence + // ends in a dangling "on .". if (routed_list.empty()) routed_list = L("no configured tool"); diff --git a/tests/fff_print/test_multifilament.cpp b/tests/fff_print/test_multifilament.cpp index 78ef687783..24f5185d8f 100644 --- a/tests/fff_print/test_multifilament.cpp +++ b/tests/fff_print/test_multifilament.cpp @@ -980,20 +980,28 @@ TEST_CASE("An IMEX plate whose filament never routes to the Primary tool is bloc CHECK(err.string.find("T2") != std::string::npos); // where the filament actually lives } -// A blended filament is mixed at the nozzle by its component toolheads, which a parallel +// A mixed filament is blended at the nozzle by its component toolheads, which a parallel // mode is already using to print copies. Unsupported regardless of where the components // route, so this must refuse even though component filament 1 sits on the declared primary -// T0 -- and it must refuse with the blended message, not the routing one. Mixed slots sit -// past the end of physical_extruder_map, so the routing rule would call them unrouted. -TEST_CASE("An IMEX plate using a blended filament is blocked", "[MultiFilament][IMEX]") +// T0 -- and it must refuse with the mixed message, not the routing one. Mixed slots normally +// sit past the end of physical_extruder_map, so the routing rule would call them unrouted. +TEST_CASE("An IMEX plate using a mixed filament is blocked", "[MultiFilament][IMEX]") { DynamicPrintConfig config = multifilament_config(8); imex_7x4_printer(config); all_regions_on_filament(config, 8); config.set_deserialize_strict({ - { "imex_parallel_mode", "copy" }, - { "filament_is_mixed", "0,0,0,0,0,0,0,1" }, - { "filament_mixed_components", ";;;;;;;1,5" }, + { "imex_parallel_mode", "copy" }, + { "filament_is_mixed", "0,0,0,0,0,0,0,1" }, + { "filament_mixed_components", ";;;;;;;1,5" }, + // The mixed arrays run parallel to filament_colour and must be sized to the filament + // count (see test_mixed_filament.cpp). validate() returns before the other five are + // read, but that is a property of where the rule sits, not something to rely on. + { "filament_mixed_sublayer_ratios", ";;;;;;;" }, + { "filament_mixed_gradient", "0,0,0,0,0,0,0,0" }, + { "filament_mixed_gradient_range", ";;;;;;;" }, + { "filament_mixed_gradient_curve", ";;;;;;;" }, + { "filament_mixed_gradient_per_part", "0,0,0,0,0,0,0,0" }, }); std::vector meshes; @@ -1008,7 +1016,83 @@ TEST_CASE("An IMEX plate using a blended filament is blocked", "[MultiFilament][ const StringObjectException err = print.validate(&warnings); REQUIRE_FALSE(err.string.empty()); - CHECK(err.string.find("Blended filaments") != std::string::npos); + CHECK(err.string.find("Mixed filaments") != std::string::npos); +} + +// Filament 8 (slot 7) is the blend, filament 1 (slot 0) is ordinary, so used_filaments is > 1 +// and the multi-color rule's gate opens too. This pins that the mixed rule still wins: with the +// two the other way round the user is told the mode's active tools all sit on one gantry -- +// a lecture about a multi-color print they never asked for -- and never learns the blend is +// the problem. +// +// The second half is what keeps this honest. The multi-color rule only fires here because this +// fixture's `copy` mode is degenerate (imex_tools_per_gantry defaults to 2, so "0:P,1:C" puts +// both tools on gantry 0). Give the mode a Span tool and it returns nothing, and this test would +// pass under EITHER ordering while appearing to guard it. So assert the rule is actually armed. +TEST_CASE("A mixed filament outranks the multi-color rule on the same plate", + "[MultiFilament][IMEX][Regression]") +{ + // Two cubes, offset: make_cube() is corner-at-origin, so identical meshes would be exactly + // coincident. validate() returns from the IMEX block before any geometry check today, but a + // future check landing earlier would fail this test for a reason it is not about. + const auto build = [](bool blend_slot_8) { + DynamicPrintConfig config = multifilament_config(8); + imex_7x4_printer(config); + all_regions_on_filament(config, 8); + config.set_deserialize_strict({ + { "imex_parallel_mode", "copy" }, + { "filament_is_mixed", "0,0,0,0,0,0,0,1" }, + { "filament_mixed_components", ";;;;;;;1,5" }, + // The mixed arrays run parallel to filament_colour and must be sized to the filament + // count (see test_mixed_filament.cpp). validate() returns before the other five are + // read, but that is a property of where the rule sits, not something to rely on. + { "filament_mixed_sublayer_ratios", ";;;;;;;" }, + { "filament_mixed_gradient", "0,0,0,0,0,0,0,0" }, + { "filament_mixed_gradient_range", ";;;;;;;" }, + { "filament_mixed_gradient_curve", ";;;;;;;" }, + { "filament_mixed_gradient_per_part", "0,0,0,0,0,0,0,0" }, + }); + if (!blend_slot_8) + config.set_deserialize_strict({ { "filament_is_mixed", "0,0,0,0,0,0,0,0" } }); + return config; + }; + const std::vector> overrides{ + { { "extruder", "8" } }, { { "extruder", "1" } }, + }; + const auto validate_plate = [&](const DynamicPrintConfig& config, Slic3r::Print& print, + Slic3r::Model& model) { + std::vector meshes; + meshes.push_back(cube(20)); + TriangleMesh second = cube(20); + second.translate(30.0, 0.0, 0.0); + meshes.push_back(second); + init_print(std::move(meshes), print, model, config, &overrides, false); + std::vector warnings; + return print.validate(&warnings); + }; + + // The multi-color rule IS armed for this plate -- without that, the check below proves nothing. + { + Slic3r::Model model; + Slic3r::Print print; + const StringObjectException err = validate_plate(build(false), print, model); + REQUIRE_FALSE(err.string.empty()); + CHECK(err.string.find("Multi-color") != std::string::npos); + } + + // With the blend present the mixed rule takes precedence over it. + { + Slic3r::Model model; + Slic3r::Print print; + const StringObjectException err = validate_plate(build(true), print, model); + REQUIRE_FALSE(err.string.empty()); + CHECK(err.string.find("not supported in IMEX parallel modes") != std::string::npos); + CHECK(err.string.find("Multi-color") == std::string::npos); + // The two rules differ in more than wording: the mixed path attaches an object (for the + // notification's "Jump to" link), the multi-color path returns none. Pins which fired + // independently of the message text. + CHECK(err.object == print.objects().front()); + } } // Guard rail: the block must not fire on a well-formed plate. Filament 1 (slot 0) routes to