mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-10 17:21:10 +00:00
fix(imex): let the mixed-filament refusal outrank the multi-color rule
A mixed filament is unsupported in a parallel mode outright, but the rule saying so ran third. A plate carrying a blend plus any second filament tripped the multi-color rule's used > 1 gate first and was told its active tools all sit on one gantry -- a diagnosis of a multi-color print the user never configured, whose remedy is to go rework the mode's tool roster. The blend was never mentioned. Move the check ahead of both rules below it; being unsupported regardless of routing or topology, it dominates them. Nothing is masked that leads anywhere else: every branch of imex_multicolor_block_reason is itself confined to non-primary modes, so the mixed message's remedy -- switch this plate to Primary -- silences those too. Say "Mixed filaments", not "Blended". Every other string in the app calls these mixed, including the button that creates one and the sibling refusal for the wipe tower filament, so the user had no way to connect the message to the feature it names. Three comments in the block were wrong, and two of them were newly wrong. The routing rule's bounds-check note still said "Blended slots are out of range by construction, but they never reach here -- the rule above returns first": the rule above is now the multi-color one, which does not return first for a single mixed filament, and out-of-range is not guaranteed at all. Mixed slots are kept at the tail of the filament arrays by convention, not by enforcement -- PresetBundle::set_num_filaments grows filament_is_mixed with resize(), so raising a printer's extruder count with a blend present lands physical slots after the mixed one. The scan is position-agnostic and stays correct; only the stated reason was wrong. The same discovery makes the empty-routed_list guard live rather than the dead code it was described as. Print::apply() normalises physical_extruder_map before validate() runs, so an unauthored map is never the cause -- but a printer with more filaments than logical extruders leaves the tail slots outside the map, and raising the extruder count does exactly that. The new test validates the plate twice. The first pass, with no blend, asserts the multi-color rule is armed at all; without it the second proves nothing, because the rule only fires here thanks to a degenerate fixture mode whose two tools share a gantry. Give that mode a Span tool and the whole test would pass under either ordering while appearing to guard it. It also pins err.object, which the mixed path sets and the multi-color path leaves null -- a discriminator that survives the next wording change. Verified by reverting the order: the test fails on both the message and the object. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
2e883df7d9
commit
018091f43b
+33
-24
@@ -1354,10 +1354,35 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
|
||||
for (unsigned int e : extruders)
|
||||
used_filaments_0b.push_back((int)e);
|
||||
|
||||
// A mixed filament is blended 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 are routed.
|
||||
//
|
||||
// Runs before BOTH rules below. Against the routing rule it would otherwise be
|
||||
// reported as merely unrouted: mixed slots are kept at the tail of the filament
|
||||
// arrays, past the physical filament count, so on a printer whose logical extruder
|
||||
// count equals that count they fall outside physical_extruder_map and resolve to no
|
||||
// head. Against the multi-color rule, a mixed slot plus any second filament trips
|
||||
// the >1 gate and earns a lecture about gantry topology the user never configured.
|
||||
// Being unsupported outright, this dominates both.
|
||||
{
|
||||
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;
|
||||
// "Mixed filament" is the term the rest of the UI uses -- the button that
|
||||
// creates one, and the sibling refusal for the wipe tower filament.
|
||||
err.string = L("Mixed filaments are not supported in IMEX parallel modes. "
|
||||
"Switch this plate to Primary mode.");
|
||||
err.object = m_objects.front();
|
||||
return err;
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
// ahead of the routing block below, which would otherwise mask it with advice that
|
||||
// leads to this error on the next slice. validate() returns on the first error.
|
||||
if (used_filaments_0b.size() > 1) {
|
||||
const std::string reason = imex_multicolor_block_reason(
|
||||
parallel_mode,
|
||||
@@ -1369,24 +1394,6 @@ StringObjectException Print::validate(std::vector<StringObjectException> *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
|
||||
@@ -1425,10 +1432,12 @@ StringObjectException Print::validate(std::vector<StringObjectException> *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 .".
|
||||
// Print::apply() normalises physical_extruder_map through
|
||||
// effective_physical_extruder_map (PrintApply.cpp) before validate() runs, so
|
||||
// an unauthored map arrives here as the identity and every slot is in range.
|
||||
// What is left is a printer with more filaments than logical extruders, where
|
||||
// the tail slots fall outside the map. Narrow, but without this the sentence
|
||||
// ends in a dangling "on .".
|
||||
if (routed_list.empty())
|
||||
routed_list = L("no configured tool");
|
||||
|
||||
|
||||
@@ -980,20 +980,28 @@ TEST_CASE("An IMEX plate whose filament never routes to the Primary tool is bloc
|
||||
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
|
||||
// A mixed filament is blended 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]")
|
||||
// T0 -- and it must refuse with the mixed message, not the routing one. Mixed slots normally
|
||||
// sit past the end of physical_extruder_map, so the routing rule would call them unrouted.
|
||||
TEST_CASE("An IMEX plate using a mixed 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" },
|
||||
{ "imex_parallel_mode", "copy" },
|
||||
{ "filament_is_mixed", "0,0,0,0,0,0,0,1" },
|
||||
{ "filament_mixed_components", ";;;;;;;1,5" },
|
||||
// The mixed arrays run parallel to filament_colour and must be sized to the filament
|
||||
// count (see test_mixed_filament.cpp). validate() returns before the other five are
|
||||
// read, but that is a property of where the rule sits, not something to rely on.
|
||||
{ "filament_mixed_sublayer_ratios", ";;;;;;;" },
|
||||
{ "filament_mixed_gradient", "0,0,0,0,0,0,0,0" },
|
||||
{ "filament_mixed_gradient_range", ";;;;;;;" },
|
||||
{ "filament_mixed_gradient_curve", ";;;;;;;" },
|
||||
{ "filament_mixed_gradient_per_part", "0,0,0,0,0,0,0,0" },
|
||||
});
|
||||
|
||||
std::vector<TriangleMesh> meshes;
|
||||
@@ -1008,7 +1016,83 @@ TEST_CASE("An IMEX plate using a blended filament is blocked", "[MultiFilament][
|
||||
const StringObjectException err = print.validate(&warnings);
|
||||
|
||||
REQUIRE_FALSE(err.string.empty());
|
||||
CHECK(err.string.find("Blended filaments") != std::string::npos);
|
||||
CHECK(err.string.find("Mixed filaments") != std::string::npos);
|
||||
}
|
||||
|
||||
// Filament 8 (slot 7) is the blend, filament 1 (slot 0) is ordinary, so used_filaments is > 1
|
||||
// and the multi-color rule's gate opens too. This pins that the mixed rule still wins: with the
|
||||
// two the other way round the user is told the mode's active tools all sit on one gantry --
|
||||
// a lecture about a multi-color print they never asked for -- and never learns the blend is
|
||||
// the problem.
|
||||
//
|
||||
// The second half is what keeps this honest. The multi-color rule only fires here because this
|
||||
// fixture's `copy` mode is degenerate (imex_tools_per_gantry defaults to 2, so "0:P,1:C" puts
|
||||
// both tools on gantry 0). Give the mode a Span tool and it returns nothing, and this test would
|
||||
// pass under EITHER ordering while appearing to guard it. So assert the rule is actually armed.
|
||||
TEST_CASE("A mixed filament outranks the multi-color rule on the same plate",
|
||||
"[MultiFilament][IMEX][Regression]")
|
||||
{
|
||||
// Two cubes, offset: make_cube() is corner-at-origin, so identical meshes would be exactly
|
||||
// coincident. validate() returns from the IMEX block before any geometry check today, but a
|
||||
// future check landing earlier would fail this test for a reason it is not about.
|
||||
const auto build = [](bool blend_slot_8) {
|
||||
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" },
|
||||
// The mixed arrays run parallel to filament_colour and must be sized to the filament
|
||||
// count (see test_mixed_filament.cpp). validate() returns before the other five are
|
||||
// read, but that is a property of where the rule sits, not something to rely on.
|
||||
{ "filament_mixed_sublayer_ratios", ";;;;;;;" },
|
||||
{ "filament_mixed_gradient", "0,0,0,0,0,0,0,0" },
|
||||
{ "filament_mixed_gradient_range", ";;;;;;;" },
|
||||
{ "filament_mixed_gradient_curve", ";;;;;;;" },
|
||||
{ "filament_mixed_gradient_per_part", "0,0,0,0,0,0,0,0" },
|
||||
});
|
||||
if (!blend_slot_8)
|
||||
config.set_deserialize_strict({ { "filament_is_mixed", "0,0,0,0,0,0,0,0" } });
|
||||
return config;
|
||||
};
|
||||
const std::vector<std::vector<ConfigBase::SetDeserializeItem>> overrides{
|
||||
{ { "extruder", "8" } }, { { "extruder", "1" } },
|
||||
};
|
||||
const auto validate_plate = [&](const DynamicPrintConfig& config, Slic3r::Print& print,
|
||||
Slic3r::Model& model) {
|
||||
std::vector<TriangleMesh> meshes;
|
||||
meshes.push_back(cube(20));
|
||||
TriangleMesh second = cube(20);
|
||||
second.translate(30.0, 0.0, 0.0);
|
||||
meshes.push_back(second);
|
||||
init_print(std::move(meshes), print, model, config, &overrides, false);
|
||||
std::vector<StringObjectException> warnings;
|
||||
return print.validate(&warnings);
|
||||
};
|
||||
|
||||
// The multi-color rule IS armed for this plate -- without that, the check below proves nothing.
|
||||
{
|
||||
Slic3r::Model model;
|
||||
Slic3r::Print print;
|
||||
const StringObjectException err = validate_plate(build(false), print, model);
|
||||
REQUIRE_FALSE(err.string.empty());
|
||||
CHECK(err.string.find("Multi-color") != std::string::npos);
|
||||
}
|
||||
|
||||
// With the blend present the mixed rule takes precedence over it.
|
||||
{
|
||||
Slic3r::Model model;
|
||||
Slic3r::Print print;
|
||||
const StringObjectException err = validate_plate(build(true), print, model);
|
||||
REQUIRE_FALSE(err.string.empty());
|
||||
CHECK(err.string.find("not supported in IMEX parallel modes") != std::string::npos);
|
||||
CHECK(err.string.find("Multi-color") == std::string::npos);
|
||||
// The two rules differ in more than wording: the mixed path attaches an object (for the
|
||||
// notification's "Jump to" link), the multi-color path returns none. Pins which fired
|
||||
// independently of the message text.
|
||||
CHECK(err.object == print.objects().front());
|
||||
}
|
||||
}
|
||||
|
||||
// Guard rail: the block must not fire on a well-formed plate. Filament 1 (slot 0) routes to
|
||||
|
||||
Reference in New Issue
Block a user