From 2d762be59003771de7a35610a15dd499abbe547f Mon Sep 17 00:00:00 2001 From: sentientstardust Date: Wed, 27 May 2026 18:07:51 +0100 Subject: [PATCH] Update Clipper2 to 2.0.1 (f9c5eb6) --- deps_src/clipper2/CMakeLists.txt | 5 +- .../include/clipper2/clipper.core.h | 158 ++- .../include/clipper2/clipper.engine.h | 28 +- .../include/clipper2/clipper.export.h | 19 + .../Clipper2Lib/include/clipper2/clipper.h | 100 +- .../include/clipper2/clipper.minkowski.h | 2 +- .../include/clipper2/clipper.offset.h | 10 +- .../include/clipper2/clipper.rectclip.h | 2 +- .../include/clipper2/clipper.triangulation.h | 30 + .../include/clipper2/clipper.version.h | 2 +- .../Clipper2Lib/src/clipper.engine.cpp | 139 +- .../Clipper2Lib/src/clipper.offset.cpp | 31 +- .../Clipper2Lib/src/clipper.rectclip.cpp | 16 +- .../Clipper2Lib/src/clipper.triangulation.cpp | 1224 +++++++++++++++++ .../clipper2/Clipper2Lib/src/clipper2_z.cpp | 1 + 15 files changed, 1583 insertions(+), 184 deletions(-) create mode 100644 deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.triangulation.h create mode 100644 deps_src/clipper2/Clipper2Lib/src/clipper.triangulation.cpp diff --git a/deps_src/clipper2/CMakeLists.txt b/deps_src/clipper2/CMakeLists.txt index c604002da70..071fb99bd0c 100644 --- a/deps_src/clipper2/CMakeLists.txt +++ b/deps_src/clipper2/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(Clipper2 VERSION 1.5.2 LANGUAGES C CXX) +project(Clipper2 VERSION 2.0.1 LANGUAGES C CXX) set(CMAKE_POSITION_INDEPENDENT_CODE ON) set(CMAKE_CXX_STANDARD 17) @@ -19,6 +19,7 @@ set(CLIPPER2_INC Clipper2Lib/include/clipper2/clipper.minkowski.h Clipper2Lib/include/clipper2/clipper.offset.h Clipper2Lib/include/clipper2/clipper.rectclip.h + Clipper2Lib/include/clipper2/clipper.triangulation.h Clipper2Lib/include/clipper2/clipper2_z.hpp ) @@ -26,6 +27,7 @@ set(CLIPPER2_SRC Clipper2Lib/src/clipper.engine.cpp Clipper2Lib/src/clipper.offset.cpp Clipper2Lib/src/clipper.rectclip.cpp + Clipper2Lib/src/clipper.triangulation.cpp Clipper2Lib/src/clipper2_z.cpp ) @@ -51,4 +53,3 @@ set_target_properties(Clipper2 PROPERTIES FOLDER Libraries SOVERSION ${PROJECT_VERSION_MAJOR} PUBLIC_HEADER "${CLIPPER2_INC}" ) - diff --git a/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.core.h b/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.core.h index 6d00f8fa073..630259764ce 100644 --- a/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.core.h +++ b/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.core.h @@ -1,8 +1,8 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 12 May 2024 * +* Date : 12 October 2025 * * Website : https://www.angusj.com * -* Copyright : Angus Johnson 2010-2024 * +* Copyright : Angus Johnson 2010-2025 * * Purpose : Core Clipper Library structures and functions * * License : https://www.boost.org/LICENSE_1_0.txt * *******************************************************************************/ @@ -141,7 +141,7 @@ namespace Clipper2Lib { } } - explicit Point() : x(0), y(0), z(0) {}; + explicit Point() : x(0), y(0), z(0) {} template Point(const T2 x_, const T2 y_, const z_type z_ = 0) @@ -193,7 +193,7 @@ namespace Clipper2Lib { } } - explicit Point() : x(0), y(0) {}; + explicit Point() : x(0), y(0) {} template Point(const T2 x_, const T2 y_) { Init(x_, y_); } @@ -251,6 +251,20 @@ namespace Clipper2Lib { template using Paths = std::vector>; + template + Path& operator<<(Path& poly, const Point& p) + { + poly.emplace_back(p); + return poly; + } + + template + Paths& operator<<(Paths& polys, const Path& p) + { + polys.emplace_back(p); + return polys; + } + using Path64 = Path; using PathD = Path; using Paths64 = std::vector< Path64>; @@ -356,13 +370,13 @@ namespace Clipper2Lib { bottom *= scale; } - bool IsEmpty() const { return bottom <= top || right <= left; }; + bool IsEmpty() const { return bottom <= top || right <= left; } bool Intersects(const Rect& rec) const { return ((std::max)(left, rec.left) <= (std::min)(right, rec.right)) && ((std::max)(top, rec.top) <= (std::min)(bottom, rec.bottom)); - }; + } bool operator==(const Rect& other) const { return left == other.left && right == other.right && @@ -688,29 +702,28 @@ namespace Clipper2Lib { return (x > 0) - (x < 0); } - struct MultiplyUInt64Result + struct UInt128Struct { - const uint64_t result = 0; - const uint64_t carry = 0; + const uint64_t lo = 0; + const uint64_t hi = 0; - bool operator==(const MultiplyUInt64Result& other) const + bool operator==(const UInt128Struct& other) const { - return result == other.result && carry == other.carry; - }; + return lo == other.lo && hi == other.hi; + } + }; - inline MultiplyUInt64Result Multiply(uint64_t a, uint64_t b) // #834, #835 + inline UInt128Struct MultiplyUInt64(uint64_t a, uint64_t b) // #834, #835 { + // note to self - lamba expressions follow const auto lo = [](uint64_t x) { return x & 0xFFFFFFFF; }; const auto hi = [](uint64_t x) { return x >> 32; }; const uint64_t x1 = lo(a) * lo(b); const uint64_t x2 = hi(a) * lo(b) + hi(x1); const uint64_t x3 = lo(a) * hi(b) + lo(x2); - const uint64_t result = lo(x3) << 32 | lo(x1); - const uint64_t carry = hi(a) * hi(b) + hi(x2) + hi(x3); - - return { result, carry }; + return { uint64_t(lo(x3) << 32 | lo(x1)), uint64_t(hi(a) * hi(b) + hi(x2) + hi(x3)) }; } // returns true if (and only if) a * b == c * d @@ -727,14 +740,50 @@ namespace Clipper2Lib { const auto abs_c = static_cast(std::abs(c)); const auto abs_d = static_cast(std::abs(d)); - const auto abs_ab = Multiply(abs_a, abs_b); - const auto abs_cd = Multiply(abs_c, abs_d); + const auto ab = MultiplyUInt64(abs_a, abs_b); + const auto cd = MultiplyUInt64(abs_c, abs_d); // nb: it's important to differentiate 0 values here from other values const auto sign_ab = TriSign(a) * TriSign(b); const auto sign_cd = TriSign(c) * TriSign(d); - return abs_ab == abs_cd && sign_ab == sign_cd; + return ab == cd && sign_ab == sign_cd; +#endif + } + + template + inline int CrossProductSign(const Point& pt1, const Point& pt2, const Point& pt3) + { + const auto a = pt2.x - pt1.x; + const auto b = pt3.y - pt2.y; + const auto c = pt2.y - pt1.y; + const auto d = pt3.x - pt2.x; + +#if (defined(__clang__) || defined(__GNUC__)) && UINTPTR_MAX >= UINT64_MAX + const auto ab = static_cast<__int128_t>(a) * static_cast<__int128_t>(b); + const auto cd = static_cast<__int128_t>(c) * static_cast<__int128_t>(d); + if (ab > cd) return 1; + else if (ab < cd) return -1; + else return 0; +#else + const auto ab = MultiplyUInt64(std::abs(a), std::abs(b)); + const auto cd = MultiplyUInt64(std::abs(c), std::abs(d)); + + const auto sign_ab = TriSign(a) * TriSign(b); + const auto sign_cd = TriSign(c) * TriSign(d); + + if (sign_ab == sign_cd) + { + int result; + if (ab.hi == cd.hi) + { + if (ab.lo == cd.lo) return 0; + result = (ab.lo > cd.lo) ? 1 : -1; + } + else result = (ab.hi > cd.hi) ? 1 : -1; + return (sign_ab > 0) ? result : -result; + } + return (sign_ab > sign_cd) ? 1 : -1; #endif } @@ -838,14 +887,18 @@ namespace Clipper2Lib { return Area(poly) >= 0; } -#if CLIPPER2_HI_PRECISION + // GetLineIntersectPt - a 'true' result is non-parallel. The 'ip' will also + // be constrained to seg1. However, it's possible that 'ip' won't be inside + // seg2, even when 'ip' hasn't been constrained (ie 'ip' is inside seg1). + +#if defined(CLIPPER2_HI_PRECISION) && CLIPPER2_HI_PRECISION // caution: this will compromise performance // https://github.com/AngusJohnson/Clipper2/issues/317#issuecomment-1314023253 // See also CPP/BenchMark/GetIntersectPtBenchmark.cpp #define CC_MIN(x,y) ((x)>(y)?(y):(x)) #define CC_MAX(x,y) ((x)<(y)?(y):(x)) template - inline bool GetSegmentIntersectPt(const Point& ln1a, const Point& ln1b, + inline bool GetLineIntersectPt(const Point& ln1a, const Point& ln1b, const Point& ln2a, const Point& ln2b, Point& ip) { double ln1dy = static_cast(ln1b.y - ln1a.y); @@ -891,11 +944,14 @@ namespace Clipper2Lib { ip.x = originx + static_cast(hitx); ip.y = originy + static_cast(hity); } +#ifdef USINGZ + ip.z = 0; +#endif return true; } #else template - inline bool GetSegmentIntersectPt(const Point& ln1a, const Point& ln1b, + inline bool GetLineIntersectPt(const Point& ln1a, const Point& ln1b, const Point& ln2a, const Point& ln2b, Point& ip) { // https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection @@ -913,7 +969,10 @@ namespace Clipper2Lib { { ip.x = static_cast(ln1a.x + t * dx1); ip.y = static_cast(ln1a.y + t * dy1); - } +#ifdef USINGZ + ip.z = 0; +#endif + } return true; } #endif @@ -949,21 +1008,44 @@ namespace Clipper2Lib { inline bool SegmentsIntersect(const Point64& seg1a, const Point64& seg1b, const Point64& seg2a, const Point64& seg2b, bool inclusive = false) { + double dy1 = static_cast(seg1b.y - seg1a.y); + double dx1 = static_cast(seg1b.x - seg1a.x); + double dy2 = static_cast(seg2b.y - seg2a.y); + double dx2 = static_cast(seg2b.x - seg2a.x); + double cp = dy1 * dx2 - dy2 * dx1; + if (cp == 0) return false; // ie parallel segments + if (inclusive) { - double res1 = CrossProduct(seg1a, seg2a, seg2b); - double res2 = CrossProduct(seg1b, seg2a, seg2b); - if (res1 * res2 > 0) return false; - double res3 = CrossProduct(seg2a, seg1a, seg1b); - double res4 = CrossProduct(seg2b, seg1a, seg1b); - if (res3 * res4 > 0) return false; - return (res1 || res2 || res3 || res4); // ensures not collinear + //result **includes** segments that touch at an end point + double t = ((seg1a.x - seg2a.x) * dy2 - (seg1a.y - seg2a.y) * dx2); + if (t == 0) return true; + if (t > 0) + { + if (cp < 0 || t > cp) return false; + } + else if (cp > 0 || t < cp) return false; // false when t more neg. than cp + + t = ((seg1a.x - seg2a.x) * dy1 - (seg1a.y - seg2a.y) * dx1); + if (t == 0) return true; + if (t > 0) return (cp > 0 && t <= cp); + else return (cp < 0 && t >= cp); // true when t less neg. than cp } - else { - return (GetSign(CrossProduct(seg1a, seg2a, seg2b)) * - GetSign(CrossProduct(seg1b, seg2a, seg2b)) < 0) && - (GetSign(CrossProduct(seg2a, seg1a, seg1b)) * - GetSign(CrossProduct(seg2b, seg1a, seg1b)) < 0); + else + { + //result **excludes** segments that touch at an end point + double t = ((seg1a.x - seg2a.x) * dy2 - (seg1a.y - seg2a.y) * dx2); + if (t == 0) return false; + if (t > 0) + { + if (cp < 0 || t >= cp) return false; + } + else if (cp > 0 || t <= cp ) return false; // false when t more neg. than cp + + t = ((seg1a.x - seg2a.x) * dy1 - (seg1a.y - seg2a.y) * dx1); + if (t == 0) return false; + if (t > 0) return (cp > 0 && t < cp); + else return (cp < 0 && t > cp); // true when t less neg. than cp } } @@ -1051,7 +1133,7 @@ namespace Clipper2Lib { val = 1 - val; // toggle val else { - double d = CrossProduct(*prev, *curr, pt); + int d = CrossProductSign(*prev, *curr, pt); if (d == 0) return PointInPolygonResult::IsOn; if ((d < 0) == is_above) val = 1 - val; } @@ -1065,7 +1147,7 @@ namespace Clipper2Lib { if (curr == cend) curr = cbegin; if (curr == cbegin) prev = cend - 1; else prev = curr - 1; - double d = CrossProduct(*prev, *curr, pt); + int d = CrossProductSign(*prev, *curr, pt); if (d == 0) return PointInPolygonResult::IsOn; if ((d < 0) == is_above) val = 1 - val; } diff --git a/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.engine.h b/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.engine.h index 8387d2337d7..2f438286810 100644 --- a/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.engine.h +++ b/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.engine.h @@ -97,7 +97,7 @@ namespace Clipper2Lib { if (splits) delete splits; // nb: don't delete the split pointers // as these are owned by ClipperBase's outrec_list_ - }; + } }; /////////////////////////////////////////////////////////////////// @@ -160,7 +160,7 @@ namespace Clipper2Lib { struct HorzJoin { OutPt* op1 = nullptr; OutPt* op2 = nullptr; - HorzJoin() {}; + HorzJoin() {} explicit HorzJoin(OutPt* ltr, OutPt* rtl) : op1(ltr), op2(rtl) { } }; @@ -280,11 +280,11 @@ namespace Clipper2Lib { void AddPaths(const Paths64& paths, PathType polytype, bool is_open); public: virtual ~ClipperBase(); - int ErrorCode() const { return error_code_; }; - void PreserveCollinear(bool val) { preserve_collinear_ = val; }; - bool PreserveCollinear() const { return preserve_collinear_;}; - void ReverseSolution(bool val) { reverse_solution_ = val; }; - bool ReverseSolution() const { return reverse_solution_; }; + int ErrorCode() const { return error_code_; } + void PreserveCollinear(bool val) { preserve_collinear_ = val; } + bool PreserveCollinear() const { return preserve_collinear_;} + void ReverseSolution(bool val) { reverse_solution_ = val; } + bool ReverseSolution() const { return reverse_solution_; } void Clear(); void AddReuseableData(const ReuseableDataContainer64& reuseable_data); #ifdef USINGZ @@ -304,7 +304,7 @@ namespace Clipper2Lib { PolyPath* parent_; public: PolyPath(PolyPath* parent = nullptr): parent_(parent){} - virtual ~PolyPath() {}; + virtual ~PolyPath() {} //https://en.cppreference.com/w/cpp/language/rule_of_three PolyPath(const PolyPath&) = delete; PolyPath& operator=(const PolyPath&) = delete; @@ -352,7 +352,7 @@ namespace Clipper2Lib { explicit PolyPath64(PolyPath64* parent = nullptr) : PolyPath(parent) {} explicit PolyPath64(PolyPath64* parent, const Path64& path) : PolyPath(parent) { polygon_ = path; } - ~PolyPath64() { + ~PolyPath64() override { childs_.resize(0); } @@ -384,7 +384,7 @@ namespace Clipper2Lib { return childs_.size(); } - const Path64& Polygon() const { return polygon_; }; + const Path64& Polygon() const { return polygon_; } double Area() const { @@ -418,7 +418,7 @@ namespace Clipper2Lib { polygon_ = path; } - ~PolyPathD() { + ~PolyPathD() override { childs_.resize(0); } @@ -458,7 +458,7 @@ namespace Clipper2Lib { return childs_.size(); } - const PathD& Polygon() const { return polygon_; }; + const PathD& Polygon() const { return polygon_; } double Area() const { @@ -548,7 +548,7 @@ namespace Clipper2Lib { } #ifdef USINGZ - void SetZCallback(ZCallbackD cb) { zCallbackD_ = cb; }; + void SetZCallback(ZCallbackD cb) { zCallbackD_ = cb; } void ZCB(const Point64& e1bot, const Point64& e1top, const Point64& e2bot, const Point64& e2top, Point64& pt) @@ -564,7 +564,7 @@ namespace Clipper2Lib { PointD e2t = PointD(e2top) * invScale_; zCallbackD_(e1b,e1t, e2b, e2t, tmp); pt.z = tmp.z; // only update 'z' - }; + } void CheckCallback() { diff --git a/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.export.h b/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.export.h index a0d33d51df2..81f847140c3 100644 --- a/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.export.h +++ b/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.export.h @@ -116,6 +116,7 @@ the four vertices that define the two segments that are intersecting. #include "clipper2/clipper.engine.h" #include "clipper2/clipper.offset.h" #include "clipper2/clipper.rectclip.h" +#include "clipper2/clipper.triangulation.h" #include #ifdef USINGZ @@ -816,6 +817,24 @@ EXTERN_DLL_EXPORT CPaths64 MinkowskiDiff64(const CPath64& cpattern, const CPath6 return CreateCPathsFromPathsT(solution); } +EXTERN_DLL_EXPORT CPaths64 Triangulate64(const CPaths64 paths, bool use_delaunay) +{ + Paths64 pp = ConvertCPathsToPathsT(paths); + Paths64 sol; + if (Triangulate(pp, sol, use_delaunay) != TriangulateResult::success) return nullptr; + return CreateCPathsFromPathsT(sol); +} + +EXTERN_DLL_EXPORT CPathsD TriangulateD(const CPathsD paths, int decimal_precison, bool use_delaunay) +{ + if (decimal_precison < -8 || decimal_precison > 8) return nullptr; + const double scale = std::pow(10, decimal_precison); + Paths64 pp = ConvertCPathsDToPaths64(paths, scale); + Paths64 sol; + if (Triangulate(pp, sol, use_delaunay) != TriangulateResult::success) return nullptr; + return CreateCPathsDFromPaths64(sol, 1 / scale); +} + #ifdef USINGZ typedef void (*DLLZCallback64)(const Point64& e1bot, const Point64& e1top, const Point64& e2bot, const Point64& e2top, Point64& pt); typedef void (*DLLZCallbackD)(const PointD& e1bot, const PointD& e1top, const PointD& e2bot, const PointD& e2top, PointD& pt); diff --git a/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.h b/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.h index f6301fc52ec..be11b7578d1 100644 --- a/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.h +++ b/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.h @@ -1,8 +1,8 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 27 April 2024 * +* Date : 5 March 2025 * * Website : https://www.angusj.com * -* Copyright : Angus Johnson 2010-2024 * +* Copyright : Angus Johnson 2010-2025 * * Purpose : This module provides a simple interface to the Clipper Library * * License : https://www.boost.org/LICENSE_1_0.txt * *******************************************************************************/ @@ -13,8 +13,9 @@ #include "clipper2/clipper.core.h" #include "clipper2/clipper.engine.h" #include "clipper2/clipper.offset.h" -#include "clipper2/clipper.minkowski.h" #include "clipper2/clipper.rectclip.h" +#include "clipper2/clipper.minkowski.h" +#include "clipper2/clipper.triangulation.h" #include #ifdef USINGZ @@ -137,7 +138,7 @@ JoinType jt, EndType et, double miter_limit = 2.0, double arc_tolerance = 0.0) { - if (!delta) return paths; + if (delta==0.0) return paths; ClipperOffset clip_offset(miter_limit, arc_tolerance); clip_offset.AddPaths(paths, jt, et); Paths64 solution; @@ -151,10 +152,10 @@ { int error_code = 0; CheckPrecisionRange(precision, error_code); - if (!delta) return paths; + if (delta==0.0) return paths; if (error_code) return PathsD(); const double scale = std::pow(10, precision); - ClipperOffset clip_offset(miter_limit, arc_tolerance); + ClipperOffset clip_offset(miter_limit, arc_tolerance * scale); clip_offset.AddPaths(ScalePaths(paths, scale, error_code), jt, et); if (error_code) return PathsD(); Paths64 solution; @@ -355,6 +356,29 @@ #endif } + inline size_t GetNext(size_t current, size_t high, + const std::vector& flags) + { + ++current; + while (current <= high && flags[current]) ++current; + if (current <= high) return current; + current = 0; + while (flags[current]) ++current; + return current; + } + + inline size_t GetPrior(size_t current, size_t high, + const std::vector& flags) + { + if (current == 0) current = high; + else --current; + while (current > 0 && flags[current]) --current; + if (!flags[current]) return current; + current = high; + while (flags[current]) --current; + return current; + } + } // end details namespace inline std::ostream& operator<< (std::ostream& os, const PolyTree64& pp) @@ -615,29 +639,6 @@ return result; } - inline size_t GetNext(size_t current, size_t high, - const std::vector& flags) - { - ++current; - while (current <= high && flags[current]) ++current; - if (current <= high) return current; - current = 0; - while (flags[current]) ++current; - return current; - } - - inline size_t GetPrior(size_t current, size_t high, - const std::vector& flags) - { - if (current == 0) current = high; - else --current; - while (current > 0 && flags[current]) --current; - if (!flags[current]) return current; - current = high; - while (flags[current]) --current; - return current; - } - template inline Path SimplifyPath(const Path &path, double epsilon, bool isClosedPath = true) @@ -669,13 +670,13 @@ start = curr; do { - curr = GetNext(curr, high, flags); + curr = details::GetNext(curr, high, flags); } while (curr != start && distSqr[curr] > epsSqr); if (curr == start) break; } - prior = GetPrior(curr, high, flags); - next = GetNext(curr, high, flags); + prior = details::GetPrior(curr, high, flags); + next = details::GetNext(curr, high, flags); if (next == prior) break; // flag for removal the smaller of adjacent 'distances' @@ -684,14 +685,14 @@ prior2 = prior; prior = curr; curr = next; - next = GetNext(next, high, flags); + next = details::GetNext(next, high, flags); } else - prior2 = GetPrior(prior, high, flags); + prior2 = details::GetPrior(prior, high, flags); flags[curr] = true; curr = next; - next = GetNext(next, high, flags); + next = details::GetNext(next, high, flags); if (isClosedPath || ((curr != high) && (curr != 0))) distSqr[curr] = PerpendicDistFromLineSqrd(path[curr], path[prior], path[next]); @@ -716,6 +717,35 @@ return result; } + + template + inline bool Path2ContainsPath1(const Path& path1, const Path& path2) + { + // precondition: paths must not intersect, except for + // transient (and presumed 'micro') path intersections + PointInPolygonResult pip = PointInPolygonResult::IsOn; + for (const Point& pt : path1) + { + switch (PointInPolygon(pt, path2)) + { + case PointInPolygonResult::IsOutside: + if (pip == PointInPolygonResult::IsOutside) return false; + pip = PointInPolygonResult::IsOutside; + break; + case PointInPolygonResult::IsInside: + if (pip == PointInPolygonResult::IsInside) return true; + pip = PointInPolygonResult::IsInside; + break; + default: + break; + } + } + if (pip != PointInPolygonResult::IsInside) return false; + // result is likely true but check midpoint + Point mp1 = GetBounds(path1).MidPoint(); + return PointInPolygon(mp1, path2) == PointInPolygonResult::IsInside; + } + template inline void RDP(const Path path, std::size_t begin, std::size_t end, double epsSqrd, std::vector& flags) diff --git a/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.minkowski.h b/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.minkowski.h index 60d8d17c335..9a23df7f6d7 100644 --- a/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.minkowski.h +++ b/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.minkowski.h @@ -63,7 +63,7 @@ namespace Clipper2Lib { quad.emplace_back(tmp[i][h]); quad.emplace_back(tmp[i][j]); quad.emplace_back(tmp[g][j]); - }; + } if (!IsPositive(quad)) std::reverse(quad.begin(), quad.end()); result.emplace_back(std::move(quad)); diff --git a/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.offset.h b/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.offset.h index a7d53df36fe..f0ef0d2691c 100644 --- a/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.offset.h +++ b/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.offset.h @@ -39,7 +39,7 @@ private: class Group { public: Paths64 paths_in; - std::optional lowest_path_idx{}; + std::optional lowest_path_idx{}; bool is_reversed = false; JoinType join_type; EndType end_type; @@ -92,14 +92,14 @@ public: bool reverse_solution = false) : miter_limit_(miter_limit), arc_tolerance_(arc_tolerance), preserve_collinear_(preserve_collinear), - reverse_solution_(reverse_solution) { }; + reverse_solution_(reverse_solution) { } - ~ClipperOffset() { Clear(); }; + ~ClipperOffset() { Clear(); } - int ErrorCode() const { return error_code_; }; + int ErrorCode() const { return error_code_; } void AddPath(const Path64& path, JoinType jt_, EndType et_); void AddPaths(const Paths64& paths, JoinType jt_, EndType et_); - void Clear() { groups_.clear(); norms.clear(); }; + void Clear() { groups_.clear(); norms.clear(); } void Execute(double delta, Paths64& sols_64); void Execute(double delta, PolyTree64& polytree); diff --git a/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.rectclip.h b/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.rectclip.h index 0825a93d768..6fee6eefd32 100644 --- a/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.rectclip.h +++ b/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.rectclip.h @@ -75,7 +75,7 @@ namespace Clipper2Lib { void ExecuteInternal(const Path64& path); Path64 GetPath(OutPt2*& op); public: - explicit RectClipLines64(const Rect64& rect) : RectClip64(rect) {}; + explicit RectClipLines64(const Rect64& rect) : RectClip64(rect) {} Paths64 Execute(const Paths64& paths); }; diff --git a/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.triangulation.h b/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.triangulation.h new file mode 100644 index 00000000000..a33cb9fe6a4 --- /dev/null +++ b/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.triangulation.h @@ -0,0 +1,30 @@ +/******************************************************************************* +* Author : Angus Johnson * +* Date : 6 December 2025 * +* Release : BETA RELEASE * +* Website : https://www.angusj.com * +* Copyright : Angus Johnson 2010-2025 * +* Purpose : Delaunay Triangulation * +* License : https://www.boost.org/LICENSE_1_0.txt * +*******************************************************************************/ + +#ifndef CLIPPER_TRIANGULATION_H +#define CLIPPER_TRIANGULATION_H + +#include +#include "clipper2/clipper.core.h" + +#ifdef USINGZ +namespace Clipper2Lib_Z { +#else +namespace Clipper2Lib { +#endif + + enum class TriangulateResult { success, fail, no_polygons, paths_intersect }; + + // Triangulate - this function will not accept intesecting paths + TriangulateResult Triangulate(const Paths64& pp, Paths64& solution, bool useDelaunay = true); + TriangulateResult Triangulate(const PathsD& pp, int decPlaces, PathsD& solution, bool useDelaunay = true); + +} // Clipper2Lib namespace +#endif // CLIPPER_TRIANGULATION_H diff --git a/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.version.h b/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.version.h index 661b0f1c588..9aec09a2a88 100644 --- a/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.version.h +++ b/deps_src/clipper2/Clipper2Lib/include/clipper2/clipper.version.h @@ -1,6 +1,6 @@ #ifndef CLIPPER_VERSION_H #define CLIPPER_VERSION_H -constexpr auto CLIPPER2_VERSION = "1.5.2"; +constexpr auto CLIPPER2_VERSION = "2.0.1"; #endif // CLIPPER_VERSION_H diff --git a/deps_src/clipper2/Clipper2Lib/src/clipper.engine.cpp b/deps_src/clipper2/Clipper2Lib/src/clipper.engine.cpp index fd77bda7bb6..12e89ba7232 100644 --- a/deps_src/clipper2/Clipper2Lib/src/clipper.engine.cpp +++ b/deps_src/clipper2/Clipper2Lib/src/clipper.engine.cpp @@ -1,8 +1,8 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 17 September 2024 * +* Date : 21 February 2026 * * Website : https://www.angusj.com * -* Copyright : Angus Johnson 2010-2024 * +* Copyright : Angus Johnson 2010-2026 * * Purpose : This is the main polygon clipping module * * License : https://www.boost.org/LICENSE_1_0.txt * *******************************************************************************/ @@ -482,8 +482,7 @@ namespace Clipper2Lib { inline void SetOwner(OutRec* outrec, OutRec* new_owner) { //precondition1: new_owner is never null - while (new_owner->owner && !new_owner->owner->pts) - new_owner->owner = new_owner->owner->owner; + new_owner->owner = GetRealOutRec(new_owner->owner); OutRec* tmp = new_owner; while (tmp && tmp != outrec) tmp = tmp->owner; if (tmp) new_owner->owner = outrec->owner; @@ -536,9 +535,9 @@ namespace Clipper2Lib { val = 1 - val; // toggle val else { - double d = CrossProduct(op2->prev->pt, op2->pt, pt); - if (d == 0) return PointInPolygonResult::IsOn; - if ((d < 0) == is_above) val = 1 - val; + int i = CrossProductSign(op2->prev->pt, op2->pt, pt); + if (i == 0) return PointInPolygonResult::IsOn; + if ((i < 0) == is_above) val = 1 - val; } is_above = !is_above; op2 = op2->next; @@ -546,9 +545,9 @@ namespace Clipper2Lib { if (is_above != starting_above) { - double d = CrossProduct(op2->prev->pt, op2->pt, pt); - if (d == 0) return PointInPolygonResult::IsOn; - if ((d < 0) == is_above) val = 1 - val; + int i = CrossProductSign(op2->prev->pt, op2->pt, pt); + if (i == 0) return PointInPolygonResult::IsOn; + if ((i < 0) == is_above) val = 1 - val; } if (val == 0) return PointInPolygonResult::IsOutside; @@ -578,30 +577,31 @@ namespace Clipper2Lib { return result; } - inline bool Path1InsidePath2(OutPt* op1, OutPt* op2) + inline bool Path2ContainsPath1(OutPt* op1, OutPt* op2) { - // we need to make some accommodation for rounding errors - // so we won't jump if the first vertex is found outside - PointInPolygonResult result; - int outside_cnt = 0; + // this function accommodates rounding errors that + // can cause path micro intersections + PointInPolygonResult pip = PointInPolygonResult::IsOn; OutPt* op = op1; - do - { - result = PointInOpPolygon(op->pt, op2); - if (result == PointInPolygonResult::IsOutside) ++outside_cnt; - else if (result == PointInPolygonResult::IsInside) --outside_cnt; + do { + switch (PointInOpPolygon(op->pt, op2)) + { + case PointInPolygonResult::IsOutside: + if (pip == PointInPolygonResult::IsOutside) return false; + pip = PointInPolygonResult::IsOutside; + break; + case PointInPolygonResult::IsInside: + if (pip == PointInPolygonResult::IsInside) return true; + pip = PointInPolygonResult::IsInside; + break; + default: break; + } op = op->next; - } while (op != op1 && std::abs(outside_cnt) < 2); - if (std::abs(outside_cnt) > 1) return (outside_cnt < 0); - // since path1's location is still equivocal, check its midpoint - Point64 mp = GetBounds(GetCleanPath(op1)).MidPoint(); - Path64 path2 = GetCleanPath(op2); - return PointInPolygon(mp, path2) != PointInPolygonResult::IsOutside; + } while (op != op1); + // result unclear, so try again using cleaned paths + return Path2ContainsPath1(GetCleanPath(op1), GetCleanPath(op2)); // (#973) } - //------------------------------------------------------------------------------ - //------------------------------------------------------------------------------ - void AddLocMin(LocalMinimaList& list, Vertex& vert, PathType polytype, bool is_open) { @@ -621,7 +621,7 @@ namespace Clipper2Lib { {return a + path.size(); }); if (total_vertex_count == 0) return; - Vertex* vertices = new Vertex[total_vertex_count], * v = vertices; + Vertex* allVertices = new Vertex[total_vertex_count], * v = allVertices; for (const Path64& path : paths) { //for each path create a circular double linked list of vertices @@ -713,7 +713,7 @@ namespace Clipper2Lib { } } // end processing current path - vertexLists.emplace_back(vertices); + vertexLists.emplace_back(allVertices); } //------------------------------------------------------------------------------ @@ -1126,21 +1126,19 @@ namespace Clipper2Lib { return newcomer.curr_x > resident.curr_x; //get the turning direction a1.top, a2.bot, a2.top - double d = CrossProduct(resident.top, newcomer.bot, newcomer.top); - if (d != 0) return d < 0; + int i = CrossProductSign(resident.top, newcomer.bot, newcomer.top); + if (i != 0) return i < 0; //edges must be collinear to get here //for starting open paths, place them according to //the direction they're about to turn if (!IsMaxima(resident) && (resident.top.y > newcomer.top.y)) { - return CrossProduct(newcomer.bot, - resident.top, NextVertex(resident)->pt) <= 0; + return (CrossProductSign(newcomer.bot, resident.top, NextVertex(resident)->pt) <= 0); } else if (!IsMaxima(newcomer) && (newcomer.top.y > resident.top.y)) { - return CrossProduct(newcomer.bot, - newcomer.top, NextVertex(newcomer)->pt) >= 0; + return (CrossProductSign(newcomer.bot, newcomer.top, NextVertex(newcomer)->pt) >= 0); } int64_t y = newcomer.bot.y; @@ -1155,7 +1153,7 @@ namespace Clipper2Lib { resident.bot, resident.top)) return true; else //compare turning direction of the alternate bound - return (CrossProduct(PrevPrevVertex(resident)->pt, + return (CrossProductSign(PrevPrevVertex(resident)->pt, newcomer.bot, PrevPrevVertex(newcomer)->pt) > 0) == newcomerIsLeft; } @@ -1565,7 +1563,7 @@ namespace Clipper2Lib { FixSelfIntersects(outrec); } - void ClipperBase::DoSplitOp(OutRec* outrec, OutPt* splitOp) + void ClipperBase::DoSplitOp (OutRec* outrec, OutPt* splitOp) { // splitOp.prev -> splitOp && // splitOp.next -> splitOp.next.next are intersecting @@ -1574,7 +1572,7 @@ namespace Clipper2Lib { outrec->pts = prevOp; Point64 ip; - GetSegmentIntersectPt(prevOp->pt, splitOp->pt, + GetLineIntersectPt(prevOp->pt, splitOp->pt, splitOp->next->pt, nextNextOp->pt, ip); #ifdef USINGZ @@ -1630,7 +1628,7 @@ namespace Clipper2Lib { if (using_polytree_) { - if (Path1InsidePath2(prevOp, newOp)) + if (Path2ContainsPath1(prevOp, newOp)) { newOr->splits = new OutRecList(); newOr->splits->emplace_back(outrec); @@ -1652,10 +1650,10 @@ namespace Clipper2Lib { void ClipperBase::FixSelfIntersects(OutRec* outrec) { OutPt* op2 = outrec->pts; + if (op2->prev == op2->next->next) + return; // because triangles can't self-intersect for (; ; ) { - // triangles can't self-intersect - if (op2->prev == op2->next->next) break; if (SegmentsIntersect(op2->prev->pt, op2->pt, op2->next->pt, op2->next->next->pt)) { @@ -1664,6 +1662,8 @@ namespace Clipper2Lib { DoSplitOp(outrec, op2); if (!outrec->pts) break; op2 = outrec->pts; + if (op2->prev == op2->next->next) + break; // again, because triangles can't self-intersect continue; } else @@ -2112,10 +2112,9 @@ namespace Clipper2Lib { e->prev_in_sel = e->prev_in_ael; e->next_in_sel = e->next_in_ael; e->jump = e->next_in_sel; - if (e->join_with == JoinWith::Left) - e->curr_x = e->prev_in_ael->curr_x; // also avoids complications - else - e->curr_x = TopX(*e, top_y); + // it is safe to ignore 'joined' edges here because + // if necessary they will be split in IntersectEdges() + e->curr_x = TopX(*e, top_y); e = e->next_in_ael; } } @@ -2262,15 +2261,14 @@ namespace Clipper2Lib { void MoveSplits(OutRec* fromOr, OutRec* toOr) { - if (!fromOr->splits) return; if (!toOr->splits) toOr->splits = new OutRecList(); OutRecList::iterator orIter = fromOr->splits->begin(); for (; orIter != fromOr->splits->end(); ++orIter) - toOr->splits->emplace_back(*orIter); + if (toOr != *orIter) // #987 + toOr->splits->emplace_back(*orIter); fromOr->splits->clear(); } - void ClipperBase::ProcessHorzJoins() { for (const HorzJoin& j : horz_join_list_) @@ -2300,7 +2298,7 @@ namespace Clipper2Lib { if (using_polytree_) //#498, #520, #584, D#576, #618 { - if (Path1InsidePath2(or1->pts, or2->pts)) + if (Path2ContainsPath1(or1->pts, or2->pts)) { //swap or1's & or2's pts OutPt* tmp = or1->pts; @@ -2311,7 +2309,7 @@ namespace Clipper2Lib { //or2 is now inside or1 or2->owner = or1; } - else if (Path1InsidePath2(or2->pts, or1->pts)) + else if (Path2ContainsPath1(or2->pts, or1->pts)) { or2->owner = or1; } @@ -2324,13 +2322,14 @@ namespace Clipper2Lib { else or2->owner = or1; } - else + else // joining, not splitting { or2->pts = nullptr; if (using_polytree_) { SetOwner(or2, or1); - MoveSplits(or2, or1); //#618 + if (or2->splits) + MoveSplits(or2, or1); //#618 } else or2->owner = or1; @@ -2350,7 +2349,7 @@ namespace Clipper2Lib { void ClipperBase::AddNewIntersectNode(Active& e1, Active& e2, int64_t top_y) { Point64 ip; - if (!GetSegmentIntersectPt(e1.bot, e1.top, e2.bot, e2.top, ip)) + if (!GetLineIntersectPt(e1.bot, e1.top, e2.bot, e2.top, ip)) ip = Point64(e1.curr_x, top_y); //parallel edges //rounding errors can occasionally place the calculated intersection @@ -2934,22 +2933,28 @@ namespace Clipper2Lib { bool ClipperBase::CheckSplitOwner(OutRec* outrec, OutRecList* splits) { - for (auto split : *splits) + // nb: use indexing (not an iterator) in case 'splits' is modified inside this loop (#1029) + for (size_t idx = 0; idx < splits->size(); ++idx) { + OutRec* split = (*splits)[idx]; + if (!split->pts && split->splits && + CheckSplitOwner(outrec, split->splits)) return true; //#942 split = GetRealOutRec(split); - if(!split || split == outrec || split->recursive_split == outrec) continue; + if (!split || split == outrec || split->recursive_split == outrec) continue; split->recursive_split = outrec; // prevent infinite loops if (split->splits && CheckSplitOwner(outrec, split->splits)) - return true; - else if (CheckBounds(split) && - IsValidOwner(outrec, split) && - split->bounds.Contains(outrec->bounds) && - Path1InsidePath2(outrec->pts, split->pts)) - { - outrec->owner = split; //found in split return true; - } + + if (!CheckBounds(split) || !split->bounds.Contains(outrec->bounds) || + !Path2ContainsPath1(outrec->pts, split->pts)) continue; + + if (!IsValidOwner(outrec, split)) // split is owned by outrec! (#957) + split->owner = outrec->owner; + + outrec->owner = split; + return true; + } return false; } @@ -2960,13 +2965,12 @@ namespace Clipper2Lib { // post-condition: if a valid path, outrec will have a polypath if (outrec->polypath || outrec->bounds.IsEmpty()) return; - while (outrec->owner) { if (outrec->owner->splits && CheckSplitOwner(outrec, outrec->owner->splits)) break; if (outrec->owner->pts && CheckBounds(outrec->owner) && outrec->owner->bounds.Contains(outrec->bounds) && - Path1InsidePath2(outrec->pts, outrec->owner->pts)) break; + Path2ContainsPath1(outrec->pts, outrec->owner->pts)) break; outrec->owner = outrec->owner->owner; } @@ -3029,6 +3033,7 @@ namespace Clipper2Lib { { OutRec* outrec = outrec_list_[i]; if (!outrec || !outrec->pts) continue; + if (outrec->is_open) { Path64 path; diff --git a/deps_src/clipper2/Clipper2Lib/src/clipper.offset.cpp b/deps_src/clipper2/Clipper2Lib/src/clipper.offset.cpp index c00a345c01c..fe633ce7540 100644 --- a/deps_src/clipper2/Clipper2Lib/src/clipper.offset.cpp +++ b/deps_src/clipper2/Clipper2Lib/src/clipper.offset.cpp @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 22 January 2025 * +* Date : 11 October 2025 * * Website : https://www.angusj.com * * Copyright : Angus Johnson 2010-2025 * * Purpose : Path Offset (Inflate/Shrink) * @@ -37,22 +37,28 @@ const double arc_const = 0.002; // <-- 1/500 // Miscellaneous methods //------------------------------------------------------------------------------ -std::optional GetLowestClosedPathIdx(const Paths64& paths) +void GetLowestClosedPathInfo(const Paths64& paths, std::optional& idx, bool& is_neg_area) { - std::optional result; + idx.reset(); Point64 botPt = Point64(INT64_MAX, INT64_MIN); for (size_t i = 0; i < paths.size(); ++i) { + double a = MAX_DBL; for (const Point64& pt : paths[i]) { if ((pt.y < botPt.y) || ((pt.y == botPt.y) && (pt.x >= botPt.x))) continue; - result = i; + if (a == MAX_DBL) + { + a = Area(paths[i]); + if (a == 0) break; // invalid closed path, so break from inner loop + is_neg_area = a < 0; + } + idx = i; botPt.x = pt.x; botPt.y = pt.y; } } - return result; } inline double Hypot(double x, double y) @@ -145,15 +151,16 @@ ClipperOffset::Group::Group(const Paths64& _paths, JoinType _join_type, EndType if (end_type == EndType::Polygon) { - lowest_path_idx = GetLowestClosedPathIdx(paths_in); + bool is_neg_area; + GetLowestClosedPathInfo(paths_in, lowest_path_idx, is_neg_area); // the lowermost path must be an outer path, so if its orientation is negative, // then flag the whole group is 'reversed' (will negate delta etc.) // as this is much more efficient than reversing every path. - is_reversed = (lowest_path_idx.has_value()) && Area(paths_in[lowest_path_idx.value()]) < 0; + is_reversed = lowest_path_idx.has_value() && is_neg_area; } else { - lowest_path_idx = std::nullopt; + lowest_path_idx.reset(); is_reversed = false; } } @@ -236,7 +243,7 @@ void ClipperOffset::DoSquare(const Path64& path, size_t j, size_t k) { PointD pt4 = PointD(pt3.x + vec.x * group_delta_, pt3.y + vec.y * group_delta_); PointD pt = ptQ; - GetSegmentIntersectPt(pt1, pt2, pt3, pt4, pt); + GetLineIntersectPt(pt1, pt2, pt3, pt4, pt); //get the second intersect point through reflecion path_out.emplace_back(ReflectPoint(pt, ptQ)); path_out.emplace_back(pt); @@ -245,7 +252,7 @@ void ClipperOffset::DoSquare(const Path64& path, size_t j, size_t k) { PointD pt4 = GetPerpendicD(path[j], norms[k], group_delta_); PointD pt = ptQ; - GetSegmentIntersectPt(pt1, pt2, pt3, pt4, pt); + GetLineIntersectPt(pt1, pt2, pt3, pt4, pt); path_out.emplace_back(pt); //get the second intersect point through reflecion path_out.emplace_back(ReflectPoint(pt, ptQ)); @@ -601,10 +608,10 @@ void ClipperOffset::ExecuteInternal(double delta) if (!solution->size()) return; - bool paths_reversed = CheckReverseOrientation(); + bool paths_reversed = CheckReverseOrientation(); //clean up self-intersections ... Clipper64 c; - c.PreserveCollinear(false); + c.PreserveCollinear(preserve_collinear_); //the solution should retain the orientation of the input c.ReverseSolution(reverse_solution_ != paths_reversed); #ifdef USINGZ diff --git a/deps_src/clipper2/Clipper2Lib/src/clipper.rectclip.cpp b/deps_src/clipper2/Clipper2Lib/src/clipper.rectclip.cpp index a4d83ad137b..216ccbab190 100644 --- a/deps_src/clipper2/Clipper2Lib/src/clipper.rectclip.cpp +++ b/deps_src/clipper2/Clipper2Lib/src/clipper.rectclip.cpp @@ -1,8 +1,8 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 5 July 2024 * +* Date : 11 October 2025 * * Website : https://www.angusj.com * -* Copyright : Angus Johnson 2010-2024 * +* Copyright : Angus Johnson 2010-2025 * * Purpose : FAST rectangular clipping * * License : https://www.boost.org/LICENSE_1_0.txt * *******************************************************************************/ @@ -77,8 +77,8 @@ namespace Clipper2Lib { bool GetSegmentIntersection(const Point64& p1, const Point64& p2, const Point64& p3, const Point64& p4, Point64& ip) { - double res1 = CrossProduct(p1, p3, p4); - double res2 = CrossProduct(p2, p3, p4); + int res1 = CrossProductSign(p1, p3, p4); + int res2 = CrossProductSign(p2, p3, p4); if (res1 == 0) { ip = p1; @@ -97,8 +97,8 @@ namespace Clipper2Lib { } if ((res1 > 0) == (res2 > 0)) return false; - double res3 = CrossProduct(p3, p1, p2); - double res4 = CrossProduct(p4, p1, p2); + int res3 = CrossProductSign(p3, p1, p2); + int res4 = CrossProductSign(p4, p1, p2); if (res3 == 0) { ip = p3; @@ -116,7 +116,7 @@ namespace Clipper2Lib { if ((res3 > 0) == (res4 > 0)) return false; // segments must intersect to get here - return GetSegmentIntersectPt(p1, p2, p3, p4, ip); + return GetLineIntersectPt(p1, p2, p3, p4, ip); } inline bool GetIntersection(const Path64& rectPath, @@ -227,7 +227,7 @@ namespace Clipper2Lib { const Point64& prev_pt, const Point64& curr_pt, const Point64& rect_mp) { if (AreOpposites(prev, curr)) - return CrossProduct(prev_pt, rect_mp, curr_pt) < 0; + return CrossProductSign(prev_pt, rect_mp, curr_pt) < 0; else return HeadingClockwise(prev, curr); } diff --git a/deps_src/clipper2/Clipper2Lib/src/clipper.triangulation.cpp b/deps_src/clipper2/Clipper2Lib/src/clipper.triangulation.cpp new file mode 100644 index 00000000000..9ef21bae29f --- /dev/null +++ b/deps_src/clipper2/Clipper2Lib/src/clipper.triangulation.cpp @@ -0,0 +1,1224 @@ +/******************************************************************************* +* Author : Angus Johnson * +* Date : 21 February 2026 * +* Release : BETA RELEASE * +* Website : https://www.angusj.com * +* Copyright : Angus Johnson 2010-2026 * +* Purpose : Constrained Delaunay Triangulation * +* License : https://www.boost.org/LICENSE_1_0.txt * +*******************************************************************************/ + +#include "clipper2/clipper.h" +#include "clipper2/clipper.triangulation.h" + +#ifdef USINGZ +namespace Clipper2Lib_Z { +#else +namespace Clipper2Lib { +#endif + + enum class EdgeKind { loose, ascend, descend }; // ascend & descend are 'fixed' edges + enum class IntersectKind { none, collinear, intersect }; + enum class EdgeContainsResult { neither, left, right }; + + //forward definitions + class Vertex2; + class Edge; + class Triangle; + + typedef std::vector VertexList; + typedef std::vector EdgeList; + + class Vertex2 { + public: + Point64 pt; + EdgeList edges; + bool innerLM = false; + Vertex2(const Point64& p64) : pt(p64) { edges.reserve(2); }; + }; + + class Edge { + public: + Vertex2* vL = nullptr; + Vertex2* vR = nullptr; + Vertex2* vB = nullptr; + Vertex2* vT = nullptr; + EdgeKind kind = EdgeKind::loose; + Triangle* triA = nullptr; + Triangle* triB = nullptr; + bool isActive = false; + Edge* nextE = nullptr; + Edge* prevE = nullptr; + }; + + class Triangle { + public: + Edge* edges[3]; + Triangle(Edge* e1, Edge* e2, Edge* e3) + { + edges[0] = e1; + edges[1] = e2; + edges[2] = e3; + } + }; + + ///////////////////////////////////////////////////////////////////////////// + // Delaunay class declaration + ///////////////////////////////////////////////////////////////////////////// + + class Delaunay { + private: + VertexList allVertices; + EdgeList allEdges; + std::vector allTriangles; + std::stack pendingDelaunayStack; + std::stack horzEdgeStack; + std::stack locMinStack; + bool useDelaunay = true; + Vertex2* lowermostVertex = nullptr; + Edge* firstActive = nullptr; + void AddPath(const Path64& path); + bool AddPaths(const Paths64& paths); + void CleanUp(); + bool FixupEdgeIntersects(); + void MergeDupOrCollinearVertices(); + void SplitEdge(Edge* longE, Edge* shortE); + bool RemoveIntersection(Edge* e1, Edge* e2); + Edge* CreateEdge(Vertex2* v1, Vertex2* v2, EdgeKind k); + Triangle* CreateTriangle(Edge* e1, Edge* e2, Edge* e3); + Edge* CreateInnerLocMinLooseEdge(Vertex2* vAbove); + Edge* HorizontalBetween(Vertex2* v1, Vertex2* v2); + void DoTriangulateLeft(Edge* edge, Vertex2* pivot, int64_t minY); + void DoTriangulateRight(Edge* edge, Vertex2* pivot, int64_t minY); + void AddEdgeToActives(Edge* edge); + void RemoveEdgeFromActives(Edge* edge); + void ForceLegal(Edge* edge); + public: + explicit Delaunay(bool delaunay = true) : useDelaunay(delaunay) {}; + ~Delaunay() { CleanUp(); }; + Paths64 Execute(const Paths64& paths, TriangulateResult& triResult); + }; + + ///////////////////////////////////////////////////////////////////////////// + // Miscellaneous functions + ///////////////////////////////////////////////////////////////////////////// + + static bool VertexListSort(const Vertex2* a, const Vertex2* b) + { + return (a->pt.y == b->pt.y) ? (a->pt.x < b->pt.x) : (a->pt.y > b->pt.y); + } + + static bool EdgeListSort(const Edge* a, const Edge* b) + { + return (a->vL->pt.x < b->vL->pt.x); + } + + static bool IsLooseEdge(const Edge& e) + { + return e.kind == EdgeKind::loose; + } + + static bool IsLeftEdge(const Edge& e) + { + // left edge (bound) of a fill region + // ie fills on the **right** side of the edge + // precondition - e is never a 'loose' edge + return e.kind == EdgeKind::ascend; + } + + static bool IsRightEdge(const Edge& e) + { + // right edge (bound) of a fill region + // but still fills on the **right** side of the edge + // precondition - e is never a 'loose' edge + return e.kind == EdgeKind::descend; + } + + static bool IsHorizontal(const Edge& e) + { + return e.vB->pt.y == e.vT->pt.y; + } + + static bool LeftTurning(const Point64& p1, const Point64& p2, const Point64& p3) + { + return CrossProductSign(p1, p2, p3) < 0; + } + + static bool RightTurning(const Point64& p1, const Point64& p2, const Point64& p3) + { + return CrossProductSign(p1, p2, p3) > 0; + } + + static bool EdgeCompleted(Edge* edge) + { + if (!edge->triA) return false; + if (edge->triB) return true; + return edge->kind != EdgeKind::loose; + } + + static EdgeContainsResult EdgeContains(const Edge* edge, const Vertex2* v) + { + if (edge->vL == v) return EdgeContainsResult::left; + else if (edge->vR == v) return EdgeContainsResult::right; + else return EdgeContainsResult::neither; + } + + static double GetAngle(const Point64& a, const Point64& b, const Point64& c) + { + //https://stackoverflow.com/a/3487062/359538 + double abx = static_cast(b.x - a.x); + double aby = static_cast(b.y - a.y); + double bcx = static_cast(b.x - c.x); + double bcy = static_cast(b.y - c.y); + double dp = (abx * bcx + aby * bcy); + double cp = (abx * bcy - aby * bcx); + return std::atan2(cp, dp); //range between -Pi and Pi + } + + static double GetLocMinAngle(Vertex2* v) + { + // todo - recheck the result's sign compared to left vs right turning + // (currently assumes left turning => positive values) + // precondition - this function is called before processing locMin. + //Assert(v->edges.size() == 2); + int asc, des; + if (v->edges[0]->kind == EdgeKind::ascend) + { + asc = 0; + des = 1; + } + else + { + des = 0; + asc = 1; + } + // winding direction - descending to ascending + return GetAngle(v->edges[des]->vT->pt, v->pt, v->edges[asc]->vT->pt); + } + + static void RemoveEdgeFromVertex(Vertex2* vert, Edge* edge) + { + auto it = std::find(vert->edges.begin(), vert->edges.end(), edge); + if (it == vert->edges.end()) DoError(undefined_error_i); + vert->edges.erase(it); + } + + static bool FindLocMinIdx(const Path64& path, size_t len, size_t& idx) + { + if (len < 3) return false; + size_t i0 = idx, n = (idx + 1) % len; + while (path[n].y <= path[idx].y) + { + idx = n; n = (n + 1) % len; + if (idx == i0) return false; // fails if the path is completely horizontal + } + while (path[n].y >= path[idx].y) + { + idx = n; n = (n + 1) % len; + } + return true; + } + + static size_t Prev(size_t& idx, size_t len) + { + if (idx == 0) return len - 1; else return idx - 1; + } + + static size_t Next(size_t& idx, size_t len) + { + return (idx + 1) % len; + } + + static Edge* FindLinkingEdge(const Vertex2* vert1, const Vertex2* vert2, bool preferAscending) + { + Edge* res = nullptr; + for (auto e : vert1->edges) + { + if (e->vL == vert2 || e->vR == vert2) + { + if (e->kind == EdgeKind::loose || + ((e->kind == EdgeKind::ascend) == preferAscending)) return e; + res = e; + } + } + return res; + } + + static Path64 PathFromTriangle(Triangle tri) + { + Path64 res; + res.reserve(3); + res.push_back(tri.edges[0]->vL->pt); + res.push_back(tri.edges[0]->vR->pt); + const Edge& e = *tri.edges[1]; + if (e.vL->pt == res[0] || e.vL->pt == res[1]) + res.push_back(e.vR->pt); + else + res.push_back(e.vL->pt); + return res; + } + + static double InCircleTest(const Point64& ptA, const Point64& ptB, + const Point64& ptC, const Point64& ptD) + { + // Return the determinant value of 3 x 3 matrix ... + // | ax-dx ay-dy Sqr(ax-dx)+Sqr(ay-dy) | + // | bx-dx by-dy Sqr(bx-dx)+Sqr(by-dy) | + // | cx-dx cy-dy Sqr(cx-dx)+Sqr(cy-dy) | + + // The *sign* of the return value is determined by + // the orientation (CW vs CCW) of ptA, ptB & ptC. + + // precondition - ptA, ptB & ptC make a *non-empty* triangle + double m00 = static_cast(ptA.x - ptD.x); + double m01 = static_cast(ptA.y - ptD.y); + double m02 = (Sqr(m00) + Sqr(m01)); + double m10 = static_cast(ptB.x - ptD.x); + double m11 = static_cast(ptB.y - ptD.y); + double m12 = (Sqr(m10) + Sqr(m11)); + double m20 = static_cast(ptC.x - ptD.x); + double m21 = static_cast(ptC.y - ptD.y); + double m22 = (Sqr(m20) + Sqr(m21)); + return m00 * (m11 * m22 - m21 * m12) - + m10 * (m01 * m22 - m21 * m02) + + m20 * (m01 * m12 - m11 * m02); + } + + static double ShortestDistFromSegment(const Point64& pt, const Point64& segPt1, const Point64& segPt2) + { + // precondition: segPt1 <> segPt2 + double dx = static_cast(segPt2.x - segPt1.x); + double dy = static_cast(segPt2.y - segPt1.y); + //Assert((dx < > 0) or (dy < > 0)); // ie segPt1 <> segPt2 + + double ax = static_cast(pt.x - segPt1.x); + double ay = static_cast(pt.y - segPt1.y); + //q = (ax * dx + ay * dy) / (dx * dx + dy * dy); + double qNum = ax * dx + ay * dy; + if (qNum < 0) // pt is closest to seq1 + return DistanceSqr(pt, segPt1); + else if (qNum > (Sqr(dx) + Sqr(dy))) // pt is closest to seq2 + return DistanceSqr(pt, segPt2); + else + return Sqr(ax * dy - dx * ay) / (dx * dx + dy * dy); + } + + static IntersectKind SegsIntersect(const Point64 s1a, const Point64 s1b, + const Point64 s2a, const Point64 s2b) + { + //ignore segments sharing an end-point + if (s1a == s2a || s1b == s2a || s1b == s2b) return IntersectKind::none; + + double dy1 = static_cast(s1b.y - s1a.y); + double dx1 = static_cast(s1b.x - s1a.x); + double dy2 = static_cast(s2b.y - s2a.y); + double dx2 = static_cast(s2b.x - s2a.x); + double cp = dy1 * dx2 - dy2 * dx1; + if (cp == 0) return IntersectKind::collinear; + + double t = (static_cast(s1a.x - s2a.x) * dy2 - + static_cast(s1a.y - s2a.y) * dx2); + + // nb: testing for t == 0 is unreliable due to float imprecision + if (t >= 0) + { + if (cp < 0 || t >= cp) return IntersectKind::none; + } + else + { + if (cp > 0 || t <= cp) return IntersectKind::none; + } + + // so far, the *segment* 's1' intersects the *line* through 's2', + // but now make sure it also intersects the *segment* 's2' + t = ((s1a.x - s2a.x) * dy1 - (s1a.y - s2a.y) * dx1); + if (t >= 0) + { + if (cp > 0 && t < cp) return IntersectKind::intersect; + } + else + { + if (cp < 0 && t > cp) return IntersectKind::intersect; + } + return IntersectKind::none; + } + + ///////////////////////////////////////////////////////////////////////////// + // Delaunay class definitions + ///////////////////////////////////////////////////////////////////////////// + + void Delaunay::CleanUp() + { + for (auto v : allVertices) delete v; + allVertices.resize(0); + for (auto e : allEdges) delete e; + allEdges.resize(0); + for (auto t : allTriangles) delete t; + allTriangles.resize(0); + + firstActive = nullptr; + lowermostVertex = nullptr; + } + + void Delaunay::ForceLegal(Edge* edge) + { + // don't try to make empty triangles legal + if (!edge->triA || !edge->triB) return; + + // vertA will be assigned the vertex in edge's triangleA + // that is NOT an end vertex of edge + // Likewise, vertB will be assigned the vertex in edge's + // triangleB that is NOT an end vertex of edge + // If edge is rotated, vertA & vertB will become its end vertices. + Vertex2* vertA = nullptr; + Vertex2* vertB = nullptr; + + // Excluding 'edge', edgesA will contain two edges (one from + // triangleA and one from triangleB) that touch edge.vL. + // And edgesB will contain the two edges that touch edge.vR. + + Edge* edgesA[3] = { nullptr, nullptr, nullptr }; + Edge* edgesB[3] = { nullptr, nullptr, nullptr }; + for (int i = 0; i < 3; ++i) + { + if (edge->triA->edges[i] == edge) continue; + switch (EdgeContains(edge->triA->edges[i], edge->vL)) + { + case EdgeContainsResult::left: + edgesA[1] = edge->triA->edges[i]; + vertA = edge->triA->edges[i]->vR; + break; + case EdgeContainsResult::right: + edgesA[1] = edge->triA->edges[i]; + vertA = edge->triA->edges[i]->vL; + break; + default: + edgesB[1] = edge->triA->edges[i]; + } + } + + for (int i = 0; i < 3; ++i) + { + if (edge->triB->edges[i] == edge) continue; + switch (EdgeContains(edge->triB->edges[i], edge->vL)) + { + case EdgeContainsResult::left: + edgesA[2] = edge->triB->edges[i]; + vertB = edge->triB->edges[i]->vR; + break; + case EdgeContainsResult::right: + edgesA[2] = edge->triB->edges[i]; + vertB = edge->triB->edges[i]->vL; + break; + default: + edgesB[2] = edge->triB->edges[i]; + } + } + + // InCircleTest reqires edge.triangleA to be a valid triangle + // if IsEmptyTriangle(edge.triangleA) then Exit; // slower + if (CrossProductSign(vertA->pt, edge->vL->pt, edge->vR->pt) == 0) return; + + // ictResult - result sign is dependant on triangleA's orientation + double ictResult = InCircleTest(vertA->pt, edge->vL->pt, edge->vR->pt, vertB->pt); + if (ictResult == 0 || // if on or out of circle then exit + (RightTurning(vertA->pt, edge->vL->pt, edge->vR->pt) == (ictResult < 0))) return; + + // TRIANGLES HERE ARE **NOT** DELAUNAY COMPLIANT, SO MAKE THEM SO. + + // NOTE: ONCE WE BEGIN DELAUNAY COMPLIANCE, vL & vR WILL + // NO LONGER REPRESENT LEFT AND RIGHT VERTEX ORIENTATION. + // THIS IS MINOR PERFORMANCE EFFICIENCY IS SAFE AS LONG AS + // THE TRIANGULATE() METHOD IS CALLED ONCE ONLY ON A GIVEN + // SET OF PATHS + + edge->vL = vertA; + edge->vR = vertB; + + edge->triA->edges[0] = edge; + for (int i = 1; i < 3; ++i) + { + edge->triA->edges[i] = edgesA[i]; + if (!edgesA[i]) DoError(undefined_error_i); + if (IsLooseEdge(*edgesA[i])) + pendingDelaunayStack.push(edgesA[i]); + // since each edge has its own triangleA and triangleB, we have to be careful + // to update the correct one ... + if (edgesA[i]->triA == edge->triA || edgesA[i]->triB == edge->triA) continue; + + if (edgesA[i]->triA == edge->triB) + edgesA[i]->triA = edge->triA; + else if (edgesA[i]->triB == edge->triB) + edgesA[i]->triB = edge->triA; + else DoError(undefined_error_i); + } + + edge->triB->edges[0] = edge; + for (int i = 1; i < 3; ++i) + { + edge->triB->edges[i] = edgesB[i]; + if (!edgesB[i]) DoError(undefined_error_i); + if (IsLooseEdge(*edgesB[i])) + pendingDelaunayStack.push(edgesB[i]); + // since each edge has its own triangleA and triangleB, we have to be careful + // to update the correct one ... + if (edgesB[i]->triA == edge->triB || edgesB[i]->triB == edge->triB) continue; + + if (edgesB[i]->triA == edge->triA) + edgesB[i]->triA = edge->triB; + else if (edgesB[i]->triB == edge->triA) + edgesB[i]->triB = edge->triB; + else DoError(undefined_error_i); + } + + } + + Edge* Delaunay::CreateEdge(Vertex2* v1, Vertex2* v2, EdgeKind k) + { + Edge* res = allEdges.emplace_back(new Edge()); + if (v1->pt.y == v2->pt.y) + { + res->vB = v1; res->vT = v2; + } + else if (v1->pt.y < v2->pt.y) + { + res->vB = v2; res->vT = v1; + } + else + { + res->vB = v1; res->vT = v2; + } + + if (v1->pt.x <= v2->pt.x) + { + res->vL = v1; res->vR = v2; + } + else + { + res->vL = v2; res->vR = v1; + } + res->kind = k; + v1->edges.push_back(res); + v2->edges.push_back(res); + + if (k == EdgeKind::loose) + { + pendingDelaunayStack.push(res); + AddEdgeToActives(res); + } + return res; + } + + Triangle* Delaunay::CreateTriangle(Edge* e1, Edge* e2, Edge* e3) + { + Triangle* res = allTriangles.emplace_back(new Triangle(e1, e2, e3)); + // nb: only expire loose edges when both sides of these edges have triangles. + for (int i = 0; i < 3; ++i) + if (res->edges[i]->triA) + { + res->edges[i]->triB = res; + // this is the edge's second triangle hence no longer active + RemoveEdgeFromActives(res->edges[i]); + } + else + { + res->edges[i]->triA = res; + // this is the edge's first triangle, so only remove + // this edge from actives if it's a fixed edge. + if (!IsLooseEdge(*res->edges[i])) + RemoveEdgeFromActives(res->edges[i]); + } + return res; + } + + bool Delaunay::RemoveIntersection(Edge* e1, Edge* e2) + { + // find which vertex is closest to the other segment + // (ie not the vertex closest to the intersection point) ... + + Vertex2* v = e1->vL; + Edge* tmpE = e2; + double d = ShortestDistFromSegment(e1->vL->pt, e2->vL->pt, e2->vR->pt); + double d2 = ShortestDistFromSegment(e1->vR->pt, e2->vL->pt, e2->vR->pt); + if (d2 < d) { d = d2; v = e1->vR; } + d2 = ShortestDistFromSegment(e2->vL->pt, e1->vL->pt, e1->vR->pt); + if (d2 < d) { d = d2; tmpE = e1; v = e2->vL; } + d2 = ShortestDistFromSegment(e2->vR->pt, e1->vL->pt, e1->vR->pt); + if (d2 < d) { d = d2; tmpE = e1; v = e2->vR; } + if (d > 1.000) + return false; // Oops - this is not just a simple 'rounding' intersection + + // split 'tmpE' into 2 edges at 'v' + Vertex2* v2 = tmpE->vT; + RemoveEdgeFromVertex(v2, tmpE); + // replace v2 in tmpE with v + if (tmpE->vL == v2) tmpE->vL = v; + else tmpE->vR = v; + tmpE->vT = v; + v->edges.push_back(tmpE); + v->innerLM = false; // #47 + // left turning is angle positive + if (tmpE->vB->innerLM && GetLocMinAngle(tmpE->vB) <= 0) + tmpE->vB->innerLM = false; // #44, 52 + // finally create a new edge between v and v2 ... + CreateEdge(v, v2, tmpE->kind); + return true; + } + + bool Delaunay::FixupEdgeIntersects() + { + // precondition - edgeList must be sorted - ascending on edge.vL.pt.x + + for (size_t i1 = 0; i1 < allEdges.size(); ++i1) + { + Edge* e1 = allEdges[i1]; + // nb: we can safely ignore edges newly created inside this for loop + for (size_t i2 = i1 + 1; i2 < allEdges.size(); ++i2) + { + Edge* e2 = allEdges[i2]; + if (e2->vL->pt.x >= e1->vR->pt.x) + break; // all 'e' from now on are too far right + + // 'e2' is inside e1's horizontal region. If 'e2' is also inside + // e1's vertical region, only then check for an intersection ... + if (e2->vT->pt.y < e1->vB->pt.y && e2->vB->pt.y > e1->vT->pt.y && + (SegsIntersect(e2->vL->pt, e2->vR->pt, + e1->vL->pt, e1->vR->pt) == IntersectKind::intersect)) + { + if (!RemoveIntersection(e2, e1)) + return false; // oops!! + } + // nb: collinear edges are managed in MergeDupOrCollinearVertices below + } + } + return true; + } + + void Delaunay::SplitEdge(Edge* longE, Edge* shortE) + { + auto oldT = longE->vT, newT = shortE->vT; + // remove longEdge from longEdge.vT.edges + RemoveEdgeFromVertex(oldT, longE); + // shorten longEdge + longE->vT = newT; + if (longE->vL == oldT) longE->vL = newT; + else longE->vR = newT; + // add shortened longEdge to newT.edges + newT->edges.push_back(longE); + // and create a new edge betweem newV, oldT + CreateEdge(newT, oldT, longE->kind); + } + + void Delaunay::MergeDupOrCollinearVertices() + { + // note: this procedure may add new edges and change the + // number of edges connected with a given vertex, but it + // won't add or delete vertices (so it's safe to use iterators) + auto vIter1 = allVertices.begin(); + for (auto vIter2 = allVertices.begin() + 1; vIter2 != allVertices.end(); ++vIter2) + { + if ((*vIter1)->pt != (*vIter2)->pt) + { + vIter1 = vIter2; + continue; + } + + // merge v1 & v2 + Vertex2* v1 = *vIter1, * v2 = *vIter2; + if (!v1->innerLM || !v2->innerLM) + v1->innerLM = false; + + // in all of v2's edges, replace links to v2 with links to v1 + for (auto e : v2->edges) + { + if (e->vB == v2) e->vB = v1; + else e->vT = v1; + if (e->vL == v2) e->vL = v1; + else e->vR = v1; + } + // move all of v2's edges to v1 + std::copy(v2->edges.begin(), v2->edges.end(), back_inserter(v1->edges)); + v2->edges.resize(0); + + // excluding horizontals, if pv.edges contains two edges + // that are *collinear* and share the same bottom coords + // but have different lengths, split the longer edge at + // the top of the shorter edge ... + for (auto itE = v1->edges.begin(); itE != v1->edges.end(); ++itE) + { + if (IsHorizontal(*(*itE)) || (*itE)->vB != v1) continue; + for (auto itE2 = itE + 1; itE2 != v1->edges.end(); ++itE2) + { + auto e1 = *itE, e2 = *itE2; + if (e2->vB != v1 || e1->vT->pt.y == e2->vT->pt.y || + (CrossProductSign(e1->vT->pt, v1->pt, e2->vT->pt) != 0)) continue; + // we have parallel edges, both heading up from v1.pt. + // split the longer edge at the top of the shorter edge. + if (e1->vT->pt.y < e2->vT->pt.y) SplitEdge(e1, e2); + else SplitEdge(e2, e1); + break; // because only two edges can be collinear + } + } + } + } + + Edge* Delaunay::CreateInnerLocMinLooseEdge(Vertex2* vAbove) + { + if (!firstActive) return nullptr; // oops!! + + int64_t xAbove = vAbove->pt.x; + int64_t yAbove = vAbove->pt.y; + + // find the closest 'active' edge with a vertex that's not above vAbove + Edge* e = firstActive, *eBelow = nullptr; + double bestD = -1.0; + while (e) + { + if (e->vL->pt.x <= xAbove && e->vR->pt.x >= xAbove && + e->vB->pt.y >= yAbove && e->vB != vAbove && e->vT != vAbove && + !LeftTurning(e->vL->pt, vAbove->pt, e->vR->pt)) + { + double d = ShortestDistFromSegment(vAbove->pt, e->vL->pt, e->vR->pt); + if (!eBelow || d < bestD) // compare e with eBelow + { + eBelow = e; + bestD = d; + } + } + e = e->nextE; + } + if (!eBelow) return nullptr; // oops!! + + // get the best vertex from 'eBelow' + Vertex2* vBest = (eBelow->vT->pt.y <= yAbove) ? eBelow->vB : eBelow->vT; + int64_t xBest = vBest->pt.x; + int64_t yBest = vBest->pt.y; + + // make sure no edges intersect 'vAbove' and 'vBest' ... + // todo: fActives is currently *unsorted* but consider making it + // a tree structure based on each edge's left and right bounds + e = firstActive; + if (xBest < xAbove) + { + while (e) + { + if (e->vR->pt.x > xBest && e->vL->pt.x < xAbove && + e->vB->pt.y > yAbove && e->vT->pt.y < yBest && + (SegsIntersect(e->vB->pt, e->vT->pt, + vBest->pt, vAbove->pt) == IntersectKind::intersect)) + { + vBest = (e->vT->pt.y > yAbove) ? e->vT : e->vB; + xBest = vBest->pt.x; + yBest = vBest->pt.y; + } + e = e->nextE; + } + } + else + { + while (e) + { + if (e->vR->pt.x < xBest && e->vL->pt.x > xAbove && + e->vB->pt.y > yAbove && e->vT->pt.y < yBest && + (SegsIntersect(e->vB->pt, e->vT->pt, + vBest->pt, vAbove->pt) == IntersectKind::intersect)) + { + vBest = e->vT->pt.y > yAbove ? e->vT : e->vB; + xBest = vBest->pt.x; + yBest = vBest->pt.y; + } + e = e->nextE; + } + } + return CreateEdge(vBest, vAbove, EdgeKind::loose); + } + + Edge* Delaunay::HorizontalBetween(Vertex2* v1, Vertex2* v2) + { + int64_t y = v1->pt.y, l, r; + if (v1->pt.x > v2->pt.x) + { + l = v2->pt.x; + r = v1->pt.x; + } + else + { + l = v1->pt.x; + r = v2->pt.x; + } + + Edge* res = firstActive; + while (res) + { + if (res->vL->pt.y == y && res->vR->pt.y == y && + res->vL->pt.x >= l && res->vR->pt.x <= r && + (res->vL->pt.x != l || res->vL->pt.x != r)) break; + res = res->nextE; + } + return res; + } + + void Delaunay::DoTriangulateLeft(Edge* edge, Vertex2* pivot, int64_t minY) + { + // precondition - pivot must be one end of edge (Usually .vB) + //Assert(!EdgeCompleted(edge)); + Vertex2* vAlt = nullptr; + Edge* eAlt = nullptr; + Vertex2* v = (edge->vB == pivot) ? edge->vT : edge->vB; + + for (auto e : pivot->edges) + { + if (e == edge || !e->isActive) continue; + Vertex2* vX = e->vT == pivot ? e->vB : e->vT; + if (vX == v) continue; + + int cps = CrossProductSign(v->pt, pivot->pt, vX->pt); + if (cps == 0) //collinear paths + { + // if pivot is between v and vX then continue; + // nb: this is important for both horiz and non-horiz collinear + if ((v->pt.x > pivot->pt.x) == (pivot->pt.x > vX->pt.x)) continue; + } + // else if right-turning or not the best edge, then continue + else if (cps > 0 || (vAlt && !LeftTurning(vX->pt, pivot->pt, vAlt->pt))) + continue; + vAlt = vX; + eAlt = e; + } + + if (!vAlt || vAlt->pt.y < minY) return; + + // Don't triangulate **across** fixed edges + if (vAlt->pt.y < pivot->pt.y) + { + if (IsLeftEdge(*eAlt)) return; + } + else if (vAlt->pt.y > pivot->pt.y) + { + if (IsRightEdge(*eAlt)) return; + } + + Edge* eX = FindLinkingEdge(vAlt, v, (vAlt->pt.y < v->pt.y)); + if (!eX) + { + // be very careful creating loose horizontals at minY + if (vAlt->pt.y == v->pt.y && v->pt.y == minY && + HorizontalBetween(vAlt, v)) return; + eX = CreateEdge(vAlt, v, EdgeKind::loose); + } + + CreateTriangle(edge, eAlt, eX); + if (!EdgeCompleted(eX)) + DoTriangulateLeft(eX, vAlt, minY); + + } + + void Delaunay::DoTriangulateRight(Edge* edge, Vertex2* pivot, int64_t minY) + { + // precondition - pivot must be one end of edge (Usually .vB) + //Assert(!EdgeCompleted(edge)); + Vertex2* vAlt = nullptr; + Edge* eAlt = nullptr; + Vertex2* v = (edge->vB == pivot) ? edge->vT : edge->vB; + + for (auto e : pivot->edges) + { + if (e == edge || !e->isActive) continue; + Vertex2* vX = e->vT == pivot ? e->vB : e->vT; + if (vX == v) continue; + + int cps = CrossProductSign(v->pt, pivot->pt, vX->pt); + if (cps == 0) //collinear paths + { + // if pivot is between v and vX then continue; + // nb: this is important for both horiz and non-horiz collinear + if ((v->pt.x > pivot->pt.x) == (pivot->pt.x > vX->pt.x)) continue; + } + // else if right-turning or not the best edge, then continue + else if (cps < 0 || (vAlt && !RightTurning(vX->pt, pivot->pt, vAlt->pt))) + continue; + vAlt = vX; + eAlt = e; + } + + if (!vAlt || vAlt->pt.y < minY) return; + + // Don't triangulate **across** fixed edges + if (vAlt->pt.y < pivot->pt.y) + { + if (IsRightEdge(*eAlt)) return; + } + else if (vAlt->pt.y > pivot->pt.y) + { + if (IsLeftEdge(*eAlt)) return; + } + + Edge* eX = FindLinkingEdge(vAlt, v, (vAlt->pt.y > v->pt.y)); + if (!eX) + { + // be very careful creating loose horizontals at minY + if (vAlt->pt.y == v->pt.y && v->pt.y == minY && + HorizontalBetween(vAlt, v)) return; + eX = CreateEdge(vAlt, v, EdgeKind::loose); + } + + CreateTriangle(edge, eX, eAlt); + if (!EdgeCompleted(eX)) + DoTriangulateRight(eX, vAlt, minY); + + + } + + void Delaunay::AddEdgeToActives(Edge* edge) + { + // nb: on occassions this method can get called twice for a given edge + // This is because, in the Triangulate() method where vertex 'edges' + // arrays are being parsed, edges can can be removed from the array + // which changes the index of following edges. + if (edge->isActive) return; + + edge->prevE = nullptr; + edge->nextE = firstActive; + edge->isActive = true; + if (firstActive) + firstActive->prevE = edge; + firstActive = edge; + } + + + void Delaunay::RemoveEdgeFromActives(Edge* edge) + { + // first, remove the edge from its vertices + RemoveEdgeFromVertex(edge->vB, edge); + RemoveEdgeFromVertex(edge->vT, edge); + + // now remove the edge from double linked list (AEL) + Edge* prev = edge->prevE; + Edge* next = edge->nextE; + if (next) next->prevE = prev; + if (prev) prev->nextE = next; + edge->isActive = false; + if (firstActive == edge) firstActive = next; + } + + Paths64 Delaunay::Execute(const Paths64& paths, TriangulateResult& triResult) + { + if (!AddPaths(paths)) + { + triResult = TriangulateResult::no_polygons; + return Paths64(); // oops! + } + + // if necessary fix path orientation because the algorithm + // expects clockwise outer paths and counter-clockwise inner paths + if (lowermostVertex->innerLM) + { + // the orientation of added paths must be wrong, so + // 1. reverse innerLM flags ... + Vertex2* lm; + while (!locMinStack.empty()) + { + lm = locMinStack.top(); + lm->innerLM = !lm->innerLM; + locMinStack.pop(); + } + // 2. swap edge kinds + for (Edge* e : allEdges) + if (e->kind == EdgeKind::ascend) + e->kind = EdgeKind::descend; + else + e->kind = EdgeKind::ascend; + } + else + { + // path orientation is fine so ... + while (!locMinStack.empty()) + locMinStack.pop(); + } + + std::sort(allEdges.begin(), allEdges.end(), EdgeListSort); + if (!FixupEdgeIntersects()) + { + CleanUp(); + triResult = TriangulateResult::paths_intersect; + return Paths64(); // oops! + } + + std::sort(allVertices.begin(), allVertices.end(), VertexListSort); + MergeDupOrCollinearVertices(); + + int64_t currY = allVertices[0]->pt.y; + for (auto vIt = allVertices.begin(); vIt != allVertices.end(); ++vIt) + { + Vertex2* v = *vIt; + if (v->edges.empty()) continue; + if (v->pt.y != currY) + { + // JOIN AN INNER LOCMIN WITH A SUITABLE EDGE BELOW + while (!locMinStack.empty()) + { + Vertex2* lm = locMinStack.top(); + locMinStack.pop(); + Edge* e = CreateInnerLocMinLooseEdge(lm); + if (!e) + { + CleanUp(); + triResult = TriangulateResult::fail; + return Paths64(); // oops! + } + + if (IsHorizontal(*e)) + { + if (e->vL == e->vB) + DoTriangulateLeft(e, e->vB, currY); else + DoTriangulateRight(e, e->vB, currY); + } + else + { + DoTriangulateLeft(e, e->vB, currY); + if (!EdgeCompleted(e)) + DoTriangulateRight(e, e->vB, currY); + } + + // and because adding locMin edges to Actives was delayed .. + AddEdgeToActives(lm->edges[0]); + AddEdgeToActives(lm->edges[1]); + } + + while (!horzEdgeStack.empty()) + { + Edge* e = horzEdgeStack.top(); + horzEdgeStack.pop(); + if (EdgeCompleted(e)) continue; + if (e->vB == e->vL) // #45 + { + if (IsLeftEdge(*e)) + DoTriangulateLeft(e, e->vB, currY); + } + else + if (IsRightEdge(*e)) + DoTriangulateRight(e, e->vB, currY); + } + currY = v->pt.y; + } + + for (int i = static_cast(v->edges.size()) - 1; i >= 0; --i) + { + // the following line may look superfluous, but within this loop + // v->edges may be altered with additions and or deletions. + // So this line *is* necessary (and why we can't use an iterator). + // Also, we need to use a *descending* index which is safe because + // any additions will be loose edges which are ignored here. + if (i >= static_cast(v->edges.size())) continue; + + Edge* e = v->edges[i]; + if (EdgeCompleted(e) || IsLooseEdge(*e)) continue; + + if (v == e->vB) + { + if (IsHorizontal(*e)) + horzEdgeStack.push(e); + // delay adding locMin edges to actives + if (!v->innerLM) + AddEdgeToActives(e); + } + else + { + if (IsHorizontal(*e)) + horzEdgeStack.push(e); + else if (IsLeftEdge(*e)) + DoTriangulateLeft(e, e->vB, v->pt.y); + else + DoTriangulateRight(e, e->vB, v->pt.y); + } + } // for v->edges loop + + if (v->innerLM) locMinStack.push(v); + + } // for allVertices loop + + + while (!horzEdgeStack.empty()) + { + Edge* e = horzEdgeStack.top(); + horzEdgeStack.pop(); + if (!EdgeCompleted(e) && e->vB == e->vL) + DoTriangulateLeft(e, e->vB, currY); + } + + if (useDelaunay) + { + // Convert triangles to Delaunay conforming + while (!pendingDelaunayStack.empty()) + { + Edge* e = pendingDelaunayStack.top(); + pendingDelaunayStack.pop(); + ForceLegal(e); + } + } + + Paths64 res; + res.reserve(allTriangles.size()); + for (auto tri : allTriangles) + { + Path64 p = PathFromTriangle(*tri); + int cps = CrossProductSign(p[0], p[1], p[2]); + if (cps == 0) continue; // skip any empty triangles + if (cps < 0) // ccw + std::reverse(p.begin(), p.end()); + res.push_back(p); + } + + CleanUp(); + triResult = TriangulateResult::success; + return res; + } + + static double DistSqr(const Point64& pt1, const Point64& pt2) + { + return Sqr(pt1.x - pt2.x) + Sqr(pt1.y - pt2.y); + } + + void Delaunay::AddPath(const Path64& path) + { + size_t len = path.size(), i0 = 0, iPrev, iNext; + // find the first locMin for the current path + if (!FindLocMinIdx(path, len, i0)) return; + iPrev = Prev(i0, len); + while (path[iPrev] == path[i0]) iPrev = Prev(iPrev, len); + iNext = Next(i0, len); + + // it is possible for a locMin here to simply be a + // collinear spike that should be ignored, so ... + size_t i = i0; + while (CrossProductSign(path[iPrev], path[i], path[iNext]) == 0) + { + FindLocMinIdx(path, len, i); + if (i == i0) return; // this is an entirely collinear path + iPrev = Prev(i, len); + while (path[iPrev] == path[i]) iPrev = Prev(iPrev, len); + iNext = Next(i, len); + } + + size_t vert_cnt = allVertices.size(); + + // we are now at the first legitimate locMin + Vertex2* v0 = allVertices.emplace_back(new Vertex2(path[i])); + + if (LeftTurning(path[iPrev], path[i], path[iNext])) + v0->innerLM = true; + Vertex2* vPrev = v0; + i = iNext; + + for (;;) + { + // vPrev is a locMin here + locMinStack.push(vPrev); + // ? update lowermostVertex ... + if (!lowermostVertex || + vPrev->pt.y > lowermostVertex->pt.y || + (vPrev->pt.y == lowermostVertex->pt.y && + vPrev->pt.x < lowermostVertex->pt.x)) + lowermostVertex = vPrev; + + iNext = Next(i, len); + if (CrossProductSign(vPrev->pt, path[i], path[iNext]) == 0) + { + i = iNext; + continue; + } + + // ascend up next bound to LocMax + while (path[i].y <= vPrev->pt.y) + { + Vertex2* v = allVertices.emplace_back(new Vertex2(path[i])); + CreateEdge(vPrev, v, EdgeKind::ascend); + vPrev = v; + i = iNext; + iNext = Next(i, len); + + while (CrossProductSign(vPrev->pt, path[i], path[iNext]) == 0) + { + i = iNext; + iNext = Next(i, len); + } + } + + // Now at a locMax, so descend to next locMin + Vertex2* vPrevPrev = vPrev; + while (i != i0 && path[i].y >= vPrev->pt.y) + { + Vertex2* v = allVertices.emplace_back(new Vertex2(path[i])); + CreateEdge(v, vPrev, EdgeKind::descend); + vPrevPrev = vPrev; + vPrev = v; + i = iNext; + iNext = Next(i, len); + + while (CrossProductSign(vPrev->pt, path[i], path[iNext]) == 0) + { + i = iNext; + iNext = Next(i, len); + } + } + + // now at the next locMin + if (i == i0) break; // break for(;;) loop + if (LeftTurning(vPrevPrev->pt, vPrev->pt, path[i])) + vPrev->innerLM = true; + } + CreateEdge(v0, vPrev, EdgeKind::descend); + + // finally, ignore this path if is not a polygon or too small + len = allVertices.size() - vert_cnt; + i = vert_cnt; + if (len < 3 || (len == 3 && // or just a very tiny triangle + ((DistSqr(allVertices[i]->pt, allVertices[i + 1]->pt) <= 1) || + (DistSqr(allVertices[i + 1]->pt, allVertices[i + 2]->pt) <= 1) || + (DistSqr(allVertices[i + 2]->pt, allVertices[i]->pt) <= 1)))) + { + for (size_t j = vert_cnt; j < allVertices.size(); ++j) + allVertices[j]->edges.clear(); // flag to ignore + } + } + + bool Delaunay::AddPaths(const Paths64& paths) + { + const auto total_vertex_count = + std::accumulate(paths.begin(), paths.end(), size_t(0), + [](const auto& a, const Path64& path) + {return a + path.size(); }); + if (total_vertex_count == 0) return false; + allVertices.reserve(allVertices.capacity() + total_vertex_count); + allEdges.reserve(allEdges.capacity() + total_vertex_count); + + for (const Path64& path : paths) + AddPath(path); + return (allVertices.size() > 2); + } + + TriangulateResult Triangulate(const Paths64& pp, Paths64& solution, bool useDelaunay) + { + TriangulateResult result; + Delaunay d(useDelaunay); + solution = d.Execute(pp, result); + return result; + } + + TriangulateResult Triangulate(const PathsD& pp, int decPlaces, PathsD& solution, bool useDelaunay) + { + int ec = 0; + double scale; + TriangulateResult result; + if (decPlaces <= 0) scale = 1; + else if (decPlaces > 8) scale = std::pow(10, 8); + else scale = std::pow(10, decPlaces); + Paths64 pp64 = ScalePaths(pp, scale, ec); + + Delaunay d(useDelaunay); + Paths64 sol64 = d.Execute(pp64, result); + solution = ScalePaths(sol64, 1 / scale, ec); + return result; + } + +} // Clipper2Lib namespace diff --git a/deps_src/clipper2/Clipper2Lib/src/clipper2_z.cpp b/deps_src/clipper2/Clipper2Lib/src/clipper2_z.cpp index f5c7c6f4fd8..385bd374636 100644 --- a/deps_src/clipper2/Clipper2Lib/src/clipper2_z.cpp +++ b/deps_src/clipper2/Clipper2Lib/src/clipper2_z.cpp @@ -6,3 +6,4 @@ #include "clipper.engine.cpp" #include "clipper.offset.cpp" #include "clipper.rectclip.cpp" +#include "clipper.triangulation.cpp"