Skip to content

Commit 65319e0

Browse files
authored
1.5.4 (#32)
1 parent 116b213 commit 65319e0

File tree

7 files changed

+251
-68
lines changed

7 files changed

+251
-68
lines changed

src/main/java/clipper2/core/InternalClipper.java

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -241,13 +241,13 @@ public static boolean IsCollinear(Point64 pt1, Point64 sharedPt, Point64 pt2) {
241241
}
242242

243243
/**
244-
* Holds the low‐ and high‐64 bits of a 128‐bit product.
244+
* Holds the low‐ and high‐64 bits of a 128‐bit unsigned product.
245245
*/
246-
private static class MultiplyUInt64Result {
246+
private static class UInt128Struct {
247247
public final long lo64;
248248
public final long hi64;
249249

250-
public MultiplyUInt64Result(long lo64, long hi64) {
250+
public UInt128Struct(long lo64, long hi64) {
251251
this.lo64 = lo64;
252252
this.hi64 = hi64;
253253
}
@@ -257,7 +257,7 @@ public MultiplyUInt64Result(long lo64, long hi64) {
257257
* Multiply two unsigned 64‐bit quantities (given in signed longs) and return
258258
* the full 128‐bit result as hi/lo.
259259
*/
260-
private static MultiplyUInt64Result multiplyUInt64(long a, long b) {
260+
private static UInt128Struct multiplyUInt64(long a, long b) {
261261
// mask to extract low 32 bits
262262
final long MASK_32 = 0xFFFFFFFFL;
263263
long aLow = a & MASK_32;
@@ -272,7 +272,7 @@ private static MultiplyUInt64Result multiplyUInt64(long a, long b) {
272272
long lo64 = ((x3 & MASK_32) << 32) | (x1 & MASK_32);
273273
long hi64 = aHigh * bHigh + (x2 >>> 32) + (x3 >>> 32);
274274

275-
return new MultiplyUInt64Result(lo64, hi64);
275+
return new UInt128Struct(lo64, hi64);
276276
}
277277

278278
/**
@@ -286,8 +286,8 @@ private static boolean productsAreEqual(long a, long b, long c, long d) {
286286
long absC = c < 0 ? -c : c;
287287
long absD = d < 0 ? -d : d;
288288

289-
MultiplyUInt64Result p1 = multiplyUInt64(absA, absB);
290-
MultiplyUInt64Result p2 = multiplyUInt64(absC, absD);
289+
UInt128Struct p1 = multiplyUInt64(absA, absB);
290+
UInt128Struct p2 = multiplyUInt64(absC, absD);
291291

292292
int signAB = triSign(a) * triSign(b);
293293
int signCD = triSign(c) * triSign(d);
@@ -302,4 +302,52 @@ private static int triSign(long x) {
302302
return x > 1 ? 1 : 0;
303303
}
304304

305+
public static Rect64 GetBounds(Path64 path) {
306+
if (path.isEmpty()) {
307+
return new Rect64();
308+
}
309+
Rect64 result = clipper2.Clipper.InvalidRect64.clone();
310+
for (Point64 pt : path) {
311+
if (pt.x < result.left) {
312+
result.left = pt.x;
313+
}
314+
if (pt.x > result.right) {
315+
result.right = pt.x;
316+
}
317+
if (pt.y < result.top) {
318+
result.top = pt.y;
319+
}
320+
if (pt.y > result.bottom) {
321+
result.bottom = pt.y;
322+
}
323+
}
324+
return result;
325+
}
326+
327+
public static boolean Path2ContainsPath1(Path64 path1, Path64 path2) {
328+
// accommodate potential rounding error before deciding either way
329+
PointInPolygonResult pip = PointInPolygonResult.IsOn;
330+
for (Point64 pt : path1) {
331+
switch (PointInPolygon(pt, path2)) {
332+
case IsOutside:
333+
if (pip == PointInPolygonResult.IsOutside) {
334+
return false;
335+
}
336+
pip = PointInPolygonResult.IsOutside;
337+
break;
338+
case IsInside:
339+
if (pip == PointInPolygonResult.IsInside) {
340+
return true;
341+
}
342+
pip = PointInPolygonResult.IsInside;
343+
break;
344+
default:
345+
break;
346+
}
347+
}
348+
// path1 is still equivocal, so test its midpoint
349+
Point64 mp = GetBounds(path1).MidPoint();
350+
return PointInPolygon(mp, path2) != PointInPolygonResult.IsOutside;
351+
}
352+
305353
}

src/main/java/clipper2/engine/ClipperBase.java

Lines changed: 39 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1561,7 +1561,7 @@ else if (pt.opEquals(ae1.localMin.vertex.pt) && !IsOpenEnd(ae1.localMin.vertex))
15611561
resultOp = AddLocalMaxPoly(ae1, ae2, pt);
15621562
} else if (IsFront(ae1) || (ae1.outrec == ae2.outrec)) {
15631563
// this 'else if' condition isn't strictly needed but
1564-
// it's sensible to split polygons that ony touch at
1564+
// it's sensible to split polygons that only touch at
15651565
// a common vertex (not at common edges).
15661566
resultOp = AddLocalMaxPoly(ae1, ae2, pt);
15671567
AddLocalMinPoly(ae1, ae2, pt);
@@ -1659,11 +1659,9 @@ private void AdjustCurrXAndCopyToSEL(long topY) {
16591659
ae.prevInSEL = ae.prevInAEL;
16601660
ae.nextInSEL = ae.nextInAEL;
16611661
ae.jump = ae.nextInSEL;
1662-
if (ae.joinWith == JoinWith.Left) {
1663-
ae.curX = ae.prevInAEL.curX; // this also avoids complications
1664-
} else {
1665-
ae.curX = TopX(ae, topY);
1666-
}
1662+
// it's safe to ignore joined edges here because
1663+
// if necessary they get split in IntersectEdges()
1664+
ae.curX = TopX(ae, topY);
16671665
// NB don't update ae.curr.y yet (see AddNewIntersectNode)
16681666
ae = ae.nextInAEL;
16691667
}
@@ -2480,7 +2478,7 @@ private static PointInPolygonResult PointInOpPolygon(Point64 pt, OutPt op) {
24802478
break;
24812479
}
24822480

2483-
// must have touched or crossed the pt.y horizonal
2481+
// must have touched or crossed the pt.y horizontal
24842482
// and this must happen an even number of times
24852483

24862484
if (op2.pt.y == pt.y) // touching the horizontal
@@ -2532,25 +2530,29 @@ private static PointInPolygonResult PointInOpPolygon(Point64 pt, OutPt op) {
25322530
private static boolean Path1InsidePath2(OutPt op1, OutPt op2) {
25332531
// we need to make some accommodation for rounding errors
25342532
// so we won't jump if the first vertex is found outside
2535-
PointInPolygonResult result;
2536-
int outsideCnt = 0;
2533+
PointInPolygonResult pip = PointInPolygonResult.IsOn;
25372534
OutPt op = op1;
25382535
do {
2539-
result = PointInOpPolygon(op.pt, op2);
2540-
if (result == PointInPolygonResult.IsOutside) {
2541-
++outsideCnt;
2542-
} else if (result == PointInPolygonResult.IsInside) {
2543-
--outsideCnt;
2536+
switch (PointInOpPolygon(op.pt, op2)) {
2537+
case IsOutside:
2538+
if (pip == PointInPolygonResult.IsOutside) {
2539+
return false;
2540+
}
2541+
pip = PointInPolygonResult.IsOutside;
2542+
break;
2543+
case IsInside:
2544+
if (pip == PointInPolygonResult.IsInside) {
2545+
return true;
2546+
}
2547+
pip = PointInPolygonResult.IsInside;
2548+
break;
2549+
default:
2550+
break;
25442551
}
25452552
op = op.next;
2546-
} while (op != op1 && Math.abs(outsideCnt) < 2);
2547-
if (Math.abs(outsideCnt) > 1) {
2548-
return (outsideCnt < 0);
2549-
}
2550-
// since path1's location is still equivocal, check its midpoint
2551-
Point64 mp = GetBounds(GetCleanPath(op1)).MidPoint();
2552-
Path64 path2 = GetCleanPath(op2);
2553-
return InternalClipper.PointInPolygon(mp, path2) != PointInPolygonResult.IsOutside;
2553+
} while (op != op1);
2554+
// result is unclear, so try again using cleaned paths
2555+
return InternalClipper.Path2ContainsPath1(GetCleanPath(op1), GetCleanPath(op2)); // #973
25542556
}
25552557

25562558
private void MoveSplits(OutRec fromOr, OutRec toOr) {
@@ -2771,9 +2773,8 @@ private void FixSelfIntersects(OutRec outrec) {
27712773
}
27722774
op2 = outrec.pts;
27732775
continue;
2774-
} else {
2775-
op2 = op2.next;
27762776
}
2777+
op2 = op2.next;
27772778
if (op2 == outrec.pts) {
27782779
break;
27792780
}
@@ -2849,28 +2850,6 @@ protected final boolean BuildPaths(Paths64 solutionClosed, Paths64 solutionOpen)
28492850
return true;
28502851
}
28512852

2852-
public static Rect64 GetBounds(Path64 path) {
2853-
if (path.isEmpty()) {
2854-
return new Rect64();
2855-
}
2856-
Rect64 result = Clipper.InvalidRect64.clone();
2857-
for (Point64 pt : path) {
2858-
if (pt.x < result.left) {
2859-
result.left = pt.x;
2860-
}
2861-
if (pt.x > result.right) {
2862-
result.right = pt.x;
2863-
}
2864-
if (pt.y < result.top) {
2865-
result.top = pt.y;
2866-
}
2867-
if (pt.y > result.bottom) {
2868-
result.bottom = pt.y;
2869-
}
2870-
}
2871-
return result;
2872-
}
2873-
28742853
private boolean CheckBounds(OutRec outrec) {
28752854
if (outrec.pts == null) {
28762855
return false;
@@ -2882,27 +2861,33 @@ private boolean CheckBounds(OutRec outrec) {
28822861
if (outrec.pts == null || !BuildPath(outrec.pts, getReverseSolution(), false, outrec.path)) {
28832862
return false;
28842863
}
2885-
outrec.bounds = GetBounds(outrec.path);
2864+
outrec.bounds = InternalClipper.GetBounds(outrec.path);
28862865
return true;
28872866
}
28882867

28892868
private boolean CheckSplitOwner(OutRec outrec, List<Integer> splits) {
2890-
if (outrec.owner == null || outrec.owner.splits == null) {
2891-
return false;
2892-
}
28932869
for (int i : splits) {
2894-
OutRec split = GetRealOutRec(outrecList.get(i));
2870+
OutRec split = outrecList.get(i);
2871+
if (split.pts == null && split.splits != null && CheckSplitOwner(outrec, split.splits)) {
2872+
return true; // #942
2873+
}
2874+
split = GetRealOutRec(split);
28952875
if (split == null || split == outrec || split.recursiveSplit == outrec) {
28962876
continue;
28972877
}
28982878
split.recursiveSplit = outrec; // #599
28992879
if (split.splits != null && CheckSplitOwner(outrec, split.splits)) {
29002880
return true;
29012881
}
2902-
if (IsValidOwner(outrec, split) && CheckBounds(split) && split.bounds.Contains(outrec.bounds) && Path1InsidePath2(outrec.pts, split.pts)) {
2903-
outrec.owner = split; // found in split
2904-
return true;
2882+
if (!CheckBounds(split) || !split.bounds.Contains(outrec.bounds) || !Path1InsidePath2(outrec.pts, split.pts)) {
2883+
continue;
2884+
}
2885+
if (!IsValidOwner(outrec, split)) {
2886+
// split is owned by outrec (#957)
2887+
split.owner = outrec.owner;
29052888
}
2889+
outrec.owner = split; // found in split
2890+
return true;
29062891
}
29072892
return false;
29082893
}

src/main/java/clipper2/offset/Group.java

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,27 +31,41 @@ class Group {
3131
}
3232

3333
if (endType == EndType.Polygon) {
34-
lowestPathIdx = GetLowestPathIdx(inPaths);
34+
LowestPathInfo lowInfo = GetLowestPathInfo(inPaths);
35+
lowestPathIdx = lowInfo.idx;
3536

3637
// the lowermost path must be an outer path, so if its orientation is negative,
3738
// then flag that the whole group is 'reversed' (will negate delta etc.)
3839
// as this is much more efficient than reversing every path.
39-
pathsReversed = (lowestPathIdx >= 0) && (Clipper.Area(inPaths.get(lowestPathIdx)) < 0);
40+
pathsReversed = (lowestPathIdx >= 0) && lowInfo.isNegArea;
4041
} else {
4142
lowestPathIdx = -1;
4243
pathsReversed = false;
4344
}
4445
}
4546

46-
private static int GetLowestPathIdx(Paths64 paths) {
47-
int result = -1;
47+
private static final class LowestPathInfo {
48+
int idx = -1;
49+
boolean isNegArea = false;
50+
}
51+
52+
private static LowestPathInfo GetLowestPathInfo(Paths64 paths) {
53+
LowestPathInfo result = new LowestPathInfo();
4854
Point64 botPt = new Point64(Long.MAX_VALUE, Long.MIN_VALUE);
4955
for (int i = 0; i < paths.size(); i++) {
56+
double area = Double.MAX_VALUE;
5057
for (Point64 pt : paths.get(i)) {
5158
if (pt.y < botPt.y || (pt.y == botPt.y && pt.x >= botPt.x)) {
5259
continue;
5360
}
54-
result = i;
61+
if (area == Double.MAX_VALUE) {
62+
area = Clipper.Area(paths.get(i));
63+
if (area == 0) {
64+
break; // invalid closed path
65+
}
66+
result.isNegArea = area < 0;
67+
}
68+
result.idx = i;
5569
botPt.x = pt.x;
5670
botPt.y = pt.y;
5771
}

src/test/java/clipper2/TestIsCollinear.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
import static org.junit.jupiter.api.Assertions.assertEquals;
44
import static org.junit.jupiter.api.Assertions.assertTrue;
55

6+
import java.lang.reflect.Field;
7+
import java.lang.reflect.Method;
8+
69
import org.junit.jupiter.api.Test;
710

811
import clipper2.core.ClipType;
@@ -15,6 +18,45 @@
1518

1619
class TestIsCollinear {
1720

21+
private static void assertMulHi(Method mulMethod, Field hiField, String aHex, String bHex, String expectedHiHex) throws Exception {
22+
long a = Long.parseUnsignedLong(aHex, 16);
23+
long b = Long.parseUnsignedLong(bHex, 16);
24+
long expectedHi = Long.parseUnsignedLong(expectedHiHex, 16);
25+
Object result = mulMethod.invoke(null, a, b);
26+
long hi = hiField.getLong(result);
27+
assertEquals(expectedHi, hi);
28+
}
29+
30+
@Test
31+
void testHiCalculation() throws Exception {
32+
Method mulMethod = InternalClipper.class.getDeclaredMethod("multiplyUInt64", long.class, long.class);
33+
mulMethod.setAccessible(true);
34+
Class<?> resultClass = Class.forName("clipper2.core.InternalClipper$UInt128Struct");
35+
Field hiField = resultClass.getDeclaredField("hi64");
36+
hiField.setAccessible(true);
37+
38+
assertMulHi(mulMethod, hiField, "51eaed81157de061", "3a271fb2745b6fe9", "129bbebdfae0464e");
39+
assertMulHi(mulMethod, hiField, "3a271fb2745b6fe9", "51eaed81157de061", "129bbebdfae0464e");
40+
assertMulHi(mulMethod, hiField, "c2055706a62883fa", "26c78bc79c2322cc", "1d640701d192519b");
41+
assertMulHi(mulMethod, hiField, "26c78bc79c2322cc", "c2055706a62883fa", "1d640701d192519b");
42+
assertMulHi(mulMethod, hiField, "874ddae32094b0de", "9b1559a06fdf83e0", "51f76c49563e5bfe");
43+
assertMulHi(mulMethod, hiField, "9b1559a06fdf83e0", "874ddae32094b0de", "51f76c49563e5bfe");
44+
assertMulHi(mulMethod, hiField, "81fb3ad3636ca900", "239c000a982a8da4", "12148e28207b83a3");
45+
assertMulHi(mulMethod, hiField, "239c000a982a8da4", "81fb3ad3636ca900", "12148e28207b83a3");
46+
assertMulHi(mulMethod, hiField, "4be0b4c5d2725c44", "990cd6db34a04c30", "2d5d1a4183fd6165");
47+
assertMulHi(mulMethod, hiField, "990cd6db34a04c30", "4be0b4c5d2725c44", "2d5d1a4183fd6165");
48+
assertMulHi(mulMethod, hiField, "978ec0c0433c01f6", "2df03d097966b536", "1b3251d91fe272a5");
49+
assertMulHi(mulMethod, hiField, "2df03d097966b536", "978ec0c0433c01f6", "1b3251d91fe272a5");
50+
assertMulHi(mulMethod, hiField, "49c5cbbcfd716344", "c489e3b34b007ad3", "38a32c74c8c191a4");
51+
assertMulHi(mulMethod, hiField, "c489e3b34b007ad3", "49c5cbbcfd716344", "38a32c74c8c191a4");
52+
assertMulHi(mulMethod, hiField, "d3361cdbeed655d5", "1240da41e324953a", "0f0f4fa11e7e8f2a");
53+
assertMulHi(mulMethod, hiField, "1240da41e324953a", "d3361cdbeed655d5", "0f0f4fa11e7e8f2a");
54+
assertMulHi(mulMethod, hiField, "51b854f8e71b0ae0", "6f8d438aae530af5", "239c04ee3c8cc248");
55+
assertMulHi(mulMethod, hiField, "6f8d438aae530af5", "51b854f8e71b0ae0", "239c04ee3c8cc248");
56+
assertMulHi(mulMethod, hiField, "bbecf7dbc6147480", "bb0f73d0f82e2236", "895170f4e9a216a7");
57+
assertMulHi(mulMethod, hiField, "bb0f73d0f82e2236", "bbecf7dbc6147480", "895170f4e9a216a7");
58+
}
59+
1860
@Test
1961
void testIsCollinear() {
2062
// A large integer not representable exactly by double.

src/test/java/clipper2/TestOffsets.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,25 @@ void TestOffsets12() { // see #873
271271
assertTrue(solution.isEmpty());
272272
}
273273

274+
@Test
275+
void TestOffsets13() { // see #965
276+
Path64 subject1 = new Path64(List.of(new Point64(0, 0), new Point64(0, 10), new Point64(10, 0)));
277+
double delta = 2;
278+
279+
Paths64 subjects1 = new Paths64();
280+
subjects1.add(subject1);
281+
Paths64 solution1 = Clipper.InflatePaths(subjects1, delta, JoinType.Miter, EndType.Polygon);
282+
long area1 = Math.round(Math.abs(Clipper.Area(solution1)));
283+
assertEquals(122L, area1);
284+
285+
Paths64 subjects2 = new Paths64();
286+
subjects2.add(subject1);
287+
subjects2.add(new Path64(List.of(new Point64(0, 20)))); // single-point path should not change output
288+
Paths64 solution2 = Clipper.InflatePaths(subjects2, delta, JoinType.Miter, EndType.Polygon);
289+
long area2 = Math.round(Math.abs(Clipper.Area(solution2)));
290+
assertEquals(122L, area2);
291+
}
292+
274293
private static Point64 midPoint(Point64 p1, Point64 p2) {
275294
Point64 result = new Point64();
276295
result.setX((p1.x + p2.x) / 2);

0 commit comments

Comments
 (0)