From f0778ef5fa5a770a9887df12cffd9ff842befb1b Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Wed, 26 Aug 2026 22:04:02 -0400 Subject: [PATCH] fix(imex): shorten the off-primary message and block blended filaments The routing error ran to roughly 450 characters and explained the mechanism before it got to the remedy. It also offered to "edit the mode in Printer Settings so its Primary tool is one of %3%", which on a plate whose filaments resolve to no head at all rendered as "one of no configured extruder". Cut it to the mode, the tool it prints with, where the plate's filaments actually are, and the two things the user can do about it. The second msgid that named candidate modes went with it. It could only suggest a mode whose primary is among the routed heads, and every mode on the printers this fires for declares 0:P, so it had nothing to offer. Blended filaments now return before that check rather than falling through it. A blend is mixed 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 route -- including when a component sits on the declared primary. Reaching the routing rule would also have described them wrongly: mixed slots sit past the end of physical_extruder_map, so they resolve to no head and read as merely unrouted. Keeps the empty-list guard the shortening first dropped. validate() reads the raw physical_extruder_map, whose registered default is a single entry, so a profile that declares IMEX modes without authoring a map leaves every slot past the first outside it -- and the sentence ended in a dangling "on .". Co-Authored-By: Claude Opus 5 (1M context) --- src/libslic3r/Print.cpp | 58 +++++++++++++------------- tests/fff_print/test_multifilament.cpp | 32 +++++++++++++- 2 files changed, 61 insertions(+), 29 deletions(-) diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 6c4ace239f..66c1e9620a 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -1369,6 +1369,24 @@ 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 @@ -1394,7 +1412,9 @@ StringObjectException Print::validate(std::vector *warnin 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. + // name the very tool it just said nothing routes to. Blended slots are + // out of range by construction, but they never reach here -- the rule + // above returns first. std::vector routed_heads; routed_heads.reserve(used_filaments_0b.size()); for (int slot_0b : used_filaments_0b) @@ -1405,36 +1425,18 @@ 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 .". 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] + "\""; - } + routed_list = L("no configured tool"); 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); + err.string = Slic3r::format( + L("IMEX mode \"%1%\" prints with T%2%, but this plate's filaments are on %3%. " + "Assign a filament loaded on T%2%, or switch this plate to Primary mode."), + parallel_mode, declared_primary, routed_list); // Gives the notification a "Jump to " link, which selects the object // and switches to Prepare -- directly enabling the first suggested remedy. err.object = m_objects.front(); diff --git a/tests/fff_print/test_multifilament.cpp b/tests/fff_print/test_multifilament.cpp index 9ec70a2c89..f5b8305d06 100644 --- a/tests/fff_print/test_multifilament.cpp +++ b/tests/fff_print/test_multifilament.cpp @@ -923,11 +923,41 @@ TEST_CASE("An IMEX plate whose filament never routes to the Primary tool is bloc 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 } +// A blended filament is mixed 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]") +{ + 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" }, + }); + + std::vector meshes; + meshes.push_back(cube(20)); + const std::vector> overrides{ { { "extruder", "8" } } }; + + Slic3r::Model model; + Slic3r::Print print; + init_print(std::move(meshes), print, model, config, &overrides, false); + + std::vector warnings; + const StringObjectException err = print.validate(&warnings); + + REQUIRE_FALSE(err.string.empty()); + CHECK(err.string.find("Blended filaments") != std::string::npos); +} + // 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",