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(); } }