diff --git a/src/main/java/clipper2/core/InternalClipper.java b/src/main/java/clipper2/core/InternalClipper.java index 02584f9..9c1f3d1 100644 --- a/src/main/java/clipper2/core/InternalClipper.java +++ b/src/main/java/clipper2/core/InternalClipper.java @@ -241,13 +241,13 @@ public static boolean IsCollinear(Point64 pt1, Point64 sharedPt, Point64 pt2) { } /** - * Holds the low‐ and high‐64 bits of a 128‐bit product. + * Holds the low‐ and high‐64 bits of a 128‐bit unsigned product. */ - private static class MultiplyUInt64Result { + private static class UInt128Struct { public final long lo64; public final long hi64; - public MultiplyUInt64Result(long lo64, long hi64) { + public UInt128Struct(long lo64, long hi64) { this.lo64 = lo64; this.hi64 = hi64; } @@ -257,7 +257,7 @@ public MultiplyUInt64Result(long lo64, long hi64) { * Multiply two unsigned 64‐bit quantities (given in signed longs) and return * the full 128‐bit result as hi/lo. */ - private static MultiplyUInt64Result multiplyUInt64(long a, long b) { + private static UInt128Struct multiplyUInt64(long a, long b) { // mask to extract low 32 bits final long MASK_32 = 0xFFFFFFFFL; long aLow = a & MASK_32; @@ -272,7 +272,7 @@ private static MultiplyUInt64Result multiplyUInt64(long a, long b) { long lo64 = ((x3 & MASK_32) << 32) | (x1 & MASK_32); long hi64 = aHigh * bHigh + (x2 >>> 32) + (x3 >>> 32); - return new MultiplyUInt64Result(lo64, hi64); + return new UInt128Struct(lo64, hi64); } /** @@ -286,8 +286,8 @@ private static boolean productsAreEqual(long a, long b, long c, long d) { long absC = c < 0 ? -c : c; long absD = d < 0 ? -d : d; - MultiplyUInt64Result p1 = multiplyUInt64(absA, absB); - MultiplyUInt64Result p2 = multiplyUInt64(absC, absD); + UInt128Struct p1 = multiplyUInt64(absA, absB); + UInt128Struct p2 = multiplyUInt64(absC, absD); int signAB = triSign(a) * triSign(b); int signCD = triSign(c) * triSign(d); @@ -302,4 +302,52 @@ private static int triSign(long x) { return x > 1 ? 1 : 0; } + public static Rect64 GetBounds(Path64 path) { + if (path.isEmpty()) { + return new Rect64(); + } + Rect64 result = clipper2.Clipper.InvalidRect64.clone(); + for (Point64 pt : path) { + if (pt.x < result.left) { + result.left = pt.x; + } + if (pt.x > result.right) { + result.right = pt.x; + } + if (pt.y < result.top) { + result.top = pt.y; + } + if (pt.y > result.bottom) { + result.bottom = pt.y; + } + } + return result; + } + + public static boolean Path2ContainsPath1(Path64 path1, Path64 path2) { + // accommodate potential rounding error before deciding either way + PointInPolygonResult pip = PointInPolygonResult.IsOn; + for (Point64 pt : path1) { + switch (PointInPolygon(pt, path2)) { + case IsOutside: + if (pip == PointInPolygonResult.IsOutside) { + return false; + } + pip = PointInPolygonResult.IsOutside; + break; + case IsInside: + if (pip == PointInPolygonResult.IsInside) { + return true; + } + pip = PointInPolygonResult.IsInside; + break; + default: + break; + } + } + // path1 is still equivocal, so test its midpoint + Point64 mp = GetBounds(path1).MidPoint(); + return PointInPolygon(mp, path2) != PointInPolygonResult.IsOutside; + } + } diff --git a/src/main/java/clipper2/engine/ClipperBase.java b/src/main/java/clipper2/engine/ClipperBase.java index 71621bb..d1b4f21 100644 --- a/src/main/java/clipper2/engine/ClipperBase.java +++ b/src/main/java/clipper2/engine/ClipperBase.java @@ -1561,7 +1561,7 @@ else if (pt.opEquals(ae1.localMin.vertex.pt) && !IsOpenEnd(ae1.localMin.vertex)) resultOp = AddLocalMaxPoly(ae1, ae2, pt); } else if (IsFront(ae1) || (ae1.outrec == ae2.outrec)) { // this 'else if' condition isn't strictly needed but - // it's sensible to split polygons that ony touch at + // it's sensible to split polygons that only touch at // a common vertex (not at common edges). resultOp = AddLocalMaxPoly(ae1, ae2, pt); AddLocalMinPoly(ae1, ae2, pt); @@ -1659,11 +1659,9 @@ private void AdjustCurrXAndCopyToSEL(long topY) { ae.prevInSEL = ae.prevInAEL; ae.nextInSEL = ae.nextInAEL; ae.jump = ae.nextInSEL; - if (ae.joinWith == JoinWith.Left) { - ae.curX = ae.prevInAEL.curX; // this also avoids complications - } else { - ae.curX = TopX(ae, topY); - } + // it's safe to ignore joined edges here because + // if necessary they get split in IntersectEdges() + ae.curX = TopX(ae, topY); // NB don't update ae.curr.y yet (see AddNewIntersectNode) ae = ae.nextInAEL; } @@ -2480,7 +2478,7 @@ private static PointInPolygonResult PointInOpPolygon(Point64 pt, OutPt op) { break; } - // must have touched or crossed the pt.y horizonal + // must have touched or crossed the pt.y horizontal // and this must happen an even number of times if (op2.pt.y == pt.y) // touching the horizontal @@ -2532,25 +2530,29 @@ private static PointInPolygonResult PointInOpPolygon(Point64 pt, OutPt op) { private static boolean Path1InsidePath2(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 outsideCnt = 0; + PointInPolygonResult pip = PointInPolygonResult.IsOn; OutPt op = op1; do { - result = PointInOpPolygon(op.pt, op2); - if (result == PointInPolygonResult.IsOutside) { - ++outsideCnt; - } else if (result == PointInPolygonResult.IsInside) { - --outsideCnt; + switch (PointInOpPolygon(op.pt, op2)) { + case IsOutside: + if (pip == PointInPolygonResult.IsOutside) { + return false; + } + pip = PointInPolygonResult.IsOutside; + break; + case IsInside: + if (pip == PointInPolygonResult.IsInside) { + return true; + } + pip = PointInPolygonResult.IsInside; + break; + default: + break; } op = op.next; - } while (op != op1 && Math.abs(outsideCnt) < 2); - if (Math.abs(outsideCnt) > 1) { - return (outsideCnt < 0); - } - // since path1's location is still equivocal, check its midpoint - Point64 mp = GetBounds(GetCleanPath(op1)).MidPoint(); - Path64 path2 = GetCleanPath(op2); - return InternalClipper.PointInPolygon(mp, path2) != PointInPolygonResult.IsOutside; + } while (op != op1); + // result is unclear, so try again using cleaned paths + return InternalClipper.Path2ContainsPath1(GetCleanPath(op1), GetCleanPath(op2)); // #973 } private void MoveSplits(OutRec fromOr, OutRec toOr) { @@ -2771,9 +2773,8 @@ private void FixSelfIntersects(OutRec outrec) { } op2 = outrec.pts; continue; - } else { - op2 = op2.next; } + op2 = op2.next; if (op2 == outrec.pts) { break; } @@ -2849,28 +2850,6 @@ protected final boolean BuildPaths(Paths64 solutionClosed, Paths64 solutionOpen) return true; } - public static Rect64 GetBounds(Path64 path) { - if (path.isEmpty()) { - return new Rect64(); - } - Rect64 result = Clipper.InvalidRect64.clone(); - for (Point64 pt : path) { - if (pt.x < result.left) { - result.left = pt.x; - } - if (pt.x > result.right) { - result.right = pt.x; - } - if (pt.y < result.top) { - result.top = pt.y; - } - if (pt.y > result.bottom) { - result.bottom = pt.y; - } - } - return result; - } - private boolean CheckBounds(OutRec outrec) { if (outrec.pts == null) { return false; @@ -2882,16 +2861,17 @@ private boolean CheckBounds(OutRec outrec) { if (outrec.pts == null || !BuildPath(outrec.pts, getReverseSolution(), false, outrec.path)) { return false; } - outrec.bounds = GetBounds(outrec.path); + outrec.bounds = InternalClipper.GetBounds(outrec.path); return true; } private boolean CheckSplitOwner(OutRec outrec, List splits) { - if (outrec.owner == null || outrec.owner.splits == null) { - return false; - } for (int i : splits) { - OutRec split = GetRealOutRec(outrecList.get(i)); + OutRec split = outrecList.get(i); + if (split.pts == null && split.splits != null && CheckSplitOwner(outrec, split.splits)) { + return true; // #942 + } + split = GetRealOutRec(split); if (split == null || split == outrec || split.recursiveSplit == outrec) { continue; } @@ -2899,10 +2879,15 @@ private boolean CheckSplitOwner(OutRec outrec, List splits) { if (split.splits != null && CheckSplitOwner(outrec, split.splits)) { return true; } - if (IsValidOwner(outrec, split) && CheckBounds(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) || !Path1InsidePath2(outrec.pts, split.pts)) { + continue; + } + if (!IsValidOwner(outrec, split)) { + // split is owned by outrec (#957) + split.owner = outrec.owner; } + outrec.owner = split; // found in split + return true; } return false; } diff --git a/src/main/java/clipper2/offset/Group.java b/src/main/java/clipper2/offset/Group.java index 289adf3..8e35a9e 100644 --- a/src/main/java/clipper2/offset/Group.java +++ b/src/main/java/clipper2/offset/Group.java @@ -31,27 +31,41 @@ class Group { } if (endType == EndType.Polygon) { - lowestPathIdx = GetLowestPathIdx(inPaths); + LowestPathInfo lowInfo = GetLowestPathInfo(inPaths); + lowestPathIdx = lowInfo.idx; // the lowermost path must be an outer path, so if its orientation is negative, // then flag that the whole group is 'reversed' (will negate delta etc.) // as this is much more efficient than reversing every path. - pathsReversed = (lowestPathIdx >= 0) && (Clipper.Area(inPaths.get(lowestPathIdx)) < 0); + pathsReversed = (lowestPathIdx >= 0) && lowInfo.isNegArea; } else { lowestPathIdx = -1; pathsReversed = false; } } - private static int GetLowestPathIdx(Paths64 paths) { - int result = -1; + private static final class LowestPathInfo { + int idx = -1; + boolean isNegArea = false; + } + + private static LowestPathInfo GetLowestPathInfo(Paths64 paths) { + LowestPathInfo result = new LowestPathInfo(); Point64 botPt = new Point64(Long.MAX_VALUE, Long.MIN_VALUE); for (int i = 0; i < paths.size(); i++) { + double area = Double.MAX_VALUE; for (Point64 pt : paths.get(i)) { if (pt.y < botPt.y || (pt.y == botPt.y && pt.x >= botPt.x)) { continue; } - result = i; + if (area == Double.MAX_VALUE) { + area = Clipper.Area(paths.get(i)); + if (area == 0) { + break; // invalid closed path + } + result.isNegArea = area < 0; + } + result.idx = i; botPt.x = pt.x; botPt.y = pt.y; } diff --git a/src/test/java/clipper2/TestIsCollinear.java b/src/test/java/clipper2/TestIsCollinear.java index 056a316..e493236 100644 --- a/src/test/java/clipper2/TestIsCollinear.java +++ b/src/test/java/clipper2/TestIsCollinear.java @@ -3,6 +3,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.lang.reflect.Field; +import java.lang.reflect.Method; + import org.junit.jupiter.api.Test; import clipper2.core.ClipType; @@ -15,6 +18,45 @@ class TestIsCollinear { + private static void assertMulHi(Method mulMethod, Field hiField, String aHex, String bHex, String expectedHiHex) throws Exception { + long a = Long.parseUnsignedLong(aHex, 16); + long b = Long.parseUnsignedLong(bHex, 16); + long expectedHi = Long.parseUnsignedLong(expectedHiHex, 16); + Object result = mulMethod.invoke(null, a, b); + long hi = hiField.getLong(result); + assertEquals(expectedHi, hi); + } + + @Test + void testHiCalculation() throws Exception { + Method mulMethod = InternalClipper.class.getDeclaredMethod("multiplyUInt64", long.class, long.class); + mulMethod.setAccessible(true); + Class resultClass = Class.forName("clipper2.core.InternalClipper$UInt128Struct"); + Field hiField = resultClass.getDeclaredField("hi64"); + hiField.setAccessible(true); + + assertMulHi(mulMethod, hiField, "51eaed81157de061", "3a271fb2745b6fe9", "129bbebdfae0464e"); + assertMulHi(mulMethod, hiField, "3a271fb2745b6fe9", "51eaed81157de061", "129bbebdfae0464e"); + assertMulHi(mulMethod, hiField, "c2055706a62883fa", "26c78bc79c2322cc", "1d640701d192519b"); + assertMulHi(mulMethod, hiField, "26c78bc79c2322cc", "c2055706a62883fa", "1d640701d192519b"); + assertMulHi(mulMethod, hiField, "874ddae32094b0de", "9b1559a06fdf83e0", "51f76c49563e5bfe"); + assertMulHi(mulMethod, hiField, "9b1559a06fdf83e0", "874ddae32094b0de", "51f76c49563e5bfe"); + assertMulHi(mulMethod, hiField, "81fb3ad3636ca900", "239c000a982a8da4", "12148e28207b83a3"); + assertMulHi(mulMethod, hiField, "239c000a982a8da4", "81fb3ad3636ca900", "12148e28207b83a3"); + assertMulHi(mulMethod, hiField, "4be0b4c5d2725c44", "990cd6db34a04c30", "2d5d1a4183fd6165"); + assertMulHi(mulMethod, hiField, "990cd6db34a04c30", "4be0b4c5d2725c44", "2d5d1a4183fd6165"); + assertMulHi(mulMethod, hiField, "978ec0c0433c01f6", "2df03d097966b536", "1b3251d91fe272a5"); + assertMulHi(mulMethod, hiField, "2df03d097966b536", "978ec0c0433c01f6", "1b3251d91fe272a5"); + assertMulHi(mulMethod, hiField, "49c5cbbcfd716344", "c489e3b34b007ad3", "38a32c74c8c191a4"); + assertMulHi(mulMethod, hiField, "c489e3b34b007ad3", "49c5cbbcfd716344", "38a32c74c8c191a4"); + assertMulHi(mulMethod, hiField, "d3361cdbeed655d5", "1240da41e324953a", "0f0f4fa11e7e8f2a"); + assertMulHi(mulMethod, hiField, "1240da41e324953a", "d3361cdbeed655d5", "0f0f4fa11e7e8f2a"); + assertMulHi(mulMethod, hiField, "51b854f8e71b0ae0", "6f8d438aae530af5", "239c04ee3c8cc248"); + assertMulHi(mulMethod, hiField, "6f8d438aae530af5", "51b854f8e71b0ae0", "239c04ee3c8cc248"); + assertMulHi(mulMethod, hiField, "bbecf7dbc6147480", "bb0f73d0f82e2236", "895170f4e9a216a7"); + assertMulHi(mulMethod, hiField, "bb0f73d0f82e2236", "bbecf7dbc6147480", "895170f4e9a216a7"); + } + @Test void testIsCollinear() { // A large integer not representable exactly by double. diff --git a/src/test/java/clipper2/TestOffsets.java b/src/test/java/clipper2/TestOffsets.java index cf28575..fed04fb 100644 --- a/src/test/java/clipper2/TestOffsets.java +++ b/src/test/java/clipper2/TestOffsets.java @@ -271,6 +271,25 @@ void TestOffsets12() { // see #873 assertTrue(solution.isEmpty()); } + @Test + void TestOffsets13() { // see #965 + Path64 subject1 = new Path64(List.of(new Point64(0, 0), new Point64(0, 10), new Point64(10, 0))); + double delta = 2; + + Paths64 subjects1 = new Paths64(); + subjects1.add(subject1); + Paths64 solution1 = Clipper.InflatePaths(subjects1, delta, JoinType.Miter, EndType.Polygon); + long area1 = Math.round(Math.abs(Clipper.Area(solution1))); + assertEquals(122L, area1); + + Paths64 subjects2 = new Paths64(); + subjects2.add(subject1); + subjects2.add(new Path64(List.of(new Point64(0, 20)))); // single-point path should not change output + Paths64 solution2 = Clipper.InflatePaths(subjects2, delta, JoinType.Miter, EndType.Polygon); + long area2 = Math.round(Math.abs(Clipper.Area(solution2))); + assertEquals(122L, area2); + } + private static Point64 midPoint(Point64 p1, Point64 p2) { Point64 result = new Point64(); result.setX((p1.x + p2.x) / 2); diff --git a/src/test/java/clipper2/TestPolygons.java b/src/test/java/clipper2/TestPolygons.java index 468cf7e..f5fb0a5 100644 --- a/src/test/java/clipper2/TestPolygons.java +++ b/src/test/java/clipper2/TestPolygons.java @@ -48,10 +48,12 @@ final void RunPolygonsTestCase(TestCase test, int testNum, Object o, Object o1) if (storedCount > 0) { if (Arrays.asList(140, 150, 165, 166, 168, 172, 173, 176, 177, 179).contains(testNum)) { assertTrue(countDiff <= 7, "Diff=" + countDiff); + } else if (testNum == 126) { + assertTrue(countDiff <= 3); + } else if (Arrays.asList(16, 27, 121).contains(testNum)) { + assertTrue(countDiff <= 2); } else if (testNum >= 120) { assertTrue(countDiff <= 6); - } else if (Arrays.asList(27, 121, 126).contains(testNum)) { - assertTrue(countDiff <= 2); } else if (Arrays.asList(23, 37, 43, 45, 87, 102, 111, 118, 119).contains(testNum)) { assertTrue(countDiff <= 1); } else { diff --git a/src/test/java/clipper2/TestPolytree.java b/src/test/java/clipper2/TestPolytree.java index 0096d87..afbe2a9 100644 --- a/src/test/java/clipper2/TestPolytree.java +++ b/src/test/java/clipper2/TestPolytree.java @@ -9,6 +9,7 @@ import java.util.List; import java.util.stream.Stream; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -139,4 +140,76 @@ private static int PolyPathContainsPoint(PolyPath64 pp, Point64 pt, int counter) return counter; } + @Test + void TestPolytree3() { // #942 + Paths64 subject = new Paths64(); + subject.add(Clipper.MakePath(new long[] { 1588700, -8717600, 1616200, -8474800, 1588700, -8474800 })); + subject.add(Clipper.MakePath(new long[] { 13583800, -15601600, 13582800, -15508500, 13555300, -15508500, 13555500, -15182200, 13010900, -15185400 })); + subject.add(Clipper.MakePath(new long[] { 956700, -3092300, 1152600, 3147400, 25600, 3151700 })); + subject.add(Clipper.MakePath(new long[] { 22575900, -16604000, 31286800, -12171900, 31110200, 4882800, 30996200, 4826300, 30414400, 5447400, 30260000, 5391500, + 29662200, 5805400, 28844500, 5337900, 28435000, 5789300, 27721400, 5026400, 22876300, 5034300, 21977700, 4414900, 21148000, 4654700, 20917600, 4653400, + 19334300, 12411000, -2591700, 12177200, 53200, 3151100, -2564300, 12149800, 7819400, 4692400, 10116000, 5228600, 6975500, 3120100, 7379700, 3124700, + 11037900, 596200, 12257000, 2587800, 12257000, 596200, 15227300, 2352700, 18444400, 1112100, 19961100, 5549400, 20173200, 5078600, 20330000, 5079300, + 20970200, 4544300, 20989600, 4563700, 19465500, 1112100, 21611600, 4182100, 22925100, 1112200, 22952700, 1637200, 23059000, 1112200, 24908100, 4181200, + 27070100, 3800600, 27238000, 3800700, 28582200, 520300, 29367800, 1050100, 29291400, 179400, 29133700, 360700, 29056700, 312600, 29121900, 332500, + 29269900, 162300, 28941400, 213100, 27491300, -3041500, 27588700, -2997800, 22104900, -16142800, 13010900, -15603000, 13555500, -15182200, + 13555300, -15508500, 13582800, -15508500, 13583100, -15154700, 1588700, -8822800, 1588700, -8379900, 1588700, -8474800, 1616200, -8474800, 1003900, + -630100, 1253300, -12284500, 12983400, -16239900 })); + subject.add(Clipper.MakePath(new long[] { 198200, 12149800, 1010600, 12149800, 1011500, 11859600 })); + subject.add(Clipper.MakePath(new long[] { 21996700, -7432000, 22096700, -7432000, 22096700, -7332000 })); + + PolyTree64 solutionTree = new PolyTree64(); + Clipper64 clipper = new Clipper64(); + clipper.AddSubject(subject); + clipper.Execute(clipper2.core.ClipType.Union, clipper2.core.FillRule.NonZero, solutionTree); + + assertTrue(solutionTree.getCount() == 1 && solutionTree.get(0).getCount() == 2 && solutionTree.get(0).get(1).getCount() == 1); + } + + @Test + void TestPolytree4() { // #957 + Paths64 subject = new Paths64(); + subject.add(Clipper.MakePath(new long[] { 77910, 46865, 78720, 46865, 78720, 48000, 77910, 48000, 77910, 46865 })); + subject.add(Clipper.MakePath(new long[] { 82780, 53015, 93600, 53015, 93600, 54335, 82780, 54335, 82780, 53015 })); + subject.add(Clipper.MakePath(new long[] { 82780, 48975, 84080, 48975, 84080, 53015, 82780, 53015, 82780, 48975 })); + subject.add(Clipper.MakePath(new long[] { 77910, 48000, 84080, 48000, 84080, 48975, 77910, 48975, 77910, 48000 })); + subject.add(Clipper.MakePath(new long[] { 89880, 40615, 90700, 40615, 90700, 46865, 89880, 46865, 89880, 40615 })); + subject.add(Clipper.MakePath(new long[] { 92700, 54335, 93600, 54335, 93600, 61420, 92700, 61420, 92700, 54335 })); + subject.add(Clipper.MakePath(new long[] { 78950, 47425, 84080, 47425, 84080, 47770, 78950, 47770, 78950, 47425 })); + subject.add(Clipper.MakePath(new long[] { 82780, 61420, 93600, 61420, 93600, 62435, 82780, 62435, 82780, 61420 })); + subject.add(Clipper.MakePath(new long[] { 101680, 63085, 100675, 63085, 100675, 47770, 100680, 47770, 100680, 40615, 101680, 40615, 101680, 63085 })); + subject.add(Clipper.MakePath(new long[] { 76195, 39880, 89880, 39880, 89880, 41045, 76195, 41045, 76195, 39880 })); + subject.add(Clipper.MakePath(new long[] { 85490, 56145, 90520, 56145, 90520, 59235, 85490, 59235, 85490, 56145 })); + subject.add(Clipper.MakePath(new long[] { 89880, 39880, 101680, 39880, 101680, 40615, 89880, 40615, 89880, 39880 })); + subject.add(Clipper.MakePath(new long[] { 89880, 46865, 100680, 46865, 100680, 47770, 89880, 47770, 89880, 46865 })); + subject.add(Clipper.MakePath(new long[] { 82780, 54335, 83280, 54335, 83280, 61420, 82780, 61420, 82780, 54335 })); + subject.add(Clipper.MakePath(new long[] { 76195, 41045, 76855, 41045, 76855, 62665, 76195, 62665, 76195, 41045 })); + subject.add(Clipper.MakePath(new long[] { 76195, 62665, 100675, 62665, 100675, 63085, 76195, 63085, 76195, 62665 })); + subject.add(Clipper.MakePath(new long[] { 82780, 41045, 84080, 41045, 84080, 47425, 82780, 47425, 82780, 41045 })); + + PolyTree64 solutionTree = new PolyTree64(); + Clipper64 clipper = new Clipper64(); + clipper.AddSubject(subject); + clipper.Execute(clipper2.core.ClipType.Union, clipper2.core.FillRule.NonZero, solutionTree); + + assertTrue(solutionTree.getCount() == 1 && solutionTree.get(0).getCount() == 2 && solutionTree.get(0).get(0).getCount() == 1); + } + + @Test + void TestPolytree5() { // #973 + Paths64 subject = new Paths64(); + subject.add(Clipper.MakePath(new long[] { 0, 0, 79530, 0, 79530, 940, 0, 940, 0, 0 })); + subject.add(Clipper.MakePath(new long[] { 0, 33360, 79530, 33360, 79530, 34300, 0, 34300, 0, 33360 })); + subject.add(Clipper.MakePath(new long[] { 78470, 940, 79530, 940, 79530, 33360, 78470, 33360, 78470, 940 })); + subject.add(Clipper.MakePath(new long[] { 0, 940, 940, 940, 940, 33360, 0, 33360, 0, 940 })); + subject.add(Clipper.MakePath(new long[] { 29290, 940, 30350, 940, 30350, 33360, 29290, 33360, 29290, 940 })); + + PolyTree64 solutionTree = new PolyTree64(); + Clipper64 clipper = new Clipper64(); + clipper.AddSubject(subject); + clipper.Execute(clipper2.core.ClipType.Union, clipper2.core.FillRule.NonZero, solutionTree); + + assertTrue(solutionTree.getCount() == 1 && solutionTree.get(0).getCount() == 2); + } + }