diff --git a/src/libslic3r/ClipperUtils.cpp b/src/libslic3r/ClipperUtils.cpp index 1f8cf8a756..0b4033ce8f 100644 --- a/src/libslic3r/ClipperUtils.cpp +++ b/src/libslic3r/ClipperUtils.cpp @@ -7,11 +7,13 @@ #include #include #include +#include #include #include #include "ClipperUtils.hpp" +#include "AABBTreeIndirect.hpp" #include "BoundingBox.hpp" #include "ExPolygon.hpp" #include "Point.hpp" @@ -278,6 +280,7 @@ Polylines c2_to_polylines(const C2::Paths64 &paths) return out; } +// Only sizes the reserve of c2_to_expolygons(): for a tree Clipper2 misnested (see below) the count is approximate. size_t c2_count_expolygons(const C2::PolyPath64 &outer) { size_t cnt = 1; @@ -287,17 +290,90 @@ size_t c2_count_expolygons(const C2::PolyPath64 &outer) return cnt; } -void c2_append_expolygons(const C2::PolyPath64 &outer, ExPolygons &out) +// Clipper2 sometimes puts a path at the wrong level of its PolyTree: a hole beside the outer path around it instead of +// under it, or an island at the level of holes. Taken as they are, a misplaced hole becomes a clockwise contour and its +// outer an expolygon that covers it; whole-layer booleans cancel the two, but anything handling expolygons one at a time +// sees the hole filled. So the orientation of a path decides its role: a clockwise path where a contour belongs is +// collected in stray_holes, for c2_attach_stray_holes() to give to the contour around it. +void c2_append_expolygons(const C2::PolyPath64 &outer, ExPolygons &out, std::vector &stray_holes) { - ExPolygon &expoly = out.emplace_back(); - expoly.contour.points = c2_to_points(outer.Polygon()); - expoly.holes.reserve(outer.Count()); - for (const auto &hole : outer) - expoly.holes.emplace_back().points = c2_to_points(hole->Polygon()); - // Islands inside the holes. - for (const auto &hole : outer) - for (const auto &island : *hole) - c2_append_expolygons(*island, out); + if (C2::Area(outer.Polygon()) < 0.) { + stray_holes.emplace_back(&outer.Polygon()); + for (const auto &island : outer) + c2_append_expolygons(*island, out, stray_holes); + return; + } + // Indexed, as the recursion below may reallocate out. + const size_t idx = out.size(); + out.emplace_back().contour.points = c2_to_points(outer.Polygon()); + out[idx].holes.reserve(outer.Count()); + for (const auto &child : outer) + if (C2::Area(child->Polygon()) > 0.) + c2_append_expolygons(*child, out, stray_holes); + else { + out[idx].holes.emplace_back().points = c2_to_points(child->Polygon()); + // Islands inside the hole. + for (const auto &island : *child) + c2_append_expolygons(*island, out, stray_holes); + } +} + +// Whether outer encloses inner, two paths of one Clipper2 result. Such paths don't cross, they only touch, and rounding +// both to the grid leaves a touching vertex up to about 1.5 units across the other path. So a vertex of inner that close +// to outer doesn't count, the first vertex clear of it decides, and a path touching at every vertex goes by a point +// strictly inside it: either lies on the same side of outer as the whole path. A sliver too thin to hold such a point is +// taken as not enclosed. A hole closely hugging an island inside it would pass, so callers skip any outer no larger than +// inner: a path inside another is always the smaller. +bool path_encloses(const Polygon &outer, const Polygon &inner) +{ + // The rounding of the two paths, and of the projection Polygon::on_boundary() measures to. + static constexpr double touching = 3.; + for (const Point &pt : inner.points) + if (! outer.on_boundary(pt, touching)) + return contains(outer, pt, true); + const std::optional pt = ClipperUtils::interior_point(inner); + return pt && contains(outer, *pt, true); +} + +// Gives each stray hole to the innermost of the contours out[first..] around it. Contours of a Clipper2 result don't +// cross, so those around a hole are nested and the innermost one is the smallest: candidates are tried smallest first, +// from the first one larger than the hole. +void c2_attach_stray_holes(ExPolygons &out, size_t first, const std::vector &stray_holes) +{ + if (stray_holes.empty()) + return; + using Tree = AABBTreeIndirect::Tree<2, coord_t>; + std::vector bboxes; + bboxes.reserve(out.size() - first); + for (size_t i = first; i < out.size(); ++ i) + bboxes.emplace_back(i, get_extents(out[i].contour)); + Tree tree; + tree.build_modify_input(bboxes); + // Contour areas, computed when a contour first becomes a candidate. + std::vector areas(out.size(), 0.); + std::vector candidates; + for (const C2::Path64 *path : stray_holes) { + Polygon hole; + hole.points = c2_to_points(*path); + const double hole_area = - C2::Area(*path); + const BoundingBox bbox = get_extents(hole); + const Tree::BoundingBox hole_bbox(bbox.min, bbox.max); + candidates.clear(); + AABBTreeIndirect::traverse(tree, + [&hole_bbox](const Tree::Node &node) { return node.bbox.contains(hole_bbox); }, + [&candidates](const Tree::Node &node) { candidates.emplace_back(node.idx); return true; }); + for (size_t idx : candidates) + if (areas[idx] == 0.) + areas[idx] = out[idx].contour.area(); + std::sort(candidates.begin(), candidates.end(), [&areas](size_t a, size_t b) { return areas[a] < areas[b]; }); + auto larger = std::upper_bound(candidates.begin(), candidates.end(), hole_area, [&areas](double area, size_t idx) { return area < areas[idx]; }); + if (auto it = std::find_if(larger, candidates.end(), [&out, &hole](size_t idx) { return path_encloses(out[idx].contour, hole); }); + it == candidates.end()) + // Nothing around it: keep the path as Clipper2 returned it. + out.emplace_back().contour = std::move(hole); + else + out[*it].holes.emplace_back(std::move(hole)); + } } ExPolygons c2_to_expolygons(const C2::PolyTree64 &tree) @@ -307,8 +383,10 @@ ExPolygons c2_to_expolygons(const C2::PolyTree64 &tree) cnt += c2_count_expolygons(*outer); ExPolygons out; out.reserve(cnt); + std::vector stray_holes; for (const auto &outer : tree) - c2_append_expolygons(*outer, out); + c2_append_expolygons(*outer, out, stray_holes); + c2_attach_stray_holes(out, 0, stray_holes); return out; } @@ -829,6 +907,48 @@ namespace ClipperUtils { out.erase(std::remove_if(out.begin(), out.end(), [](const ExPolygonsTile &tile) { return tile.members.empty(); }), out.end()); return out; } + + // Between two consecutive heights of its vertices the polygon is a set of trapezoids, and the line through the middle + // of that band meets no vertex and no horizontal edge: its crossings with the edges alternate into and out of the + // polygon, and there each trapezoid is at least half as wide as at its widest. So the bands are tried widest first, + // until the middle of the widest span on that line rounds to a point strictly inside. A band under two units has no + // grid line inside it. + std::optional interior_point(const Polygon &polygon) + { + std::vector heights; + heights.reserve(polygon.size()); + for (const Point &pt : polygon.points) + heights.emplace_back(pt.y()); + std::sort(heights.begin(), heights.end()); + heights.erase(std::unique(heights.begin(), heights.end()), heights.end()); + std::vector> bands; + for (size_t i = 1; i < heights.size(); ++ i) + if (heights[i] - heights[i - 1] >= 2) + bands.emplace_back(heights[i - 1], heights[i]); + std::sort(bands.begin(), bands.end(), [](const auto &a, const auto &b) { return a.second - a.first > b.second - b.first; }); + std::vector crossings; + for (const auto &[low, high] : bands) { + const coord_t y = low + (high - low) / 2; + crossings.clear(); + for (size_t i = 0, j = polygon.size() - 1; i < polygon.size(); j = i ++) { + const Point &a = polygon.points[j]; + const Point &b = polygon.points[i]; + if ((a.y() > y) != (b.y() > y)) + crossings.emplace_back(double(a.x()) + double(y - a.y()) * double(b.x() - a.x()) / double(b.y() - a.y())); + } + std::sort(crossings.begin(), crossings.end()); + // Inside from the first crossing to the second, from the third to the fourth... + double left = 0., right = 0.; + for (size_t i = 1; i < crossings.size(); i += 2) + if (crossings[i] - crossings[i - 1] > right - left) { + left = crossings[i - 1]; + right = crossings[i]; + } + if (const Point pt(coord_t(std::round(0.5 * (left + right))), y); right > left && contains(polygon, pt, false)) + return pt; + } + return std::nullopt; + } } static Slic3r::ExPolygons clipper_ex_by_piece(ClipType clipType, const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset) @@ -1075,24 +1195,75 @@ Lines _clipper_ln(ClipType clipType, const Lines &subject, const Polygons &clip) return retval; } -static void traverse_pt_outside_in(const C2::PolyPath64 &parent, Polygons *retval) +// Chains the loops of one level of the tree, each followed by the loops under it, depth first. Clipper2 may put a loop +// beside the loop around it instead of under it (see c2_append_expolygons()), out of the nesting order the callers rely +// on: the spiral of FillSpiralInset continues only into a loop inside the last one. So a loop whose orientation doesn't +// fit its level (CCW under the top or a CW loop, CW under a CCW loop) goes under the smallest loop of the level around +// it, the one Clipper2 failed to put it under, and stays where it is if none is. +static void traverse_pt_outside_in(const std::vector &nodes, bool holes, Polygons *retval) { - // collect ordering points - Points ordering_points; - ordering_points.reserve(parent.Count()); - for (const auto &node : parent) - ordering_points.emplace_back(node->Polygon().front().x, node->Polygon().front().y); + struct Loop + { + const C2::PolyPath64 *node; + double area; + // The loops to traverse under this one: its children, then the misplaced loops it takes in. + std::vector next; + bool moved = false; + }; + std::vector loops; + std::vector fitting, misfits; + loops.reserve(nodes.size()); + for (const C2::PolyPath64 *node : nodes) { + Loop &loop = loops.emplace_back(Loop{ node, C2::Area(node->Polygon()), {} }); + loop.next.reserve(node->Count()); + for (const auto &child : *node) + loop.next.emplace_back(child.get()); + ((loop.area < 0.) == holes ? fitting : misfits).emplace_back(loops.size() - 1); + } + if (! misfits.empty()) { + // Candidates are tried smallest first, from the first one larger than the misplaced loop (see path_encloses()). + std::sort(fitting.begin(), fitting.end(), [&loops](size_t a, size_t b) { return std::abs(loops[a].area) < std::abs(loops[b].area); }); + std::vector bounds(loops.size()); + for (size_t i = 0; i < loops.size(); ++ i) + bounds[i] = C2::GetBounds(loops[i].node->Polygon()); + for (size_t misfit : misfits) { + Loop &loop = loops[misfit]; + const C2::Rect64 &box = bounds[misfit]; + Polygon inner; + inner.points = c2_to_points(loop.node->Polygon()); + for (size_t idx : fitting) { + // The slack of a vertex rounded across the candidate, as in c2_attach_stray_holes(). + const C2::Rect64 &around = bounds[idx]; + if (std::abs(loops[idx].area) <= std::abs(loop.area) || around.left - SCALED_EPSILON > box.left || + around.right + SCALED_EPSILON < box.right || around.top - SCALED_EPSILON > box.top || around.bottom + SCALED_EPSILON < box.bottom) + continue; + Polygon outer; + outer.points = c2_to_points(loops[idx].node->Polygon()); + if (path_encloses(outer, inner)) { + loops[idx].next.emplace_back(loop.node); + loop.moved = true; + break; + } + } + } + } - // Perform the ordering, push results recursively. + // Chain the loops staying on this level, push results recursively. + std::vector staying; + Points ordering_points; + for (size_t i = 0; i < loops.size(); ++ i) + if (! loops[i].moved) { + staying.emplace_back(i); + ordering_points.emplace_back(loops[i].node->Polygon().front().x, loops[i].node->Polygon().front().y); + } //FIXME pass the last point to chain_points? for (size_t idx : chain_points(ordering_points)) { - const C2::PolyPath64 &node = *parent.Child(idx); - retval->emplace_back().points = c2_to_points(node.Polygon()); - if (node.IsHole()) + const Loop &loop = loops[staying[idx]]; + retval->emplace_back().points = c2_to_points(loop.node->Polygon()); + if (loop.area < 0.) // Orient a hole, which is clockwise oriented, to CCW. retval->back().reverse(); - // traverse the next depth - traverse_pt_outside_in(node, retval); + traverse_pt_outside_in(loop.next, loop.area > 0., retval); } } @@ -1100,8 +1271,12 @@ Polygons union_pt_chained_outside_in(const Polygons &subject) { C2::PolyTree64 tree; c2_clip(C2::ClipType::Union, to_paths64(ClipperUtils::PolygonsProvider(subject)), {}, C2::FillRule::EvenOdd, tree); + std::vector top_level; + top_level.reserve(tree.Count()); + for (const auto &node : tree) + top_level.emplace_back(node.get()); Polygons retval; - traverse_pt_outside_in(tree, &retval); + traverse_pt_outside_in(top_level, false, &retval); return retval; } @@ -1138,6 +1313,8 @@ ExPolygons top_level_expolygons(const ExPolygons &expolygons, ExPolygons *nested c2_clip(C2::ClipType::Union, to_paths64(ClipperUtils::ExPolygonsProvider(expolygons)), {}, C2::FillRule::EvenOdd, tree); ExPolygons out; out.reserve(tree.Count()); + const size_t first_nested = nested ? nested->size() : 0; + std::vector stray_holes; for (const auto &outer : tree) { ExPolygon &expoly = out.emplace_back(); expoly.contour.points = c2_to_points(outer->Polygon()); @@ -1145,9 +1322,11 @@ ExPolygons top_level_expolygons(const ExPolygons &expolygons, ExPolygons *nested expoly.holes.emplace_back().points = c2_to_points(hole->Polygon()); if (nested) for (const auto &island : *hole) - c2_append_expolygons(*island, *nested); + c2_append_expolygons(*island, *nested, stray_holes); } } + if (nested) + c2_attach_stray_holes(*nested, first_nested, stray_holes); return out; } diff --git a/src/libslic3r/ClipperUtils.hpp b/src/libslic3r/ClipperUtils.hpp index 216815aa65..43904ff4b4 100644 --- a/src/libslic3r/ClipperUtils.hpp +++ b/src/libslic3r/ClipperUtils.hpp @@ -12,6 +12,7 @@ #include #include #include +#include #include namespace Slic3r { @@ -342,6 +343,10 @@ namespace ClipperUtils { }; [[nodiscard]] std::vector tile_expolygons(const ExPolygons &expolygons, size_t per_tile); + // A grid point strictly inside the polygon, or none if the polygon is at most a few units thick everywhere. + // Decides on which side of another path a path lies when every one of its vertices touches that path. + [[nodiscard]] std::optional interior_point(const Polygon &polygon); + } // offset Polygons diff --git a/tests/libslic3r/test_clipper_utils.cpp b/tests/libslic3r/test_clipper_utils.cpp index 2d0e1fab1b..4f8d872ae5 100644 --- a/tests/libslic3r/test_clipper_utils.cpp +++ b/tests/libslic3r/test_clipper_utils.cpp @@ -12,10 +12,13 @@ #include #include #include +#include #include #include #include +#include +#include #include "libslic3r/ClipperUtils.hpp" #include "libslic3r/ExPolygon.hpp" #include "libslic3r/Point.hpp" @@ -390,3 +393,244 @@ TEST_CASE("Tiled diff and intersection return the same polygons as the plain cal REQUIRE(area(intersection_plain) > 0.); CHECK(same_rings(intersection_tiled, intersection_plain, tolerance)); } + +// An outline with a small clockwise hole well inside it, cut along its edge by three clip polygons that never reach the +// hole. Clipper2 puts the hole at the top level of its PolyTree, beside the outline rather than under it. The coordinates +// come from a sliced layer reduced to the few paths that still do this; most changes to them don't. +static const Polygons misnesting_subject{ + Polygon({ { 23000000, 31000000 }, { 7500000, 25900000 }, { 200000, 26450004 }, { 0, 26450004 }, { 0, 26550004 }, + { -100000, 26550004 }, { -300000, 26600000 }, { 13000000, 0 } }), + Polygon({ { 10200000, 24250000 }, { 10500000, 24250000 }, { 9800000, 24000000 } }), + Polygon({ { 4500000, 22500000 }, { 5000000, 22000000 }, { 4000000, 22000000 } }), +}; +static const Polygons misnesting_clip{ + Polygon({ { 0, 26550004 }, { -200000, 26550004 }, { -200000, 26000000 } }), + Polygon({ { 0, 26450004 }, { 100000, 26450004 }, { 0, 26000000 } }), + Polygon({ { 10000000, 24000000 }, { 10199999, 24250000 }, { 10500000, 24250000 }, { 8000000, 26500000 } }), +}; + +// The case above inside a frame, a square outline with a square hole around the case, and with an island in its hole. +// Clipper2 then puts the hole beside the outline inside the frame's hole, where the frame and the outline both enclose +// it, and the island under the misplaced hole. +static Polygons misnesting_framed_subject() +{ + Polygons subject = misnesting_subject; + subject.emplace_back(Polygon({ { -2000000, -2000000 }, { 25000000, -2000000 }, { 25000000, 33000000 }, { -2000000, 33000000 } })); + subject.emplace_back(Polygon({ { -1000000, -1000000 }, { -1000000, 32000000 }, { 24000000, 32000000 }, { 24000000, -1000000 } })); + subject.emplace_back(Polygon({ { 4400000, 22100000 }, { 4600000, 22100000 }, { 4500000, 22300000 } })); + return subject; +} +// In the hole of the framed case, beside its island. +static const Point in_framed_hole(4500000, 22400000); +// In the island of the framed case. +static const Point in_framed_island(4500000, 22150000); + +static void check_orientation(const ExPolygons &expolygons) +{ + for (const ExPolygon &expoly : expolygons) { + CHECK(expoly.contour.is_counter_clockwise()); + for (const Polygon &hole : expoly.holes) + CHECK(hole.is_clockwise()); + } +} + +// How many of the expolygons contain the point, each on its own, as a per-piece operation sees them. +static size_t pieces_containing(const ExPolygons &expolygons, const Point &pt) +{ + return std::count_if(expolygons.begin(), expolygons.end(), [&pt](const ExPolygon &expoly) { return expoly.contains(pt); }); +} + +TEST_CASE("Every hole of a boolean result belongs to the expolygon around it", "[ClipperUtils]") { + const Polygon &hole = misnesting_subject.back(); + REQUIRE(hole.is_clockwise()); + const ExPolygons result = diff_ex(misnesting_subject, misnesting_clip); + check_orientation(result); + CHECK(pieces_containing(result, hole.centroid()) == 0); + CHECK_THAT(area(result), Catch::Matchers::WithinRel(area(diff(misnesting_subject, misnesting_clip)), 1e-9)); +} + +TEST_CASE("A misplaced hole goes to the innermost contour around it, an island in it stays filled", "[ClipperUtils]") { + const Polygons subject = misnesting_framed_subject(); + const ExPolygons result = diff_ex(subject, misnesting_clip); + check_orientation(result); + // Given to the frame instead of the outline, the hole would leave the outline covering it. + CHECK(pieces_containing(result, in_framed_hole) == 0); + CHECK(pieces_containing(result, in_framed_island) == 1); + CHECK_THAT(area(result), Catch::Matchers::WithinRel(area(diff(subject, misnesting_clip)), 1e-9)); +} + +TEST_CASE("A misplaced hole touching the contour around it goes to that contour", "[ClipperUtils]") { + // The first two vertices of the hole the subject's triangle cuts round a fraction of a unit outside the outline, the + // third is well inside it. Reduced from a sliced layer; Clipper2 puts the hole at the top level of its PolyTree. + const Polygons subject{ + Polygon({ { 1000000, 5000000 }, { 1000000, 4000000 }, { 6000000, 0 } }), + Polygon({ { 1024999, 4500000 }, { 1100011, 4574998 }, { 1100011, 4450000 } }), + }; + const Polygons clip{ + Polygon({ { 1500000, 5000000 }, { 1062508, 4612507 }, { 1212486, 4462504 } }), + Polygon({ { 1100000, 4430000 }, { 1024998, 4500001 }, { 2000000, 3000000 } }), + }; + const ExPolygons result = diff_ex(subject, clip); + check_orientation(result); + CHECK(pieces_containing(result, subject.back().centroid()) == 0); + CHECK_THAT(area(result), Catch::Matchers::WithinRel(area(diff(subject, clip)), 1e-9)); +} + +TEST_CASE("A misplaced island comes out as an expolygon of its own", "[ClipperUtils]") { + // Clipper2 puts the island of this result directly under the outer contour, at the level of its holes, instead of + // under the hole around it. Reduced from a sliced layer. + const Polygons subject{ + Polygon({ { 5200000, 700000 }, { 0, 5000000 }, { 0, 0 } }), + Polygon({ { 3100000, 825000 }, { 3250000, 900001 }, { 3250000, 1000000 }, { 4000000, 1500000 }, { 3000000, 500000 } }), + }; + const Polygons clip{ + Polygon({ { 4500000, 1200000 }, { 4000000, 1000000 }, { 5000000, 800000 } }), + Polygon({ { 3250000, 900001 }, { 3250000, 1000000 }, { 3000000, 1000000 }, { 3500000, 1100000 }, { 3100000, 1000000 }, { 3183343, 866672 } }), + Polygon({ { 4000000, 1000000 }, { 4000000, 1000000 }, { 4000000, 1000000 } }), + }; + const ExPolygons result = diff_ex(subject, clip); + check_orientation(result); + // In the island. + CHECK(pieces_containing(result, Point(3230000, 1020000)) == 1); + CHECK_THAT(area(result), Catch::Matchers::WithinRel(area(diff(subject, clip)), 1e-9)); +} + +TEST_CASE("Nested islands of an even-odd union keep their misplaced holes", "[ClipperUtils]") { + // The framed case as one even-odd union: the frame is the top level, everything in its hole is nested, and Clipper2 + // puts the case's hole beside the outline among the nested islands. + ExPolygons input; + for (const Polygon &polygon : misnesting_framed_subject()) + input.emplace_back(polygon); + for (const Polygon &polygon : misnesting_clip) + input.emplace_back(polygon); + ExPolygons nested; + const ExPolygons top_level = top_level_expolygons(input, &nested); + REQUIRE(top_level.size() == 1); + check_orientation(nested); + CHECK(pieces_containing(nested, in_framed_hole) == 0); + CHECK(pieces_containing(nested, in_framed_island) == 1); +} + +TEST_CASE("Loops chained outside in keep their nesting where Clipper2 misplaces one", "[ClipperUtils]") { + // An even-odd union of the case's paths leaves the hole at the top level of the PolyTree, among the outer loops. + Polygons loops = misnesting_subject; + polygons_append(loops, misnesting_clip); + const Polygons chained = union_pt_chained_outside_in(loops); + REQUIRE(chained.size() == 7); + for (const Polygon &loop : chained) + CHECK(loop.is_counter_clockwise()); + // The hole's loop comes right after the outline's, which has no other loop inside it. + const auto outline = std::max_element(chained.begin(), chained.end(), [](const Polygon &a, const Polygon &b) { return a.area() < b.area(); }); + REQUIRE(outline + 1 != chained.end()); + CHECK((outline + 1)->area() == Catch::Approx(- misnesting_subject.back().area())); +} + +TEST_CASE("A misplaced hole hugging an island inside it still goes to the outline", "[ClipperUtils]") { + // The case's hole with an island filling most of it, 20 units from it: the hole's vertices all lie close to the + // island and its middle inside it, yet the island is inside the hole, not around it. + Polygons subject = misnesting_subject; + subject.emplace_back(Polygon({ { 4000020, 22000010 }, { 4500000, 22150000 }, { 4999980, 22000010 }, + { 4650000, 22160000 }, { 4500000, 22499980 }, { 4350000, 22160000 } })); + const ExPolygons result = diff_ex(subject, misnesting_clip); + check_orientation(result); + // In the hole below the island, and in the island. + CHECK(pieces_containing(result, Point(4500000, 22050000)) == 0); + CHECK(pieces_containing(result, Point(4500000, 22300000)) == 1); + CHECK_THAT(area(result), Catch::Matchers::WithinRel(area(diff(subject, misnesting_clip)), 1e-9)); +} + +TEST_CASE("A concave misplaced hole touching the outline at every vertex goes to it", "[ClipperUtils]") { + // The case with the outline drawn in to two more vertices and the hole replaced by a concave pentagon, each of its + // vertices within 3 units of the outline; the middle of its bounding box is outside both. + Polygons subject = misnesting_subject; + subject.front().points.insert(subject.front().points.end(), { { 7000000, 20000000 }, { 20000000, 22000000 } }); + subject.back() = Polygon({ { 1500002, 22999999 }, { 20333332, 22999999 }, { 19999999, 22000001 }, { 6999998, 20000001 }, { 12999995, 13 } }); + const ExPolygons result = diff_ex(subject, misnesting_clip); + check_orientation(result); + CHECK(pieces_containing(result, Point(10000000, 22500000)) == 0); + CHECK_THAT(area(result), Catch::Matchers::WithinRel(area(diff(subject, misnesting_clip)), 1e-9)); +} + +TEST_CASE("A point strictly inside a polygon, where there is one", "[ClipperUtils]") { + // A notch with a horizontal edge at the middle of its bounding box, so a line through that middle runs along the edge; + // two lobes joined by a one unit neck and a square with a one unit spike, where the widest band between vertex heights + // runs through the thin part. + const Polygons wide{ + Polygon({ { 0, 0 }, { 2000000, 0 }, { 2000000, 5000000 }, { 8000000, 5000000 }, { 8000000, 0 }, { 10000000, 0 }, + { 10000000, 10000000 }, { 0, 10000000 } }), + Polygon({ { 0, 0 }, { 100, 0 }, { 100, 5 }, { 51, 5 }, { 51, 25 }, { 100, 25 }, { 100, 30 }, { 0, 30 }, { 0, 25 }, + { 50, 25 }, { 50, 5 }, { 0, 5 } }), + Polygon({ { 0, 0 }, { 1000000, 0 }, { 1000000, 1000000 }, { 500001, 1000000 }, { 500001, 5000000 }, { 500000, 5000000 }, + { 500000, 1000000 }, { 0, 1000000 } }), + }; + for (Polygon polygon : wide) + for (int winding = 0; winding < 2; ++ winding, polygon.reverse()) { + const std::optional pt = ClipperUtils::interior_point(polygon); + REQUIRE(pt); + CHECK(contains(polygon, *pt, false)); + } + // One unit thick everywhere, these hold no grid point strictly inside: a strip, and rows one unit high joined at the + // left. + CHECK_FALSE(ClipperUtils::interior_point(Polygon({ { 0, 0 }, { 1000, 0 }, { 1000, 1 }, { 0, 1 } }))); + Polygon rows({ { 0, 0 }, { 100, 0 } }); + for (coord_t y = 1; y < 100; y += 2) + rows.points.insert(rows.points.end(), { { 100, y }, { 1, y }, { 1, y + 1 }, { 100, y + 1 } }); + rows.points.emplace_back(0, 100); + CHECK_FALSE(ClipperUtils::interior_point(rows)); +} + +TEST_CASE("A correctly nested result converts in the order of its tree", "[ClipperUtils]") { + // Two squares, one with a hole holding an island, the other with two holes and a corner cut off. Clipper2 nests this + // correctly, so the expolygons follow its tree, each path as Clipper2 returned it: an outline with its holes in tree + // order, then the islands of those holes, outline by outline. + const Polygons subject{ + Polygon({ { 0, 0 }, { 10000000, 0 }, { 10000000, 10000000 }, { 0, 10000000 } }), + Polygon({ { 2000000, 2000000 }, { 2000000, 8000000 }, { 8000000, 8000000 }, { 8000000, 2000000 } }), + Polygon({ { 4000000, 4000000 }, { 6000000, 4000000 }, { 6000000, 6000000 }, { 4000000, 6000000 } }), + Polygon({ { 20000000, 0 }, { 32000000, 0 }, { 32000000, 12000000 }, { 20000000, 12000000 } }), + Polygon({ { 21000000, 1000000 }, { 21000000, 4000000 }, { 24000000, 4000000 }, { 24000000, 1000000 } }), + Polygon({ { 26000000, 6000000 }, { 26000000, 10000000 }, { 30000000, 10000000 }, { 30000000, 6000000 } }), + }; + const Polygons clip{ Polygon({ { 31000000, -1000000 }, { 33000000, -1000000 }, { 33000000, 1000000 }, { 31000000, 1000000 } }) }; + ExPolygons expected(3); + expected[0].contour = Polygon({ { 31000000, 1000000 }, { 32000000, 1000000 }, { 32000000, 12000000 }, { 20000000, 12000000 }, + { 20000000, 0 }, { 31000000, 0 } }); + expected[0].holes = { Polygon({ { 26000000, 10000000 }, { 30000000, 10000000 }, { 30000000, 6000000 }, { 26000000, 6000000 } }), + Polygon({ { 21000000, 4000000 }, { 24000000, 4000000 }, { 24000000, 1000000 }, { 21000000, 1000000 } }) }; + expected[1].contour = Polygon({ { 10000000, 10000000 }, { 0, 10000000 }, { 0, 0 }, { 10000000, 0 } }); + expected[1].holes = { Polygon({ { 2000000, 8000000 }, { 8000000, 8000000 }, { 8000000, 2000000 }, { 2000000, 2000000 } }) }; + expected[2].contour = Polygon({ { 6000000, 6000000 }, { 4000000, 6000000 }, { 4000000, 4000000 }, { 6000000, 4000000 } }); + CHECK(diff_ex(subject, clip) == expected); +} + +TEST_CASE("Loops of a correctly nested union chain in the order of its tree", "[ClipperUtils]") { + // Three nested squares and two more beside them, as concentric fill hands them over: chained outside in, depth first, + // each loop as Clipper2 returned it, the holes reversed to run counter-clockwise. + const Polygons loops{ + Polygon({ { 0, 0 }, { 10000000, 0 }, { 10000000, 10000000 }, { 0, 10000000 } }), + Polygon({ { 1000000, 1000000 }, { 9000000, 1000000 }, { 9000000, 9000000 }, { 1000000, 9000000 } }), + Polygon({ { 2000000, 2000000 }, { 8000000, 2000000 }, { 8000000, 8000000 }, { 2000000, 8000000 } }), + Polygon({ { 20000000, 0 }, { 32000000, 0 }, { 32000000, 12000000 }, { 20000000, 12000000 } }), + Polygon({ { 21000000, 1000000 }, { 31000000, 1000000 }, { 31000000, 11000000 }, { 21000000, 11000000 } }), + }; + const Polygons expected{ + Polygon({ { 10000000, 10000000 }, { 0, 10000000 }, { 0, 0 }, { 10000000, 0 } }), + Polygon({ { 9000000, 1000000 }, { 9000000, 9000000 }, { 1000000, 9000000 }, { 1000000, 1000000 } }), + Polygon({ { 8000000, 8000000 }, { 2000000, 8000000 }, { 2000000, 2000000 }, { 8000000, 2000000 } }), + Polygon({ { 32000000, 12000000 }, { 20000000, 12000000 }, { 20000000, 0 }, { 32000000, 0 } }), + Polygon({ { 31000000, 1000000 }, { 31000000, 11000000 }, { 21000000, 11000000 }, { 21000000, 1000000 } }), + }; + CHECK(union_pt_chained_outside_in(loops) == expected); +} + +TEST_CASE("A clockwise sliver with nothing around it comes back as Clipper2 returned it", "[ClipperUtils]") { + // Rounding leaves a clockwise sliver of half a square unit beside the result's one piece, outside it. Reduced from a + // sliced layer. It is no hole of anything, so it stays a contour of its own, neither dropped nor given to the piece. + const Polygons subject{ Polygon({ { 875000, 200000 }, { 900002, 175002 }, { 950001, 150004 } }) }; + const Polygons clip{ Polygon({ { 879409, 185299 }, { 950000, 150004 }, { 1000000, 200000 } }) }; + const ExPolygons result = diff_ex(subject, clip); + REQUIRE(result.size() == 2); + CHECK(std::count_if(result.begin(), result.end(), [](const ExPolygon &expoly) { return expoly.contour.is_clockwise() && expoly.holes.empty(); }) == 1); + CHECK(std::all_of(result.begin(), result.end(), [](const ExPolygon &expoly) { return expoly.holes.empty(); })); + CHECK_THAT(area(result), Catch::Matchers::WithinRel(area(diff(subject, clip)), 1e-9)); +}