mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-10 17:21:10 +00:00
Refuse Span-mode colors on tools that cannot print them
Only the primary and its Span tools print a Span plate's colors; the other active tools replay them. The multicolor rule never checked where each color went, so a filament on a copying or mirroring tool, an unused one, or past the end of the physical extruder map was accepted. It is now refused, and the message names the tools that print colors and where the map sends the offending filament. The plate's warning badge also read the raw physical_extruder_map rather than the effective one slicing uses, so on printers that set no map it disagreed with the slicer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
3637a6d164
commit
d4d79fa328
@@ -8,6 +8,7 @@
|
||||
#include <sstream>
|
||||
#include <unordered_set>
|
||||
|
||||
#include <boost/format.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
|
||||
#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<int> 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 {};
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<ConfigOptionInt>("imex_tools_per_gantry");
|
||||
auto* pem_opt = printer_cfg.option<ConfigOptionInts>("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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user