Files
OrcaSlicer/tests/libslic3r/test_imex_arrange.cpp
T
Clifford GarwoodandClaude Opus 5.5 2ede6d1631 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>
2026-10-10 01:44:42 -04:00

288 lines
10 KiB
C++

#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.));
}