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",