Browse Source

clipper2: Update to 1.5.3

Jakub Marcowski 1 month ago
parent
commit
256fa39e51

+ 1 - 2
thirdparty/README.md

@@ -120,7 +120,7 @@ Files extracted from upstream source:
 ## clipper2
 
 - Upstream: https://github.com/AngusJohnson/Clipper2
-- Version: 1.5.2 (6901921c4be75126d1de60bfd24bd86a61319fd0, 2025)
+- Version: 1.5.3 (fa165fe8364b7d0e5d5db2182369b8c82348f4ea, 2025)
 - License: BSL 1.0
 
 Files extracted from upstream source:
@@ -131,7 +131,6 @@ Files extracted from upstream source:
 Patches:
 
 - `0001-disable-exceptions.patch` (GH-80796)
-- `0002-llvm-disable-int128-math.patch` (GH-95964)
 
 
 ## cvtt

+ 77 - 22
thirdparty/clipper2/include/clipper2/clipper.core.h

@@ -1,8 +1,8 @@
 /*******************************************************************************
 * Author    :  Angus Johnson                                                   *
-* Date      :  12 May 2024                                                     *
+* Date      :  24 March 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                           *
 *******************************************************************************/
@@ -250,6 +250,20 @@ namespace Clipper2Lib
   template <typename T>
   using Paths = std::vector<Path<T>>;
 
+  template <typename T, typename T2=T>
+  Path<T>& operator<<(Path<T>& poly, const Point<T2>& p)
+  {
+    poly.emplace_back(p);
+    return poly;
+  }
+
+  template <typename T>
+  Paths<T>& operator<<(Paths<T>& polys, const Path<T>& p)
+  {
+    polys.emplace_back(p);
+    return polys;
+  }
+
   using Path64 = Path<int64_t>;
   using PathD = Path<double>;
   using Paths64 = std::vector< Path64>;
@@ -687,56 +701,97 @@ 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 Multiply(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);
+    const uint64_t lobits = lo(x3) << 32 | lo(x1);
+    const uint64_t hibits = hi(a) * hi(b) + hi(x2) + hi(x3);
 
-    return { result, carry };
+    return { lobits, hibits };
   }
 
   // returns true if (and only if) a * b == c * d
   inline bool ProductsAreEqual(int64_t a, int64_t b, int64_t c, int64_t d)
   {
-// Work around LLVM issue: https://github.com/llvm/llvm-project/issues/16778
-// Details: https://github.com/godotengine/godot/pull/95964#issuecomment-2306581804
-// #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);
-//     return ab == cd;
-// #else
+#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);
+    return ab == cd;
+#else
     // nb: unsigned values needed for calculating overflow carry
     const auto abs_a = static_cast<uint64_t>(std::abs(a));
     const auto abs_b = static_cast<uint64_t>(std::abs(b));
     const auto abs_c = static_cast<uint64_t>(std::abs(c));
     const auto abs_d = static_cast<uint64_t>(std::abs(d));
 
-    const auto abs_ab = Multiply(abs_a, abs_b);
-    const auto abs_cd = Multiply(abs_c, abs_d);
+    const auto ab = Multiply(abs_a, abs_b);
+    const auto cd = Multiply(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;
-// #endif
+    return ab == cd && sign_ab == sign_cd;
+#endif
+  }
+
+  template <typename T>
+  inline int CrossProductSign(const Point<T>& pt1, const Point<T>& pt2, const Point<T>& 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
+    // nb: unsigned values needed for calculating carry into 'hi'
+    const auto abs_a = static_cast<uint64_t>(std::abs(a));
+    const auto abs_b = static_cast<uint64_t>(std::abs(b));
+    const auto abs_c = static_cast<uint64_t>(std::abs(c));
+    const auto abs_d = static_cast<uint64_t>(std::abs(d));
+
+    const auto ab = Multiply(abs_a, abs_b);
+    const auto cd = Multiply(abs_c, 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
   }
 
   template <typename T>

+ 61 - 32
thirdparty/clipper2/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                           *
 *******************************************************************************/
@@ -150,7 +150,7 @@ namespace Clipper2Lib {
     if (!delta) 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<int64_t,double>(paths, scale, error_code), jt, et);
     if (error_code) return PathsD();
     Paths64 solution;
@@ -351,6 +351,29 @@ namespace Clipper2Lib {
 #endif
     }
 
+    inline size_t GetNext(size_t current, size_t high,
+      const std::vector<bool>& 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<bool>& 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)
@@ -611,29 +634,6 @@ namespace Clipper2Lib {
     return result;
   }
 
-  inline size_t GetNext(size_t current, size_t high,
-    const std::vector<bool>& 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<bool>& 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 <typename T>
   inline Path<T> SimplifyPath(const Path<T> &path,
     double epsilon, bool isClosedPath = true)
@@ -665,13 +665,13 @@ namespace Clipper2Lib {
         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'
@@ -680,14 +680,14 @@ namespace Clipper2Lib {
         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]);
@@ -712,6 +712,35 @@ namespace Clipper2Lib {
     return result;
   }
 
+ 
+  template <typename T>
+  inline bool Path2ContainsPath1(const Path<T>& path1, const Path<T>& path2)
+  {
+    // precondition: paths must not intersect, except for
+    // transient (and presumed 'micro') path intersections 
+    PointInPolygonResult pip = PointInPolygonResult::IsOn;
+    for (const Point<T>& 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<T> mp1 = GetBounds(path1).MidPoint();
+    return PointInPolygon(mp1, path2) == PointInPolygonResult::IsInside;
+  }
+
   template <typename T>
   inline void RDP(const Path<T> path, std::size_t begin,
     std::size_t end, double epsSqrd, std::vector<bool>& flags)

+ 1 - 1
thirdparty/clipper2/include/clipper2/clipper.offset.h

@@ -35,7 +35,7 @@ private:
 	class Group {
 	public:
 		Paths64 paths_in;
-        std::optional<size_t> lowest_path_idx{};
+    std::optional<size_t> lowest_path_idx{};
 		bool is_reversed = false;
 		JoinType join_type;
 		EndType end_type;

+ 1 - 1
thirdparty/clipper2/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 = "1.5.3";
 
 #endif  // CLIPPER_VERSION_H

+ 0 - 32
thirdparty/clipper2/patches/0002-llvm-disable-int128-math.patch

@@ -1,32 +0,0 @@
-diff --git a/thirdparty/clipper2/include/clipper2/clipper.core.h b/thirdparty/clipper2/include/clipper2/clipper.core.h
-index 110bee4c10..aa003bf032 100644
---- a/thirdparty/clipper2/include/clipper2/clipper.core.h
-+++ b/thirdparty/clipper2/include/clipper2/clipper.core.h
-@@ -715,11 +715,13 @@ namespace Clipper2Lib
-   // returns true if (and only if) a * b == c * d
-   inline bool ProductsAreEqual(int64_t a, int64_t b, int64_t c, int64_t d)
-   {
--#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);
--    return ab == cd;
--#else
-+// Work around LLVM issue: https://github.com/llvm/llvm-project/issues/16778
-+// Details: https://github.com/godotengine/godot/pull/95964#issuecomment-2306581804
-+// #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);
-+//     return ab == cd;
-+// #else
-     // nb: unsigned values needed for calculating overflow carry
-     const auto abs_a = static_cast<uint64_t>(std::abs(a));
-     const auto abs_b = static_cast<uint64_t>(std::abs(b));
-@@ -734,7 +736,7 @@ namespace Clipper2Lib
-     const auto sign_cd = TriSign(c) * TriSign(d);
- 
-     return abs_ab == abs_cd && sign_ab == sign_cd;
--#endif
-+// #endif
-   }
- 
-   template <typename T>

+ 79 - 64
thirdparty/clipper2/src/clipper.engine.cpp

@@ -1,8 +1,8 @@
 /*******************************************************************************
 * Author    :  Angus Johnson                                                   *
-* Date      :  17 September 2024                                               *
+* Date      :  4 May 2025                                                      *
 * Website   :  https://www.angusj.com                                          *
-* Copyright :  Angus Johnson 2010-2024                                         *
+* Copyright :  Angus Johnson 2010-2025                                         *
 * Purpose   :  This is the main polygon clipping module                        *
 * License   :  https://www.boost.org/LICENSE_1_0.txt                           *
 *******************************************************************************/
@@ -478,8 +478,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;
@@ -532,9 +531,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;
@@ -542,9 +541,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;
@@ -574,30 +573,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)
   {
@@ -1122,21 +1122,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;
@@ -1151,7 +1149,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;
   }
 
@@ -1561,7 +1559,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
@@ -1626,7 +1624,7 @@ namespace Clipper2Lib {
 
       if (using_polytree_)
       {
-        if (Path1InsidePath2(prevOp, newOp))
+        if (Path2ContainsPath1(prevOp, newOp))
         {
           newOr->splits = new OutRecList();
           newOr->splits->emplace_back(outrec);
@@ -1648,19 +1646,32 @@ 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))
       {
-        if (op2 == outrec->pts || op2->next == outrec->pts)
-          outrec->pts = outrec->pts->prev;
-        DoSplitOp(outrec, op2);
-        if (!outrec->pts) break;
-        op2 = outrec->pts;
-        continue;
+        if (SegmentsIntersect(op2->prev->pt,
+          op2->pt, op2->next->next->pt, op2->next->next->next->pt))
+        {
+          // adjacent intersections (ie a micro self-intersections)
+          op2 = DuplicateOp(op2, false);
+          op2->pt = op2->next->next->next->pt;
+          op2 = op2->next;
+        }
+        else
+        {
+          if (op2 == outrec->pts || op2->next == outrec->pts)
+            outrec->pts = outrec->pts->prev;
+          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
         op2 = op2->next;
@@ -2258,7 +2269,6 @@ 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)
@@ -2266,7 +2276,6 @@ namespace Clipper2Lib {
     fromOr->splits->clear();
   }
 
-
   void ClipperBase::ProcessHorzJoins()
   {
     for (const HorzJoin& j : horz_join_list_)
@@ -2295,8 +2304,8 @@ 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;
@@ -2307,7 +2316,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;
           }
@@ -2320,13 +2329,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;
@@ -2932,20 +2942,24 @@ namespace Clipper2Lib {
   {
     for (auto split : *splits)
     {
+      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;
-      }
+        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;
   }
@@ -2962,7 +2976,7 @@ namespace Clipper2Lib {
       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;
     }
 
@@ -3025,6 +3039,7 @@ namespace Clipper2Lib {
     {
       OutRec* outrec = outrec_list_[i];
       if (!outrec || !outrec->pts) continue;
+
       if (outrec->is_open)
       {
         Path64 path;

+ 18 - 11
thirdparty/clipper2/src/clipper.offset.cpp

@@ -1,6 +1,6 @@
 /*******************************************************************************
 * Author    :  Angus Johnson                                                   *
-* Date      :  22 January 2025                                                 *
+* Date      :  4 May 2025                                                      *
 * Website   :  https://www.angusj.com                                          *
 * Copyright :  Angus Johnson 2010-2025                                         *
 * Purpose   :  Path Offset (Inflate/Shrink)                                    *
@@ -33,22 +33,28 @@ const double arc_const = 0.002; // <-- 1/500
 // Miscellaneous methods
 //------------------------------------------------------------------------------
 
-std::optional<size_t> GetLowestClosedPathIdx(const Paths64& paths)
+void GetLowestClosedPathInfo(const Paths64& paths, std::optional<size_t>& idx, bool& is_neg_area)
 {
-    std::optional<size_t> 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) || 
+			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)
@@ -141,15 +147,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;
 	}
 }
@@ -597,10 +604,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