From 2cb8e9c4ea81c037b2fdd4d74b5b5091a5c86cfb Mon Sep 17 00:00:00 2001 From: ExPikaPaka Date: Wed, 30 Sep 2026 08:49:34 +0200 Subject: [PATCH] Compare the tiled booleans polygon by polygon Area alone would pass on a result whose pieces were merged across tiles or which kept the cut edges of the clip. The rings are compared after rotating each to its lowest point and sorting, so only the ordering is free. The fixture now also asserts it really is split into more than one tile, which is the path being tested. --- tests/libslic3r/test_clipper_utils.cpp | 47 ++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/tests/libslic3r/test_clipper_utils.cpp b/tests/libslic3r/test_clipper_utils.cpp index c69c686521..02f158ac95 100644 --- a/tests/libslic3r/test_clipper_utils.cpp +++ b/tests/libslic3r/test_clipper_utils.cpp @@ -1,7 +1,9 @@ #include +#include #include #include +#include #include #include "libslic3r/ClipperUtils.hpp" @@ -300,7 +302,38 @@ TEST_CASE("Traversing Clipper PolyTree", "[ClipperUtils]") { } } -TEST_CASE("Tiled diff and intersection cover the same area as the plain calls", "[ClipperUtils]") { +// Rings flattened to x,y,x,y... and sorted, with each ring rotated to start at its lowest point: two +// encodings of the same geometry compare equal however the pieces came back or wherever a ring started. +static std::vector> canonical_rings(const ExPolygons &expolygons) +{ + std::vector> rings; + const auto add = [&rings](const Polygon &poly) { + if (poly.points.empty()) + return; + Points pts = poly.points; + std::rotate(pts.begin(), + std::min_element(pts.begin(), pts.end(), [](const Point &a, const Point &b) { + return std::make_pair(a.x(), a.y()) < std::make_pair(b.x(), b.y()); + }), + pts.end()); + std::vector flat; + flat.reserve(pts.size() * 2); + for (const Point &p : pts) { + flat.emplace_back(p.x()); + flat.emplace_back(p.y()); + } + rings.emplace_back(std::move(flat)); + }; + for (const ExPolygon &expoly : expolygons) { + add(expoly.contour); + for (const Polygon &hole : expoly.holes) + add(hole); + } + std::sort(rings.begin(), rings.end()); + return rings; +} + +TEST_CASE("Tiled diff and intersection return the same polygons as the plain calls", "[ClipperUtils]") { // A grid of disjoint framed squares, enough of them to be split into several tiles. const int n = 40; const coord_t cell = scaled(2.), side = scaled(1.5), frame = scaled(0.3); @@ -327,19 +360,19 @@ TEST_CASE("Tiled diff and intersection cover the same area as the plain calls", } polygons_append(clip, to_polygons(big)); - const auto xor_area = [](const ExPolygons &a, const ExPolygons &b) { return area(diff_ex(a, b)) + area(diff_ex(b, a)); }; const ApplySafetyOffset safety = GENERATE(ApplySafetyOffset::No, ApplySafetyOffset::Yes); - const double tolerance = double(scaled(0.001)) * double(span); + + // The point of the fixture: below 128 pieces the helpers fall back to a single tile and the tiled + // path under test is never taken. + REQUIRE(ClipperUtils::tile_expolygons(subject, 32).size() > 1); const ExPolygons diff_plain = diff_ex(subject, clip, safety); const ExPolygons diff_tiled = diff_ex_by_piece(subject, clip, safety); REQUIRE(area(diff_plain) > 0.); - CHECK_THAT(area(diff_tiled), Catch::Matchers::WithinRel(area(diff_plain), 1e-9)); - CHECK(xor_area(diff_tiled, diff_plain) < tolerance); + CHECK(canonical_rings(diff_tiled) == canonical_rings(diff_plain)); const ExPolygons intersection_plain = intersection_ex(subject, clip, safety); const ExPolygons intersection_tiled = intersection_ex_by_piece(subject, clip, safety); REQUIRE(area(intersection_plain) > 0.); - CHECK_THAT(area(intersection_tiled), Catch::Matchers::WithinRel(area(intersection_plain), 1e-9)); - CHECK(xor_area(intersection_tiled, intersection_plain) < tolerance); + CHECK(canonical_rings(intersection_tiled) == canonical_rings(intersection_plain)); }