Belt: stop the purge prism printing plastic no toolchange needs

Two independent leaks of filament on the belt purge prism, plus the
replan safety net the second one needs.

1. The early-truncation scan bounded itself with the prism's own
   toolchanges. ToolOrdering covers the whole print and the prism is a
   printed object in it, so the "last toolchange" the scan found was on
   the prism's own top layers -- it runs past every model object by
   design -- and the truncation cancelled nothing. Bound the scan at the
   tallest non-prism object (support layers included; on a belt they can
   top the object). On MCTEST5 that was 197 toolchanges over 39.4 mm of
   tower that no swap ever needed.

2. On a layer with no toolchange, the prism's entire fill printed as
   solid infill in its own filament. Drop the fills no toolchange
   claimed, right after the purge marking and before
   ensure_perimeters_infills_order() force-overrides whatever is left.
   Perimeters stay so the bar keeps a continuous wall. An earlier version
   of this deleted the entities and had to be reverted: psWipeTower can
   rerun without regenerating infill, and a later tool ordering may claim
   what this one did not. The entities are now stashed with their layer,
   region and index and put back exactly, the same reversibility contract
   layer truncation already had.

3. Both stashes go stale if an object step reruns: make_fills() clears
   and regenerates fills over m_layers only, so a stale stash would put
   old fills back next to new ones, and truncated layers would keep old
   perimeters/fills. Undo the plan's edits at the top of Print::process()
   whenever psWipeTower is not done. Every object-step invalidation also
   invalidates psWipeTower, so that condition is exactly "some object
   step may rerun"; when it is done nothing regenerates and the edits
   must stay. This also covers a prism left behind after belt mode is
   turned off, which previously stayed truncated forever.

WipingExtrusions::is_entity_overridden() becomes public so the prism can
tell claimed fills from unclaimed ones.

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 1b6fb2a81f
commit 1bcfe58064
5 changed files with 162 additions and 7 deletions
+104 -5
View File
@@ -149,7 +149,7 @@ void Print::_plan_belt_purge()
// previous plan so a newly higher toolchange can use its original layers.
for (PrintObject *po : m_objects)
if (po->config().belt_purge_tower_object.value)
po->belt_restore_truncated_layers();
po->belt_undo_purge_plan();
// Must run before ToolOrdering is built: LayerTools merge per-object layer
// print_z values, and the prism only absorbs purge where its (snapped)
@@ -191,11 +191,44 @@ void Print::_plan_belt_purge()
// flush object), so truncating afterwards would leave dangling overrides
// pointing into deleted layers.
{
// The tool ordering covers the WHOLE print, and the prism is a printed
// object in it. Left unbounded, the scan below sees the prism's own
// toolchanges on layers above every model object -- the prism runs past
// them by design (ramp/height compensation at the tilted ends) -- so
// last_tc_z lands at the prism's own top and the truncation cancels
// nothing. The tower ends up justifying its own existence.
//
// Nothing above the tallest printed object can require a color change,
// so bound the scan there. On MCTEST5 that is 197 toolchanges spanning
// z=154.00..193.20 with the tallest object topping out at 153.80, i.e.
// 39.4 mm of tower that no swap ever needed.
// Support layers count too: on a belt they can extend above the object's
// own top, and a toolchange there is a real one.
double obj_top_z = -1.;
for (const PrintObject *po : m_objects) {
if (po->config().belt_purge_tower_object.value)
continue;
if (!po->layers().empty())
obj_top_z = std::max(obj_top_z, po->layers().back()->print_z);
if (!po->support_layers().empty())
obj_top_z = std::max(obj_top_z, po->support_layers().back()->print_z);
}
double last_tc_z = -1.;
unsigned int cur_ext = m_wipe_tower_data.tool_ordering.first_extruder();
for (const auto &lt : m_wipe_tower_data.tool_ordering.layer_tools())
for (const auto &lt : m_wipe_tower_data.tool_ordering.layer_tools()) {
// layer_tools() is ordered by print_z ascending.
if (obj_top_z >= 0. && lt.print_z > obj_top_z + EPSILON)
break;
for (const unsigned int e : lt.extruders)
if (e != cur_ext) { last_tc_z = lt.print_z; cur_ext = e; }
}
// Deliberately NOT cancelling the prism outright when no object toolchange
// exists: belt_truncate_layers_above(0.) empties m_layers, and an object
// with zero layers is not something the rest of the pipeline expects. The
// GUI already declines to create a prism unless more than one filament is
// in use, so this case is a stale prism, not a hot path -- leave it whole
// rather than risk a zero-layer object.
if (last_tc_z >= 0.)
for (PrintObject *po : m_objects)
if (po->config().belt_purge_tower_object.value && !po->layers().empty()) {
@@ -251,9 +284,25 @@ void Print::_plan_belt_purge()
current_extruder_id = extruder_id;
}
// Do not destructively remove unclaimed fill entities here. psWipeTower
// can rerun without regenerating infill, and a later tool ordering may
// need entities that were unclaimed by the previous plan.
// Plastic saving: drop the prism's fills that no toolchange on this layer
// claimed. At this point the prism's OVERRIDDEN fills are exactly the
// purge; the rest would print as solid infill in the prism's own filament
// for nothing -- which is the whole prism on a layer with no toolchange
// (141 of 692 layers on MCTEST5 before the truncation fix). Perimeters are
// left alone so the bar keeps a continuous wall along the belt.
//
// Non-destructive: the entities are stashed with their positions and put
// back by belt_restore_dropped_fills() at the top of the next plan. An
// earlier version deleted them outright, which broke replanning when a
// later tool ordering needed what this one had not claimed -- that is why
// it was removed rather than kept.
if (prism_po != nullptr) {
const auto &we = layer_tools.wiping_extrusions();
prism_po->belt_drop_unclaimed_fills(
prism_po->get_layer_at_printz(layer_tools.print_z, EPSILON),
[&we, prism_po](const ExtrusionEntity *e) { return we.is_entity_overridden(e, prism_po, 0); });
}
layer_tools.wiping_extrusions().ensure_perimeters_infills_order(*this);
if (layer_leftover > 0.f) {
total_leftover += layer_leftover;
@@ -330,6 +379,56 @@ size_t PrintObject::belt_truncate_layers_above(coordf_t z)
return removed;
}
// Plastic saving on the purge prism: keep only the fills a toolchange claimed.
//
// Called per layer from _plan_belt_purge(), after the real-purge marking and
// BEFORE ensure_perimeters_infills_order() -- that pass force-overrides every
// remaining fill on the prism (it is a dedicated flush object), so afterwards
// everything looks claimed and nothing could be distinguished.
size_t PrintObject::belt_drop_unclaimed_fills(Layer *layer, const std::function<bool(const ExtrusionEntity*)> &claimed)
{
if (layer == nullptr)
return 0;
size_t dropped = 0;
for (size_t ri = 0; ri < layer->regions().size(); ++ri) {
LayerRegion *lr = layer->get_region(ri);
auto &ents = lr->fills.entities;
ExtrusionEntitiesPtr keep;
keep.reserve(ents.size());
for (size_t i = 0; i < ents.size(); ++i) {
if (claimed(ents[i])) {
keep.emplace_back(ents[i]);
} else {
// Stash with its original index so the restore is exact.
m_belt_dropped_fills.push_back(BeltDroppedFill{ layer, ri, i, ents[i] });
++dropped;
}
}
ents = std::move(keep);
}
return dropped;
}
void PrintObject::belt_restore_dropped_fills()
{
if (m_belt_dropped_fills.empty())
return;
// Ascending index per (layer, region): inserting in that order lands every
// entity back at its original position, because each insertion shifts only
// the entries after it, which are themselves still to be inserted.
std::stable_sort(m_belt_dropped_fills.begin(), m_belt_dropped_fills.end(),
[](const BeltDroppedFill &a, const BeltDroppedFill &b) {
if (a.layer != b.layer) return a.layer < b.layer;
if (a.region_idx != b.region_idx) return a.region_idx < b.region_idx;
return a.index < b.index;
});
for (const BeltDroppedFill &d : m_belt_dropped_fills) {
auto &ents = d.layer->get_region(d.region_idx)->fills.entities;
ents.insert(ents.begin() + std::min(d.index, ents.size()), d.entity);
}
m_belt_dropped_fills.clear();
}
void PrintObject::belt_restore_truncated_layers()
{
if (m_belt_truncated_layers.empty())
+6 -2
View File
@@ -74,13 +74,17 @@ public:
void set_layer_tools_ptr(const LayerTools* lt) { m_layer_tools = lt; }
private:
// Returns true if entity is not printed with its usual extruder for a given copy.
// Returns true if entity is not printed with its usual extruder for a given
// copy -- i.e. it was claimed as a wiping/purge extrusion. Public because the
// belt purge prism uses it to tell which of its fills actually carry purge
// from the ones that are unclaimed waste (Print::_plan_belt_purge()).
bool is_entity_overridden(const ExtrusionEntity* entity, const PrintObject *object, size_t copy_id) const {
auto it = entity_map.find(std::make_tuple(entity, object));
return it != entity_map.end() && copy_id < it->second.size() && it->second[copy_id] != -1;
}
private:
int first_nonsoluble_extruder_on_layer(const PrintConfig& print_config) const;
int last_nonsoluble_extruder_on_layer(const PrintConfig& print_config) const;
+13
View File
@@ -2490,6 +2490,19 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
if (m_objects.empty())
return;
// Belt purge prism: _plan_belt_purge() (psWipeTower) truncates the prism's
// layers and drops its unclaimed fills, stashing both so a replan can undo
// them. The object steps below regenerate per-layer content over m_layers
// ONLY, so if any of them is about to rerun the stashes must go back first;
// otherwise truncated layers keep stale perimeters/fills and dropped fills
// are re-inserted next to freshly generated ones. Every object-step
// invalidation also invalidates psWipeTower, so "psWipeTower not done" is
// exactly "some object step may rerun" -- and when it IS done nothing below
// regenerates, and the plan's edits have to stay.
if (!this->is_step_done(psWipeTower))
for (PrintObject *obj : m_objects)
obj->belt_undo_purge_plan();
for (PrintObject *obj : m_objects)
obj->clear_shared_object();
+32
View File
@@ -582,6 +582,28 @@ private:
// Wipe-tower-only invalidations do not necessarily reslice the object, so
// truncation must be reversible when later toolchanges move upward.
void belt_restore_truncated_layers();
// Belt purge prism, plastic saving: drop the fills on one layer that no
// toolchange claimed. `claimed` reports whether an entity was overridden as
// purge; everything else on that layer would otherwise print as solid infill
// in the prism's own filament for nothing. Perimeters are never touched, so
// the bar keeps a continuous wall along the belt.
//
// Entities are STASHED, not deleted, with their original positions -- the
// same reversibility contract belt_truncate_layers_above() has, and the
// reason the original version of this had to be removed: psWipeTower can
// rerun without regenerating infill, and a later tool ordering may claim what
// this one did not. Returns the number of entities dropped.
size_t belt_drop_unclaimed_fills(Layer *layer, const std::function<bool(const ExtrusionEntity*)> &claimed);
// Put every stashed fill back at its original index. Must run before a replan.
void belt_restore_dropped_fills();
// Undo every edit _plan_belt_purge() made to this object's layers, leaving
// m_layers exactly as the object steps produced it. Fills first: they point
// into layers that are still live, and truncated layers were stashed whole
// with their own fills untouched, so the two stashes never share an entity.
// Print::process() calls this before any object step may rerun (those steps
// regenerate per-layer content over m_layers only, so a stale stash would
// otherwise be restored on top of fresh content); the plan calls it too.
void belt_undo_purge_plan() { belt_restore_dropped_fills(); belt_restore_truncated_layers(); }
//BBS
ExPolygons _shrink_contour_holes(double contour_delta, double hole_delta, const ExPolygons& polys) const;
// BBS
@@ -625,6 +647,16 @@ private:
SlicingParameters m_slicing_params;
LayerPtrs m_layers;
LayerPtrs m_belt_truncated_layers;
// Fills removed by belt_drop_unclaimed_fills(), owned by this vector until
// restored or until clear_layers() deletes them. An entity is in exactly one
// of the live collection or this stash, never both.
struct BeltDroppedFill {
Layer *layer { nullptr };
size_t region_idx { 0 };
size_t index { 0 }; // position in the original fills.entities
ExtrusionEntity *entity { nullptr };
};
std::vector<BeltDroppedFill> m_belt_dropped_fills;
SupportLayerPtrs m_support_layers;
// Belt brim, generated in posSupportMaterial by BeltBrim.cpp. Object-local
// slicing frame, one entry per object layer plus a prologue of brim-only
+7
View File
@@ -1119,6 +1119,13 @@ void PrintObject::clear_layers()
for (Layer *l : m_belt_truncated_layers)
delete l;
m_belt_truncated_layers.clear();
// Fills dropped for plastic saving are owned by the stash while they sit
// outside their layer's collection, so they are freed here too. Order
// matters only in that these point at layers deleted just above, and we
// never dereference the layer -- just the entity.
for (const BeltDroppedFill &d : m_belt_dropped_fills)
delete d.entity;
m_belt_dropped_fills.clear();
}
}