Attach holes Clipper2 misplaces to the expolygon around them

This commit is contained in:
SoftFever committed 2026-10-21 01:13:02 +08:00
1 parent 55b8fe296f
commit 651eed392c
3 files changed
+453 -25

No files matched your search

+204 -25
View File
@@ -7,11 +7,13 @@
#include <cmath>
#include <limits>
#include <numeric>
#include <optional>
#include <unordered_map>
#include <tbb/parallel_for.h>
#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<const C2::Path64*> &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<Point> 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<const C2::Path64*> &stray_holes)
{
if (stray_holes.empty())
return;
using Tree = AABBTreeIndirect::Tree<2, coord_t>;
std::vector<AABBTreeIndirect::BoundingBoxWrapper> 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<double> areas(out.size(), 0.);
std::vector<size_t> 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<const C2::Path64*> 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<Point> interior_point(const Polygon &polygon)
{
std::vector<coord_t> 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<std::pair<coord_t, coord_t>> 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<double> 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<const C2::PolyPath64*> &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<const C2::PolyPath64*> next;
bool moved = false;
};
std::vector<Loop> loops;
std::vector<size_t> 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<C2::Rect64> 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<size_t> 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<const C2::PolyPath64*> 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<const C2::Path64*> 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;
}
+5
View File
@@ -12,6 +12,7 @@
#include <cstddef>
#include <iterator>
#include <cassert>
#include <optional>
#include <vector>
namespace Slic3r {
@@ -342,6 +343,10 @@ namespace ClipperUtils {
};
[[nodiscard]] std::vector<ExPolygonsTile> 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<Point> interior_point(const Polygon &polygon);
}
// offset Polygons
+244
View File
@@ -12,10 +12,13 @@
#include <utility>
#include <vector>
#include <cstdlib>
#include <optional>
#include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp>
#include <catch2/catch_approx.hpp>
#include <catch2/matchers/catch_matchers.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#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<Point> 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));
}