Belt arrange: group colours along the belt, keep the purge tower's strip free, stop the tower at the plate end

On a belt the parts print in belt order, so every colour change between parts
is a filament change. Arrange packs items in extruder order already, but it
grew the pile around its centre, so the colours ended up interleaved. A belt
print now packs from the leading end of the bed, each row filling across the
belt before the pile advances, and the objective charges an item for every
packed part of another colour it does not fully follow along the belt,
counting the tilted layers that reach cot(angle) * height past a part, so
each colour prints as one run. The direction follows the slicing rotation: a
rotation about X prints toward +Y, one about Y toward -X, and a negative angle
flips it. Packing from the edge also means the brim has to be kept on the
bed: a belt brim is printed brim_width wide for every brim type, so that much
is reserved along every edge (between parts the brims may overlap, as on any
printer).

The purge prism is regenerated from the arranged parts, flush with the far
edge of the bed, yet arrange moved it about like a part and packed parts into
the strip it comes back to. Arrange now skips the prism and reserves its strip
with a fixed virtual item, like a bed exclusion area, whenever the parts use
more than one filament.

The prism's length follows the parts plus a ramp per unit of height; with the
height at its cap that ran 100 mm past the end of a 500 mm belt and the project
could not print. The bar now stops at the plate end, and the purge planner's
existing warning reports what the shortened bar cannot absorb.
This commit is contained in:
harrierpigeon
2026-10-03 12:03:18 -05:00
parent 488c6d8226
commit 6b304884d2
7 changed files with 254 additions and 26 deletions
+93 -4
View File
@@ -15,6 +15,9 @@
#include "slic3r/GUI/GUI_ObjectList.hpp"
#include "libnest2d/common.hpp"
#include "libslic3r/Geometry.hpp"
#include <cmath>
#include <set>
#define SAVE_ARRANGE_POLY 0
@@ -63,6 +66,14 @@ public:
}
};
// The belt purge prism is generated from the arranged parts (ensure_belt_purge_tower),
// so arrange neither moves it nor packs around it; it reserves the prism's strip instead.
static bool is_belt_purge_prism(const ModelObject *mo)
{
const ConfigOption *opt = mo->config.option("belt_purge_tower_object");
return opt != nullptr && opt->getBool();
}
// BBS: add partplate logic
static WipeTower get_wipe_tower(const Plater &plater, int plate_idx)
{
@@ -77,6 +88,63 @@ arrangement::ArrangePolygon get_wipetower_arrange_poly(WipeTower* tower)
return ap;
}
// Belt printers pack their parts against the edges of the bed, so what has to stay
// free there is reserved with fixed virtual items on every plate, the way the bed's
// own exclusion areas are:
// - the strip the purge prism comes back to, flush with the far lateral edge (see
// ensure_belt_purge_tower), when the parts use more than one filament;
// - the brim along every edge: a belt brim is printed brim_width wide for every brim
// type but none. Between parts the brims may overlap, as on any printer.
void ArrangeJob::prepare_belt_regions(int num_plates)
{
if (!params.is_belt || params.is_seq_print)
return;
const DynamicPrintConfig &config = wxGetApp().preset_bundle->full_config();
const BoundingBoxf bed = get_extents(config.opt<ConfigOptionPoints>("printable_area")->values);
const bool belt_is_y = params.belt_axis == 1;
std::vector<BoundingBoxf> regions;
std::set<int> filaments;
for (const ArrangePolygons *items : { &m_selected, &m_unselected })
for (const ArrangePolygon &ap : *items)
if (!ap.is_virt_object)
filaments.insert(ap.extrude_ids.begin(), ap.extrude_ids.end());
if (config.opt_bool("enable_belt_purge_tower") && filaments.size() > 1) {
// The prism is at least one millimetre per island plus the gaps between them.
const int islands = int(filaments.size()) - 1;
const double width = std::max(config.opt_float("belt_purge_tower_width"), 2. * islands - 1.) + 1.; // + the prism's edge inset
BoundingBoxf strip = bed;
if (belt_is_y)
strip.min.x() = std::max(bed.min.x(), bed.max.x() - width);
else
strip.min.y() = std::max(bed.min.y(), bed.max.y() - width);
regions.push_back(strip);
}
// Virtual items are inflated by the one millimetre exclusion gap already.
const double brim = config.opt_enum<BrimType>("brim_type") == btNoBrim ? 0. :
config.opt_float("brim_width") + config.opt_float("brim_object_gap") + config.opt_float("extra_brim_width") - 1.;
if (brim > 0.) {
regions.emplace_back(bed.min, Vec2d(bed.min.x() + brim, bed.max.y()));
regions.emplace_back(Vec2d(bed.max.x() - brim, bed.min.y()), bed.max);
regions.emplace_back(bed.min, Vec2d(bed.max.x(), bed.min.y() + brim));
regions.emplace_back(Vec2d(bed.min.x(), bed.max.y() - brim), bed.max);
}
for (int j = 0; j < num_plates; ++j)
for (size_t i = 0; i < regions.size(); ++i) {
ArrangePolygon ap;
ap.poly.contour = scaled(regions[i]).polygon();
ap.translation = Vec2crd(0, 0);
ap.rotation = 0.f;
ap.is_virt_object = true;
ap.bed_idx = j;
ap.height = 1;
ap.name = "BeltRegion" + std::to_string(i);
m_unselected.emplace_back(std::move(ap));
}
}
void ArrangeJob::clear_input()
{
const Model &model = m_plater->model();
@@ -128,6 +196,8 @@ void ArrangeJob::prepare_selected() {
for (size_t oidx = 0; oidx < model.objects.size(); ++oidx) {
const Selection::InstanceIdxsList* instlist = obj_sel[oidx];
ModelObject* mo = model.objects[oidx];
if (is_belt_purge_prism(mo))
continue;
std::vector<bool> inst_sel(mo->instances.size(), false);
@@ -176,6 +246,7 @@ void ArrangeJob::prepare_selected() {
}
prepare_wipe_tower();
prepare_belt_regions(MAX_NUM_PLATES);
// The strides have to be removed from the fixed items. For the
@@ -206,6 +277,8 @@ void ArrangeJob::prepare_all() {
// Go through the objects and check if inside the selection
for (size_t oidx = 0; oidx < model.objects.size(); ++oidx) {
ModelObject *mo = model.objects[oidx];
if (is_belt_purge_prism(mo))
continue;
for (size_t i = 0; i < mo->instances.size(); ++i) {
ModelInstance * mi = mo->instances[i];
@@ -252,6 +325,7 @@ void ArrangeJob::prepare_all() {
// add the virtual object into unselect list if has
plate_list.preprocess_exclude_areas(m_unselected, enable_wrapping, MAX_NUM_PLATES);
prepare_belt_regions(MAX_NUM_PLATES);
}
arrangement::ArrangePolygon estimate_wipe_tower_info(int plate_index, std::set<int>& extruder_ids)
@@ -290,10 +364,9 @@ void ArrangeJob::prepare_wipe_tower()
bool enable_prime_tower = op && op->getBool();
if (!enable_prime_tower || params.is_seq_print) return;
// Belt printers have no classic wipe tower; purging goes into the belt
// purge prism, which is a real model object and arranges like any other.
if (const auto *belt_opt = wxGetApp().preset_bundle->printers.get_edited_preset().config.option<ConfigOptionBool>("belt_printer");
belt_opt && belt_opt->value)
// Belt printers have no classic wipe tower; purging goes into the belt purge
// prism, whose strip prepare_belt_regions() reserves.
if (params.is_belt)
return;
bool smooth_timelapse = false;
@@ -399,6 +472,8 @@ void ArrangeJob::prepare_partplate() {
for (size_t oidx = 0; oidx < model.objects.size(); ++oidx)
{
ModelObject* mo = model.objects[oidx];
if (is_belt_purge_prism(mo))
continue;
for (size_t inst_idx = 0; inst_idx < mo->instances.size(); ++inst_idx)
{
bool in_plate = plate->contain_instance(oidx, inst_idx) || plate->intersect_instance(oidx, inst_idx);
@@ -434,6 +509,7 @@ void ArrangeJob::prepare_partplate() {
// add the virtual object into unselect list if has
plate_list.preprocess_exclude_areas(m_unselected, enable_wrapping, current_plate_index + 1);
prepare_belt_regions(current_plate_index + 1);
}
//BBS: add partplate logic
@@ -785,6 +861,19 @@ arrangement::ArrangeParams init_arrange_params(Plater *p)
params.is_seq_print = settings.is_seq_print;
params.min_obj_distance = scaled(settings.distance);
params.align_to_y_axis = settings.align_to_y_axis;
if (print_config.belt_printer.value) {
// Parts print in belt order: the belt runs across the gantry's tilt axis, a
// rotation about X prints toward +Y and one about Y toward -X (see
// BeltTransform), a negative angle flips that, and a tilted layer reaches
// cot(angle) * height past a part's far edge.
const BeltRotationAxis axis = print_config.belt_slice_rotation.value;
const double angle = print_config.belt_slice_rotation_angle.value;
const bool tilted = (axis == BeltRotationAxis::X || axis == BeltRotationAxis::Y) && std::abs(angle) > EPSILON;
params.is_belt = true;
params.belt_axis = axis == BeltRotationAxis::Y ? 0 : 1;
params.belt_reversed = tilted && ((axis == BeltRotationAxis::Y) != (angle < 0.));
params.belt_tilt_slope = tilted ? float(1. / std::tan(Geometry::deg2rad(std::clamp(std::abs(angle), 5., 90.)))) : 0.f;
}
int state = p->get_prepare_state();
if (state == Job::JobPrepareState::PREPARE_STATE_MENU) {