Belt purge tower: one prism per plate

ensure_belt_purge_tower only ever looked at the current plate and kept a single
prism, so the other plates had no tower and switching plates moved the one
prism around. Every plate is now planned on its own: a prism that lies on no
plate is stale, a plate whose prism matches its recorded inputs is left alone,
the rest are deleted and recreated, highest index first.
This commit is contained in:
harrierpigeon
2026-10-03 18:59:01 -05:00
parent ca934716f5
commit 6a0d07664f
3 changed files with 313 additions and 258 deletions
+305 -253
View File
@@ -33,28 +33,45 @@ namespace GUI {
// is sized so each tilted slicing plane's cross-section through the prism can
// absorb the worst-case purge volume of one layer; length follows the printed
// objects along the belt (plus ramp/height compensation at both tilted ends).
bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, ObjectList *obj_list, BeltPurgeSignature &sig)
bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, ObjectList *obj_list, std::vector<BeltPurgeSignature> &sigs)
{
auto is_prism = [](const ModelObject *mo) {
const ConfigOption *opt = mo->config.option("belt_purge_tower_object");
return opt != nullptr && opt->getBool();
};
std::vector<int> prism_idxs;
// Every plate gets its own prism. A prism belongs to the plate it lies on; one
// that lies on no plate is stale.
const int plate_count = partplate_list.get_plate_count();
sigs.resize(size_t(plate_count));
std::vector<std::vector<int>> prisms_by_plate(static_cast<size_t>(plate_count));
std::vector<int> stale_prisms;
for (int i = 0; i < (int) model.objects.size(); ++i)
if (is_prism(model.objects[i]))
prism_idxs.push_back(i);
if (is_prism(model.objects[i])) {
const int plate_idx = model.objects[i]->instances.empty() ? -1 : partplate_list.find_instance(i, 0);
if (plate_idx >= 0 && plate_idx < plate_count)
prisms_by_plate[size_t(plate_idx)].push_back(i);
else
stale_prisms.push_back(i);
}
// Deletes prism objects, keeping the sidebar and part plates in sync
// (same primitives as Plater::priv::remove(), minus scene update — the
// caller refreshes the scene).
auto remove_prisms = [&](const std::vector<int> &idxs) {
// caller refreshes the scene). Highest index first, so the others stay valid.
auto remove_prisms = [&](std::vector<int> idxs) {
std::sort(idxs.begin(), idxs.end());
for (auto it = idxs.rbegin(); it != idxs.rend(); ++it) {
model.delete_object(size_t(*it));
partplate_list.notify_instance_removed(*it, -1);
obj_list->delete_object_from_list(size_t(*it));
}
};
auto all_prisms = [&]() {
std::vector<int> all = stale_prisms;
for (const auto &v : prisms_by_plate)
all.insert(all.end(), v.begin(), v.end());
return all;
};
// Cheap early-out for non-belt printers: only belt printers ever get a belt
// purge tower, and this runs on every background-process tick — so avoid the
@@ -63,10 +80,11 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object
{
const auto *belt_pre = wxGetApp().preset_bundle->printers.get_edited_preset().config.option<ConfigOptionBool>("belt_printer");
if (belt_pre == nullptr || !belt_pre->value) {
sig = BeltPurgeSignature{};
if (prism_idxs.empty())
std::fill(sigs.begin(), sigs.end(), BeltPurgeSignature{});
const std::vector<int> all = all_prisms();
if (all.empty())
return false;
remove_prisms(prism_idxs);
remove_prisms(all);
return true;
}
}
@@ -93,97 +111,15 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object
const auto *seq_opt = print_config.option<ConfigOptionEnum<PrintSequence>>("print_sequence");
const bool by_object = seq_opt != nullptr && seq_opt->value == PrintSequence::ByObject;
// Filaments used and bounding extent of the non-prism objects on the
// current plate (1-based filament ids; volume extruder 0 = object default).
PartPlate *plate = partplate_list.get_curr_plate();
std::set<int> filaments;
double x_min = std::numeric_limits<double>::max();
double x_max = -std::numeric_limits<double>::max();
double y_min = std::numeric_limits<double>::max();
double y_max = -std::numeric_limits<double>::max();
double z_max = 0.;
bool have_objects = false;
if (belt && plate != nullptr) {
for (int obj_idx = 0; obj_idx < (int) model.objects.size(); ++obj_idx) {
const ModelObject *mo = model.objects[obj_idx];
if (is_prism(mo))
continue;
int obj_extruder = 1;
if (const ConfigOption *opt = mo->config.option("extruder"); opt != nullptr && opt->getInt() > 0)
obj_extruder = opt->getInt();
bool any_instance_on_plate = false;
for (int inst_idx = 0; inst_idx < (int) mo->instances.size(); ++inst_idx) {
if (!plate->contain_instance_totally(obj_idx, inst_idx))
continue;
any_instance_on_plate = true;
const BoundingBoxf3 bb = mo->instance_bounding_box(inst_idx);
x_min = std::min(x_min, bb.min.x());
x_max = std::max(x_max, bb.max.x());
y_min = std::min(y_min, bb.min.y());
y_max = std::max(y_max, bb.max.y());
z_max = std::max(z_max, bb.max.z());
}
if (!any_instance_on_plate)
continue;
have_objects = true;
for (const ModelVolume *mv : mo->volumes)
for (int e : mv->get_extruders())
filaments.insert(e > 0 ? e : obj_extruder);
}
}
// Mixed filament slots are expanded to their physical components below.
std::vector<unsigned char> is_mixed;
std::vector<std::string> comp_strs;
if (const auto *o = full_cfg.option<ConfigOptionBools>("filament_is_mixed"))
is_mixed = o->values;
if (const auto *o = full_cfg.option<ConfigOptionStrings>("filament_mixed_components"))
comp_strs = o->values;
// A mixed filament slot is VIRTUAL: it never reaches a nozzle. At slice time
// ToolOrdering::resolve_mixed_filaments() replaces it with its physical
// components, so the toolchanges the prism has to absorb are between those
// components, not to the mixed slot itself. Counting the slot as a filament
// of its own therefore over-provisions the prism by one island per mixed slot
// -- the "extra purge tower" -- and, when every component is already used by
// another object, by an island that can never be reached at all.
//
// Expand here with the same helper the backend uses (Print.cpp's sequential
// path), so the GUI sizes the prism against the same filament set the slicer
// will actually produce. No-op when no filament is mixed.
{
// Copies, not references: a ternary with an empty-vector fallback would bind
// a reference to a temporary.
std::vector<unsigned char> is_mixed;
std::vector<std::string> comp_strs;
if (const auto *o = full_cfg.option<ConfigOptionBools>("filament_is_mixed"))
is_mixed = o->values;
if (const auto *o = full_cfg.option<ConfigOptionStrings>("filament_mixed_components"))
comp_strs = o->values;
if (has_any_mixed_filament(is_mixed)) {
std::vector<unsigned int> zero_based;
zero_based.reserve(filaments.size());
for (int f : filaments)
if (f > 0)
zero_based.push_back((unsigned int) (f - 1));
zero_based = expand_mixed_filaments(zero_based, is_mixed, comp_strs);
filaments.clear();
for (unsigned int f : zero_based)
filaments.insert((int) f + 1);
}
}
const bool wanted = belt && prime_tower_enabled && !by_object && have_objects && filaments.size() > 1;
if (!wanted) {
sig = BeltPurgeSignature{};
if (prism_idxs.empty())
return false;
remove_prisms(prism_idxs);
return true;
}
// --- Sizing -----------------------------------------------------------
const int n_islands = std::max(1, (int) filaments.size() - 1);
const double gap = 1.0;
// Every disconnected island needs at least 1 mm of printable width. Honor
// the configured total width whenever possible, but never let the island
// layout silently grow past the footprint used for placement.
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 gap = 1.0;
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
@@ -201,15 +137,6 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object
const double sin_t = std::sin(theta);
const double cot_t = std::cos(theta) / sin_t;
// Parts' extent along the belt-travel axis and the lateral (across-belt) axis.
const double belt_min = belt_is_y ? y_min : x_min;
const double belt_max = belt_is_y ? y_max : x_max;
const double lat_min = belt_is_y ? x_min : y_min;
const double lat_max = belt_is_y ? x_max : y_max;
// Worst-case purge volume of one layer: up to (filament count - 1)
// toolchanges, each needing the worst flush matrix entry (mirrors the
// volume selection in Print::_plan_belt_purge()).
// NOTE: both purge_in_prime_tower and single_extruder_multi_material are
// PRINTER options (Preset.cpp s_Preset_printer_options) — read them from the
// printer preset. Reading purge_in_prime_tower from the print preset returns
@@ -218,71 +145,9 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object
// the backend Print::_plan_belt_purge() which reads both from the merged config.
const bool use_matrix = (printer_config.has("purge_in_prime_tower") && printer_config.opt_bool("purge_in_prime_tower"))
&& (printer_config.has("single_extruder_multi_material") && printer_config.opt_bool("single_extruder_multi_material"));
double max_flush = print_config.has("prime_volume") ? print_config.opt_float("prime_volume") : 45.;
if (use_matrix) {
const size_t extruder_nums = wxGetApp().preset_bundle->get_printer_extruder_count();
const std::vector<double> matrix = get_flush_volumes_matrix(
project_config.option<ConfigOptionFloats>("flush_volumes_matrix")->values, 0, extruder_nums);
const auto * multi_opt = project_config.option<ConfigOptionFloats>("flush_multiplier");
const double multiplier = multi_opt != nullptr && !multi_opt->values.empty() ? multi_opt->get_at(0) : 1.;
const int n_total = (int) (std::sqrt(double(matrix.size())) + 0.5);
double m = 0.;
for (int i : filaments)
for (int j : filaments)
if (i != j && i <= n_total && j <= n_total)
m = std::max(m, matrix[size_t(i - 1) * n_total + size_t(j - 1)]);
if (m > 0.)
max_flush = m * multiplier;
}
const double v_layer = double(filaments.size() - 1) * max_flush;
// Height from the per-layer purge demand. A tilted slicing plane cuts a
// 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 prime_volume = print_config.has("prime_volume") ? print_config.opt_float("prime_volume") : 45.;
const double printable_height = printer_config.has("printable_height") ? printer_config.opt_float("printable_height") : 250.;
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) ----------------------------------------
auto q = [](double v) { return std::lround(v * 10.0); }; // 0.1 mm quantization
BeltPurgeSignature new_sig;
new_sig.valid = true;
new_sig.filament_count = (int) filaments.size();
new_sig.key[0] = q(width);
new_sig.key[1] = q(layer_h);
new_sig.key[2] = q(height);
new_sig.key[3] = q(belt_min);
new_sig.key[4] = q(belt_max);
new_sig.key[5] = q(lat_min);
new_sig.key[6] = q(z_max);
new_sig.key[7] = static_cast<long>(rot);
new_sig.key[8] = std::lround(theta * 10000.0);
new_sig.key[9] = q(lat_max);
const Vec3d plate_origin = plate->get_origin();
new_sig.key[10] = q(plate_origin.x());
new_sig.key[11] = q(plate_origin.y());
if (const auto *bed_opt = printer_config.option<ConfigOptionPoints>("printable_area"); bed_opt != nullptr && !bed_opt->values.empty()) {
const BoundingBoxf bed = get_extents(bed_opt->values);
new_sig.key[12] = q(bed.max.x());
new_sig.key[13] = q(bed.max.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 ----------------------------------------------------------
// Belt-travel axis. With the mesh rotated by theta before slicing, a machine
// point (y,z) maps to slicing-Z = y*sin(theta) + z*cos(theta). The parts
// occupy slicing-Z in [y_min*sin, y_max*sin + z_max*cos], and the bar's
// FULL-cross-section region (the part not in a triangular end ramp) spans
// slicing-Z [belt_start*sin + H*cos, belt_end*sin]. Covering the parts'
// whole band needs belt_start <= y_min - H*cot (bar's own leading ramp) and
// belt_end >= y_max + z_max*cot (parts' top features print further up the
// belt). The trailing z_max*cot term dominates the bar's own ramp.
// The bed (printable_area) is plate-local but model instances live in the
// plate's world frame, so the plate origin is added to every bed coordinate.
const double inset = 1.;
@@ -291,34 +156,225 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object
bed_opt != nullptr && !bed_opt->values.empty())
bed_ext = get_extents(bed_opt->values);
// Along the belt the bar stops at the end of the plate: a longer bar cannot be
// printed, and the cross-sections it loses there are reported by the purge
// planner when the parts' last layers then purge more than the bar holds.
const double margin = 5.;
const double ramp_compensation = height / sin_t;
const double belt_origin = plate_origin[belt_is_y ? 1 : 0];
double belt_end = belt_max + margin + ramp_compensation + z_max * cot_t; // + parts' top-feature belt reach
if (bed_ext.defined)
belt_end = std::min(belt_end, belt_origin + (belt_is_y ? bed_ext.max.y() : bed_ext.max.x()) - inset);
const double belt_start = std::max(belt_origin, std::min(belt_min - ramp_compensation, belt_end - 10.)); // leading ramp, toward belt origin
const double length = std::max(belt_end - belt_start, 10.);
belt_end = belt_start + length;
const double belt_center = 0.5 * (belt_start + belt_end);
// What each plate needs, decided before anything is deleted or created.
struct Plan
{
bool wanted = false;
BeltPurgeSignature sig;
int n_islands = 1;
double w_sub = 0., length = 0., height = 0.;
Vec3d center = Vec3d::Zero();
};
std::vector<Plan> plans(static_cast<size_t>(plate_count));
// Across-belt: flush against the bed's maximum edge, inset by half the bar
// width so the bar's far edge sits on the boundary and the whole bar stays
// on the bed. lat_min/lat_max come from instance_bounding_box (world frame).
const double lat_origin = plate_origin[belt_is_y ? 0 : 1];
double lat_center = lat_max + 5. + 0.5 * width; // fallback: just past the parts
if (bed_ext.defined) {
const double bed_lat_max = belt_is_y ? bed_ext.max.x() : bed_ext.max.y();
lat_center = lat_origin + bed_lat_max - inset - 0.5 * width;
for (int plate_idx = 0; plate_idx < plate_count; ++plate_idx) {
Plan &plan = plans[size_t(plate_idx)];
PartPlate *plate = partplate_list.get_plate(plate_idx);
// Filaments used and bounding extent of the non-prism objects on this
// plate (1-based filament ids; volume extruder 0 = object default).
std::set<int> filaments;
double x_min = std::numeric_limits<double>::max();
double x_max = -std::numeric_limits<double>::max();
double y_min = std::numeric_limits<double>::max();
double y_max = -std::numeric_limits<double>::max();
double z_max = 0.;
bool have_objects = false;
if (belt && plate != nullptr) {
for (int obj_idx = 0; obj_idx < (int) model.objects.size(); ++obj_idx) {
const ModelObject *mo = model.objects[obj_idx];
if (is_prism(mo))
continue;
int obj_extruder = 1;
if (const ConfigOption *opt = mo->config.option("extruder"); opt != nullptr && opt->getInt() > 0)
obj_extruder = opt->getInt();
bool any_instance_on_plate = false;
for (int inst_idx = 0; inst_idx < (int) mo->instances.size(); ++inst_idx) {
if (!plate->contain_instance_totally(obj_idx, inst_idx))
continue;
any_instance_on_plate = true;
const BoundingBoxf3 bb = mo->instance_bounding_box(inst_idx);
x_min = std::min(x_min, bb.min.x());
x_max = std::max(x_max, bb.max.x());
y_min = std::min(y_min, bb.min.y());
y_max = std::max(y_max, bb.max.y());
z_max = std::max(z_max, bb.max.z());
}
if (!any_instance_on_plate)
continue;
have_objects = true;
for (const ModelVolume *mv : mo->volumes)
for (int e : mv->get_extruders())
filaments.insert(e > 0 ? e : obj_extruder);
}
}
// A mixed filament slot is VIRTUAL: it never reaches a nozzle. At slice time
// ToolOrdering::resolve_mixed_filaments() replaces it with its physical
// components, so the toolchanges the prism has to absorb are between those
// components, not to the mixed slot itself. Counting the slot as a filament
// of its own therefore over-provisions the prism by one island per mixed slot
// -- the "extra purge tower" -- and, when every component is already used by
// another object, by an island that can never be reached at all.
//
// Expand here with the same helper the backend uses (Print.cpp's sequential
// path), so the GUI sizes the prism against the same filament set the slicer
// will actually produce. No-op when no filament is mixed.
if (has_any_mixed_filament(is_mixed)) {
std::vector<unsigned int> zero_based;
zero_based.reserve(filaments.size());
for (int f : filaments)
if (f > 0)
zero_based.push_back((unsigned int) (f - 1));
zero_based = expand_mixed_filaments(zero_based, is_mixed, comp_strs);
filaments.clear();
for (unsigned int f : zero_based)
filaments.insert((int) f + 1);
}
plan.wanted = belt && prime_tower_enabled && !by_object && have_objects && filaments.size() > 1;
if (!plan.wanted)
continue;
// --- Sizing -----------------------------------------------------------
const int n_islands = std::max(1, (int) filaments.size() - 1);
// Every disconnected island needs at least 1 mm of printable width. Honor
// the configured total width whenever possible, but never let the island
// layout silently grow past the footprint used for placement.
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;
// Parts' extent along the belt-travel axis and the lateral (across-belt) axis.
const double belt_min = belt_is_y ? y_min : x_min;
const double belt_max = belt_is_y ? y_max : x_max;
const double lat_min = belt_is_y ? x_min : y_min;
const double lat_max = belt_is_y ? x_max : y_max;
// Worst-case purge volume of one layer: up to (filament count - 1)
// toolchanges, each needing the worst flush matrix entry (mirrors the
// volume selection in Print::_plan_belt_purge()).
double max_flush = prime_volume;
if (use_matrix) {
const size_t extruder_nums = wxGetApp().preset_bundle->get_printer_extruder_count();
const std::vector<double> matrix = get_flush_volumes_matrix(
project_config.option<ConfigOptionFloats>("flush_volumes_matrix")->values, 0, extruder_nums);
const auto * multi_opt = project_config.option<ConfigOptionFloats>("flush_multiplier");
const double multiplier = multi_opt != nullptr && !multi_opt->values.empty() ? multi_opt->get_at(0) : 1.;
const int n_total = (int) (std::sqrt(double(matrix.size())) + 0.5);
double m = 0.;
for (int i : filaments)
for (int j : filaments)
if (i != j && i <= n_total && j <= n_total)
m = std::max(m, matrix[size_t(i - 1) * n_total + size_t(j - 1)]);
if (m > 0.)
max_flush = m * multiplier;
}
const double v_layer = double(filaments.size() - 1) * max_flush;
// Height from the per-layer purge demand. A tilted slicing plane cuts a
// 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;
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) ----------------------------------------
auto q = [](double v) { return std::lround(v * 10.0); }; // 0.1 mm quantization
BeltPurgeSignature &new_sig = plan.sig;
new_sig.valid = true;
new_sig.filament_count = (int) filaments.size();
new_sig.key[0] = q(width);
new_sig.key[1] = q(layer_h);
new_sig.key[2] = q(height);
new_sig.key[3] = q(belt_min);
new_sig.key[4] = q(belt_max);
new_sig.key[5] = q(lat_min);
new_sig.key[6] = q(z_max);
new_sig.key[7] = static_cast<long>(rot);
new_sig.key[8] = std::lround(theta * 10000.0);
new_sig.key[9] = q(lat_max);
const Vec3d plate_origin = plate->get_origin();
new_sig.key[10] = q(plate_origin.x());
new_sig.key[11] = q(plate_origin.y());
if (bed_ext.defined) {
new_sig.key[12] = q(bed_ext.max.x());
new_sig.key[13] = q(bed_ext.max.y());
}
// --- Position ----------------------------------------------------------
// Belt-travel axis. With the mesh rotated by theta before slicing, a machine
// point (y,z) maps to slicing-Z = y*sin(theta) + z*cos(theta). The parts
// occupy slicing-Z in [y_min*sin, y_max*sin + z_max*cos], and the bar's
// FULL-cross-section region (the part not in a triangular end ramp) spans
// slicing-Z [belt_start*sin + H*cos, belt_end*sin]. Covering the parts'
// whole band needs belt_start <= y_min - H*cot (bar's own leading ramp) and
// belt_end >= y_max + z_max*cot (parts' top features print further up the
// belt). The trailing z_max*cot term dominates the bar's own ramp.
//
// Along the belt the bar stops at the end of the plate: a longer bar cannot be
// printed, and the cross-sections it loses there are reported by the purge
// planner when the parts' last layers then purge more than the bar holds.
const double margin = 5.;
const double ramp_compensation = height / sin_t;
const double belt_origin = plate_origin[belt_is_y ? 1 : 0];
double belt_end = belt_max + margin + ramp_compensation + z_max * cot_t; // + parts' top-feature belt reach
if (bed_ext.defined)
belt_end = std::min(belt_end, belt_origin + (belt_is_y ? bed_ext.max.y() : bed_ext.max.x()) - inset);
const double belt_start = std::max(belt_origin, std::min(belt_min - ramp_compensation, belt_end - 10.)); // leading ramp, toward belt origin
const double length = std::max(belt_end - belt_start, 10.);
belt_end = belt_start + length;
const double belt_center = 0.5 * (belt_start + belt_end);
// Across-belt: flush against the bed's maximum edge, inset by half the bar
// width so the bar's far edge sits on the boundary and the whole bar stays
// on the bed. lat_min/lat_max come from instance_bounding_box (world frame).
const double lat_origin = plate_origin[belt_is_y ? 0 : 1];
double lat_center = lat_max + 5. + 0.5 * width; // fallback: just past the parts
if (bed_ext.defined) {
const double bed_lat_max = belt_is_y ? bed_ext.max.x() : bed_ext.max.y();
lat_center = lat_origin + bed_lat_max - inset - 0.5 * width;
}
plan.n_islands = n_islands;
plan.w_sub = (width - (n_islands - 1) * gap) / n_islands;
plan.length = length;
plan.height = height;
plan.center = Vec3d(belt_is_y ? lat_center : belt_center,
belt_is_y ? belt_center : lat_center,
0.5 * height);
}
const Vec3d desired_center(belt_is_y ? lat_center : belt_center,
belt_is_y ? belt_center : lat_center,
0.5 * height);
// --- Decide ---------------------------------------------------------------
// A plate whose prism exists and matches its recorded inputs is left alone, so
// subsequent ticks are no-ops until the parts/config actually change.
std::vector<int> to_delete = stale_prisms;
std::vector<int> to_create;
for (int plate_idx = 0; plate_idx < plate_count; ++plate_idx) {
const Plan &plan = plans[size_t(plate_idx)];
const std::vector<int> &existing = prisms_by_plate[size_t(plate_idx)];
BeltPurgeSignature &sig = sigs[size_t(plate_idx)];
if (!plan.wanted) {
sig = BeltPurgeSignature{};
to_delete.insert(to_delete.end(), existing.begin(), existing.end());
} else if (existing.size() == 1 && model.objects[size_t(existing.front())]->instances.size() == 1 && plan.sig == sig) {
continue;
} else {
to_delete.insert(to_delete.end(), existing.begin(), existing.end());
to_create.push_back(plate_idx);
}
}
if (to_delete.empty() && to_create.empty())
return false;
remove_prisms(to_delete);
// --- (Re)create -----------------------------------------------------------
// Build the prism as N DISCONNECTED sub-bars side by side across the belt,
// N = (filaments - 1) = the worst-case number of toolchanges on one layer.
// Why: mark_wiping_extrusions overrides whole extrusion-entity COLLECTIONS,
@@ -337,68 +393,64 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object
// reintroduce the multi-swap-per-layer absorption bug; a hair over ~2 line
// widths also keeps gap-fill from bridging them. 1 mm is about as close as
// they can butt up while staying individually purgeable.
const double w_sub = (width - (n_islands - 1) * gap) / n_islands;
for (int plate_idx : to_create) {
const Plan &plan = plans[size_t(plate_idx)];
// --- (Re)create ---------------------------------------------------------
if (!prism_idxs.empty())
remove_prisms(prism_idxs);
TriangleMesh prism_mesh;
for (int i = 0; i < plan.n_islands; ++i) {
const double lat_off = i * (plan.w_sub + gap);
// Box dims: lateral = w_sub, along-belt = length, vertical = height.
TriangleMesh box = belt_is_y ? make_cube(plan.w_sub, plan.length, plan.height) // X = lateral, Y = belt
: make_cube(plan.length, plan.w_sub, plan.height); // X = belt, Y = lateral
box.translate(belt_is_y ? Vec3f((float) lat_off, 0.f, 0.f) : Vec3f(0.f, (float) lat_off, 0.f));
prism_mesh.merge(box);
}
TriangleMesh prism_mesh;
for (int i = 0; i < n_islands; ++i) {
const double lat_off = i * (w_sub + gap);
// Box dims: lateral = w_sub, along-belt = length, vertical = height.
TriangleMesh box = belt_is_y ? make_cube(w_sub, length, height) // X = lateral, Y = belt
: make_cube(length, w_sub, height); // X = belt, Y = lateral
box.translate(belt_is_y ? Vec3f((float) lat_off, 0.f, 0.f) : Vec3f(0.f, (float) lat_off, 0.f));
prism_mesh.merge(box);
ModelObject *new_object = model.add_object();
new_object->name = _u8L("Belt Purge Tower");
new_object->add_instance();
ModelVolume *new_volume = new_object->add_volume(std::move(prism_mesh));
new_volume->name = new_object->name;
auto &cfg = new_object->config;
cfg.set_key_value("belt_purge_tower_object", new ConfigOptionBool(true));
cfg.set_key_value("flush_into_objects", new ConfigOptionBool(true));
cfg.set_key_value("extruder", new ConfigOptionInt(1));
// Sacrificial solid prism: one wall, no shells, dense rectilinear infill —
// every extrusion is overriddable, so the absorbed volume matches the
// cross-section x layer-height estimate used for the height above.
cfg.set_key_value("wall_loops", new ConfigOptionInt(1));
cfg.set_key_value("top_shell_layers", new ConfigOptionInt(0));
cfg.set_key_value("bottom_shell_layers", new ConfigOptionInt(0));
cfg.set_key_value("sparse_infill_density", new ConfigOptionPercent(100));
cfg.set_key_value("sparse_infill_pattern", new ConfigOptionEnum<InfillPattern>(ipRectilinear));
cfg.set_key_value("enable_support", new ConfigOptionBool(false));
cfg.set_key_value("brim_type", new ConfigOptionEnum<BrimType>(btNoBrim));
cfg.set_key_value("seam_slope_type", new ConfigOptionEnum<SeamScarfType>(SeamScarfType::None));
cfg.set_key_value("precise_z_height", new ConfigOptionBool(false));
// Position by the belt-calibration pattern: drop to the bed, then translate
// the instance by the delta between the object's ACTUAL bbox center and the
// target. Setting the instance offset directly is unreliable here — the
// freshly added cube's local frame is not centered, so set_offset() lands
// the min corner (not the center) on the target, leaving the bar centered
// on the bed edge with half of it hanging off.
new_object->invalidate_bounding_box();
new_object->ensure_on_bed();
const BoundingBoxf3 cur = new_object->bounding_box_exact();
new_object->translate_instances(Vec3d(plan.center.x() - cur.center().x(),
plan.center.y() - cur.center().y(),
0.0));
new_object->instances.front()->set_assemble_transformation(new_object->instances.front()->get_transformation());
const size_t obj_idx = model.objects.size() - 1;
// Registers the object in the sidebar and notifies the part plates;
// selection is left untouched (auto-managed object).
obj_list->add_object_to_list(obj_idx, /*call_selection_changed=*/false);
sigs[size_t(plate_idx)] = plan.sig;
}
ModelObject *new_object = model.add_object();
new_object->name = _u8L("Belt Purge Tower");
new_object->add_instance();
ModelVolume *new_volume = new_object->add_volume(std::move(prism_mesh));
new_volume->name = new_object->name;
auto &cfg = new_object->config;
cfg.set_key_value("belt_purge_tower_object", new ConfigOptionBool(true));
cfg.set_key_value("flush_into_objects", new ConfigOptionBool(true));
cfg.set_key_value("extruder", new ConfigOptionInt(1));
// Sacrificial solid prism: one wall, no shells, dense rectilinear infill —
// every extrusion is overriddable, so the absorbed volume matches the
// cross-section x layer-height estimate used for the height above.
cfg.set_key_value("wall_loops", new ConfigOptionInt(1));
cfg.set_key_value("top_shell_layers", new ConfigOptionInt(0));
cfg.set_key_value("bottom_shell_layers", new ConfigOptionInt(0));
cfg.set_key_value("sparse_infill_density", new ConfigOptionPercent(100));
cfg.set_key_value("sparse_infill_pattern", new ConfigOptionEnum<InfillPattern>(ipRectilinear));
cfg.set_key_value("enable_support", new ConfigOptionBool(false));
cfg.set_key_value("brim_type", new ConfigOptionEnum<BrimType>(btNoBrim));
cfg.set_key_value("seam_slope_type", new ConfigOptionEnum<SeamScarfType>(SeamScarfType::None));
cfg.set_key_value("precise_z_height", new ConfigOptionBool(false));
// Position by the belt-calibration pattern: drop to the bed, then translate
// the instance by the delta between the object's ACTUAL bbox center and the
// target. Setting the instance offset directly is unreliable here — the
// freshly added cube's local frame is not centered, so set_offset() lands
// the min corner (not the center) on the target, leaving the bar centered
// on the bed edge with half of it hanging off.
new_object->invalidate_bounding_box();
new_object->ensure_on_bed();
const BoundingBoxf3 cur = new_object->bounding_box_exact();
new_object->translate_instances(Vec3d(desired_center.x() - cur.center().x(),
desired_center.y() - cur.center().y(),
0.0));
new_object->instances.front()->set_assemble_transformation(new_object->instances.front()->get_transformation());
const size_t obj_idx = model.objects.size() - 1;
// Registers the object in the sidebar and notifies the part plates;
// selection is left untouched (auto-managed object).
obj_list->add_object_to_list(obj_idx, /*call_selection_changed=*/false);
// Record the inputs that produced this prism so subsequent ticks are no-ops
// until the parts/config actually change.
sig = new_sig;
return true;
}
+6 -3
View File
@@ -1,5 +1,7 @@
#pragma once
#include <vector>
// ORCA-Belt: auto-managed purge prism for belt printers.
//
// Kept in its own translation unit (not buried in Plater.cpp) so it stays out
@@ -34,12 +36,13 @@ struct BeltPurgeSignature
}
};
// Keep the auto-generated belt purge prism in sync with the current config and
// plate contents. Creates / updates / removes the marked prism ModelObject.
// Keep the auto-generated belt purge prisms, one per plate, in sync with the
// current config and plate contents. Creates / updates / removes the marked prism
// ModelObjects; `sigs` holds the last inputs per plate index.
// Returns true when the model was mutated (caller should refresh the scene).
// Runs on every background-process update, so it is idempotent: it only mutates
// the model when the desired prism differs from the cached signature in `sig`.
bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, ObjectList *obj_list, BeltPurgeSignature &sig);
bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, ObjectList *obj_list, std::vector<BeltPurgeSignature> &sigs);
} // namespace GUI
} // namespace Slic3r
+2 -2
View File
@@ -6993,7 +6993,7 @@ struct Plater::priv
// config and plate contents (thin wrapper over GUI::ensure_belt_purge_tower
// in BeltPurgeTower.cpp). Returns true when the model was mutated.
bool ensure_belt_purge_tower();
BeltPurgeSignature m_belt_purge_sig;
std::vector<BeltPurgeSignature> m_belt_purge_sigs;
void delete_all_objects_from_model();
void reset(bool apply_presets_change = false);
void center_selection();
@@ -10633,7 +10633,7 @@ void Plater::priv::process_validation_warnings(const std::vector<StringObjectExc
// wrapper that hands it the model, plates, object list, and cached signature.
bool Plater::priv::ensure_belt_purge_tower()
{
return GUI::ensure_belt_purge_tower(model, partplate_list, sidebar->obj_list(), m_belt_purge_sig);
return GUI::ensure_belt_purge_tower(model, partplate_list, sidebar->obj_list(), m_belt_purge_sigs);
}