Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 55 additions & 7 deletions src/main/java/clipper2/core/InternalClipper.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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;
Expand All @@ -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);
}

/**
Expand All @@ -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);
Expand All @@ -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;
}

}
93 changes: 39 additions & 54 deletions src/main/java/clipper2/engine/ClipperBase.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand All @@ -2882,27 +2861,33 @@ 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<Integer> 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;
}
split.recursiveSplit = outrec; // #599
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;
}
Expand Down
24 changes: 19 additions & 5 deletions src/main/java/clipper2/offset/Group.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
42 changes: 42 additions & 0 deletions src/test/java/clipper2/TestIsCollinear.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand Down
19 changes: 19 additions & 0 deletions src/test/java/clipper2/TestOffsets.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading