12 | * In general, this means developers will have to read the documentation to
13 | * determine when a null value is acceptable and whether it is necessary to
14 | * check for a null value.
15 | *
16 | * This annotation is useful mostly for overriding a {@link Nonnull} annotation.
17 | * Static analysis tools should generally treat the annotated items as though
18 | * they had no annotation, unless they are configured to minimize false
19 | * negatives. Use {@link CheckForNull} to indicate that the element value should
20 | * always be checked for a null value.
21 | *
22 | * When this annotation is applied to a method it applies to the method return
23 | * value.
24 | */
25 | @Documented
26 | @Retention(RetentionPolicy.RUNTIME)
27 | @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.LOCAL_VARIABLE, ElementType.TYPE_USE })
28 | public @interface Nullable {
29 |
30 | }
--------------------------------------------------------------------------------
/src/main/java/clipper2/core/ClipType.java:
--------------------------------------------------------------------------------
1 | package clipper2.core;
2 |
3 | /**
4 | * All polygon clipping is performed with a Clipper object with the specific
5 | * boolean operation indicated by the ClipType parameter passed in its Execute
6 | * method.
7 | *
8 | * With regard to open paths (polylines), clipping rules generally match those
9 | * of closed paths (polygons). However, when there are both polyline and polygon
10 | * subjects, the following clipping rules apply:
11 | *
12 | *
union operations - polylines will be clipped by any overlapping polygons
13 | * so that only non-overlapped portions will be returned in the solution,
14 | * together with solution polygons
15 | *
intersection, difference and xor operations - polylines will be clipped
16 | * by 'clip' polygons, and there will be not interaction between polylines and
17 | * any subject polygons.
18 | *
19 | *
20 | * There are four boolean operations:
21 | *
22 | *
AND (intersection) - regions covered by both subject and clip
23 | * polygons
24 | *
OR (union) - regions covered by subject or clip polygons, or both
25 | * polygons
26 | *
NOT (difference) - regions covered by subject, but not clip polygons
27 | *
XOR (exclusive or) - regions covered by subject or clip polygons, but not
28 | * both
29 | *
30 | */
31 | public enum ClipType {
32 |
33 | None,
34 | /** Preserves regions covered by both subject and clip polygons */
35 | Intersection,
36 | /** Preserves regions covered by subject or clip polygons, or both polygons */
37 | Union,
38 | /** Preserves regions covered by subject, but not clip polygons */
39 | Difference,
40 | /** Preserves regions covered by subject or clip polygons, but not both */
41 | Xor;
42 |
43 | }
--------------------------------------------------------------------------------
/src/main/java/clipper2/core/FillRule.java:
--------------------------------------------------------------------------------
1 | package clipper2.core;
2 |
3 | /**
4 | * Complex polygons are defined by one or more closed paths that set both outer
5 | * and inner polygon boundaries. But only portions of these paths (or
6 | * 'contours') may be setting polygon boundaries, so crossing a path may or may
7 | * not mean entering or exiting a 'filled' polygon region. For this reason
8 | * complex polygons require filling rules that define which polygon sub-regions
9 | * will be considered inside a given polygon, and which sub-regions will not.
10 | *
11 | * The Clipper Library supports 4 filling rules: Even-Odd, Non-Zero, Positive
12 | * and Negative. These rules are base on the winding numbers (see below) of each
13 | * polygon sub-region, which in turn are based on the orientation of each path.
14 | * Orientation is determined by the order in which vertices are declared during
15 | * path construction, and whether these vertices progress roughly clockwise or
16 | * counter-clockwise.
17 | *
18 | * By far the most widely used filling rules for polygons are EvenOdd and
19 | * NonZero, sometimes called Alternate and Winding respectively.
20 | *
21 | * https://en.wikipedia.org/wiki/Nonzero-rule
22 | */
23 | public enum FillRule {
24 |
25 | /** Only odd numbered sub-regions are filled */
26 | EvenOdd,
27 | /** Only non-zero sub-regions are filled */
28 | NonZero,
29 | /** Only sub-regions with winding counts > 0 are filled */
30 | Positive,
31 | /** Only sub-regions with winding counts < 0 are filled */
32 | Negative;
33 |
34 | }
--------------------------------------------------------------------------------
/src/main/java/clipper2/core/InternalClipper.java:
--------------------------------------------------------------------------------
1 | package clipper2.core;
2 |
3 | import clipper2.engine.PointInPolygonResult;
4 |
5 | public final class InternalClipper {
6 |
7 | public static final double MAX_COORD = Long.MAX_VALUE >> 2;
8 | public static final double MIN_COORD = -MAX_COORD;
9 | private static final long Invalid64 = Long.MAX_VALUE;
10 |
11 | public static final double DEFAULT_ARC_TOLERANCE = 0.25;
12 | private static final double FLOATING_POINT_TOLERANCE = 1E-12;
13 | // private static final double DEFAULT_MIN_EDGE_LENGTH = 0.1;
14 |
15 | private static final String PRECISION_RANGE_ERROR = "Error: Precision is out of range.";
16 |
17 | public static void CheckPrecision(int precision) {
18 | if (precision < -8 || precision > 8) {
19 | throw new IllegalArgumentException(PRECISION_RANGE_ERROR);
20 | }
21 | }
22 |
23 | private InternalClipper() {
24 | }
25 |
26 | public static boolean IsAlmostZero(double value) {
27 | return (Math.abs(value) <= FLOATING_POINT_TOLERANCE);
28 | }
29 |
30 | public static double CrossProduct(Point64 pt1, Point64 pt2, Point64 pt3) {
31 | // typecast to double to avoid potential int overflow
32 | return ((double) (pt2.x - pt1.x) * (pt3.y - pt2.y) - (double) (pt2.y - pt1.y) * (pt3.x - pt2.x));
33 | }
34 |
35 | public static double DotProduct(Point64 pt1, Point64 pt2, Point64 pt3) {
36 | // typecast to double to avoid potential int overflow
37 | return ((double) (pt2.x - pt1.x) * (pt3.x - pt2.x) + (double) (pt2.y - pt1.y) * (pt3.y - pt2.y));
38 | }
39 |
40 | public static double CrossProduct(PointD vec1, PointD vec2) {
41 | return (vec1.y * vec2.x - vec2.y * vec1.x);
42 | }
43 |
44 | public static double DotProduct(PointD vec1, PointD vec2) {
45 | return (vec1.x * vec2.x + vec1.y * vec2.y);
46 | }
47 |
48 | public static long CheckCastInt64(double val) {
49 | if ((val >= MAX_COORD) || (val <= MIN_COORD)) {
50 | return Invalid64;
51 | }
52 | return (long) Math.rint(val);
53 | }
54 |
55 | public static boolean GetIntersectPoint(Point64 ln1a, Point64 ln1b, Point64 ln2a, Point64 ln2b, /* out */ Point64 ip) {
56 | double dy1 = (ln1b.y - ln1a.y);
57 | double dx1 = (ln1b.x - ln1a.x);
58 | double dy2 = (ln2b.y - ln2a.y);
59 | double dx2 = (ln2b.x - ln2a.x);
60 |
61 | double det = dy1 * dx2 - dy2 * dx1;
62 |
63 | if (det == 0.0) {
64 | ip.x = 0;
65 | ip.y = 0;
66 | return false;
67 | }
68 |
69 | // Calculate the intersection parameter 't' along the first line segment
70 | double t = ((ln1a.x - ln2a.x) * dy2 - (ln1a.y - ln2a.y) * dx2) / det;
71 |
72 | // Determine the intersection point based on 't'
73 | if (t <= 0.0) {
74 | ip.x = ln1a.x;
75 | ip.y = ln1a.y;
76 | } else if (t >= 1.0) {
77 | ip.x = ln1b.x;
78 | ip.y = ln1b.y;
79 | } else {
80 | // avoid using constructor (and rounding too) as they affect performance //664
81 | ip.x = (long) (ln1a.x + t * dx1);
82 | ip.y = (long) (ln1a.y + t * dy1);
83 | }
84 |
85 | // Intersection found (even if clamped to endpoints)
86 | return true;
87 | }
88 |
89 | public static boolean SegsIntersect(Point64 seg1a, Point64 seg1b, Point64 seg2a, Point64 seg2b) {
90 | return SegsIntersect(seg1a, seg1b, seg2a, seg2b, false);
91 | }
92 |
93 | public static boolean SegsIntersect(Point64 seg1a, Point64 seg1b, Point64 seg2a, Point64 seg2b, boolean inclusive) {
94 | if (inclusive) {
95 | double res1 = CrossProduct(seg1a, seg2a, seg2b);
96 | double res2 = CrossProduct(seg1b, seg2a, seg2b);
97 | if (res1 * res2 > 0) {
98 | return false;
99 | }
100 | double res3 = CrossProduct(seg2a, seg1a, seg1b);
101 | double res4 = CrossProduct(seg2b, seg1a, seg1b);
102 | if (res3 * res4 > 0) {
103 | return false;
104 | }
105 | // ensure NOT collinear
106 | return (res1 != 0 || res2 != 0 || res3 != 0 || res4 != 0);
107 | } else {
108 | return (CrossProduct(seg1a, seg2a, seg2b) * CrossProduct(seg1b, seg2a, seg2b) < 0)
109 | && (CrossProduct(seg2a, seg1a, seg1b) * CrossProduct(seg2b, seg1a, seg1b) < 0);
110 | }
111 | }
112 |
113 | public static Point64 GetClosestPtOnSegment(Point64 offPt, Point64 seg1, Point64 seg2) {
114 | if (seg1.x == seg2.x && seg1.y == seg2.y) {
115 | return seg1;
116 | }
117 | double dx = (seg2.x - seg1.x);
118 | double dy = (seg2.y - seg1.y);
119 | double q = ((offPt.x - seg1.x) * dx + (offPt.y - seg1.y) * dy) / ((dx * dx) + (dy * dy));
120 | if (q < 0) {
121 | q = 0;
122 | } else if (q > 1) {
123 | q = 1;
124 | }
125 | return new Point64(seg1.x + Math.rint(q * dx), seg1.y + Math.rint(q * dy));
126 | }
127 |
128 | public static PointInPolygonResult PointInPolygon(Point64 pt, Path64 polygon) {
129 | int len = polygon.size(), start = 0;
130 | if (len < 3) {
131 | return PointInPolygonResult.IsOutside;
132 | }
133 |
134 | while (start < len && polygon.get(start).y == pt.y) {
135 | start++;
136 | }
137 | if (start == len) {
138 | return PointInPolygonResult.IsOutside;
139 | }
140 |
141 | double d;
142 | boolean isAbove = polygon.get(start).y < pt.y, startingAbove = isAbove;
143 | int val = 0, i = start + 1, end = len;
144 | while (true) {
145 | if (i == end) {
146 | if (end == 0 || start == 0) {
147 | break;
148 | }
149 | end = start;
150 | i = 0;
151 | }
152 |
153 | if (isAbove) {
154 | while (i < end && polygon.get(i).y < pt.y) {
155 | i++;
156 | }
157 | if (i == end) {
158 | continue;
159 | }
160 | } else {
161 | while (i < end && polygon.get(i).y > pt.y) {
162 | i++;
163 | }
164 | if (i == end) {
165 | continue;
166 | }
167 | }
168 |
169 | Point64 curr = polygon.get(i), prev;
170 | if (i > 0) {
171 | prev = polygon.get(i - 1);
172 | } else {
173 | prev = polygon.get(len - 1);
174 | }
175 |
176 | if (curr.y == pt.y) {
177 | if (curr.x == pt.x || (curr.y == prev.y && ((pt.x < prev.x) != (pt.x < curr.x)))) {
178 | return PointInPolygonResult.IsOn;
179 | }
180 | i++;
181 | if (i == start) {
182 | break;
183 | }
184 | continue;
185 | }
186 |
187 | if (pt.x < curr.x && pt.x < prev.x) {
188 | // we're only interested in edges crossing on the left
189 | } else if (pt.x > prev.x && pt.x > curr.x) {
190 | val = 1 - val; // toggle val
191 | } else {
192 | d = CrossProduct(prev, curr, pt);
193 | if (d == 0) {
194 | return PointInPolygonResult.IsOn;
195 | }
196 | if ((d < 0) == isAbove) {
197 | val = 1 - val;
198 | }
199 | }
200 | isAbove = !isAbove;
201 | i++;
202 | }
203 |
204 | if (isAbove != startingAbove) {
205 | if (i == len) {
206 | i = 0;
207 | }
208 | if (i == 0) {
209 | d = CrossProduct(polygon.get(len - 1), polygon.get(0), pt);
210 | } else {
211 | d = CrossProduct(polygon.get(i - 1), polygon.get(i), pt);
212 | }
213 | if (d == 0) {
214 | return PointInPolygonResult.IsOn;
215 | }
216 | if ((d < 0) == isAbove) {
217 | val = 1 - val;
218 | }
219 | }
220 |
221 | if (val == 0) {
222 | return PointInPolygonResult.IsOutside;
223 | }
224 | return PointInPolygonResult.IsInside;
225 | }
226 |
227 | /**
228 | * Given three points, returns true if they are collinear.
229 | */
230 | public static boolean IsCollinear(Point64 pt1, Point64 sharedPt, Point64 pt2) {
231 | long a = sharedPt.x - pt1.x;
232 | long b = pt2.y - sharedPt.y;
233 | long c = sharedPt.y - pt1.y;
234 | long d = pt2.x - sharedPt.x;
235 | // use the exact‐arithmetic product test
236 | return productsAreEqual(a, b, c, d);
237 | }
238 |
239 | /**
240 | * Holds the low‐ and high‐64 bits of a 128‐bit product.
241 | */
242 | private static class MultiplyUInt64Result {
243 | public final long lo64;
244 | public final long hi64;
245 |
246 | public MultiplyUInt64Result(long lo64, long hi64) {
247 | this.lo64 = lo64;
248 | this.hi64 = hi64;
249 | }
250 | }
251 |
252 | /**
253 | * Multiply two unsigned 64‐bit quantities (given in signed longs) and return
254 | * the full 128‐bit result as hi/lo.
255 | */
256 | private static MultiplyUInt64Result multiplyUInt64(long a, long b) {
257 | // mask to extract low 32 bits
258 | final long MASK_32 = 0xFFFFFFFFL;
259 | long aLow = a & MASK_32;
260 | long aHigh = a >>> 32;
261 | long bLow = b & MASK_32;
262 | long bHigh = b >>> 32;
263 |
264 | long x1 = aLow * bLow;
265 | long x2 = aHigh * bLow + (x1 >>> 32);
266 | long x3 = aLow * bHigh + (x2 & MASK_32);
267 |
268 | long lo64 = ((x3 & MASK_32) << 32) | (x1 & MASK_32);
269 | long hi64 = aHigh * bHigh + (x2 >>> 32) + (x3 >>> 32);
270 |
271 | return new MultiplyUInt64Result(lo64, hi64);
272 | }
273 |
274 | /**
275 | * Returns true iff a*b == c*d (as 128‐bit signed products). We compare both
276 | * magnitude (via unsigned 128‐bit) and sign.
277 | */
278 | private static boolean productsAreEqual(long a, long b, long c, long d) {
279 | // unsigned absolute values; note: -Long.MIN_VALUE == Long.MIN_VALUE
280 | long absA = a < 0 ? -a : a;
281 | long absB = b < 0 ? -b : b;
282 | long absC = c < 0 ? -c : c;
283 | long absD = d < 0 ? -d : d;
284 |
285 | MultiplyUInt64Result p1 = multiplyUInt64(absA, absB);
286 | MultiplyUInt64Result p2 = multiplyUInt64(absC, absD);
287 |
288 | int signAB = triSign(a) * triSign(b);
289 | int signCD = triSign(c) * triSign(d);
290 |
291 | return p1.lo64 == p2.lo64 && p1.hi64 == p2.hi64 && signAB == signCD;
292 | }
293 |
294 | private static int triSign(long x) {
295 | return x > 0 ? 1 : (x < 0 ? -1 : 0);
296 | }
297 |
298 | }
--------------------------------------------------------------------------------
/src/main/java/clipper2/core/Path64.java:
--------------------------------------------------------------------------------
1 | package clipper2.core;
2 |
3 | import java.util.ArrayList;
4 | import java.util.Arrays;
5 | import java.util.List;
6 |
7 | /**
8 | * This structure contains a sequence of Point64 vertices defining a single
9 | * contour (see also terminology). Paths may be open and represent a series of
10 | * line segments defined by 2 or more vertices, or they may be closed and
11 | * represent polygons. Whether or not a path is open depends on its context.
12 | * Closed paths may be 'outer' contours, or they may be 'hole' contours, and
13 | * this usually depends on their orientation (whether arranged roughly
14 | * clockwise, or arranged counter-clockwise).
15 | */
16 | @SuppressWarnings("serial")
17 | public class Path64 extends ArrayList {
18 |
19 | public Path64() {
20 | super();
21 | }
22 |
23 | public Path64(int n) {
24 | super(n);
25 | }
26 |
27 | public Path64(List path) {
28 | super(path);
29 | }
30 |
31 | public Path64(Point64... path) {
32 | super(Arrays.asList(path));
33 | }
34 |
35 | @Override
36 | public String toString() {
37 | String s = "";
38 | for (Point64 p : this) {
39 | s = s + p.toString() + " ";
40 | }
41 | return s;
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/src/main/java/clipper2/core/PathD.java:
--------------------------------------------------------------------------------
1 | package clipper2.core;
2 |
3 | import java.util.ArrayList;
4 | import java.util.List;
5 |
6 | /**
7 | * This structure contains a sequence of PointD vertices defining a single
8 | * contour (see also terminology). Paths may be open and represent a series of
9 | * line segments defined by 2 or more vertices, or they may be closed and
10 | * represent polygons. Whether or not a path is open depends on its context.
11 | * Closed paths may be 'outer' contours, or they may be 'hole' contours, and
12 | * this usually depends on their orientation (whether arranged roughly
13 | * clockwise, or arranged counter-clockwise).
14 | */
15 | @SuppressWarnings("serial")
16 | public class PathD extends ArrayList {
17 |
18 | public PathD() {
19 | super();
20 | }
21 |
22 | public PathD(int n) {
23 | super(n);
24 | }
25 |
26 | public PathD(List path) {
27 | super(path);
28 | }
29 |
30 | @Override
31 | public String toString() {
32 | String s = "";
33 | for (PointD p : this) {
34 | s = s + p.toString() + " ";
35 | }
36 | return s;
37 | }
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/src/main/java/clipper2/core/PathType.java:
--------------------------------------------------------------------------------
1 | package clipper2.core;
2 |
3 | public enum PathType {
4 |
5 | Subject, Clip;
6 |
7 | }
--------------------------------------------------------------------------------
/src/main/java/clipper2/core/Paths64.java:
--------------------------------------------------------------------------------
1 | package clipper2.core;
2 |
3 | import java.util.ArrayList;
4 | import java.util.Arrays;
5 | import java.util.List;
6 |
7 | /**
8 | * Paths64 represent one or more Path64 structures. While a single path can
9 | * represent a simple polygon, multiple paths are usually required to define
10 | * complex polygons that contain one or more holes.
11 | */
12 | @SuppressWarnings("serial")
13 | public class Paths64 extends ArrayList {
14 |
15 | public Paths64() {
16 | super();
17 | }
18 |
19 | public Paths64(int n) {
20 | super(n);
21 | }
22 |
23 | public Paths64(List paths) {
24 | super(paths);
25 | }
26 |
27 | public Paths64(Path64... paths) {
28 | super(Arrays.asList(paths));
29 | }
30 |
31 | @Override
32 | public String toString() {
33 | String s = "";
34 | for (Path64 p : this) {
35 | s = s + p.toString() + "\n";
36 | }
37 | return s;
38 | }
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/src/main/java/clipper2/core/PathsD.java:
--------------------------------------------------------------------------------
1 | package clipper2.core;
2 |
3 | import java.util.ArrayList;
4 | import java.util.List;
5 |
6 | /**
7 | * PathsD represent one or more PathD structures. While a single path can
8 | * represent a simple polygon, multiple paths are usually required to define
9 | * complex polygons that contain one or more holes.
10 | */
11 | @SuppressWarnings("serial")
12 | public class PathsD extends ArrayList {
13 |
14 | public PathsD() {
15 | super();
16 | }
17 |
18 | public PathsD(int n) {
19 | super(n);
20 | }
21 |
22 | public PathsD(List paths) {
23 | super(paths);
24 | }
25 |
26 | @Override
27 | public String toString() {
28 | String s = "";
29 | for (PathD p : this) {
30 | s = s + p.toString() + "\n";
31 | }
32 | return s;
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/src/main/java/clipper2/core/Point64.java:
--------------------------------------------------------------------------------
1 | package clipper2.core;
2 |
3 | /**
4 | * The Point64 structure is used to represent a single vertex (or coordinate) in
5 | * a series that together make a path or contour (see Path64). Closed paths are
6 | * usually referred to as polygons, and open paths are referred to as lines or
7 | * polylines.
8 | *
9 | * All coordinates are represented internally using integers as this is the only
10 | * way to ensure numerical robustness. While the library also accepts floating
11 | * point coordinates (see PointD), these will be converted into integers
12 | * internally (using user specified scaling).
13 | */
14 | public final class Point64 {
15 |
16 | public long x;
17 | public long y;
18 |
19 | public Point64() {
20 | }
21 |
22 | public Point64(Point64 pt) {
23 | this.x = pt.x;
24 | this.y = pt.y;
25 | }
26 |
27 | public Point64(long x, long y) {
28 | this.x = x;
29 | this.y = y;
30 | }
31 |
32 | public Point64(double x, double y) {
33 | this.x = (long) Math.rint(x);
34 | this.y = (long) Math.rint(y);
35 | }
36 |
37 | public Point64(PointD pt) {
38 | x = (long) Math.rint(pt.x);
39 | y = (long) Math.rint(pt.y);
40 | }
41 |
42 | public Point64(Point64 pt, double scale) {
43 | x = (long) Math.rint(pt.x * scale);
44 | y = (long) Math.rint(pt.y * scale);
45 | }
46 |
47 | public Point64(PointD pt, double scale) {
48 | x = (long) Math.rint(pt.x * scale);
49 | y = (long) Math.rint(pt.y * scale);
50 | }
51 |
52 | public void setX(double x) {
53 | this.x = (long) Math.rint(x);
54 | }
55 |
56 | public void setY(double y) {
57 | this.y = (long) Math.rint(y);
58 | }
59 |
60 | /**
61 | * Set x,y of this point equal to another.
62 | * @param other
63 | */
64 | public void set(Point64 other) {
65 | this.x = other.x;
66 | this.y = other.y;
67 | }
68 |
69 | public boolean opEquals(Point64 o) {
70 | return x == o.x && y == o.y;
71 | }
72 |
73 | public static boolean opEquals(Point64 lhs, Point64 rhs) {
74 | return lhs.x == rhs.x && lhs.y == rhs.y;
75 | }
76 |
77 | public boolean opNotEquals(Point64 o) {
78 | return x != o.x || y != o.y;
79 | }
80 |
81 | public static boolean opNotEquals(Point64 lhs, Point64 rhs) {
82 | return lhs.x != rhs.x || lhs.y != rhs.y;
83 | }
84 |
85 | public static Point64 opAdd(Point64 lhs, Point64 rhs) {
86 | return new Point64(lhs.x + rhs.x, lhs.y + rhs.y);
87 | }
88 |
89 | public static Point64 opSubtract(Point64 lhs, Point64 rhs) {
90 | return new Point64(lhs.x - rhs.x, lhs.y - rhs.y);
91 | }
92 |
93 | @Override
94 | public final String toString() {
95 | return String.format("(%1$s,%2$s) ", x, y); // nb: trailing space
96 | }
97 |
98 | @Override
99 | public final boolean equals(Object obj) {
100 | if (obj instanceof Point64) {
101 | Point64 p = (Point64) obj;
102 | return opEquals(this, p);
103 | }
104 | return false;
105 | }
106 |
107 | @Override
108 | public int hashCode() {
109 | return Long.hashCode(x * 31 + y);
110 | }
111 |
112 | @Override
113 | public Point64 clone() {
114 | return new Point64(x, y);
115 | }
116 | }
--------------------------------------------------------------------------------
/src/main/java/clipper2/core/PointD.java:
--------------------------------------------------------------------------------
1 | package clipper2.core;
2 |
3 | /**
4 | * The PointD structure is used to represent a single floating point coordinate.
5 | * A series of these coordinates forms a PathD structure.
6 | */
7 | public final class PointD {
8 |
9 | public double x;
10 | public double y;
11 |
12 | public PointD() {
13 | }
14 |
15 | public PointD(PointD pt) {
16 | x = pt.x;
17 | y = pt.y;
18 | }
19 |
20 | public PointD(Point64 pt) {
21 | x = pt.x;
22 | y = pt.y;
23 | }
24 |
25 | public PointD(PointD pt, double scale) {
26 | x = pt.x * scale;
27 | y = pt.y * scale;
28 | }
29 |
30 | public PointD(Point64 pt, double scale) {
31 | x = pt.x * scale;
32 | y = pt.y * scale;
33 | }
34 |
35 | public PointD(long x, long y) {
36 | this.x = x;
37 | this.y = y;
38 | }
39 |
40 | public PointD(double x, double y) {
41 | this.x = x;
42 | this.y = y;
43 | }
44 |
45 | public void Negate() {
46 | x = -x;
47 | y = -y;
48 | }
49 |
50 | @Override
51 | public final String toString() {
52 | return String.format("(%1$f,%2$f) ", x, y);
53 | }
54 |
55 | public static boolean opEquals(PointD lhs, PointD rhs) {
56 | return InternalClipper.IsAlmostZero(lhs.x - rhs.x) && InternalClipper.IsAlmostZero(lhs.y - rhs.y);
57 | }
58 |
59 | public static boolean opNotEquals(PointD lhs, PointD rhs) {
60 | return !InternalClipper.IsAlmostZero(lhs.x - rhs.x) || !InternalClipper.IsAlmostZero(lhs.y - rhs.y);
61 | }
62 |
63 | @Override
64 | public final boolean equals(Object obj) {
65 | if (obj instanceof PointD) {
66 | PointD p = (PointD) obj;
67 | return opEquals(this, p);
68 | }
69 | return false;
70 | }
71 |
72 | @Override
73 | public final int hashCode() {
74 | return Double.hashCode(x * 31 + y);
75 | }
76 |
77 | @Override
78 | public PointD clone() {
79 | return new PointD(x, y);
80 | }
81 | }
--------------------------------------------------------------------------------
/src/main/java/clipper2/core/Rect64.java:
--------------------------------------------------------------------------------
1 | package clipper2.core;
2 |
3 | public final class Rect64 {
4 |
5 | public long left;
6 | public long top;
7 | public long right;
8 | public long bottom;
9 | private static final String InvalidRect = "Invalid Rect64 assignment";
10 |
11 | public Rect64() {
12 | }
13 |
14 | public Rect64(long l, long t, long r, long b) {
15 | if (r < l || b < t) {
16 | throw new IllegalArgumentException(InvalidRect);
17 | }
18 | left = l;
19 | top = t;
20 | right = r;
21 | bottom = b;
22 | }
23 |
24 | public Rect64(boolean isValid) {
25 | if (isValid) {
26 | left = 0;
27 | top = 0;
28 | right = 0;
29 | bottom = 0;
30 | } else {
31 | left = Long.MAX_VALUE;
32 | top = Long.MAX_VALUE;
33 | right = Long.MIN_VALUE;
34 | bottom = Long.MIN_VALUE;
35 | }
36 | }
37 |
38 | public Rect64(Rect64 rec) {
39 | left = rec.left;
40 | top = rec.top;
41 | right = rec.right;
42 | bottom = rec.bottom;
43 | }
44 |
45 | public long getWidth() {
46 | return right - left;
47 | }
48 |
49 | public void setWidth(long value) {
50 | right = left + value;
51 | }
52 |
53 | public long getHeight() {
54 | return bottom - top;
55 | }
56 |
57 | public void setHeight(long value) {
58 | bottom = top + value;
59 | }
60 |
61 | public Path64 AsPath() {
62 | Path64 result = new Path64(4);
63 | result.add(new Point64(left, top));
64 | result.add(new Point64(right, top));
65 | result.add(new Point64(right, bottom));
66 | result.add(new Point64(left, bottom));
67 | return result;
68 | }
69 |
70 | public boolean IsEmpty() {
71 | return bottom <= top || right <= left;
72 | }
73 |
74 | public boolean IsValid() {
75 | return left < Long.MAX_VALUE;
76 | }
77 |
78 | public Point64 MidPoint() {
79 | return new Point64((left + right) / 2, (top + bottom) / 2);
80 | }
81 |
82 | public boolean Contains(Point64 pt) {
83 | return pt.x > left && pt.x < right && pt.y > top && pt.y < bottom;
84 | }
85 |
86 | public boolean Intersects(Rect64 rec) {
87 | return (Math.max(left, rec.left) <= Math.min(right, rec.right)) && (Math.max(top, rec.top) <= Math.min(bottom, rec.bottom));
88 | }
89 |
90 | public boolean Contains(Rect64 rec) {
91 | return rec.left >= left && rec.right <= right && rec.top >= top && rec.bottom <= bottom;
92 | }
93 |
94 | @Override
95 | public Rect64 clone() {
96 | Rect64 varCopy = new Rect64();
97 |
98 | varCopy.left = this.left;
99 | varCopy.top = this.top;
100 | varCopy.right = this.right;
101 | varCopy.bottom = this.bottom;
102 |
103 | return varCopy;
104 | }
105 | }
--------------------------------------------------------------------------------
/src/main/java/clipper2/core/RectD.java:
--------------------------------------------------------------------------------
1 | package clipper2.core;
2 |
3 | import java.util.Arrays;
4 |
5 | public final class RectD {
6 |
7 | public double left;
8 | public double top;
9 | public double right;
10 | public double bottom;
11 | private static final String InvalidRect = "Invalid RectD assignment";
12 |
13 | public RectD() {
14 | }
15 |
16 | public RectD(double l, double t, double r, double b) {
17 | if (r < l || b < t) {
18 | throw new IllegalArgumentException(InvalidRect);
19 | }
20 | left = l;
21 | top = t;
22 | right = r;
23 | bottom = b;
24 | }
25 |
26 | public RectD(RectD rec) {
27 | left = rec.left;
28 | top = rec.top;
29 | right = rec.right;
30 | bottom = rec.bottom;
31 | }
32 |
33 | public RectD(boolean isValid) {
34 | if (isValid) {
35 | left = 0;
36 | top = 0;
37 | right = 0;
38 | bottom = 0;
39 | } else {
40 | left = Double.MAX_VALUE;
41 | top = Double.MAX_VALUE;
42 | right = -Double.MAX_VALUE;
43 | bottom = -Double.MAX_VALUE;
44 | }
45 | }
46 |
47 | public double getWidth() {
48 | return right - left;
49 | }
50 |
51 | public void setWidth(double value) {
52 | right = left + value;
53 | }
54 |
55 | public double getHeight() {
56 | return bottom - top;
57 | }
58 |
59 | public void setHeight(double value) {
60 | bottom = top + value;
61 | }
62 |
63 | public boolean IsEmpty() {
64 | return bottom <= top || right <= left;
65 | }
66 |
67 | public PointD MidPoint() {
68 | return new PointD((left + right) / 2, (top + bottom) / 2);
69 | }
70 |
71 | public boolean Contains(PointD pt) {
72 | return pt.x > left && pt.x < right && pt.y > top && pt.y < bottom;
73 | }
74 |
75 | public boolean Contains(RectD rec) {
76 | return rec.left >= left && rec.right <= right && rec.top >= top && rec.bottom <= bottom;
77 | }
78 |
79 | public boolean Intersects(RectD rec) {
80 | return (Math.max(left, rec.left) < Math.min(right, rec.right)) && (Math.max(top, rec.top) < Math.min(bottom, rec.bottom));
81 | }
82 |
83 | public PathD AsPath() {
84 | PathD result = new PathD(
85 | Arrays.asList(new PointD(left, top), new PointD(right, top), new PointD(right, bottom), new PointD(left, bottom)));
86 | return result;
87 | }
88 |
89 | @Override
90 | public RectD clone() {
91 | RectD varCopy = new RectD();
92 |
93 | varCopy.left = this.left;
94 | varCopy.top = this.top;
95 | varCopy.right = this.right;
96 | varCopy.bottom = this.bottom;
97 |
98 | return varCopy;
99 | }
100 | }
--------------------------------------------------------------------------------
/src/main/java/clipper2/core/package-info.java:
--------------------------------------------------------------------------------
1 | package clipper2.core;
--------------------------------------------------------------------------------
/src/main/java/clipper2/engine/Clipper64.java:
--------------------------------------------------------------------------------
1 | package clipper2.engine;
2 |
3 | import clipper2.core.ClipType;
4 | import clipper2.core.FillRule;
5 | import clipper2.core.Path64;
6 | import clipper2.core.PathType;
7 | import clipper2.core.Paths64;
8 |
9 | /**
10 | * The Clipper class performs boolean 'clipping'. This class is very similar to
11 | * ClipperD except that coordinates passed to Clipper64 objects are of type
12 | * long instead of type double.
13 | */
14 | public class Clipper64 extends ClipperBase {
15 |
16 | public final void addPath(Path64 path, PathType polytype) {
17 | addPath(path, polytype, false);
18 | }
19 |
20 | public final void addPath(Path64 path, PathType polytype, boolean isOpen) {
21 | super.AddPath(path, polytype, isOpen);
22 | }
23 |
24 | public final void addPaths(Paths64 paths, PathType polytype) {
25 | addPaths(paths, polytype, false);
26 | }
27 |
28 | public final void addPaths(Paths64 paths, PathType polytype, boolean isOpen) {
29 | super.AddPaths(paths, polytype, isOpen);
30 | }
31 |
32 | public final void addSubject(Paths64 paths) {
33 | addPaths(paths, PathType.Subject);
34 | }
35 |
36 | public final void addOpenSubject(Paths64 paths) {
37 | addPaths(paths, PathType.Subject, true);
38 | }
39 |
40 | public final void addClip(Paths64 paths) {
41 | addPaths(paths, PathType.Clip);
42 | }
43 |
44 | /**
45 | * Once subject and clip paths have been assigned (via
46 | * {@link #addSubject(Paths64) addSubject()}, {@link #addOpenSubject(Paths64)
47 | * addOpenSubject()} and {@link #addClip(Paths64) addClip()} methods),
48 | * Execute() can then perform the specified clipping operation
49 | * (intersection, union, difference or XOR).
50 | *
51 | * The solution parameter can be either a Paths64 or a PolyTree64, though since
52 | * the Paths64 structure is simpler and more easily populated (with clipping
53 | * about 5% faster), it should generally be preferred.
54 | *
55 | * While polygons in solutions should never intersect (either with other
56 | * polygons or with themselves), they will frequently be nested such that outer
57 | * polygons will contain inner 'hole' polygons with in turn may contain outer
58 | * polygons (to any level of nesting). And given that PolyTree64 and PolyTreeD
59 | * preserve these parent-child relationships, these two PolyTree classes will be
60 | * very useful to some users.
61 | */
62 | public final boolean Execute(ClipType clipType, FillRule fillRule, Paths64 solutionClosed, Paths64 solutionOpen) {
63 | solutionClosed.clear();
64 | solutionOpen.clear();
65 | try {
66 | ExecuteInternal(clipType, fillRule);
67 | BuildPaths(solutionClosed, solutionOpen);
68 | } catch (Exception e) {
69 | succeeded = false;
70 | }
71 |
72 | ClearSolutionOnly();
73 | return succeeded;
74 | }
75 |
76 | public final boolean Execute(ClipType clipType, FillRule fillRule, Paths64 solutionClosed) {
77 | return Execute(clipType, fillRule, solutionClosed, new Paths64());
78 | }
79 |
80 | public final boolean Execute(ClipType clipType, FillRule fillRule, PolyTree64 polytree, Paths64 openPaths) {
81 | polytree.Clear();
82 | openPaths.clear();
83 | usingPolytree = true;
84 | try {
85 | ExecuteInternal(clipType, fillRule);
86 | BuildTree(polytree, openPaths);
87 | } catch (Exception e) {
88 | succeeded = false;
89 | }
90 |
91 | ClearSolutionOnly();
92 | return succeeded;
93 | }
94 |
95 | public final boolean Execute(ClipType clipType, FillRule fillRule, PolyTree64 polytree) {
96 | return Execute(clipType, fillRule, polytree, new Paths64());
97 | }
98 |
99 | @Override
100 | public void AddReuseableData(ReuseableDataContainer64 reuseableData) {
101 | this.AddReuseableData(reuseableData);
102 | }
103 |
104 | }
--------------------------------------------------------------------------------
/src/main/java/clipper2/engine/ClipperD.java:
--------------------------------------------------------------------------------
1 | package clipper2.engine;
2 |
3 | import clipper2.Clipper;
4 | import clipper2.core.ClipType;
5 | import clipper2.core.FillRule;
6 | import clipper2.core.Path64;
7 | import clipper2.core.PathD;
8 | import clipper2.core.PathType;
9 | import clipper2.core.Paths64;
10 | import clipper2.core.PathsD;
11 |
12 | /**
13 | * The ClipperD class performs boolean 'clipping'. This class is very similar to
14 | * Clipper64 except that coordinates passed to ClipperD objects are of type
15 | * double instead of type long.
16 | */
17 | public class ClipperD extends ClipperBase {
18 |
19 | private double scale;
20 | private double invScale;
21 |
22 | public ClipperD() {
23 | this(2);
24 | }
25 |
26 | /**
27 | * @param roundingDecimalPrecision default = 2
28 | */
29 | public ClipperD(int roundingDecimalPrecision) {
30 | if (roundingDecimalPrecision < -8 || roundingDecimalPrecision > 8) {
31 | throw new IllegalArgumentException("Error - RoundingDecimalPrecision exceeds the allowed range.");
32 | }
33 | scale = Math.pow(10, roundingDecimalPrecision);
34 | invScale = 1 / scale;
35 | }
36 |
37 | public void AddPath(PathD path, PathType polytype) {
38 | AddPath(path, polytype, false);
39 | }
40 |
41 | public void AddPath(PathD path, PathType polytype, boolean isOpen) {
42 | super.AddPath(Clipper.ScalePath64(path, scale), polytype, isOpen);
43 | }
44 |
45 | public void AddPaths(PathsD paths, PathType polytype) {
46 | AddPaths(paths, polytype, false);
47 | }
48 |
49 | public void AddPaths(PathsD paths, PathType polytype, boolean isOpen) {
50 | super.AddPaths(Clipper.ScalePaths64(paths, scale), polytype, isOpen);
51 | }
52 |
53 | public void AddSubject(PathD path) {
54 | AddPath(path, PathType.Subject);
55 | }
56 |
57 | public void AddOpenSubject(PathD path) {
58 | AddPath(path, PathType.Subject, true);
59 | }
60 |
61 | public void AddClip(PathD path) {
62 | AddPath(path, PathType.Clip);
63 | }
64 |
65 | public void AddSubjects(PathsD paths) {
66 | AddPaths(paths, PathType.Subject);
67 | }
68 |
69 | public void AddOpenSubjects(PathsD paths) {
70 | AddPaths(paths, PathType.Subject, true);
71 | }
72 |
73 | public void AddClips(PathsD paths) {
74 | AddPaths(paths, PathType.Clip);
75 | }
76 |
77 | public boolean Execute(ClipType clipType, FillRule fillRule, PathsD solutionClosed, PathsD solutionOpen) {
78 | Paths64 solClosed64 = new Paths64(), solOpen64 = new Paths64();
79 |
80 | boolean success = true;
81 | solutionClosed.clear();
82 | solutionOpen.clear();
83 | try {
84 | ExecuteInternal(clipType, fillRule);
85 | BuildPaths(solClosed64, solOpen64);
86 | } catch (Exception e) {
87 | success = false;
88 | }
89 |
90 | ClearSolutionOnly();
91 | if (!success) {
92 | return false;
93 | }
94 |
95 | for (Path64 path : solClosed64) {
96 | solutionClosed.add(Clipper.ScalePathD(path, invScale));
97 | }
98 | for (Path64 path : solOpen64) {
99 | solutionOpen.add(Clipper.ScalePathD(path, invScale));
100 | }
101 |
102 | return true;
103 | }
104 |
105 | public boolean Execute(ClipType clipType, FillRule fillRule, PathsD solutionClosed) {
106 | return Execute(clipType, fillRule, solutionClosed, new PathsD());
107 | }
108 |
109 | public boolean Execute(ClipType clipType, FillRule fillRule, PolyTreeD polytree, PathsD openPaths) {
110 | polytree.Clear();
111 | polytree.setScale(scale);
112 | openPaths.clear();
113 | Paths64 oPaths = new Paths64();
114 | boolean success = true;
115 | try {
116 | ExecuteInternal(clipType, fillRule);
117 | BuildTree(polytree, oPaths);
118 | } catch (Exception e) {
119 | success = false;
120 | }
121 | ClearSolutionOnly();
122 | if (!success) {
123 | return false;
124 | }
125 | if (!oPaths.isEmpty()) {
126 | for (Path64 path : oPaths) {
127 | openPaths.add(Clipper.ScalePathD(path, invScale));
128 | }
129 | }
130 |
131 | return true;
132 | }
133 |
134 | public boolean Execute(ClipType clipType, FillRule fillRule, PolyTreeD polytree) {
135 | return Execute(clipType, fillRule, polytree, new PathsD());
136 | }
137 |
138 | }
--------------------------------------------------------------------------------
/src/main/java/clipper2/engine/LocalMinima.java:
--------------------------------------------------------------------------------
1 | package clipper2.engine;
2 |
3 | import clipper2.core.PathType;
4 | import clipper2.engine.ClipperBase.Vertex;
5 |
6 | final class LocalMinima {
7 |
8 | Vertex vertex;
9 | PathType polytype = PathType.Subject;
10 | boolean isOpen = false;
11 |
12 | LocalMinima() {
13 | }
14 |
15 | LocalMinima(Vertex vertex, PathType polytype) {
16 | this(vertex, polytype, false);
17 | }
18 |
19 | LocalMinima(Vertex vertex, PathType polytype, boolean isOpen) {
20 | this.vertex = vertex;
21 | this.polytype = polytype;
22 | this.isOpen = isOpen;
23 | }
24 |
25 | boolean opEquals(LocalMinima o) {
26 | return vertex == o.vertex;
27 | }
28 |
29 | boolean opNotEquals(LocalMinima o) {
30 | return vertex != o.vertex;
31 | }
32 |
33 | @Override
34 | public boolean equals(Object obj) {
35 | if (obj instanceof LocalMinima) {
36 | LocalMinima minima = (LocalMinima) obj;
37 | return this == minima;
38 | }
39 | return false;
40 | }
41 |
42 | @Override
43 | public int hashCode() {
44 | return vertex.hashCode();
45 | }
46 |
47 | @Override
48 | protected LocalMinima clone() {
49 | LocalMinima varCopy = new LocalMinima();
50 |
51 | varCopy.vertex = this.vertex;
52 | varCopy.polytype = this.polytype;
53 | varCopy.isOpen = this.isOpen;
54 |
55 | return varCopy;
56 | }
57 | }
--------------------------------------------------------------------------------
/src/main/java/clipper2/engine/NodeIterator.java:
--------------------------------------------------------------------------------
1 | package clipper2.engine;
2 |
3 | import java.util.Iterator;
4 | import java.util.List;
5 | import java.util.NoSuchElementException;
6 |
7 | public class NodeIterator implements Iterator {
8 |
9 | List ppbList;
10 | int position = 0;
11 |
12 | NodeIterator(List childs) {
13 | ppbList = childs;
14 | }
15 |
16 | @Override
17 | public final boolean hasNext() {
18 | return (position < ppbList.size());
19 | }
20 |
21 | @Override
22 | public PolyPathBase next() {
23 | if (position < 0 || position >= ppbList.size()) {
24 | throw new NoSuchElementException();
25 | }
26 | return ppbList.get(position++);
27 | }
28 |
29 | }
--------------------------------------------------------------------------------
/src/main/java/clipper2/engine/PointInPolygonResult.java:
--------------------------------------------------------------------------------
1 | package clipper2.engine;
2 |
3 | public enum PointInPolygonResult {
4 |
5 | IsOn, IsInside, IsOutside;
6 |
7 | }
--------------------------------------------------------------------------------
/src/main/java/clipper2/engine/PolyPath64.java:
--------------------------------------------------------------------------------
1 | package clipper2.engine;
2 |
3 | import clipper2.Clipper;
4 | import clipper2.Nullable;
5 | import clipper2.core.Path64;
6 |
7 | /**
8 | * PolyPath64 objects are contained inside PolyTree64s and represents a single
9 | * polygon contour. PolyPath64s can also contain children, and there's no limit
10 | * to nesting. Each child's Polygon will be inside its parent's Polygon.
11 | */
12 | public class PolyPath64 extends PolyPathBase {
13 |
14 | private Path64 polygon;
15 |
16 | public PolyPath64() {
17 | this(null);
18 | }
19 |
20 | public PolyPath64(@Nullable PolyPathBase parent) {
21 | super(parent);
22 | }
23 |
24 | @Override
25 | public PolyPathBase AddChild(Path64 p) {
26 | PolyPath64 newChild = new PolyPath64(this);
27 | newChild.setPolygon(p);
28 | children.add(newChild);
29 | return newChild;
30 | }
31 |
32 | public final PolyPath64 get(int index) {
33 | if (index < 0 || index >= children.size()) {
34 | throw new IllegalStateException();
35 | }
36 | return (PolyPath64) children.get(index);
37 | }
38 |
39 | public final double Area() {
40 | double result = getPolygon() == null ? 0 : Clipper.Area(getPolygon());
41 | for (PolyPathBase polyPathBase : children) {
42 | PolyPath64 child = (PolyPath64) polyPathBase;
43 | result += child.Area();
44 | }
45 | return result;
46 | }
47 |
48 | public final Path64 getPolygon() {
49 | return polygon;
50 | }
51 |
52 | private void setPolygon(Path64 value) {
53 | polygon = value;
54 | }
55 | }
--------------------------------------------------------------------------------
/src/main/java/clipper2/engine/PolyPathBase.java:
--------------------------------------------------------------------------------
1 | package clipper2.engine;
2 |
3 | import java.util.ArrayList;
4 | import java.util.List;
5 |
6 | import clipper2.Nullable;
7 | import clipper2.core.Path64;
8 |
9 | public abstract class PolyPathBase implements Iterable {
10 |
11 | @Nullable
12 | PolyPathBase parent;
13 | List children = new ArrayList<>();
14 |
15 | PolyPathBase(@Nullable PolyPathBase parent) {
16 | this.parent = parent;
17 | }
18 |
19 | PolyPathBase() {
20 | this(null);
21 | }
22 |
23 | @Override
24 | public final NodeIterator iterator() {
25 | return new NodeIterator(children);
26 | }
27 |
28 | private int getLevel() {
29 | int result = 0;
30 | @Nullable
31 | PolyPathBase pp = parent;
32 | while (pp != null) {
33 | ++result;
34 | pp = pp.parent;
35 | }
36 | return result;
37 | }
38 |
39 | /**
40 | * Indicates whether the Polygon property represents a hole or the outer bounds
41 | * of a polygon.
42 | */
43 | public final boolean getIsHole() {
44 | int lvl = getLevel();
45 | return lvl != 0 && (lvl & 1) == 0;
46 | }
47 |
48 | /**
49 | * Indicates the number of contained children.
50 | */
51 | public final int getCount() {
52 | return children.size();
53 | }
54 |
55 | public abstract PolyPathBase AddChild(Path64 p);
56 |
57 | /**
58 | * This method clears the Polygon and deletes any contained children.
59 | */
60 | public final void Clear() {
61 | children.clear();
62 | }
63 |
64 | private String toStringInternal(int idx, int level) {
65 | int count = children.size();
66 | String result = "", padding = "", plural = "s";
67 | if (children.size() == 1) {
68 | plural = "";
69 | }
70 | // Create padding by concatenating spaces
71 | for (int i = 0; i < level * 2; i++) {
72 | padding += " ";
73 | }
74 |
75 | if ((level & 1) == 0) {
76 | result += String.format("%s+- hole (%d) contains %d nested polygon%s.\n", padding, idx, children.size(), plural);
77 | } else {
78 | result += String.format("%s+- polygon (%d) contains %d hole%s.\n", padding, idx, children.size(), plural);
79 | }
80 | for (int i = 0; i < count; i++) {
81 | if (children.get(i).getCount() > 0) {
82 | result += children.get(i).toStringInternal(i, level + 1);
83 | }
84 | }
85 | return result;
86 | }
87 |
88 | @Override
89 | public String toString() {
90 | int count = children.size();
91 | if (getLevel() > 0) {
92 | return ""; // only accept tree root
93 | }
94 | String plural = "s";
95 | if (children.size() == 1) {
96 | plural = "";
97 | }
98 | String result = String.format("Polytree with %d polygon%s.\n", children.size(), plural);
99 | for (int i = 0; i < count; i++) {
100 | if (children.get(i).getCount() > 0) {
101 | result += children.get(i).toStringInternal(i, 1);
102 | }
103 | }
104 | return result + '\n';
105 | }
106 |
107 | }
--------------------------------------------------------------------------------
/src/main/java/clipper2/engine/PolyPathD.java:
--------------------------------------------------------------------------------
1 | package clipper2.engine;
2 |
3 | import clipper2.Clipper;
4 | import clipper2.Nullable;
5 | import clipper2.core.Path64;
6 | import clipper2.core.PathD;
7 |
8 | public class PolyPathD extends PolyPathBase {
9 |
10 | private PathD polygon;
11 | private double scale;
12 |
13 | PolyPathD() {
14 | this(null);
15 | }
16 |
17 | PolyPathD(@Nullable PolyPathBase parent) {
18 | super(parent);
19 | }
20 |
21 | @Override
22 | public PolyPathBase AddChild(Path64 p) {
23 | PolyPathD newChild = new PolyPathD(this);
24 | newChild.setScale(scale);
25 | newChild.setPolygon(Clipper.ScalePathD(p, scale));
26 | children.add(newChild);
27 | return newChild;
28 | }
29 |
30 | public final PolyPathD get(int index) {
31 | if (index < 0 || index >= children.size()) {
32 | throw new IllegalStateException();
33 | }
34 | return (PolyPathD) children.get(index);
35 | }
36 |
37 | public final double Area() {
38 | double result = getPolygon() == null ? 0 : Clipper.Area(getPolygon());
39 | for (PolyPathBase polyPathBase : children) {
40 | PolyPathD child = (PolyPathD) polyPathBase;
41 | result += child.Area();
42 | }
43 | return result;
44 | }
45 |
46 | public final PathD getPolygon() {
47 | return polygon;
48 | }
49 |
50 | private void setPolygon(PathD value) {
51 | polygon = value;
52 | }
53 |
54 | public double getScale() {
55 | return scale;
56 | }
57 |
58 | public final void setScale(double value) {
59 | scale = value;
60 | }
61 | }
--------------------------------------------------------------------------------
/src/main/java/clipper2/engine/PolyTree64.java:
--------------------------------------------------------------------------------
1 | package clipper2.engine;
2 |
3 | /**
4 | * PolyTree64 is a read-only data structure that receives solutions from
5 | * clipping operations. It's an alternative to the Paths64 data structure which
6 | * also receives solutions. However the principle advantage of PolyTree64 over
7 | * Paths64 is that it also represents the parent-child relationships of the
8 | * polygons in the solution (where a parent's Polygon will contain all its
9 | * children Polygons).
10 | *
11 | * The PolyTree64 object that's to receive a clipping solution is passed as a
12 | * parameter to Clipper64.Execute. When the clipping operation finishes, this
13 | * object will be populated with data representing the clipped solution.
14 | *
15 | * A PolyTree64 object is a container for any number of PolyPath64 child
16 | * objects, each representing a single polygon contour. Direct descendants of
17 | * PolyTree64 will always be outer polygon contours. PolyPath64 children may in
18 | * turn contain their own children to any level of nesting. Children of outer
19 | * polygon contours will always represent holes, and children of holes will
20 | * always represent nested outer polygon contours.
21 | *
22 | * PolyTree64 is a specialised PolyPath64 object that's simply as a container
23 | * for other PolyPath64 objects and its own polygon property will always be
24 | * empty.
25 | *
26 | * PolyTree64 will never contain open paths (unlike in Clipper1) since open
27 | * paths can't contain paths. When clipping open paths, these will always be
28 | * represented in solutions via a separate Paths64 structure.
29 | */
30 | public class PolyTree64 extends PolyPath64 {
31 | }
--------------------------------------------------------------------------------
/src/main/java/clipper2/engine/PolyTreeD.java:
--------------------------------------------------------------------------------
1 | package clipper2.engine;
2 |
3 | /**
4 | * PolyTreeD is a read-only data structure that receives solutions from clipping
5 | * operations. It's an alternative to the PathsD data structure which also
6 | * receives solutions. However the principle advantage of PolyTreeD over PathsD
7 | * is that it also represents the parent-child relationships of the polygons in
8 | * the solution (where a parent's Polygon will contain all its children
9 | * Polygons).
10 | *
11 | * The PolyTreeD object that's to receive a clipping solution is passed as a
12 | * parameter to ClipperD.Execute. When the clipping operation finishes, this
13 | * object will be populated with data representing the clipped solution.
14 | *
15 | * A PolyTreeD object is a container for any number of PolyPathD child objects,
16 | * each representing a single polygon contour. PolyTreeD's top level children
17 | * will always be outer polygon contours. PolyPathD children may in turn contain
18 | * their own children to any level of nesting. Children of outer polygon
19 | * contours will always represent holes, and children of holes will always
20 | * represent nested outer polygon contours.
21 | *
22 | * PolyTreeD is a specialised PolyPathD object that's simply as a container for
23 | * other PolyPathD objects and its own polygon property will always be empty.
24 | *
25 | * PolyTreeD will never contain open paths (unlike in Clipper1) since open paths
26 | * can't contain (own) other paths. When clipping open paths, these will always
27 | * be represented in solutions via a separate PathsD structure.
28 | */
29 | public class PolyTreeD extends PolyPathD {
30 | }
--------------------------------------------------------------------------------
/src/main/java/clipper2/engine/package-info.java:
--------------------------------------------------------------------------------
1 | /**
2 | * The Clipper64 and ClipperD classes in this unit encapsulate all the logic
3 | * that performs path clipping. Clipper64 clips Paths64 paths, and ClipperD
4 | * clips PathsD paths.
5 | *
6 | * For complex clipping operations (on open paths, and when using PolyTrees,
7 | * etc.), you'll need to implement these classes directly. But for simpler
8 | * clipping operations, the clipping functions in the Clipper Unit will be
9 | * easier to use.
10 | *
11 | * The PolyTree64 and PolyTreeD classes are optional data structures that, like
12 | * Paths64 and PathsD, receive polygon solutions from clipping operations. This
13 | * Polytree structure reflects polygon ownership (which polygons contain other
14 | * polygons). But using Polytrees will slow clipping, usually by 10-50%.
15 | */
16 | package clipper2.engine;
--------------------------------------------------------------------------------
/src/main/java/clipper2/offset/ClipperOffset.java:
--------------------------------------------------------------------------------
1 | package clipper2.offset;
2 |
3 | import static clipper2.core.InternalClipper.DEFAULT_ARC_TOLERANCE;
4 | import static clipper2.core.InternalClipper.MAX_COORD;
5 | import static clipper2.core.InternalClipper.MIN_COORD;
6 |
7 | import java.util.ArrayList;
8 | import java.util.Arrays;
9 | import java.util.List;
10 |
11 | import clipper2.Clipper;
12 | import clipper2.core.ClipType;
13 | import clipper2.core.FillRule;
14 | import clipper2.core.InternalClipper;
15 | import clipper2.core.Path64;
16 | import clipper2.core.PathD;
17 | import clipper2.core.Paths64;
18 | import clipper2.core.Point64;
19 | import clipper2.core.PointD;
20 | import clipper2.core.Rect64;
21 | import clipper2.engine.Clipper64;
22 | import clipper2.engine.PolyTree64;
23 | import tangible.OutObject;
24 | import tangible.RefObject;
25 |
26 | /**
27 | * Manages the process of offsetting (inflating/deflating) both open and closed
28 | * paths using different join types and end types.
29 | *
30 | * Geometric offsetting refers to the process of creating parallel
31 | * curves that are offset a specified distance from their primary curves.
32 | *
33 | * Library users will rarely need to access this class directly since it's
34 | * generally easier to use the
35 | * {@link Clipper#InflatePaths(Paths64, double, JoinType, EndType)
36 | * InflatePaths()} function for polygon offsetting.
37 | *
38 | * Notes:
39 | *
40 | *
When offsetting closed paths (polygons), a positive offset delta
41 | * specifies how much outer polygon contours will expand and inner "hole"
42 | * contours will contract. The converse occurs with negative deltas.
43 | *
You cannot offset open paths (polylines) with negative deltas
44 | * because it's not possible to contract/shrink open paths.
45 | *
When offsetting, it's important not to confuse EndType.Polygon
46 | * with EndType.Joined. EndType.Polygon should be used when
47 | * offsetting polygons (closed paths). EndType.Joined should be used with
48 | * polylines (open paths).
49 | *
Offsetting should not be performed on intersecting closed
50 | * paths, as doing so will almost always produce undesirable results.
51 | * Intersections must be removed before offsetting, which can be achieved
52 | * through a Union clipping operation.
53 | *
It is OK to offset self-intersecting open paths (polylines), though the
54 | * intersecting (overlapping) regions will be flattened in the solution
55 | * polygon.
56 | *
When offsetting closed paths (polygons), the winding direction of
57 | * paths in the solution will match that of the paths prior to offsetting.
58 | * Polygons with hole regions should comply with NonZero filling.
59 | *
When offsetting open paths (polylines), the solutions will always have
60 | * Positive orientation.
61 | *
Path order following offsetting very likely won't match
62 | * path order prior to offsetting.
63 | *
While the ClipperOffset class itself won't accept paths with floating
64 | * point coordinates, the InflatePaths function will accept paths with
65 | * floating point coordinates.
66 | *
Redundant segments should be removed before offsetting (see
67 | * {@link Clipper#SimplifyPaths(Paths64, double) SimplifyPaths()}), and between
68 | * offsetting operations too. These redundant segments not only slow down
69 | * offsetting, but they can cause unexpected blemishes in offset solutions.
70 | *
71 | */
72 | public class ClipperOffset {
73 |
74 | private static double TOLERANCE = 1.0E-12;
75 | private static final String COORD_RANGE_ERROR = "Error: Coordinate range.";
76 |
77 | private final List groupList = new ArrayList<>();
78 | private Path64 pathOut = new Path64();
79 | private final PathD normals = new PathD();
80 | private final Paths64 solution = new Paths64();
81 | private double groupDelta; // *0.5 for open paths; *-1.0 for negative areas
82 | private double delta;
83 | private double mitLimSqr;
84 | private double stepsPerRad;
85 | private double stepSin;
86 | private double stepCos;
87 | private JoinType joinType;
88 | private EndType endType;
89 | private double arcTolerance;
90 | private boolean mergeGroups;
91 | private double miterLimit;
92 | private boolean preserveCollinear;
93 | private boolean reverseSolution;
94 | private DeltaCallback64 deltaCallback;
95 |
96 | /**
97 | * @see #ClipperOffset(double, double, boolean, boolean)
98 | */
99 | public ClipperOffset(double miterLimit, double arcTolerance, boolean preserveCollinear) {
100 | this(miterLimit, arcTolerance, preserveCollinear, false);
101 | }
102 |
103 | /**
104 | * @see #ClipperOffset(double, double, boolean, boolean)
105 | */
106 | public ClipperOffset(double miterLimit, double arcTolerance) {
107 | this(miterLimit, arcTolerance, false, false);
108 | }
109 |
110 | /**
111 | * @see #ClipperOffset(double, double, boolean, boolean)
112 | */
113 | public ClipperOffset(double miterLimit) {
114 | this(miterLimit, 0.25, false, false);
115 | }
116 |
117 | /**
118 | * Creates a ClipperOffset object, using default parameters.
119 | *
120 | * @see #ClipperOffset(double, double, boolean, boolean)
121 | */
122 | public ClipperOffset() {
123 | this(2.0, 0.25, false, false);
124 | }
125 |
126 | /**
127 | * Creates a ClipperOffset object, using the supplied parameters.
128 | *
129 | * @param miterLimit This property sets the maximum distance in multiples
130 | * of groupDelta that vertices can be offset from their
131 | * original positions before squaring is applied.
132 | * (Squaring truncates a miter by 'cutting it off' at 1
133 | * × groupDelta distance from the original vertex.)
134 | *
135 | * The default value for miterLimit is 2
136 | * (i.e. twice groupDelta). This is also the smallest
137 | * MiterLimit that's allowed. If mitering was
138 | * unrestricted (ie without any squaring), then offsets
139 | * at very acute angles would generate unacceptably
140 | * long 'spikes'.
141 | * @param arcTolerance Since flattened paths can never perfectly represent
142 | * arcs (see Trigonometry), this property specifies a
143 | * maximum acceptable imperfection for rounded curves
144 | * during offsetting.
145 | *
146 | * It's important to make arcTolerance a sensible
147 | * fraction of the offset groupDelta (arc radius).
148 | * Large tolerances relative to the offset groupDelta
149 | * will produce poor arc approximations but, just as
150 | * importantly, very small tolerances will slow
151 | * offsetting performance without noticeably improving
152 | * curve quality.
153 | *
154 | * arcTolerance is only relevant when offsetting with
155 | * {@link JoinType#Round} and / or
156 | * {@link EndType#Round} (see
157 | * {{@link #AddPath(Path64, JoinType, EndType)
158 | * AddPath()} and
159 | * {@link #AddPaths(Paths64, JoinType, EndType)
160 | * AddPaths()}. The default arcTolerance is 0.25.
161 | * @param preserveCollinear When adjacent edges are collinear in closed path
162 | * solutions, the common vertex can safely be removed
163 | * to simplify the solution without altering path
164 | * shape. However, because some users prefer to retain
165 | * these common vertices, this feature is optional.
166 | * Nevertheless, when adjacent edges in solutions are
167 | * collinear and also create a 'spike' by overlapping,
168 | * the vertex creating the spike will be removed
169 | * irrespective of the PreserveCollinear setting. This
170 | * property is false by default.
171 | * @param reverseSolution reverses the solution's orientation
172 | */
173 | public ClipperOffset(double miterLimit, double arcTolerance, boolean preserveCollinear, boolean reverseSolution) {
174 | setMiterLimit(miterLimit);
175 | setArcTolerance(arcTolerance);
176 | setMergeGroups(true);
177 | setPreserveCollinear(preserveCollinear);
178 | setReverseSolution(reverseSolution);
179 | }
180 |
181 | public final void Clear() {
182 | groupList.clear();
183 | }
184 |
185 | public final void AddPath(Path64 path, JoinType joinType, EndType endType) {
186 | int cnt = path.size();
187 | if (cnt == 0) {
188 | return;
189 | }
190 | Paths64 pp = new Paths64(Arrays.asList(path));
191 | AddPaths(pp, joinType, endType);
192 | }
193 |
194 | public final void AddPaths(Paths64 paths, JoinType joinType, EndType endType) {
195 | int cnt = paths.size();
196 | if (cnt == 0) {
197 | return;
198 | }
199 | groupList.add(new Group(paths, joinType, endType));
200 | }
201 |
202 | private int CalcSolutionCapacity() {
203 | int result = 0;
204 | for (Group g : groupList) {
205 | result += (g.endType == EndType.Joined) ? g.inPaths.size() * 2 : g.inPaths.size();
206 | }
207 | return result;
208 | }
209 |
210 | private void ExecuteInternal(double delta) {
211 | solution.clear();
212 | if (groupList.isEmpty()) {
213 | return;
214 | }
215 | solution.ensureCapacity(CalcSolutionCapacity());
216 |
217 | // make sure the offset delta is significant
218 | if (Math.abs(delta) < 0.5) {
219 | for (Group group : groupList) {
220 | for (Path64 path : group.inPaths) {
221 | solution.add(path);
222 | }
223 | }
224 | return;
225 | }
226 | this.delta = delta;
227 | this.mitLimSqr = (miterLimit <= 1 ? 2.0 : 2.0 / Clipper.Sqr(miterLimit));
228 |
229 | for (Group group : groupList) {
230 | DoGroupOffset(group);
231 | }
232 | }
233 |
234 | boolean CheckPathsReversed() {
235 | boolean result = false;
236 | for (Group g : groupList) {
237 | if (g.endType == EndType.Polygon) {
238 | result = g.pathsReversed;
239 | break;
240 | }
241 | }
242 |
243 | return result;
244 | }
245 |
246 | public final void Execute(double delta, Paths64 solution) {
247 | solution.clear();
248 | ExecuteInternal(delta);
249 | if (groupList.isEmpty()) {
250 | return;
251 | }
252 |
253 | boolean pathsReversed = CheckPathsReversed();
254 | FillRule fillRule = pathsReversed ? FillRule.Negative : FillRule.Positive;
255 |
256 | // clean up self-intersections ...
257 | Clipper64 c = new Clipper64();
258 | c.setPreserveCollinear(preserveCollinear);
259 | // the solution should retain the orientation of the input
260 | c.setReverseSolution(reverseSolution != pathsReversed);
261 | c.AddSubject(this.solution);
262 | c.Execute(ClipType.Union, fillRule, solution);
263 | }
264 |
265 | public void Execute(DeltaCallback64 deltaCallback64, Paths64 solution) {
266 | deltaCallback = deltaCallback64;
267 | Execute(1.0, solution);
268 | }
269 |
270 | public void Execute(double delta, PolyTree64 solutionTree) {
271 | solutionTree.Clear();
272 | ExecuteInternal(delta);
273 | if (groupList.isEmpty()) {
274 | return;
275 | }
276 |
277 | boolean pathsReversed = CheckPathsReversed();
278 | FillRule fillRule = pathsReversed ? FillRule.Negative : FillRule.Positive;
279 | // clean up self-intersections ...
280 | Clipper64 c = new Clipper64();
281 | c.setPreserveCollinear(preserveCollinear);
282 | // the solution should normally retain the orientation of the input
283 | c.setReverseSolution(reverseSolution != pathsReversed);
284 | c.AddSubject(this.solution);
285 | c.Execute(ClipType.Union, fillRule, solutionTree);
286 | }
287 |
288 | public final double getArcTolerance() {
289 | return arcTolerance;
290 | }
291 |
292 | public final void setArcTolerance(double value) {
293 | arcTolerance = value;
294 | }
295 |
296 | public final boolean getMergeGroups() {
297 | return mergeGroups;
298 | }
299 |
300 | public final void setMergeGroups(boolean value) {
301 | mergeGroups = value;
302 | }
303 |
304 | public final double getMiterLimit() {
305 | return miterLimit;
306 | }
307 |
308 | public final void setMiterLimit(double value) {
309 | miterLimit = value;
310 | }
311 |
312 | public final boolean getPreserveCollinear() {
313 | return preserveCollinear;
314 | }
315 |
316 | public final void setPreserveCollinear(boolean value) {
317 | preserveCollinear = value;
318 | }
319 |
320 | public final boolean getReverseSolution() {
321 | return reverseSolution;
322 | }
323 |
324 | public final void setReverseSolution(boolean value) {
325 | reverseSolution = value;
326 | }
327 |
328 | public final void setDeltaCallBack64(DeltaCallback64 callback) {
329 | deltaCallback = callback;
330 | }
331 |
332 | public final DeltaCallback64 getDeltaCallBack64() {
333 | return deltaCallback;
334 | }
335 |
336 | private static PointD GetUnitNormal(Point64 pt1, Point64 pt2) {
337 | double dx = (pt2.x - pt1.x);
338 | double dy = (pt2.y - pt1.y);
339 | if ((dx == 0) && (dy == 0)) {
340 | return new PointD();
341 | }
342 |
343 | double f = 1.0 / Math.sqrt(dx * dx + dy * dy);
344 | dx *= f;
345 | dy *= f;
346 |
347 | return new PointD(dy, -dx);
348 | }
349 |
350 | private static PointD TranslatePoint(PointD pt, double dx, double dy) {
351 | return new PointD(pt.x + dx, pt.y + dy);
352 | }
353 |
354 | private static PointD ReflectPoint(PointD pt, PointD pivot) {
355 | return new PointD(pivot.x + (pivot.x - pt.x), pivot.y + (pivot.y - pt.y));
356 | }
357 |
358 | private static boolean AlmostZero(double value) {
359 | return AlmostZero(value, 0.001);
360 | }
361 |
362 | private static boolean AlmostZero(double value, double epsilon) {
363 | return Math.abs(value) < epsilon;
364 | }
365 |
366 | private static double Hypotenuse(double x, double y) {
367 | return Math.sqrt(x * x + y * y);
368 | }
369 |
370 | private static PointD NormalizeVector(PointD vec) {
371 | double h = Hypotenuse(vec.x, vec.y);
372 | if (AlmostZero(h)) {
373 | return new PointD(0, 0);
374 | }
375 | double inverseHypot = 1 / h;
376 | return new PointD(vec.x * inverseHypot, vec.y * inverseHypot);
377 | }
378 |
379 | private static PointD GetAvgUnitVector(PointD vec1, PointD vec2) {
380 | return NormalizeVector(new PointD(vec1.x + vec2.x, vec1.y + vec2.y));
381 | }
382 |
383 | private static PointD IntersectPoint(PointD pt1a, PointD pt1b, PointD pt2a, PointD pt2b) {
384 | if (InternalClipper.IsAlmostZero(pt1a.x - pt1b.x)) { // vertical
385 | if (InternalClipper.IsAlmostZero(pt2a.x - pt2b.x)) {
386 | return new PointD(0, 0);
387 | }
388 | double m2 = (pt2b.y - pt2a.y) / (pt2b.x - pt2a.x);
389 | double b2 = pt2a.y - m2 * pt2a.x;
390 | return new PointD(pt1a.x, m2 * pt1a.x + b2);
391 | }
392 |
393 | if (InternalClipper.IsAlmostZero(pt2a.x - pt2b.x)) { // vertical
394 | double m1 = (pt1b.y - pt1a.y) / (pt1b.x - pt1a.x);
395 | double b1 = pt1a.y - m1 * pt1a.x;
396 | return new PointD(pt2a.x, m1 * pt2a.x + b1);
397 | } else {
398 | double m1 = (pt1b.y - pt1a.y) / (pt1b.x - pt1a.x);
399 | double b1 = pt1a.y - m1 * pt1a.x;
400 | double m2 = (pt2b.y - pt2a.y) / (pt2b.x - pt2a.x);
401 | double b2 = pt2a.y - m2 * pt2a.x;
402 | if (InternalClipper.IsAlmostZero(m1 - m2)) {
403 | return new PointD(0, 0);
404 | }
405 | double x = (b2 - b1) / (m1 - m2);
406 | return new PointD(x, m1 * x + b1);
407 | }
408 | }
409 |
410 | private Point64 GetPerpendic(Point64 pt, PointD norm) {
411 | return new Point64(pt.x + norm.x * groupDelta, pt.y + norm.y * groupDelta);
412 | }
413 |
414 | private PointD GetPerpendicD(Point64 pt, PointD norm) {
415 | return new PointD(pt.x + norm.x * groupDelta, pt.y + norm.y * groupDelta);
416 | }
417 |
418 | private void DoBevel(Path64 path, int j, int k) {
419 | Point64 pt1, pt2;
420 | if (j == k) {
421 | double absDelta = Math.abs(groupDelta);
422 | pt1 = new Point64(path.get(j).x - absDelta * normals.get(j).x, path.get(j).y - absDelta * normals.get(j).y);
423 | pt2 = new Point64(path.get(j).x + absDelta * normals.get(j).x, path.get(j).y + absDelta * normals.get(j).y);
424 | } else {
425 | pt1 = new Point64(path.get(j).x + groupDelta * normals.get(k).x, path.get(j).y + groupDelta * normals.get(k).y);
426 | pt2 = new Point64(path.get(j).x + groupDelta * normals.get(j).x, path.get(j).y + groupDelta * normals.get(j).y);
427 | }
428 | pathOut.add(pt1);
429 | pathOut.add(pt2);
430 | }
431 |
432 | private void DoSquare(Path64 path, int j, int k) {
433 | PointD vec;
434 | if (j == k) {
435 | vec = new PointD(normals.get(j).y, -normals.get(j).x);
436 | } else {
437 | vec = GetAvgUnitVector(new PointD(-normals.get(k).y, normals.get(k).x), new PointD(normals.get(j).y, -normals.get(j).x));
438 | }
439 | double absDelta = Math.abs(groupDelta);
440 | // now offset the original vertex delta units along unit vector
441 | PointD ptQ = new PointD(path.get(j));
442 | ptQ = TranslatePoint(ptQ, absDelta * vec.x, absDelta * vec.y);
443 |
444 | // get perpendicular vertices
445 | PointD pt1 = TranslatePoint(ptQ, groupDelta * vec.y, groupDelta * -vec.x);
446 | PointD pt2 = TranslatePoint(ptQ, groupDelta * -vec.y, groupDelta * vec.x);
447 | // get 2 vertices along one edge offset
448 | PointD pt3 = GetPerpendicD(path.get(k), normals.get(k));
449 |
450 | if (j == k) {
451 | PointD pt4 = new PointD(pt3.x + vec.x * groupDelta, pt3.y + vec.y * groupDelta);
452 | PointD pt = IntersectPoint(pt1, pt2, pt3, pt4);
453 | // get the second intersect point through reflecion
454 | pathOut.add(new Point64(ReflectPoint(pt, ptQ)));
455 | pathOut.add(new Point64(pt));
456 | } else {
457 | PointD pt4 = GetPerpendicD(path.get(j), normals.get(k));
458 | PointD pt = IntersectPoint(pt1, pt2, pt3, pt4);
459 | pathOut.add(new Point64(pt));
460 | // get the second intersect point through reflecion
461 | pathOut.add(new Point64(ReflectPoint(pt, ptQ)));
462 | }
463 | }
464 |
465 | private void DoMiter(Group group, Path64 path, int j, int k, double cosA) {
466 | final double q = groupDelta / (cosA + 1);
467 | pathOut.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));
468 | }
469 |
470 | private void DoRound(Path64 path, int j, int k, double angle) {
471 | if (deltaCallback != null) {
472 | // when deltaCallback is assigned, groupDelta won't be constant,
473 | // so we'll need to do the following calculations for *every* vertex.
474 | double absDelta = Math.abs(groupDelta);
475 | double arcTol = arcTolerance > 0.01 ? arcTolerance : Math.log10(2 + absDelta) * InternalClipper.DEFAULT_ARC_TOLERANCE;
476 | double stepsPer360 = Math.PI / Math.acos(1 - arcTol / absDelta);
477 | stepSin = Math.sin((2 * Math.PI) / stepsPer360);
478 | stepCos = Math.cos((2 * Math.PI) / stepsPer360);
479 | if (groupDelta < 0.0) {
480 | stepSin = -stepSin;
481 | }
482 | stepsPerRad = stepsPer360 / (2 * Math.PI);
483 | }
484 |
485 | final Point64 pt = path.get(j);
486 | PointD offsetVec = new PointD(normals.get(k).x * groupDelta, normals.get(k).y * groupDelta);
487 | if (j == k) {
488 | offsetVec.Negate();
489 | }
490 | pathOut.add(new Point64(pt.x + offsetVec.x, pt.y + offsetVec.y));
491 | int steps = (int) Math.ceil(stepsPerRad * Math.abs(angle)); // #448, #456
492 | for (int i = 1; i < steps; ++i) // ie 1 less than steps
493 | {
494 | offsetVec = new PointD(offsetVec.x * stepCos - stepSin * offsetVec.y, offsetVec.x * stepSin + offsetVec.y * stepCos);
495 | pathOut.add(new Point64(pt.x + offsetVec.x, pt.y + offsetVec.y));
496 | }
497 | pathOut.add(GetPerpendic(pt, normals.get(j)));
498 | }
499 |
500 | private void BuildNormals(Path64 path) {
501 | int cnt = path.size();
502 | normals.clear();
503 |
504 | for (int i = 0; i < cnt - 1; i++) {
505 | normals.add(GetUnitNormal(path.get(i), path.get(i + 1)));
506 | }
507 | normals.add(GetUnitNormal(path.get(cnt - 1), path.get(0)));
508 | }
509 |
510 | private void OffsetPoint(Group group, Path64 path, int j, RefObject k) {
511 | // Let A = change in angle where edges join
512 | // A == 0: ie no change in angle (flat join)
513 | // A == PI: edges 'spike'
514 | // sin(A) < 0: right turning
515 | // cos(A) < 0: change in angle is more than 90 degree
516 | double sinA = InternalClipper.CrossProduct(normals.get(j), normals.get(k.argValue));
517 | double cosA = InternalClipper.DotProduct(normals.get(j), normals.get(k.argValue));
518 | if (sinA > 1.0) {
519 | sinA = 1.0;
520 | } else if (sinA < -1.0) {
521 | sinA = -1.0;
522 | }
523 |
524 | if (deltaCallback != null) {
525 | groupDelta = deltaCallback.calculate(path, normals, j, k.argValue);
526 | if (group.pathsReversed) {
527 | groupDelta = -groupDelta;
528 | }
529 | }
530 | if (Math.abs(groupDelta) < TOLERANCE) {
531 | pathOut.add(path.get(j));
532 | return;
533 | }
534 |
535 | if (cosA > -0.99 && (sinA * groupDelta < 0)) { // test for concavity first (#593)
536 | // is concave
537 | pathOut.add(GetPerpendic(path.get(j), normals.get(k.argValue)));
538 | // this extra point is the only (simple) way to ensure that
539 | // path reversals are fully cleaned with the trailing clipper
540 | pathOut.add(path.get(j)); // (#405)
541 | pathOut.add(GetPerpendic(path.get(j), normals.get(j)));
542 | } else if (cosA > 0.999 && joinType != JoinType.Round) {
543 | // almost straight - less than 2.5 degree (#424, #482, #526 & #724)
544 | DoMiter(group, path, j, k.argValue, cosA);
545 | } else if (joinType == JoinType.Miter) {
546 | // miter unless the angle is sufficiently acute to exceed ML
547 | if (cosA > mitLimSqr - 1) {
548 | DoMiter(group, path, j, k.argValue, cosA);
549 | } else {
550 | DoSquare(path, j, k.argValue);
551 | }
552 | } else if (joinType == JoinType.Round) {
553 | DoRound(path, j, k.argValue, Math.atan2(sinA, cosA));
554 | } else if (joinType == JoinType.Bevel) {
555 | DoBevel(path, j, k.argValue);
556 | } else {
557 | DoSquare(path, j, k.argValue);
558 | }
559 |
560 | k.argValue = j;
561 | }
562 |
563 | private void OffsetPolygon(Group group, Path64 path) {
564 | pathOut = new Path64();
565 | int cnt = path.size();
566 | RefObject prev = new RefObject(cnt - 1);
567 | for (int i = 0; i < cnt; i++) {
568 | OffsetPoint(group, path, i, prev);
569 | }
570 | solution.add(pathOut);
571 | }
572 |
573 | private void OffsetOpenJoined(Group group, Path64 path) {
574 | OffsetPolygon(group, path);
575 | path = Clipper.ReversePath(path);
576 | BuildNormals(path);
577 | OffsetPolygon(group, path);
578 | }
579 |
580 | private void OffsetOpenPath(Group group, Path64 path) {
581 | pathOut = new Path64();
582 | int highI = path.size() - 1;
583 | if (deltaCallback != null) {
584 | groupDelta = deltaCallback.calculate(path, normals, 0, 0);
585 | }
586 | // do the line start cap
587 | if (Math.abs(groupDelta) < TOLERANCE) {
588 | pathOut.add(path.get(0));
589 | } else {
590 | switch (endType) {
591 | case Butt :
592 | DoBevel(path, 0, 0);
593 | break;
594 | case Round :
595 | DoRound(path, 0, 0, Math.PI);
596 | break;
597 | default :
598 | DoSquare(path, 0, 0);
599 | break;
600 | }
601 | }
602 | // offset the left side going forward
603 | for (int i = 1, k = 0; i < highI; i++) {
604 | OffsetPoint(group, path, i, new RefObject<>(k)); // NOTE creating new ref object correct?
605 | }
606 | // reverse normals ...
607 | for (int i = highI; i > 0; i--) {
608 | normals.set(i, new PointD(-normals.get(i - 1).x, -normals.get(i - 1).y));
609 | }
610 | normals.set(0, normals.get(highI));
611 | if (deltaCallback != null) {
612 | groupDelta = deltaCallback.calculate(path, normals, highI, highI);
613 | }
614 | // do the line end cap
615 | if (Math.abs(groupDelta) < TOLERANCE) {
616 | pathOut.add(path.get(highI));
617 | } else {
618 | switch (endType) {
619 | case Butt :
620 | DoBevel(path, highI, highI);
621 | break;
622 | case Round :
623 | DoRound(path, highI, highI, Math.PI);
624 | break;
625 | default :
626 | DoSquare(path, highI, highI);
627 | break;
628 | }
629 | }
630 | // offset the left side going back
631 | for (int i = highI, k = 0; i > 0; i--) {
632 | OffsetPoint(group, path, i, new RefObject<>(k)); // NOTE creating new ref object correct?
633 | }
634 | solution.add(pathOut);
635 | }
636 |
637 | private static boolean ToggleBoolIf(boolean val, boolean condition) {
638 | return condition ? !val : val;
639 | }
640 |
641 | private void DoGroupOffset(Group group) {
642 | if (group.endType == EndType.Polygon) {
643 | // a straight path (2 points) can now also be 'polygon' offset
644 | // where the ends will be treated as (180 deg.) joins
645 | if (group.lowestPathIdx < 0) {
646 | delta = Math.abs(delta);
647 | }
648 | groupDelta = (group.pathsReversed) ? -delta : delta;
649 | } else {
650 | groupDelta = Math.abs(delta); // * 0.5
651 | }
652 |
653 | double absDelta = Math.abs(groupDelta);
654 | if (!ValidateBounds(group.boundsList, absDelta)) {
655 | throw new RuntimeException(COORD_RANGE_ERROR);
656 | }
657 |
658 | joinType = group.joinType;
659 | endType = group.endType;
660 |
661 | if (group.joinType == JoinType.Round || group.endType == EndType.Round) {
662 | // calculate a sensible number of steps (for 360 deg for the given offset
663 | // arcTol - when fArcTolerance is undefined (0), the amount of
664 | // curve imprecision that's allowed is based on the size of the
665 | // offset (delta). Obviously very large offsets will almost always
666 | // require much less precision.
667 | double arcTol = arcTolerance > 0.01 ? arcTolerance : Math.log10(2 + absDelta) * InternalClipper.DEFAULT_ARC_TOLERANCE;
668 | double stepsPer360 = Math.PI / Math.acos(1 - arcTol / absDelta);
669 | stepSin = Math.sin((2 * Math.PI) / stepsPer360);
670 | stepCos = Math.cos((2 * Math.PI) / stepsPer360);
671 | if (groupDelta < 0.0) {
672 | stepSin = -stepSin;
673 | }
674 | stepsPerRad = stepsPer360 / (2 * Math.PI);
675 | }
676 |
677 | int i = 0;
678 | for (Path64 p : group.inPaths) {
679 | // NOTE use int i rather than 3 iterators
680 | Rect64 pathBounds = group.boundsList.get(i);
681 | boolean isHole = group.isHoleList.get(i++);
682 | if (!pathBounds.IsValid()) {
683 | continue;
684 | }
685 | int cnt = p.size();
686 | if ((cnt == 0) || ((cnt < 3) && (endType == EndType.Polygon))) {
687 | continue;
688 | }
689 |
690 | pathOut = new Path64();
691 | if (cnt == 1) {
692 | Point64 pt = p.get(0);
693 |
694 | // single vertex so build a circle or square ...
695 | if (group.endType == EndType.Round) {
696 | double r = absDelta;
697 | int steps = (int) Math.ceil(stepsPerRad * 2 * Math.PI);
698 | pathOut = Clipper.Ellipse(pt, r, r, steps);
699 | } else {
700 | int d = (int) Math.ceil(groupDelta);
701 | Rect64 r = new Rect64(pt.x - d, pt.y - d, pt.x + d, pt.y + d);
702 | pathOut = r.AsPath();
703 | }
704 | solution.add(pathOut);
705 | continue;
706 | } // end of offsetting a single point
707 |
708 | // when shrinking outer paths, make sure they can shrink this far (#593)
709 | // also when shrinking holes, make sure they too can shrink this far (#715)
710 | if (((groupDelta > 0) == ToggleBoolIf(isHole, group.pathsReversed)) && (Math.min(pathBounds.getWidth(), pathBounds.getHeight()) <= -groupDelta * 2))
711 | continue;
712 |
713 | if (cnt == 2 && group.endType == EndType.Joined) {
714 | endType = (group.joinType == JoinType.Round) ? EndType.Round : EndType.Square;
715 | }
716 |
717 | BuildNormals(p);
718 | if (endType == EndType.Polygon) {
719 | OffsetPolygon(group, p);
720 | } else if (endType == EndType.Joined) {
721 | OffsetOpenJoined(group, p);
722 | } else {
723 | OffsetOpenPath(group, p);
724 | }
725 | }
726 | }
727 |
728 | private static boolean ValidateBounds(List boundsList, double delta) {
729 | int intDelta = (int) delta;
730 | for (Rect64 r : boundsList) {
731 | if (!r.IsValid()) {
732 | continue; // ignore invalid paths
733 | } else if (r.left < MIN_COORD + intDelta || r.right > MAX_COORD + intDelta || r.top < MIN_COORD + intDelta || r.bottom > MAX_COORD + intDelta) {
734 | return false;
735 | }
736 | }
737 | return true;
738 | }
739 |
740 | }
--------------------------------------------------------------------------------
/src/main/java/clipper2/offset/DeltaCallback64.java:
--------------------------------------------------------------------------------
1 | package clipper2.offset;
2 |
3 | import clipper2.core.Path64;
4 | import clipper2.core.PathD;
5 |
6 | /**
7 | * Functional interface for calculating a variable delta during polygon
8 | * offsetting.
9 | *
10 | * Implementations of this interface define how to calculate the delta (the
11 | * amount of offset) to apply at each point in a polygon during an offset
12 | * operation. The offset can vary from point to point, allowing for variable
13 | * offsetting.
14 | */
15 | @FunctionalInterface
16 | public interface DeltaCallback64 {
17 | /**
18 | * Calculates the delta (offset) for a given point in the polygon path.
19 | *
20 | * This method is used during polygon offsetting operations to determine the
21 | * amount by which each point of the polygon should be offset.
22 | *
23 | * @param path The {@link Path64} object representing the original polygon
24 | * path.
25 | * @param path_norms The {@link PathD} object containing the normals of the
26 | * path, which may be used to influence the delta calculation.
27 | * @param currPt The index of the current point in the path for which the
28 | * delta is being calculated.
29 | * @param prevPt The index of the previous point in the path, which can be
30 | * referenced to determine the delta based on adjacent
31 | * segments.
32 | * @return A {@code double} value representing the calculated delta for the
33 | * current point. This value will be used to offset the point in the
34 | * resulting polygon.
35 | */
36 | double calculate(Path64 path, PathD path_norms, int currPt, int prevPt);
37 | }
38 |
--------------------------------------------------------------------------------
/src/main/java/clipper2/offset/EndType.java:
--------------------------------------------------------------------------------
1 | package clipper2.offset;
2 |
3 | /**
4 | * The EndType enumerator is only needed when offsetting (inflating/shrinking).
5 | * It isn't needed for polygon clipping.
6 | *
7 | * EndType has 5 values:
8 | *
9 | *
Polygon: the path is treated as a polygon
10 | *
Join: ends are joined and the path treated as a polyline
11 | *
Square: ends extend the offset amount while being squared off
12 | *
Round: ends extend the offset amount while being rounded off
13 | *
Butt: ends are squared off without any extension
14 | *
15 | * With both EndType.Polygon and EndType.Join, path closure will occur
16 | * regardless of whether or not the first and last vertices in the path match.
17 | */
18 | public enum EndType {
19 |
20 | Polygon, Joined, Butt, Square, Round;
21 |
22 | }
--------------------------------------------------------------------------------
/src/main/java/clipper2/offset/Group.java:
--------------------------------------------------------------------------------
1 | package clipper2.offset;
2 |
3 | import java.util.ArrayList;
4 | import java.util.Collections;
5 | import java.util.List;
6 |
7 | import clipper2.Clipper;
8 | import clipper2.core.Path64;
9 | import clipper2.core.Paths64;
10 | import clipper2.core.Point64;
11 | import clipper2.core.Rect64;
12 |
13 | class Group {
14 |
15 | Paths64 inPaths;
16 | List boundsList;
17 | List isHoleList;
18 | JoinType joinType;
19 | EndType endType;
20 | boolean pathsReversed;
21 | int lowestPathIdx;
22 |
23 | Group(Paths64 paths, JoinType joinType) {
24 | this(paths, joinType, EndType.Polygon);
25 | }
26 |
27 | Group(Paths64 paths, JoinType joinType, EndType endType) {
28 | this.joinType = joinType;
29 | this.endType = endType;
30 |
31 | boolean isJoined = ((endType == EndType.Polygon) || (endType == EndType.Joined));
32 | inPaths = new Paths64(paths.size());
33 |
34 | for (Path64 path : paths) {
35 | inPaths.add(Clipper.StripDuplicates(path, isJoined));
36 | }
37 |
38 | // get bounds of each path --> boundsList
39 | boundsList = new ArrayList<>(inPaths.size());
40 | GetMultiBounds(inPaths, boundsList);
41 |
42 | if (endType == EndType.Polygon) {
43 | lowestPathIdx = GetLowestPathIdx(boundsList);
44 | isHoleList = new ArrayList<>(inPaths.size());
45 |
46 | for (Path64 path : inPaths) {
47 | isHoleList.add(Clipper.Area(path) < 0);
48 | }
49 |
50 | // the lowermost path must be an outer path, so if its orientation is negative,
51 | // then flag that the whole group is 'reversed' (will negate delta etc.)
52 | // as this is much more efficient than reversing every path.
53 | pathsReversed = (lowestPathIdx >= 0) && isHoleList.get(lowestPathIdx);
54 | if (pathsReversed) {
55 | for (int i = 0; i < isHoleList.size(); i++) {
56 | isHoleList.set(i, !isHoleList.get(i));
57 | }
58 | }
59 | } else {
60 | lowestPathIdx = -1;
61 | isHoleList = new ArrayList<>(Collections.nCopies(inPaths.size(), false));
62 | pathsReversed = false;
63 | }
64 | }
65 |
66 | private static void GetMultiBounds(Paths64 paths, List boundsList) {
67 | for (Path64 path : paths) {
68 | if (path.size() < 1) {
69 | boundsList.add(Clipper.InvalidRect64.clone());
70 | continue;
71 | }
72 |
73 | Point64 pt1 = path.get(0);
74 | Rect64 r = new Rect64(pt1.x, pt1.y, pt1.x, pt1.y);
75 |
76 | for (Point64 pt : path) {
77 | if (pt.y > r.bottom) {
78 | r.bottom = pt.y;
79 | } else if (pt.y < r.top) {
80 | r.top = pt.y;
81 | }
82 | if (pt.x > r.right) {
83 | r.right = pt.x;
84 | } else if (pt.x < r.left) {
85 | r.left = pt.x;
86 | }
87 | }
88 |
89 | boundsList.add(r);
90 | }
91 | }
92 |
93 | private static int GetLowestPathIdx(List boundsList) {
94 | int result = -1;
95 | Point64 botPt = new Point64(Long.MAX_VALUE, Long.MIN_VALUE);
96 | for (int i = 0; i < boundsList.size(); i++) {
97 | Rect64 r = boundsList.get(i);
98 | if (!r.IsValid()) {
99 | continue; // ignore invalid paths
100 | } else if (r.bottom > botPt.y || (r.bottom == botPt.y && r.left < botPt.x)) {
101 | botPt = new Point64(r.left, r.bottom);
102 | result = i;
103 | }
104 | }
105 | return result;
106 | }
107 |
108 | }
--------------------------------------------------------------------------------
/src/main/java/clipper2/offset/JoinType.java:
--------------------------------------------------------------------------------
1 | package clipper2.offset;
2 |
3 | /**
4 | * The JoinType enumerator is only needed when offsetting (inflating/shrinking).
5 | * It isn't needed for polygon clipping.
6 | *
7 | * When adding paths to a ClipperOffset object via the AddPaths method, the
8 | * joinType parameter may be any one of these types - Square, Round, Miter, or
9 | * Round.
10 | *
11 | */
12 | public enum JoinType {
13 | /**
14 | * Convex joins will be truncated using a 'squaring' edge. And the mid-points of
15 | * these squaring edges will be exactly the offset distance away from their
16 | * original (or starting) vertices.
17 | */
18 | Square,
19 | /**
20 | * Rounding is applied to all convex joins with the arc radius being the offset
21 | * distance, and the original join vertex the arc center.
22 | */
23 | Round,
24 | /**
25 | * Edges are first offset a specified distance away from and parallel to their
26 | * original (ie starting) edge positions. These offset edges are then extended
27 | * to points where they intersect with adjacent edge offsets. However a limit
28 | * must be imposed on how far mitered vertices can be from their original
29 | * positions to avoid very convex joins producing unreasonably long and narrow
30 | * spikes). To avoid unsightly spikes, joins will be 'squared' wherever
31 | * distances between original vertices and their calculated offsets exceeds a
32 | * specified value (expressed as a ratio relative to the offset distance).
33 | */
34 | Miter,
35 | /**
36 | * Bevelled joins are similar to 'squared' joins except that squaring won't
37 | * occur at a fixed distance. While bevelled joins may not be as pretty as
38 | * squared joins, bevelling is much easier (ie faster) than squaring. And
39 | * perhaps this is why bevelling rather than squaring is preferred in numerous
40 | * graphics display formats (including SVG and PDF document formats).
41 | */
42 | Bevel;
43 |
44 | }
--------------------------------------------------------------------------------
/src/main/java/clipper2/offset/package-info.java:
--------------------------------------------------------------------------------
1 | /**
2 | * This unit contains the ClipperOffset class that performs all polygon
3 | * offsetting. Nevertheless, the vast majority of offset operations can be
4 | * performed using the simple InflatePaths function that's found in the Clipper
5 | * Unit.
6 | */
7 | package clipper2.offset;
--------------------------------------------------------------------------------
/src/main/java/clipper2/package-info.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Clipper2 is an open source freeware library that performs line and polygon
3 | * clipping, and offsetting.
4 | */
5 | package clipper2;
--------------------------------------------------------------------------------
/src/main/java/clipper2/rectclip/RectClip64.java:
--------------------------------------------------------------------------------
1 | package clipper2.rectclip;
2 |
3 | import java.util.ArrayList;
4 | import java.util.List;
5 |
6 | import clipper2.Clipper;
7 | import clipper2.core.InternalClipper;
8 | import clipper2.core.Path64;
9 | import clipper2.core.Paths64;
10 | import clipper2.core.Point64;
11 | import clipper2.core.Rect64;
12 | import clipper2.engine.PointInPolygonResult;
13 | import tangible.RefObject;
14 |
15 | /**
16 | * RectClip64 intersects subject polygons with the specified rectangular
17 | * clipping region. Polygons may be simple or complex (self-intersecting).
18 | *