Arrange every IDEX/IQEX plate inside its own primary zone

Arrange read the zones of the selected plate only and applied them to
every bed it packed. Arranging all plates with a plate in Primary
selected spread a parallel plate's parts across its whole bed, and with
a parallel plate selected, a Primary plate's parts were squeezed into
the zone. Arranging a single plate other than the first also ignored its
collision strips: they were tagged with the plate's index, while the
plate packs into the arranger's bed 0.

The IDEX/IQEX constraints now live in ImexArranger (IMEXArrange), and
ArrangeJob only snapshots each plate's zones on the main thread and
calls it:
- When every bed, plates the arrange may add included, has the same
  primary zone, the bed shape is that zone, so parts stay centered in it.
- Otherwise each plate in a parallel mode fences off the rest of its bed
  with fixed items on its own bed, and its parts are then arranged again
  inside the zone, so they sit centered rather than piled against the
  zone edge nearest the bed's center. Plates in Primary keep the whole
  bed, and plates the arrange adds take the process preset's mode.
- Zone edges inside the bed get the bed's own edge margin, and strips
  also keep room for the brim.
- A part too big for its zone, which libnest2d's first-fit retry places
  across the fixed items, is left unarranged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Clifford Garwood
2026-10-10 01:44:42 -04:00
co-authored by Claude Opus 5.5
parent 8b5280098f
commit 2ede6d1631
7 changed files with 656 additions and 51 deletions
+2
View File
@@ -325,6 +325,8 @@ set(lisbslic3r_sources
Geometry/VoronoiUtils.cpp
Geometry/VoronoiUtils.hpp
Geometry/VoronoiVisualUtils.hpp
IMEXArrange.cpp
IMEXArrange.hpp
IMEXHelpers.cpp
IMEXHelpers.hpp
IMEXZones.cpp
+220
View File
@@ -0,0 +1,220 @@
#include "libslic3r/IMEXArrange.hpp"
#include <algorithm>
#include <cstddef>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include <libnest2d/common.hpp>
#include "libslic3r/Arrange.hpp"
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/Config.hpp"
#include "libslic3r/IMEXHelpers.hpp"
#include "libslic3r/IMEXZones.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/Polygon.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/libslic3r.h"
namespace Slic3r {
namespace {
// The arranger's names for the keep-outs. finish() leaves the fences out when it arranges a
// plate inside its zone, where the zone itself is the bed shape.
constexpr const char* kFenceName = "IMEXOutsidePrimaryZone";
constexpr const char* kStripName = "IMEXCollisionZone";
// Each plate computes its zones from where it sits, so the same zone differs in the last bits.
bool same_zone(const BoundingBoxf& a, const BoundingBoxf& b)
{
return is_approx(a.min, b.min) && is_approx(a.max, b.max);
}
BoundingBox inset(BoundingBox box, const Point& by)
{
box.min += by;
box.max -= by;
return box;
}
// The parts of `bed` outside `zone`: full-depth boxes left and right of it, and the pieces in
// front of and behind it. Empty pieces are left out.
std::vector<BoundingBox> boxes_around(const BoundingBox& bed, const BoundingBox& zone)
{
std::vector<BoundingBox> out;
auto add = [&out](coord_t x0, coord_t y0, coord_t x1, coord_t y1) {
if (x1 > x0 && y1 > y0)
out.emplace_back(Point(x0, y0), Point(x1, y1));
};
const coord_t x0 = std::max(zone.min.x(), bed.min.x());
const coord_t x1 = std::min(zone.max.x(), bed.max.x());
add(bed.min.x(), bed.min.y(), zone.min.x(), bed.max.y());
add(zone.max.x(), bed.min.y(), bed.max.x(), bed.max.y());
add(x0, bed.min.y(), x1, zone.min.y());
add(x0, zone.max.y(), x1, bed.max.y());
return out;
}
} // namespace
std::optional<ImexArrangeZones> imex_arrange_zones(const ImexZoneLayout& layout)
{
if (!layout.primary_zone_box)
return std::nullopt;
ImexArrangeZones zones{*layout.primary_zone_box, {}};
for (const BoundingBoxf3& strip : layout.collision_zones)
zones.collision_zones.emplace_back(Vec2d(strip.min.head<2>()), Vec2d(strip.max.head<2>()));
return zones;
}
void ImexArrangeInput::read_config(const DynamicPrintConfig& full_config)
{
// A plate the arrange adds has no mode of its own, so it takes the process preset's.
added_plate.reset();
if (adds_plates) {
const auto* mode = full_config.option<ConfigOptionString>("imex_parallel_mode");
added_plate = imex_arrange_zones(compute_imex_zone_layout(full_config, kImexPrimaryMode, mode ? mode->value : std::string(),
get_extents(full_config.opt<ConfigOptionPoints>("printable_area")->values)));
}
// An estimate: auto brim picks its own width, which brim_width stands in for.
const auto* brim_type = full_config.option<ConfigOptionEnum<BrimType>>("brim_type");
brim = !brim_type || brim_type->value == btNoBrim || brim_type->value == btInnerOnly ?
0. :
full_config.opt_float("brim_width") + full_config.opt_float("brim_object_gap");
}
ImexArranger::ImexArranger(const ImexArrangeInput& input, const arrangement::ArrangeParams& params, const Points& bed)
: m_bed(bed)
, m_bed_bb(bed)
, m_margin(scaled(std::max(0.f, params.bed_shrink_x)), scaled(std::max(0.f, params.bed_shrink_y)))
, m_adds_plates(input.adds_plates)
{
for (size_t i = 0; i < input.beds.size(); ++i)
if (input.beds[i])
m_beds.push_back({int(i), *input.beds[i]});
// The bed shape can be a zone only when every plate being arranged has it, and so does every
// plate the arrange may add.
const auto& first = input.beds.empty() ? std::nullopt : input.beds.front();
if (first &&
std::all_of(input.beds.begin(), input.beds.end(),
[&](const std::optional<ImexArrangeZones>& b) { return b && same_zone(b->primary_zone, first->primary_zone); }) &&
(!input.adds_plates || (input.added_plate && same_zone(input.added_plate->primary_zone, first->primary_zone))))
m_shared_room = room_in(first->primary_zone);
// The beds past the plates being arranged become new plates.
if (input.adds_plates && input.added_plate)
for (int bed_idx = int(input.beds.size()); bed_idx < MAX_NUM_PLATES; ++bed_idx)
m_beds.push_back({bed_idx, *input.added_plate});
// A zone narrower than its margins leaves no room, and its fences then fill the whole bed.
const Point strip_margin = m_margin + Point::Constant(scaled(input.brim));
for (const Bed& b : m_beds) {
if (!m_shared_room)
for (const BoundingBox& box : boxes_around(m_bed_bb, inset(scaled(b.zones.primary_zone), m_margin)))
m_keep_outs.push_back({b.bed_idx, box, true});
for (const BoundingBoxf& strip : b.zones.collision_zones)
m_keep_outs.push_back({b.bed_idx, inset(scaled(strip), -strip_margin), false});
}
}
std::optional<BoundingBox> ImexArranger::room_in(const BoundingBoxf& zone) const
{
BoundingBox room = inset(scaled(zone), m_margin);
room.min = room.min.cwiseMax(m_bed_bb.min);
room.max = room.max.cwiseMin(m_bed_bb.max);
if (room.min.x() >= room.max.x() || room.min.y() >= room.max.y())
return std::nullopt;
return room;
}
Points ImexArranger::bed_shape() const { return m_shared_room ? m_shared_room->polygon().points : m_bed; }
void ImexArranger::add_keep_outs(arrangement::ArrangePolygons& fixed) const
{
for (const KeepOut& keep_out : m_keep_outs) {
arrangement::ArrangePolygon ap;
ap.poly.contour = keep_out.box.polygon();
ap.is_virt_object = true;
ap.bed_idx = keep_out.bed_idx;
ap.height = 1;
ap.name = keep_out.fence ? kFenceName : kStripName;
fixed.push_back(std::move(ap));
}
}
bool ImexArranger::crosses_keep_out(const arrangement::ArrangePolygon& part, int bed_idx) const
{
const Polygon hull = part.transformed_poly().contour;
return std::any_of(m_keep_outs.begin(), m_keep_outs.end(), [&](const KeepOut& keep_out) {
return keep_out.bed_idx == bed_idx && !intersection(hull, keep_out.box.polygon()).empty();
});
}
void ImexArranger::finish(arrangement::ArrangePolygons& items,
const arrangement::ArrangePolygons& before,
const arrangement::ArrangePolygons& fixed,
const arrangement::ArrangeParams& params) const
{
for (arrangement::ArrangePolygon& part : items)
if (crosses_keep_out(part, part.bed_idx))
part.bed_idx = arrangement::UNARRANGED;
// Only fenced plates need centering, and arranging one plate never fences.
if (m_shared_room || !m_adds_plates)
return;
arrangement::ArrangeParams one_plate = params;
one_plate.progressind = {};
for (const Bed& b : m_beds) {
const std::optional<BoundingBox> room = room_in(b.zones.primary_zone);
std::vector<size_t> on_bed;
for (size_t i = 0; i < items.size(); ++i)
if (items[i].bed_idx == b.bed_idx)
on_bed.push_back(i);
if (on_bed.empty() || !room)
continue;
arrangement::ArrangePolygons parts, bed_fixed;
// Bed 0, where the nester takes a part as placeable rather than as one that does not fit.
for (size_t i : on_bed) {
parts.push_back(before[i]);
parts.back().bed_idx = 0;
}
for (const arrangement::ArrangePolygon& ap : fixed)
if (ap.bed_idx == b.bed_idx && ap.name != kFenceName) {
bed_fixed.push_back(ap);
bed_fixed.back().bed_idx = 0;
}
arrangement::arrange(parts, bed_fixed, room->polygon().points, one_plate);
// A canceled pass leaves parts it never reached on bed 0 with their old ids.
if (one_plate.stopcondition && one_plate.stopcondition())
return;
// Keep the first layout when this one does not fit.
if (!std::all_of(parts.begin(), parts.end(), [&](const arrangement::ArrangePolygon& part) {
return part.bed_idx == 0 && !crosses_keep_out(part, b.bed_idx);
}))
continue;
// By-object printing follows the packing order, which this pass numbered 0..n-1: carry
// it over onto the ids the first pass gave these parts.
std::vector<int> ids;
for (size_t i : on_bed)
ids.push_back(items[i].itemid);
std::sort(ids.begin(), ids.end());
for (size_t k = 0; k < on_bed.size(); ++k) {
items[on_bed[k]].translation = parts[k].translation;
items[on_bed[k]].rotation = parts[k].rotation;
items[on_bed[k]].itemid = ids[parts[k].itemid];
}
}
}
} // namespace Slic3r
+101
View File
@@ -0,0 +1,101 @@
#pragma once
#include <optional>
#include <vector>
#include "libslic3r/Arrange.hpp"
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/Point.hpp"
namespace Slic3r {
class DynamicPrintConfig;
struct ImexZoneLayout;
// A plate's IDEX/IQEX zones as Arrange sees them, in plate-local millimeters.
struct ImexArrangeZones
{
BoundingBoxf primary_zone;
std::vector<BoundingBoxf> collision_zones;
};
// The zones of `layout`, none when it has no primary zone, i.e. outside a parallel mode.
std::optional<ImexArrangeZones> imex_arrange_zones(const ImexZoneLayout& layout);
// What an arrange needs to know about the plates it packs into.
struct ImexArrangeInput
{
// Per arranger bed, in order: the zones of the plate it packs into, none for a plate whose
// parts may go anywhere on the bed.
std::vector<std::optional<ImexArrangeZones>> beds;
// Whether the beds past `beds` become new plates, as when arranging every plate, and the
// zones such a plate gets.
bool adds_plates = false;
std::optional<ImexArrangeZones> added_plate;
// Room a strip keeps for the brim: brim width plus its gap to the part, in mm.
double brim = 0.;
// Sets `added_plate` and `brim` from the full config. Call after setting `adds_plates`.
void read_config(const DynamicPrintConfig& full_config);
};
// Keeps an arrange inside each IDEX/IQEX plate's primary zone and out of that zone's carriage
// collision strips.
//
// The arranger packs every bed into one bed shape. When every bed has the same primary zone,
// that shape becomes the zone, which keeps the parts centered in it. Otherwise each plate in a
// parallel mode fences off the rest of its bed with fixed items on its own bed, leaving the
// other plates the whole bed, and finish() arranges each fenced plate again inside its zone,
// since a fenced pile sits against the zone edge nearest the bed's center.
//
// A zone edge inside the bed gets the margin the bed's own edges get, since a copy printed from
// a part at the zone edge sits at the far edge of the bed. A strip also gets the brim, which
// would take the nozzle into it.
class ImexArranger
{
public:
// `bed` is the arranger's bed shape, shrunk by params.bed_shrink_x/y.
ImexArranger(const ImexArrangeInput& input, const arrangement::ArrangeParams& params, const Points& bed);
// Whether any bed has a zone. Nothing below has an effect otherwise.
bool active() const { return !m_beds.empty(); }
// The bed shape to arrange in.
Points bed_shape() const;
// Adds the fixed items that keep parts out of each bed's keep-outs.
void add_keep_outs(arrangement::ArrangePolygons& fixed) const;
// Call after arrangement::arrange(items, fixed, bed_shape(), params), with `before` holding
// `items` as they were before it. Leaves unarranged any part across a keep-out: one that does
// not fit an empty bed is retried there without the bed's fixed items, so a part too big for
// its zone comes back across them. Then centers each fenced plate's parts in its zone.
void finish(arrangement::ArrangePolygons& items, const arrangement::ArrangePolygons& before,
const arrangement::ArrangePolygons& fixed, const arrangement::ArrangeParams& params) const;
private:
struct Bed
{
int bed_idx;
ImexArrangeZones zones;
};
struct KeepOut
{
int bed_idx;
BoundingBox box;
bool fence; // outside the primary zone, rather than a strip
};
std::optional<BoundingBox> room_in(const BoundingBoxf& zone) const;
bool crosses_keep_out(const arrangement::ArrangePolygon& part, int bed_idx) const;
Points m_bed;
BoundingBox m_bed_bb;
Point m_margin;
bool m_adds_plates = false;
std::vector<Bed> m_beds;
std::optional<BoundingBox> m_shared_room;
std::vector<KeepOut> m_keep_outs;
};
} // namespace Slic3r
+41 -48
View File
@@ -5,6 +5,7 @@
#include "libslic3r/MTUtils.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/ModelArrange.hpp"
#include "libslic3r/IMEXArrange.hpp"
#include "slic3r/GUI/PartPlate.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
@@ -537,6 +538,36 @@ void ArrangeJob::prepare_partplate() {
prepare_belt_regions(current_plate_index + 1);
}
// Snapshot each plate's IMEX zones on the main thread, so process() never has to touch
// PartPlate's IMEX cache; see the members' declaration for why that matters. The beds are the
// arranger's: arranging one plate packs into bed 0, and arranging all of them numbers the beds
// over the unlocked plates, as postprocess_bed_index_for_selected() maps them back.
void ArrangeJob::prepare_imex_zones()
{
m_imex = {};
PartPlateList& plate_list = m_plater->get_partplate_list();
std::vector<PartPlate*> plates;
if (only_on_partplate)
plates.push_back(plate_list.get_curr_plate());
else
for (int i = 0; i < plate_list.get_plate_count(); ++i)
if (!plate_list.get_plate(i)->is_locked())
plates.push_back(plate_list.get_plate(i));
for (PartPlate* plate : plates) {
std::optional<ImexArrangeZones> zones;
if (const std::optional<BoundingBoxf> pz = plate ? plate->imex_primary_zone() : std::nullopt) {
const Vec2d o = plate->get_origin().head<2>();
zones = ImexArrangeZones{BoundingBoxf(Vec2d(pz->min - o), Vec2d(pz->max - o)), {}};
for (const BoundingBoxf3& strip : plate->imex_collision_zones())
zones->collision_zones.emplace_back(Vec2d(strip.min.head<2>() - o), Vec2d(strip.max.head<2>() - o));
}
m_imex.beds.push_back(std::move(zones));
}
m_imex.adds_plates = !only_on_partplate;
m_imex.read_config(wxGetApp().preset_bundle->full_config());
}
//BBS: add partplate logic
void ArrangeJob::prepare()
{
@@ -555,22 +586,6 @@ void ArrangeJob::prepare()
Model::setExtruderParams(config, numExtruders);
Model::setPrintSpeedTable(config, print_config);
// Snapshot the IMEX zones here, on the main thread, so process() never has to touch
// PartPlate's IMEX cache. See the members' declaration for why that matters.
m_imex_primary_zone_local.reset();
m_imex_collision_zones_local.clear();
if (PartPlate* curr_plate = m_plater->get_partplate_list().get_curr_plate()) {
if (auto pz = curr_plate->imex_primary_zone()) {
const Vec3d plate_origin = curr_plate->get_origin();
const double ox = plate_origin.x(), oy = plate_origin.y();
m_imex_primary_zone_local = BoundingBoxf(Vec2d(pz->min.x() - ox, pz->min.y() - oy),
Vec2d(pz->max.x() - ox, pz->max.y() - oy));
for (const BoundingBoxf3& cz : curr_plate->imex_collision_zones())
m_imex_collision_zones_local.emplace_back(Vec2d(cz.min.x() - ox, cz.min.y() - oy),
Vec2d(cz.max.x() - ox, cz.max.y() - oy));
}
}
int state = m_plater->get_prepare_state();
if (state == Job::JobPrepareState::PREPARE_STATE_DEFAULT) {
only_on_partplate = false;
@@ -581,6 +596,9 @@ void ArrangeJob::prepare()
prepare_partplate();
}
// After prepare_all(), which locks the plates it must leave alone.
prepare_imex_zones();
#if SAVE_ARRANGE_POLY
if (1)
@@ -662,38 +680,10 @@ void ArrangeJob::process(Ctl &ctl)
Points bedpts = get_shrink_bedpts(m_plater->config(),params);
// When an IDEX/IQEX parallel mode is active, constrain auto-arrange to the primary zone only
// and treat carriage collision strips as hard excluded regions.
// NOTE: the plate's zone boxes are in global (world) coordinates because they derive from
// m_shape, which includes the plate origin offset. The arranger works in plate-local space
// (origin = 0,0), so the plate origin is subtracted when the snapshot is taken in prepare();
// m_imex_primary_zone_local / m_imex_collision_zones_local are already plate-local here.
if (const auto& pz = m_imex_primary_zone_local) {
BoundingBox scaled_pz = scaled(*pz);
bedpts = {
{ scaled_pz.min.x(), scaled_pz.min.y() },
{ scaled_pz.max.x(), scaled_pz.min.y() },
{ scaled_pz.max.x(), scaled_pz.max.y() },
{ scaled_pz.min.x(), scaled_pz.max.y() },
};
for (const BoundingBoxf& cz : m_imex_collision_zones_local) {
Polygon poly({
{ scaled(cz.min.x()), scaled(cz.min.y()) },
{ scaled(cz.max.x()), scaled(cz.min.y()) },
{ scaled(cz.max.x()), scaled(cz.max.y()) },
{ scaled(cz.min.x()), scaled(cz.max.y()) },
});
arrangement::ArrangePolygon ap;
ap.poly.contour = poly;
ap.translation = Vec2crd(0, 0);
ap.rotation = 0.0;
ap.is_virt_object = true;
ap.bed_idx = current_plate_index;
ap.height = 1;
ap.name = "IMEXCollisionZone";
m_unselected.emplace_back(std::move(ap));
}
}
// Keep the parts on IDEX/IQEX plates in their primary zones; see ImexArranger.
const ImexArranger imex(m_imex, params, bedpts);
bedpts = imex.bed_shape();
imex.add_keep_outs(m_unselected);
bool enable_wrapping = global_config.option<ConfigOptionBool>("enable_wrapping_detection")->value;
partplate_list.preprocess_exclude_areas(params.excluded_regions, enable_wrapping, 1, scale_(1));
@@ -719,7 +709,10 @@ void ArrangeJob::process(Ctl &ctl)
<<", bbox:"<<get_extents(item.poly).min.transpose()<<","<<get_extents(item.poly).max.transpose();
}
const ArrangePolygons before = imex.active() ? m_selected : ArrangePolygons();
arrangement::arrange(m_selected, m_unselected, bedpts, params);
if (!ctl.was_canceled())
imex.finish(m_selected, before, m_unselected, params);
// sort by item id
std::sort(m_selected.begin(), m_selected.end(), [](auto a, auto b) {return a.itemid < b.itemid; });
+4 -3
View File
@@ -11,6 +11,7 @@
#include "Job.hpp"
#include "libslic3r/Arrange.hpp"
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/IMEXArrange.hpp"
namespace Slic3r {
@@ -43,10 +44,9 @@ class ArrangeJob : public Job
// cannot be moved off the main thread on its own.) Snapshotting plain geometry here keeps
// every one of those touches on the main thread.
//
// Both are stored already converted to plate-local coordinates, which is the space the
// The zones are stored already converted to plate-local coordinates, which is the space the
// arranger works in.
std::optional<BoundingBoxf> m_imex_primary_zone_local;
std::vector<BoundingBoxf> m_imex_collision_zones_local;
ImexArrangeInput m_imex;
arrangement::ArrangeParams params;
int current_plate_index = 0;
@@ -69,6 +69,7 @@ class ArrangeJob : public Job
void prepare_partplate();
void prepare_wipe_tower();
void prepare_belt_regions(int num_plates);
void prepare_imex_zones();
ArrangePolygon prepare_arrange_polygon(void* instance);
+1
View File
@@ -37,6 +37,7 @@ add_executable(${_TEST_NAME}_tests
test_fill_plane_path.cpp
test_fill_tpms_adaptive.cpp
test_geometry.cpp
test_imex_arrange.cpp
test_imex_helpers.cpp
test_imex_zones.cpp
test_multimaterial_segmentation.cpp
+287
View File
@@ -0,0 +1,287 @@
#include <catch2/catch_all.hpp>
#include <algorithm>
#include <optional>
#include <vector>
#include "catch2/catch_test_macros.hpp"
#include "catch2/matchers/catch_matchers.hpp"
#include "catch2/matchers/catch_matchers_floating_point.hpp"
#include "libslic3r/Arrange.hpp"
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/ExPolygon.hpp"
#include "libslic3r/IMEXArrange.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/Polygon.hpp"
using namespace Slic3r;
using Catch::Matchers::WithinAbs;
namespace {
// A 300 x 200 mm bed. A two-carriage IDEX in a parallel mode gives T0 the left half, and a
// mirror mode adds a 30 mm strip along that half's inner edge.
const BoundingBoxf kBed{Vec2d(0., 0.), Vec2d(300., 200.)};
const BoundingBoxf kLeftHalf{Vec2d(0., 0.), Vec2d(150., 200.)};
const BoundingBoxf kRightHalf{Vec2d(150., 0.), Vec2d(300., 200.)};
const BoundingBoxf kStrip{Vec2d(120., 0.), Vec2d(150., 200.)};
const ImexArrangeZones kCopy{kLeftHalf, {}};
const ImexArrangeZones kMirror{kLeftHalf, {kStrip}};
// A 1 mm bed edge margin, and no rotation, so every expected box below is exact.
arrangement::ArrangeParams test_params()
{
arrangement::ArrangeParams params;
params.progressind = {};
params.bed_shrink_x = 1.f;
params.bed_shrink_y = 1.f;
params.allow_rotations = false;
return params;
}
// The bed shrunk by that 1 mm margin, as get_shrink_bedpts() hands it to the arranger.
Points shrunk_bed()
{
return scaled(BoundingBoxf(Vec2d(1., 1.), Vec2d(299., 199.))).polygon().points;
}
// A `width` x `depth` mm part, kept 1 mm from its neighbors, ready to arrange: on bed 0, as
// get_instance_arrange_poly() leaves it.
arrangement::ArrangePolygon part(double width, double depth)
{
arrangement::ArrangePolygon ap;
ap.poly.contour = scaled(BoundingBoxf(Vec2d(-width / 2., -depth / 2.), Vec2d(width / 2., depth / 2.))).polygon();
ap.inflation = scaled(1.);
ap.extrude_ids = {0};
ap.height = 10.;
ap.bed_idx = 0;
return ap;
}
arrangement::ArrangePolygons squares(int count, double size)
{
arrangement::ArrangePolygons out(count, part(size, size));
for (int i = 0; i < count; ++i)
out[i].itemid = i;
return out;
}
// Arranges `items` the way ArrangeJob does.
void arrange(arrangement::ArrangePolygons& items, const ImexArrangeInput& input)
{
const arrangement::ArrangeParams params = test_params();
const ImexArranger imex(input, params, shrunk_bed());
arrangement::ArrangePolygons fixed;
imex.add_keep_outs(fixed);
const arrangement::ArrangePolygons before = items;
arrangement::arrange(items, fixed, imex.bed_shape(), params);
imex.finish(items, before, fixed, params);
}
BoundingBoxf extents(const arrangement::ArrangePolygons& items)
{
BoundingBox bb;
for (const arrangement::ArrangePolygon& ap : items)
bb.merge(get_extents(ap.transformed_poly()));
return BoundingBoxf(unscaled(bb.min), unscaled(bb.max));
}
bool crosses(const arrangement::ArrangePolygon& ap, const BoundingBoxf& box)
{
return !intersection(ap.transformed_poly().contour, scaled(box).polygon()).empty();
}
} // namespace
TEST_CASE("ImexArranger changes nothing when no plate has zones", "[IMEXArrange][IMEX]")
{
ImexArrangeInput input;
input.beds = {std::nullopt, std::nullopt};
input.adds_plates = true;
const ImexArranger imex(input, test_params(), shrunk_bed());
CHECK_FALSE(imex.active());
CHECK(imex.bed_shape() == shrunk_bed());
arrangement::ArrangePolygons fixed;
imex.add_keep_outs(fixed);
CHECK(fixed.empty());
}
TEST_CASE("Plates sharing a primary zone arrange inside it, clear of the strip", "[IMEXArrange][IMEX]")
{
// The second plate computes the same zone from where it sits, off in the last bits.
ImexArrangeZones shifted = kMirror;
shifted.primary_zone.max.x() += 1e-9;
ImexArrangeInput input;
input.beds = {kMirror, shifted};
input.adds_plates = true;
input.added_plate = kMirror;
// The bed shape is the zone less the bed's 1 mm edge margin.
const ImexArranger imex(input, test_params(), shrunk_bed());
const BoundingBox shape(imex.bed_shape());
CHECK(shape.min == scaled(Vec2d(1., 1.)));
CHECK(shape.max == scaled(Vec2d(149., 199.)));
// Enough parts that, centered in the zone, they would reach the strip.
arrangement::ArrangePolygons items = squares(20, 20.);
arrange(items, input);
for (const arrangement::ArrangePolygon& ap : items) {
CHECK(ap.bed_idx >= 0);
CHECK_FALSE(crosses(ap, kStrip));
}
}
TEST_CASE("Beside a plate in Primary, a parallel plate's parts are centered in its zone", "[IMEXArrange][IMEX]")
{
ImexArrangeInput input;
input.beds = {kMirror, std::nullopt};
input.adds_plates = true;
// Different zones: the bed shape stays the whole bed, and the mirror plate is fenced in.
const ImexArranger imex(input, test_params(), shrunk_bed());
CHECK(imex.bed_shape() == shrunk_bed());
arrangement::ArrangePolygons items = squares(4, 20.);
arrange(items, input);
for (const arrangement::ArrangePolygon& ap : items)
REQUIRE(ap.bed_idx == 0);
const BoundingBoxf pile = extents(items);
CHECK(kLeftHalf.contains(pile.min));
CHECK(kLeftHalf.contains(pile.max));
// Centered in the zone, rather than piled against its edge nearest the bed's center.
CHECK_THAT(pile.center().x(), WithinAbs(75., 5.));
CHECK_THAT(pile.center().y(), WithinAbs(100., 5.));
}
TEST_CASE("A plate in Primary uses the whole bed beside a parallel plate", "[IMEXArrange][IMEX]")
{
ImexArrangeInput input;
input.beds = {std::nullopt, kMirror};
input.adds_plates = true;
arrangement::ArrangePolygons items = squares(4, 20.);
arrange(items, input);
for (const arrangement::ArrangePolygon& ap : items)
REQUIRE(ap.bed_idx == 0);
CHECK_THAT(extents(items).center().x(), WithinAbs(150., 5.));
}
TEST_CASE("A part too big for its primary zone is left unarranged", "[IMEXArrange][IMEX]")
{
ImexArrangeInput input;
SECTION("arranging one plate, the zone is the bed shape")
{
input.beds = {kCopy};
}
SECTION("arranging all plates, the zone is fenced in")
{
input.beds = {kCopy, std::nullopt};
input.adds_plates = true;
}
// Narrower than the bed but wider than the zone.
arrangement::ArrangePolygons items{part(180., 50.)};
arrange(items, input);
CHECK(items.front().bed_idx == arrangement::UNARRANGED);
}
TEST_CASE("A strip keeps room for the bed margin and the brim", "[IMEXArrange][IMEX]")
{
ImexArrangeInput input;
input.beds = {kMirror};
input.brim = 5.;
const ImexArranger imex(input, test_params(), shrunk_bed());
arrangement::ArrangePolygons fixed;
imex.add_keep_outs(fixed);
REQUIRE(fixed.size() == 1);
// Grown by the 1 mm margin and the 5 mm brim on every side.
const BoundingBox strip = get_extents(fixed.front().poly);
CHECK(strip.min == scaled(Vec2d(114., -6.)));
CHECK(strip.max == scaled(Vec2d(156., 206.)));
}
TEST_CASE("A plate the arrange adds takes the zones it will have", "[IMEXArrange][IMEX]")
{
ImexArrangeInput input;
input.beds = {kCopy};
input.adds_plates = true;
SECTION("the same zone keeps it the bed shape, with the new plates' strips")
{
input.added_plate = kMirror;
const ImexArranger imex(input, test_params(), shrunk_bed());
CHECK(imex.bed_shape() != shrunk_bed());
arrangement::ArrangePolygons fixed;
imex.add_keep_outs(fixed);
auto on = [&fixed](int bed_idx) {
return std::count_if(fixed.begin(), fixed.end(), [bed_idx](const arrangement::ArrangePolygon& ap) { return ap.bed_idx == bed_idx; });
};
CHECK(on(0) == 0);
CHECK(on(1) == 1);
}
SECTION("a new plate in Primary gets the whole bed, so the zone is fenced in instead")
{
const ImexArranger imex(input, test_params(), shrunk_bed());
CHECK(imex.bed_shape() == shrunk_bed());
arrangement::ArrangePolygons fixed;
imex.add_keep_outs(fixed);
for (const arrangement::ArrangePolygon& ap : fixed)
CHECK(ap.bed_idx == 0);
}
SECTION("another zone makes the whole bed the bed shape")
{
input.added_plate = ImexArrangeZones{kRightHalf, {}};
const ImexArranger imex(input, test_params(), shrunk_bed());
CHECK(imex.bed_shape() == shrunk_bed());
}
SECTION("arranging one plate never adds one")
{
input.adds_plates = false;
input.added_plate = ImexArrangeZones{kRightHalf, {}};
const ImexArranger imex(input, test_params(), shrunk_bed());
CHECK(imex.bed_shape() != shrunk_bed());
}
}
TEST_CASE("A zone narrower than its margins takes no parts", "[IMEXArrange][IMEX]")
{
ImexArrangeInput input;
input.beds = {ImexArrangeZones{BoundingBoxf(Vec2d(0., 0.), Vec2d(1.5, 200.)), {}}};
const ImexArranger imex(input, test_params(), shrunk_bed());
CHECK(imex.bed_shape() == shrunk_bed());
arrangement::ArrangePolygons items = squares(1, 20.);
arrange(items, input);
CHECK(items.front().bed_idx == arrangement::UNARRANGED);
}
TEST_CASE("Parts that spill onto a new plate in Primary use its whole bed", "[IMEXArrange][IMEX]")
{
// Every plate in copy mode, while a plate the arrange adds is in Primary.
ImexArrangeInput input;
input.beds = {kCopy};
input.adds_plates = true;
// The zone holds 3 x 4 of these 40 mm parts, so the rest spill onto a new plate.
arrangement::ArrangePolygons items = squares(16, 40.);
arrange(items, input);
arrangement::ArrangePolygons spilled;
for (const arrangement::ArrangePolygon& ap : items) {
REQUIRE(ap.bed_idx >= 0);
if (ap.bed_idx == 0) {
const BoundingBoxf placed = extents({ap});
CHECK(kLeftHalf.contains(placed.min));
CHECK(kLeftHalf.contains(placed.max));
} else
spilled.push_back(ap);
}
REQUIRE_FALSE(spilled.empty());
// Centered on the whole bed, not packed into the copy plate's zone.
CHECK_THAT(extents(spilled).center().x(), WithinAbs(150., 10.));
}