diff --git a/src/libslic3r/IMEXHelpers.cpp b/src/libslic3r/IMEXHelpers.cpp index ec3319fb04..209c37dda5 100644 --- a/src/libslic3r/IMEXHelpers.cpp +++ b/src/libslic3r/IMEXHelpers.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include "libslic3r/ClipperUtils.hpp" @@ -167,6 +168,31 @@ std::string imex_multicolor_block_reason(const std::string& parallel_mode, "Primary mode, or open the printer settings IDEX/IQEX Modes editor and mark a " "tool on the primary's gantry as Span."); } + // Only the primary and its Span partners print the plate's colors; every other active head + // replays them. A color routed anywhere else -- a copying head, one the mode leaves inactive, + // or past the end of the map -- has no head of its own to print from. + std::set color_heads{ primary_physical }; + for (const auto& [phys, role] : parse_imex_active_tools(active_tools_str)) + if (phys / tpg == primary_gantry && role == ImexRole::Span) + color_heads.insert(phys); + for (int filament : used_filaments_0b) { + const int phys = pem.values.empty() ? filament + : (filament >= 0 && filament < (int) pem.values.size()) ? pem.values[filament] : -1; + if (color_heads.count(phys)) + continue; + std::string tools; + for (int head : color_heads) + tools += (tools.empty() ? "T" : "/T") + std::to_string(head); + // Filaments are numbered from 1 as the sidebar shows them, heads by their T index; the map + // decides which head a filament is on, so with MMU lanes the two numbers differ. + if (phys < 0) + return (boost::format(L("In this Span mode only %1% print colors, but filament %2% isn't routed to any " + "tool. Use filaments routed to %1%, or switch this plate to Primary mode.")) % + tools % (filament + 1)).str(); + return (boost::format(L("In this Span mode only %1% print colors, but filament %2% is routed to T%3%. Use " + "filaments routed to %1%, or switch this plate to Primary mode.")) % + tools % (filament + 1) % phys).str(); + } return {}; } diff --git a/src/libslic3r/IMEXHelpers.hpp b/src/libslic3r/IMEXHelpers.hpp index 96bf1e211c..645af85862 100644 --- a/src/libslic3r/IMEXHelpers.hpp +++ b/src/libslic3r/IMEXHelpers.hpp @@ -221,10 +221,12 @@ bool imex_suppresses_bare_toolchange(const std::string& parallel_mode, unsigned // // Multi-color in a parallel mode requires the firmware to swap tools mid-print on the // primary gantry while the slaved gantry follows automatically. That works only when -// every used filament has its own dedicated physical head (no MMU lane sharing) AND -// the active mode definition explicitly marks a Span tool on the primary's gantry -// (Span declares "this tool is the within-gantry multicolor partner of Primary"; it -// disambiguates the multicolor topology from independent same-gantry copies). +// every used filament has its own dedicated physical head (no MMU lane sharing), the +// active mode definition explicitly marks a Span tool on the primary's gantry (Span +// declares "this tool is the within-gantry multicolor partner of Primary"; it +// disambiguates the multicolor topology from independent same-gantry copies), AND every +// used filament routes to the primary or one of its Span tools, the only heads that print +// colors of their own. // // Catches: // - IDEX (1 tool per gantry): primary's gantry can never carry a Span partner → blocked @@ -235,7 +237,8 @@ bool imex_suppresses_bare_toolchange(const std::string& parallel_mode, unsigned // - IQEX 4-tool independent copies (T0:P,T1:C,T2:M,T3:M): no Span declared, T1 is an // independent copy → blocked // - IQEX paired-gantry multicolor (T0:P,T1:S,T2:M,T3:M): Span on T1 declares the -// multicolor partner, no MMU sharing → ALLOWED +// multicolor partner, no MMU sharing → ALLOWED for colors on T0 and T1; a color +// routed to T2, T3 or past the end of the map → blocked // // Returns empty string for: non-IMEX (empty parallel_mode), Primary mode, single-color // prints, or any configuration where multi-color is physically supportable. diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index dab20fb356..8afcbcd389 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -1273,15 +1273,16 @@ bool PartPlate::has_imex_multimaterial_conflict() const const std::string mode = get_effective_imex_mode(); if (mode == kImexPrimaryMode) return false; - // Both keys bail rather than defaulting through imex_cfg_int(), deliberately, and for the same - // reason: this reports a routing CONFLICT, so it must run on the printer's real configuration - // or not at all. Without physical_extruder_map there is no mapping to check; with a defaulted + // Bails rather than defaulting through imex_cfg_int(), deliberately: this reports a routing + // CONFLICT, so it must run on the printer's real configuration or not at all. With a defaulted // grid the conflict would be computed against a shape the printer does not have. A false // conflict warning is worse than staying quiet, so an absent key means "no answer" here -- // unlike the geometry paths, where a defaulted value still describes a drawable bed. auto* tpg_opt = printer_cfg.option("imex_tools_per_gantry"); - auto* pem_opt = printer_cfg.option("physical_extruder_map"); - if (!tpg_opt || !pem_opt) return false; + if (!tpg_opt) return false; + // The map Print::apply() hands validate(): a printer that authors none gets the identity, + // which the raw key's single default entry does not describe. + const ConfigOptionInts pem = effective_physical_extruder_map(*pb); // Resolve the active mode's tools string from the printer config. Print::validate() // resolves the same name the same way and keeps going on an empty result -- the mixed @@ -1311,7 +1312,7 @@ bool PartPlate::has_imex_multimaterial_conflict() const return true; } - return !imex_multicolor_block_reason(mode, active_tools_str, tpg_opt->value, used_0b, *pem_opt).empty(); + return !imex_multicolor_block_reason(mode, active_tools_str, tpg_opt->value, used_0b, pem).empty(); } void PartPlate::render_imex_zones(bool force_default_color) diff --git a/tests/libslic3r/test_imex_helpers.cpp b/tests/libslic3r/test_imex_helpers.cpp index cdc7fabaca..26105cc360 100644 --- a/tests/libslic3r/test_imex_helpers.cpp +++ b/tests/libslic3r/test_imex_helpers.cpp @@ -263,6 +263,34 @@ TEST_CASE("imex_multicolor_block_reason - IQEX paired-gantry multicolor allowed REQUIRE(imex_multicolor_block_reason("copy", "0:P,1:S,2:M,3:M", 2, {0, 1}, pem).empty()); } +TEST_CASE("A Span mode refuses a color routed to a tool outside the primary's gantry pair", "[IMEX]") { + // 2x2 IQEX mc-copy: T0 (Primary) and T1 (Span) print the plate's colors; T2 and T3 replay + // them on the other gantry. A third painted filament routed to T3 has no head of its own. + auto pem = make_pem({0, 1, 2, 3}); + const std::string reason = imex_multicolor_block_reason("mc-copy", "0:P,1:S,2:C,3:C", 2, {0, 1, 3}, pem); + // The message names the color tools and where the map sends the offending filament (1-based). + REQUIRE_THAT(reason, Catch::Matchers::ContainsSubstring("only T0/T1 print colors, but filament 4 is routed to T3")); + REQUIRE_THAT(imex_multicolor_block_reason("mc-copy", "0:P,1:S,2:C,3:C", 2, {0, 4}, pem), + Catch::Matchers::ContainsSubstring("filament 5 isn't routed to any tool")); + // A head the mode leaves inactive is no better, and neither is no head at all. + REQUIRE_FALSE(imex_multicolor_block_reason("mc-copy", "0:P,1:S,3:C", 2, {0, 2}, pem).empty()); + REQUIRE_FALSE(imex_multicolor_block_reason("mc-copy", "0:P,1:S,2:C,3:C", 2, {0, 4}, pem).empty()); + // Being on the primary's gantry is not enough; the head has to be a Span partner. + REQUIRE_FALSE(imex_multicolor_block_reason("mc-copy", "0:P,1:S,2:C,3:C,4:C,5:C", 3, {0, 2}, + make_pem({0, 1, 2, 3, 4, 5})).empty()); + // Routing goes through the map: with AFC lanes on head 0, filament 4 lands on the Span head + // and filament 5 on a copying one. + const auto afc = make_pem({0, 0, 0, 0, 1, 2, 3}); + REQUIRE(imex_multicolor_block_reason("mc-copy", "0:P,1:S,2:C,3:C", 2, {0, 4}, afc).empty()); + REQUIRE_THAT(imex_multicolor_block_reason("mc-copy", "0:P,1:S,2:C,3:C", 2, {0, 5}, afc), + Catch::Matchers::ContainsSubstring("filament 6 is routed to T2")); + // The pair's own two colors stay allowed, and so does a third Span partner where the + // gantry carries one. + REQUIRE(imex_multicolor_block_reason("mc-copy", "0:P,1:S,2:C,3:C", 2, {0, 1}, pem).empty()); + REQUIRE(imex_multicolor_block_reason("mc-copy", "0:P,1:S,2:S,3:C,4:C,5:C", 3, {0, 1, 2}, + make_pem({0, 1, 2, 3, 4, 5})).empty()); +} + TEST_CASE("imex_multicolor_block_reason - MMU lane sharing blocks multi-color", "[IMEX]") { // pem maps both filament 0 and filament 1 to the same physical head 0 — that's // an MMU/AFC manifold. IMEX parallel modes can't slave the secondary gantry