diff --git a/src/libslic3r/BeltPurge.cpp b/src/libslic3r/BeltPurge.cpp index 3e261a1882..b249f21bd2 100644 --- a/src/libslic3r/BeltPurge.cpp +++ b/src/libslic3r/BeltPurge.cpp @@ -79,7 +79,7 @@ void Print::_align_belt_purge_layers() { PrintObject *prism = nullptr; for (PrintObject *po : m_objects) - if (po->config().belt_purge_tower_object.value) { + if (po->config().belt_purge_tower_object.value && !po->layers().empty()) { prism = po; break; } @@ -144,6 +144,13 @@ void Print::_plan_belt_purge() { m_wipe_tower_data.clear(); + // psWipeTower may be invalidated without posSlice (for example after a + // filament-map or tool-ordering change). Restore a prism shortened by the + // 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(); + // Must run before ToolOrdering is built: LayerTools merge per-object layer // print_z values, and the prism only absorbs purge where its (snapped) // layers coincide with the toolchange layers. @@ -191,7 +198,7 @@ void Print::_plan_belt_purge() if (e != cur_ext) { last_tc_z = lt.print_z; cur_ext = e; } if (last_tc_z >= 0.) for (PrintObject *po : m_objects) - if (po->config().belt_purge_tower_object.value) { + if (po->config().belt_purge_tower_object.value && !po->layers().empty()) { po->belt_truncate_layers_above(last_tc_z); break; } @@ -244,32 +251,9 @@ void Print::_plan_belt_purge() current_extruder_id = extruder_id; } - // Plastic saving: drop the prism's infill that no toolchange on this - // layer claimed. At this point (after the real-purge marking, before the - // force-override pass below) the prism's OVERRIDDEN fills are exactly the - // purge; the rest is infill that would otherwise print as the prism's own - // filament for nothing — which is most of the prism on layers with little - // or no purge. We keep perimeters so the bar's wall shell stays - // continuous along the belt. (Must run before ensure_perimeters_infills_ - // order, which force-overrides every remaining fill and would make them - // all look "claimed".) - if (prism_po != nullptr) { - if (Layer *pl = prism_po->get_layer_at_printz(layer_tools.print_z, EPSILON)) { - const auto &we = layer_tools.wiping_extrusions(); - for (LayerRegion *lr : pl->regions()) { - auto &ents = lr->fills.entities; - size_t keep = 0; - for (size_t r = 0; r < ents.size(); ++r) { - if (we.is_entity_overridden(ents[r], prism_po, 0)) - ents[keep++] = ents[r]; - else - delete ents[r]; - } - ents.resize(keep); - } - } - } - + // 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. layer_tools.wiping_extrusions().ensure_perimeters_infills_order(*this); if (layer_leftover > 0.f) { total_leftover += layer_leftover; @@ -328,14 +312,15 @@ void PrintObject::belt_shift_layer_grid(double delta) // layer's upper-layer link. Returns the number of layers removed. size_t PrintObject::belt_truncate_layers_above(coordf_t z) { + // A repeated plan always starts from the restored full layer set. + assert(m_belt_truncated_layers.empty()); size_t keep = m_layers.size(); while (keep > 0 && m_layers[keep - 1]->print_z > z + EPSILON) --keep; if (keep >= m_layers.size()) return 0; const size_t removed = m_layers.size() - keep; - for (size_t i = keep; i < m_layers.size(); ++i) - delete m_layers[i]; + m_belt_truncated_layers.assign(m_layers.begin() + keep, m_layers.end()); m_layers.resize(keep); if (!m_layers.empty()) m_layers.back()->upper_layer = nullptr; @@ -345,4 +330,17 @@ size_t PrintObject::belt_truncate_layers_above(coordf_t z) return removed; } +void PrintObject::belt_restore_truncated_layers() +{ + if (m_belt_truncated_layers.empty()) + return; + + m_layers.insert(m_layers.end(), m_belt_truncated_layers.begin(), m_belt_truncated_layers.end()); + m_belt_truncated_layers.clear(); + for (size_t i = 0; i < m_layers.size(); ++i) { + m_layers[i]->lower_layer = i == 0 ? nullptr : m_layers[i - 1]; + m_layers[i]->upper_layer = i + 1 < m_layers.size() ? m_layers[i + 1] : nullptr; + } +} + } // namespace Slic3r diff --git a/src/libslic3r/GCode/ToolOrdering.hpp b/src/libslic3r/GCode/ToolOrdering.hpp index 52778690bb..da5b4171b4 100644 --- a/src/libslic3r/GCode/ToolOrdering.hpp +++ b/src/libslic3r/GCode/ToolOrdering.hpp @@ -45,14 +45,6 @@ public: void ensure_perimeters_infills_order(const Print& print); - // 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). Used by the belt purge - // tower to tell which prism fills carry purge vs. which are unclaimed waste. - 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; - } - bool is_overriddable(const ExtrusionEntityCollection& ee, const PrintConfig& print_config, const PrintObject& object, const PrintRegion& region) const; bool is_overriddable_and_mark(const ExtrusionEntityCollection& ee, const PrintConfig& print_config, const PrintObject& object, const PrintRegion& region) { bool out = this->is_overriddable(ee, print_config, object, region); @@ -79,6 +71,12 @@ 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. + 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; + } + 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 7cec3de5eb..87bb892c9e 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -1486,6 +1486,14 @@ StringObjectException Print::validate(std::vector *warnin add_warning(warningtemp); } + if (m_config.belt_printer.value && m_config.enable_belt_purge_tower.value) { + const size_t prism_count = std::count_if(m_objects.begin(), m_objects.end(), [](const PrintObject *object) { + return object->config().belt_purge_tower_object.value; + }); + if (prism_count > 1) + return {L("The project contains multiple managed belt purge towers. Reload the plate or toggle the belt purge tower off and on to regenerate it.")}; + } + if (m_config.enable_prime_tower) { for (const PrintObject* object : m_objects) { if (object->config().precise_z_height.value) { diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index 2591f6cbd1..5eddd65100 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -565,6 +565,10 @@ private: // Belt mode: remove layers above z (cancel the purge prism past the last // toolchange). Returns how many layers were dropped. size_t belt_truncate_layers_above(coordf_t z); + // Restore layers removed by belt_truncate_layers_above() before replanning. + // 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(); //BBS ExPolygons _shrink_contour_holes(double contour_delta, double hole_delta, const ExPolygons& polys) const; // BBS @@ -607,6 +611,7 @@ private: SlicingParameters m_slicing_params; LayerPtrs m_layers; + LayerPtrs m_belt_truncated_layers; 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 cd0bc34a8e..4770ccebe0 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -1116,6 +1116,9 @@ void PrintObject::clear_layers() for (Layer *l : m_layers) delete l; m_layers.clear(); + for (Layer *l : m_belt_truncated_layers) + delete l; + m_belt_truncated_layers.clear(); } } diff --git a/src/slic3r/GUI/BeltPurgeTower.cpp b/src/slic3r/GUI/BeltPurgeTower.cpp index cf1489d6a0..8c5764072e 100644 --- a/src/slic3r/GUI/BeltPurgeTower.cpp +++ b/src/slic3r/GUI/BeltPurgeTower.cpp @@ -150,6 +150,7 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object const double min_width = n_islands + (n_islands - 1) * gap; const double width = std::max(min_width, print_config.has("belt_purge_tower_width") ? print_config.opt_float("belt_purge_tower_width") : 35.); + const double printable_width = width - (n_islands - 1) * gap; const double layer_h = print_config.has("layer_height") ? print_config.opt_float("layer_height") : 0.2; // Belt geometry. The rotation axis is the gantry tilt axis; the belt @@ -203,15 +204,15 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object const double v_layer = double(filaments.size() - 1) * max_flush; // Height from the per-layer purge demand. A tilted slicing plane cuts a - // width x (height/sin) rectangle out of the bar, so one layer slab absorbs - // width * (height/sin) * layer_height of purge. Solve for the height that + // printable_width x (height/sin) rectangle out of the bars, so one layer + // slab absorbs printable_width * (height/sin) * layer_height of purge. Solve for the height that // holds the worst-case per-layer purge, with eta (infill/perimeter packing) // and a safety margin for the tilt ramps / grid-alignment slop, plus a // minimum so the tower is a real printable body rather than a sliver. const double eta = 0.85; const double safety = 1.6; const double printable_height = printer_config.has("printable_height") ? printer_config.opt_float("printable_height") : 250.; - double height = safety * v_layer * sin_t / (width * layer_h * eta); + double height = safety * v_layer * sin_t / (printable_width * layer_h * eta); height = std::clamp(height, 8.0, std::max(8.0, printable_height)); // --- Idempotence (input-keyed) ---------------------------------------- @@ -229,7 +230,10 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object new_sig.key[7] = static_cast(rot); new_sig.key[8] = std::lround(theta * 10000.0); new_sig.key[9] = q(lat_max); - if (prism_idxs.size() == 1 && new_sig == sig) + const Vec3d plate_origin = plate->get_origin(); + new_sig.key[10] = q(plate_origin.x()); + new_sig.key[11] = q(plate_origin.y()); + if (prism_idxs.size() == 1 && model.objects[size_t(prism_idxs.front())]->instances.size() == 1 && new_sig == sig) return false; // already up to date — do not touch the model // --- Position ---------------------------------------------------------- @@ -243,7 +247,8 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object // belt). The trailing z_max*cot term dominates the bar's own ramp. const double margin = 5.; const double ramp_compensation = height / sin_t; - const double belt_start = std::max(0.0, belt_min - ramp_compensation); // leading ramp, toward Y=0 + const double belt_origin = plate_origin[belt_is_y ? 1 : 0]; + const double belt_start = std::max(belt_origin, belt_min - ramp_compensation); // leading ramp, toward belt origin const double belt_end = belt_max + margin + ramp_compensation + z_max * cot_t; // + parts' top-feature belt reach const double length = std::max(belt_end - belt_start, 10.); const double belt_center = 0.5 * (belt_start + belt_end); @@ -253,7 +258,6 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object // on the bed. The bed (printable_area) is plate-local but model instances // live in the plate's world frame, so add the plate origin's lateral // component. lat_min/lat_max come from instance_bounding_box (world frame). - const Vec3d plate_origin = plate->get_origin(); const double lat_origin = plate_origin[belt_is_y ? 0 : 1]; const double inset = 1.; double lat_center = lat_max + 5. + 0.5 * width; // fallback: just past the parts diff --git a/src/slic3r/GUI/BeltPurgeTower.hpp b/src/slic3r/GUI/BeltPurgeTower.hpp index 5e5b614834..c5a6d301a0 100644 --- a/src/slic3r/GUI/BeltPurgeTower.hpp +++ b/src/slic3r/GUI/BeltPurgeTower.hpp @@ -22,12 +22,12 @@ struct BeltPurgeSignature { bool valid = false; int filament_count = 0; - long key[10] = {0}; // rounded geometry inputs (0.1 mm units) + long key[12] = {0}; // rounded geometry and plate inputs (0.1 mm units) bool operator==(const BeltPurgeSignature &o) const { if (valid != o.valid || filament_count != o.filament_count) return false; - for (int i = 0; i < 10; ++i) + for (int i = 0; i < 12; ++i) if (key[i] != o.key[i]) return false; return true; diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index 71a111ef29..7912e6aae9 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -999,7 +999,7 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in toggle_line("wipe_tower_extra_rib_length", have_rib_wall); toggle_line("wipe_tower_rib_width", have_rib_wall); toggle_line("wipe_tower_fillet_wall", have_rib_wall); - toggle_field("prime_tower_width", have_prime_tower && !have_rib_wall); + toggle_field("prime_tower_width", have_prime_tower && !have_rib_wall && !is_belt_printer); toggle_line("single_extruder_multi_material_priming", !bSEMM && have_prime_tower && supports_wipe_tower_2 && !is_belt_printer); diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index ba3432b4ad..fc70199521 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -335,6 +335,23 @@ TEST_CASE("Belt purge planning requires its managed purge object", "[Print][Purg CHECK_FALSE(print.has_wipe_tower()); } +TEST_CASE("Belt purge rejects multiple managed purge objects", "[Print][PurgeTower][Regression]") +{ + DynamicPrintConfig config = multifilament_config(2, { + { "belt_printer", 1 }, + { "enable_belt_purge_tower", 1 } + }); + + Model model; + Print print; + build_cubes(model, print, config, /*n=*/2, /*overlap=*/false); + for (ModelObject *object : model.objects) + object->config.set_key_value("belt_purge_tower_object", new ConfigOptionBool(true)); + print.apply(model, config); + + CHECK_FALSE(print.validate().string.empty()); +} + TEST_CASE("A default slice emits perimeter, infill, and skirt", "[Print]") { const std::string gcode = slice({ cube(20) }, {