├── .github └── workflows │ └── maven.yml ├── .gitignore ├── README.md ├── jitpack.yml ├── pom.xml └── src ├── main ├── java │ ├── clipper2 │ │ ├── Clipper.java │ │ ├── Minkowski.java │ │ ├── Nullable.java │ │ ├── core │ │ │ ├── ClipType.java │ │ │ ├── FillRule.java │ │ │ ├── InternalClipper.java │ │ │ ├── Path64.java │ │ │ ├── PathD.java │ │ │ ├── PathType.java │ │ │ ├── Paths64.java │ │ │ ├── PathsD.java │ │ │ ├── Point64.java │ │ │ ├── PointD.java │ │ │ ├── Rect64.java │ │ │ ├── RectD.java │ │ │ └── package-info.java │ │ ├── engine │ │ │ ├── Clipper64.java │ │ │ ├── ClipperBase.java │ │ │ ├── ClipperD.java │ │ │ ├── LocalMinima.java │ │ │ ├── NodeIterator.java │ │ │ ├── PointInPolygonResult.java │ │ │ ├── PolyPath64.java │ │ │ ├── PolyPathBase.java │ │ │ ├── PolyPathD.java │ │ │ ├── PolyTree64.java │ │ │ ├── PolyTreeD.java │ │ │ └── package-info.java │ │ ├── offset │ │ │ ├── ClipperOffset.java │ │ │ ├── DeltaCallback64.java │ │ │ ├── EndType.java │ │ │ ├── Group.java │ │ │ ├── JoinType.java │ │ │ └── package-info.java │ │ ├── package-info.java │ │ └── rectclip │ │ │ ├── RectClip64.java │ │ │ ├── RectClipLines64.java │ │ │ └── package-info.java │ └── tangible │ │ ├── OutObject.java │ │ └── RefObject.java └── java9 │ └── module-info.java └── test ├── java └── clipper2 │ ├── BenchmarkClipper1.java │ ├── BenchmarkClipper2.java │ ├── ClipperFileIO.java │ ├── TestLines.java │ ├── TestOffsetOrientation.java │ ├── TestOffsets.java │ ├── TestPolygons.java │ ├── TestPolytree.java │ └── TestRectClip.java └── resources ├── Lines.txt ├── Polygons.txt └── PolytreeHoleOwner2.txt /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | name: Java CI with Maven 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | pull_request: 7 | branches: ["master"] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Set up JDK 17 | uses: actions/setup-java@v4 18 | with: 19 | java-version: '21' # Updated to latest LTS 20 | distribution: 'temurin' 21 | cache: 'maven' 22 | 23 | - name: Build with Maven 24 | run: mvn -B package --file pom.xml 25 | 26 | - name: Run tests 27 | run: mvn -B test 28 | 29 | - name: Upload build artifacts 30 | if: success() 31 | uses: actions/upload-artifact@v4 32 | with: 33 | name: package 34 | path: target/*.jar 35 | retention-days: 5 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | 25 | # Eclipse 26 | .classpath 27 | .project 28 | .settings/ 29 | /target/ 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![](https://jitpack.io/v/micycle1/Clipper2-java.svg)](https://jitpack.io/#micycle1/Clipper2-java) 2 | 3 | 4 | # Clipper2-java 5 | A Java port of _[Clipper2](https://github.com/AngusJohnson/Clipper2)_, an open source freeware software library that performs line and polygon clipping, and offsetting. 6 | 7 | ## Usage 8 | 9 | ### Overview 10 | 11 | The interface of *Clipper2-java* is identical to the original C# version. 12 | 13 | The `Clipper` class provides static methods for clipping, path-offsetting, minkowski-sums and path simplification. 14 | For more complex clipping operations (e.g. when clipping open paths or when outputs are expected to include polygons nested within holes of others), use the `Clipper64` or `ClipperD` classes directly. 15 | 16 | ### Maven 17 | *Clipper2-java* is available as Maven/Gradle artifact via [Jitpack](https://jitpack.io/#micycle1/Clipper2-java). 18 | 19 | ### Example 20 | 21 | ```java 22 | Paths64 subj = new Paths64(); 23 | Paths64 clip = new Paths64(); 24 | subj.add(Clipper.MakePath(new int[] { 100, 50, 10, 79, 65, 2, 65, 98, 10, 21 })); 25 | clip.add(Clipper.MakePath(new int[] { 98, 63, 4, 68, 77, 8, 52, 100, 19, 12 })); 26 | Paths64 solution = Clipper.Union(subj, clip, FillRule.NonZero); 27 | solution.get(0).forEach(p -> System.out.println(p.toString())); 28 | ``` 29 | 30 | 31 | ## Port Info 32 | * _tangiblesoftwaresolutions_' C# to Java Converter did the heavy lifting (but then a lot of manual work was required). 33 | * Wrapper objects are used to replicate C# `ref` (pass-by-reference) behaviour. This isn't very Java-esque but avoids an unmanageable refactoring effort. 34 | * Code passes all tests: polygon, line and polytree. 35 | * Uses lower-case (x, y) for point coordinates. 36 | * Private local variables have been renamed to their _camelCase_ variant but public methods (i.e. those of `Clipper.class`) retain their C# _PascalCase_ names (for now...). 37 | * Benchmarks can be run by including `jmh:benchmark` to the chosen maven goal. 38 | * `scanlineList` from `ClipperBase` uses Java `TreeSet` (variable renamed to `scanlineSet`). 39 | 40 | ## Benchmark 41 | _lightbringer's_ Java [port](https://github.com/lightbringer/clipper-java) of Clipper1 is benchmarked against this project in the benchmarks. *Clipper2-java* is faster, which becomes more pronounced input size grows. 42 | ``` 43 | Benchmark (edgeCount) Mode Cnt Score Error Units 44 | Clipper1.Intersection 1000 avgt 2 0.209 s/op 45 | Clipper1.Intersection 2000 avgt 2 1.123 s/op 46 | Clipper1.Intersection 4000 avgt 2 9.691 s/op 47 | Clipper2.Intersection 1000 avgt 2 0.130 s/op 48 | Clipper2.Intersection 2000 avgt 2 0.852 s/op 49 | Clipper2.Intersection 4000 avgt 2 3.465 s/op 50 | ``` 51 | -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | jdk: 2 | - openjdk17 3 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | micycle 6 | clipper2 7 | 1.3.1 8 | Clipper2 9 | 10 | 1.36 11 | UTF-8 12 | 13 | 14 | 15 | 16 | org.apache.maven.plugins 17 | maven-compiler-plugin 18 | 3.11.0 19 | 20 | 1.8 21 | 1.8 22 | 23 | 24 | org.openjdk.jmh 25 | jmh-generator-annprocess 26 | ${jmh.version} 27 | 28 | 29 | 30 | 31 | 32 | compile-java-9 33 | compile 34 | 35 | compile 36 | 37 | 38 | 9 39 | 40 | ${project.basedir}/src/main/java9 41 | 42 | true 43 | 44 | 45 | 46 | 47 | 48 | org.apache.maven.plugins 49 | maven-jar-plugin 50 | 3.3.0 51 | 52 | 53 | 54 | true 55 | 56 | 57 | 58 | 59 | 60 | pw.krejci 61 | jmh-maven-plugin 62 | 0.2.2 63 | 64 | 1.8 65 | 1.8 66 | 0 67 | 0 68 | 1 69 | 2 70 | avgt 71 | 1 72 | 73 | 74 | 75 | org.apache.maven.plugins 76 | maven-surefire-plugin 77 | 3.1.2 78 | 79 | 4 80 | false 81 | 82 | 83 | 84 | org.apache.maven.plugins 85 | maven-source-plugin 86 | 3.3.0 87 | 88 | 89 | attach-sources 90 | 91 | jar-no-fork 92 | 93 | 94 | 95 | 96 | 97 | org.apache.maven.plugins 98 | maven-javadoc-plugin 99 | 3.6.0 100 | 101 | false 102 | all 103 | 8 104 | 105 | 106 | 107 | attach-javadocs 108 | 109 | jar 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | jitpack.io 119 | https://jitpack.io 120 | 121 | 122 | 123 | 124 | org.junit.jupiter 125 | junit-jupiter 126 | 5.9.2 127 | test 128 | 129 | 130 | org.openjdk.jmh 131 | jmh-core 132 | ${jmh.version} 133 | test 134 | 135 | 136 | org.openjdk.jmh 137 | jmh-generator-annprocess 138 | ${jmh.version} 139 | test 140 | 141 | 142 | pw.krejci 143 | jmh-maven-plugin 144 | 0.2.2 145 | test 146 | 147 | 148 | com.github.jchamlin 149 | clipper-java 150 | b4dcd50c51 151 | test 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /src/main/java/clipper2/Minkowski.java: -------------------------------------------------------------------------------- 1 | package clipper2; 2 | 3 | import clipper2.core.FillRule; 4 | import clipper2.core.Path64; 5 | import clipper2.core.PathD; 6 | import clipper2.core.Paths64; 7 | import clipper2.core.PathsD; 8 | import clipper2.core.Point64; 9 | 10 | public class Minkowski { 11 | 12 | public static Paths64 Sum(Path64 pattern, Path64 path, boolean isClosed) { 13 | return Clipper.Union(MinkowskiInternal(pattern, path, true, isClosed), FillRule.NonZero); 14 | } 15 | 16 | public static PathsD Sum(PathD pattern, PathD path, boolean isClosed) { 17 | return Sum(pattern, path, isClosed, 2); 18 | } 19 | 20 | public static PathsD Sum(PathD pattern, PathD path, boolean isClosed, int decimalPlaces) { 21 | double scale = Math.pow(10, decimalPlaces); 22 | Paths64 tmp = Clipper.Union( 23 | MinkowskiInternal(Clipper.ScalePath64(pattern, scale), Clipper.ScalePath64(path, scale), true, isClosed), FillRule.NonZero); 24 | return Clipper.ScalePathsD(tmp, 1 / scale); 25 | } 26 | 27 | public static Paths64 Diff(Path64 pattern, Path64 path, boolean isClosed) { 28 | return Clipper.Union(MinkowskiInternal(pattern, path, false, isClosed), FillRule.NonZero); 29 | } 30 | 31 | public static PathsD Diff(PathD pattern, PathD path, boolean isClosed) { 32 | return Diff(pattern, path, isClosed, 2); 33 | } 34 | 35 | public static PathsD Diff(PathD pattern, PathD path, boolean isClosed, int decimalPlaces) { 36 | double scale = Math.pow(10, decimalPlaces); 37 | Paths64 tmp = Clipper.Union( 38 | MinkowskiInternal(Clipper.ScalePath64(pattern, scale), Clipper.ScalePath64(path, scale), false, isClosed), 39 | FillRule.NonZero); 40 | return Clipper.ScalePathsD(tmp, 1 / scale); 41 | } 42 | 43 | private static Paths64 MinkowskiInternal(Path64 pattern, Path64 path, boolean isSum, boolean isClosed) { 44 | int delta = isClosed ? 0 : 1; 45 | int patLen = pattern.size(), pathLen = path.size(); 46 | Paths64 tmp = new Paths64(pathLen); 47 | 48 | for (Point64 pathPt : path) { 49 | Path64 path2 = new Path64(patLen); 50 | if (isSum) { 51 | for (Point64 basePt : pattern) { 52 | path2.add(Point64.opAdd(pathPt, basePt)); 53 | } 54 | } else { 55 | for (Point64 basePt : pattern) { 56 | path2.add(Point64.opSubtract(pathPt, basePt)); 57 | } 58 | } 59 | tmp.add(path2); 60 | } 61 | 62 | Paths64 result = new Paths64((pathLen - delta) * patLen); 63 | int g = isClosed ? pathLen - 1 : 0; 64 | 65 | int h = patLen - 1; 66 | for (int i = delta; i < pathLen; i++) { 67 | for (int j = 0; j < patLen; j++) { 68 | Path64 quad = new Path64(tmp.get(g).get(h), tmp.get(i).get(h), tmp.get(i).get(j), tmp.get(g).get(j)); 69 | if (!Clipper.IsPositive(quad)) { 70 | result.add(Clipper.ReversePath(quad)); 71 | } else { 72 | result.add(quad); 73 | } 74 | h = j; 75 | } 76 | g = i; 77 | } 78 | return result; 79 | } 80 | 81 | } -------------------------------------------------------------------------------- /src/main/java/clipper2/Nullable.java: -------------------------------------------------------------------------------- 1 | package clipper2; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.ElementType; 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | import java.lang.annotation.Target; 8 | 9 | /** 10 | * The annotated element could be null under some circumstances. 11 | *

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 | *

19 | * 20 | * There are four boolean operations: 21 | * 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 | *

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 | *

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 | *

19 | * This function is extremely fast when compared to the Library's general 20 | * purpose Intersect clipper. Where Intersect has roughly O(n³) performance, 21 | * RectClip64 has O(n) performance. 22 | * 23 | * @since 1.0.6 24 | */ 25 | public class RectClip64 { 26 | 27 | // NOTE based on RectClip from Clipper2 v1.5.2 28 | 29 | protected enum Location { 30 | left, top, right, bottom, inside 31 | } 32 | 33 | protected final Rect64 rect_; 34 | protected final Point64 mp_; 35 | protected final Path64 rectPath_; 36 | protected Rect64 pathBounds_; 37 | protected List results_; 38 | protected List[] edges_; 39 | protected int currIdx_; 40 | 41 | public RectClip64(Rect64 rect) { 42 | currIdx_ = -1; 43 | rect_ = rect; 44 | mp_ = rect.MidPoint(); 45 | rectPath_ = rect.AsPath(); 46 | results_ = new ArrayList<>(); 47 | edges_ = new ArrayList[8]; 48 | for (int i = 0; i < 8; i++) { 49 | edges_[i] = new ArrayList<>(); 50 | } 51 | } 52 | 53 | protected OutPt2 add(Point64 pt) { 54 | return add(pt, false); 55 | } 56 | 57 | protected OutPt2 add(Point64 pt, boolean startingNewPath) { 58 | int curr = results_.size(); 59 | OutPt2 result; 60 | if (curr == 0 || startingNewPath) { 61 | result = new OutPt2(pt); 62 | results_.add(result); 63 | result.ownerIdx = curr; 64 | result.prev = result; 65 | result.next = result; 66 | } else { 67 | curr--; 68 | OutPt2 prevOp = results_.get(curr); 69 | if (prevOp.pt.equals(pt)) { 70 | return prevOp; 71 | } 72 | result = new OutPt2(pt); 73 | result.ownerIdx = curr; 74 | result.next = prevOp.next; 75 | prevOp.next.prev = result; 76 | prevOp.next = result; 77 | result.prev = prevOp; 78 | results_.set(curr, result); 79 | } 80 | return result; 81 | } 82 | 83 | private static boolean path1ContainsPath2(Path64 p1, Path64 p2) { 84 | int io = 0; 85 | for (Point64 pt : p2) { 86 | PointInPolygonResult pip = InternalClipper.PointInPolygon(pt, p1); 87 | switch (pip) { 88 | case IsInside : 89 | io--; 90 | break; 91 | case IsOutside : 92 | io++; 93 | break; 94 | } 95 | if (Math.abs(io) > 1) { 96 | break; 97 | } 98 | } 99 | return io <= 0; 100 | } 101 | 102 | private static boolean isClockwise(Location prev, Location curr, Point64 p1, Point64 p2, Point64 mid) { 103 | if (areOpposites(prev, curr)) { 104 | return InternalClipper.CrossProduct(p1, mid, p2) < 0; 105 | } 106 | return headingClockwise(prev, curr); 107 | } 108 | 109 | private static boolean areOpposites(Location a, Location b) { 110 | return Math.abs(a.ordinal() - b.ordinal()) == 2; 111 | } 112 | 113 | private static boolean headingClockwise(Location a, Location b) { 114 | return (a.ordinal() + 1) % 4 == b.ordinal(); 115 | } 116 | 117 | private static Location getAdjacentLocation(Location loc, boolean cw) { 118 | int d = cw ? 1 : 3; 119 | return Location.values()[(loc.ordinal() + d) % 4]; 120 | } 121 | 122 | private static OutPt2 unlinkOp(OutPt2 op) { 123 | if (op.next == op) { 124 | return null; 125 | } 126 | op.prev.next = op.next; 127 | op.next.prev = op.prev; 128 | return op.next; 129 | } 130 | 131 | private static OutPt2 unlinkOpBack(OutPt2 op) { 132 | if (op.next == op) { 133 | return null; 134 | } 135 | op.prev.next = op.next; 136 | op.next.prev = op.prev; 137 | return op.prev; 138 | } 139 | 140 | private static int getEdgesForPt(Point64 pt, Rect64 r) { 141 | int res = 0; 142 | if (pt.x == r.left) { 143 | res = 1; 144 | } else if (pt.x == r.right) { 145 | res = 4; 146 | } 147 | if (pt.y == r.top) { 148 | res += 2; 149 | } else if (pt.y == r.bottom) { 150 | res += 8; 151 | } 152 | return res; 153 | } 154 | 155 | private static boolean isHeadingClockwise(Point64 p1, Point64 p2, int idx) { 156 | switch (idx) { 157 | case 0 : 158 | return p2.y < p1.y; 159 | case 1 : 160 | return p2.x > p1.x; 161 | case 2 : 162 | return p2.y > p1.y; 163 | default : 164 | return p2.x < p1.x; 165 | } 166 | } 167 | 168 | private static boolean hasHorzOverlap(Point64 l1, Point64 r1, Point64 l2, Point64 r2) { 169 | return l1.x < r2.x && r1.x > l2.x; 170 | } 171 | 172 | private static boolean hasVertOverlap(Point64 t1, Point64 b1, Point64 t2, Point64 b2) { 173 | return t1.y < b2.y && b1.y > t2.y; 174 | } 175 | 176 | private static void addToEdge(List edge, OutPt2 op) { 177 | if (op.edge != null) { 178 | return; 179 | } 180 | op.edge = edge; 181 | edge.add(op); 182 | } 183 | 184 | private static void uncoupleEdge(OutPt2 op) { 185 | if (op.edge == null) { 186 | return; 187 | } 188 | List e = op.edge; 189 | for (int i = 0; i < e.size(); i++) { 190 | if (e.get(i) == op) { 191 | e.set(i, null); 192 | break; 193 | } 194 | } 195 | op.edge = null; 196 | } 197 | 198 | private static void setNewOwner(OutPt2 op, int idx) { 199 | op.ownerIdx = idx; 200 | OutPt2 o = op.next; 201 | while (o != op) { 202 | o.ownerIdx = idx; 203 | o = o.next; 204 | } 205 | } 206 | 207 | private void addCorner(Location prev, Location curr) { 208 | add(headingClockwise(prev, curr) ? rectPath_.get(prev.ordinal()) : rectPath_.get(curr.ordinal())); 209 | } 210 | 211 | private void addCorner(RefObject locRefObject, boolean cw) { 212 | if (cw) { 213 | add(rectPath_.get(locRefObject.argValue.ordinal())); 214 | locRefObject.argValue = getAdjacentLocation(locRefObject.argValue, true); 215 | } else { 216 | locRefObject.argValue = getAdjacentLocation(locRefObject.argValue, false); 217 | add(rectPath_.get(locRefObject.argValue.ordinal())); 218 | } 219 | } 220 | 221 | protected static boolean getLocation(Rect64 r, Point64 pt, RefObject locRefObject) { 222 | Location loc; 223 | if (pt.x == r.left && pt.y >= r.top && pt.y <= r.bottom) { 224 | locRefObject.argValue = Location.left; 225 | return false; 226 | } 227 | if (pt.x == r.right && pt.y >= r.top && pt.y <= r.bottom) { 228 | locRefObject.argValue = Location.right; 229 | return false; 230 | } 231 | if (pt.y == r.top && pt.x >= r.left && pt.x <= r.right) { 232 | locRefObject.argValue = Location.top; 233 | return false; 234 | } 235 | if (pt.y == r.bottom && pt.x >= r.left && pt.x <= r.right) { 236 | locRefObject.argValue = Location.bottom; 237 | return false; 238 | } 239 | if (pt.x < r.left) { 240 | loc = Location.left; 241 | } else if (pt.x > r.right) { 242 | loc = Location.right; 243 | } else if (pt.y < r.top) { 244 | loc = Location.top; 245 | } else if (pt.y > r.bottom) { 246 | loc = Location.bottom; 247 | } else { 248 | loc = Location.inside; 249 | } 250 | locRefObject.argValue = loc; 251 | return true; 252 | } 253 | 254 | private static boolean isHorizontal(Point64 a, Point64 b) { 255 | return a.y == b.y; 256 | } 257 | 258 | private static boolean getSegmentIntersection(Point64 p1, Point64 p2, Point64 p3, Point64 p4, Point64 ipRefObject) { 259 | double r1 = InternalClipper.CrossProduct(p1, p3, p4); 260 | double r2 = InternalClipper.CrossProduct(p2, p3, p4); 261 | if (r1 == 0) { 262 | ipRefObject.set(p1); 263 | if (r2 == 0) { 264 | return false; 265 | } 266 | if (p1.equals(p3) || p1.equals(p4)) { 267 | return true; 268 | } 269 | if (isHorizontal(p3, p4)) { 270 | return (p1.x > p3.x) == (p1.x < p4.x); 271 | } 272 | return (p1.y > p3.y) == (p1.y < p4.y); 273 | } 274 | if (r2 == 0) { 275 | ipRefObject.set(p2); 276 | if (p2.equals(p3) || p2.equals(p4)) { 277 | return true; 278 | } 279 | if (isHorizontal(p3, p4)) { 280 | return (p2.x > p3.x) == (p2.x < p4.x); 281 | } 282 | return (p2.y > p3.y) == (p2.y < p4.y); 283 | } 284 | if ((r1 > 0) == (r2 > 0)) { 285 | ipRefObject.set(new Point64(0, 0)); 286 | return false; 287 | } 288 | double r3 = InternalClipper.CrossProduct(p3, p1, p2); 289 | double r4 = InternalClipper.CrossProduct(p4, p1, p2); 290 | if (r3 == 0) { 291 | ipRefObject.set(p3); 292 | if (p3.equals(p1) || p3.equals(p2)) { 293 | return true; 294 | } 295 | if (isHorizontal(p1, p2)) { 296 | return (p3.x > p1.x) == (p3.x < p2.x); 297 | } 298 | return (p3.y > p1.y) == (p3.y < p2.y); 299 | } 300 | if (r4 == 0) { 301 | ipRefObject.set(p4); 302 | if (p4.equals(p1) || p4.equals(p2)) { 303 | return true; 304 | } 305 | if (isHorizontal(p1, p2)) { 306 | return (p4.x > p1.x) == (p4.x < p2.x); 307 | } 308 | return (p4.y > p1.y) == (p4.y < p2.y); 309 | } 310 | if ((r3 > 0) == (r4 > 0)) { 311 | ipRefObject.set(new Point64(0, 0)); 312 | return false; 313 | } 314 | return InternalClipper.GetIntersectPoint(p1, p2, p3, p4, ipRefObject); 315 | } 316 | 317 | protected static boolean getIntersection(Path64 rectPath, Point64 p, Point64 p2, RefObject locRefObject, Point64 ipRefObject) { 318 | ipRefObject.set(new Point64(0, 0)); 319 | switch (locRefObject.argValue) { 320 | case left : 321 | if (getSegmentIntersection(p, p2, rectPath.get(0), rectPath.get(3), ipRefObject)) { 322 | return true; 323 | } 324 | if (p.y < rectPath.get(0).y && getSegmentIntersection(p, p2, rectPath.get(0), rectPath.get(1), ipRefObject)) { 325 | locRefObject.argValue = Location.top; 326 | return true; 327 | } 328 | if (!getSegmentIntersection(p, p2, rectPath.get(2), rectPath.get(3), ipRefObject)) { 329 | return false; 330 | } 331 | locRefObject.argValue = Location.bottom; 332 | return true; 333 | case right : 334 | if (getSegmentIntersection(p, p2, rectPath.get(1), rectPath.get(2), ipRefObject)) { 335 | return true; 336 | } 337 | if (p.y < rectPath.get(0).y && getSegmentIntersection(p, p2, rectPath.get(0), rectPath.get(1), ipRefObject)) { 338 | locRefObject.argValue = Location.top; 339 | return true; 340 | } 341 | if (!getSegmentIntersection(p, p2, rectPath.get(2), rectPath.get(3), ipRefObject)) { 342 | return false; 343 | } 344 | locRefObject.argValue = Location.bottom; 345 | return true; 346 | case top : 347 | if (getSegmentIntersection(p, p2, rectPath.get(0), rectPath.get(1), ipRefObject)) { 348 | return true; 349 | } 350 | if (p.x < rectPath.get(0).x && getSegmentIntersection(p, p2, rectPath.get(0), rectPath.get(3), ipRefObject)) { 351 | locRefObject.argValue = Location.left; 352 | return true; 353 | } 354 | if (p.x <= rectPath.get(1).x || !getSegmentIntersection(p, p2, rectPath.get(1), rectPath.get(2), ipRefObject)) { 355 | return false; 356 | } 357 | locRefObject.argValue = Location.right; 358 | return true; 359 | case bottom : 360 | if (getSegmentIntersection(p, p2, rectPath.get(2), rectPath.get(3), ipRefObject)) { 361 | return true; 362 | } 363 | if (p.x < rectPath.get(3).x && getSegmentIntersection(p, p2, rectPath.get(0), rectPath.get(3), ipRefObject)) { 364 | locRefObject.argValue = Location.left; 365 | return true; 366 | } 367 | if (p.x <= rectPath.get(2).x || !getSegmentIntersection(p, p2, rectPath.get(1), rectPath.get(2), ipRefObject)) { 368 | return false; 369 | } 370 | locRefObject.argValue = Location.right; 371 | return true; 372 | default : 373 | if (getSegmentIntersection(p, p2, rectPath.get(0), rectPath.get(3), ipRefObject)) { 374 | locRefObject.argValue = Location.left; 375 | return true; 376 | } 377 | if (getSegmentIntersection(p, p2, rectPath.get(0), rectPath.get(1), ipRefObject)) { 378 | locRefObject.argValue = Location.top; 379 | return true; 380 | } 381 | if (getSegmentIntersection(p, p2, rectPath.get(1), rectPath.get(2), ipRefObject)) { 382 | locRefObject.argValue = Location.right; 383 | return true; 384 | } 385 | if (!getSegmentIntersection(p, p2, rectPath.get(2), rectPath.get(3), ipRefObject)) { 386 | return false; 387 | } 388 | locRefObject.argValue = Location.bottom; 389 | return true; 390 | } 391 | } 392 | 393 | protected void getNextLocation(Path64 path, RefObject locRefObject, RefObject iRefObject, int highI) { 394 | Location loc = locRefObject.argValue; 395 | int i = iRefObject.argValue; 396 | switch (loc) { 397 | case left : 398 | while (i <= highI && path.get(i).x <= rect_.left) { 399 | i++; 400 | } 401 | if (i <= highI) { 402 | if (path.get(i).x >= rect_.right) { 403 | loc = Location.right; 404 | } else if (path.get(i).y <= rect_.top) { 405 | loc = Location.top; 406 | } else if (path.get(i).y >= rect_.bottom) { 407 | loc = Location.bottom; 408 | } else { 409 | loc = Location.inside; 410 | } 411 | } 412 | break; 413 | case top : 414 | while (i <= highI && path.get(i).y <= rect_.top) { 415 | i++; 416 | } 417 | if (i <= highI) { 418 | if (path.get(i).y >= rect_.bottom) { 419 | loc = Location.bottom; 420 | } else if (path.get(i).x <= rect_.left) { 421 | loc = Location.left; 422 | } else if (path.get(i).x >= rect_.right) { 423 | loc = Location.right; 424 | } else { 425 | loc = Location.inside; 426 | } 427 | } 428 | break; 429 | case right : 430 | while (i <= highI && path.get(i).x >= rect_.right) { 431 | i++; 432 | } 433 | if (i <= highI) { 434 | if (path.get(i).x <= rect_.left) { 435 | loc = Location.left; 436 | } else if (path.get(i).y <= rect_.top) { 437 | loc = Location.top; 438 | } else if (path.get(i).y >= rect_.bottom) { 439 | loc = Location.bottom; 440 | } else { 441 | loc = Location.inside; 442 | } 443 | } 444 | break; 445 | case bottom : 446 | while (i <= highI && path.get(i).y >= rect_.bottom) { 447 | i++; 448 | } 449 | if (i <= highI) { 450 | if (path.get(i).y <= rect_.top) { 451 | loc = Location.top; 452 | } else if (path.get(i).x <= rect_.left) { 453 | loc = Location.left; 454 | } else if (path.get(i).x >= rect_.right) { 455 | loc = Location.right; 456 | } else { 457 | loc = Location.inside; 458 | } 459 | } 460 | break; 461 | case inside : 462 | while (i <= highI) { 463 | Point64 pt = path.get(i); 464 | if (pt.x < rect_.left) { 465 | loc = Location.left; 466 | break; 467 | } else if (pt.x > rect_.right) { 468 | loc = Location.right; 469 | break; 470 | } else if (pt.y > rect_.bottom) { 471 | loc = Location.bottom; 472 | break; 473 | } else if (pt.y < rect_.top) { 474 | loc = Location.top; 475 | break; 476 | } else { 477 | add(pt); 478 | i++; 479 | continue; 480 | } 481 | } 482 | break; 483 | } 484 | locRefObject.argValue = loc; 485 | iRefObject.argValue = i; 486 | } 487 | 488 | private static boolean startLocsAreClockwise(List locs) { 489 | int res = 0; 490 | for (int i = 1; i < locs.size(); i++) { 491 | int d = locs.get(i).ordinal() - locs.get(i - 1).ordinal(); 492 | switch (d) { 493 | case -1 : 494 | res--; 495 | break; 496 | case 1 : 497 | res++; 498 | break; 499 | case -3 : 500 | res++; 501 | break; 502 | case 3 : 503 | res--; 504 | break; 505 | } 506 | } 507 | return res > 0; 508 | } 509 | 510 | protected void executeInternal(Path64 path) { 511 | if (path.size() < 3 || rect_.IsEmpty()) { 512 | return; 513 | } 514 | 515 | // ––– setup 516 | List startLocs = new ArrayList<>(); 517 | Location firstCross = Location.inside; 518 | Location crossingLoc = Location.inside; 519 | Location prev = Location.inside; 520 | 521 | int highI = path.size() - 1; 522 | RefObject locRefObject = new RefObject<>(); 523 | 524 | // find the location of the last point 525 | if (!getLocation(rect_, path.get(highI), locRefObject)) { 526 | prev = locRefObject.argValue; 527 | int j = highI - 1; 528 | RefObject prevRefObject = new RefObject<>(prev); 529 | while (j >= 0 && !getLocation(rect_, path.get(j), prevRefObject)) { 530 | j--; 531 | } 532 | if (j < 0) { 533 | // never touched the rect at all 534 | for (Point64 pt : path) { 535 | add(pt); 536 | } 537 | return; 538 | } 539 | prev = prevRefObject.argValue; 540 | if (prev == Location.inside) { 541 | locRefObject.argValue = Location.inside; 542 | } 543 | } 544 | 545 | // **capture the very first loc** for the tail‐end test 546 | Location startingLoc = locRefObject.argValue; 547 | 548 | // ––– main loop 549 | int i = 0; 550 | while (i <= highI) { 551 | prev = locRefObject.argValue; 552 | Location prevCrossLoc = crossingLoc; 553 | 554 | // advance i to the next index where the rect‐location changes 555 | RefObject iRefObject = new RefObject<>(i); 556 | getNextLocation(path, locRefObject, iRefObject, highI); 557 | i = iRefObject.argValue; 558 | if (i > highI) { 559 | break; 560 | } 561 | 562 | // current segment runs from path[i-1] to path[i] 563 | Point64 prevPt = (i == 0) ? path.get(highI) : path.get(i - 1); 564 | crossingLoc = locRefObject.argValue; 565 | 566 | // see if that segment hits the rectangle boundary 567 | RefObject crossRefObject = new RefObject<>(crossingLoc); 568 | Point64 ipRefObject = new Point64(); 569 | if (!getIntersection(rectPath_, path.get(i), prevPt, crossRefObject, ipRefObject)) { 570 | // still entirely outside 571 | crossingLoc = crossRefObject.argValue; 572 | if (prevCrossLoc == Location.inside) { 573 | boolean cw = isClockwise(prev, locRefObject.argValue, prevPt, path.get(i), mp_); 574 | do { 575 | startLocs.add(prev); 576 | prev = getAdjacentLocation(prev, cw); 577 | } while (prev != locRefObject.argValue); 578 | crossingLoc = prevCrossLoc; 579 | } else if (prev != Location.inside && prev != locRefObject.argValue) { 580 | boolean cw = isClockwise(prev, locRefObject.argValue, prevPt, path.get(i), mp_); 581 | RefObject pRefObject = new RefObject<>(prev); 582 | do { 583 | addCorner(pRefObject, cw); 584 | prev = pRefObject.argValue; 585 | } while (prev != locRefObject.argValue); 586 | } 587 | 588 | // **only place we increment i in the no‐intersection case** 589 | i++; 590 | continue; 591 | } 592 | 593 | // we *did* intersect 594 | crossingLoc = crossRefObject.argValue; 595 | Point64 ip = ipRefObject; 596 | 597 | if (locRefObject.argValue == Location.inside) { 598 | // entering rectangle 599 | if (firstCross == Location.inside) { 600 | firstCross = crossingLoc; 601 | startLocs.add(prev); 602 | } else if (prev != crossingLoc) { 603 | boolean cw = isClockwise(prev, crossingLoc, prevPt, path.get(i), mp_); 604 | RefObject pRefObject = new RefObject<>(prev); 605 | do { 606 | addCorner(pRefObject, cw); 607 | prev = pRefObject.argValue; 608 | } while (prev != crossingLoc); 609 | } 610 | } else if (prev != Location.inside) { 611 | // passing all the way through 612 | RefObject loc2RefObject = new RefObject<>(prev); 613 | Point64 ip2RefObject = new Point64(); 614 | getIntersection(rectPath_, prevPt, path.get(i), loc2RefObject, ip2RefObject); 615 | Location newLoc = loc2RefObject.argValue; 616 | 617 | if (prevCrossLoc != Location.inside && prevCrossLoc != newLoc) { 618 | addCorner(prevCrossLoc, newLoc); 619 | } 620 | if (firstCross == Location.inside) { 621 | firstCross = newLoc; 622 | startLocs.add(prev); 623 | } 624 | locRefObject.argValue = crossingLoc; 625 | add(ip2RefObject); 626 | 627 | if (ip.equals(ip2RefObject)) { 628 | RefObject tmpRefObject = new RefObject<>(crossingLoc); 629 | RefObject onRectRefObject = new RefObject<>(); 630 | getLocation(rect_, path.get(i), onRectRefObject); 631 | addCorner(tmpRefObject, headingClockwise(tmpRefObject.argValue, onRectRefObject.argValue)); 632 | crossingLoc = tmpRefObject.argValue; 633 | 634 | i++; 635 | continue; 636 | } 637 | } else { 638 | // exiting rectangle 639 | locRefObject.argValue = crossingLoc; 640 | if (firstCross == Location.inside) { 641 | firstCross = crossingLoc; 642 | } 643 | } 644 | 645 | // add the intersection point 646 | add(ip); 647 | 648 | // no other explicit i++ here; getNextLocation will advance on the next loop 649 | } 650 | 651 | // ––– tail‐end logic (unchanged) 652 | if (firstCross == Location.inside) { 653 | // never intersected 654 | if (startingLoc == Location.inside || !pathBounds_.Contains(rect_) || !path1ContainsPath2(path, rectPath_)) { 655 | return; 656 | } 657 | 658 | boolean cw = startLocsAreClockwise(startLocs); 659 | for (int j = 0; j < 4; j++) { 660 | int k = cw ? j : 3 - j; 661 | add(rectPath_.get(k)); 662 | addToEdge(edges_[k * 2], results_.get(0)); 663 | } 664 | } else if (locRefObject.argValue != Location.inside && (locRefObject.argValue != firstCross || startLocs.size() > 2)) { 665 | if (!startLocs.isEmpty()) { 666 | prev = locRefObject.argValue; 667 | for (Location loc2 : startLocs) { 668 | if (prev == loc2) { 669 | continue; 670 | } 671 | boolean c = headingClockwise(prev, loc2); 672 | RefObject pRefObject = new RefObject<>(prev); 673 | addCorner(pRefObject, c); 674 | prev = pRefObject.argValue; 675 | } 676 | locRefObject.argValue = prev; 677 | } 678 | if (locRefObject.argValue != firstCross) { 679 | RefObject pRefObject = new RefObject<>(locRefObject.argValue); 680 | addCorner(pRefObject, headingClockwise(locRefObject.argValue, firstCross)); 681 | } 682 | } 683 | } 684 | 685 | public Paths64 Execute(List paths) { 686 | Paths64 res = new Paths64(); 687 | if (rect_.IsEmpty()) { 688 | return res; 689 | } 690 | for (Path64 path : paths) { 691 | if (path.size() < 3) { 692 | continue; 693 | } 694 | pathBounds_ = Clipper.GetBounds(path); 695 | if (!rect_.Intersects(pathBounds_)) { 696 | continue; 697 | } 698 | if (rect_.Contains(pathBounds_)) { 699 | res.add(path); 700 | continue; 701 | } 702 | executeInternal(path); 703 | checkEdges(); 704 | for (int i = 0; i < 4; i++) { 705 | tidyEdgePair(i, edges_[i * 2], edges_[i * 2 + 1]); 706 | } 707 | for (OutPt2 op : results_) { 708 | Path64 tmp = getPath(op); 709 | if (!tmp.isEmpty()) { 710 | res.add(tmp); 711 | } 712 | } 713 | results_.clear(); 714 | for (int i = 0; i < 8; i++) { 715 | edges_[i].clear(); 716 | } 717 | } 718 | return res; 719 | } 720 | 721 | private void checkEdges() { 722 | for (int i = 0; i < results_.size(); i++) { 723 | OutPt2 op = results_.get(i); 724 | if (op == null) { 725 | continue; 726 | } 727 | OutPt2 o2 = op; 728 | do { 729 | if (InternalClipper.IsCollinear(o2.prev.pt, o2.pt, o2.next.pt)) { 730 | if (o2 == op) { 731 | o2 = unlinkOpBack(o2); 732 | if (o2 == null) { 733 | break; 734 | } 735 | op = o2.prev; 736 | } else { 737 | o2 = unlinkOpBack(o2); 738 | if (o2 == null) { 739 | break; 740 | } 741 | } 742 | } else { 743 | o2 = o2.next; 744 | } 745 | } while (o2 != op); 746 | if (o2 == null) { 747 | results_.set(i, null); 748 | continue; 749 | } 750 | results_.set(i, o2); 751 | int e1 = getEdgesForPt(op.prev.pt, rect_); 752 | o2 = op; 753 | do { 754 | int e2 = getEdgesForPt(o2.pt, rect_); 755 | if (e2 != 0 && o2.edge == null) { 756 | int comb = e1 & e2; 757 | for (int j = 0; j < 4; j++) { 758 | if ((comb & (1 << j)) == 0) { 759 | continue; 760 | } 761 | if (isHeadingClockwise(o2.prev.pt, o2.pt, j)) { 762 | addToEdge(edges_[j * 2], o2); 763 | } else { 764 | addToEdge(edges_[j * 2 + 1], o2); 765 | } 766 | } 767 | } 768 | e1 = e2; 769 | o2 = o2.next; 770 | } while (o2 != op); 771 | } 772 | } 773 | 774 | private void tidyEdgePair(int idx, List cw, List ccw) { 775 | if (ccw.isEmpty()) { 776 | return; 777 | } 778 | boolean isH = (idx == 1 || idx == 3); 779 | boolean cwL = (idx == 1 || idx == 2); 780 | int i = 0, j = 0; 781 | while (i < cw.size()) { 782 | OutPt2 p1 = cw.get(i); 783 | if (p1 == null || p1.next == p1.prev) { 784 | cw.set(i, null); 785 | j = 0; 786 | i++; 787 | continue; 788 | } 789 | while (j < ccw.size() && (ccw.get(j) == null || ccw.get(j).next == ccw.get(j).prev)) { 790 | j++; 791 | } 792 | if (j == ccw.size()) { 793 | i++; 794 | j = 0; 795 | continue; 796 | } 797 | OutPt2 p2, p1a, p2a; 798 | if (cwL) { 799 | p1 = cw.get(i).prev; 800 | p1a = cw.get(i); 801 | p2 = ccw.get(j); 802 | p2a = ccw.get(j).prev; 803 | } else { 804 | p1 = cw.get(i); 805 | p1a = cw.get(i).prev; 806 | p2 = ccw.get(j).prev; 807 | p2a = ccw.get(j); 808 | } 809 | if ((isH && !hasHorzOverlap(p1.pt, p1a.pt, p2.pt, p2a.pt)) || (!isH && !hasVertOverlap(p1.pt, p1a.pt, p2.pt, p2a.pt))) { 810 | j++; 811 | continue; 812 | } 813 | boolean rejoin = p1a.ownerIdx != p2.ownerIdx; 814 | if (rejoin) { 815 | results_.set(p2.ownerIdx, null); 816 | setNewOwner(p2, p1a.ownerIdx); 817 | } 818 | if (cwL) { 819 | p1.next = p2; 820 | p2.prev = p1; 821 | p1a.prev = p2a; 822 | p2a.next = p1a; 823 | } else { 824 | p1.prev = p2; 825 | p2.next = p1; 826 | p1a.next = p2a; 827 | p2a.prev = p1a; 828 | } 829 | if (!rejoin) { 830 | int ni = results_.size(); 831 | results_.add(p1a); 832 | setNewOwner(p1a, ni); 833 | } 834 | OutPt2 o, o2; 835 | if (cwL) { 836 | o = p2; 837 | o2 = p1a; 838 | } else { 839 | o = p1; 840 | o2 = p2a; 841 | } 842 | results_.set(o.ownerIdx, o); 843 | results_.set(o2.ownerIdx, o2); 844 | boolean oL, o2L; 845 | if (isH) { 846 | oL = o.pt.x > o.prev.pt.x; 847 | o2L = o2.pt.x > o2.prev.pt.x; 848 | } else { 849 | oL = o.pt.y > o.prev.pt.y; 850 | o2L = o2.pt.y > o2.prev.pt.y; 851 | } 852 | if (o.next == o.prev || o.pt.equals(o.prev.pt)) { 853 | if (o2L == cwL) { 854 | cw.set(i, o2); 855 | ccw.set(j, null); 856 | } else { 857 | ccw.set(j, o2); 858 | cw.set(i, null); 859 | } 860 | } else if (o2.next == o2.prev || o2.pt.equals(o2.prev.pt)) { 861 | if (oL == cwL) { 862 | cw.set(i, o); 863 | ccw.set(j, null); 864 | } else { 865 | ccw.set(j, o); 866 | cw.set(i, null); 867 | } 868 | } else if (oL == o2L) { 869 | if (oL == cwL) { 870 | cw.set(i, o); 871 | uncoupleEdge(o2); 872 | addToEdge(cw, o2); 873 | ccw.set(j, null); 874 | } else { 875 | cw.set(i, null); 876 | ccw.set(j, o2); 877 | uncoupleEdge(o); 878 | addToEdge(ccw, o); 879 | j = 0; 880 | } 881 | } else { 882 | if (oL == cwL) { 883 | cw.set(i, o); 884 | } else { 885 | ccw.set(j, o); 886 | } 887 | if (o2L == cwL) { 888 | cw.set(i, o2); 889 | } else { 890 | ccw.set(j, o2); 891 | } 892 | } 893 | } 894 | } 895 | 896 | private static Path64 getPath(OutPt2 op) { 897 | Path64 res = new Path64(); 898 | if (op == null || op.prev == op.next) { 899 | return res; 900 | } 901 | OutPt2 start = op.next; 902 | while (start != null && start != op) { 903 | if (InternalClipper.IsCollinear(start.prev.pt, start.pt, start.next.pt)) { 904 | op = start.prev; 905 | start = unlinkOp(start); 906 | } else { 907 | start = start.next; 908 | } 909 | } 910 | if (start == null) { 911 | return new Path64(); 912 | } 913 | res.add(op.pt); 914 | OutPt2 p2 = op.next; 915 | while (p2 != op) { 916 | res.add(p2.pt); 917 | p2 = p2.next; 918 | } 919 | return res; 920 | } 921 | 922 | class OutPt2 { 923 | public OutPt2 next; 924 | public OutPt2 prev; 925 | public Point64 pt; 926 | public int ownerIdx; 927 | public List edge; 928 | 929 | public OutPt2(Point64 pt) { 930 | this.pt = pt; 931 | } 932 | } 933 | } 934 | -------------------------------------------------------------------------------- /src/main/java/clipper2/rectclip/RectClipLines64.java: -------------------------------------------------------------------------------- 1 | package clipper2.rectclip; 2 | 3 | import clipper2.Clipper; 4 | import clipper2.core.Path64; 5 | import clipper2.core.Paths64; 6 | import clipper2.core.Point64; 7 | import clipper2.core.Rect64; 8 | import tangible.RefObject; 9 | 10 | /** 11 | * RectClipLines64 intersects subject open paths (polylines) with the specified 12 | * rectangular clipping region. 13 | *

14 | * This function is extremely fast when compared to the Library's general 15 | * purpose Intersect clipper. Where Intersect has roughly O(n³) performance, 16 | * RectClipLines64 has O(n) performance. 17 | * 18 | * @since 1.0.6 19 | */ 20 | public class RectClipLines64 extends RectClip64 { 21 | 22 | public RectClipLines64(Rect64 rect) { 23 | super(rect); 24 | } 25 | 26 | public Paths64 Execute(Paths64 paths) { 27 | Paths64 res = new Paths64(); 28 | if (rect_.IsEmpty()) { 29 | return res; 30 | } 31 | for (Path64 path : paths) { 32 | if (path.size() < 2) { 33 | continue; 34 | } 35 | pathBounds_ = Clipper.GetBounds(path); 36 | if (!rect_.Intersects(pathBounds_)) { 37 | continue; 38 | } 39 | executeInternal(path); 40 | for (OutPt2 op : results_) { 41 | Path64 tmp = getPath(op); 42 | if (!tmp.isEmpty()) { 43 | res.add(tmp); 44 | } 45 | } 46 | results_.clear(); 47 | for (int i = 0; i < 8; i++) { 48 | edges_[i].clear(); 49 | } 50 | } 51 | return res; 52 | } 53 | 54 | private static Path64 getPath(OutPt2 op) { 55 | Path64 res = new Path64(); 56 | if (op == null || op == op.next) { 57 | return res; 58 | } 59 | op = op.next; 60 | res.add(op.pt); 61 | OutPt2 p2 = op.next; 62 | while (p2 != op) { 63 | res.add(p2.pt); 64 | p2 = p2.next; 65 | } 66 | return res; 67 | } 68 | 69 | @Override 70 | protected void executeInternal(Path64 path) { 71 | results_.clear(); 72 | if (path.size() < 2 || rect_.IsEmpty()) { 73 | return; 74 | } 75 | RefObject locRefObject = new RefObject<>(Location.inside); 76 | int i = 1, highI = path.size() - 1; 77 | if (!getLocation(rect_, path.get(0), locRefObject)) { 78 | RefObject prevRefObject = new RefObject<>(locRefObject.argValue); 79 | while (i <= highI && !getLocation(rect_, path.get(i), prevRefObject)) { 80 | i++; 81 | } 82 | if (i > highI) { 83 | for (Point64 pt : path) { 84 | add(pt); 85 | } 86 | return; 87 | } 88 | if (prevRefObject.argValue == Location.inside) { 89 | locRefObject.argValue = Location.inside; 90 | } 91 | i = 1; 92 | } 93 | if (locRefObject.argValue == Location.inside) { 94 | add(path.get(0)); 95 | } 96 | while (i <= highI) { 97 | Location prev = locRefObject.argValue; 98 | RefObject iRefObject = new RefObject<>(i); 99 | getNextLocation(path, locRefObject, iRefObject, highI); 100 | i = iRefObject.argValue; 101 | if (i > highI) { 102 | break; 103 | } 104 | Point64 prevPt = path.get(i - 1); 105 | RefObject crossRefObject = new RefObject<>(locRefObject.argValue); 106 | Point64 ipRefObject = new Point64(); 107 | if (!getIntersection(rectPath_, path.get(i), prevPt, crossRefObject, ipRefObject)) { 108 | i++; 109 | continue; 110 | } 111 | Point64 ip = ipRefObject; 112 | if (locRefObject.argValue == Location.inside) { 113 | add(ip, true); 114 | } else if (prev != Location.inside) { 115 | Point64 ip2RefObject = new Point64(); 116 | getIntersection(rectPath_, prevPt, path.get(i), new RefObject<>(prev), ip2RefObject); 117 | add(ip2RefObject, true); 118 | add(ip, true); 119 | } else { 120 | add(ip); 121 | } 122 | i++; 123 | } 124 | } 125 | } -------------------------------------------------------------------------------- /src/main/java/clipper2/rectclip/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This unit contains the code that implements the RectClip64 functions found in 3 | * the Clipper Unit. 4 | * 5 | * @since 1.0.6 6 | */ 7 | package clipper2.rectclip; -------------------------------------------------------------------------------- /src/main/java/tangible/OutObject.java: -------------------------------------------------------------------------------- 1 | package tangible; 2 | 3 | //---------------------------------------------------------------------------------------- 4 | // Copyright © 2007 - 2020 Tangible Software Solutions, Inc. 5 | // This class can be used by anyone provided that the copyright notice remains intact. 6 | // 7 | // This class is used to replicate the ability to have 'out' parameters in Java. 8 | //---------------------------------------------------------------------------------------- 9 | public class OutObject { 10 | public T argValue; 11 | } -------------------------------------------------------------------------------- /src/main/java/tangible/RefObject.java: -------------------------------------------------------------------------------- 1 | package tangible; 2 | 3 | //---------------------------------------------------------------------------------------- 4 | // Copyright © 2007 - 2020 Tangible Software Solutions, Inc. 5 | // This class can be used by anyone provided that the copyright notice remains intact. 6 | // 7 | // This class is used to replicate the ability to pass arguments by reference in Java. 8 | //---------------------------------------------------------------------------------------- 9 | public final class RefObject extends OutObject { 10 | public T argValue; 11 | 12 | public RefObject() { 13 | } 14 | 15 | public RefObject(T refArg) { 16 | argValue = refArg; 17 | } 18 | } -------------------------------------------------------------------------------- /src/main/java9/module-info.java: -------------------------------------------------------------------------------- 1 | module clipper2 { 2 | exports clipper2; 3 | exports clipper2.core; 4 | exports clipper2.engine; 5 | exports clipper2.offset; 6 | exports clipper2.rectclip; 7 | 8 | exports tangible; 9 | } -------------------------------------------------------------------------------- /src/test/java/clipper2/BenchmarkClipper1.java: -------------------------------------------------------------------------------- 1 | package clipper2; 2 | 3 | import java.util.Random; 4 | import java.util.concurrent.TimeUnit; 5 | 6 | import org.openjdk.jmh.annotations.Benchmark; 7 | import org.openjdk.jmh.annotations.Level; 8 | import org.openjdk.jmh.annotations.Measurement; 9 | import org.openjdk.jmh.annotations.OutputTimeUnit; 10 | import org.openjdk.jmh.annotations.Param; 11 | import org.openjdk.jmh.annotations.Scope; 12 | import org.openjdk.jmh.annotations.Setup; 13 | import org.openjdk.jmh.annotations.State; 14 | 15 | import de.lighti.clipper.Clipper.ClipType; 16 | import de.lighti.clipper.Clipper.PolyFillType; 17 | import de.lighti.clipper.Clipper.PolyType; 18 | import de.lighti.clipper.DefaultClipper; 19 | import de.lighti.clipper.Path; 20 | import de.lighti.clipper.Paths; 21 | import de.lighti.clipper.Point.LongPoint; 22 | 23 | /** 24 | * Benchmarks for Clipper 1. Class is located within test folder in order to be 25 | * found by jmh-maven-plugin. 26 | */ 27 | @Measurement(time = 3) 28 | public class BenchmarkClipper1 { 29 | 30 | private static final int DisplayWidth = 800; 31 | private static final int DisplayHeight = 600; 32 | 33 | private static long seed = 0; 34 | 35 | @State(Scope.Benchmark) 36 | public static class BenchmarkState { 37 | 38 | Paths subj; 39 | Paths clip; 40 | Paths solution; 41 | 42 | @Param({ "1000", "2000", "4000" }) 43 | public int edgeCount; 44 | 45 | @Setup(Level.Invocation) // recreate path for each edgeCount run 46 | public void setup() { 47 | Random rand = new Random(seed++); 48 | 49 | subj = new Paths(); 50 | clip = new Paths(); 51 | solution = new Paths(); 52 | 53 | subj.add(MakeRandomPath(DisplayWidth, DisplayHeight, edgeCount, rand)); 54 | clip.add(MakeRandomPath(DisplayWidth, DisplayHeight, edgeCount, rand)); 55 | } 56 | 57 | } 58 | 59 | @Benchmark 60 | @OutputTimeUnit(TimeUnit.SECONDS) 61 | public void Intersection(BenchmarkState state) { 62 | DefaultClipper c = new DefaultClipper(); 63 | c.addPaths(state.subj, PolyType.SUBJECT, true); 64 | c.addPaths(state.clip, PolyType.CLIP, true); 65 | c.execute(ClipType.INTERSECTION, state.solution, PolyFillType.NON_ZERO, PolyFillType.NON_ZERO); 66 | } 67 | 68 | private static LongPoint MakeRandomPt(int maxWidth, int maxHeight, Random rand) { 69 | long x = rand.nextInt(maxWidth); 70 | long y = rand.nextInt(maxHeight); 71 | return new LongPoint(x, y); 72 | } 73 | 74 | private static Path MakeRandomPath(int width, int height, int count, Random rand) { 75 | Path result = new Path(count); 76 | for (int i = 0; i < count; ++i) { 77 | result.add(MakeRandomPt(width, height, rand)); 78 | } 79 | return result; 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /src/test/java/clipper2/BenchmarkClipper2.java: -------------------------------------------------------------------------------- 1 | package clipper2; 2 | 3 | import java.util.Random; 4 | import java.util.concurrent.TimeUnit; 5 | 6 | import org.openjdk.jmh.annotations.Benchmark; 7 | import org.openjdk.jmh.annotations.Level; 8 | import org.openjdk.jmh.annotations.Measurement; 9 | import org.openjdk.jmh.annotations.OutputTimeUnit; 10 | import org.openjdk.jmh.annotations.Param; 11 | import org.openjdk.jmh.annotations.Scope; 12 | import org.openjdk.jmh.annotations.Setup; 13 | import org.openjdk.jmh.annotations.State; 14 | 15 | import clipper2.core.ClipType; 16 | import clipper2.core.FillRule; 17 | import clipper2.core.Path64; 18 | import clipper2.core.Paths64; 19 | import clipper2.core.Point64; 20 | import clipper2.engine.Clipper64; 21 | 22 | /** 23 | * Benchmarks for Clipper 2. Class is located within test folder in order to be 24 | * found by jmh-maven-plugin. 25 | */ 26 | @Measurement(time = 3) // run each benchmark continuously upto 3s 27 | public class BenchmarkClipper2 { 28 | 29 | private static final int DisplayWidth = 800; 30 | private static final int DisplayHeight = 600; 31 | 32 | private static long seed = 0; 33 | 34 | @State(Scope.Benchmark) 35 | public static class BenchmarkState { 36 | 37 | Paths64 subj; 38 | Paths64 clip; 39 | Paths64 solution; 40 | 41 | @Param({ "1000", "2000", "4000" }) 42 | public int edgeCount; 43 | 44 | @Setup(Level.Invocation) 45 | public void setup() { 46 | Random rand = new Random(seed++); 47 | 48 | subj = new Paths64(); 49 | clip = new Paths64(); 50 | solution = new Paths64(); 51 | 52 | subj.add(MakeRandomPath(DisplayWidth, DisplayHeight, edgeCount, rand)); 53 | clip.add(MakeRandomPath(DisplayWidth, DisplayHeight, edgeCount, rand)); 54 | } 55 | 56 | } 57 | 58 | @Benchmark 59 | @OutputTimeUnit(TimeUnit.SECONDS) 60 | public void Intersection(BenchmarkState state) { 61 | Clipper64 c = new Clipper64(); 62 | c.AddSubject(state.subj); // closed 63 | c.AddClip(state.clip); // closed 64 | c.Execute(ClipType.Intersection, FillRule.NonZero, state.solution); 65 | } 66 | 67 | private static Point64 MakeRandomPt(int maxWidth, int maxHeight, Random rand) { 68 | long x = rand.nextInt(maxWidth); 69 | long y = rand.nextInt(maxHeight); 70 | return new Point64(x, y); 71 | } 72 | 73 | private static Path64 MakeRandomPath(int width, int height, int count, Random rand) { 74 | Path64 result = new Path64(count); 75 | for (int i = 0; i < count; ++i) { 76 | result.add(MakeRandomPt(width, height, rand)); 77 | } 78 | return result; 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/test/java/clipper2/ClipperFileIO.java: -------------------------------------------------------------------------------- 1 | package clipper2; 2 | 3 | import java.io.IOException; 4 | import java.nio.file.Files; 5 | import java.nio.file.Paths; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | import clipper2.core.ClipType; 10 | import clipper2.core.FillRule; 11 | import clipper2.core.Path64; 12 | import clipper2.core.Paths64; 13 | import clipper2.core.Point64; 14 | 15 | class ClipperFileIO { 16 | 17 | static class TestCase { 18 | private final String caption; 19 | private final ClipType clipType; 20 | private final FillRule fillRule; 21 | private final long area; 22 | private final int count; 23 | private final int GetIdx; 24 | private final Paths64 subj; 25 | private final Paths64 subj_open; 26 | private final Paths64 clip; 27 | private final int testNum; 28 | 29 | TestCase(String caption, ClipType clipType, FillRule fillRule, long area, int count, int GetIdx, Paths64 subj, Paths64 subj_open, 30 | Paths64 clip, int testNum) { 31 | this.caption = caption; 32 | this.clipType = clipType; 33 | this.fillRule = fillRule; 34 | this.area = area; 35 | this.count = count; 36 | this.GetIdx = GetIdx; 37 | this.subj = subj; 38 | this.subj_open = subj_open; 39 | this.clip = clip; 40 | this.testNum = testNum; 41 | } 42 | 43 | public String caption() { 44 | return caption; 45 | } 46 | 47 | public ClipType clipType() { 48 | return clipType; 49 | } 50 | 51 | public FillRule fillRule() { 52 | return fillRule; 53 | } 54 | 55 | public long area() { 56 | return area; 57 | } 58 | 59 | public int count() { 60 | return count; 61 | } 62 | 63 | public int GetIdx() { 64 | return GetIdx; 65 | } 66 | 67 | public Paths64 subj() { 68 | return subj; 69 | } 70 | 71 | public Paths64 subj_open() { 72 | return subj_open; 73 | } 74 | 75 | public Paths64 clip() { 76 | return clip; 77 | } 78 | 79 | public int testNum() { 80 | return testNum; 81 | } 82 | } 83 | 84 | static List loadTestCases(String testFileName) throws IOException { 85 | List lines = Files.readAllLines(Paths.get(String.format("src/test/resources/%s", testFileName))); 86 | lines = new ArrayList<>(lines); 87 | lines.add(""); 88 | 89 | String caption = ""; 90 | ClipType ct = ClipType.None; 91 | FillRule fillRule = FillRule.EvenOdd; 92 | long area = 0; 93 | int count = 0; 94 | int GetIdx = 0; 95 | Paths64 subj = new Paths64(); 96 | Paths64 subj_open = new Paths64(); 97 | Paths64 clip = new Paths64(); 98 | 99 | List cases = new ArrayList<>(); 100 | 101 | for (String s : lines) { 102 | if (s.matches("\\s*")) { 103 | if (GetIdx != 0) { 104 | cases.add(new TestCase(caption, ct, fillRule, area, count, GetIdx, new Paths64(subj), new Paths64(subj_open), 105 | new Paths64(clip), cases.size() + 1)); 106 | subj.clear(); 107 | subj_open.clear(); 108 | clip.clear(); 109 | GetIdx = 0; 110 | } 111 | continue; 112 | } 113 | 114 | if (s.indexOf("CAPTION: ") == 0) { 115 | caption = s.substring(9); 116 | continue; 117 | } 118 | 119 | if (s.indexOf("CLIPTYPE: ") == 0) { 120 | if (s.indexOf("INTERSECTION") > 0) { 121 | ct = ClipType.Intersection; 122 | } else if (s.indexOf("UNION") > 0) { 123 | ct = ClipType.Union; 124 | } else if (s.indexOf("DIFFERENCE") > 0) { 125 | ct = ClipType.Difference; 126 | } else { 127 | ct = ClipType.Xor; 128 | } 129 | continue; 130 | } 131 | 132 | if (s.indexOf("FILLTYPE: ") == 0 || s.indexOf("FILLRULE: ") == 0) { 133 | if (s.indexOf("EVENODD") > 0) { 134 | fillRule = FillRule.EvenOdd; 135 | } else if (s.indexOf("POSITIVE") > 0) { 136 | fillRule = FillRule.Positive; 137 | } else if (s.indexOf("NEGATIVE") > 0) { 138 | fillRule = FillRule.Negative; 139 | } else { 140 | fillRule = FillRule.NonZero; 141 | } 142 | continue; 143 | } 144 | 145 | if (s.indexOf("SOL_AREA: ") == 0) { 146 | area = Long.parseLong(s.substring(10)); 147 | continue; 148 | } 149 | 150 | if (s.indexOf("SOL_COUNT: ") == 0) { 151 | count = Integer.parseInt(s.substring(11)); 152 | continue; 153 | } 154 | 155 | if (s.indexOf("SUBJECTS_OPEN") == 0) { 156 | GetIdx = 2; 157 | continue; 158 | } else if (s.indexOf("SUBJECTS") == 0) { 159 | GetIdx = 1; 160 | continue; 161 | } else if (s.indexOf("CLIPS") == 0) { 162 | GetIdx = 3; 163 | continue; 164 | } else { 165 | // continue; 166 | } 167 | 168 | Paths64 paths = PathFromStr(s); // 0 or 1 path 169 | if (paths == null || paths.isEmpty()) { 170 | if (GetIdx == 3) { 171 | // return result; 172 | } 173 | if (s.indexOf("SUBJECTS_OPEN") == 0) { 174 | GetIdx = 2; 175 | } else if (s.indexOf("CLIPS") == 0) { 176 | GetIdx = 3; 177 | } else { 178 | // return result; 179 | } 180 | continue; 181 | } 182 | if (GetIdx == 1 && !paths.get(0).isEmpty()) { 183 | subj.add(paths.get(0)); 184 | } else if (GetIdx == 2) { 185 | subj_open.add(paths.get(0)); 186 | } else { 187 | clip.add(paths.get(0)); 188 | } 189 | 190 | } 191 | 192 | return cases; 193 | } 194 | 195 | static Paths64 PathFromStr(String s) { 196 | if (s == null) { 197 | return new Paths64(); 198 | } 199 | Path64 p = new Path64(); 200 | Paths64 pp = new Paths64(); 201 | int len = s.length(), i = 0, j; 202 | while (i < len) { 203 | boolean isNeg; 204 | while (s.charAt(i) < 33 && i < len) { 205 | i++; 206 | } 207 | if (i >= len) { 208 | break; 209 | } 210 | // get X ... 211 | isNeg = s.charAt(i) == 45; 212 | if (isNeg) { 213 | i++; 214 | } 215 | if (i >= len || s.charAt(i) < 48 || s.charAt(i) > 57) { 216 | break; 217 | } 218 | j = i + 1; 219 | while (j < len && s.charAt(j) > 47 && s.charAt(j) < 58) { 220 | j++; 221 | } 222 | Long x = LongTryParse(s.substring(i, j)); 223 | if (x == null) { 224 | break; 225 | } 226 | if (isNeg) { 227 | x = -x; 228 | } 229 | // skip space or comma between X & Y ... 230 | i = j; 231 | while (i < len && (s.charAt(i) == 32 || s.charAt(i) == 44)) { 232 | i++; 233 | } 234 | // get Y ... 235 | if (i >= len) { 236 | break; 237 | } 238 | isNeg = s.charAt(i) == 45; 239 | if (isNeg) { 240 | i++; 241 | } 242 | if (i >= len || s.charAt(i) < 48 || s.charAt(i) > 57) { 243 | break; 244 | } 245 | j = i + 1; 246 | while (j < len && s.charAt(j) > 47 && s.charAt(j) < 58) { 247 | j++; 248 | } 249 | Long y = LongTryParse(s.substring(i, j)); 250 | if (y == null) { 251 | break; 252 | } 253 | if (isNeg) { 254 | y = -y; 255 | } 256 | p.add(new Point64(x, y)); 257 | // skip trailing space, comma ... 258 | i = j; 259 | int nlCnt = 0; 260 | while (i < len && (s.charAt(i) < 33 || s.charAt(i) == 44)) { 261 | if (i >= len) { 262 | break; 263 | } 264 | if (s.charAt(i) == 10) { 265 | nlCnt++; 266 | if (nlCnt == 2) { 267 | if (p.size() > 0) { 268 | pp.add(p); 269 | } 270 | p = new Path64(); 271 | } 272 | } 273 | i++; 274 | } 275 | } 276 | if (p.size() > 0) { 277 | pp.add(p); 278 | } 279 | return pp; 280 | } 281 | 282 | private static @Nullable Long LongTryParse(String s) { 283 | try { 284 | return Long.valueOf(s); 285 | } catch (NumberFormatException e) { 286 | return null; 287 | } 288 | } 289 | 290 | } 291 | -------------------------------------------------------------------------------- /src/test/java/clipper2/TestLines.java: -------------------------------------------------------------------------------- 1 | package clipper2; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertTrue; 5 | 6 | import java.io.IOException; 7 | import java.util.stream.Stream; 8 | 9 | import org.junit.jupiter.params.ParameterizedTest; 10 | import org.junit.jupiter.params.provider.Arguments; 11 | import org.junit.jupiter.params.provider.MethodSource; 12 | 13 | import clipper2.ClipperFileIO.TestCase; 14 | import clipper2.core.Paths64; 15 | import clipper2.engine.Clipper64; 16 | 17 | class TestLines { 18 | 19 | private static final Stream testCases() throws IOException { 20 | return ClipperFileIO.loadTestCases("Lines.txt").stream().map(t -> Arguments.of(t, t.caption(), t.clipType(), t.fillRule())); 21 | } 22 | 23 | @MethodSource("testCases") 24 | @ParameterizedTest(name = "{1} {2} {3}") 25 | final void RunLinesTestCase(TestCase test, String caption, Object o, Object o1) { 26 | Clipper64 c64 = new Clipper64(); 27 | Paths64 solution = new Paths64(); 28 | Paths64 solution_open = new Paths64(); 29 | 30 | c64.AddSubject(test.subj()); 31 | c64.AddOpenSubject(test.subj_open()); 32 | c64.AddClip(test.clip()); 33 | c64.Execute(test.clipType(), test.fillRule(), solution, solution_open); 34 | 35 | if (test.area() > 0) { 36 | double area2 = Clipper.Area(solution); 37 | assertEquals(test.area(), area2, test.area() * 0.005); 38 | } 39 | 40 | if (test.count() > 0 && Math.abs(solution.size() - test.count()) > 0) { 41 | assertTrue(Math.abs(solution.size() - test.count()) < 2, 42 | String.format("Vertex count incorrect. Difference=%s", (solution.size() - test.count()))); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/clipper2/TestOffsetOrientation.java: -------------------------------------------------------------------------------- 1 | package clipper2; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | 5 | import java.util.List; 6 | 7 | import org.junit.jupiter.api.Test; 8 | 9 | import clipper2.core.Path64; 10 | import clipper2.core.Paths64; 11 | import clipper2.offset.ClipperOffset; 12 | import clipper2.offset.EndType; 13 | import clipper2.offset.JoinType; 14 | 15 | class TestOffsetOrientation { 16 | 17 | @Test 18 | void TestOffsettingOrientation1() { 19 | Paths64 subject = new Paths64(Clipper.MakePath(new int[] { 0, 0, 0, 5, 5, 5, 5, 0 })); 20 | 21 | Paths64 solution = Clipper.InflatePaths(subject, 1, JoinType.Round, EndType.Polygon); 22 | 23 | assertEquals(1, solution.size()); 24 | // when offsetting, output orientation should match input 25 | assertTrue(Clipper.IsPositive(subject.get(0)) == Clipper.IsPositive(solution.get(0))); 26 | } 27 | 28 | @Test 29 | void TestOffsettingOrientation2() { 30 | Path64 s1 = Clipper.MakePath(new int[] { 20, 220, 280, 220, 280, 280, 20, 280 }); 31 | Path64 s2 = Clipper.MakePath(new int[] { 0, 200, 0, 300, 300, 300, 300, 200 }); 32 | Paths64 subject = new Paths64(List.of(s1, s2)); 33 | 34 | ClipperOffset co = new ClipperOffset(); 35 | co.setReverseSolution(true); 36 | co.AddPaths(subject, JoinType.Round, EndType.Polygon); 37 | 38 | Paths64 solution = new Paths64(); 39 | co.Execute(5, solution); 40 | 41 | assertEquals(2, solution.size()); 42 | /* 43 | * When offsetting, output orientation should match input EXCEPT when 44 | * ReverseSolution == true However, input path ORDER may not match output path 45 | * order. For example, order will change whenever inner paths (holes) are 46 | * defined before their container outer paths (as above). And when offsetting 47 | * multiple outer paths, their order will likely change too. Due to the 48 | * sweep-line algorithm used, paths with larger Y coordinates will likely be 49 | * listed first. 50 | */ 51 | assertTrue(Clipper.IsPositive(subject.get(1)) != Clipper.IsPositive(solution.get(0))); 52 | 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/test/java/clipper2/TestOffsets.java: -------------------------------------------------------------------------------- 1 | package clipper2; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | 5 | import java.util.Collections; 6 | import java.util.List; 7 | 8 | import org.junit.jupiter.api.Test; 9 | 10 | import clipper2.core.Path64; 11 | import clipper2.core.Paths64; 12 | import clipper2.core.Point64; 13 | import clipper2.core.PointD; 14 | import clipper2.offset.ClipperOffset; 15 | import clipper2.offset.EndType; 16 | import clipper2.offset.JoinType; 17 | 18 | class TestOffsets { 19 | 20 | @Test 21 | void TestOffsets2() { // see #448 & #456 22 | double scale = 10, delta = 10 * scale, arc_tol = 0.25 * scale; 23 | 24 | Paths64 subject = new Paths64(); 25 | Paths64 solution = new Paths64(); 26 | ClipperOffset c = new ClipperOffset(); 27 | subject.add(Clipper.MakePath(new long[] { 50, 50, 100, 50, 100, 150, 50, 150, 0, 100 })); 28 | 29 | subject = Clipper.ScalePaths(subject, scale); 30 | 31 | c.AddPaths(subject, JoinType.Round, EndType.Polygon); 32 | c.setArcTolerance(arc_tol); 33 | c.Execute(delta, solution); 34 | 35 | double min_dist = delta * 2; 36 | double max_dist = 0; 37 | 38 | for (Point64 subjPt : subject.get(0)) { 39 | Point64 prevPt = solution.get(0).get(solution.get(0).size() - 1); 40 | for (Point64 pt : solution.get(0)) { 41 | Point64 mp = midPoint(prevPt, pt); 42 | double d = distance(mp, subjPt); 43 | if (d < delta * 2) { 44 | if (d < min_dist) 45 | min_dist = d; 46 | if (d > max_dist) 47 | max_dist = d; 48 | } 49 | prevPt = pt; 50 | } 51 | } 52 | 53 | assertTrue(min_dist + 1 >= delta - arc_tol); // +1 for rounding errors 54 | assertTrue(solution.get(0).size() <= 21); 55 | } 56 | 57 | @Test 58 | void TestOffsets3() { // see #424 59 | Paths64 subjects = new Paths64(List.of(Clipper.MakePath(new long[] { 1525311078, 1352369439, 1526632284, 1366692987, 1519397110, 1367437476, 1520246456, 60 | 1380177674, 1520613458, 1385913385, 1517383844, 1386238444, 1517771817, 1392099983, 1518233190, 1398758441, 1518421934, 1401883197, 1518694564, 61 | 1406612275, 1520267428, 1430289121, 1520770744, 1438027612, 1521148232, 1443438264, 1521441833, 1448964260, 1521683005, 1452518932, 1521819320, 62 | 1454374912, 1527943004, 1454154711, 1527649403, 1448523858, 1535901696, 1447989084, 1535524209, 1442788147, 1538953052, 1442463089, 1541553521, 63 | 1442242888, 1541459149, 1438855987, 1538764308, 1439076188, 1538575565, 1436832236, 1538764308, 1436832236, 1536509870, 1405374956, 1550497874, 64 | 1404347351, 1550214758, 1402428457, 1543818445, 1402868859, 1543734559, 1402124370, 1540672717, 1402344571, 1540473487, 1399995761, 1524996506, 65 | 1400981422, 1524807762, 1398223667, 1530092585, 1397898609, 1531675935, 1397783265, 1531392819, 1394920653, 1529809469, 1395025510, 1529348096, 66 | 1388880855, 1531099218, 1388660654, 1530826588, 1385158410, 1532955197, 1384938209, 1532661596, 1379003269, 1532472852, 1376235028, 1531277476, 67 | 1376350372, 1530050642, 1361806623, 1599487345, 1352704983, 1602758902, 1378489467, 1618990858, 1376350372, 1615058698, 1344085688, 1603230761, 68 | 1345700495, 1598648484, 1346329641, 1598931599, 1348667965, 1596698132, 1348993024, 1595775386, 1342722540 }))); 69 | 70 | Paths64 solution = Clipper.InflatePaths(subjects, -209715, JoinType.Miter, EndType.Polygon); 71 | assertTrue(solution.get(0).size() - subjects.get(0).size() <= 1); 72 | } 73 | 74 | @Test 75 | void TestOffsets4() { // see #482 76 | Paths64 paths = new Paths64(List.of(Clipper.MakePath(new long[] { 0, 0, 20000, 200, 40000, 0, 40000, 50000, 0, 50000, 0, 0 }))); 77 | Paths64 solution = Clipper.InflatePaths(paths, -5000, JoinType.Square, EndType.Polygon); 78 | assertEquals(5, solution.get(0).size()); 79 | 80 | paths = new Paths64(List.of(Clipper.MakePath(new long[] { 0, 0, 20000, 400, 40000, 0, 40000, 50000, 0, 50000, 0, 0 }))); 81 | solution = Clipper.InflatePaths(paths, -5000, JoinType.Square, EndType.Polygon); 82 | assertEquals(5, solution.get(0).size()); 83 | 84 | paths = new Paths64(List.of(Clipper.MakePath(new long[] { 0, 0, 20000, 400, 40000, 0, 40000, 50000, 0, 50000, 0, 0 }))); 85 | solution = Clipper.InflatePaths(paths, -5000, JoinType.Round, EndType.Polygon, 2, 100); 86 | assertTrue(solution.get(0).size() > 5); 87 | 88 | paths = new Paths64(List.of(Clipper.MakePath(new long[] { 0, 0, 20000, 1500, 40000, 0, 40000, 50000, 0, 50000, 0, 0 }))); 89 | solution = Clipper.InflatePaths(paths, -5000, JoinType.Round, EndType.Polygon, 2, 100); 90 | assertTrue(solution.get(0).size() > 5); 91 | } 92 | 93 | @Test 94 | void TestOffsets6() { 95 | Path64 squarePath = Clipper.MakePath(new long[] { 620, 620, -620, 620, -620, -620, 620, -620 }); 96 | 97 | Path64 complexPath = Clipper.MakePath(new long[] { 20, -277, 42, -275, 59, -272, 80, -266, 97, -261, 114, -254, 135, -243, 149, -235, 167, -222, 182, 98 | -211, 197, -197, 212, -181, 223, -167, 234, -150, 244, -133, 253, -116, 260, -99, 267, -78, 272, -61, 275, -40, 278, -18, 276, -39, 272, -61, 99 | 267, -79, 260, -99, 253, -116, 245, -133, 235, -150, 223, -167, 212, -181, 197, -197, 182, -211, 168, -222, 152, -233, 135, -243, 114, -254, 97, 100 | -261, 80, -267, 59, -272, 42, -275, 20, -278 }); 101 | 102 | Paths64 subjects = new Paths64(List.of(squarePath, complexPath)); 103 | 104 | final double offset = -50; 105 | ClipperOffset offseter = new ClipperOffset(); 106 | 107 | offseter.AddPaths(subjects, JoinType.Round, EndType.Polygon); 108 | Paths64 solution = new Paths64(); 109 | offseter.Execute(offset, solution); 110 | 111 | assertEquals(2, solution.size()); 112 | 113 | double area = Clipper.Area(solution.get(1)); 114 | assertTrue(area < -47500); 115 | } 116 | 117 | @Test 118 | void TestOffsets7() { // (#593 & #715) 119 | Paths64 solution; 120 | Paths64 subject = new Paths64(List.of(Clipper.MakePath(new long[] { 0, 0, 100, 0, 100, 100, 0, 100 }))); 121 | 122 | solution = Clipper.InflatePaths(subject, -50, JoinType.Miter, EndType.Polygon); 123 | assertEquals(0, solution.size()); 124 | 125 | subject.add(Clipper.MakePath(new long[] { 40, 60, 60, 60, 60, 40, 40, 40 })); 126 | solution = Clipper.InflatePaths(subject, 10, JoinType.Miter, EndType.Polygon); 127 | assertEquals(1, solution.size()); 128 | 129 | Collections.reverse(subject.get(0)); 130 | Collections.reverse(subject.get(1)); 131 | solution = Clipper.InflatePaths(subject, 10, JoinType.Miter, EndType.Polygon); 132 | assertEquals(1, solution.size()); 133 | 134 | subject = new Paths64(List.of(subject.get(0))); 135 | solution = Clipper.InflatePaths(subject, -50, JoinType.Miter, EndType.Polygon); 136 | assertEquals(0, solution.size()); 137 | } 138 | 139 | @Test 140 | void TestOffsets8() { // (#724) 141 | Paths64 subject = new Paths64(List.of(Clipper.MakePath(new long[] { 91759700, -49711991, 83886095, -50331657, -872415388, -50331657, -880288993, 142 | -49711991, -887968725, -47868251, -895265482, -44845834, -901999593, -40719165, -908005244, -35589856, -913134553, -29584205, -917261224, 143 | -22850094, -920283639, -15553337, -922127379, -7873605, -922747045, 0, -922747045, 1434498600, -922160557, 1442159790, -920414763, 1449642437, 144 | -917550346, 1456772156, -913634061, 1463382794, -908757180, 1469320287, -903033355, 1474446264, -896595982, 1478641262, -889595081, 1481807519, 145 | -882193810, 1483871245, -876133965, 1484596521, -876145751, 1484713389, -875781839, 1485061090, -874690056, 1485191762, -874447580, 1485237014, 146 | -874341490, 1485264094, -874171960, 1485309394, -873612294, 1485570372, -873201878, 1485980788, -872941042, 1486540152, -872893274, 1486720070, 147 | -872835064, 1487162210, -872834788, 1487185500, -872769052, 1487406000, -872297948, 1487583168, -871995958, 1487180514, -871995958, 1486914040, 148 | -871908872, 1486364208, -871671308, 1485897962, -871301302, 1485527956, -870835066, 1485290396, -870285226, 1485203310, -868659019, 1485203310, 149 | -868548443, 1485188472, -868239649, 1484791011, -868239527, 1484783879, -838860950, 1484783879, -830987345, 1484164215, -823307613, 1482320475, 150 | -816010856, 1479298059, -809276745, 1475171390, -803271094, 1470042081, -752939437, 1419710424, -747810128, 1413704773, -743683459, 1406970662, 151 | -740661042, 1399673904, -738817302, 1391994173, -738197636, 1384120567, -738197636, 1244148246, -738622462, 1237622613, -739889768, 1231207140, 152 | -802710260, 995094494, -802599822, 995052810, -802411513, 994586048, -802820028, 993050638, -802879992, 992592029, -802827240, 992175479, 153 | -802662144, 991759637, -802578556, 991608039, -802511951, 991496499, -801973473, 990661435, -801899365, 990554757, -801842657, 990478841, 154 | -801770997, 990326371, -801946911, 989917545, -801636397, 989501855, -801546099, 989389271, -800888669, 988625013, -800790843, 988518907, 155 | -800082405, 987801675, -799977513, 987702547, -799221423, 987035738, -799109961, 986944060, -798309801, 986330832, -798192297, 986247036, 156 | -797351857, 985690294, -797228867, 985614778, -796352124, 985117160, -796224232, 985050280, -795315342, 984614140, -795183152, 984556216, 157 | -794246418, 984183618, -794110558, 984134924, -793150414, 983827634, -793011528, 983788398, -792032522, 983547874, -791891266, 983518284, 158 | -790898035, 983345662, -790755079, 983325856, -789752329, 983221956, -789608349, 983212030, -787698545, 983146276, -787626385, 983145034, 159 | -536871008, 983145034, -528997403, 982525368, -521317671, 980681627, -514020914, 977659211, -507286803, 973532542, -501281152, 968403233, 160 | -496151843, 962397582, -492025174, 955663471, -489002757, 948366714, -487159017, 940686982, -486539351, 932813377, -486539351, 667455555, 161 | -486537885, 667377141, -486460249, 665302309, -486448529, 665145917, -486325921, 664057737, -486302547, 663902657, -486098961, 662826683, 162 | -486064063, 662673784, -485780639, 661616030, -485734413, 661466168, -485372735, 660432552, -485315439, 660286564, -484877531, 659282866, 163 | -484809485, 659141568, -484297795, 658173402, -484219379, 658037584, -483636768, 657110363, -483548422, 656980785, -482898150, 656099697, 164 | -482800368, 655977081, -482086070, 655147053, -481979398, 655032087, -481205068, 654257759, -481090104, 654151087, -480260074, 653436789, 165 | -480137460, 653339007, -479256372, 652688735, -479126794, 652600389, -478199574, 652017779, -478063753, 651939363, -477095589, 651427672, 166 | -476954289, 651359626, -475950593, 650921718, -475804605, 650864422, -474770989, 650502744, -474621127, 650456518, -473563373, 650173094, 167 | -473410475, 650138196, -472334498, 649934610, -472179420, 649911236, -471091240, 649788626, -470934848, 649776906, -468860016, 649699272, 168 | -468781602, 649697806, -385876037, 649697806, -378002432, 649078140, -370322700, 647234400, -363025943, 644211983, -356291832, 640085314, 169 | -350286181, 634956006, -345156872, 628950354, -341030203, 622216243, -338007786, 614919486, -336164046, 607239755, -335544380, 599366149, 170 | -335544380, 571247184, -335426942, 571236100, -335124952, 570833446, -335124952, 569200164, -335037864, 568650330, -334800300, 568184084, 171 | -334430294, 567814078, -333964058, 567576517, -333414218, 567489431, -331787995, 567489431, -331677419, 567474593, -331368625, 567077133, 172 | -331368503, 567070001, -142068459, 567070001, -136247086, 566711605, -136220070, 566848475, -135783414, 567098791, -135024220, 567004957, 173 | -134451560, 566929159, -134217752, 566913755, -133983942, 566929159, -133411282, 567004957, -132665482, 567097135, -132530294, 567091859, 174 | -132196038, 566715561, -132195672, 566711157, -126367045, 567070001, -33554438, 567070001, -27048611, 566647761, -20651940, 565388127, 175 | -14471751, 563312231, -8611738, 560454902, 36793963, 534548454, 43059832, 530319881, 48621743, 525200596, 53354240, 519306071, 57150572, 176 | 512769270, 59925109, 505737634, 61615265, 498369779, 62182919, 490831896, 62182919, 474237629, 62300359, 474226543, 62602349, 473823889, 177 | 62602349, 472190590, 62689435, 471640752, 62926995, 471174516, 63297005, 470804506, 63763241, 470566946, 64313081, 470479860, 65939308, 178 | 470479860, 66049884, 470465022, 66358678, 470067562, 66358800, 470060430, 134217752, 470060430, 134217752, 0, 133598086, -7873605, 131754346, 179 | -15553337, 128731929, -22850094, 124605260, -29584205, 119475951, -35589856, 113470300, -40719165, 106736189, -44845834, 99439432, -47868251, 180 | 91759700, -49711991 181 | }))); 182 | 183 | double offset = -50329979.277800001; 184 | double arc_tol = 5000; 185 | 186 | Paths64 solution = Clipper.InflatePaths(subject, offset, JoinType.Round, EndType.Polygon, 2, arc_tol); 187 | OffsetQual oq = getOffsetQuality(subject.get(0), solution.get(0), offset); 188 | double smallestDist = distance(oq.smallestInSub, oq.smallestInSol); 189 | double largestDist = distance(oq.largestInSub, oq.largestInSol); 190 | final double rounding_tolerance = 1.0; 191 | offset = Math.abs(offset); 192 | 193 | assertTrue(offset - smallestDist - rounding_tolerance <= arc_tol); 194 | assertTrue(largestDist - offset - rounding_tolerance <= arc_tol); 195 | } 196 | 197 | @Test 198 | void TestOffsets9() { // (#733) 199 | // solution orientations should match subject orientations UNLESS 200 | // reverse_solution is set true in ClipperOffset's constructor 201 | 202 | // start subject's orientation positive ... 203 | Paths64 subject = new Paths64(Clipper.MakePath(new long[] { 100, 100, 200, 100, 200, 400, 100, 400 })); 204 | Paths64 solution = Clipper.InflatePaths(subject, 50, JoinType.Miter, EndType.Polygon); 205 | assertEquals(1, solution.size()); 206 | assertTrue(Clipper.IsPositive(solution.get(0))); 207 | 208 | // reversing subject's orientation should not affect delta direction 209 | // (ie where positive deltas inflate). 210 | Collections.reverse(subject.get(0)); 211 | solution = Clipper.InflatePaths(subject, 50, JoinType.Miter, EndType.Polygon); 212 | assertEquals(1, solution.size()); 213 | assertTrue(Math.abs(Clipper.Area(solution.get(0))) > Math.abs(Clipper.Area(subject.get(0)))); 214 | assertFalse(Clipper.IsPositive(solution.get(0))); 215 | 216 | ClipperOffset co = new ClipperOffset(2, 0, false, true); // last param. reverses solution 217 | co.AddPaths(subject, JoinType.Miter, EndType.Polygon); 218 | co.Execute(50, solution); 219 | assertEquals(1, solution.size()); 220 | assertTrue(Math.abs(Clipper.Area(solution.get(0))) > Math.abs(Clipper.Area(subject.get(0)))); 221 | assertTrue(Clipper.IsPositive(solution.get(0))); 222 | 223 | // add a hole (ie has reverse orientation to outer path) 224 | subject.add(Clipper.MakePath(new long[] { 130, 130, 170, 130, 170, 370, 130, 370 })); 225 | solution = Clipper.InflatePaths(subject, 30, JoinType.Miter, EndType.Polygon); 226 | assertEquals(1, solution.size()); 227 | assertFalse(Clipper.IsPositive(solution.get(0))); 228 | 229 | co.Clear(); // should still reverse solution orientation 230 | co.AddPaths(subject, JoinType.Miter, EndType.Polygon); 231 | co.Execute(30, solution); 232 | assertEquals(1, solution.size()); 233 | assertTrue(Math.abs(Clipper.Area(solution.get(0))) > Math.abs(Clipper.Area(subject.get(0)))); 234 | assertTrue(Clipper.IsPositive(solution.get(0))); 235 | 236 | solution = Clipper.InflatePaths(subject, -15, JoinType.Miter, EndType.Polygon); 237 | assertEquals(0, solution.size()); 238 | } 239 | 240 | private static Point64 midPoint(Point64 p1, Point64 p2) { 241 | Point64 result = new Point64(); 242 | result.setX((p1.x + p2.x) / 2); 243 | result.setY((p1.y + p2.y) / 2); 244 | return result; 245 | } 246 | 247 | private static double distance(Point64 pt1, Point64 pt2) { 248 | long dx = pt1.x - pt2.x; 249 | long dy = pt1.y - pt2.y; 250 | return Math.sqrt(dx * dx + dy * dy); 251 | } 252 | 253 | static class OffsetQual { 254 | PointD smallestInSub; 255 | PointD smallestInSol; 256 | PointD largestInSub; 257 | PointD largestInSol; 258 | } 259 | 260 | private static OffsetQual getOffsetQuality(Path64 subject, Path64 solution, double delta) { 261 | if (subject.size() == 0 || solution.size() == 0) 262 | return new OffsetQual(); 263 | 264 | double desiredDistSqr = delta * delta; 265 | double smallestSqr = desiredDistSqr; 266 | double largestSqr = desiredDistSqr; 267 | OffsetQual oq = new OffsetQual(); 268 | 269 | final int subVertexCount = 4; // 1 .. 100 :) 270 | final double subVertexFrac = 1.0 / subVertexCount; 271 | Point64 solPrev = solution.get(solution.size() - 1); 272 | 273 | for (Point64 solPt0 : solution) { 274 | for (int i = 0; i < subVertexCount; ++i) { 275 | // divide each edge in solution into series of sub-vertices (solPt) 276 | PointD solPt = new PointD(solPrev.x + (solPt0.x - solPrev.x) * subVertexFrac * i, solPrev.y + (solPt0.y - solPrev.y) * subVertexFrac * i); 277 | 278 | // now find the closest point in subject to each of these solPt 279 | PointD closestToSolPt = new PointD(0, 0); 280 | double closestDistSqr = Double.POSITIVE_INFINITY; 281 | Point64 subPrev = subject.get(subject.size() - 1); 282 | 283 | for (Point64 subPt : subject) { 284 | PointD closestPt = getClosestPointOnSegment(solPt, subPt, subPrev); 285 | subPrev = subPt; 286 | double sqrDist = distanceSqr(closestPt, solPt); 287 | if (sqrDist < closestDistSqr) { 288 | closestDistSqr = sqrDist; 289 | closestToSolPt = closestPt; 290 | } 291 | } 292 | 293 | // see how this distance compares with every other solPt 294 | if (closestDistSqr < smallestSqr) { 295 | smallestSqr = closestDistSqr; 296 | oq.smallestInSub = closestToSolPt; 297 | oq.smallestInSol = solPt; 298 | } 299 | if (closestDistSqr > largestSqr) { 300 | largestSqr = closestDistSqr; 301 | oq.largestInSub = closestToSolPt; 302 | oq.largestInSol = solPt; 303 | } 304 | } 305 | solPrev = solPt0; 306 | } 307 | return oq; 308 | } 309 | 310 | private static PointD getClosestPointOnSegment(PointD offPt, Point64 seg1, Point64 seg2) { 311 | // Handle case where segment is actually a point 312 | if (seg1.x == seg2.x && seg1.y == seg2.y) { 313 | return new PointD(seg1.x, seg1.y); 314 | } 315 | 316 | double dx = seg2.x - seg1.x; 317 | double dy = seg2.y - seg1.y; 318 | 319 | double q = ((offPt.x - seg1.x) * dx + (offPt.y - seg1.y) * dy) / (dx * dx + dy * dy); 320 | 321 | // Clamp q between 0 and 1 322 | q = Math.max(0, Math.min(1, q)); 323 | 324 | return new PointD(seg1.x + q * dx, seg1.y + q * dy); 325 | } 326 | private static double distanceSqr(PointD pt1, PointD pt2) { 327 | double dx = pt1.x - pt2.x; 328 | double dy = pt1.y - pt2.y; 329 | return dx * dx + dy * dy; 330 | } 331 | 332 | private static double distance(PointD pt1, PointD pt2) { 333 | return Math.sqrt(distanceSqr(pt1, pt2)); 334 | } 335 | 336 | } 337 | -------------------------------------------------------------------------------- /src/test/java/clipper2/TestPolygons.java: -------------------------------------------------------------------------------- 1 | package clipper2; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertTrue; 4 | 5 | import java.io.IOException; 6 | import java.util.Arrays; 7 | import java.util.stream.Stream; 8 | 9 | import org.junit.jupiter.params.ParameterizedTest; 10 | import org.junit.jupiter.params.provider.Arguments; 11 | import org.junit.jupiter.params.provider.MethodSource; 12 | 13 | import clipper2.ClipperFileIO.TestCase; 14 | import clipper2.core.Paths64; 15 | import clipper2.engine.Clipper64; 16 | 17 | class TestPolygons { 18 | 19 | private static final Stream testCases() throws IOException { 20 | return ClipperFileIO.loadTestCases("Polygons.txt").stream().map(t -> Arguments.of(t, t.testNum(), t.clipType(), t.fillRule())); 21 | } 22 | 23 | @MethodSource("testCases") 24 | @ParameterizedTest(name = "{1}: {2} {3}") 25 | final void RunPolygonsTestCase(TestCase test, int testNum, Object o, Object o1) { 26 | Clipper64 c64 = new Clipper64(); 27 | Paths64 solution = new Paths64(); 28 | Paths64 solution_open = new Paths64(); 29 | 30 | c64.AddSubject(test.subj()); 31 | c64.AddOpenSubject(test.subj_open()); 32 | c64.AddClip(test.clip()); 33 | c64.Execute(test.clipType(), test.fillRule(), solution, solution_open); 34 | 35 | int measuredCount = solution.size(); 36 | long measuredArea = (long) Clipper.Area(solution); 37 | int storedCount = test.count(); 38 | long storedArea = test.area(); 39 | int countDiff = storedCount > 0 ? Math.abs(storedCount - measuredCount) : 0; 40 | long areaDiff = storedArea > 0 ? Math.abs(storedArea - measuredArea) : 0; 41 | double areaDiffRatio = storedArea <= 0 ? 0 : (double) areaDiff / storedArea; 42 | 43 | // check polygon counts 44 | if (storedCount > 0) { 45 | if (Arrays.asList(140, 150, 165, 166, 168, 172, 173, 176, 177, 179).contains(testNum)) { 46 | assertTrue(countDiff <= 7, "Diff=" + countDiff); 47 | } else if (testNum >= 120) { 48 | assertTrue(countDiff <= 6); 49 | } else if (Arrays.asList(27, 121, 126).contains(testNum)) { 50 | assertTrue(countDiff <= 2); 51 | } else if (Arrays.asList(23, 37, 43, 45, 87, 102, 111, 118, 119).contains(testNum)) { 52 | assertTrue(countDiff <= 1); 53 | } else { 54 | assertTrue(countDiff == 0); 55 | } 56 | } 57 | 58 | // check polygon areas 59 | if (storedArea > 0) { 60 | if (Arrays.asList(19, 22, 23, 24).contains(test.testNum())) { 61 | assertTrue(areaDiffRatio <= 0.5); 62 | } else if (testNum == 193) { 63 | assertTrue(areaDiffRatio <= 0.25); 64 | } else if (testNum == 63) { 65 | assertTrue(areaDiffRatio <= 0.1); 66 | } else if (testNum == 16) { 67 | assertTrue(areaDiffRatio <= 0.075); 68 | } else if (Arrays.asList(15, 26).contains(test.testNum())) { 69 | assertTrue(areaDiffRatio <= 0.05); 70 | } else if (Arrays.asList(52, 53, 54, 59, 60, 64, 117, 118, 119, 184).contains(test.testNum())) { 71 | assertTrue(areaDiffRatio <= 0.02); 72 | } else { 73 | assertTrue(areaDiffRatio <= 0.01); 74 | } 75 | } 76 | 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/test/java/clipper2/TestPolytree.java: -------------------------------------------------------------------------------- 1 | package clipper2; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertFalse; 5 | import static org.junit.jupiter.api.Assertions.assertTrue; 6 | 7 | import java.io.IOException; 8 | import java.util.Arrays; 9 | import java.util.List; 10 | import java.util.stream.Stream; 11 | 12 | import org.junit.jupiter.params.ParameterizedTest; 13 | import org.junit.jupiter.params.provider.Arguments; 14 | import org.junit.jupiter.params.provider.MethodSource; 15 | 16 | import clipper2.ClipperFileIO.TestCase; 17 | import clipper2.core.Path64; 18 | import clipper2.core.Paths64; 19 | import clipper2.core.Point64; 20 | import clipper2.engine.Clipper64; 21 | import clipper2.engine.PointInPolygonResult; 22 | import clipper2.engine.PolyPath64; 23 | import clipper2.engine.PolyPathBase; 24 | import clipper2.engine.PolyTree64; 25 | import tangible.RefObject; 26 | 27 | class TestPolytree { 28 | 29 | private static final Stream testCases() throws IOException { 30 | return ClipperFileIO.loadTestCases("PolytreeHoleOwner2.txt").stream() 31 | .map(t -> Arguments.of(t, t.caption(), t.clipType(), t.fillRule())); 32 | } 33 | 34 | @MethodSource("testCases") 35 | @ParameterizedTest(name = "{1} {2} {3}") 36 | final void RunPolytreeTestCase(TestCase test, String caption, Object o, Object o1) { 37 | PolyTree64 solutionTree = new PolyTree64(); 38 | Paths64 solution_open = new Paths64(); 39 | Clipper64 clipper = new Clipper64(); 40 | 41 | Paths64 subject = test.subj(); 42 | Paths64 subjectOpen = test.subj_open(); 43 | Paths64 clip = test.clip(); 44 | 45 | List pointsOfInterestOutside = Arrays.asList(new Point64(21887, 10420), new Point64(21726, 10825), 46 | new Point64(21662, 10845), new Point64(21617, 10890)); 47 | 48 | for (Point64 pt : pointsOfInterestOutside) { 49 | for (Path64 path : subject) { 50 | assertEquals(PointInPolygonResult.IsOutside, Clipper.PointInPolygon(pt, path), 51 | "outside point of interest found inside subject"); 52 | } 53 | } 54 | 55 | List pointsOfInterestInside = Arrays.asList(new Point64(21887, 10430), new Point64(21843, 10520), 56 | new Point64(21810, 10686), new Point64(21900, 10461)); 57 | 58 | for (Point64 pt : pointsOfInterestInside) { 59 | int poi_inside_counter = 0; 60 | for (Path64 path : subject) { 61 | if (Clipper.PointInPolygon(pt, path) == PointInPolygonResult.IsInside) { 62 | poi_inside_counter++; 63 | } 64 | } 65 | assertEquals(1, poi_inside_counter, String.format("poi_inside_counter - expected 1 but got %1$s", poi_inside_counter)); 66 | } 67 | 68 | clipper.AddSubject(subject); 69 | clipper.AddOpenSubject(subjectOpen); 70 | clipper.AddClip(clip); 71 | clipper.Execute(test.clipType(), test.fillRule(), solutionTree, solution_open); 72 | 73 | Paths64 solutionPaths = Clipper.PolyTreeToPaths64(solutionTree); 74 | double a1 = Clipper.Area(solutionPaths), a2 = solutionTree.Area(); 75 | 76 | assertTrue(a1 > 330000, String.format("solution has wrong area - value expected: 331,052; value returned; %1$s ", a1)); 77 | 78 | assertTrue(Math.abs(a1 - a2) < 0.0001, 79 | String.format("solution tree has wrong area - value expected: %1$s; value returned; %2$s ", a1, a2)); 80 | 81 | assertTrue(CheckPolytreeFullyContainsChildren(solutionTree), "The polytree doesn't properly contain its children"); 82 | 83 | for (Point64 pt : pointsOfInterestOutside) { 84 | assertFalse(PolytreeContainsPoint(solutionTree, pt), "The polytree indicates it contains a point that it should not contain"); 85 | } 86 | 87 | for (Point64 pt : pointsOfInterestInside) { 88 | assertTrue(PolytreeContainsPoint(solutionTree, pt), 89 | "The polytree indicates it does not contain a point that it should contain"); 90 | } 91 | } 92 | 93 | private static boolean CheckPolytreeFullyContainsChildren(PolyTree64 polytree) { 94 | for (PolyPathBase p : polytree) { 95 | PolyPath64 child = (PolyPath64) p; 96 | if (child.getCount() > 0 && !PolyPathFullyContainsChildren(child)) { 97 | return false; 98 | } 99 | } 100 | return true; 101 | } 102 | 103 | private static boolean PolyPathFullyContainsChildren(PolyPath64 pp) { 104 | for (PolyPathBase c : pp) { 105 | PolyPath64 child = (PolyPath64) c; 106 | for (Point64 pt : child.getPolygon()) { 107 | if (Clipper.PointInPolygon(pt, pp.getPolygon()) == PointInPolygonResult.IsOutside) { 108 | return false; 109 | } 110 | } 111 | if (child.getCount() > 0 && !PolyPathFullyContainsChildren(child)) { 112 | return false; 113 | } 114 | } 115 | return true; 116 | } 117 | 118 | private static boolean PolytreeContainsPoint(PolyTree64 pp, Point64 pt) { 119 | int counter = 0; 120 | for (int i = 0; i < pp.getCount(); i++) { 121 | PolyPath64 child = pp.get(i); 122 | tangible.RefObject tempRef_counter = new RefObject<>(counter); 123 | PolyPathContainsPoint(child, pt, tempRef_counter); 124 | counter = tempRef_counter.argValue; 125 | } 126 | assertTrue(counter >= 0, "Polytree has too many holes"); 127 | return counter != 0; 128 | } 129 | 130 | private static void PolyPathContainsPoint(PolyPath64 pp, Point64 pt, RefObject counter) { 131 | if (Clipper.PointInPolygon(pt, pp.getPolygon()) != PointInPolygonResult.IsOutside) { 132 | if (pp.getIsHole()) { 133 | counter.argValue--; 134 | } else { 135 | counter.argValue++; 136 | } 137 | } 138 | for (int i = 0; i < pp.getCount(); i++) { 139 | PolyPath64 child = pp.get(i); 140 | PolyPathContainsPoint(child, pt, counter); 141 | } 142 | } 143 | 144 | } 145 | -------------------------------------------------------------------------------- /src/test/java/clipper2/TestRectClip.java: -------------------------------------------------------------------------------- 1 | package clipper2; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | 5 | import clipper2.core.FillRule; 6 | import clipper2.core.Path64; 7 | import clipper2.core.Paths64; 8 | import clipper2.core.Rect64; 9 | 10 | import org.junit.jupiter.api.Test; 11 | 12 | /** 13 | * RectClip tests. Ported from C++ version. 14 | */ 15 | class TestRectClip { 16 | 17 | @Test 18 | void testRectClip() { 19 | Paths64 sub = new Paths64(); 20 | Paths64 clp = new Paths64(); 21 | Paths64 sol; // Solution will be assigned by RectClip 22 | 23 | Rect64 rect = new Rect64(100, 100, 700, 500); 24 | clp.add(rect.AsPath()); 25 | 26 | // Test case 1: Subject is identical to clip rect 27 | sub.add(Clipper.MakePath(new long[] { 100, 100, 700, 100, 700, 500, 100, 500 })); 28 | sol = Clipper.RectClip(rect, sub); 29 | // Use Math.abs because area can be negative depending on orientation 30 | assertEquals(Math.abs(Clipper.Area(sub)), Math.abs(Clipper.Area(sol)), "Test 1 failed"); 31 | 32 | // Test case 2: Subject partially outside but covers same area within clip rect 33 | sub.clear(); 34 | sub.add(Clipper.MakePath(new long[] { 110, 110, 700, 100, 700, 500, 100, 500 })); 35 | sol = Clipper.RectClip(rect, sub); 36 | // Area might differ slightly due to clipping precise shape, but conceptually 37 | // check against original subject 38 | // A better check might involve comparing vertices if area isn't exact? 39 | // Or check against the expected clipped shape area if known. 40 | // For now, let's keep the original logic's intent, assuming Area reflects the 41 | // clipped portion. 42 | assertEquals(Math.abs(Clipper.Area(sub)), Math.abs(Clipper.Area(sol)), "Test 2 failed"); // Might be brittle 43 | 44 | // Test case 3: Subject partially outside, clipped area should equal clip rect 45 | // area 46 | sub.clear(); 47 | sub.add(Clipper.MakePath(new long[] { 90, 90, 700, 100, 700, 500, 100, 500 })); 48 | sol = Clipper.RectClip(rect, sub); 49 | assertEquals(Math.abs(Clipper.Area(clp)), Math.abs(Clipper.Area(sol)), "Test 3 failed"); 50 | 51 | // Test case 4: Subject fully inside clip rect 52 | sub.clear(); 53 | sub.add(Clipper.MakePath(new long[] { 110, 110, 690, 110, 690, 490, 110, 490 })); 54 | sol = Clipper.RectClip(rect, sub); 55 | assertEquals(Math.abs(Clipper.Area(sub)), Math.abs(Clipper.Area(sol)), "Test 4 failed"); 56 | 57 | // Test case 5: Subject touches edge, should result in empty solution 58 | sub.clear(); 59 | clp.clear(); // Clear previous clip path 60 | rect = new Rect64(390, 290, 410, 310); 61 | // No need to add rect.AsPath() to clp for RectClip, rect object is passed 62 | // directly 63 | sub.add(Clipper.MakePath(new long[] { 410, 290, 500, 290, 500, 310, 410, 310 })); 64 | sol = Clipper.RectClip(rect, sub); 65 | assertTrue(sol.isEmpty(), "Test 5 failed - should be empty"); 66 | 67 | // Test case 6: Triangle outside rect 68 | sub.clear(); 69 | sub.add(Clipper.MakePath(new long[] { 430, 290, 470, 330, 390, 330 })); 70 | sol = Clipper.RectClip(rect, sub); 71 | assertTrue(sol.isEmpty(), "Test 6 failed - should be empty"); 72 | 73 | // Test case 7: Triangle outside rect 74 | sub.clear(); 75 | sub.add(Clipper.MakePath(new long[] { 450, 290, 480, 330, 450, 330 })); 76 | sol = Clipper.RectClip(rect, sub); 77 | assertTrue(sol.isEmpty(), "Test 7 failed - should be empty"); 78 | 79 | // Test case 8: Complex polygon clipped, check bounds of result 80 | sub.clear(); 81 | sub.add(Clipper.MakePath(new long[] { 208, 66, 366, 112, 402, 303, 234, 332, 233, 262, 243, 140, 215, 126, 40, 172 })); 82 | rect = new Rect64(237, 164, 322, 248); 83 | sol = Clipper.RectClip(rect, sub); 84 | assertFalse(sol.isEmpty(), "Test 8 failed - should not be empty"); // Basic check 85 | Rect64 solBounds = Clipper.GetBounds(sol); 86 | // Check if the resulting bounds match the clipping rectangle bounds 87 | // Note: The clipped polygon might not *fill* the entire rect, but its bounds 88 | // should ideally be constrained *within* or *equal to* the clip rect if it 89 | // intersects fully. 90 | // The C++ test checks if the width/height *match* the clip rect width/height. 91 | // This implies the clipped result must touch all four sides of the clip rect. 92 | assertEquals(rect.getWidth(), solBounds.getWidth(), "Test 8 failed - Width mismatch"); 93 | assertEquals(rect.getHeight(), solBounds.getHeight(), "Test 8 failed - Height mismatch"); 94 | } 95 | 96 | @Test 97 | void testRectClip2() { 98 | Rect64 rect = new Rect64(54690, 0, 65628, 6000); 99 | Paths64 subject = new Paths64(); 100 | subject.add(Clipper.MakePath(new long[] { 700000, 6000, 0, 6000, 0, 5925, 700000, 5925 })); 101 | 102 | Paths64 solution = Clipper.RectClip(rect, subject); 103 | 104 | assertNotNull(solution, "TestRectClip2 Solution should not be null"); 105 | assertEquals(1, solution.size(), "TestRectClip2 Should have 1 path"); 106 | assertEquals(4, solution.get(0).size(), "TestRectClip2 Path should have 4 points"); 107 | } 108 | 109 | @Test 110 | void testRectClip3() { 111 | Rect64 r = new Rect64(-1800000000L, -137573171L, -1741475021L, 3355443L); 112 | Paths64 subject = new Paths64(); 113 | Paths64 solution; 114 | 115 | subject.add(Clipper.MakePath(new long[] { -1800000000L, 10005000L, -1800000000L, -5000L, -1789994999L, -5000L, -1789994999L, 10005000L })); 116 | 117 | solution = Clipper.RectClip(r, subject); 118 | 119 | assertNotNull(solution, "TestRectClip3 Solution should not be null"); 120 | assertEquals(1, solution.size(), "TestRectClip3 Should have 1 path"); 121 | assertFalse(solution.get(0).isEmpty(), "TestRectClip3 Path should not be empty"); 122 | Path64 expectedPath = Clipper.MakePath(new long[] { -1789994999L, 3355443L, -1800000000L, 3355443L, -1800000000L, -5000L, -1789994999L, -5000L }); 123 | assertEquals(Math.abs(Clipper.Area(expectedPath)), Math.abs(Clipper.Area(solution.get(0))), "TestRectClip3 Area check"); 124 | 125 | } 126 | 127 | private static Path64 clipper(Rect64 r, Paths64 subject) { 128 | return Clipper.Intersect(subject, new Paths64(r.AsPath()), FillRule.EvenOdd).get(0); 129 | } 130 | } -------------------------------------------------------------------------------- /src/test/resources/Lines.txt: -------------------------------------------------------------------------------- 1 | CAPTION: 1. 2 | CLIPTYPE: DIFFERENCE 3 | FILLRULE: EVENODD 4 | SOL_AREA: 8 5 | SOL_COUNT: 1 6 | SUBJECTS 7 | 5,4, 8,4, 8,8, 5,8 8 | SUBJECTS_OPEN 9 | 6,7, 6,5 10 | CLIPS 11 | 7,9, 4,9, 4,6, 7,6 12 | 13 | CAPTION: 2. 14 | CLIPTYPE: INTERSECTION 15 | FILLRULE: EVENODD 16 | SOL_AREA: 0 17 | SOL_COUNT: 0 18 | SUBJECTS_OPEN 19 | 40,10, 10,10, 10,90, 90,90, 90,10, 60,10 20 | CLIPS 21 | 0,0, 100,0, 100,100, 0,100 22 | 23 | CAPTION: 3. 24 | CLIPTYPE: INTERSECTION 25 | FILLRULE: EVENODD 26 | SOL_AREA: 0 27 | SOL_COUNT: 0 28 | SUBJECTS_OPEN 29 | 40,90, 10,90, 10,10, 90,10, 90,90, 60,90 30 | CLIPS 31 | 40,90, 10,90, 10,10, 90,10, 90,90, 60,90 32 | 33 | CAPTION: 4. 34 | CLIPTYPE: INTERSECTION 35 | FILLRULE: EVENODD 36 | SOL_AREA: 0 37 | SOL_COUNT: 0 38 | SUBJECTS_OPEN 39 | 40,90, 10,90, 10,10, 90,10, 90,90, 60,90 40 | CLIPS 41 | 0,0, 100,0, 100,100, 0,100 42 | 43 | CAPTION: 5. 44 | CLIPTYPE: INTERSECTION 45 | FILLRULE: EVENODD 46 | SOL_AREA: 0 47 | SOL_COUNT: 0 48 | SUBJECTS_OPEN 49 | 10,40, 10,10, 90,10, 90,90, 10,90, 10,60 50 | CLIPS 51 | 0,0, 100,0, 100,100, 0,100 52 | 53 | CAPTION: 6. 54 | CLIPTYPE: INTERSECTION 55 | FILLRULE: EVENODD 56 | SOL_AREA: 0 57 | SOL_COUNT: 0 58 | SUBJECTS_OPEN 59 | 90,40, 90,10, 10,10, 10,90, 90,90, 90,60 60 | CLIPS 61 | 0,0, 100,0, 100,100, 0,100 62 | 63 | CAPTION: 7. 64 | CLIPTYPE: INTERSECTION 65 | FILLRULE: EVENODD 66 | SOL_AREA: 0 67 | SOL_COUNT: 0 68 | SUBJECTS_OPEN 69 | 40,10, 10,10, 10,90, 90,90, 90,10, 60,10 70 | CLIPS 71 | 20,0, 120,0, 120,100, 20,100 72 | 73 | CAPTION: 8. 74 | CLIPTYPE: INTERSECTION 75 | FILLRULE: EVENODD 76 | SOL_AREA: 0 77 | SOL_COUNT: 0 78 | SUBJECTS_OPEN 79 | 40,10, 10,10, 10,90, 90,90, 90,10, 60,10 80 | CLIPS 81 | -20,0, 80,0, 80,100, -20,100 82 | 83 | CAPTION: 9. 84 | CLIPTYPE: INTERSECTION 85 | FILLRULE: EVENODD 86 | SOL_AREA: 0 87 | SOL_COUNT: 0 88 | SUBJECTS_OPEN 89 | 40,90, 10,90, 10,10, 90,10, 90,90, 60,90 90 | CLIPS 91 | 20,0, 120,0, 120,100, 20,100 92 | 93 | CAPTION: 10. 94 | CLIPTYPE: INTERSECTION 95 | FILLRULE: EVENODD 96 | SOL_AREA: 0 97 | SOL_COUNT: 0 98 | SUBJECTS_OPEN 99 | 40,90, 10,90, 10,10, 90,10, 90,90, 60,90 100 | CLIPS 101 | -20,0, 80,0, 80,100, -20,100 102 | 103 | CAPTION: 11. 104 | CLIPTYPE: INTERSECTION 105 | FILLRULE: EVENODD 106 | SOL_AREA: 0 107 | SOL_COUNT: 0 108 | SUBJECTS_OPEN 109 | 10,40, 10,10, 90,10, 90,90, 10,90, 10,60 110 | CLIPS 111 | 20,0, 120,0, 120,100, 20,100 112 | 113 | CAPTION: 12. 114 | CLIPTYPE: INTERSECTION 115 | FILLRULE: EVENODD 116 | SOL_AREA: 0 117 | SOL_COUNT: 0 118 | SUBJECTS_OPEN 119 | 10,40, 10,10, 90,10, 90,90, 10,90, 10,60 120 | CLIPS 121 | -20,0, 80,0, 80,100, -20,100 122 | 123 | CAPTION: 13. 124 | CLIPTYPE: INTERSECTION 125 | FILLRULE: EVENODD 126 | SOL_AREA: 0 127 | SOL_COUNT: 0 128 | SUBJECTS_OPEN 129 | 90,40, 90,10, 10,10, 10,90, 90,90, 90,60 130 | CLIPS 131 | 20,0, 120,0, 120,100, 20,100 132 | 133 | CAPTION: 14. 134 | CLIPTYPE: INTERSECTION 135 | FILLRULE: EVENODD 136 | SOL_AREA: 0 137 | SOL_COUNT: 0 138 | SUBJECTS_OPEN 139 | 90,40, 90,10, 10,10, 10,90, 90,90, 90,60 140 | CLIPS 141 | -20,0, 80,0, 80,100, -20,100 142 | 143 | CAPTION: 15. 144 | CLIPTYPE: INTERSECTION 145 | FILLRULE: EVENODD 146 | SOL_AREA: 0 147 | SOL_COUNT: 0 148 | SUBJECTS_OPEN 149 | 65,-15, 65,-55, -5,-55 150 | CLIPS 151 | 30,-80, 30,0, 50,0, 50,-80 152 | 153 | CAPTION: 16. 154 | CLIPTYPE: INTERSECTION 155 | FILLRULE: EVENODD 156 | SOL_AREA: 0 157 | SOL_COUNT: 0 158 | SUBJECTS_OPEN 159 | 30,-80, 30,0 160 | CLIPS 161 | -5,-55, -5,-15, 65,-15, 65,-55 162 | 163 | --------------------------------------------------------------------------------