Skip to content
Merged
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
98 changes: 53 additions & 45 deletions src/main/java/clipper2/Clipper.java
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ public static void BooleanOp(ClipType clipType, @Nullable PathsD subject, @Nulla
* @see #InflatePaths(PathsD, double, JoinType, EndType, double, double, int)
*/
public static Paths64 InflatePaths(Paths64 paths, double delta, JoinType joinType, EndType endType) {
return InflatePaths(paths, delta, joinType, endType, 2.0, 0.25);
return InflatePaths(paths, delta, joinType, endType, 2.0, 0.0);
}

/**
Expand All @@ -244,14 +244,14 @@ public static Paths64 InflatePaths(Paths64 paths, double delta, JoinType joinTyp
* @see #InflatePaths(PathsD, double, JoinType, EndType, double, double, int)
*/
public static PathsD InflatePaths(PathsD paths, double delta, JoinType joinType, EndType endType, double miterLimit) {
return InflatePaths(paths, delta, joinType, endType, miterLimit, 0.25, 8);
return InflatePaths(paths, delta, joinType, endType, miterLimit, 0.0, 8);
}

/**
* @see #InflatePaths(PathsD, double, JoinType, EndType, double, double, int)
*/
public static PathsD InflatePaths(PathsD paths, double delta, JoinType joinType, EndType endType) {
return InflatePaths(paths, delta, joinType, endType, 2.0, 0.25, 8);
return InflatePaths(paths, delta, joinType, endType, 2.0, 0.0, 8);
}

/**
Expand Down Expand Up @@ -992,29 +992,33 @@ public static double PerpendicDistFromLineSqrd(Point64 pt, Point64 line1, Point6
}

public static void RDP(Path64 path, int begin, int end, double epsSqrd, List<Boolean> flags) {
int idx = 0;
double maxD = 0;
while (end > begin && path.get(begin).equals(path.get(end))) {
flags.set(end--, false);
}
for (int i = begin + 1; i < end; ++i) {
// PerpendicDistFromLineSqrd - avoids expensive Sqrt()
double d = PerpendicDistFromLineSqrd(path.get(i), path.get(begin), path.get(end));
if (d <= maxD) {
while (true) {
int idx = 0;
double maxD = 0;
while (end > begin && path.get(begin).equals(path.get(end))) {
flags.set(end--, false);
}
for (int i = begin + 1; i < end; ++i) {
// PerpendicDistFromLineSqrd - avoids expensive Sqrt()
double d = PerpendicDistFromLineSqrd(path.get(i), path.get(begin), path.get(end));
if (d <= maxD) {
continue;
}
maxD = d;
idx = i;
}
if (maxD <= epsSqrd) {
return;
}
flags.set(idx, true);
if (idx > begin + 1) {
RDP(path, begin, idx, epsSqrd, flags);
}
if (idx < end - 1) {
begin = idx;
continue;
}
maxD = d;
idx = i;
}
if (maxD <= epsSqrd) {
return;
}
flags.set(idx, true);
if (idx > begin + 1) {
RDP(path, begin, idx, epsSqrd, flags);
}
if (idx < end - 1) {
RDP(path, idx, end, epsSqrd, flags);
break;
}
}

Expand Down Expand Up @@ -1081,29 +1085,33 @@ public static Paths64 RamerDouglasPeucker(Paths64 paths, double epsilon) {
}

public static void RDP(PathD path, int begin, int end, double epsSqrd, List<Boolean> flags) {
int idx = 0;
double maxD = 0;
while (end > begin && path.get(begin).equals(path.get(end))) {
flags.set(end--, false);
}
for (int i = begin + 1; i < end; ++i) {
// PerpendicDistFromLineSqrd - avoids expensive Sqrt()
double d = PerpendicDistFromLineSqrd(path.get(i), path.get(begin), path.get(end));
if (d <= maxD) {
while (true) {
int idx = 0;
double maxD = 0;
while (end > begin && path.get(begin).equals(path.get(end))) {
flags.set(end--, false);
}
for (int i = begin + 1; i < end; ++i) {
// PerpendicDistFromLineSqrd - avoids expensive Sqrt()
double d = PerpendicDistFromLineSqrd(path.get(i), path.get(begin), path.get(end));
if (d <= maxD) {
continue;
}
maxD = d;
idx = i;
}
if (maxD <= epsSqrd) {
return;
}
flags.set(idx, true);
if (idx > begin + 1) {
RDP(path, begin, idx, epsSqrd, flags);
}
if (idx < end - 1) {
begin = idx;
continue;
}
maxD = d;
idx = i;
}
if (maxD <= epsSqrd) {
return;
}
flags.set(idx, true);
if (idx > begin + 1) {
RDP(path, begin, idx, epsSqrd, flags);
}
if (idx < end - 1) {
RDP(path, idx, end, epsSqrd, flags);
break;
}
}

Expand Down
7 changes: 5 additions & 2 deletions src/main/java/clipper2/Minkowski.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
import clipper2.core.PathsD;
import clipper2.core.Point64;

public class Minkowski {
public final class Minkowski {

private Minkowski() {
}

public static Paths64 Sum(Path64 pattern, Path64 path, boolean isClosed) {
return Clipper.Union(MinkowskiInternal(pattern, path, true, isClosed), FillRule.NonZero);
Expand Down Expand Up @@ -78,4 +81,4 @@ private static Paths64 MinkowskiInternal(Path64 pattern, Path64 path, boolean is
return result;
}

}
}
4 changes: 2 additions & 2 deletions src/main/java/clipper2/core/ClipType.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
*/
public enum ClipType {

None,
NoClip,
/** Preserves regions covered by both subject and clip polygons */
Intersection,
Comment on lines 31 to 35
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renaming the public enum value ClipType.None to NoClip is a source- and binary-incompatible API change for any consumers referencing the old constant. If the goal is to improve naming, consider keeping None as a deprecated alias (and updating internal checks to treat both None and NoClip as the no-op value), or document this explicitly as a breaking change in release notes.

Copilot uses AI. Check for mistakes.
/** Preserves regions covered by subject or clip polygons, or both polygons */
Expand All @@ -40,4 +40,4 @@ public enum ClipType {
/** Preserves regions covered by subject or clip polygons, but not both */
Xor;

}
}
6 changes: 4 additions & 2 deletions src/main/java/clipper2/core/InternalClipper.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ public final class InternalClipper {
public static final double MIN_COORD = -MAX_COORD;
private static final long Invalid64 = Long.MAX_VALUE;

Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change removes the previously public InternalClipper.DEFAULT_ARC_TOLERANCE constant. If any downstream code depends on it (even just for configuring offsets), this will be a source/binary breaking change. Consider keeping the constant (possibly deprecated) or providing an equivalent public constant in the new location so consumers have a stable reference.

Suggested change
/**
* @deprecated This constant is kept for backward compatibility.
* Prefer configuring arc tolerance via the newer APIs
* where available.
*/
@Deprecated
public static final double DEFAULT_ARC_TOLERANCE = 0.25;

Copilot uses AI. Check for mistakes.
public static final double DEFAULT_ARC_TOLERANCE = 0.25;
private static final double FLOATING_POINT_TOLERANCE = 1E-12;
// private static final double DEFAULT_MIN_EDGE_LENGTH = 0.1;

Expand Down Expand Up @@ -297,7 +296,10 @@ private static boolean productsAreEqual(long a, long b, long c, long d) {
}

private static int triSign(long x) {
return x > 0 ? 1 : (x < 0 ? -1 : 0);
if (x < 0) {
return -1;
}
return x > 1 ? 1 : 0;
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

triSign is intended to return -1/0/1 based on the sign of x, but the current implementation returns 0 for x == 1 (because it checks x > 1). This will make productsAreEqual treat some positive products as having a zero sign (e.g., 110 vs 25), causing InternalClipper.IsCollinear to return false for collinear points. Update the condition to treat all x > 0 as positive.

Suggested change
return x > 1 ? 1 : 0;
return x > 0 ? 1 : 0;

Copilot uses AI. Check for mistakes.
}

}
13 changes: 12 additions & 1 deletion src/main/java/clipper2/core/Rect64.java
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,17 @@ public boolean Contains(Rect64 rec) {
return rec.left >= left && rec.right <= right && rec.top >= top && rec.bottom <= bottom;
}

public static Rect64 opAdd(Rect64 lhs, Rect64 rhs) {
if (!lhs.IsValid()) {
return rhs.clone();
}
if (!rhs.IsValid()) {
return lhs.clone();
}
return new Rect64(Math.min(lhs.left, rhs.left), Math.min(lhs.top, rhs.top), Math.max(lhs.right, rhs.right),
Math.max(lhs.bottom, rhs.bottom));
}

@Override
public Rect64 clone() {
Rect64 varCopy = new Rect64();
Expand All @@ -102,4 +113,4 @@ public Rect64 clone() {

return varCopy;
}
}
}
49 changes: 30 additions & 19 deletions src/main/java/clipper2/engine/ClipperBase.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
*/
abstract class ClipperBase {

private ClipType cliptype = ClipType.None;
private ClipType cliptype = ClipType.NoClip;
private FillRule fillrule = FillRule.EvenOdd;
private Active actives = null;
private Active sel = null;
Expand Down Expand Up @@ -1670,7 +1670,7 @@ private void AdjustCurrXAndCopyToSEL(long topY) {
}

protected final void ExecuteInternal(ClipType ct, FillRule fillRule) {
if (ct == ClipType.None) {
if (ct == ClipType.NoClip) {
return;
}
fillrule = fillRule;
Expand Down Expand Up @@ -2723,25 +2723,36 @@ private void DoSplitOp(OutRec outrec, OutPt splitOp) {
prevOp.next = newOp2;
}

if (absArea2 > 1 && (absArea2 > absArea1 || ((area2 > 0) == (area1 > 0)))) {
OutRec newOutRec = NewOutRec();
newOutRec.owner = outrec.owner;
splitOp.outrec = newOutRec;
splitOp.next.outrec = newOutRec;
if (absArea2 <= 1 || (!(absArea2 > absArea1) && ((area2 > 0) != (area1 > 0)))) {
return;
}

if (usingPolytree) {
if (outrec.splits == null) {
outrec.splits = new ArrayList<>();
}
outrec.splits.add(newOutRec.idx);
}
OutRec newOutRec = NewOutRec();
newOutRec.owner = outrec.owner;
splitOp.outrec = newOutRec;
splitOp.next.outrec = newOutRec;

OutPt newOp = new OutPt(ip, newOutRec);
newOp.prev = splitOp.next;
newOp.next = splitOp;
newOutRec.pts = newOp;
splitOp.prev = newOp;
splitOp.next.next = newOp;

OutPt newOp = new OutPt(ip, newOutRec);
newOp.prev = splitOp.next;
newOp.next = splitOp;
newOutRec.pts = newOp;
splitOp.prev = newOp;
splitOp.next.next = newOp;
if (!usingPolytree) {
return;
}

if (Path1InsidePath2(prevOp, newOp)) {
if (newOutRec.splits == null) {
newOutRec.splits = new ArrayList<>();
}
newOutRec.splits.add(outrec.idx);
} else {
if (outrec.splits == null) {
outrec.splits = new ArrayList<>();
}
outrec.splits.add(newOutRec.idx);
}
// else { splitOp = null; splitOp.next = null; }
}
Expand Down
15 changes: 8 additions & 7 deletions src/main/java/clipper2/engine/ClipperD.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
public class ClipperD extends ClipperBase {

private static final String PRECISION_RANGE_ERROR = "Error: Precision is out of range.";
private double scale;
private double invScale;

Expand All @@ -28,7 +29,7 @@ public ClipperD() {
*/
public ClipperD(int roundingDecimalPrecision) {
if (roundingDecimalPrecision < -8 || roundingDecimalPrecision > 8) {
throw new IllegalArgumentException("Error - RoundingDecimalPrecision exceeds the allowed range.");
throw new IllegalArgumentException(PRECISION_RANGE_ERROR);
}
scale = Math.pow(10, roundingDecimalPrecision);
invScale = 1 / scale;
Expand Down Expand Up @@ -124,13 +125,13 @@ public boolean Execute(ClipType clipType, FillRule fillRule, PolyTreeD polytree,
if (!success) {
return false;
}
if (!oPaths.isEmpty()) {
openPaths.ensureCapacity(oPaths.size());
for (Path64 path : oPaths) {
openPaths.add(Clipper.ScalePathD(path, invScale));
}
if (oPaths.isEmpty()) {
return true;
}
openPaths.ensureCapacity(oPaths.size());
for (Path64 path : oPaths) {
openPaths.add(Clipper.ScalePathD(path, invScale));
}

return true;
}

Expand Down
23 changes: 11 additions & 12 deletions src/main/java/clipper2/offset/ClipperOffset.java
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@
*/
public class ClipperOffset {

private static double TOLERANCE = 1.0E-12;
private static final double TOLERANCE = 1.0E-12;
private static final double ARC_CONST = 0.002;

private final List<Group> groupList = new ArrayList<>();
private Path64 pathOut = new Path64();
Expand Down Expand Up @@ -105,7 +106,7 @@ public ClipperOffset(double miterLimit, double arcTolerance) {
* @see #ClipperOffset(double, double, boolean, boolean)
*/
public ClipperOffset(double miterLimit) {
this(miterLimit, 0.25, false, false);
this(miterLimit, 0.0, false, false);
}

/**
Expand All @@ -114,7 +115,7 @@ public ClipperOffset(double miterLimit) {
* @see #ClipperOffset(double, double, boolean, boolean)
*/
public ClipperOffset() {
this(2.0, 0.25, false, false);
this(2.0, 0.0, false, false);
}

/**
Expand Down Expand Up @@ -151,7 +152,8 @@ public ClipperOffset() {
* {{@link #AddPath(Path64, JoinType, EndType)
* AddPath()} and
* {@link #AddPaths(Paths64, JoinType, EndType)
* AddPaths()}. The default arcTolerance is 0.25.
* AddPaths()}. The default arcTolerance is 0.0, which
* enables automatic scaling (offset radius / 500).
* @param preserveCollinear When adjacent edges are collinear in closed path
* solutions, the common vertex can safely be removed
* to simplify the solution without altering path
Expand Down Expand Up @@ -460,7 +462,7 @@ private void DoRound(Path64 path, int j, int k, double angle) {
// when deltaCallback is assigned, groupDelta won't be constant,
// so we'll need to do the following calculations for *every* vertex.
double absDelta = Math.abs(groupDelta);
double arcTol = arcTolerance > 0.01 ? arcTolerance : Math.log10(2 + absDelta) * InternalClipper.DEFAULT_ARC_TOLERANCE;
double arcTol = arcTolerance > 0.01 ? arcTolerance : absDelta * ARC_CONST;
double stepsPer360 = Math.PI / Math.acos(1 - arcTol / absDelta);
stepSin = Math.sin((2 * Math.PI) / stepsPer360);
stepCos = Math.cos((2 * Math.PI) / stepsPer360);
Expand Down Expand Up @@ -530,13 +532,10 @@ private int OffsetPoint(Group group, Path64 path, int j, int k) {

if (cosA > -0.999 && (sinA * groupDelta < 0)) { // test for concavity first (#593)
// is concave
// Insert 3 points so concave joins create regions that the trailing union
// operation can cleanly remove, including over-shrunk path reversals.
pathOut.add(GetPerpendic(path.get(j), normals.get(k)));
// this extra point is the only simple way to ensure that path reversals
// (ie over-shrunk paths) are fully cleaned out with the trailing union op.
// However it's probably safe to skip this whenever an angle is almost flat.
if (cosA < 0.99) {
pathOut.add(path.get(j)); // (#405)
}
pathOut.add(path.get(j)); // (#405, #873, #916)
pathOut.add(GetPerpendic(path.get(j), normals.get(j)));
} else if (cosA > 0.999 && joinType != JoinType.Round) {
// almost straight - less than 2.5 degree (#424, #482, #526 & #724)
Expand Down Expand Up @@ -682,7 +681,7 @@ private void DoGroupOffset(Group group) {
// arcTol - when arcTolerance is undefined (0) then curve imprecision
// will be relative to the size of the offset (delta). Obviously very
// large offsets will almost always require much less precision.
double arcTol = arcTolerance > 0.01 ? arcTolerance : Math.log10(2 + absDelta) * InternalClipper.DEFAULT_ARC_TOLERANCE;
double arcTol = arcTolerance > 0.01 ? arcTolerance : absDelta * ARC_CONST;
double stepsPer360 = Math.PI / Math.acos(1 - arcTol / absDelta);
stepSin = Math.sin((2 * Math.PI) / stepsPer360);
stepCos = Math.cos((2 * Math.PI) / stepsPer360);
Expand Down
Loading