mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-15 04:57:34 +00:00
Harden belt purge tower replanning
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -1486,6 +1486,14 @@ StringObjectException Print::validate(std::vector<StringObjectException> *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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<long>(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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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) }, {
|
||||
|
||||
Reference in New Issue
Block a user