From 1bcfe58064ea16387a3f4107d7ea782fc43c6e9a Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 12 Sep 2026 22:23:14 -0500 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01SsuY8Laiyh7q2zPVVKV3HZ --- src/libslic3r/BeltPurge.cpp | 109 +++++++++++++++++++++++++-- src/libslic3r/GCode/ToolOrdering.hpp | 8 +- src/libslic3r/Print.cpp | 13 ++++ src/libslic3r/Print.hpp | 32 ++++++++ src/libslic3r/PrintObject.cpp | 7 ++ 5 files changed, 162 insertions(+), 7 deletions(-) diff --git a/src/libslic3r/BeltPurge.cpp b/src/libslic3r/BeltPurge.cpp index b249f21bd2..37d89ca14e 100644 --- a/src/libslic3r/BeltPurge.cpp +++ b/src/libslic3r/BeltPurge.cpp @@ -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 < : m_wipe_tower_data.tool_ordering.layer_tools()) + for (const auto < : 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 &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()) diff --git a/src/libslic3r/GCode/ToolOrdering.hpp b/src/libslic3r/GCode/ToolOrdering.hpp index f38cb4bdc8..9ccf6e90fb 100644 --- a/src/libslic3r/GCode/ToolOrdering.hpp +++ b/src/libslic3r/GCode/ToolOrdering.hpp @@ -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; diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 86272e612c..1fc92aa9fc 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -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(); diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index f3da337bb7..638bb93105 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -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 &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 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 diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 6d9bfba090..2f02588d37 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -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(); } }