Skip to content

Commit

Permalink
6e15ba0 added support for variable offsets
Browse files Browse the repository at this point in the history
  • Loading branch information
micycle1 committed Jan 22, 2024
1 parent c9676bf commit ee017db
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 65 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>micycle</groupId>
<artifactId>clipper2</artifactId>
<version>1.2.2</version>
<version>1.2.4</version>
<name>Clipper2</name>
<properties>
<jmh.version>1.36</jmh.version>
Expand Down
10 changes: 7 additions & 3 deletions src/main/java/clipper2/engine/ClipperBase.java
Original file line number Diff line number Diff line change
Expand Up @@ -2601,18 +2601,22 @@ private void ProcessHorzJoins() {
SetOwner(or2, or1);
} else if (Path1InsidePath2(or1.pts, or2.pts)) {
SetOwner(or1, or2);
if (or1.splits == null) {
or1.splits = new ArrayList<>();
}
or1.splits.add(or2.idx); // (#520)
} else {
if (or1.splits == null) {
or1.splits = new ArrayList<>();
}
or1.splits.add(or2.idx); // (#498)
or2.owner = or1;
}
} else {
}
else {
or2.owner = or1;
}

outrecList.add(or2);
outrecList.add(or2); // NOTE removed in 6e15ba0, but then fails tests
} else {
or2.pts = null;
if (usingPolytree) {
Expand Down
172 changes: 111 additions & 61 deletions src/main/java/clipper2/offset/ClipperOffset.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

import static clipper2.core.InternalClipper.DEFAULT_ARC_TOLERANCE;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import clipper2.Clipper;
import clipper2.core.ClipType;
import clipper2.core.FillRule;
Expand All @@ -17,10 +21,6 @@
import tangible.OutObject;
import tangible.RefObject;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
* Geometric offsetting refers to the process of creating parallel curves that
* are offset a specified distance from their primary curves.
Expand All @@ -41,12 +41,13 @@
*/
public class ClipperOffset {

private static double TOLERANCE = 1.0E-12;

private final List<Group> groupList = new ArrayList<>();
private final PathD normals = new PathD();
private final Paths64 solution = new Paths64();
private double groupDelta; // *0.5 for open paths; *-1.0 for negative areas
private double delta;
private double absGroupDelta;
private double mitLimSqr;
private double stepsPerRad;
private double stepSin;
Expand All @@ -58,6 +59,7 @@ public class ClipperOffset {
private double miterLimit;
private boolean preserveCollinear;
private boolean reverseSolution;
private DeltaCallback64 deltaCallback;

/**
* @see #ClipperOffset(double, double, boolean, boolean)
Expand Down Expand Up @@ -93,8 +95,8 @@ public ClipperOffset() {
* Creates a ClipperOffset object, using the supplied parameters.
*
* @param miterLimit This property sets the maximum distance in multiples
* of groupDelta that vertices can be offset from
* their original positions before squaring is applied.
* of groupDelta that vertices can be offset from their
* original positions before squaring is applied.
* (Squaring truncates a miter by 'cutting it off' at 1
* × groupDelta distance from the original vertex.)
* <p>
Expand Down Expand Up @@ -203,6 +205,11 @@ public final void Execute(double delta, Paths64 solution) {
}
}

public void Execute(DeltaCallback64 deltaCallback64, Paths64 solution) {
deltaCallback = deltaCallback64;
Execute(1.0, solution);
}

public void Execute(double delta, PolyTree64 polytree) {
polytree.Clear();
ExecuteInternal(delta);
Expand Down Expand Up @@ -260,6 +267,14 @@ public final void setReverseSolution(boolean value) {
reverseSolution = value;
}

public final void setDeltaCallBack64(DeltaCallback64 callback) {
deltaCallback = callback;
}

public final DeltaCallback64 getDeltaCallBack64() {
return deltaCallback;
}

private static PointD GetUnitNormal(Point64 pt1, Point64 pt2) {
double dx = (pt2.x - pt1.x);
double dy = (pt2.y - pt1.y);
Expand Down Expand Up @@ -374,10 +389,10 @@ private void DoSquare(Group group, Path64 path, int j, int k) {
} else {
vec = GetAvgUnitVector(new PointD(-normals.get(k).y, normals.get(k).x), new PointD(normals.get(j).y, -normals.get(j).x));
}

double absDelta = Math.abs(groupDelta);
// now offset the original vertex delta units along unit vector
PointD ptQ = new PointD(path.get(j));
ptQ = TranslatePoint(ptQ, absGroupDelta * vec.x, absGroupDelta * vec.y);
ptQ = TranslatePoint(ptQ, absDelta * vec.x, absDelta * vec.y);

// get perpendicular vertices
PointD pt1 = TranslatePoint(ptQ, groupDelta * vec.y, groupDelta * -vec.x);
Expand All @@ -401,12 +416,26 @@ private void DoSquare(Group group, Path64 path, int j, int k) {
}

private void DoMiter(Group group, Path64 path, int j, int k, double cosA) {
double q = groupDelta / (cosA + 1);
final double q = groupDelta / (cosA + 1);
group.outPath.add(new Point64(path.get(j).x + (normals.get(k).x + normals.get(j).x) * q,
path.get(j).y + (normals.get(k).y + normals.get(j).y) * q));
}

private void DoRound(Group group, Path64 path, int j, int k, double angle) {
if (deltaCallback != null) {
// 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) * DEFAULT_ARC_TOLERANCE;
double stepsPer360 = Math.PI / Math.acos(1 - arcTol / absDelta);
stepSin = Math.sin((2 * Math.PI) / stepsPer360);
stepCos = Math.cos((2 * Math.PI) / stepsPer360);
if (groupDelta < 0.0) {
stepSin = -stepSin;
}
stepsPerRad = stepsPer360 / (2 * Math.PI);
}

Point64 pt = path.get(j);
PointD offsetVec = new PointD(normals.get(k).x * groupDelta, normals.get(k).y * groupDelta);
if (j == k) {
Expand Down Expand Up @@ -449,36 +478,37 @@ private void OffsetPoint(Group group, Path64 path, int j, RefObject<Integer> k)
sinA = -1.0;
}

if (cosA > 0.99) // almost straight - less than 8 degrees
{
group.outPath.add(GetPerpendic(path.get(j), normals.get(k.argValue)));
if (cosA < 0.9998) { // greater than 1 degree (#424)
group.outPath.add(GetPerpendic(path.get(j), normals.get(j))); // (#418)
}
} else if (cosA > -0.99 && (sinA * groupDelta < 0)) // is concave
{
if (deltaCallback != null) {
groupDelta = deltaCallback.calculate(path, normals, j, k.argValue);
}
if (Math.abs(groupDelta) < TOLERANCE) {
group.outPath.add(path.get(j));
return;
}

if (cosA > 0.99) {
DoMiter(group, path, j, k.argValue, cosA);
} else if (cosA > -0.99 && (sinA * groupDelta < 0)) {
// is concave
group.outPath.add(GetPerpendic(path.get(j), normals.get(k.argValue)));
// this extra point is the only (simple) way to ensure that
// path reversals are fully cleaned with the trailing clipper
group.outPath.add(path.get(j)); // (#405)
group.outPath.add(GetPerpendic(path.get(j), normals.get(j)));
} else if (joinType == JoinType.Round) {
DoRound(group, path, j, k.argValue, Math.atan2(sinA, cosA));
} else if (joinType == JoinType.Miter) {
// miter unless the angle is so acute the miter would exceeds ML
if (cosA > mitLimSqr - 1) {
DoMiter(group, path, j, k.argValue, cosA);
} else {
DoSquare(group, path, j, k.argValue);
}
}
// don't bother squaring angles that deviate < ~20 degrees because
// squaring will be indistinguishable from mitering and just be a lot slower
else if (cosA > 0.9) {
DoMiter(group, path, j, k.argValue, cosA);
} else {
} else if (joinType == JoinType.Square) {
// angle less than 8 degrees or a squared join
DoSquare(group, path, j, k.argValue);
} else {
DoRound(group, path, j, k.argValue, Math.atan2(sinA, cosA));
}

k.argValue = j;
}

Expand All @@ -503,19 +533,28 @@ private void OffsetOpenPath(Group group, Path64 path) {
group.outPath = new Path64();
int highI = path.size() - 1;

if (deltaCallback != null) {
groupDelta = deltaCallback.calculate(path, normals, 0, 0);
}

// do the line start cap
switch (this.endType) {
case Butt :
group.outPath
.add(new Point64(path.get(0).x - normals.get(0).x * groupDelta, path.get(0).y - normals.get(0).y * groupDelta));
group.outPath.add(GetPerpendic(path.get(0), normals.get(0)));
break;
case Round :
DoRound(group, path, 0, 0, Math.PI);
break;
default :
DoSquare(group, path, 0, 0);
break;
if (Math.abs(groupDelta) < TOLERANCE) {
group.outPath.add(path.get(0));
} else {
// do the line start cap
switch (this.endType) {
case Butt :
group.outPath
.add(new Point64(path.get(0).x - normals.get(0).x * groupDelta, path.get(0).y - normals.get(0).y * groupDelta));
group.outPath.add(GetPerpendic(path.get(0), normals.get(0)));
break;
case Round :
DoRound(group, path, 0, 0, Math.PI);
break;
default :
DoSquare(group, path, 0, 0);
break;
}
}

// offset the left side going forward
Expand All @@ -530,19 +569,26 @@ private void OffsetOpenPath(Group group, Path64 path) {
}
normals.set(0, normals.get(highI));

if (deltaCallback != null) {
groupDelta = deltaCallback.calculate(path, normals, highI, highI);
}
// do the line end cap
switch (this.endType) {
case Butt :
group.outPath.add(new Point64(path.get(highI).x - normals.get(highI).x * groupDelta,
path.get(highI).y - normals.get(highI).y * groupDelta));
group.outPath.add(GetPerpendic(path.get(highI), normals.get(highI)));
break;
case Round :
DoRound(group, path, highI, highI, Math.PI);
break;
default :
DoSquare(group, path, highI, highI);
break;
if (Math.abs(groupDelta) < TOLERANCE) {
group.outPath.add(path.get(highI));
} else {
switch (this.endType) {
case Butt :
group.outPath.add(new Point64(path.get(highI).x - normals.get(highI).x * groupDelta,
path.get(highI).y - normals.get(highI).y * groupDelta));
group.outPath.add(GetPerpendic(path.get(highI), normals.get(highI)));
break;
case Round :
DoRound(group, path, highI, highI, Math.PI);
break;
default :
DoSquare(group, path, highI, highI);
break;
}
}

// offset the left side going back
Expand All @@ -556,8 +602,10 @@ private void OffsetOpenPath(Group group, Path64 path) {

private void DoGroupOffset(Group group) {
if (group.endType == EndType.Polygon) {
// the lowermost polygon must be an outer polygon. So we can use that as the
// designated orientation for outer polygons (needed for tidy-up clipping)
/*
* The lowermost polygon must be an outer polygon. So we can use that as the
* designated orientation for outer polygons (needed for tidy-up clipping)
*/
OutObject<Integer> lowestIdx = new OutObject<>();
OutObject<Rect64> grpBounds = new OutObject<>();
GetBoundsAndLowestPolyIdx(group.inPaths, lowestIdx, grpBounds);
Expand All @@ -576,18 +624,20 @@ private void DoGroupOffset(Group group) {
group.pathsReversed = false;
this.groupDelta = Math.abs(this.delta) * 0.5;
}
this.absGroupDelta = Math.abs(this.groupDelta);
double absDelta = Math.abs(this.groupDelta);
this.joinType = group.joinType;
this.endType = group.endType;

// calculate a sensible number of steps (for 360 deg for the given offset
if (group.joinType == JoinType.Round || group.endType == EndType.Round) {
// arcTol - when fArcTolerance is undefined (0), the amount of
// curve imprecision that's allowed is based on the size of the
// offset (delta). Obviously very large offsets will almost always
// require much less precision. See also offset_triginometry2.svg
double arcTol = arcTolerance > 0.01 ? arcTolerance : Math.log10(2 + this.absGroupDelta) * DEFAULT_ARC_TOLERANCE;
double stepsPer360 = Math.PI / Math.acos(1 - arcTol / absGroupDelta);
if (deltaCallback == null && (group.joinType == JoinType.Round || group.endType == EndType.Round)) {
/*
* arcTol - when fArcTolerance is undefined (0), the amount of curve imprecision
* that's allowed is based on the size of the offset (delta). Obviously very
* large offsets will almost always require much less precision. See also
* offset_triginometry2.svg
*/
double arcTol = arcTolerance > 0.01 ? arcTolerance : Math.log10(2 + absDelta) * DEFAULT_ARC_TOLERANCE;
double stepsPer360 = Math.PI / Math.acos(1 - arcTol / absDelta);
stepSin = Math.sin((2 * Math.PI) / stepsPer360);
stepCos = Math.cos((2 * Math.PI) / stepsPer360);
if (groupDelta < 0.0) {
Expand All @@ -609,7 +659,7 @@ private void DoGroupOffset(Group group) {
group.outPath = new Path64();
// single vertex so build a circle or square ...
if (group.endType == EndType.Round) {
double r = this.absGroupDelta;
double r = absDelta;
group.outPath = Clipper.Ellipse(path.get(0), r, r);
} else {
int d = (int) Math.ceil(this.groupDelta);
Expand Down
37 changes: 37 additions & 0 deletions src/main/java/clipper2/offset/DeltaCallback64.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package clipper2.offset;

import clipper2.core.Path64;
import clipper2.core.PathD;

/**
* Functional interface for calculating a variable delta during polygon
* offsetting.
* <p>
* Implementations of this interface define how to calculate the delta (the
* amount of offset) to apply at each point in a polygon during an offset
* operation. The offset can vary from point to point, allowing for variable
* offsetting.
*/
@FunctionalInterface
public interface DeltaCallback64 {
/**
* Calculates the delta (offset) for a given point in the polygon path.
* <p>
* This method is used during polygon offsetting operations to determine the
* amount by which each point of the polygon should be offset.
*
* @param path The {@link Path64} object representing the original polygon
* path.
* @param path_norms The {@link PathD} object containing the normals of the
* path, which may be used to influence the delta calculation.
* @param currPt The index of the current point in the path for which the
* delta is being calculated.
* @param prevPt The index of the previous point in the path, which can be
* referenced to determine the delta based on adjacent
* segments.
* @return A {@code double} value representing the calculated delta for the
* current point. This value will be used to offset the point in the
* resulting polygon.
*/
double calculate(Path64 path, PathD path_norms, int currPt, int prevPt);
}

0 comments on commit ee017db

Please sign in to comment.