mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-20 22:18:30 +00:00
Attach holes Clipper2 misplaces to the expolygon around them
This commit is contained in:
1 parent
55b8fe296f
commit
651eed392c
3 files changed
+453
-25
No files matched your search
+204
-25
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
Reference in new issue
Block a user