├── .gitignore ├── demo └── readme-example.js ├── package.json ├── LICENSE.txt ├── README.md ├── test └── test.js └── 2d-polygon-boolean.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /demo/readme-example.js: -------------------------------------------------------------------------------- 1 | var polygonBoolean = require('../2d-polygon-boolean'); 2 | 3 | var subject = [ 4 | [0, 0], 5 | [100, 0], 6 | [100, 100], 7 | [0, 100] 8 | ]; 9 | 10 | var clip = [ 11 | [90, 90], 12 | [110, 90], 13 | [110, 110], 14 | [90, 110], 15 | [90, 90] 16 | ]; 17 | 18 | 19 | var union = polygonBoolean(subject, clip, 'or'); 20 | console.log('union results', union); 21 | 22 | var cut = polygonBoolean(subject, clip, 'not'); 23 | console.log('cut results', cut); 24 | 25 | var intersect = polygonBoolean(subject, clip, 'and'); 26 | console.log('intersect results', intersect); 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "2d-polygon-boolean", 3 | "version": "1.0.1", 4 | "description": "perform boolean operations on arbitrary polygons in 2d", 5 | "main": "2d-polygon-boolean.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "tape test/test.js | tap-spec" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/tmpvar/polygon.clip.js.git" 15 | }, 16 | "keywords": [ 17 | "polygon", 18 | "geometry", 19 | "math", 20 | "2d", 21 | "clipping" 22 | ], 23 | "author": "Elijah Insua ", 24 | "license": "MIT", 25 | "readmeFilename": "README.md", 26 | "dependencies": { 27 | "2d-polygon-area": "^1.0.0", 28 | "point-in-big-polygon": "~1.0.0", 29 | "segseg": "~0.2.0", 30 | "signum": "^1.0.0" 31 | }, 32 | "devDependencies": { 33 | "tape": "~3.0.3", 34 | "tap-spec": "~2.1.2" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright © 2014 Elijah Insua 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the “Software”), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 2d-polygon-boolean 2 | 3 | Implementation of the Greiner-Hormann "efficient clipping of arbitrary polygons" [paper](http://www.inf.usi.ch/hormann/papers/Greiner.1998.ECO.pdf) 4 | 5 | ## install 6 | 7 | `npm install 2d-polygon-boolean` 8 | 9 | ## use 10 | 11 | ### signature 12 | 13 | `var polygons = polygonBoolean(array1, array2, mode)` 14 | 15 | Where mode is the string `and` (intersect), `or` (union), `not` (cut) 16 | 17 | `polygons` is an array of arrays of arrays 18 | 19 | e.g 20 | ```javascript 21 | [ 22 | [ 23 | [0, 0], 24 | [0, 1], 25 | [1, 1] 26 | ] 27 | ] 28 | ``` 29 | ### example 30 | 31 | ```javascript 32 | 33 | var polygonBoolean = require('2d-polygon-boolean'); 34 | 35 | var subject = [ 36 | [0, 0], 37 | [100, 0], 38 | [100, 100], 39 | [0, 100] 40 | ]; 41 | 42 | var clip = [ 43 | [90, 90], 44 | [110, 90], 45 | [110, 110], 46 | [90, 110], 47 | [90, 90] 48 | ]; 49 | 50 | 51 | var union = polygonBoolean(subject, clip, 'or'); 52 | console.log('union results', union); 53 | 54 | /* 55 | union results [ [ [ 100, 90 ], 56 | [ 100, 0 ], 57 | [ 0, 0 ], 58 | [ 0, 100 ], 59 | [ 90, 100 ], 60 | [ 90, 110 ], 61 | [ 110, 110 ], 62 | [ 110, 90 ] ] ] 63 | */ 64 | 65 | var cut = polygonBoolean(subject, clip, 'not'); 66 | console.log('cut results', cut); 67 | 68 | /* 69 | cut results [ [ [ 100, 90 ], 70 | [ 100, 0 ], 71 | [ 0, 0 ], 72 | [ 0, 100 ], 73 | [ 90, 100 ], 74 | [ 90, 90 ] ] ] 75 | */ 76 | 77 | var intersect = polygonBoolean(subject, clip, 'and'); 78 | console.log('intersect results', intersect); 79 | 80 | /* 81 | intersect results [ [ [ 100, 90 ], [ 100, 100 ], [ 90, 100 ], [ 90, 90 ] ] ] 82 | */ 83 | ``` 84 | 85 | # license 86 | 87 | [MIT](LICENSE.txt) 88 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | var pbool = require('../2d-polygon-boolean'); 3 | 4 | var subject = [ 5 | [0, 0], 6 | [100, 0], 7 | [100, 100], 8 | [0, 100], 9 | [0, 0] 10 | ]; 11 | 12 | var clip = [ 13 | [90, 90], 14 | [110, 90], 15 | [110, 110], 16 | [90, 110], 17 | [90, 90] 18 | ]; 19 | 20 | var clip2 = [ 21 | [85, 95], 22 | [95, 95], 23 | [95, 105], 24 | [85, 105] 25 | ]; 26 | 27 | test('diff polys and return the remainder of the subject', function(t) { 28 | var difference = pbool(subject, clip, 'not')[0]; 29 | t.deepEqual(difference[0], [100, 90]); 30 | t.deepEqual(difference[1], [100, 0]); 31 | t.deepEqual(difference[2], [0, 0]); 32 | t.deepEqual(difference[3], [0, 100]); 33 | t.deepEqual(difference[4], [90, 100]); 34 | t.deepEqual(difference[5], [90, 90]); 35 | t.end(); 36 | }); 37 | 38 | test('return the intersection of `subject` and `clip`', function(t) { 39 | var intersection = pbool(subject, clip, 'and')[0]; 40 | t.deepEqual(intersection[0], [100, 90]); 41 | t.deepEqual(intersection[1], [100, 100]); 42 | t.deepEqual(intersection[2], [90, 100]); 43 | t.deepEqual(intersection[3], [90, 90]); 44 | t.end(); 45 | }); 46 | 47 | test('return the intersection of `subject` and `clip2`', function(t) { 48 | var intersection = pbool(subject, clip2, 'and')[0]; 49 | 50 | t.deepEqual(intersection[0], [95, 100]); 51 | t.deepEqual(intersection[1], [85, 100]); 52 | t.deepEqual(intersection[2], [85, 95]); 53 | t.deepEqual(intersection[3], [95, 95]); 54 | t.end(); 55 | }); 56 | 57 | test('return the intersection of `clip` and `clip2`', function(t) { 58 | var union3 = pbool(clip, clip2, 'and')[0]; 59 | t.deepEqual(union3[0], [90, 105]); 60 | t.deepEqual(union3[1], [90, 95]); 61 | t.deepEqual(union3[2], [95, 95]); 62 | t.deepEqual(union3[3], [95, 105]); 63 | t.end(); 64 | }); 65 | 66 | test('reuse polygons', function(t) { 67 | var intersection = pbool(subject, clip, 'and')[0]; 68 | var intersection2 = pbool(intersection, clip2, 'and')[0]; 69 | t.deepEqual(intersection2[0], [95, 100]); 70 | t.deepEqual(intersection2[1], [90, 100]); 71 | t.deepEqual(intersection2[2], [90, 95]); 72 | t.deepEqual(intersection2[3], [95, 95]); 73 | t.end(); 74 | }); 75 | 76 | test('union polygons', function(t) { 77 | var union = pbool(subject, clip, 'or')[0]; 78 | 79 | t.deepEqual(union, [ 80 | [100, 90], 81 | [100, 0], 82 | [0, 0], 83 | [0, 100], 84 | [90, 100], 85 | [90, 110], 86 | [110, 110], 87 | [110, 90] 88 | ]); 89 | 90 | t.end(); 91 | }); 92 | 93 | test('return multiple polygons (intersect)', function(t) { 94 | var a = [ 95 | [ 0, 0], 96 | [60, 0], 97 | [60, 30], 98 | [40, 30], 99 | [40, 10], 100 | [20, 10], 101 | [20, 30], 102 | [0, 30] 103 | ]; 104 | 105 | var b = [ 106 | [-10, 15], 107 | [70, 15], 108 | [70, 25], 109 | [-20, 25] 110 | ]; 111 | 112 | var i = pbool(a, b, 'and'); 113 | 114 | t.equal(i.length, 2); 115 | 116 | t.deepEqual(i[0], [ 117 | [60, 15], 118 | [60, 25], 119 | [40, 25], 120 | [40, 15] 121 | ]); 122 | 123 | t.deepEqual(i[1], [ 124 | [20, 15], 125 | [20, 25], 126 | [0, 25], 127 | [0, 15] 128 | ]); 129 | 130 | t.end(); 131 | }); 132 | 133 | test('union polygons (colinear side)', function(t) { 134 | var union = pbool([ 135 | [0, 0], 136 | [1, 0], 137 | [1, 1], 138 | [0, 1] 139 | ], [ 140 | [1, 0], 141 | [2, 0], 142 | [2, 1], 143 | [1, 1] 144 | ], 'or')[0]; 145 | 146 | t.deepEqual(union, [ 147 | [0, 0] 148 | [2, 0] 149 | [2, 1] 150 | [0, 1] 151 | ]); 152 | 153 | t.end(); 154 | }); 155 | 156 | test('containment - union - subject container', function(t) { 157 | var a = [[0, 0], [100, 0], [100, 100], [0, 100]]; 158 | var b = [[10, 10], [20, 10], [20, 20], [10, 20]]; 159 | 160 | var union = pbool(a, b, 'or') 161 | t.deepEqual(union[0], a, 'just return the outside'); 162 | t.ok(a[0] !== union[0][0], 'creates a copy') 163 | 164 | t.end(); 165 | }); 166 | 167 | test('containment - union - clip container', function(t) { 168 | var a = [[0, 0], [100, 0], [100, 100], [0, 100]]; 169 | var b = [[10, 10], [20, 10], [20, 20], [10, 20]]; 170 | 171 | var union = pbool(b, a, 'or') 172 | t.deepEqual(union[0], a, 'just return the outside'); 173 | t.ok(a[0] !== union[0][0], 'creates a copy') 174 | 175 | t.end(); 176 | }); 177 | 178 | test('no-intersection - union', function(t) { 179 | var a = [[0, 0], [5, 0], [5, 5], [0, 5]]; 180 | var b = [[20, 10], [20, 10], [20, 20], [10, 20]]; 181 | 182 | var union = pbool(a, b, 'or') 183 | t.deepEqual(union, [a, b], 'return both'); 184 | t.ok(a[0] !== union[0][0], 'creates a copy') 185 | t.ok(b[0] !== union[1][0], 'creates a copy') 186 | 187 | t.end(); 188 | }); 189 | 190 | test('containment - and - subject container', function(t) { 191 | var a = [[0, 0], [100, 0], [100, 100], [0, 100]]; 192 | var b = [[10, 10], [20, 10], [20, 20], [10, 20]]; 193 | 194 | var and = pbool(a, b, 'and') 195 | t.deepEqual(b, and[0], 'just return the inside'); 196 | t.ok(b[0] !== and[0][0], 'creates a copy') 197 | 198 | t.end(); 199 | }); 200 | 201 | test('containment - and - clip container', function(t) { 202 | var a = [[0, 0], [100, 0], [100, 100], [0, 100]]; 203 | var b = [[10, 10], [20, 10], [20, 20], [10, 20]]; 204 | 205 | var and = pbool(b, a, 'and') 206 | t.deepEqual(b, and[0], 'just return the inside'); 207 | t.ok(b[0] !== and[0][0], 'creates a copy') 208 | 209 | t.end(); 210 | }); 211 | 212 | test('containment - not - subject container', function(t) { 213 | var a = [[0, 0], [100, 0], [100, 100], [0, 100]]; 214 | var b = [[10, 10], [20, 10], [20, 20], [10, 20]]; 215 | 216 | var not = pbool(a, b, 'not') 217 | t.deepEqual([a, b.reverse()], not, 'return both'); 218 | t.ok(b[0] !== not[1][0], 'creates a copy') 219 | 220 | t.end(); 221 | }); 222 | 223 | test('containment - not - subject container', function(t) { 224 | var a = [[0, 0], [100, 0], [100, 100], [0, 100]]; 225 | var b = [[10, 10], [20, 10], [20, 20], [10, 20]]; 226 | 227 | var not = pbool(b, a, 'not') 228 | t.deepEqual([a, b.reverse()], not, 'return both'); 229 | t.ok(b[0] !== not[1][0], 'creates a copy') 230 | 231 | t.end(); 232 | }); 233 | -------------------------------------------------------------------------------- /2d-polygon-boolean.js: -------------------------------------------------------------------------------- 1 | // Implementation of the Greiner-Hormann polygon clipping algorithm 2 | // 3 | 4 | var segseg = require('segseg'); 5 | var preprocessPolygon = require("point-in-big-polygon"); 6 | var area = require('2d-polygon-area'); 7 | var sign = require('signum'); 8 | var abs = Math.abs; 9 | 10 | function copy(a) { 11 | var l = a.length; 12 | var out = new Array(l); 13 | for (var i = 0; i 0; 180 | if (type === 'or') { 181 | ce = !ce; 182 | } 183 | 184 | for(clip = clipList; clip.next; clip = clip.next) { 185 | if(clip.intersect) { 186 | clip.entry = ce; 187 | ce = !ce; 188 | } 189 | } 190 | }; 191 | 192 | function collectClipResults(subjectList, clipList) { 193 | subjectList.createLoop(); 194 | clipList.createLoop(); 195 | 196 | var crt, results = [], result; 197 | 198 | while ((crt = subjectList.firstNodeOfInterest()) !== subjectList) { 199 | result = []; 200 | for (; !crt.visited; crt = crt.neighbor) { 201 | 202 | result.push(crt.vec); 203 | var forward = crt.entry 204 | while(true) { 205 | crt.visited = true; 206 | crt = forward ? crt.next : crt.prev; 207 | 208 | if(crt.intersect) { 209 | crt.visited = true; 210 | break; 211 | } else { 212 | result.push(crt.vec); 213 | } 214 | } 215 | } 216 | 217 | results.push(clean(result)); 218 | } 219 | 220 | return results; 221 | }; 222 | 223 | function polygonBoolean(subjectPoly, clipPoly, operation) { 224 | 225 | var subjectList = createLinkedList(subjectPoly); 226 | var clipList = createLinkedList(clipPoly); 227 | var clipContains = preprocessPolygon([clipPoly]); 228 | var subjectContains = preprocessPolygon([subjectPoly]); 229 | 230 | var subject, clip, res; 231 | 232 | // Phase 1: Identify and store intersections between the subject 233 | // and clip polygons 234 | var isects = identifyIntersections(subjectList, clipList); 235 | 236 | if (isects) { 237 | // Phase 2: walk the resulting linked list and mark each intersection 238 | // as entering or exiting 239 | identifyIntersectionType( 240 | subjectList, 241 | clipList, 242 | clipContains, 243 | subjectContains, 244 | operation 245 | ); 246 | 247 | // Phase 3: collect resulting polygons 248 | res = collectClipResults(subjectList, clipList); 249 | } else { 250 | // No intersections 251 | 252 | var inner = clipContains(subjectPoly[0]) < 0; 253 | var outer = subjectContains(clipPoly[0]) < 0; 254 | 255 | // TODO: slice will not copy the vecs 256 | 257 | res = []; 258 | switch (operation) { 259 | case 'or': 260 | if (!inner && !outer) { 261 | res.push(copy(subjectPoly)); 262 | res.push(copy(clipPoly)); 263 | } else if (inner) { 264 | res.push(copy(clipPoly)); 265 | } else if (outer) { 266 | res.push(copy(subjectPoly)); 267 | } 268 | break; 269 | 270 | case 'and': 271 | if (inner) { 272 | res.push(copy(subjectPoly)) 273 | } else if (outer) { 274 | res.push(copy(clipPoly)); 275 | } else { 276 | throw new Error('woops') 277 | } 278 | break; 279 | 280 | case 'not': 281 | var sclone = copy(subjectPoly); 282 | var cclone = copy(clipPoly); 283 | 284 | var sarea = area(sclone); 285 | var carea = area(cclone); 286 | if (sign(sarea) === sign(carea)) { 287 | if (outer) { 288 | cclone.reverse(); 289 | } else if (inner) { 290 | sclone.reverse(); 291 | } 292 | } 293 | 294 | res.push(sclone); 295 | 296 | if (abs(sarea) > abs(carea)) { 297 | res.push(cclone); 298 | } else { 299 | res.unshift(cclone); 300 | } 301 | 302 | break 303 | } 304 | } 305 | 306 | return res; 307 | }; 308 | 309 | module.exports = polygonBoolean; 310 | --------------------------------------------------------------------------------