From f5126cbeea567779b52ee48cca46bf793fbbefd0 Mon Sep 17 00:00:00 2001 From: Sean Connelly Date: Thu, 16 Jun 2016 16:16:00 -0500 Subject: [PATCH] better API design plus images --- README.md | 245 +++++++++++++++------- dist/demo.html | 39 ++-- dist/polybool.js | 437 +++++++++++++++++++--------------------- dist/polybool.min.js | 2 +- example.png | Bin 0 -> 3944 bytes flowchart.png | Bin 0 -> 8286 bytes index.js | 218 +++++++++----------- lib/intersecter.js | 6 +- lib/segment-selector.js | 213 ++++++++++---------- 9 files changed, 601 insertions(+), 559 deletions(-) create mode 100644 example.png create mode 100644 flowchart.png diff --git a/README.md b/README.md index 64ce9b4..e803dcf 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,10 @@ Boolean operations on polygons (union, intersection, difference, xor) 1. Clips polygons for all boolean operations 2. Removes unnecessary vertices -3. Handles segments that are coincident (overlap perfectly, share vertices, one inside the other, etc) +3. Handles segments that are coincident (overlap perfectly, share vertices, one inside the other, + etc) 4. Uses formulas that take floating point irregularities into account (via configurable epsilon) +5. Proides an API for constructing efficient sequences of operations # Demo @@ -25,111 +27,184 @@ Based somewhat on the F. Martinez (2008) algorithm: `npm install polybooljs` -Or, for the browser, look in the [`dist/`](https://github.com/voidqk/polybooljs/tree/master/dist) directory for a single file build. When included on a page, it will expose the global `PolyBool`. +Or, for the browser, look in the [`dist/`](https://github.com/voidqk/polybooljs/tree/master/dist) +directory for a single file build. When included on a page, it will expose the global `PolyBool`. -# Crash Course - -## Example +# Example ```javascript var PolyBool = require('polybooljs'); -PolyBool.union( - [ [[100,100],[200,200],[300,100]], [[300,100],[300,200],[400,100]] ], - false, - [ [[50,50],[200,50],[300,200]] ], - false -); -==> { +PolyBool.intersect({ + regions: [ + [[50,50], [150,150], [190,50]], + [[130,50], [290,150], [290,50]] + ], + inverted: false + }, { + regions: [ + [[110,20], [110,110], [20,20]], + [[130,170], [130,20], [260,20], [260,170]] + ], + inverted: false + }); +===> { regions: [ - [[400,100],[300,100],[300,200], - [260,140],[300,100],[233.33333333333331,100], - [200,50],[50,50],[133.33333333333331,100], - [100,100],[200,200],[237.5,162.5],[300,200]] + [[50,50], [110,50], [110,110]], + [[178,80], [130,50], [130,130], [150,150]], + [[178,80], [190,50], [260,50], [260,131.25]] ], inverted: false } ``` -## Basic functions +![PolyBool Example](https://github.com/voidqk/polybooljs/raw/master/example.png) + +## Basic Usage ```javascript -PolyBool.union( // poly1 || poly2 - regions1, inverted1, // <-- polygon 1 - regions2, inverted2, // <-- polygon 2 - [epsilon, [buildLog]] // <-- optional -); -PolyBool.intersect (...same...); // poly1 && poly2 -PolyBool.difference (...same...); // poly1 - poly2 -PolyBool.differenceRev(...same...); // poly2 - poly1 -PolyBool.xor (...same...); // poly1 ^ poly2 +PolyBool.union (poly1, poly2); // poly1 || poly2 +PolyBool.intersect (poly1, poly2); // poly1 && poly2 +PolyBool.difference (poly1, poly2); // poly1 - poly2 +PolyBool.differenceRev(poly1, poly2); // poly2 - poly1 +PolyBool.xor (poly1, poly2); // poly1 != poly2 ``` -Where `regionsX` is a list regions for the polygon: +Where `poly1`, `poly2`, and the return value are Polygon objects, in the format of: ```javascript -[ - [ [10, 10], [20, 20], [30, 10] ], // <- a single region - [ [20, 10], [30, 20], [40, 10] ] -] +// polygon format +{ + regions: [ // list of regions + // each region is a list of points + [[50,50], [150,150], [190,50]], + [[130,50], [290,150], [290,50]] + ], + inverted: false // is this polygon inverted? +} ``` -A single region is a list of points in `[x, y]` format. +# Core API + +```javascript +var segments = PolyBool.segments(polygon); +var combined = PolyBool.combine(segments1, segments2); +var segments = PolyBool.selectUnion(combined); +var segments = PolyBool.selectIntersect(combined); +var segments = PolyBool.selectDifference(combined); +var segments = PolyBool.selectDifferenceRev(combined); +var segments = PolyBool.selectXor(combined); +var polygon = PolyBool.polygon(segments); +``` + +Depending on your needs, it might be more efficient to construct your own sequence of operations +using the lower-level API. Note that `PolyBool.union`, `PolyBool.intersect`, etc, are just thin +wrappers for convenience. + +There are three types of objects you will encounter in the core API: + +1. Polygons (discussed above, this is a list of regions and an `inverted` flag) +2. Segments +3. Combined Segments -And `invertedX` is a bool indicating whether that polygon is inverted or not. +The basic flow chart of the API is: -The parameters `epsilon` and `buildLog` are explained below, but can safely be ignored for most uses. +![PolyBool API Flow Chart](https://github.com/voidqk/polybooljs/raw/master/flowchart.png) -Returns an object: +You start by converting Polygons to Segments using `PolyBool.segments(poly)`. + +You convert Segments to Combined Segments using `PolyBool.combine(seg1, seg2)`. + +You select the resulting Segments from the Combined Segments using one of the selection operators +`PolyBool.selectUnion(combined)`, `PolyBool.selectIntersect(combined)`, etc. These selection +functions return Segments. + +Once you're done, you convert the Segments back to Polygons using `PolyBool.polygon(segments)`. + +Each transition is costly, so you want to navigate wisely. The selection transition is the least +costly. + +## Advanced Example 1 + +Suppose you wanted to union a list of polygons together. The naive way to do it would be: ```javascript -{ - regions: , - inverted: +// works but not efficient +var result = PolyBool.union(polygons[0], polygons[1]); +for (var i = 2; i < polygons.length; i++) + result = PolyBool.union(result, polygons[i]); +return result; +``` + +Instead, it's more efficient to use the core API directly, like this: + +```javascript +// works AND efficient +var segments = PolyBool.segments(polygons[0]); +for (var i = 1; i < polygons.length; i++){ + var seg2 = PolyBool.segments(polygons[i]); + var comb = PolyBool.combine(segments, seg2); + segments = PolyBool.selectUnion(comb); } +return PolyBool.polygon(segments); ``` -# Computing Multiple Results +## Advanced Example 2 -The algorithm produces enough information to calculate all the operations. If you want multiple operations performed, it is much more efficient to request all of them at once, via: +Suppose you want to calculate all operations on two polygons. The naive way to do it would be: ```javascript -PolyBool.calculate( - regions1, inverted1, // <--- polygon 1, like before - regions2, inverted2, // <--- polygon 2, like before - operations, // <--- list of operations to compute - [epsilon, [buildLog]] // <--- optional -); +// works but not efficient +return { + union: PolyBool.union (poly1, poly2), + intersect: PolyBool.intersect (poly1, poly2), + difference: PolyBool.difference (poly1, poly2), + differenceRev: PolyBool.differenceRev(poly1, poly2), + xor: PolyBool.xor (poly1, poly2) +}; ``` -Where `regionsX`/`invertedX` are the same as before, and `operations` is a list of strings that are the operations to perform. +Instead, it's more efficient to use the core API directly, like this: -To calculate all operations, pass the following as the `operations` parameter: +```javascript +// works AND efficient +var seg1 = PolyBool.segments(poly1); +var seg2 = PolyBool.segments(poly2); +var comb = PolyBool.combine(seg1, seg2); +return { + union : PolyBool.polygon(PolyBool.selectUnion (comb)), + intersect : PolyBool.polygon(PolyBool.selectIntersect (comb)), + difference : PolyBool.polygon(PolyBool.selectDifference (comb)), + differenceRev: PolyBool.polygon(PolyBool.selectDifferenceRev(comb)), + xor : PolyBool.polygon(PolyBool.selectXor (comb)) +}; +``` -`[ 'union', 'intersect', 'difference', 'differenceRev', 'xor' ]` +## Advanced Example 3 -If you want less operations, just remove them from the list. Order doesn't matter. +As an added bonus, just going from Polygon to Segments and back performs simplification on the +polygon. -The function returns an object with the result of each requested operation as the key: +Suppose you have garbage polygon data and just want to clean it up. The naive way to do it would +be: ```javascript -{ - union: { // <-- only exists if 'union' was passed in operations list - regions: , - inverted: - }, - intersect: { // <-- only exists if 'intersect' was in operations... etc - regions: , - inverted: <...etc...> - }, - ...etc... -} +// union the polygon with nothing in order to clean up the data +// works but not efficient +var cleaned = PolyBool.union(polygon, { regions: [], inverted: false }); ``` -# Epsilon +Instead, skip the combination and selection phase: -Due to the beauty of floating point reality, floating point calculations are not exactly perfect. This is a problem when trying to detect whether lines are on top of each other, or if vertices are exactly the same. +```javascript +// works AND efficient +var cleaned = PolyBool.polygon(PolyBool.segments(polygon)); +``` + +# Epsilon -The `epsilon` value in the API function calls allows you to set the boundary for considering values equal. It is a number, and the default is `0.0000000001`. +Due to the beauty of floating point reality, floating point calculations are not exactly perfect. +This is a problem when trying to detect whether lines are on top of each other, or if vertices are +exactly the same. Normally you would expect this to work: @@ -149,30 +224,50 @@ else /* A and B are not equal */; ``` -This not-quite-equal problem is a bit annoying. +You can set the epsilon value using: -Fortunately, `PolyBool` has already figured out (or stolen) the required formulas, so all you need to do is provide an `epsilon` value, and everything will (read: should) work. +`PolyBool.epsilon(newEpsilonValue);` -If your polygons are really really large or really really tiny, then you will probably have to come up with your own epsilon value. +Or, if you just want to get the current value: -If `PolyBool` detects that your epsilon is too small, it will throw an error to try and help you. +`var currentEpsilon = PolyBool.epsilon();` + +The default epsilon value is `0.0000000001`. + +If your polygons are really really large or really really tiny, then you will probably have to come +up with your own epsilon value -- otherwise, the default should be fine. + +If `PolyBool` detects that your epsilon is too small or too large, it will throw an error: + +``` +PolyBool: Zero-length segment detected; your epsilon is probably too small or too large +``` # Build Log -The optional `buildLog` parameter is used strictly for debugging and creating the animation in the demo. +The library also has an option for tracking execution of the internal algorithms. This is useful +for debugging or creating the animation on the demo page. -It simply logs the processing of the algorithm, so it can be inspected and played back. +By default, the logging is disabled. But you can enable or reset it via: -If you want a build log for some reason, you can create one via: +`var buildLog = PolyBool.buildLog(true);` -`var buildLog = PolyBool.BuildLog();` +The return value is an empty list that will have log entries added to it as more API calls are made. -You can inspect the log by looking in the values inside of `buildLog.list`: +You can inspect the log by looking in the values: ```javascript -buildLog.list.forEach(function(logEntry){ +buildLog.forEach(function(logEntry){ console.log(logEntry.type, logEntry.data); }); ``` Don't rely on the build log functionality to be consistent across releases. + +You can disable the build log via: + +`PolyBool.buildLog(false);` + +You can get the current list (or `false` if disabled) via: + +`var currentLog = PolyBool.buildLog();` diff --git a/dist/demo.html b/dist/demo.html index 14de750..26dc9be 100644 --- a/dist/demo.html +++ b/dist/demo.html @@ -353,27 +353,12 @@ 'Blue - Red': PolyBool.differenceRev, 'Xor' : PolyBool.xor })[mode]; - var BL = PolyBool.BuildLog(); - try{ - clipResult = { - result: func( - poly1.regions, poly1.inverted, - poly2.regions, poly2.inverted, - false, BL - ), - build_log: BL.list - }; - } - catch (e){ - clipResult = { - result: { regions: [], inverted: false }, - build_log: BL.list - }; - throw e; - } - finally { - redraw(); - } + var BL = PolyBool.buildLog(true); + clipResult = { + result: func(poly1, poly2), + build_log: BL + }; + redraw(); } function drawRegions(regions, offset){ @@ -453,6 +438,18 @@ bl_vert = bl.x; break; case 'new_seg': + for (var i = 0; i < bl_segs.length; i++){ + if (bl_segs[i].id === bl.seg.id){ + bl_segs.splice(i, 1); + break; + } + } + for (var i = 0; i < bl_oldsegs.length; i++){ + if (bl_oldsegs[i].id === bl.seg.id){ + bl_oldsegs.splice(i, 1); + break; + } + } bl_segs.push(bl.seg); bl_segid[bl.seg.id] = { phase: bl_phase, diff --git a/dist/polybool.js b/dist/polybool.js index eb56695..f7f6771 100644 --- a/dist/polybool.js +++ b/dist/polybool.js @@ -5,150 +5,112 @@ * @preserve Project Home: https://github.com/voidqk/polybooljs */ +var BuildLog = require('./lib/build-log'); var Epsilon = require('./lib/epsilon'); var Intersecter = require('./lib/intersecter'); var SegmentChainer = require('./lib/segment-chainer'); var SegmentSelector = require('./lib/segment-selector'); -function calc(regions1, inverted1, regions2, inverted2, operations, epsilon, buildLog){ - // main algorithm - // - // this function is exposed as PolyBool.calculate(...) - // - // or you can use the single-use functions instead, PolyBool.union(...), etc -- see exported API - // below in the PolyBool object - // - // each regionsX is a list of regions - // [ region1, region2, region3, ... ] - // each region is a list of points - // [ [x1, y1], [x2, y2], [x3, y3], ... ] - // - // invertedX is a bool that says whether that region is inverted or not - // - // operations is a list of strings of the operations you want calculated... - // to calculate everything, pass: - // [ 'union', 'intersect', 'difference', 'reverseDifference', 'xor' ] - // or just remove the strings you don't want to waste time calculating - // - // epsilon is optional, but would specify what tolerance to use when performing calculations, - // and should be a PolyBool.Epsilon object... - // i.e., PolyBool.Epsilon(0.0001) says that `value` is zero when `Math.abs(value) < 0.0001` - // - // buildLog is optional, but would specify a PolyBool.BuildLog object that keeps track of the - // processing of the different algorithms... only useful for inspection/debugging, or creating a - // pretty animation :-) - // - // this function returns an object with the keys set to the result of the specified operation: - // { - // union: { <-- only exists if 'union' was in operations list - // regions: [ region1, etc...], list of regions in the result, - // inverted: true/false bool whether the result polygon is inverted - // }, - // intersect: { <-- only exists if 'intersect' was in operations list, etc - // same as union, etc... - // }, - // other operations... - // } - - if (operations.length <= 0) - return {}; - var inverted = { - union: inverted1 || inverted2, - intersect: inverted1 && inverted2, - difference: inverted1 && !inverted2, - differenceRev: !inverted1 && inverted2, - xor: inverted1 !== inverted2 - }; - operations.forEach(function(op){ - if (!inverted.hasOwnProperty(op)) - throw new Error('invalid PolyBool operation: ' + op); - }); - - if (typeof epsilon === 'number') - epsilon = Epsilon(epsilon); - else - epsilon = Epsilon(); - - var i1 = Intersecter(true, epsilon, buildLog); - regions1.forEach(i1.addRegion); - var s1 = i1.calculate(inverted1); - - var i2 = Intersecter(true, epsilon, buildLog); - regions2.forEach(i2.addRegion); - var s2 = i2.calculate(inverted2); - - if (buildLog) - buildLog.reset(); - - var i3 = Intersecter(false, epsilon, buildLog); - var s3 = i3.calculate(s1, inverted1, s2, inverted2); +var buildLog = false; +var epsilon = Epsilon(); - var result = {}; - operations.forEach(function(op){ - var s4 = SegmentSelector[op](s3, buildLog); +var PolyBool = { + // getter/setter for buildLog + buildLog: function(bl){ + if (bl === true) + buildLog = BuildLog(); + else if (bl === false) + buildLog = false; + return buildLog === false ? false : buildLog.list; + }, + // getter/setter for epsilon + epsilon: function(v){ + return epsilon.epsilon(v); + }, - result[op] = { - regions: SegmentChainer(s4, epsilon, buildLog), - inverted: inverted[op] + // core API + segments: function(poly){ + var i = Intersecter(true, epsilon, buildLog); + poly.regions.forEach(i.addRegion); + return { + segments: i.calculate(poly.inverted), + inverted: poly.inverted }; - }); - - return result; -} - -var PolyBool = { - BuildLog: require('./lib/build-log'), - Epsilon: Epsilon, - Intersecter: Intersecter, - SegmentChainer: SegmentChainer, - SegmentSelector: SegmentSelector, - - // basic operations for common cases - union: function(regions1, inverted1, regions2, inverted2, epsilon, buildLog){ - return calc( - regions1, inverted1, - regions2, inverted2, - ['union'], - epsilon, buildLog - ).union; }, - intersect: function(regions1, inverted1, regions2, inverted2, epsilon, buildLog){ - return calc( - regions1, inverted1, - regions2, inverted2, - ['intersect'], - epsilon, buildLog - ).intersect; + combine: function(segments1, segments2){ + var i3 = Intersecter(false, epsilon, buildLog); + return { + combined: i3.calculate( + segments1.segments, segments1.inverted, + segments2.segments, segments2.inverted + ), + inverted1: segments1.inverted, + inverted2: segments2.inverted + }; }, - difference: function(regions1, inverted1, regions2, inverted2, epsilon, buildLog){ - return calc( - regions1, inverted1, - regions2, inverted2, - ['difference'], - epsilon, buildLog - ).difference; + selectUnion: function(combined){ + return { + segments: SegmentSelector.union(combined.combined, buildLog), + inverted: combined.inverted1 || combined.inverted2 + } + }, + selectIntersect: function(combined){ + return { + segments: SegmentSelector.intersect(combined.combined, buildLog), + inverted: combined.inverted1 && combined.inverted2 + } + }, + selectDifference: function(combined){ + return { + segments: SegmentSelector.difference(combined.combined, buildLog), + inverted: combined.inverted1 && !combined.inverted2 + } }, - differenceRev: function(regions1, inverted1, regions2, inverted2, epsilon, buildLog){ - return calc( - regions1, inverted1, - regions2, inverted2, - ['differenceRev'], - epsilon, buildLog - ).differenceRev; + selectDifferenceRev: function(combined){ + return { + segments: SegmentSelector.differenceRev(combined.combined, buildLog), + inverted: !combined.inverted1 && combined.inverted2 + } }, - xor: function(regions1, inverted1, regions2, inverted2, epsilon, buildLog){ - return calc( - regions1, inverted1, - regions2, inverted2, - ['xor'], - epsilon, buildLog - ).xor; + selectXor: function(combined){ + return { + segments: SegmentSelector.xor(combined.combined, buildLog), + inverted: combined.inverted1 !== combined.inverted2 + } + }, + polygon: function(segments){ + return { + regions: SegmentChainer(segments.segments, epsilon, buildLog), + inverted: segments.inverted + }; }, - // or, slightly more beefy, just expose calc directly: - calculate: calc + // helper functions for common operations + union: function(poly1, poly2){ + return operate(poly1, poly2, PolyBool.selectUnion); + }, + intersect: function(poly1, poly2){ + return operate(poly1, poly2, PolyBool.selectIntersect); + }, + difference: function(poly1, poly2){ + return operate(poly1, poly2, PolyBool.selectDifference); + }, + differenceRev: function(poly1, poly2){ + return operate(poly1, poly2, PolyBool.selectDifferenceRev); + }, + xor: function(poly1, poly2){ + return operate(poly1, poly2, PolyBool.selectXor); + } }; +function operate(poly1, poly2, selector){ + var seg1 = PolyBool.segments(poly1); + var seg2 = PolyBool.segments(poly2); + var comb = PolyBool.combine(seg1, seg2); + var seg3 = selector(comb); + return PolyBool.polygon(seg3); +} + if (typeof window === 'object') window.PolyBool = PolyBool; @@ -832,8 +794,10 @@ function Intersecter(selfIntersection, eps, buildLog){ else{ var st = ev.status; - if (st === null) - throw new Error('PolyBool: Zero-length segment detected; your epsilon is probably too small'); + if (st === null){ + throw new Error('PolyBool: Zero-length segment detected; your epsilon is ' + + 'probably too small or too large'); + } // removing the status will create two new adjacent edges, so we'll need to check // for those @@ -1266,12 +1230,23 @@ function select(segments, selection, buildLog){ var result = []; segments.forEach(function(seg){ var index = - (seg.myFill .above ? 8 : 0) + - (seg.myFill .below ? 4 : 0) + + (seg.myFill.above ? 8 : 0) + + (seg.myFill.below ? 4 : 0) + ((seg.otherFill && seg.otherFill.above) ? 2 : 0) + ((seg.otherFill && seg.otherFill.below) ? 1 : 0); - if (selection[index]) - result.push(seg); + if (selection[index] !== 0){ + // copy the segment to the results, while also calculating the fill status + result.push({ + id: buildLog ? buildLog.segmentId() : -1, + start: seg.start, + end: seg.end, + myFill: { + above: selection[index] === 1, // 1 if filled above + below: selection[index] === 2 // 2 if filled below + }, + otherFill: null + }); + } }); if (buildLog) @@ -1282,128 +1257,128 @@ function select(segments, selection, buildLog){ var SegmentSelector = { union: function(segments, buildLog){ // primary | secondary - // above1 below1 above2 below2 Keep? - // 0 0 0 0 => 0 - // 0 0 0 1 => 1 - // 0 0 1 0 => 1 - // 0 0 1 1 => 0 - // 0 1 0 0 => 1 - // 0 1 0 1 => 1 - // 0 1 1 0 => 0 - // 0 1 1 1 => 0 - // 1 0 0 0 => 1 - // 1 0 0 1 => 0 - // 1 0 1 0 => 1 - // 1 0 1 1 => 0 - // 1 1 0 0 => 0 - // 1 1 0 1 => 0 - // 1 1 1 0 => 0 - // 1 1 1 1 => 0 + // above1 below1 above2 below2 Keep? Value + // 0 0 0 0 => no 0 + // 0 0 0 1 => yes filled below 2 + // 0 0 1 0 => yes filled above 1 + // 0 0 1 1 => no 0 + // 0 1 0 0 => yes filled below 2 + // 0 1 0 1 => yes filled below 2 + // 0 1 1 0 => no 0 + // 0 1 1 1 => no 0 + // 1 0 0 0 => yes filled above 1 + // 1 0 0 1 => no 0 + // 1 0 1 0 => yes filled above 1 + // 1 0 1 1 => no 0 + // 1 1 0 0 => no 0 + // 1 1 0 1 => no 0 + // 1 1 1 0 => no 0 + // 1 1 1 1 => no 0 return select(segments, [ - 0, 1, 1, 0, - 1, 1, 0, 0, + 0, 2, 1, 0, + 2, 2, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0 ], buildLog); }, intersect: function(segments, buildLog){ // primary & secondary - // above1 below1 above2 below2 Keep? - // 0 0 0 0 => 0 - // 0 0 0 1 => 0 - // 0 0 1 0 => 0 - // 0 0 1 1 => 0 - // 0 1 0 0 => 0 - // 0 1 0 1 => 1 - // 0 1 1 0 => 0 - // 0 1 1 1 => 1 - // 1 0 0 0 => 0 - // 1 0 0 1 => 0 - // 1 0 1 0 => 1 - // 1 0 1 1 => 1 - // 1 1 0 0 => 0 - // 1 1 0 1 => 1 - // 1 1 1 0 => 1 - // 1 1 1 1 => 0 + // above1 below1 above2 below2 Keep? Value + // 0 0 0 0 => no 0 + // 0 0 0 1 => no 0 + // 0 0 1 0 => no 0 + // 0 0 1 1 => no 0 + // 0 1 0 0 => no 0 + // 0 1 0 1 => yes filled below 2 + // 0 1 1 0 => no 0 + // 0 1 1 1 => yes filled below 2 + // 1 0 0 0 => no 0 + // 1 0 0 1 => no 0 + // 1 0 1 0 => yes filled above 1 + // 1 0 1 1 => yes filled above 1 + // 1 1 0 0 => no 0 + // 1 1 0 1 => yes filled below 2 + // 1 1 1 0 => yes filled above 1 + // 1 1 1 1 => no 0 return select(segments, [ 0, 0, 0, 0, - 0, 1, 0, 1, + 0, 2, 0, 2, 0, 0, 1, 1, - 0, 1, 1, 0 + 0, 2, 1, 0 ], buildLog); }, difference: function(segments, buildLog){ // primary - secondary - // above1 below1 above2 below2 Keep? - // 0 0 0 0 => 0 - // 0 0 0 1 => 0 - // 0 0 1 0 => 0 - // 0 0 1 1 => 0 - // 0 1 0 0 => 1 - // 0 1 0 1 => 0 - // 0 1 1 0 => 1 - // 0 1 1 1 => 0 - // 1 0 0 0 => 1 - // 1 0 0 1 => 1 - // 1 0 1 0 => 0 - // 1 0 1 1 => 0 - // 1 1 0 0 => 0 - // 1 1 0 1 => 1 - // 1 1 1 0 => 1 - // 1 1 1 1 => 0 + // above1 below1 above2 below2 Keep? Value + // 0 0 0 0 => no 0 + // 0 0 0 1 => no 0 + // 0 0 1 0 => no 0 + // 0 0 1 1 => no 0 + // 0 1 0 0 => yes filled below 2 + // 0 1 0 1 => no 0 + // 0 1 1 0 => yes filled below 2 + // 0 1 1 1 => no 0 + // 1 0 0 0 => yes filled above 1 + // 1 0 0 1 => yes filled above 1 + // 1 0 1 0 => no 0 + // 1 0 1 1 => no 0 + // 1 1 0 0 => no 0 + // 1 1 0 1 => yes filled above 1 + // 1 1 1 0 => yes filled below 2 + // 1 1 1 1 => no 0 return select(segments, [ 0, 0, 0, 0, - 1, 0, 1, 0, + 2, 0, 2, 0, 1, 1, 0, 0, - 0, 1, 1, 0 + 0, 1, 2, 0 ], buildLog); }, differenceRev: function(segments, buildLog){ // secondary - primary - // above1 below1 above2 below2 Keep? - // 0 0 0 0 => 0 - // 0 0 0 1 => 1 - // 0 0 1 0 => 1 - // 0 0 1 1 => 0 - // 0 1 0 0 => 0 - // 0 1 0 1 => 0 - // 0 1 1 0 => 1 - // 0 1 1 1 => 1 - // 1 0 0 0 => 0 - // 1 0 0 1 => 1 - // 1 0 1 0 => 0 - // 1 0 1 1 => 1 - // 1 1 0 0 => 0 - // 1 1 0 1 => 0 - // 1 1 1 0 => 0 - // 1 1 1 1 => 0 + // above1 below1 above2 below2 Keep? Value + // 0 0 0 0 => no 0 + // 0 0 0 1 => yes filled below 2 + // 0 0 1 0 => yes filled above 1 + // 0 0 1 1 => no 0 + // 0 1 0 0 => no 0 + // 0 1 0 1 => no 0 + // 0 1 1 0 => yes filled above 1 + // 0 1 1 1 => yes filled above 1 + // 1 0 0 0 => no 0 + // 1 0 0 1 => yes filled below 2 + // 1 0 1 0 => no 0 + // 1 0 1 1 => yes filled below 2 + // 1 1 0 0 => no 0 + // 1 1 0 1 => no 0 + // 1 1 1 0 => no 0 + // 1 1 1 1 => no 0 return select(segments, [ - 0, 1, 1, 0, + 0, 2, 1, 0, 0, 0, 1, 1, - 0, 1, 0, 1, + 0, 2, 0, 2, 0, 0, 0, 0, ], buildLog); }, xor: function(segments, buildLog){ // primary ^ secondary - // above1 below1 above2 below2 Keep? - // 0 0 0 0 => 0 - // 0 0 0 1 => 1 - // 0 0 1 0 => 1 - // 0 0 1 1 => 0 - // 0 1 0 0 => 1 - // 0 1 0 1 => 0 - // 0 1 1 0 => 0 - // 0 1 1 1 => 1 - // 1 0 0 0 => 1 - // 1 0 0 1 => 0 - // 1 0 1 0 => 0 - // 1 0 1 1 => 1 - // 1 1 0 0 => 0 - // 1 1 0 1 => 1 - // 1 1 1 0 => 1 - // 1 1 1 1 => 0 + // above1 below1 above2 below2 Keep? Value + // 0 0 0 0 => no 0 + // 0 0 0 1 => yes filled below 2 + // 0 0 1 0 => yes filled above 1 + // 0 0 1 1 => no 0 + // 0 1 0 0 => yes filled below 2 + // 0 1 0 1 => no 0 + // 0 1 1 0 => no 0 + // 0 1 1 1 => yes filled above 1 + // 1 0 0 0 => yes filled above 1 + // 1 0 0 1 => no 0 + // 1 0 1 0 => no 0 + // 1 0 1 1 => yes filled below 2 + // 1 1 0 0 => no 0 + // 1 1 0 1 => yes filled above 1 + // 1 1 1 0 => yes filled below 2 + // 1 1 1 1 => no 0 return select(segments, [ - 0, 1, 1, 0, - 1, 0, 0, 1, - 1, 0, 0, 1, - 0, 1, 1, 0 + 0, 2, 1, 0, + 2, 0, 0, 1, + 1, 0, 0, 2, + 0, 1, 2, 0 ], buildLog); } }; diff --git a/dist/polybool.min.js b/dist/polybool.min.js index 3c98a5b..43ff638 100644 --- a/dist/polybool.min.js +++ b/dist/polybool.min.js @@ -3,4 +3,4 @@ * @license MIT * @preserve Project Home: https://github.com/voidqk/polybooljs */ -var Epsilon=require("./lib/epsilon");var Intersecter=require("./lib/intersecter");var SegmentChainer=require("./lib/segment-chainer");var SegmentSelector=require("./lib/segment-selector");function calc(regions1,inverted1,regions2,inverted2,operations,epsilon,buildLog){if(operations.length<=0)return{};var inverted={union:inverted1||inverted2,intersect:inverted1&&inverted2,difference:inverted1&&!inverted2,differenceRev:!inverted1&&inverted2,xor:inverted1!==inverted2};operations.forEach(function(op){if(!inverted.hasOwnProperty(op))throw new Error("invalid PolyBool operation: "+op)});if(typeof epsilon==="number")epsilon=Epsilon(epsilon);else epsilon=Epsilon();var i1=Intersecter(true,epsilon,buildLog);regions1.forEach(i1.addRegion);var s1=i1.calculate(inverted1);var i2=Intersecter(true,epsilon,buildLog);regions2.forEach(i2.addRegion);var s2=i2.calculate(inverted2);if(buildLog)buildLog.reset();var i3=Intersecter(false,epsilon,buildLog);var s3=i3.calculate(s1,inverted1,s2,inverted2);var result={};operations.forEach(function(op){var s4=SegmentSelector[op](s3,buildLog);result[op]={regions:SegmentChainer(s4,epsilon,buildLog),inverted:inverted[op]}});return result}var PolyBool={BuildLog:require("./lib/build-log"),Epsilon:Epsilon,Intersecter:Intersecter,SegmentChainer:SegmentChainer,SegmentSelector:SegmentSelector,union:function(regions1,inverted1,regions2,inverted2,epsilon,buildLog){return calc(regions1,inverted1,regions2,inverted2,["union"],epsilon,buildLog).union},intersect:function(regions1,inverted1,regions2,inverted2,epsilon,buildLog){return calc(regions1,inverted1,regions2,inverted2,["intersect"],epsilon,buildLog).intersect},difference:function(regions1,inverted1,regions2,inverted2,epsilon,buildLog){return calc(regions1,inverted1,regions2,inverted2,["difference"],epsilon,buildLog).difference},differenceRev:function(regions1,inverted1,regions2,inverted2,epsilon,buildLog){return calc(regions1,inverted1,regions2,inverted2,["differenceRev"],epsilon,buildLog).differenceRev},xor:function(regions1,inverted1,regions2,inverted2,epsilon,buildLog){return calc(regions1,inverted1,regions2,inverted2,["xor"],epsilon,buildLog).xor},calculate:calc};if(typeof window==="object")window.PolyBool=PolyBool;module.exports=PolyBool},{"./lib/build-log":2,"./lib/epsilon":3,"./lib/intersecter":4,"./lib/segment-chainer":6,"./lib/segment-selector":7}],2:[function(require,module,exports){function BuildLog(){var my;var nextSegmentId=0;var curVert=false;function push(type,data){my.list.push({type:type,data:data?JSON.parse(JSON.stringify(data)):void 0});return my}my={list:[],segmentId:function(){return nextSegmentId++},checkIntersection:function(seg1,seg2){return push("check",{seg1:seg1,seg2:seg2})},segmentChop:function(seg,end){push("div_seg",{seg:seg,pt:end});return push("chop",{seg:seg,pt:end})},statusRemove:function(seg){return push("pop_seg",{seg:seg})},segmentUpdate:function(seg){return push("seg_update",{seg:seg})},segmentNew:function(seg,primary){return push("new_seg",{seg:seg,primary:primary})},segmentRemove:function(seg){return push("rem_seg",{seg:seg})},tempStatus:function(seg,above,below){return push("temp_status",{seg:seg,above:above,below:below})},rewind:function(seg){return push("rewind",{seg:seg})},status:function(seg,above,below){return push("status",{seg:seg,above:above,below:below})},vert:function(x){if(x===curVert)return my;curVert=x;return push("vert",{x:x})},log:function(data){if(typeof data!=="string")data=JSON.stringify(data,false," ");return push("log",{txt:data})},reset:function(){return push("reset")},selected:function(segs){return push("selected",{segs:segs})},chainStart:function(seg){return push("chain_start",{seg:seg})},chainRemoveHead:function(index,pt){return push("chain_rem_head",{index:index,pt:pt})},chainRemoveTail:function(index,pt){return push("chain_rem_tail",{index:index,pt:pt})},chainNew:function(pt1,pt2){return push("chain_new",{pt1:pt1,pt2:pt2})},chainMatch:function(index){return push("chain_match",{index:index})},chainClose:function(index){return push("chain_close",{index:index})},chainAddHead:function(index,pt){return push("chain_add_head",{index:index,pt:pt})},chainAddTail:function(index,pt){return push("chain_add_tail",{index:index,pt:pt})},chainConnect:function(index1,index2){return push("chain_con",{index1:index1,index2:index2})},chainReverse:function(index){return push("chain_rev",{index:index})},chainJoin:function(index1,index2){return push("chain_join",{index1:index1,index2:index2})},done:function(){return push("done")}};return my}module.exports=BuildLog},{}],3:[function(require,module,exports){function Epsilon(eps){if(typeof eps!=="number")eps=1e-10;var my={epsilon:function(v){if(typeof v==="number")eps=v;return eps},pointAboveOrOnLine:function(pt,left,right){var Ax=left[0];var Ay=left[1];var Bx=right[0];var By=right[1];var Cx=pt[0];var Cy=pt[1];return(Bx-Ax)*(Cy-Ay)-(By-Ay)*(Cx-Ax)>=-eps},pointBetween:function(p,left,right){var d_py_ly=p[1]-left[1];var d_rx_lx=right[0]-left[0];var d_px_lx=p[0]-left[0];var d_ry_ly=right[1]-left[1];var dot=d_px_lx*d_rx_lx+d_py_ly*d_ry_ly;if(dot-eps)return false;return true},pointsSameX:function(p1,p2){return Math.abs(p1[0]-p2[0])0})}function checkIntersection(ev1,ev2){var seg1=ev1.seg;var seg2=ev2.seg;var a1=seg1.start;var a2=seg1.end;var b1=seg2.start;var b2=seg2.end;if(buildLog)buildLog.checkIntersection(seg1,seg2);var i=eps.linesIntersect(a1,a2,b1,b2);if(i===false){if(!eps.pointsCollinear(a1,a2,b1))return false;if(eps.pointsSame(a1,b2)||eps.pointsSame(a2,b1))return false;var a1_equ_b1=eps.pointsSame(a1,b1);var a2_equ_b2=eps.pointsSame(a2,b2);if(a1_equ_b1&&a2_equ_b2)return ev2;var a1_between=!a1_equ_b1&&eps.pointBetween(a1,b1,b2);var a2_between=!a2_equ_b2&&eps.pointBetween(a2,b1,b2);if(a1_equ_b1){if(a2_between){eventDivide(ev2,a2)}else{eventDivide(ev1,b2)}return ev2}else if(a1_between){if(!a2_equ_b2){if(a2_between){eventDivide(ev2,a2)}else{eventDivide(ev1,b2)}}eventDivide(ev2,a1);return false}}else{if(i.alongA===0){if(i.alongB===-1)eventDivide(ev1,b1);else if(i.alongB===0)eventDivide(ev1,i.pt);else if(i.alongB===1)eventDivide(ev1,b2)}if(i.alongB===0){if(i.alongA===-1)eventDivide(ev2,a1);else if(i.alongA===0)eventDivide(ev2,i.pt);else if(i.alongA===1)eventDivide(ev2,a2)}}return false}var segments=[];while(!event_root.isEmpty()){var ev=event_root.getHead();if(buildLog)buildLog.vert(ev.pt[0]);if(ev.isStart){if(buildLog)buildLog.segmentNew(ev.seg,ev.primary);var surrounding=statusFindSurrounding(ev);var above=surrounding.before?surrounding.before.ev:null;var below=surrounding.after?surrounding.after.ev:null;if(buildLog){buildLog.tempStatus(ev.seg,above?above.seg:false,below?below.seg:false)}function checkBothIntersections(){if(above){var eve=checkIntersection(ev,above);if(eve)return eve}if(below)return checkIntersection(ev,below);return false}var eve=checkBothIntersections();if(eve){if(selfIntersection){eve.seg.myFill.above=!eve.seg.myFill.above}else{eve.seg.otherFill=ev.seg.myFill}if(buildLog)buildLog.segmentUpdate(eve.seg);ev.other.remove();ev.remove()}if(event_root.getHead()!==ev){if(buildLog)buildLog.rewind(ev.seg);continue}if(selfIntersection){var toggle;if(ev.seg.myFill.below===null)toggle=true;else toggle=ev.seg.myFill.above!==ev.seg.myFill.below;if(!below){ev.seg.myFill.below=primaryPolyInverted}else{ev.seg.myFill.below=below.seg.myFill.above}if(toggle)ev.seg.myFill.above=!ev.seg.myFill.below;else ev.seg.myFill.above=ev.seg.myFill.below}else{if(ev.seg.otherFill===null){var inside;if(!below){inside=ev.primary?secondaryPolyInverted:primaryPolyInverted}else{if(ev.primary===below.primary)inside=below.seg.otherFill.above;else inside=below.seg.myFill.above}ev.seg.otherFill={above:inside,below:inside}}}if(buildLog){buildLog.status(ev.seg,above?above.seg:false,below?below.seg:false)}ev.other.status=surrounding.insert(LinkedList.node({ev:ev}))}else{var st=ev.status;if(st===null)throw new Error("PolyBool: Zero-length segment detected; your epsilon is probably too small");if(status_root.exists(st.prev)&&status_root.exists(st.next))checkIntersection(st.prev.ev,st.next.ev);if(buildLog)buildLog.statusRemove(st.ev.seg);st.remove();if(!ev.primary){var s=ev.seg.myFill;ev.seg.myFill=ev.seg.otherFill;ev.seg.otherFill=s}segments.push(ev.seg)}event_root.getHead().remove()}if(buildLog)buildLog.done();return segments}if(!selfIntersection){return{calculate:function(segments1,inverted1,segments2,inverted2){segments1.forEach(function(seg){eventAddSegment(seg,true)});segments2.forEach(function(seg){eventAddSegment(seg,false)});return calculate(inverted1,inverted2)}}}return{addRegion:function(region){var pt1;var pt2=region[region.length-1];for(var i=0;i=-eps},pointBetween:function(p,left,right){var d_py_ly=p[1]-left[1];var d_rx_lx=right[0]-left[0];var d_px_lx=p[0]-left[0];var d_ry_ly=right[1]-left[1];var dot=d_px_lx*d_rx_lx+d_py_ly*d_ry_ly;if(dot-eps)return false;return true},pointsSameX:function(p1,p2){return Math.abs(p1[0]-p2[0])0})}function checkIntersection(ev1,ev2){var seg1=ev1.seg;var seg2=ev2.seg;var a1=seg1.start;var a2=seg1.end;var b1=seg2.start;var b2=seg2.end;if(buildLog)buildLog.checkIntersection(seg1,seg2);var i=eps.linesIntersect(a1,a2,b1,b2);if(i===false){if(!eps.pointsCollinear(a1,a2,b1))return false;if(eps.pointsSame(a1,b2)||eps.pointsSame(a2,b1))return false;var a1_equ_b1=eps.pointsSame(a1,b1);var a2_equ_b2=eps.pointsSame(a2,b2);if(a1_equ_b1&&a2_equ_b2)return ev2;var a1_between=!a1_equ_b1&&eps.pointBetween(a1,b1,b2);var a2_between=!a2_equ_b2&&eps.pointBetween(a2,b1,b2);if(a1_equ_b1){if(a2_between){eventDivide(ev2,a2)}else{eventDivide(ev1,b2)}return ev2}else if(a1_between){if(!a2_equ_b2){if(a2_between){eventDivide(ev2,a2)}else{eventDivide(ev1,b2)}}eventDivide(ev2,a1);return false}}else{if(i.alongA===0){if(i.alongB===-1)eventDivide(ev1,b1);else if(i.alongB===0)eventDivide(ev1,i.pt);else if(i.alongB===1)eventDivide(ev1,b2)}if(i.alongB===0){if(i.alongA===-1)eventDivide(ev2,a1);else if(i.alongA===0)eventDivide(ev2,i.pt);else if(i.alongA===1)eventDivide(ev2,a2)}}return false}var segments=[];while(!event_root.isEmpty()){var ev=event_root.getHead();if(buildLog)buildLog.vert(ev.pt[0]);if(ev.isStart){if(buildLog)buildLog.segmentNew(ev.seg,ev.primary);var surrounding=statusFindSurrounding(ev);var above=surrounding.before?surrounding.before.ev:null;var below=surrounding.after?surrounding.after.ev:null;if(buildLog){buildLog.tempStatus(ev.seg,above?above.seg:false,below?below.seg:false)}function checkBothIntersections(){if(above){var eve=checkIntersection(ev,above);if(eve)return eve}if(below)return checkIntersection(ev,below);return false}var eve=checkBothIntersections();if(eve){if(selfIntersection){eve.seg.myFill.above=!eve.seg.myFill.above}else{eve.seg.otherFill=ev.seg.myFill}if(buildLog)buildLog.segmentUpdate(eve.seg);ev.other.remove();ev.remove()}if(event_root.getHead()!==ev){if(buildLog)buildLog.rewind(ev.seg);continue}if(selfIntersection){var toggle;if(ev.seg.myFill.below===null)toggle=true;else toggle=ev.seg.myFill.above!==ev.seg.myFill.below;if(!below){ev.seg.myFill.below=primaryPolyInverted}else{ev.seg.myFill.below=below.seg.myFill.above}if(toggle)ev.seg.myFill.above=!ev.seg.myFill.below;else ev.seg.myFill.above=ev.seg.myFill.below}else{if(ev.seg.otherFill===null){var inside;if(!below){inside=ev.primary?secondaryPolyInverted:primaryPolyInverted}else{if(ev.primary===below.primary)inside=below.seg.otherFill.above;else inside=below.seg.myFill.above}ev.seg.otherFill={above:inside,below:inside}}}if(buildLog){buildLog.status(ev.seg,above?above.seg:false,below?below.seg:false)}ev.other.status=surrounding.insert(LinkedList.node({ev:ev}))}else{var st=ev.status;if(st===null){throw new Error("PolyBool: Zero-length segment detected; your epsilon is "+"probably too small or too large")}if(status_root.exists(st.prev)&&status_root.exists(st.next))checkIntersection(st.prev.ev,st.next.ev);if(buildLog)buildLog.statusRemove(st.ev.seg);st.remove();if(!ev.primary){var s=ev.seg.myFill;ev.seg.myFill=ev.seg.otherFill;ev.seg.otherFill=s}segments.push(ev.seg)}event_root.getHead().remove()}if(buildLog)buildLog.done();return segments}if(!selfIntersection){return{calculate:function(segments1,inverted1,segments2,inverted2){segments1.forEach(function(seg){eventAddSegment(seg,true)});segments2.forEach(function(seg){eventAddSegment(seg,false)});return calculate(inverted1,inverted2)}}}return{addRegion:function(region){var pt1;var pt2=region[region.length-1];for(var i=0;iP7LYKEkx6{V)(R9=TJ4uCePWsYT;Lti%8fa=t-UPywN>V`43#sLtV-Dw!z5g55duzS3_FnsYe!sncXP@8NYk&66IO64^ zuC_%D004E@L(WG5KmjGMw^bG8me0ndJb43)KHwjHED{?X8$t{P_MeSB9jfnoAtWsH zXlTgUD;L?J4gj#q;)sXO0T2Y=aY5ca!9LJIt^fc8F3ao6gPyY+n;9FEjI%P^-ZxBS zW@a)NjG>_+KA+#w(b0a35}fZ*6zSy`FCzrXyYceK~o*m!DcDn345GaY*M z)m5=rtau+-+yF7|+`c(Od(zS&{#ASrK1X_y%;;teoJ|-*drVuQHv*tRn@;zw*B{8Q zm@h$vB;eh$lX&ZgRo?^;v?8hRQW3hY(O0R#de4T#-XuQ6~Wd}D?d zs0B5?3Z3sAPMle z=Z<-Tp?{)J8(Gk3w3TliKbjB+f|V6Z>Of0N3&>=EngHl_5WE@(?nXM>gP;bIfB2hk_!|H~+FhOZ`&{lG7SG3ud|}Fx zjIikU)+2A|8?FUeW^}neRw$I}{N}$!RU2B8m{R;8yzqH0zo;g+a?G9Epf?lWJKLO) z{-7(2P9L2N8oJ6~SZ+t-U81I^KDX(?e` zqxmr2K5({J?K^OcS;UczhVB$}cacZ;_=Jzf>!Vp!LF*f9AW@NVc5r9@ z?~$S*LiW}sbBD3C8TQU%-0_H6pQnrroBwaVhAm|YnByGk>A498y^9VX=3>&cq)_?ng(!CEm$j_%F-X^V4vF@IeSNh*>5 z9XgLTMrQ1}I=|{LQ;B{*`@4N~{apUA>t;>~VKq_DzMnK#verZPE3%r_NIp~$GG{m} z{Xsa7icF8_hG1mM@=jOL_j(~7GLl^GA>ltm9ApOxX~f-;yw^wZ5I;i~vE~NeYj6B2 z`XTZhp$Ml;BId~bxw^Z=kVWZ3iJEMXgs*{pW_z;2Tc;gt6z!NW#%WXmIm=oR!3>bg zY}WBk$Sdg=iM3Q@@7M#0lk$ThlS2VWb(El(8=-7VTi32V+1c^i&I!PWIxxFh2l(KD>OLEgdy!v zF)serPOphC2zVlg_30t3Ij=A!DwjK^T8CBj#p|_sTSN}x3JrWvYiKVpH>+T)@^YG( z95%YS7}lJd)6C?!Lj9;%)uUd}fnV%|ns}bZJq(ogLNwLRv~@}(ULhzEcQ<2I4_j<% z3&M*um3SI7%pO{uXpm5Hj?^yIlKYp)cf(1qr9N^w)b4HLJ1(jVRq39MfUxL3z0H% zr-zqf+ksK@i9(gg>D?1?~0kaMjk37R1!j6B^JkEx_hH-c{ zDW%0v4z($@dEgsh4l&L)#DfH5VkLI`qh?;~A5E}UZSi{}tVNh=sX0{ukuTI$sAEV^ z)B(h0SnZ_jM&FdwhbJk4yHOG8A1KG{QmACpz_wLhPF|o_@Gd{pR)!<9A3NTmDdPk* zX}5;kq@)*{9dCnrnU@M6dgyx|D0gHY4Uf1ycx}B5*QZ6aolU&a#SUP}IDc3x#=GRg zsXc8|QG)iTyTm;%?+QZthrc418^TbP3qG@BLXVe!x>SycajRpSE|{Mn-pmru+g@_l zO25^7+`F|Xs%`_(nYp`iA%IG}^VK9nMn~etCe};oN7%dJKNf-}RlqQV;M-xhV= zGosSR)~^vEh6I~+%g~hB7kRY4f}I0U!4cFd2A1Rf+U6)(iJn2UCaCAalOt~HXk-48 zwn_?&T~q{CC`SgC?cHqSM%JRc5~~R6*>F+>S;q!*M%vm}u-#>yKm)aoz{~osAW%hF z5(3S$T&YR$`DPrDcl<|e^7o22buY}R(>RMK#LcLiJ7&9v zeM6}iD$R#*UA-^z8XN6g-b%#=YIhn%Yk%PBz|p{nRA*!c+RG zFZ_ytAv{F7@k2#~xe<-q^=L#L+6Vu(HAS%s)FyYfjQUP~l4%*_2r<0nj<4A5)Ga4< zbh3u3jM8zFrPH3T+NNcEaj+hHBxa#&Yio&RB%9K;CC;9*wZ3LQ-VJXR)is;15{!G8 zbLrw-{5u*3e{s+zKcbpEk{K)>N84GxOQRycnx6m5v(d;v4m~Z)e!Sxm`^D_yxQS%>SCHE*}%V#%^K8|!e zU>5wN$y#)?Vbum+Tt7F5&^w28uKF%;Wo~1!^b=sLc=avDR|bD~VkGQQQorx8*tbVi zsz*u-$xpc59It&x|K0ZilUlLK)wL?u7t4f8Hd?sJL6-#3RI7Yal47+_Luvp2ESj!f z$xRO1++<2R!v~>6Ta*HdD^fw#>1^BO=Xmd>DWQ&bzwk)zvhKJ+FSDF5=KQ(Fe*WJLTzH_=(empV$aNZ zzj3f5`9#}cm$SShQK)ToHaJo(mlFHt5$wFhPj1Ss{Y%~2c}vz$qEQ_3Exg>{G4dIQ z_!vxn1piV{JVUhBH3%nlb1B1J^m8u@O$K^2C$SQ^Nn=~Fe_M=TQVv2)TMQIU^wOmW zAvkI3Kk>LG0+k*woCZ%z_Re;3yDp{H!WgZg0u&7&!0sBscq6PRT(2Yz^FY*fvMEkk zkC?L78TSu7s(aN|?l~NsSvB`hhIjk*fAH$&m%~uWY(6+QD9=Y z5K_!>mCM{f_yy8F*#oK}>Yuy}=E_V>uDo(bDD4O%Oy6bG-(N}%M5LNhW2f&v5lty$ z3Jvx#9gwS-HY>~=eclmDJxKhEtbdLqkxurBWCsK9{ued15L9WSGfyL*w!STB)UFG* z(XQs3Nht|F=sP|AJWi%25y@`TA7eD-RPHKh!x+%+i;nkKKfSZZ6Y5w06Pn8sMdyhR zRyiLMqbR>Wxx64zcqu+u4Uq>W&o>R7TGHFi#kRue-P9;5uJI@i& z#IM+KzNS2oI#Y2(tbvi?K@%JX4Hn#;!Ck}P?y%guyS26Z z=llMg_pR=y`*fY|Q(fojsBenWXefjzFfcG^02v7t7?^iOZ~vo6aBpjJzG(Q{hrmrz z%T3kM!p+mz#T-V|%+bW09AIy3X|7^!Z07AeWG(;$0|)a>UQP1#^>u4&YkPb9-q_f1c6MG{TXT1JUtL`#ARw5Vo12`R9334U85vnzT!cU%gM))( zV`KC4^UKT2)6>&aQ&Xp>r>?H9XJ=>6&(9AJ4|{uiP$=}vmoEzo3wL*SFE1~%v$Mm) z!@Ik?>+9>cx3~BA_nVuWhlhtND=Xi=eKR#RRaaMET3XW5(o$7bjgOCab8{OS8oIo^ zyt=yj{P{B&4EFT&3`pm2SC9T5>BCMMR@)TE)Ifsc=`udiQHQW6jl z;N#;H9UaZe%Ie_Y(9+V<(9lp@TMGh#^7Hd4DJhwmnSnrHMn*<(aBx;umbSLGhlfX5 zS(%!e8Vd`{#Kc5oWMp}Hxv{aarKM#;LPC0ax~;9PsHmv5wRL!SxT2yWH8pi%VWG6N zbW~K7x3{;2g+)h42LJ#N6cqgU@goNZhq$=7oSa--TwHZ^bx~1~ot+&P7FJ+jAR!?k zIyyQkDk?iWJ02chXlUq%4id;K_HS%drB zT%$)qlke$`f8eVx0pRBytx9l=e@9+G@<_{V_F1;8%V|qeDJ*%|M=>Od|HlIE*Y9NR zxyY=Wz7{{>AhaBhY_+Gl86WukU9kQB?zr^?E@G2iPdL^;We-96=-9Pp%!?FUTLtM_ zbX`ntXa%04tSoxD0k1V9D*Xc@ZPO?2o_PIHzQTTH*11+s$8K+1E$5!~v=A%aD*9Wt z=3ckpMC!cCMQ+jcPS8(hQVSYb~RB0G? z$iW9cX6VDMTat2h8n*cKOyAN4-V+m}4dGePjXw96EbEvU5oNc!BY$q-@diBZjmZSH zRlMjgm{NVb+yyVO9YW$&Rd!U#;tF|HZ@Jt^UQoe*)H>$D`UxKI-?KE~>HfY)7|as) zyX3$>wPadXhxZ)-w=Vd`h{7>wI)0O4v>xMQNFwZMEzvzx_vCRAI|c_V zYw3oys4ffTtG$dj~W2s?UvVY4jMD z)!&JG42X(V14fmmMq8wsxmVTODYRKj{0e)>Z)!)7Z{luWLY@3*kaFWK*JF?B5Tr2Z z1g#E{jMi%={rT|Gd@@N;kb~@sNS@iSZqnHR;-D$5&-ZCWtO3!6j9_H_uEiJejBl^>H zAL5BpBxlOtk$Q*xA@OTH4mhu$alN2QuN@Y!GIO&?_52;CoxgF73(EIX$1N5kpvTzX zd!x`EL*U|QVjV^%m-%nA&CQHWhW7=X&}D?VLjjwP(A<3zkllLN33k1LpVQDIaDc}} z)a<}@x;8;J16($#1pkD!@yP1f*6oJ~rW*g;guhc_wt@Qje3U3{;ybk`ipu2GMno*> zrNzE7_kZ1LsHjSi=QD~-f!FE2*Q>X%*-7R~oA~9!?2mhHIMhSC!7m0?XKiHh9)b>R zUJ&L`uFk_7K0@~Bbbf(jphM*~Ou+-fhX469AHkbUPT&v`lI}A(a5vk8zt`j?ua0Oc zb?y>v`OLyX)?8bL9+b*{Xf*lea+P0w9Rr|bL5t`lo?YPTXOE^{k-N2927T<}*U6;W zh;Y*e>O7_lhEdpmz+vWo13#dnlTmQr@$h-gFR3dFQm?J(HGbQk*fzO1}S~n(9y*?PF%nyP3`{kC`m0IhKiW(-x0kG^MUSE5tO~~}Uqc(4Uoog(%Z{lI<+lKBv;o&Q7HOR^4nr{;?f6@2khL!Oi5l)uj zC9c-j_-^PU>`obBC}}p`ym>17VN^<<%8pbQQhmhm3U!ma?ra*rF-?{F%FXgMaF9!6Y?Y@hsn{LB z^2tReNFINaYoBS?PZ~BE2q4KS02h}M9HW2jFOOdjY1YWTL{bFQEyE?thM_l%@0rWY zjC^LbmID2?-oZ;yo0~ELXfGl&K%BOdHKo2c5=ZbRFiOoY!%kNn>lVLzkEITbAGbvD|w!tVq z`}*27ryUY17mZ5$La1W`XjxIhr%$(`PQsA*G;(XeZKaMLeA;A-r`6^@e3p?UE`>r( zgOQb9G&hv5PFBrm&)F{yvt!cqF>7N1k_6jf(V0;a+7lK99h5gc3+>v#Zxb2dCi3%9 z8m>VarsCA$V1BJiE`BszrHFUV;K6r{-W9a_5JGwPk*Kto+owQRB3bcKPxdB0wkRmS z>lZA5jO+#_^E>FoNz1*2hA!DT9fvE9QP6GB+5Pv*_bNi}`3Go*S;>#)1AKHTtq8DW zTfNk3qs$edug}8YwSEEc5+ql_z(6xc|I4@I1(WOR<;Wg4>&%Hp;SHuKpEV5DsZEti zU;HOcF!f>k8{GU<<8jYxTj;xk_}31sICi9Co{Nx zfmj#!fW2Tbvig$L%(tX{t+&hT8nuS$X9MBs8XTJ`B5UW~@+uY%6K-ex!Y$*LXFK-N z3nYY25T(nKD<+`r4AvxO-_DvHR4^|*4UB!vCRj&?iv|sDp&K>rESPjb7(*wpEIbEl zf)d9@FUbP^&72xMmO1zJvw+kjuyoGxuV$5AI0MwnGX(Xgm zzupf^Z_t~s*|d=cdguMUUN5o^Uf@0$-Pa||>*3z~c(T|0a1r$ zlVu5-hhU_0Aa`|^h*fqt^Sxjckuk+hT(nfZP*8Kp6hI)W85^&g)2^RC+R|83yC~`c zV;xDZSNz?sdF<7eQ5(ghv-EGNuL(HO$!h}ygs-??ZXVGQC9?b2yBN+8nuscd}TWU)8mg)@ny-e8uCE^s^k!l=yIpN+x$4drySry#{B6O%*h64LvB{e5|4lbH{WM-%H!vq7=um27QvFhwlIyv^H<{9c zKhxEznA`%IrxNq7)b{ME?7rGR*myUoDilKEk9LT;pTMn+LmWHr%U(6bK0#w1ak27) z1H2kW6z)~2#e%ti5hs1~XEp^PMIvf>rU7UTf8^<53S%NbX6v%g*|d;C<-AV-QyP^Lu8+eyMO! zx}Epvdv-69J+H-^ZIlk+Sk_aazm5N-K-B!pO5w5iQ3rYdW}LIsZAi$T_&ZS5dT&L| zDA^|Z56{=EMTAc$vAv}}xO&!H+CKG+)6TW!w}XrYvCM`&!l9}>_|+6h%Y7CMIHSna ze;cG{D9x35lp*W;JEX9o5(Qc7GGUKUk`Uj{X*wO`lWd5t*zFj#={)*+G0rh7UI*q~ ztnpG!(Z=?C^|L#)UkUH|5KwMNw-g!f?1VEQx$!Ic2YW2~^;4-|KtO`E6fZTD(g8Cn zepACST+Eaw7E@Bg|0n?n$pe$!#U9zL17$IHw%$dRw>J$RI$S%3gu*8$jedTHZ^vP; z3-W%wRk~~=^DD?>m-dpW5_|>MUs;XF5<|RBct3Eg2~d8gChb2(!#Y>ctKGAYjn|J| zm-yO+XT)Ry2xC8w=&AoH+3ng3glpqg$9E2R49urj_x|WE=J~nPL?wpJzTtd)4(xtvnj($|@|&;p#;kc` zVm>+CJtUgXbPU+CkS>>1Q7%<&Bl3=Y1P5g_^QRb?U?@4&{>*?kW zJa1sH^5`D?up1kZHd8**2-|eN={GS6+B*zLQ-D2m&KDV-Adb0v8tQN^>${^PjgIv; z>1F|~S7Xls1nfI%FVi6db8G)Jf7$YvVTbxpo_OML1#C=1WK8 z+RbS=vWMLBc@DLFd%p9A&?KfrXoU8|Y~Wd~B$c7NhKZT5DPPmd2uqT;P#v_uF-!$y z>3u*$w5I`H6^ig)Q~h#D>WN^O^W0|yDsKnULrcu%GwdeEa?Su3H9gG&cPulut-Fir zS3M-5NbxG(qr$4%580WP+DpgaZ^Ehc931Lsk1Xot8NU80D%;mksSf$v^?(fh9758J zGLgujXj>5uaz*^1VM+YdL^?x97KI*CSa#2g~II8t!*ZRA6ZlX1fC8(BTj2 zWms@MDv-IuWgxHH=D5tR0z2iCPt*F^G6QpJ%y~)KT~-~a9Li{ST0Ud%&5dJRY(I!A5T5A{Y>BYb{Oqd@|h3d}u5El*o z3W8(f#k(9@SM05umi2UJCE{{@P@PY0!@xu4lS;<71knR>OxQdWm?+W zF7oeeQR8F%7lCNaY^ypQ_oO3SqFA=6Q`!{@U7#g(7Kf~}77n&|nG_~XypviqhkP+4 zqBOHYMYSNK@I1QEO@*I7dP|}KlB8^6e)I4cnbD(o$W8@~!y8<_nF z94U^El#$fBS4#vO$yYkn$}+is>3uS|i*27W@7dOq6mnu zhPUbIz5S5B-EJFw_R1F1#$H(Lxh{&S;98zjD=#9>9#2uAPHV>5$Ch)QNYk6_?Olhvh7yQxJT1@@VXE zo#Li#@>duOpCaMRGWQu341o@cy%KVN$6q=MoerGjyVW=@ei_Y6(k%3PPIUq5?UMOm zIP7GxT%Pnqr&7Lgql|Nn=xcb`rIX!_W2{k zQ+sozVvI&h zvQS-5^q?AS)i!eI@a$&X?LIDl!Xk^1rGcm+(KXpGIM&efcBYGlycml6ZpQGI^#!;2 z&ybCNp0oRW)65$e%j1o80}|YQ0h)KLhC=1X&kdvT3Nm+(>r(rQg>>ETg{9aqlg*GTx%F3cAI~u%cWOn$xa)+nxOOTz5e?P z+}0sZK{T2E-;X2EMUVO;;2ctLaQFX2UBk>{f$X;>gRsX>>!8sI??k~+YCn_HzS6>dJiN(eUG|?Lg4q~Y!7Px ziX2eV(ftnC5QdK@VUh2h2;taNhM(`v0XVn)+vPuOBFswKq676C|N7$*o9&G3(&3Oc zj{sp~KAR~=Z%L7}~z zhfbN2M`E?twdr2HuCx4;kcAf29IZ2ZNr(q-r#LoD8=oQ|Ny(pMoD-l=?Vop%KGEfz zzqan8Fvme1WG&z;a$D?hc>Kmd7MJHCwBmFc@+dMb@c4513ZAakUN9^Cu^fu*J4#q} zfl2mBQ>V`=GTqbAaYQ6BCnGvic?vK#R!o&N&jJhxnVb?PE4ajnIz(H>(CqSIe8_-$ zd@v-BLhOi}7|Yo5ZG4&NR}11|YHfs0-TdgP0zetPw~C`fEB%K%u0>{kT)C{bPW!Z0 zXoPA|bao2Lzk2e{AL}1Nqbcdu z{4_eIDoy>l(W=(~&8IbOaU5$qW^^@RNno$&3$ZPN)e1aTKnv33Qzg&54?h=xqqJTF zPCGgUq4%_*PtK>k7PL5Qb+5HAO*-zTYoYmFWGlCEg7jcug{5m$8Fjhhq&Hi(lW?ZN zv~k&I`Sd;{UY`}0&)wOaF>VmYwFATZcdbZmHwv&3EQT{dO@Az$_wf;tHeb9U&`?* zm-v>HDX!^YjibSR-FSTt2V-?+W%bao6+oOzww|S~C8cWL*Cvmpyg9XJEOjoK!JOGI z14EpEHqNy)@-283$4&MAQ|#ULOkz+l{1!xL<}Qz@-$m8X(D_E% zpjz#SHUovR2(X~Oxy+y-`VCM>xW}`5LeyXbWlzNd(R--RF3oQ5rh5uM*}p|185;KV zDim4^|Y3y2bCq4{GTHm^zR8JE`{Igj25o*^!J}x zQbG{?<{g^2v?&^|YW%u*9C511v|4rdP_mQo=S~<>J`? z)Vb|3WQ8MUk_klJWh zeFY7BQW$f|MyN6)jea=GFqvhx4-CQEBA#*k}k4-=;-@;`|2v{l2L#9^C^ zZH}a8+1gZgJw&)LjVBC+VQvSdx46~JqA)|K83LvWpze^S2r>=k4$D=?q8#r)q>r^JY= z4%cj~zm8V`cPQt8HK23TU#H=sSeZO^LQikl+2v_=t>WNZRR;nY>cpOR$u0?F#{g&Y z$(fXpSs5_ebQ}}F#z8u9-!%PqaDH5KTZ~bs5yz6X3{JxSo!gywZ;FSTLpCTy%VQTF z8T?%%f81;*{y%WLuSd0|DgY=MOafzZ(!=eRUi^%PMbz^1fh;7@s_dTmA7GP9cADsP z2l*bou#K`<0mC7k<$7u=hsvwT%CIvE(4O|T0(P*+@3x{;IG{B6Aw3YG?-y|?PB5OX z9ro`|9|8Xv?EgJvlL_4tjmM_A9(gOr6;BNqyFe>scidk@oP*B8k57Og(LDMQs48Ca zk~)}}_TD)7*7$~5+@$IHV2XW6Fexh0xALt-SWZgwG8lwD8)g)<+!AG-$eK~^WwJh8kZw-JxSoY24|6{=DH28{b1S4-JMCk<1a( 0 - // 0 0 0 1 => 1 - // 0 0 1 0 => 1 - // 0 0 1 1 => 0 - // 0 1 0 0 => 1 - // 0 1 0 1 => 1 - // 0 1 1 0 => 0 - // 0 1 1 1 => 0 - // 1 0 0 0 => 1 - // 1 0 0 1 => 0 - // 1 0 1 0 => 1 - // 1 0 1 1 => 0 - // 1 1 0 0 => 0 - // 1 1 0 1 => 0 - // 1 1 1 0 => 0 - // 1 1 1 1 => 0 + // above1 below1 above2 below2 Keep? Value + // 0 0 0 0 => no 0 + // 0 0 0 1 => yes filled below 2 + // 0 0 1 0 => yes filled above 1 + // 0 0 1 1 => no 0 + // 0 1 0 0 => yes filled below 2 + // 0 1 0 1 => yes filled below 2 + // 0 1 1 0 => no 0 + // 0 1 1 1 => no 0 + // 1 0 0 0 => yes filled above 1 + // 1 0 0 1 => no 0 + // 1 0 1 0 => yes filled above 1 + // 1 0 1 1 => no 0 + // 1 1 0 0 => no 0 + // 1 1 0 1 => no 0 + // 1 1 1 0 => no 0 + // 1 1 1 1 => no 0 return select(segments, [ - 0, 1, 1, 0, - 1, 1, 0, 0, + 0, 2, 1, 0, + 2, 2, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0 ], buildLog); }, intersect: function(segments, buildLog){ // primary & secondary - // above1 below1 above2 below2 Keep? - // 0 0 0 0 => 0 - // 0 0 0 1 => 0 - // 0 0 1 0 => 0 - // 0 0 1 1 => 0 - // 0 1 0 0 => 0 - // 0 1 0 1 => 1 - // 0 1 1 0 => 0 - // 0 1 1 1 => 1 - // 1 0 0 0 => 0 - // 1 0 0 1 => 0 - // 1 0 1 0 => 1 - // 1 0 1 1 => 1 - // 1 1 0 0 => 0 - // 1 1 0 1 => 1 - // 1 1 1 0 => 1 - // 1 1 1 1 => 0 + // above1 below1 above2 below2 Keep? Value + // 0 0 0 0 => no 0 + // 0 0 0 1 => no 0 + // 0 0 1 0 => no 0 + // 0 0 1 1 => no 0 + // 0 1 0 0 => no 0 + // 0 1 0 1 => yes filled below 2 + // 0 1 1 0 => no 0 + // 0 1 1 1 => yes filled below 2 + // 1 0 0 0 => no 0 + // 1 0 0 1 => no 0 + // 1 0 1 0 => yes filled above 1 + // 1 0 1 1 => yes filled above 1 + // 1 1 0 0 => no 0 + // 1 1 0 1 => yes filled below 2 + // 1 1 1 0 => yes filled above 1 + // 1 1 1 1 => no 0 return select(segments, [ 0, 0, 0, 0, - 0, 1, 0, 1, + 0, 2, 0, 2, 0, 0, 1, 1, - 0, 1, 1, 0 + 0, 2, 1, 0 ], buildLog); }, difference: function(segments, buildLog){ // primary - secondary - // above1 below1 above2 below2 Keep? - // 0 0 0 0 => 0 - // 0 0 0 1 => 0 - // 0 0 1 0 => 0 - // 0 0 1 1 => 0 - // 0 1 0 0 => 1 - // 0 1 0 1 => 0 - // 0 1 1 0 => 1 - // 0 1 1 1 => 0 - // 1 0 0 0 => 1 - // 1 0 0 1 => 1 - // 1 0 1 0 => 0 - // 1 0 1 1 => 0 - // 1 1 0 0 => 0 - // 1 1 0 1 => 1 - // 1 1 1 0 => 1 - // 1 1 1 1 => 0 + // above1 below1 above2 below2 Keep? Value + // 0 0 0 0 => no 0 + // 0 0 0 1 => no 0 + // 0 0 1 0 => no 0 + // 0 0 1 1 => no 0 + // 0 1 0 0 => yes filled below 2 + // 0 1 0 1 => no 0 + // 0 1 1 0 => yes filled below 2 + // 0 1 1 1 => no 0 + // 1 0 0 0 => yes filled above 1 + // 1 0 0 1 => yes filled above 1 + // 1 0 1 0 => no 0 + // 1 0 1 1 => no 0 + // 1 1 0 0 => no 0 + // 1 1 0 1 => yes filled above 1 + // 1 1 1 0 => yes filled below 2 + // 1 1 1 1 => no 0 return select(segments, [ 0, 0, 0, 0, - 1, 0, 1, 0, + 2, 0, 2, 0, 1, 1, 0, 0, - 0, 1, 1, 0 + 0, 1, 2, 0 ], buildLog); }, differenceRev: function(segments, buildLog){ // secondary - primary - // above1 below1 above2 below2 Keep? - // 0 0 0 0 => 0 - // 0 0 0 1 => 1 - // 0 0 1 0 => 1 - // 0 0 1 1 => 0 - // 0 1 0 0 => 0 - // 0 1 0 1 => 0 - // 0 1 1 0 => 1 - // 0 1 1 1 => 1 - // 1 0 0 0 => 0 - // 1 0 0 1 => 1 - // 1 0 1 0 => 0 - // 1 0 1 1 => 1 - // 1 1 0 0 => 0 - // 1 1 0 1 => 0 - // 1 1 1 0 => 0 - // 1 1 1 1 => 0 + // above1 below1 above2 below2 Keep? Value + // 0 0 0 0 => no 0 + // 0 0 0 1 => yes filled below 2 + // 0 0 1 0 => yes filled above 1 + // 0 0 1 1 => no 0 + // 0 1 0 0 => no 0 + // 0 1 0 1 => no 0 + // 0 1 1 0 => yes filled above 1 + // 0 1 1 1 => yes filled above 1 + // 1 0 0 0 => no 0 + // 1 0 0 1 => yes filled below 2 + // 1 0 1 0 => no 0 + // 1 0 1 1 => yes filled below 2 + // 1 1 0 0 => no 0 + // 1 1 0 1 => no 0 + // 1 1 1 0 => no 0 + // 1 1 1 1 => no 0 return select(segments, [ - 0, 1, 1, 0, + 0, 2, 1, 0, 0, 0, 1, 1, - 0, 1, 0, 1, + 0, 2, 0, 2, 0, 0, 0, 0, ], buildLog); }, xor: function(segments, buildLog){ // primary ^ secondary - // above1 below1 above2 below2 Keep? - // 0 0 0 0 => 0 - // 0 0 0 1 => 1 - // 0 0 1 0 => 1 - // 0 0 1 1 => 0 - // 0 1 0 0 => 1 - // 0 1 0 1 => 0 - // 0 1 1 0 => 0 - // 0 1 1 1 => 1 - // 1 0 0 0 => 1 - // 1 0 0 1 => 0 - // 1 0 1 0 => 0 - // 1 0 1 1 => 1 - // 1 1 0 0 => 0 - // 1 1 0 1 => 1 - // 1 1 1 0 => 1 - // 1 1 1 1 => 0 + // above1 below1 above2 below2 Keep? Value + // 0 0 0 0 => no 0 + // 0 0 0 1 => yes filled below 2 + // 0 0 1 0 => yes filled above 1 + // 0 0 1 1 => no 0 + // 0 1 0 0 => yes filled below 2 + // 0 1 0 1 => no 0 + // 0 1 1 0 => no 0 + // 0 1 1 1 => yes filled above 1 + // 1 0 0 0 => yes filled above 1 + // 1 0 0 1 => no 0 + // 1 0 1 0 => no 0 + // 1 0 1 1 => yes filled below 2 + // 1 1 0 0 => no 0 + // 1 1 0 1 => yes filled above 1 + // 1 1 1 0 => yes filled below 2 + // 1 1 1 1 => no 0 return select(segments, [ - 0, 1, 1, 0, - 1, 0, 0, 1, - 1, 0, 0, 1, - 0, 1, 1, 0 + 0, 2, 1, 0, + 2, 0, 0, 1, + 1, 0, 0, 2, + 0, 1, 2, 0 ], buildLog); } };