mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-10 17:21:10 +00:00
fix(imex): refuse a plate whose filament cannot reach the mode's primary tool
The IMEX primary tool prints the sliced paths directly, so it can only load a filament that physical_extruder_map routes to it. The ghost filament picker enforces that for the secondary tools -- it offers only lanes whose pem entry equals that head -- but the primary's filament comes from the ordinary object filament selector, which has no IMEX awareness. Nothing detected the mismatch: collect_imex_warnings() computes the same condition and discards it into a display fallback, and the multi-color rule never examines it. Block it in Print::validate() via the existing imex_primary_tool_for_mode and imex_primary_logical_from_objects helpers. The message names the declared primary, the heads the plate's filaments actually live on, and any configured modes whose primary would work, and carries the object so the notification can offer a jump to it. Blocks rather than warns, matching the multi-color rule: the plate is not printable as configured, and where the routed head is also absent from the mode's active tools the 1st->2nd layer temperature branch skips it too, leaving that head at its initial-layer temperature for the whole job. The multi-color check now runs first. Its constraints -- an MMU manifold sharing one head, a single-gantry mode -- cannot be fixed by switching mode, so the more specific error should win rather than be masked by routing advice that leads straight back to it. The extruders().size() > 1 gate moved onto that call, since the routing check must also see single-filament plates, which is its common case. The copy-mode guard-rail test printed on a filament routed off the primary, so it asserted a plate this rule now refuses; retargeted to a well-formed plate. Its replacement pins the object's own extruder, because ModelVolume reports its extruder_id and would otherwise put a primary-routed slot on the plate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
8edf6b9d4f
commit
baef398ee3
+93
-10
@@ -1331,11 +1331,14 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
|
||||
// IDEX/IQEX: block multi-color in non-primary parallel modes when the active
|
||||
// configuration can't physically support it (no within-gantry toolchange path
|
||||
// or MMU lane sharing). See imex_multicolor_block_reason() for the full rule.
|
||||
if (m_config.is_imex.value && extruders.size() > 1 && !m_objects.empty()) {
|
||||
// Both IMEX checks below need the active mode's tools string, so the parallel table is
|
||||
// walked once here. Note this block is NOT gated on extruders.size() > 1: the primary
|
||||
// routing check applies to a single-filament plate too, which is its most common case.
|
||||
if (m_config.is_imex.value && !m_objects.empty()) {
|
||||
const std::string& parallel_mode = m_objects.front()->config().imex_parallel_mode.value;
|
||||
if (!parallel_mode.empty() && parallel_mode != kImexPrimaryMode) {
|
||||
// Look up the active mode's tools string in the printer config's parallel
|
||||
// table. Fall back to empty (block helper handles it gracefully).
|
||||
// table. Fall back to empty (the helpers below handle it gracefully).
|
||||
std::string active_tools_str;
|
||||
const auto& mode_names = m_config.imex_mode_names.values;
|
||||
const auto& mode_tools = m_config.imex_mode_active_tools.values;
|
||||
@@ -1349,14 +1352,94 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
|
||||
used_filaments_0b.reserve(extruders.size());
|
||||
for (unsigned int e : extruders)
|
||||
used_filaments_0b.push_back((int)e);
|
||||
const std::string reason = imex_multicolor_block_reason(
|
||||
parallel_mode,
|
||||
active_tools_str,
|
||||
m_config.imex_tools_per_gantry.value,
|
||||
used_filaments_0b,
|
||||
m_config.physical_extruder_map);
|
||||
if (!reason.empty())
|
||||
return { reason };
|
||||
|
||||
// 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.
|
||||
if (used_filaments_0b.size() > 1) {
|
||||
const std::string reason = imex_multicolor_block_reason(
|
||||
parallel_mode,
|
||||
active_tools_str,
|
||||
m_config.imex_tools_per_gantry.value,
|
||||
used_filaments_0b,
|
||||
m_config.physical_extruder_map);
|
||||
if (!reason.empty())
|
||||
return { reason };
|
||||
}
|
||||
|
||||
// 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
|
||||
// comes from the ordinary object filament selector, which has no IMEX awareness,
|
||||
// so nothing detected the mismatch. collect_imex_warnings() computes the same
|
||||
// condition and discards it into a display fallback.
|
||||
//
|
||||
// Blocks rather than warns, matching the multi-color rule above. The plate is not
|
||||
// printable as configured: the primary tool executes the toolpaths while the flow
|
||||
// and temperatures were computed for a filament it cannot load. Where the routed
|
||||
// head is also absent from the mode's active tools, the 1st->2nd layer temperature
|
||||
// branch (GCode.cpp, mutually exclusive with the standard path) skips it too, so
|
||||
// that head holds nozzle_temperature_initial_layer for the whole job. First-layer
|
||||
// temperatures are unaffected -- _print_first_layer_extruder_temperatures is not
|
||||
// IMEX-branched -- so this is a stuck-hot nozzle, not a cold one.
|
||||
const ConfigOptionInts& pem = m_config.physical_extruder_map;
|
||||
const int declared_primary = imex_primary_tool_for_mode(active_tools_str);
|
||||
if (declared_primary >= 0 && !pem.values.empty()) {
|
||||
std::vector<int> used_slots_1b;
|
||||
used_slots_1b.reserve(used_filaments_0b.size());
|
||||
for (int slot_0b : used_filaments_0b)
|
||||
used_slots_1b.push_back(slot_0b + 1);
|
||||
if (imex_primary_logical_from_objects(used_slots_1b, pem, declared_primary) < 0) {
|
||||
// Bounds-check exactly as imex_primary_logical_from_objects does. get_at()
|
||||
// CLAMPS out-of-range slots to values.front(), which would let the message
|
||||
// name the very tool it just said nothing routes to.
|
||||
std::vector<int> routed_heads;
|
||||
routed_heads.reserve(used_filaments_0b.size());
|
||||
for (int slot_0b : used_filaments_0b)
|
||||
if (slot_0b >= 0 && slot_0b < (int) pem.values.size())
|
||||
routed_heads.push_back(pem.values[slot_0b]);
|
||||
std::sort(routed_heads.begin(), routed_heads.end());
|
||||
routed_heads.erase(std::unique(routed_heads.begin(), routed_heads.end()), routed_heads.end());
|
||||
std::string routed_list;
|
||||
for (int head : routed_heads)
|
||||
routed_list += (routed_list.empty() ? "T" : ", T") + std::to_string(head);
|
||||
if (routed_list.empty())
|
||||
routed_list = L("no configured extruder");
|
||||
|
||||
// Name the modes that would actually work, by primary tool. Suggesting a
|
||||
// tool number is useless on its own -- the plate's mode menu lists mode
|
||||
// names, never their primaries.
|
||||
std::string candidate_modes;
|
||||
for (size_t i = 0; i < mode_names.size() && i < mode_tools.size(); ++i) {
|
||||
const int primary_i = imex_primary_tool_for_mode(mode_tools[i]);
|
||||
if (primary_i >= 0 && std::binary_search(routed_heads.begin(), routed_heads.end(), primary_i))
|
||||
candidate_modes += (candidate_modes.empty() ? "\"" : ", \"") + mode_names[i] + "\"";
|
||||
}
|
||||
|
||||
StringObjectException err;
|
||||
err.string = candidate_modes.empty()
|
||||
? Slic3r::format(
|
||||
L("IMEX mode \"%1%\" prints with tool T%2%, but no filament used on this plate "
|
||||
"is loaded on T%2% -- the plate's filaments are on %3%. The Primary tool "
|
||||
"prints the sliced paths directly, so it can only use a filament the "
|
||||
"printer's physical extruder map routes to it. Assign an object a filament "
|
||||
"loaded on T%2%, switch this plate to Primary mode, or edit the mode in "
|
||||
"Printer Settings so its Primary tool is one of %3%."),
|
||||
parallel_mode, declared_primary, routed_list)
|
||||
: Slic3r::format(
|
||||
L("IMEX mode \"%1%\" prints with tool T%2%, but no filament used on this plate "
|
||||
"is loaded on T%2% -- the plate's filaments are on %3%. The Primary tool "
|
||||
"prints the sliced paths directly, so it can only use a filament the "
|
||||
"printer's physical extruder map routes to it. Assign an object a filament "
|
||||
"loaded on T%2%, switch this plate to Primary mode, or switch it to one of: %4%."),
|
||||
parallel_mode, declared_primary, routed_list, candidate_modes);
|
||||
// Gives the notification a "Jump to <object>" link, which selects the object
|
||||
// and switches to Prepare -- directly enabling the first suggested remedy.
|
||||
err.object = m_objects.front();
|
||||
return err;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -804,14 +804,22 @@ TEST_CASE("Primary-mode IMEX prints mark only the filament slot they print with"
|
||||
}
|
||||
|
||||
// Guard rail for the fix above: the modes the supplement exists for must keep marking their
|
||||
// secondaries. `copy` declares heads 0 and 1; neither is the initial tool's head (filament 6
|
||||
// routes to head 2), so both are still enumerated. Head 0 -> slot 0, head 1 -> slot 4.
|
||||
// secondaries. Printed on filament 1 (slot 0), which pem routes to head 0 -- the head `copy`
|
||||
// declares Primary -- so the plate is well-formed and validate() lets it through. Head 0 is
|
||||
// the initial tool's head and is skipped (tool_ordering already marked slot 0); head 1 is the
|
||||
// secondary and resolves to slot 4.
|
||||
//
|
||||
// This case previously printed on filament 6, which routes to head 2 while `copy` declares
|
||||
// head 0 Primary. Print::validate() now refuses that plate outright (no filament on it can
|
||||
// feed the Primary tool), so asserting it slices correctly would contradict
|
||||
// "An IMEX plate whose filament never routes to the primary carriage is blocked" below.
|
||||
// slice() does not surface validate()'s return, so the contradiction would have gone unnoticed.
|
||||
TEST_CASE("Copy-mode IMEX prints still mark every secondary carriage's filament slot",
|
||||
"[MultiFilament][IMEX]")
|
||||
{
|
||||
DynamicPrintConfig config = multifilament_config(7);
|
||||
imex_7x4_printer(config);
|
||||
all_regions_on_filament(config, 6);
|
||||
all_regions_on_filament(config, 1);
|
||||
config.set_deserialize_strict({
|
||||
{ "imex_parallel_mode", "copy" },
|
||||
{ "machine_start_gcode",
|
||||
@@ -822,9 +830,9 @@ TEST_CASE("Copy-mode IMEX prints still mark every secondary carriage's filament
|
||||
|
||||
const std::string gcode = slice({ cube(20) }, config);
|
||||
|
||||
CHECK(gcode.find(";USED5:1") != std::string::npos);
|
||||
CHECK(gcode.find(";USED0:1") != std::string::npos);
|
||||
CHECK(gcode.find(";USED4:1") != std::string::npos);
|
||||
CHECK(gcode.find(";USED0:1") != std::string::npos); // the filament actually printed
|
||||
CHECK(gcode.find(";USED4:1") != std::string::npos); // head 1, the secondary carriage
|
||||
CHECK(gcode.find(";USED5:0") != std::string::npos);
|
||||
}
|
||||
|
||||
// The two IMEX changes are coupled: get_imex_active_tools() now returns an empty roster in
|
||||
@@ -885,3 +893,57 @@ TEST_CASE("IQEX modes emit first- and second-layer temperatures for every active
|
||||
CHECK(gcode.find("M104 S240 T2") != std::string::npos);
|
||||
CHECK(gcode.find("M104 S240 T3") != std::string::npos);
|
||||
}
|
||||
|
||||
// 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 enforces that for
|
||||
// the secondary tools; the primary's filament comes from the ordinary object selector, which
|
||||
// has no IMEX awareness. `copy` declares T0 Primary, but every filament this plate uses --
|
||||
// filament 6, slot 5 -- routes to head 2, so nothing can feed T0 and validate() must refuse.
|
||||
//
|
||||
// The object's own extruder is pinned too: ModelVolume::get_extruders() reports the volume's
|
||||
// extruder_id (1 by default), which would put slot 0 on the plate. Slot 0 routes to head 0,
|
||||
// the declared primary, so the plate would be well-formed and correctly NOT blocked.
|
||||
TEST_CASE("An IMEX plate whose filament never routes to the Primary tool is blocked",
|
||||
"[MultiFilament][IMEX]")
|
||||
{
|
||||
DynamicPrintConfig config = multifilament_config(7);
|
||||
imex_7x4_printer(config);
|
||||
all_regions_on_filament(config, 6);
|
||||
config.set_deserialize_strict({ { "imex_parallel_mode", "copy" } });
|
||||
|
||||
std::vector<TriangleMesh> meshes;
|
||||
meshes.push_back(cube(20));
|
||||
const std::vector<std::vector<ConfigBase::SetDeserializeItem>> overrides{ { { "extruder", "6" } } };
|
||||
|
||||
Slic3r::Model model;
|
||||
Slic3r::Print print;
|
||||
init_print(std::move(meshes), print, model, config, &overrides, false);
|
||||
|
||||
std::vector<StringObjectException> warnings;
|
||||
const StringObjectException err = print.validate(&warnings);
|
||||
|
||||
REQUIRE_FALSE(err.string.empty());
|
||||
CHECK(err.string.find("Primary tool") != std::string::npos);
|
||||
CHECK(err.string.find("T0") != std::string::npos); // the declared primary
|
||||
CHECK(err.string.find("T2") != std::string::npos); // where the filament actually lives
|
||||
}
|
||||
|
||||
// Guard rail: the block must not fire on a well-formed plate. Filament 1 (slot 0) routes to
|
||||
// head 0, which `copy` declares Primary, so the Primary tool has something to print with.
|
||||
TEST_CASE("An IMEX plate whose filament routes to the Primary tool validates",
|
||||
"[MultiFilament][IMEX]")
|
||||
{
|
||||
DynamicPrintConfig config = multifilament_config(7);
|
||||
imex_7x4_printer(config);
|
||||
all_regions_on_filament(config, 1);
|
||||
config.set_deserialize_strict({ { "imex_parallel_mode", "copy" } });
|
||||
|
||||
Slic3r::Model model;
|
||||
Slic3r::Print print;
|
||||
init_print({ cube(20) }, print, model, config);
|
||||
|
||||
std::vector<StringObjectException> warnings;
|
||||
const StringObjectException err = print.validate(&warnings);
|
||||
|
||||
CHECK(err.string.empty());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user