├── .babelrc ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── config.js ├── package-lock.json ├── package.json ├── src ├── index.d.ts └── index.js └── tests ├── index.html └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | // transpile to common node & browser compatible js, keeping modules 4 | "module": { 5 | "presets": [ 6 | ["latest", { 7 | "modules": false 8 | }] 9 | ] 10 | }, 11 | // transpile to common node & browser compatible js, using commonjs 12 | "main": { 13 | "presets": ["latest"] 14 | } 15 | }, 16 | "plugins": [ 17 | "babel-plugin-transform-object-rest-spread" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | jspm_packages 3 | 4 | node_modules 5 | 6 | lib 7 | module 8 | dist -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | 2 | jspm_packages 3 | 4 | node_modules 5 | 6 | tests -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Casper Lamboo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ClipperJS 2 | [Clipper](https://sourceforge.net/projects/jsclipper/) abstraction layer (simplified API) 3 | 4 | Target of this library is to remove complexity and create an overall cleaner, JavaScript idiomatic API for Clipper. 5 | 6 | When using this API one class, Shape, is used. Shape is a collection of paths that are all collectively closed or open. Shapes with holes are defined as multiple closed paths where the outlines are clockwise and the holes are counter-clockwise. Shapes has multiple functions that can be used to for instance compute complex boolean operations from one just call. 7 | 8 | The next two code examples show a simple Boolean operation in clipper and in ClipperJS. Both use two predefined paths 9 | 10 | ```javascript 11 | const subjectPaths = [[{ X: 30, Y: 30 }, { X: 10, Y: 30 }, { X: 10, Y: 10 }, { X: 30, Y: 10 }]]; 12 | const clipPaths = [[{ X: 20, Y: 20 }, { X: 0, Y: 20 }, { X: 0, Y:0 }, { X: 20, Y: 0 }]]; 13 | ``` 14 | 15 | A boolean intersect operation in clipper looks like this 16 | ```javascript 17 | const result = new ClipperLib.Paths(); 18 | const clipper = new ClipperLib.Clipper(); 19 | clipper.AddPaths(subjectPaths, ClipperLib.PolyType.ptSubject, true); 20 | clipper.AddPaths(clipPaths, ClipperLib.PolyType.ptClip, true); 21 | clipper.Execute(ClipperLib.ClipType.ctIntersection, result); 22 | 23 | // result = [[{ X: 20, Y: 20 }, { X: 10, Y: 20 }, { X: 10, Y: 10 }, { X: 20, Y: 10 }]] 24 | ``` 25 | 26 | In ClipperJS 27 | ```javascript 28 | const subject = new Shape(subjectPaths, true); 29 | const clip = new Shape(subjectPaths, true); 30 | 31 | const result = subject.intersect(clip); 32 | 33 | // result = { closed: true, paths: [[{ X: 20, Y: 20 }, { X: 10, Y: 20 }, { X: 10, Y: 10 }, { X: 20, Y: 10 }]] } 34 | ``` 35 | 36 | # Usage 37 | 38 | ### Using JSPM (ECMAScript / ES6 Module) 39 | 40 | Install the library. 41 | 42 | ``` 43 | jspm install github:Doodle3D/clipper-js 44 | ``` 45 | 46 | Include the library. 47 | 48 | ```javascript 49 | import Shape from 'Doodle3D/clipper-js'; 50 | ``` 51 | 52 | ### Using NPM (CommonJS module) 53 | 54 | Install the library. 55 | 56 | ``` 57 | npm install @doodle3d/clipper-js --save 58 | ``` 59 | 60 | Include the library. 61 | 62 | ```javascript 63 | var Shape = require('clipper-js'); 64 | ``` 65 | 66 | # API 67 | 68 | **Shape** 69 | 70 | Shape accepts 3 optional arguments, `paths`, `closed` and `capitalConversion`. `paths` can be be devined with both upper case and lower case. Clipper only uses uppercase properties, when input is given with lower case `captalConversion` needs to be set to `true`. 71 | ```javascript 72 | new Shape([ paths = [], closed = true, capitalConversion = false, integerConversion = false, removeDuplicates = false ]) 73 | 74 | paths = [...[...{ X: Number, Y: Number }] || [...[...{ x: Number, y: Number }] 75 | paths = Array 76 | closed = Bool 77 | capitalConversion = Bool 78 | integerConversion = Bool 79 | removeDuplicates = Bool 80 | ``` 81 | - paths: the paths that make up the shape 82 | - closed: Shape is a polygon or line 83 | - capitalConversion: converts lower case x and y to uppercase X and Y 84 | - integerConversion: clipper only works with intergers, sometimes when input is in floats clipper fails 85 | - removeDuplicates: clipper sometimes fails when there are duplicate points in the data set, this argument filters out all the duplicate points 86 | 87 | `Note: due to the nature of Clipper, some functions are destructive and some are non-destructive.` 88 | 89 | 90 | **[Boolean operation: Union](https://sourceforge.net/p/jsclipper/wiki/documentation/#clipperlibclipperexecute)** 91 | ```javascript 92 | Shape = Shape.union( clipShape: Shape ) 93 | ``` 94 | - clipShape: clip of the boolean operation 95 | 96 | 97 | **[Boolean operation: Difference](https://sourceforge.net/p/jsclipper/wiki/documentation/#clipperlibclipperexecute)** 98 | ```javascript 99 | Shape = Shape.difference( clipShape: Shape ) 100 | ``` 101 | - clipShape: clip of the boolean operation 102 | 103 | 104 | **[Boolean operation: Intersect](https://sourceforge.net/p/jsclipper/wiki/documentation/#clipperlibclipperexecute)** 105 | ```javascript 106 | Shape = Shape.intersect( clipShape: Shape ) 107 | ``` 108 | - clipShape: clip of the boolean operation 109 | 110 | 111 | **[Boolean operation: Xor](https://sourceforge.net/p/jsclipper/wiki/documentation/#clipperlibclipperexecute)** 112 | ```javascript 113 | Shape = Shape.xor( clipShape: Shape ) 114 | ``` 115 | - clipShape: clip of the boolean operation 116 | 117 | 118 | **[Offset](https://sourceforge.net/p/jsclipper/wiki/documentation/#clipperoffset)** 119 | ```javascript 120 | Shape = Shape.offset( offset: Number, options: { 121 | jointType = 'jtSquare', 122 | endType = 'etClosedPolygon', 123 | miterLimit = 2.0, 124 | roundPrecision = 0.25 125 | } ) 126 | ``` 127 | - offset: clip off the boolean operation 128 | - options: optional arguments with default values 129 | - [jointType](https://sourceforge.net/p/jsclipper/wiki/documentation/#clipperlibjointype): join type of the offset 130 | - [endType](https://sourceforge.net/p/jsclipper/wiki/documentation/#clipperlibendtype): end type of the offset 131 | - [mitterLimit](https://sourceforge.net/p/jsclipper/wiki/documentation/#clipperlibclipperoffsetmiterlimit): mitter limit 132 | - [roundPrecision](https://sourceforge.net/p/jsclipper/wiki/documentation/#clipperlibclipperoffsetarctolerance): arc tolerance 133 | 134 | offsets the shape along its normal. 135 | 136 | 137 | **[Scale Up](https://sourceforge.net/p/jsclipper/wiki/documentation/#clipperlibjsscaleuppaths)** 138 | ```javascript 139 | Shape.scaleUp( factor: Number ) 140 | ``` 141 | scale up with factor. 142 | 143 | `Note: destructive` 144 | 145 | 146 | **[Scale Down](https://sourceforge.net/p/jsclipper/wiki/documentation/#clipperlibjsscaledownpaths)** 147 | ```javascript 148 | Shape.scaleDown( factor: Number ) 149 | ``` 150 | scale up with factor. 151 | 152 | `Note: destructive` 153 | 154 | 155 | **First Point** 156 | ```javascript 157 | { X: Number, Y: Number } || { x: Number, y: Number } = Shape.firstPoint([ capitalConversion: false ]) 158 | ``` 159 | - capitalConversion: converts uppercase X and Y to lowercase x and y. 160 | 161 | returns position of the first point. 162 | 163 | 164 | **Last Point** 165 | ```javascript 166 | { X: Number, Y: Number } || { x: Number, y: Number } = Shape.lastPoint([ capitalConversion: false ]) 167 | ``` 168 | - capitalConversion: converts uppercase X and Y to lowercase x and y. 169 | 170 | returns position of the last point. 171 | 172 | 173 | **Total Area** 174 | ```javascript 175 | Number = Shape.totalArea() 176 | ``` 177 | returns total area of the shape. 178 | 179 | 180 | **[Area](https://sourceforge.net/p/jsclipper/wiki/documentation/#clipperlibclipperarea)** 181 | ```javascript 182 | Number = Shape.area( index: Int ) 183 | ``` 184 | - index: index of the sub shape 185 | 186 | returns area of the sub shape (negative if counter-clock wise). 187 | 188 | 189 | **Areas** 190 | ```javascript 191 | [...Number] = Shape.areas() 192 | ``` 193 | returns array of areas of all sub shapes. 194 | 195 | **Total Perimeter** 196 | ```javascript 197 | Number = Shape.totalPerimeter() 198 | ``` 199 | returns total perimeter of the shape. 200 | 201 | 202 | **[Perimeter](https://sourceforge.net/p/jsclipper/wiki/documentation/#clipperlibjsperimeterofpath)** 203 | ```javascript 204 | Number = Shape.perimeter( index: Int ) 205 | ``` 206 | - index: index of the sub shape 207 | 208 | returns perimeter of the sub shape. 209 | 210 | 211 | **Perimeters** 212 | ```javascript 213 | [...Number] = Shape.perimeters() 214 | ``` 215 | returns array of perimeters of all sub shapes. 216 | 217 | 218 | **[Reverse](https://sourceforge.net/p/jsclipper/wiki/documentation/#clipperlibclipperreversepaths)** 219 | ```javascript 220 | Shape.reverse() 221 | ``` 222 | reverses the order of all sub shapes. 223 | 224 | 225 | **Treshold Area** 226 | ```javascript 227 | Shape.tresholdArea( minArea: Number ) 228 | ``` 229 | - minArea: minimal size of area 230 | 231 | removes all sub shapes from shape which are smaller then min area. 232 | 233 | 234 | **Join** 235 | ```javascript 236 | Shape.join( shape ) 237 | ``` 238 | joins shape with given shape. 239 | 240 | `Note: destructive` 241 | 242 | 243 | **[Clone](https://sourceforge.net/p/jsclipper/wiki/documentation/#clipperlibjsclone)** 244 | ```javascript 245 | Shape = Shape.clone() 246 | ``` 247 | returns copy of shape. 248 | 249 | 250 | **[Shape Bounds](https://sourceforge.net/p/jsclipper/wiki/documentation/#clipperlibjsboundsofpaths)** 251 | ```javascript 252 | { 253 | left: Int, 254 | right: Int, 255 | top: Int, 256 | bottom: Int, 257 | width: Int, 258 | height: Int, 259 | size: Int 260 | } = Shape.shapeBounds() 261 | ``` 262 | returns bounding box of shape. 263 | 264 | 265 | **[Path Bounds](https://sourceforge.net/p/jsclipper/wiki/documentation/#clipperlibjsboundsofpath)** 266 | ```javascript 267 | { 268 | left: Int, 269 | right: Int, 270 | top: Int, 271 | bottom: Int, 272 | width: Int, 273 | height: Int, 274 | size: Int 275 | } = Shape.pathBounds( index: Int ) 276 | ``` 277 | returns bounding box of sub shape. 278 | 279 | 280 | **[Clean](https://sourceforge.net/p/jsclipper/wiki/documentation/#clipperlibclippercleanpolygons)** 281 | ```javascript 282 | Shape = Shape.clean( cleanDelta: Number ) 283 | ``` 284 | 285 | **[Orientation](https://sourceforge.net/p/jsclipper/wiki/documentation/#clipperlibclipperorientation)** 286 | ```javascript 287 | Bool = Shape.orientation( index: Int ) 288 | ``` 289 | returns orientation of the sub shape. True if clockwise, false if counter clock wise. 290 | 291 | 292 | **Point In Shape** 293 | ```javascript 294 | Bool = Shape.pointInShape( { X: Number, Y: Number }, [ capitalConversion = false, integerConversion = false ] ) 295 | ``` 296 | - point: position used for hit detection 297 | - capitalConversion: converts lower case x and y to uppercase X and Y 298 | - integerConversion: converts point to intpoint 299 | 300 | returns if point is in shape. 301 | 302 | 303 | **[Point In Path](https://sourceforge.net/p/jsclipper/wiki/documentation/#clipperlibclipperpointinpolygon)** 304 | ```javascript 305 | Bool = Shape.pointInPath( index: Int, { X: Number, Y: Number }, [ capitalConversion = false, integerConversion = false ] ) 306 | ``` 307 | - point: position used for hit detection 308 | - index: index of sub shape 309 | - capitalConversion: converts lower case x and y to uppercase X and Y 310 | - integerConversion: converts point to intpoint 311 | 312 | returns if point is in sub shape. 313 | 314 | 315 | **Fix Orientation** 316 | ```javascript 317 | Shape.fixOrientation() 318 | ``` 319 | when given path with holes, outline must be clockwise and holes must be counter-clockwise. Tries to fix the orientation. 320 | 321 | `Note: destructive` 322 | 323 | 324 | **[Simplify](https://sourceforge.net/p/jsclipper/wiki/documentation/#clipperlibclippersimplifypolygons)** 325 | ```javascript 326 | Shape = Shape.simplify(fillType: String) 327 | ``` 328 | Simplifies shape using filltype. Only works for closed shapes. 329 | 330 | 331 | **Separate Shapes** 332 | ```javascript 333 | [...Shape] = Shape.separateShapes() 334 | ``` 335 | when using union operations multiple shapes can be created. Separate Shapes splits these shapes into separate instances. All shapes keep their holes. 336 | 337 | 338 | **Round** 339 | ```javascript 340 | Shape = Shape.round() 341 | ``` 342 | Returns new instance of Shape with all points rounded to Integers. 343 | 344 | **Remove Duplicates** 345 | ```javascript 346 | Shape = Shape.removeDuplicates() 347 | ``` 348 | Returns new instance of Shape with all duplicate points removed. 349 | 350 | **Map To Lower** 351 | ```javascript 352 | [...[...{ x: Number, y: Number }]] = Shape.mapToLower() 353 | ``` 354 | returns paths array with lower case x and y. 355 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | System.config({ 2 | baseURL: "/", 3 | defaultJSExtensions: true, 4 | transpiler: "babel", 5 | babelOptions: { 6 | "optional": [ 7 | "runtime", 8 | "optimisation.modules.system" 9 | ] 10 | }, 11 | paths: { 12 | "github:*": "jspm_packages/github/*", 13 | "npm:*": "jspm_packages/npm/*" 14 | }, 15 | 16 | map: { 17 | "babel": "npm:babel-core@5.8.35", 18 | "babel-runtime": "npm:babel-runtime@5.8.35", 19 | "clipper-lib": "npm:clipper-lib@1.0.0", 20 | "core-js": "npm:core-js@1.2.6", 21 | "tape": "npm:tape@4.4.0", 22 | "github:jspm/nodelibs-assert@0.1.0": { 23 | "assert": "npm:assert@1.3.0" 24 | }, 25 | "github:jspm/nodelibs-buffer@0.1.0": { 26 | "buffer": "npm:buffer@3.6.0" 27 | }, 28 | "github:jspm/nodelibs-events@0.1.1": { 29 | "events": "npm:events@1.0.2" 30 | }, 31 | "github:jspm/nodelibs-path@0.1.0": { 32 | "path-browserify": "npm:path-browserify@0.0.0" 33 | }, 34 | "github:jspm/nodelibs-process@0.1.2": { 35 | "process": "npm:process@0.11.2" 36 | }, 37 | "github:jspm/nodelibs-stream@0.1.0": { 38 | "stream-browserify": "npm:stream-browserify@1.0.0" 39 | }, 40 | "github:jspm/nodelibs-util@0.1.0": { 41 | "util": "npm:util@0.10.3" 42 | }, 43 | "npm:assert@1.3.0": { 44 | "util": "npm:util@0.10.3" 45 | }, 46 | "npm:babel-runtime@5.8.35": { 47 | "process": "github:jspm/nodelibs-process@0.1.2" 48 | }, 49 | "npm:brace-expansion@1.1.3": { 50 | "balanced-match": "npm:balanced-match@0.3.0", 51 | "concat-map": "npm:concat-map@0.0.1" 52 | }, 53 | "npm:buffer@3.6.0": { 54 | "base64-js": "npm:base64-js@0.0.8", 55 | "child_process": "github:jspm/nodelibs-child_process@0.1.0", 56 | "fs": "github:jspm/nodelibs-fs@0.1.2", 57 | "ieee754": "npm:ieee754@1.1.6", 58 | "isarray": "npm:isarray@1.0.0", 59 | "process": "github:jspm/nodelibs-process@0.1.2" 60 | }, 61 | "npm:clipper-lib@1.0.0": { 62 | "process": "github:jspm/nodelibs-process@0.1.2" 63 | }, 64 | "npm:core-js@1.2.6": { 65 | "fs": "github:jspm/nodelibs-fs@0.1.2", 66 | "path": "github:jspm/nodelibs-path@0.1.0", 67 | "process": "github:jspm/nodelibs-process@0.1.2", 68 | "systemjs-json": "github:systemjs/plugin-json@0.1.0" 69 | }, 70 | "npm:core-util-is@1.0.2": { 71 | "buffer": "github:jspm/nodelibs-buffer@0.1.0" 72 | }, 73 | "npm:define-properties@1.1.2": { 74 | "foreach": "npm:foreach@2.0.5", 75 | "object-keys": "npm:object-keys@1.0.9" 76 | }, 77 | "npm:es-abstract@1.5.0": { 78 | "es-to-primitive": "npm:es-to-primitive@1.1.1", 79 | "function-bind": "npm:function-bind@1.0.2", 80 | "is-callable": "npm:is-callable@1.1.2", 81 | "is-regex": "npm:is-regex@1.0.3" 82 | }, 83 | "npm:es-to-primitive@1.1.1": { 84 | "is-callable": "npm:is-callable@1.1.2", 85 | "is-date-object": "npm:is-date-object@1.0.1", 86 | "is-symbol": "npm:is-symbol@1.0.1" 87 | }, 88 | "npm:glob@5.0.15": { 89 | "assert": "github:jspm/nodelibs-assert@0.1.0", 90 | "events": "github:jspm/nodelibs-events@0.1.1", 91 | "fs": "github:jspm/nodelibs-fs@0.1.2", 92 | "inflight": "npm:inflight@1.0.4", 93 | "inherits": "npm:inherits@2.0.1", 94 | "minimatch": "npm:minimatch@3.0.0", 95 | "once": "npm:once@1.3.3", 96 | "path": "github:jspm/nodelibs-path@0.1.0", 97 | "path-is-absolute": "npm:path-is-absolute@1.0.0", 98 | "process": "github:jspm/nodelibs-process@0.1.2", 99 | "util": "github:jspm/nodelibs-util@0.1.0" 100 | }, 101 | "npm:has@1.0.1": { 102 | "function-bind": "npm:function-bind@1.0.2" 103 | }, 104 | "npm:inflight@1.0.4": { 105 | "once": "npm:once@1.3.3", 106 | "process": "github:jspm/nodelibs-process@0.1.2", 107 | "wrappy": "npm:wrappy@1.0.1" 108 | }, 109 | "npm:inherits@2.0.1": { 110 | "util": "github:jspm/nodelibs-util@0.1.0" 111 | }, 112 | "npm:is-date-object@1.0.1": { 113 | "systemjs-json": "github:systemjs/plugin-json@0.1.0" 114 | }, 115 | "npm:is-regex@1.0.3": { 116 | "systemjs-json": "github:systemjs/plugin-json@0.1.0" 117 | }, 118 | "npm:isarray@1.0.0": { 119 | "systemjs-json": "github:systemjs/plugin-json@0.1.0" 120 | }, 121 | "npm:minimatch@3.0.0": { 122 | "brace-expansion": "npm:brace-expansion@1.1.3", 123 | "path": "github:jspm/nodelibs-path@0.1.0" 124 | }, 125 | "npm:once@1.3.3": { 126 | "wrappy": "npm:wrappy@1.0.1" 127 | }, 128 | "npm:path-browserify@0.0.0": { 129 | "process": "github:jspm/nodelibs-process@0.1.2" 130 | }, 131 | "npm:path-is-absolute@1.0.0": { 132 | "process": "github:jspm/nodelibs-process@0.1.2" 133 | }, 134 | "npm:process@0.11.2": { 135 | "assert": "github:jspm/nodelibs-assert@0.1.0" 136 | }, 137 | "npm:readable-stream@1.1.13": { 138 | "buffer": "github:jspm/nodelibs-buffer@0.1.0", 139 | "core-util-is": "npm:core-util-is@1.0.2", 140 | "events": "github:jspm/nodelibs-events@0.1.1", 141 | "inherits": "npm:inherits@2.0.1", 142 | "isarray": "npm:isarray@0.0.1", 143 | "process": "github:jspm/nodelibs-process@0.1.2", 144 | "stream-browserify": "npm:stream-browserify@1.0.0", 145 | "string_decoder": "npm:string_decoder@0.10.31" 146 | }, 147 | "npm:resolve@1.1.7": { 148 | "fs": "github:jspm/nodelibs-fs@0.1.2", 149 | "path": "github:jspm/nodelibs-path@0.1.0", 150 | "process": "github:jspm/nodelibs-process@0.1.2", 151 | "systemjs-json": "github:systemjs/plugin-json@0.1.0" 152 | }, 153 | "npm:resumer@0.0.0": { 154 | "process": "github:jspm/nodelibs-process@0.1.2", 155 | "through": "npm:through@2.3.8" 156 | }, 157 | "npm:stream-browserify@1.0.0": { 158 | "events": "github:jspm/nodelibs-events@0.1.1", 159 | "inherits": "npm:inherits@2.0.1", 160 | "readable-stream": "npm:readable-stream@1.1.13" 161 | }, 162 | "npm:string.prototype.trim@1.1.2": { 163 | "define-properties": "npm:define-properties@1.1.2", 164 | "es-abstract": "npm:es-abstract@1.5.0", 165 | "function-bind": "npm:function-bind@1.0.2" 166 | }, 167 | "npm:string_decoder@0.10.31": { 168 | "buffer": "github:jspm/nodelibs-buffer@0.1.0" 169 | }, 170 | "npm:tape@4.4.0": { 171 | "deep-equal": "npm:deep-equal@1.0.1", 172 | "defined": "npm:defined@1.0.0", 173 | "events": "github:jspm/nodelibs-events@0.1.1", 174 | "fs": "github:jspm/nodelibs-fs@0.1.2", 175 | "function-bind": "npm:function-bind@1.0.2", 176 | "glob": "npm:glob@5.0.15", 177 | "has": "npm:has@1.0.1", 178 | "inherits": "npm:inherits@2.0.1", 179 | "minimist": "npm:minimist@1.2.0", 180 | "object-inspect": "npm:object-inspect@1.0.2", 181 | "path": "github:jspm/nodelibs-path@0.1.0", 182 | "process": "github:jspm/nodelibs-process@0.1.2", 183 | "resolve": "npm:resolve@1.1.7", 184 | "resumer": "npm:resumer@0.0.0", 185 | "string.prototype.trim": "npm:string.prototype.trim@1.1.2", 186 | "through": "npm:through@2.3.8" 187 | }, 188 | "npm:through@2.3.8": { 189 | "process": "github:jspm/nodelibs-process@0.1.2", 190 | "stream": "github:jspm/nodelibs-stream@0.1.0" 191 | }, 192 | "npm:util@0.10.3": { 193 | "inherits": "npm:inherits@2.0.1", 194 | "process": "github:jspm/nodelibs-process@0.1.2" 195 | } 196 | } 197 | }); 198 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@doodle3d/clipper-js", 3 | "version": "1.0.11", 4 | "description": "Clipper-lib abstraction layer (simplified API)", 5 | "main": "lib/index.js", 6 | "module": "module/index.js", 7 | "esnext": "src/index.js", 8 | "scripts": { 9 | "prepublish": "npm run build", 10 | "build": "npm run build:main && npm run build:module", 11 | "build:main": "BABEL_ENV=main babel src -s -d lib", 12 | "build:module": "BABEL_ENV=module babel src -s -d module", 13 | "test": "npm run test:node", 14 | "test:browser": "npm run serve -- --startPath='tests/'", 15 | "test:node": "babel-node tests/index.js" 16 | }, 17 | "typings": "src/index.d.ts", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://www.github.com/Doodle3D/clipper-js.git" 21 | }, 22 | "keywords": [ 23 | "clipper", 24 | "vector", 25 | "js" 26 | ], 27 | "author": "Casper @ Doodle3D", 28 | "license": "MIT", 29 | "devDependencies": { 30 | "babel-cli": "^6.5.1", 31 | "babel-core": "^6.5.2", 32 | "babel-plugin-transform-object-rest-spread": "^6.23.0", 33 | "babel-preset-latest": "^6.5.0", 34 | "browser-sync": "^2.26.7", 35 | "server": "^1.0.27", 36 | "tape": "^4.13.2" 37 | }, 38 | "dependencies": { 39 | "@doodle3d/clipper-lib": "^6.4.2-b" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | export interface Bounds { 2 | left: number; 3 | right: number; 4 | top: number; 5 | bottom: number; 6 | } 7 | 8 | export interface Point { 9 | X: number; 10 | Y: number; 11 | } 12 | 13 | export interface PointLower { 14 | x: number; 15 | y: number; 16 | } 17 | 18 | export default class Shape { 19 | paths: Point[][]; 20 | closed: boolean; 21 | 22 | constructor(paths: Point[][]); 23 | constructor(paths: Point[][], closed: boolean); 24 | constructor(paths: (Point | PointLower)[][], closed: boolean, capitalConversion: boolean, integerConversion?: boolean); 25 | constructor(paths: (Point | PointLower)[][], closed: boolean, capitalConversion: boolean, integerConversion?: boolean, removeDuplicates?: boolean); 26 | 27 | union(clipShape: Shape): Shape; 28 | difference(clipShape: Shape): Shape; 29 | intersect(clipShape: Shape): Shape; 30 | xor(clipShape: Shape): Shape; 31 | 32 | offset(offset: number, options?: any): Shape; 33 | scaleUp(factor: number): this; 34 | scaleDown(factor: number): this; 35 | 36 | firstPoint(): Point 37 | firstPoint(toLower?: boolean): Point | PointLower; 38 | lastPoint(): Point 39 | lastPoint(toLower?: boolean): Point | PointLower; 40 | 41 | areas(): number[]; 42 | area(index: number): number; 43 | totalArea(): number; 44 | perimeter(index: number): number; 45 | perimeters(): number[]; 46 | totalPerimeter(): number; 47 | reverse(): this; 48 | thresholdArea(minArea: number): this; 49 | 50 | join(shape: Shape): this; 51 | clone(): Shape; 52 | 53 | shapeBounds(): Bounds; 54 | pathBounds(index: number): Bounds; 55 | 56 | clean(cleanDelta: number): Shape; 57 | 58 | orientation(index: number): boolean; 59 | pointInShape(point: Point): boolean; 60 | pointInShape(point: Point | PointLower, capitalConversion: boolean, integerConversion?: boolean): boolean; 61 | pointInPath(index: number, point: Point): boolean; 62 | pointInPath(index: number, point: Point | PointLower, capitalConversion: boolean, integerConversion?: boolean): boolean; 63 | 64 | fixOrientation(): this; 65 | simplify(fillType: string): Shape | this; 66 | separateShapes(): Shape[]; 67 | round(): Shape; 68 | removeDuplicates(): Shape; 69 | mapToLower(): PointLower[][]; 70 | } 71 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import ClipperLib from '@doodle3d/clipper-lib/clipper'; 2 | 3 | let errorCallback; 4 | export const setErrorCallback = (callback) => { 5 | errorCallback = callback; 6 | } 7 | ClipperLib.Error = (message) => { 8 | if (errorCallback) errorCallback(message); 9 | } 10 | 11 | const CLIPPER = new ClipperLib.Clipper(); 12 | const CLIPPER_OFFSET = new ClipperLib.ClipperOffset(); 13 | 14 | export default class Shape { 15 | constructor(paths = [], closed = true, capitalConversion = false, integerConversion = false, removeDuplicates = false) { 16 | this.paths = paths; 17 | if (capitalConversion) this.paths = this.paths.map(mapLowerToCapital); 18 | if (integerConversion) this.paths = this.paths.map(mapToRound); 19 | if (removeDuplicates) this.paths = this.paths.map(filterPathsDuplicates); 20 | this.closed = closed; 21 | } 22 | 23 | _clip(type, ...clipShapes) { 24 | const solution = new ClipperLib.PolyTree(); 25 | 26 | CLIPPER.Clear(); 27 | CLIPPER.AddPaths(this.paths, ClipperLib.PolyType.ptSubject, this.closed); 28 | for (let i = 0; i < clipShapes.length; i ++) { 29 | const clipShape = clipShapes[i]; 30 | CLIPPER.AddPaths(clipShape.paths, ClipperLib.PolyType.ptClip, clipShape.closed); 31 | } 32 | CLIPPER.Execute(type, solution); 33 | 34 | const newShape = ClipperLib.Clipper.PolyTreeToPaths(solution); 35 | return new Shape(newShape, this.closed); 36 | } 37 | 38 | union(...clipShapes) { 39 | return this._clip(ClipperLib.ClipType.ctUnion, ...clipShapes); 40 | } 41 | 42 | difference(...clipShapes) { 43 | return this._clip(ClipperLib.ClipType.ctDifference, ...clipShapes); 44 | } 45 | 46 | intersect(...clipShapes) { 47 | return this._clip(ClipperLib.ClipType.ctIntersection, ...clipShapes); 48 | } 49 | 50 | xor(...clipShapes) { 51 | return this._clip(ClipperLib.ClipType.ctXor, ...clipShapes); 52 | } 53 | 54 | offset(offset, options = {}) { 55 | const { 56 | jointType = 'jtSquare', 57 | endType = 'etClosedPolygon', 58 | miterLimit = 2.0, 59 | roundPrecision = 0.25 60 | } = options; 61 | 62 | CLIPPER_OFFSET.Clear(); 63 | CLIPPER_OFFSET.ArcTolerance = roundPrecision; 64 | CLIPPER_OFFSET.MiterLimit = miterLimit; 65 | 66 | const offsetPaths = new ClipperLib.Paths(); 67 | CLIPPER_OFFSET.AddPaths(this.paths, ClipperLib.JoinType[jointType], ClipperLib.EndType[endType]); 68 | CLIPPER_OFFSET.Execute(offsetPaths, offset); 69 | 70 | return new Shape(offsetPaths, true); 71 | } 72 | 73 | scaleUp(factor) { 74 | ClipperLib.JS.ScaleUpPaths(this.paths, factor); 75 | 76 | return this; 77 | } 78 | 79 | scaleDown(factor) { 80 | ClipperLib.JS.ScaleDownPaths(this.paths, factor); 81 | 82 | return this; 83 | } 84 | 85 | firstPoint(toLower = false) { 86 | if (this.paths.length === 0) { 87 | return; 88 | } 89 | 90 | const firstPath = this.paths[0]; 91 | const firstPoint = firstPath[0]; 92 | if (toLower) { 93 | return vectorToLower(firstPoint); 94 | } else { 95 | return firstPoint; 96 | } 97 | } 98 | 99 | lastPoint(toLower = false) { 100 | if (this.paths.length === 0) { 101 | return; 102 | } 103 | 104 | const lastPath = this.paths[this.paths.length - 1]; 105 | const lastPoint = this.closed ? lastPath[0] : lastPath[lastPath.length - 1]; 106 | if (toLower) { 107 | return vectorToLower(lastPoint); 108 | } else { 109 | return lastPoint; 110 | } 111 | } 112 | 113 | areas() { 114 | const areas = this.paths.map((path, i) => this.area(i)); 115 | return areas; 116 | } 117 | 118 | area(index) { 119 | const path = this.paths[index]; 120 | const area = ClipperLib.Clipper.Area(path); 121 | return area; 122 | } 123 | 124 | totalArea() { 125 | return this.areas().reduce((totalArea, area) => totalArea + area, 0); 126 | } 127 | 128 | perimeter(index) { 129 | const path = this.paths[index]; 130 | const perimeter = ClipperLib.JS.PerimeterOfPath(path, this.closed, 1); 131 | return perimeter; 132 | } 133 | 134 | perimeters() { 135 | return this.paths.map(path => ClipperLib.JS.PerimeterOfPath(path, this.closed, 1)); 136 | } 137 | 138 | totalPerimeter() { 139 | const perimeter = ClipperLib.JS.PerimeterOfPaths(this.paths, this.closed); 140 | return perimeter; 141 | } 142 | 143 | reverse() { 144 | ClipperLib.Clipper.ReversePaths(this.paths); 145 | 146 | return this; 147 | } 148 | 149 | thresholdArea(minArea) { 150 | for (const path of [...this.paths]) { 151 | const area = Math.abs(ClipperLib.Clipper.Area(path)); 152 | 153 | if (area < minArea) { 154 | const index = this.paths.indexOf(path); 155 | this.paths.splice(index, 1); 156 | } 157 | } 158 | return this; 159 | } 160 | 161 | join(shape) { 162 | this.paths.splice(this.paths.length, 0, ...shape.paths); 163 | 164 | return this; 165 | } 166 | 167 | clone() { 168 | return new Shape(ClipperLib.JS.Clone(this.paths), this.closed); 169 | } 170 | 171 | shapeBounds() { 172 | return ClipperLib.JS.BoundsOfPaths(this.paths); 173 | } 174 | 175 | pathBounds(index) { 176 | const path = this.paths[index]; 177 | 178 | return ClipperLib.JS.BoundsOfPath(path); 179 | } 180 | 181 | clean(cleanDelta) { 182 | return new Shape(ClipperLib.Clipper.CleanPolygons(this.paths, cleanDelta), this.closed); 183 | } 184 | 185 | orientation(index) { 186 | const path = this.paths[index]; 187 | return ClipperLib.Clipper.Orientation(path); 188 | } 189 | 190 | pointInShape(point, capitalConversion = false, integerConversion = false) { 191 | if (capitalConversion) point = vectorToCapital(point); 192 | if (integerConversion) point = roundVector(point); 193 | for (let i = 0; i < this.paths.length; i ++) { 194 | const pointInPath = this.pointInPath(i, point); 195 | const orientation = this.orientation(i); 196 | 197 | if ((!pointInPath && orientation) || (pointInPath && !orientation)) { 198 | return false; 199 | } 200 | } 201 | 202 | return true; 203 | } 204 | 205 | pointInPath(index, point, capitalConversion = false, integerConversion = false) { 206 | if (capitalConversion) point = vectorToCapital(point); 207 | if (integerConversion) point = roundVector(point); 208 | const path = this.paths[index]; 209 | const intPoint = { X: Math.round(point.X), Y: Math.round(point.Y) }; 210 | 211 | return ClipperLib.Clipper.PointInPolygon(intPoint, path) > 0; 212 | } 213 | 214 | fixOrientation() { 215 | if (!this.closed) { 216 | return this; 217 | } 218 | 219 | if (this.totalArea() < 0) { 220 | this.reverse(); 221 | } 222 | 223 | return this; 224 | } 225 | 226 | simplify(fillType) { 227 | if (this.closed) { 228 | const shape = ClipperLib.Clipper.SimplifyPolygons(this.paths, ClipperLib.PolyFillType[fillType]); 229 | return new Shape(shape, true); 230 | } else { 231 | return this; 232 | } 233 | } 234 | 235 | separateShapes() { 236 | const shapes = []; 237 | 238 | if (!this.closed) { 239 | for (const path of this.paths) { 240 | shapes.push(new Shape([path], false)); 241 | } 242 | } else { 243 | const areas = new WeakMap(); 244 | const outlines = []; 245 | const holes = []; 246 | 247 | for (let i = 0; i < this.paths.length; i ++) { 248 | const path = this.paths[i]; 249 | const orientation = this.orientation(i); 250 | 251 | if (orientation) { 252 | const area = this.area(i); 253 | areas.set(path, area); 254 | outlines.push(path); 255 | } else { 256 | holes.push(path); 257 | } 258 | } 259 | 260 | outlines.sort((a, b) => areas.get(a) - areas.get(b)); 261 | 262 | for (const outline of outlines) { 263 | const shape = [outline]; 264 | 265 | const index = this.paths.indexOf(outline); 266 | 267 | for (const hole of [...holes]) { 268 | const pointInHole = this.pointInPath(index, hole[0]); 269 | if (pointInHole) { 270 | shape.push(hole); 271 | 272 | const index = holes.indexOf(hole); 273 | holes.splice(index, 1); 274 | } 275 | } 276 | 277 | shapes.push(new Shape(shape, true)); 278 | } 279 | } 280 | 281 | return shapes; 282 | } 283 | 284 | round() { 285 | return new Shape(this.paths.map(mapToRound), this.closed); 286 | } 287 | 288 | removeDuplicates() { 289 | return new Shape(this.paths.map(filterPathsDuplicates), this.closed); 290 | } 291 | 292 | mapToLower() { 293 | return this.paths.map(mapCapitalToLower); 294 | } 295 | } 296 | 297 | function mapCapitalToLower(path) { 298 | return path.map(vectorToLower); 299 | } 300 | 301 | function vectorToLower({ X, Y }) { 302 | return { x: X, y: Y }; 303 | } 304 | 305 | function mapLowerToCapital(path) { 306 | return path.map(vectorToCapital); 307 | } 308 | 309 | function vectorToCapital({ x, y }) { 310 | return { X: x, Y: y }; 311 | } 312 | 313 | function mapToRound(path) { 314 | return path.map(roundVector); 315 | } 316 | 317 | function roundVector({ X, Y }) { 318 | return { X: Math.round(X), Y: Math.round(Y) }; 319 | } 320 | 321 | function filterPathsDuplicates(path) { 322 | return path.filter(filterPathDuplicates); 323 | } 324 | 325 | function filterPathDuplicates(point, i, array) { 326 | if (i === 0) return true; 327 | 328 | const prevPoint = array[i - 1]; 329 | return !(point.X === prevPoint.X && point.Y === prevPoint.Y); 330 | } 331 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ClipperJS Unit Test 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | const test = require('tape'); 2 | const Shape = require('../lib/index.js').default; 3 | 4 | test('boolean operation: union', (assert) => { 5 | const shapeA = new Shape([rect(0, 0, 20, 20)], true, true); 6 | const shapeB = new Shape([rect(10, 10, 20, 20)], true, true); 7 | 8 | const actual = shapeA.union(shapeB); 9 | const expected = { closed: true, paths: [[{ X: 20, Y: 10 }, { X: 30, Y: 10 }, { X: 30, Y: 30 }, { X: 10, Y: 30 }, { X: 10, Y: 20 }, { X: 0, Y: 20 }, { X: 0, Y: 0 }, { X: 20, Y: 0 }]] }; 10 | 11 | assert.ok(actual.paths.length === 1, 'should generate one path'); 12 | assert.deepEqual(actual, expected, 'should generate one merged path'); 13 | 14 | assert.end(); 15 | }); 16 | 17 | test('boolean operation: intersect', (assert) => { 18 | const shapeA = new Shape([rect(0, 0, 20, 20)], true, true); 19 | const shapeB = new Shape([rect(10, 10, 20, 20)], true, true); 20 | 21 | const actual = shapeA.intersect(shapeB); 22 | const expected = { closed: true, paths: [[{ X: 20, Y: 20 }, { X: 10, Y: 20 }, { X: 10, Y: 10 }, { X: 20, Y: 10 }]] }; 23 | 24 | assert.ok(actual.paths.length === 1, 'should generate one path'); 25 | assert.deepEqual(actual, expected, 'should generate one path with only overlapping'); 26 | 27 | assert.end(); 28 | }); 29 | 30 | test('boolean operation: xor', (assert) => { 31 | const shapeA = new Shape([rect(0, 0, 20, 20)], true, true); 32 | const shapeB = new Shape([rect(10, 10, 20, 20)], true, true); 33 | 34 | const actual = shapeA.xor(shapeB); 35 | const expected = { closed: true, paths: [[{ X: 30, Y: 30 }, { X: 10, Y: 30 }, { X: 10, Y: 20 }, { X: 20, Y: 20 }, { X: 20, Y: 10 }, { X: 30, Y: 10 } ], [ { X: 20, Y: 10 }, { X: 10, Y: 10 }, { X: 10, Y: 20 }, { X: 0, Y: 20 }, { X: 0, Y: 0 }, { X: 20, Y: 0 }]] }; 36 | 37 | assert.ok(actual.paths.length === 2, 'should generate two paths'); 38 | assert.deepEqual(actual, expected, 'should generate two paths with overlapping removed'); 39 | 40 | assert.end(); 41 | }); 42 | 43 | test('boolean operation: difference', (assert) => { 44 | const shapeA = new Shape([rect(0, 0, 20, 20)], true, true); 45 | const shapeB = new Shape([rect(10, 10, 20, 20)], true, true); 46 | 47 | const actual = shapeA.difference(shapeB); 48 | const expected = { closed: true, paths: [[{ X: 20, Y: 10 }, { X: 10, Y: 10 }, { X: 10, Y: 20 }, { X: 0, Y: 20 }, { X: 0, Y: 0 }, { X: 20, Y: 0 }]] }; 49 | 50 | assert.ok(actual.paths.length === 1, 'should generate one path'); 51 | assert.deepEqual(actual, expected, 'should generate one paths where shapeB is removed'); 52 | 53 | assert.end(); 54 | }); 55 | 56 | test('convert from and to uppercase', (assert) => { 57 | const paths = [rect()]; 58 | const shape = new Shape(paths, true, true); 59 | const actualUpper = shape; 60 | const expectedUpper = { closed: true, paths: [[{ X: 0, Y: 0 }, { X: 10, Y: 0 }, { X: 10, Y: 10 }, { X: 0, Y: 10 }, { X: 0, Y: 0 }]]}; 61 | assert.deepEqual(actualUpper, expectedUpper, 'should generate uppercase path'); 62 | 63 | const actualLower = shape.mapToLower(); 64 | const expectedLower = [[{ x: 0, y: 0 }, { x: 10, y: 0 }, { x: 10, y: 10 }, { x: 0, y: 10 }, { x: 0, y: 0 }]]; 65 | assert.deepEqual(actualLower, expectedLower, 'should generate uppercase path'); 66 | 67 | assert.end(); 68 | }); 69 | 70 | // test('remove overlap', (assert) => { 71 | // const paths = [rect(10, 0, 10, 30), rect(0, 10, 30, 10)]; 72 | // const shape = new Shape(paths, true, true); 73 | // const actual = shape.removeOverlap(); 74 | // 75 | // const expected = { closed: true, paths: [[{ X: 20, Y: 10 }, { X: 30, Y: 10 }, { X: 30, Y: 20 }, { X: 20, Y: 20 }, { X: 20, Y: 30 }, { X: 10, Y: 30 }, { X: 10, Y: 20 }, { X: 0, Y: 20 }, { X: 0, Y: 10 }, { X: 10, Y: 10 }, { X: 10, Y: 0 }, { X: 20, Y: 0 }]] }; 76 | // 77 | // assert.ok(actual.paths.length === 1, 'should generate one path') 78 | // assert.deepEqual(actual, expected, 'should generate merged path'); 79 | // 80 | // assert.end(); 81 | // }); 82 | 83 | test('seperate shapes', (assert) => { 84 | const holeA = rect(10, 10, 10, 10).reverse(); 85 | const outlineA = rect(0, 0, 30, 30); 86 | const outlineB = rect(40, 0, 30, 30); 87 | const paths = [holeA, outlineA, outlineB]; 88 | 89 | const shape = new Shape(paths, true, true); 90 | const actual = shape.separateShapes(); 91 | 92 | const expected = [ 93 | { closed: true, paths: [[{ X: 0, Y: 0 }, { X: 30, Y: 0 }, { X: 30, Y: 30 }, { X: 0, Y: 30 }, { X: 0, Y: 0 }], [{ X: 10, Y: 10 }, { X: 10, Y: 20 }, { X: 20, Y: 20 }, { X: 20, Y: 10 }, { X: 10, Y: 10 }]] }, 94 | { closed: true, paths: [[{ X: 40, Y: 0 }, { X: 70, Y: 0 }, { X: 70, Y: 30 }, { X: 40, Y: 30 }, { X: 40, Y: 0 }]] } 95 | ]; 96 | 97 | assert.ok(actual.length === 2, 'should generate two shapes') 98 | assert.deepEqual(actual, expected, 'should generate two shapes'); 99 | 100 | assert.end(); 101 | }); 102 | 103 | test('orientation', (assert) => { 104 | const paths = [rect(), rect().reverse()]; 105 | const shape = new Shape(paths, true, true); 106 | 107 | const actualA = shape.orientation(0); 108 | const actualB = shape.orientation(1); 109 | 110 | assert.ok(actualA === true, 'orientation A should be clockwise'); 111 | assert.ok(actualB === false, 'orientation A should be counter-clockwise'); 112 | 113 | assert.end(); 114 | }); 115 | 116 | test('perimeter', (assert) => { 117 | const paths = [rect(), rect().reverse()]; 118 | const shape = new Shape(paths, true, true); 119 | 120 | const actualA = shape.perimeter(0); 121 | const expectedA = 40; // 4 * 10 122 | const actualB = shape.perimeters(); 123 | const expectedB = [40, 40]; 124 | const actualC = shape.totalPerimeter(); 125 | const expectedC = 80; 126 | 127 | assert.ok(actualA, 'single perimeter should produce the correct value'); 128 | assert.deepEqual(actualB, expectedB, 'perimeters should produce array of perimeters'); 129 | assert.ok(actualC === expectedC, 'totalPerimeter should produce the correct value'); 130 | 131 | assert.end(); 132 | }); 133 | 134 | function rect(x = 0, y = 0, w = 10, h = 10) { 135 | return [ 136 | { x, y }, 137 | { x: w + x, y }, 138 | { x: w + x, y: h + y }, 139 | { x, y: h + y }, 140 | { x, y } 141 | ]; 142 | } 143 | --------------------------------------------------------------------------------