From 6b304884d24ded41e428517e995decf5915dca47 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 3 Oct 2026 12:03:18 -0500 Subject: [PATCH] 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. --- src/libslic3r/Arrange.cpp | 93 +++++++++++++++++++++++++--- src/libslic3r/Arrange.hpp | 7 +++ src/slic3r/GUI/BeltPurgeTower.cpp | 34 ++++++++--- src/slic3r/GUI/BeltPurgeTower.hpp | 4 +- src/slic3r/GUI/Jobs/ArrangeJob.cpp | 97 ++++++++++++++++++++++++++++-- src/slic3r/GUI/Jobs/ArrangeJob.hpp | 1 + tests/libslic3r/test_arrange.cpp | 44 ++++++++++++++ 7 files changed, 254 insertions(+), 26 deletions(-) diff --git a/src/libslic3r/Arrange.cpp b/src/libslic3r/Arrange.cpp index fd496537f4..d6e386b5d4 100644 --- a/src/libslic3r/Arrange.cpp +++ b/src/libslic3r/Arrange.cpp @@ -276,7 +276,13 @@ Points get_shrink_bedpts(const DynamicPrintConfig* print_cfg, const ArrangeParam template void fill_config(PConf& pcfg, const ArrangeParams ¶ms) { - if (params.is_seq_print) { + if (params.is_belt) { + // Pack from the end of the belt that prints first. + pcfg.starting_point = !params.belt_reversed ? PConf::Alignment::BOTTOM_LEFT : + params.belt_axis == 1 ? PConf::Alignment::TOP_LEFT : + PConf::Alignment::BOTTOM_RIGHT; + } + else if (params.is_seq_print) { // Start placing the items from the center of the print bed pcfg.starting_point = PConf::Alignment::BOTTOM_LEFT; } @@ -421,7 +427,51 @@ protected: return bindist; } - double dist_to_bin(const Box& ibb, const ClipperLib::IntPoint& origin_pack, typename Packer::PlacementConfig::Alignment starting_point_alignment) + // Belt printers pack from the end of the belt that prints first, and a corner + // packer's checks (pile inside the bin, pack origin) apply to them as well. + bool corner_packing() const { return params.is_belt || m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT; } + + static double at(const Box::PointType &pt, int i) { return double(i == 0 ? getX(pt) : getY(pt)); } + + // Position along the belt in print order: increasing from the end that prints first. + double belt_pos(const Box::PointType &pt) const { return params.belt_reversed ? -at(pt, params.belt_axis) : at(pt, params.belt_axis); } + double belt_start(const Box &bb) const { return belt_pos(params.belt_reversed ? bb.maxCorner() : bb.minCorner()); } + double belt_end(const Box &bb) const { return belt_pos(params.belt_reversed ? bb.minCorner() : bb.maxCorner()); } + + // The corner of the bin the belt pile grows from. + Box::PointType belt_origin() const + { + const Box bb = sl::boundingBox(m_bin); + auto o = bb.minCorner(); + if (params.belt_reversed) { + if (params.belt_axis == 0) setX(o, getX(bb.maxCorner())); + else setY(o, getY(bb.maxCorner())); + } + return o; + } + + // An item's far edge in print order is what it costs (so a row fills across the + // belt before the pile advances), with a slight pull toward the near lateral + // edge and the same penalty as the bottom-left heuristic for sitting outside + // the corner. + double dist_along_belt(const Box &ibb) + { + const Box bin = sl::boundingBox(m_bin); + const int l = 1 - params.belt_axis; + const double lat = at(ibb.minCorner(), l) - at(bin.minCorner(), l); + double d = belt_end(ibb) - belt_start(bin); + d += lat < 0 ? 10 * -lat : 0.1 * lat; + if (double behind = belt_start(ibb) - belt_start(bin); behind < 0) + d += 10 * -behind; + return norm(d); + } + + double corner_bindist(const Box &ibb, const ClipperLib::IntPoint &origin_pack) + { + return params.is_belt ? dist_along_belt(ibb) : dist_for_BOTTOM_LEFT(ibb, origin_pack); + } + + double dist_to_bin(const Box& ibb, const ClipperLib::IntPoint& origin_pack, typename Packer::PlacementConfig::Alignment starting_point_alignment) { double bindist = 0; if (starting_point_alignment == PConfig::Alignment::BOTTOM_LEFT) @@ -510,8 +560,8 @@ protected: // The smalles distance from the arranged pile center: double dist = norm(*(std::min_element(dists.begin(), dists.end()))); - if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT) { - double bindist = dist_for_BOTTOM_LEFT(ibb, origin_pack); + if (corner_packing()) { + double bindist = corner_bindist(ibb, origin_pack); score = 0.2 * dist + 0.8 * bindist; } else { @@ -568,8 +618,8 @@ protected: break; } case LAST_BIG_ITEM: { - if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT) { - score = dist_for_BOTTOM_LEFT(ibb, origin_pack); + if (corner_packing()) { + score = corner_bindist(ibb, origin_pack); } else { if (m_pilebb.defined) @@ -584,8 +634,8 @@ protected: // already processed bigger items. // No need to play around with the anchor points, the center will be // just fine for small items - if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT) - score = dist_for_BOTTOM_LEFT(ibb, origin_pack); + if (corner_packing()) + score = corner_bindist(ibb, origin_pack); else { // Align mainly around existing items score = 0.8 * norm(pl::distance(ibb.center(), bigbb.center()))+ 0.2*norm(pl::distance(ibb.center(), origin_pack)); @@ -686,6 +736,28 @@ protected: score += 1 * (new_extruder_cnt-last_extruder_cnt); } + // On a belt the parts print in belt order, so every colour change between + // parts is a filament change. Items arrive sorted by extruder and the pile + // grows from the leading end; keep each colour's run contiguous by charging + // an item for every packed item of another colour it does not fully follow, + // counting the tilted layers that reach belt_tilt_slope * height past that + // item's far edge. + if (params.is_belt && !params.is_seq_print) { + const std::set item_colours(item.extrude_ids.begin(), item.extrude_ids.end()); + const double item_start = belt_start(ibb); + for (Item &p : m_items) { + if (p.is_virt_object) + continue; + const std::set p_colours(p.extrude_ids.begin(), p.extrude_ids.end()); + const bool same_colour = std::includes(item_colours.begin(), item_colours.end(), p_colours.begin(), p_colours.end()) + || std::includes(p_colours.begin(), p_colours.end(), item_colours.begin(), item_colours.end()); + if (same_colour) + continue; + if (item_start < belt_end(p.boundingBox()) + scaled(p.height * params.belt_tilt_slope)) + score += 10.; + } + } + return std::make_tuple(score, fullbb); } @@ -762,7 +834,8 @@ public: auto binbb = sl::boundingBox(m_bin); - auto starting_point = cfg.starting_point == PConfig::Alignment::BOTTOM_LEFT ? binbb.minCorner() : binbb.center(); + auto starting_point = this->params.is_belt ? belt_origin() : + cfg.starting_point == PConfig::Alignment::BOTTOM_LEFT ? binbb.minCorner() : binbb.center(); // if we have wipe tower, items should be arranged around wipe tower for (Item itm : items) { if (itm.is_wipe_tower) { @@ -913,7 +986,7 @@ std::function AutoArranger::g auto mp = m_merged_pile; mp.emplace_back(itm.transformedShape()); auto chull = sl::convexHull(mp); - if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT) + if (corner_packing()) { if (!sl::isInside(chull, m_bin)) score += LARGE_COST_TO_REJECT; diff --git a/src/libslic3r/Arrange.hpp b/src/libslic3r/Arrange.hpp index ad125aee7d..b8fc0f7268 100644 --- a/src/libslic3r/Arrange.hpp +++ b/src/libslic3r/Arrange.hpp @@ -137,6 +137,13 @@ struct ArrangeParams { float nozzle_height = 0; float printable_height = 256.0; Vec2d align_center{ 0.5,0.5 }; + // Belt printer: items print in the order they lie along the belt axis, from + // its low end unless belt_reversed, and a part's top prints + // belt_tilt_slope * height further along it than its base. + bool is_belt = false; + int belt_axis = 1; // 0 = X, 1 = Y + bool belt_reversed = false; + float belt_tilt_slope = 1.f; // cot(belt tilt angle), 0 when the belt is not tilted ArrangePolygons excluded_regions; // regions cant't be used ArrangePolygons nonprefered_regions; // regions can be used but not prefered diff --git a/src/slic3r/GUI/BeltPurgeTower.cpp b/src/slic3r/GUI/BeltPurgeTower.cpp index a8b0b0c0b8..3dc50d4cb2 100644 --- a/src/slic3r/GUI/BeltPurgeTower.cpp +++ b/src/slic3r/GUI/BeltPurgeTower.cpp @@ -266,6 +266,11 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object 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("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 @@ -278,26 +283,35 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object // 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.; + BoundingBoxf bed_ext; + if (const auto *bed_opt = printer_config.option("printable_area"); + 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]; - 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 + 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. 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). + // 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]; - const double inset = 1.; double lat_center = lat_max + 5. + 0.5 * width; // fallback: just past the parts - if (const auto *bed_opt = printer_config.option("printable_area"); - bed_opt != nullptr && !bed_opt->values.empty()) { - const BoundingBoxf bed_ext = get_extents(bed_opt->values); - const double bed_lat_max = belt_is_y ? bed_ext.max.x() : bed_ext.max.y(); + 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; } diff --git a/src/slic3r/GUI/BeltPurgeTower.hpp b/src/slic3r/GUI/BeltPurgeTower.hpp index c5a6d301a0..27d5b47c94 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[12] = {0}; // rounded geometry and plate inputs (0.1 mm units) + long key[14] = {0}; // rounded geometry, plate and bed 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 < 12; ++i) + for (int i = 0; i < 14; ++i) if (key[i] != o.key[i]) return false; return true; diff --git a/src/slic3r/GUI/Jobs/ArrangeJob.cpp b/src/slic3r/GUI/Jobs/ArrangeJob.cpp index cd4cc83dab..a21bf52c19 100644 --- a/src/slic3r/GUI/Jobs/ArrangeJob.cpp +++ b/src/slic3r/GUI/Jobs/ArrangeJob.cpp @@ -15,6 +15,9 @@ #include "slic3r/GUI/GUI_ObjectList.hpp" #include "libnest2d/common.hpp" +#include "libslic3r/Geometry.hpp" +#include +#include #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("printable_area")->values); + const bool belt_is_y = params.belt_axis == 1; + std::vector regions; + + std::set 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("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 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& 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("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) { diff --git a/src/slic3r/GUI/Jobs/ArrangeJob.hpp b/src/slic3r/GUI/Jobs/ArrangeJob.hpp index 0c9f03de01..19143c3254 100644 --- a/src/slic3r/GUI/Jobs/ArrangeJob.hpp +++ b/src/slic3r/GUI/Jobs/ArrangeJob.hpp @@ -46,6 +46,7 @@ class ArrangeJob : public Job //BBS:prepare the items from current selected partplate void prepare_partplate(); void prepare_wipe_tower(); + void prepare_belt_regions(int num_plates); ArrangePolygon prepare_arrange_polygon(void* instance); diff --git a/tests/libslic3r/test_arrange.cpp b/tests/libslic3r/test_arrange.cpp index a8bc56d2d5..1c13180f01 100644 --- a/tests/libslic3r/test_arrange.cpp +++ b/tests/libslic3r/test_arrange.cpp @@ -285,6 +285,50 @@ TEST_CASE("Arrange keeps a pile aligned near an edge on the bed", "[Arrange]") require_no_overlap(items); } +// On a belt the parts print in belt order, so two colours that alternate along the +// belt, or sit side by side, cost a filament change on every shared layer. Arrange +// keeps each colour together: no part shares belt length with a part of another +// colour, counting the tilted layers that run cot(angle) * height past its far edge, +// whichever end of the belt prints first. +TEST_CASE("Arrange groups the colours of a belt print along the belt", "[Arrange][belt]") +{ + const bool reversed = GENERATE(false, true); + CAPTURE(reversed); + const BoundingBox belt = bed(95, 500); + ArrangePolygons items = squares(6, 30., 20.); + for (size_t i = 0; i < items.size(); ++i) + items[i].extrude_ids = { int(i % 3) + 1 }; // three colours, two parts each + ArrangeParams params = quiet_params(scaled(2.)); + params.align_center = Vec2d(0.5, 0.05); + params.is_belt = true; + params.belt_axis = 1; + params.belt_reversed = reversed; + params.belt_tilt_slope = 1.f; // 45 degrees + + arrange(items, belt, params); + require_no_overlap(items); + + // Belt position in print order, so the same check serves both directions. + const coord_t dir = reversed ? -1 : 1; + auto start = [&](const ArrangePolygon &ap) { const BoundingBox bb = ap.transformed_poly().contour.bounding_box(); return dir * (reversed ? bb.max.y() : bb.min.y()); }; + auto end = [&](const ArrangePolygon &ap) { const BoundingBox bb = ap.transformed_poly().contour.bounding_box(); return dir * (reversed ? bb.min.y() : bb.max.y()) + scaled(ap.height * params.belt_tilt_slope); }; + + for (const ArrangePolygon &ap : items) { + REQUIRE(ap.bed_idx == 0); + CHECK(belt.contains(ap.transformed_poly().contour.bounding_box())); + } + for (const ArrangePolygon &a : items) + for (const ArrangePolygon &b : items) { + if (a.extrude_ids == b.extrude_ids) + continue; + // The part printed later starts after the earlier one has finished. + const coord_t earlier_end = start(a) <= start(b) ? end(a) : end(b); + const coord_t later_start = std::max(start(a), start(b)); + INFO("colour " << a.extrude_ids.front() << " vs " << b.extrude_ids.front()); + CHECK(earlier_end <= later_start); + } +} + TEST_CASE("Sequential print floors the object distance by object height", "[Arrange]") { // The only place sequential-print clearance is enforced. The arrange menu offers