mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-06-21 19:33:26 +00:00
* fix(libnest2d): skip the excluded-region alignment pass when there are none NfpPlacer::finalAlign(), run from clearItems() and the destructor, always ran the "find a best position inside the NFP of fixed items" pass even when no items are fixed. With nothing to avoid, calcnfp() computes the inner-fit NFP of the pile and can feed clipper a coordinate outside its allowed range. On Linux/clang the value stays in range so it went unnoticed; on MSVC the clipper "Coordinate outside allowed range" exception escapes the noexcept destructor and aborts the process (exit 0xC0000409). Build the excluded set up front and only run the pass when it is non-empty. The block exists solely to keep the pile clear of fixed items (excluded regions / wipe tower), so it is a no-op when there are none and the wipe-tower behaviour is unchanged. * test(libnest2d): remove dead nesting tests and split the suite by feature Seven of the suite's hidden [.] test cases drove code paths Orca abandoned at the BambuStudio fork: BottomLeftPlacer (used nowhere in src/) and the stock default NfpPlacer backend, which returns zero bins in Orca. They have been red since the fork and are never registered with ctest. Remove them. Split the 1,000-line libnest2d_tests_main.cpp into per-feature files, per the repo convention, sharing a header for the no-fit-polygon backend setup that every translation unit must agree on (ODR): libnest2d_tests.cpp Item and nest() basics test_geometry.cpp geometry primitives test_nfp.cpp no-fit-polygon machinery libnest2d_test_utils.hpp shared includes and the NFP backend specialisation Along the way: drop a debug exportSVG() helper that only wrote a file on test failure (so the suite never leaves stray assets), convert the deprecated Catch::Approx to WithinRel/WithinAbs matchers, and give the tests descriptive names. * test(libnest2d): add NfpPlacer unit tests NfpPlacer is the placement engine the arranger drives, but the suite only covered the geometry primitives. Add a fixture and five tests that exercise pack()/accept() directly: a single item lands in the bin, an oversized item is rejected, the first item is seeded for every starting point, many items pack without overlap, and the rotation candidates are searched. This lifts nfpplacer.hpp line coverage from 42% to 87% in the libnest2d suite. * test(libslic3r): add arrangement::arrange() integration coverage The libnest2d suite cannot reach Orca's real nesting entry point because it does not link libslic3r. Add test_arrange.cpp driving arrangement::arrange(): items land on the bed and within bounds, do not overlap, are spaced by their inflation, an oversized item stays unplaced, overflow spills onto virtual beds, an empty input is a no-op, and the DONT_ALIGN and USER_DEFINED final-alignment paths are exercised. A self-test guards the overlap check the other cases use.
225 lines
7.1 KiB
C++
225 lines
7.1 KiB
C++
#include <catch2/catch_all.hpp>
|
|
|
|
#include "libslic3r/Arrange.hpp"
|
|
#include "libslic3r/BoundingBox.hpp"
|
|
#include "libslic3r/ClipperUtils.hpp"
|
|
#include "libslic3r/ExPolygon.hpp"
|
|
|
|
using namespace Slic3r;
|
|
using namespace Slic3r::arrangement;
|
|
|
|
namespace {
|
|
|
|
using Catch::Matchers::WithinRel;
|
|
|
|
// Square of the given (scaled) side, lower-left at the origin. bed_idx starts at
|
|
// 0 because arrange() seeds the nester's bin from it (see ModelArrange.cpp).
|
|
ArrangePolygon make_square(coord_t side)
|
|
{
|
|
ArrangePolygon ap;
|
|
Polygon p;
|
|
p.points = {Point(0, 0), Point(side, 0), Point(side, side), Point(0, side)};
|
|
ap.poly = ExPolygon(p);
|
|
ap.bed_idx = 0;
|
|
return ap;
|
|
}
|
|
|
|
ArrangePolygons squares(int n, double side_mm)
|
|
{
|
|
ArrangePolygons items;
|
|
for (int i = 0; i < n; ++i)
|
|
items.emplace_back(make_square(scaled(side_mm)));
|
|
return items;
|
|
}
|
|
|
|
// Bed [0,0]..[w,h] in scaled coordinates.
|
|
BoundingBox bed(double w_mm, double h_mm)
|
|
{
|
|
return BoundingBox(Point(0, 0), Point(scaled(w_mm), scaled(h_mm)));
|
|
}
|
|
|
|
// The default progress callback prints to stdout; silence it.
|
|
ArrangeParams quiet_params(coord_t min_dist = 0)
|
|
{
|
|
ArrangeParams p{min_dist};
|
|
p.progressind = [](unsigned, std::string) {};
|
|
return p;
|
|
}
|
|
|
|
ExPolygons placed_shapes(const ArrangePolygons &items)
|
|
{
|
|
ExPolygons out;
|
|
out.reserve(items.size());
|
|
for (const ArrangePolygon &ap : items)
|
|
out.emplace_back(ap.transformed_poly());
|
|
return out;
|
|
}
|
|
|
|
// Area double-counted across the shapes: the sum counts overlaps twice, the
|
|
// union once, so the difference is the overlapping area (0 when disjoint).
|
|
double overlap_area(const ExPolygons &shapes)
|
|
{
|
|
double sum = 0;
|
|
for (const ExPolygon &e : shapes)
|
|
sum += e.area();
|
|
double uni = 0;
|
|
for (const ExPolygon &e : union_ex(shapes))
|
|
uni += e.area();
|
|
return sum - uni;
|
|
}
|
|
|
|
// Relative tolerance absorbs the area-unit rounding the clipper union introduces.
|
|
bool disjoint(const ExPolygons &shapes)
|
|
{
|
|
double total = 0;
|
|
for (const ExPolygon &e : shapes)
|
|
total += e.area();
|
|
return overlap_area(shapes) <= total * 1e-9;
|
|
}
|
|
|
|
void require_no_overlap(const ArrangePolygons &items)
|
|
{
|
|
REQUIRE(disjoint(placed_shapes(items)));
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// Prove the overlap check the other tests rely on actually detects overlap.
|
|
TEST_CASE("overlap_area detects overlap and ignores touching edges", "[Arrange]")
|
|
{
|
|
auto square_at = [](double x_mm) {
|
|
ArrangePolygon ap = make_square(scaled(20.));
|
|
ap.translation = Vec2crd(scaled(x_mm), 0);
|
|
return ap.transformed_poly();
|
|
};
|
|
ExPolygon a = square_at(0.);
|
|
|
|
SECTION("disjoint shapes are reported disjoint") {
|
|
REQUIRE(disjoint({a, square_at(30.)}));
|
|
}
|
|
SECTION("edge-touching shapes are reported disjoint") {
|
|
REQUIRE(disjoint({a, square_at(20.)}));
|
|
}
|
|
SECTION("overlapping shapes are not, and the area is measured") {
|
|
REQUIRE_FALSE(disjoint({a, square_at(10.)}));
|
|
REQUIRE_THAT(overlap_area({a, square_at(10.)}),
|
|
WithinRel(double(scaled(10.)) * scaled(20.), 1e-9)); // 10x20 mm
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Arrange places every item on the physical bed", "[Arrange]")
|
|
{
|
|
ArrangePolygons items = squares(5, 20.);
|
|
arrange(items, bed(200, 200), quiet_params(scaled(1.)));
|
|
|
|
for (const ArrangePolygon &ap : items)
|
|
REQUIRE(ap.bed_idx == 0);
|
|
}
|
|
|
|
TEST_CASE("Arranged items stay within the bed", "[Arrange]")
|
|
{
|
|
ArrangePolygons items = squares(6, 30.);
|
|
arrange(items, bed(200, 200), quiet_params(scaled(1.)));
|
|
|
|
for (const ArrangePolygon &ap : items) {
|
|
REQUIRE(ap.bed_idx == 0);
|
|
REQUIRE(bed(200, 200).contains(ap.transformed_poly().contour.bounding_box()));
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Arranged items do not overlap", "[Arrange]")
|
|
{
|
|
ArrangePolygons items = squares(6, 40.);
|
|
arrange(items, bed(250, 250), quiet_params(scaled(2.)));
|
|
|
|
require_no_overlap(items);
|
|
}
|
|
|
|
TEST_CASE("Arrange spaces items by their inflation", "[Arrange]")
|
|
{
|
|
// Per-item inflation is how the arranger enforces clearance (the GUI fills it
|
|
// from min_obj_distance). Two items inflated 4mm each end up >= 8mm apart.
|
|
ArrangePolygons items = squares(4, 20.);
|
|
for (ArrangePolygon &ap : items)
|
|
ap.inflation = scaled(4.);
|
|
arrange(items, bed(200, 200), quiet_params());
|
|
|
|
// Axis-aligned squares are their own bounding boxes, so the clearance between
|
|
// a pair is the distance between their boxes (1mm slack for nester rounding).
|
|
std::vector<BoundingBox> boxes;
|
|
for (const ExPolygon &e : placed_shapes(items))
|
|
boxes.push_back(e.contour.bounding_box());
|
|
|
|
double min_gap = std::numeric_limits<double>::max();
|
|
for (size_t i = 0; i < boxes.size(); ++i)
|
|
for (size_t j = i + 1; j < boxes.size(); ++j) {
|
|
coord_t sx = std::max<coord_t>(0, std::max(boxes[j].min.x() - boxes[i].max.x(),
|
|
boxes[i].min.x() - boxes[j].max.x()));
|
|
coord_t sy = std::max<coord_t>(0, std::max(boxes[j].min.y() - boxes[i].max.y(),
|
|
boxes[i].min.y() - boxes[j].max.y()));
|
|
min_gap = std::min(min_gap, std::sqrt(double(sx) * sx + double(sy) * sy));
|
|
}
|
|
|
|
REQUIRE(min_gap >= double(scaled(8.)) - double(scaled(0.5)));
|
|
}
|
|
|
|
TEST_CASE("An item larger than the bed cannot be placed", "[Arrange]")
|
|
{
|
|
ArrangePolygons items;
|
|
items.emplace_back(make_square(scaled(20.)));
|
|
items.emplace_back(make_square(scaled(400.))); // far bigger than the bed
|
|
|
|
arrange(items, bed(200, 200), quiet_params(scaled(1.)));
|
|
|
|
REQUIRE(items[0].bed_idx == 0);
|
|
REQUIRE(items[1].bed_idx == UNARRANGED);
|
|
}
|
|
|
|
TEST_CASE("Items overflowing one bed spill onto virtual beds", "[Arrange]")
|
|
{
|
|
ArrangePolygons items = squares(8, 90.); // eight 90mm squares cannot share a 200x200 bed
|
|
arrange(items, bed(200, 200), quiet_params(scaled(2.)));
|
|
|
|
int max_bed = 0;
|
|
for (const ArrangePolygon &ap : items) {
|
|
REQUIRE(ap.bed_idx >= 0); // placed somewhere
|
|
max_bed = std::max(max_bed, ap.bed_idx);
|
|
}
|
|
REQUIRE(max_bed >= 1); // at least one on a virtual bed
|
|
}
|
|
|
|
TEST_CASE("Arrange handles an empty input", "[Arrange]")
|
|
{
|
|
ArrangePolygons items;
|
|
REQUIRE_NOTHROW(arrange(items, bed(200, 200), quiet_params()));
|
|
REQUIRE(items.empty());
|
|
}
|
|
|
|
TEST_CASE("Arrange without final alignment keeps items disjoint", "[Arrange]")
|
|
{
|
|
// do_final_align = false selects Alignment::DONT_ALIGN (skips recentering).
|
|
ArrangePolygons items = squares(6, 40.);
|
|
ArrangeParams params = quiet_params(scaled(2.));
|
|
params.do_final_align = false;
|
|
|
|
arrange(items, bed(250, 250), params);
|
|
|
|
for (const ArrangePolygon &ap : items)
|
|
REQUIRE(ap.bed_idx == 0);
|
|
require_no_overlap(items);
|
|
}
|
|
|
|
TEST_CASE("Arrange aligns the pile to a custom center", "[Arrange]")
|
|
{
|
|
// align_center != (0.5, 0.5) selects Alignment::USER_DEFINED.
|
|
ArrangePolygons items = squares(5, 30.);
|
|
ArrangeParams params = quiet_params(scaled(2.));
|
|
params.align_center = Vec2d(0.3, 0.7);
|
|
|
|
arrange(items, bed(250, 250), params);
|
|
|
|
for (const ArrangePolygon &ap : items)
|
|
REQUIRE(ap.bed_idx == 0);
|
|
require_no_overlap(items);
|
|
}
|