Belt: size the purge prism against physical filaments, not mixed slots

A mixed filament slot is virtual: ToolOrdering::resolve_mixed_filaments()
replaces it with its physical components before any G-code is emitted, so
the toolchanges the prism has to absorb are between those components.
ensure_belt_purge_tower() counted the slot as a filament of its own,
provisioning one island per mixed slot that no swap can ever reach -- the
"extra purge tower" on MCTEST5, where filament 5 is a 50/50 blend of 2
and 4 and the G-code reports 0.00 g of it used.

Expand the assigned set with the same expand_mixed_filaments() the
backend uses, so the GUI sizes the prism against the filament set the
slicer actually produces. No-op when nothing is mixed. Test covers the
MCTEST5 shape, a mixed slot whose components are otherwise unused, and
the no-mixing case.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SsuY8Laiyh7q2zPVVKV3HZ
This commit is contained in:
harrierpigeon
2026-09-12 22:23:14 -05:00
co-authored by Claude Fable 5.1
parent 1bcfe58064
commit a430430690
2 changed files with 78 additions and 0 deletions
+34
View File
@@ -9,6 +9,7 @@
#include "libslic3r/Preset.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/FilamentMixer.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/Geometry.hpp"
#include "libslic3r/BoundingBox.hpp"
@@ -131,6 +132,39 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object
}
}
// A mixed filament slot is VIRTUAL: it never reaches a nozzle. At slice time
// ToolOrdering::resolve_mixed_filaments() replaces it with its physical
// components, so the toolchanges the prism has to absorb are between those
// components, not to the mixed slot itself. Counting the slot as a filament
// of its own therefore over-provisions the prism by one island per mixed slot
// -- the "extra purge tower" -- and, when every component is already used by
// another object, by an island that can never be reached at all.
//
// Expand here with the same helper the backend uses (Print.cpp's sequential
// path), so the GUI sizes the prism against the same filament set the slicer
// will actually produce. No-op when no filament is mixed.
{
// Copies, not references: a ternary with an empty-vector fallback would bind
// a reference to a temporary.
std::vector<unsigned char> is_mixed;
std::vector<std::string> comp_strs;
if (const auto *o = full_cfg.option<ConfigOptionBools>("filament_is_mixed"))
is_mixed = o->values;
if (const auto *o = full_cfg.option<ConfigOptionStrings>("filament_mixed_components"))
comp_strs = o->values;
if (has_any_mixed_filament(is_mixed)) {
std::vector<unsigned int> zero_based;
zero_based.reserve(filaments.size());
for (int f : filaments)
if (f > 0)
zero_based.push_back((unsigned int) (f - 1));
zero_based = expand_mixed_filaments(zero_based, is_mixed, comp_strs);
filaments.clear();
for (unsigned int f : zero_based)
filaments.insert((int) f + 1);
}
}
const bool wanted = belt && prime_tower_enabled && !by_object && have_objects && filaments.size() > 1;
if (!wanted) {
sig = BeltPurgeSignature{};
+44
View File
@@ -57,6 +57,50 @@ TEST_CASE("expand_mixed_filaments replaces mixed slots with their components", "
}
}
TEST_CASE("belt purge tower island count ignores virtual mixed slots", "[FilamentMixer][belt]")
{
// Regression for the "extra purge tower" on a belt printer with a mixed
// filament (MCTEST5). The belt purge prism is sized as
// n_islands = used_filaments.size() - 1
// and GUI::ensure_belt_purge_tower() collected those filaments straight off
// the model objects' extruder assignments. A mixed slot is VIRTUAL -- no
// nozzle carries it, and ToolOrdering::resolve_mixed_filaments() replaces it
// with its components before any G-code is emitted -- so counting it as a
// filament of its own provisions one island that can never be reached.
//
// MCTEST5: five cubes on extruders 1..5, where filament 5 is a 50/50 blend of
// filaments 2 and 4. The G-code uses only T0..T3 and reports
// "filament used [g] = 53.35, 141.11, 40.84, 107.23, 0.00" -- filament 5
// consumes nothing, exactly as a virtual slot should.
const std::vector<unsigned char> is_mixed = {0, 0, 0, 0, 1};
const std::vector<std::string> comp_strs = {"", "", "", "", "2,4"};
// The set the sizer used to see: slots 0..4 (filaments 1..5).
const std::vector<unsigned int> assigned = {0, 1, 2, 3, 4};
const auto physical = expand_mixed_filaments(assigned, is_mixed, comp_strs);
// Slot 4 dissolves into 1 and 3, which are already present.
REQUIRE(physical == std::vector<unsigned int>({0, 1, 2, 3}));
// Four physical filaments => three transitions => three islands, not four.
REQUIRE(int(physical.size()) - 1 == 3);
REQUIRE(int(assigned.size()) - 1 == 4); // what it produced before the fix
SECTION("A mixed slot whose components are otherwise unused still counts them") {
// Only the mixed slot is assigned: it must still yield its two components,
// i.e. one island, rather than collapsing to zero.
const auto only_mixed = expand_mixed_filaments({4}, is_mixed, comp_strs);
REQUIRE(only_mixed == std::vector<unsigned int>({1, 3}));
REQUIRE(int(only_mixed.size()) - 1 == 1);
}
SECTION("No mixed filaments anywhere leaves the set untouched") {
const std::vector<unsigned char> none_mixed = {0, 0, 0, 0, 0};
REQUIRE_FALSE(has_any_mixed_filament(none_mixed));
REQUIRE(expand_mixed_filaments(assigned, none_mixed, {"", "", "", "", ""}) == assigned);
}
}
TEST_CASE("check_mixed_filament_integrity flags dangling component references", "[FilamentMixer]")
{
const std::vector<unsigned char> is_mixed = {0, 0, 1};