From b4dd16bbfb7cac2162853ff7bd3d75dc41255b72 Mon Sep 17 00:00:00 2001 From: hkrish Date: Mon, 29 Apr 2013 21:36:12 +0200 Subject: [PATCH 1/3] Boolean Operations. This is probably a crude integration. Need to resolve some issues, such as finding the right place for constants, private classes etc. --- src/path/PathItem.js | 626 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 618 insertions(+), 8 deletions(-) diff --git a/src/path/PathItem.js b/src/path/PathItem.js index 4819c8bf..7f703fe7 100644 --- a/src/path/PathItem.js +++ b/src/path/PathItem.js @@ -36,16 +36,16 @@ var PathItem = this.PathItem = Item.extend(/** @lends PathItem# */{ * // {x: 30, y: 25} and a size of {width: 50, height: 50}: * var path = new Path.Rectangle(new Point(30, 25), new Size(50, 50)); * path.strokeColor = 'black'; - * + * * var secondPath = path.clone(); * var intersectionGroup = new Group(); - * + * * function onFrame(event) { * secondPath.rotate(3); - * + * * var intersections = path.getIntersections(secondPath); * intersectionGroup.removeChildren(); - * + * * for (var i = 0; i < intersections.length; i++) { * var intersectionPath = new Path.Circle({ * center: intersections[i].point, @@ -88,7 +88,7 @@ var PathItem = this.PathItem = Item.extend(/** @lends PathItem# */{ current = new Point(); // the current position function getCoord(index, coord, update) { - var val = parseFloat(coords[index]); + var val = parseFloat(coords[index]); if (relative) val += current[coord]; if (update) @@ -174,6 +174,616 @@ var PathItem = this.PathItem = Item.extend(/** @lends PathItem# */{ break; } } + }, + + + /** + * Calculates the Union of two paths + * Boolean API. + * @param {PathItem} path + * @return {CompoundPath} union of this & path + */ + unite: function( path ){ + function UnionOp( lnk, isInsidePath1, isInsidePath2 ){ + if( isInsidePath1 || isInsidePath2 ){ return false; } + return true; + } + return this._computeBoolean( this, path, UnionOp, 'unite' ); + }, + + /** + * Calculates the Intersection between two paths + * Boolean API. + * @param {PathItem} path + * @return {CompoundPath} Intersection of this & path + */ + intersect: function( path ){ + function IntersectionOp( lnk, isInsidePath1, isInsidePath2 ){ + if( !isInsidePath1 && !isInsidePath2 ){ + return false; + } + return true; + } + return this._computeBoolean( this, path, IntersectionOp, 'intersect' ); + }, + + /** + * Calculates this path + * Boolean API. + * @param {PathItem} path + * @return {CompoundPath} this path + */ + subtract: function( path ){ + function SubtractionOp( lnk, isInsidePath1, isInsidePath2 ){ + var lnkid = lnk.id; + if( lnkid === 1 && isInsidePath2 ){ + return false; + } else if( lnkid === 2 && !isInsidePath1 ){ + return false; + } + return true; + } + return this._computeBoolean( this, path, SubtractionOp, 'subtract' ); + }, + + // Some constants + // Need to find a home for these + // for _IntersectionID and _UNIQUE_ID, we could use the Base._uid? // tried; doesn't work. + _NORMAL_NODE: 1, + _INTERSECTION_NODE: 2, + _IntersectionID: 1, + _UNIQUE_ID: 1, + + /** + * The datastructure for boolean computation: + * _Node - Connects 2 Links, represents a Segment + * _Link - Connects 2 Nodes, represents a Curve + * Graph - List of Links + */ + /** + * Nodes in the graph are analogous to Segment objects + * with additional linkage information to track intersections etc. + * (enough to do a complete graph traversal) + * @param {Point} _point + * @param {Point} _handleIn + * @param {Point} _handleOut + * @param {Any} _id + */ + _Node: function( _point, _handleIn, _handleOut, _id, isBaseContour, _uid ){ + var _NORMAL_NODE = 1; + var _INTERSECTION_NODE = 2; + + this.id = _id; + this.isBaseContour = isBaseContour; + this.type = _NORMAL_NODE; + this.point = _point; + this.handleIn = _handleIn; // handleIn + this.handleOut = _handleOut; // handleOut + this.linkIn = null; // aka linkIn + this.linkOut = null; // linkOut + this.uniqueID = _uid; + + // In case of an intersection this will be a merged node. + // And we need space to save the "other _Node's" parameters before merging. + this.idB = null; + this.isBaseContourB = false; + // this.pointB = this.point; // point should be the same + this.handleBIn = null; + this.handleBOut = null; + this.linkBIn = null; + this.linkBOut = null; + + this._segment = null; + + this.getSegment = function( recalculate ){ + if( this.type === _INTERSECTION_NODE && recalculate ){ + // point this.linkIn and this.linkOut to those active ones + // also point this.handleIn and this.handleOut to correct in and out handles + // If a link is null, make sure the corresponding handle is also null + this.handleIn = (this.linkIn)? this.handleIn : null; + this.handleOut = (this.linkOut)? this.handleOut : null; + this.handleBIn = (this.linkBIn)? this.handleBIn : null; + this.handleBOut = (this.linkBOut)? this.handleBOut : null; + // Select the valid links + this.linkIn = this.linkIn || this.linkBIn; // linkIn + this.linkOut = this.linkOut || this.linkBOut; // linkOut + // Also update the references in links to point to "this" _Node + if( !this.linkIn || !this.linkOut ){ + throw { name: 'Boolean Error', message: 'No matching link found at ixID: ' + + this._intersectionID + " point: " + this.point.toString() }; + } + this.linkIn.nodeOut = this; // linkIn.nodeEnd + this.linkOut.nodeIn = this; // linkOut.nodeStart + this.handleIn = this.handleIn || this.handleBIn; + this.handleOut = this.handleOut || this.handleBOut; + this.isBaseContour = this.isBaseContour | this.isBaseContourB; + } + this._segment = this._segment || new Segment( this.point, this.handleIn, this.handleOut ); + return this._segment; + }; + }, + + /** + * Links in the graph are analogous to CUrve objects + * @param {_Node} _nodeIn + * @param {_Node} _nodeOut + * @param {Any} _id + */ + _Link: function( _nodeIn, _nodeOut, _id, isBaseContour, _winding ) { + this.id = _id; + this.isBaseContour = isBaseContour; + this.winding = _winding; + this.nodeIn = _nodeIn; // nodeStart + this.nodeOut = _nodeOut; // nodeEnd + this.nodeIn.linkOut = this; // nodeStart.linkOut + this.nodeOut.linkIn = this; // nodeEnd.linkIn + this._curve = null; + this.intersections = []; + + // for reusing the paperjs function we need to (temperorily) build a Curve object from this _Link + // for performance reasons we cache it. + this.getCurve = function() { + this._curve = this._curve || new Curve( this.nodeIn.getSegment(), this.nodeOut.getSegment() ); + return this._curve; + }; + }, + + /** + * makes a graph. Only works on paths, for compound paths we need to + * make graphs for each of the child paths and merge them. + * @param {Path} path + * @param {Integer} id + * @return {Array} Links + */ + _makeGraph: function( path, id, isBaseContour ){ + var graph = []; + var segs = path.segments, prevNode = null, firstNode = null, nuLink, nuNode, + winding = path.clockwise; + for( i = 0, l = segs.length; i < l; i++ ){ + // var nuSeg = segs[i].clone(); + var nuSeg = segs[i]; + nuNode = new this._Node( nuSeg.point, nuSeg.handleIn, nuSeg.handleOut, id, isBaseContour, ++this._UNIQUE_ID ); + if( prevNode ) { + nuLink = new this._Link( prevNode, nuNode, id, isBaseContour, winding ); + graph.push( nuLink ); + } + prevNode = nuNode; + if( !firstNode ){ + firstNode = nuNode; + } + } + // the path is closed + nuLink = new this._Link( prevNode, firstNode, id, isBaseContour, winding ); + graph.push( nuLink ); + return graph; + }, + + /** + * To deal with a HTML canvas requirement where CompoundPaths' child contours + * has to be of different winding direction for correctly filling holes. + * But if some individual countours are disjoint, i.e. islands, we have to + * reorient them so that + * the holes have opposit winding direction ( already handled by paperjs ) + * islands has to have same winding direction ( as the first child of the path ) + * + * Does NOT handle selfIntersecting CompoundPaths. + * + * @param {CompoundPath} path - Input CompoundPath, Note: This path could be modified if need be. + * @return {boolean} the winding direction of the base contour( true if clockwise ) + */ + _reorientCompoundPath: function( path ){ + if( !(path instanceof CompoundPath) ){ return path.clockwise; } + var children = path.children, len = children.length, baseWinding; + var bounds = new Array( len ); + var tmparray = new Array( len ); + baseWinding = children[0].clockwise; + // Omit the first path + for (i = 0; i < len; i++) { + bounds[i] = children[i].bounds; + tmparray[i] = 0; + } + for (i = 0; i < len; i++) { + var p1 = children[i]; + for (j = 0; j < len; j++) { + var p2 = children[j]; + if( i !== j && bounds[i].contains( bounds[j] ) ){ + tmparray[j]++; + } + } + } + for (i = 1; i < len; i++) { + if ( tmparray[i] % 2 === 0 ) { + children[i].clockwise = baseWinding; + } + } + return baseWinding; + }, + + + _computeBoolean: function( _path1, _path2, operator, operatorName ){ + this._IntersectionID = 1; + this._UNIQUE_ID = 1; + // We work on duplicate paths since the algorithm may modify the original paths + var path1 = _path1.clone(); + var path2 = _path2.clone(); + var i, j, k, l, lnk, crv, node, nuNode, leftLink, rightLink; + var path1Clockwise = true, path2Clockwise = true; + // If one of the operands is empty, resolve self-intersections on the second operand + var childCount1 = (_path1 instanceof CompoundPath)? _path1.children.length : _path1.curves.length; + var childCount2 = (_path2 instanceof CompoundPath)? _path2.children.length : _path2.curves.length; + var resolveSelfIntersections = !childCount1 | !childCount2; + // Reorient the compound paths, i.e. make all the islands wind in the same direction + // and holes in the opposit direction. + // Do this only if we are not resolving selfIntersections: + // Resolving self-intersections work on compound paths, but, we might get different results! + if( !resolveSelfIntersections ){ + path1Clockwise = this._reorientCompoundPath( path1 ); + path2Clockwise = this._reorientCompoundPath( path2 ); + } + // Cache the bounding rectangle of paths + // so we can make the test for containment quite a bit faster + path1._bounds = (childCount1)? path1.bounds : null; + path2._bounds = (childCount2)? path2.bounds : null; + // Prepare the graphs. Graphs are list of Links that retains + // full connectivity information. The order of links in a graph is not important + // That allows us to sort and merge graphs and 'splice' links with their splits easily. + // Also, this is the place to resolve self-intersecting paths + var graph = [], path1Children, path2Children, base; + if( path1 instanceof CompoundPath ){ + path1Children = path1.children; + for (i = 0, base = true, l = path1Children.length; i < l; i++, base = false) { + path1Children[i].closed = true; + graph = graph.concat( this._makeGraph( path1Children[i], 1, base )); + } + } else { + path1.closed = true; + path1Clockwise = path1.clockwise; + graph = graph.concat( this._makeGraph( path1, 1, true ) ); + } + + // if operator is BooleanOps.Subtraction, then reverse path2 + // so that the nodes and links will link correctly + var reverse = ( operatorName === 'subtract' )? true: false; + path2Clockwise = (reverse)? !path2Clockwise : path2Clockwise; + if( path2 instanceof CompoundPath ){ + path2Children = path2.children; + for (i = 0, base = true, l = path2Children.length; i < l; i++, base = false) { + path2Children[i].closed = true; + if( reverse ){ path2Children[i].reverse(); } + graph = graph.concat( this._makeGraph( path2Children[i], 2, base )); + } + } else { + path2.closed = true; + if( reverse ){ path2.reverse(); } + path2Clockwise = path2.clockwise; + graph = graph.concat( this._makeGraph( path2, 2, true ) ); + } + + // Sort function to sort intersections according to the 'parameter'(t) in a link (curve) + function ixSort( a, b ){ return a.parameter - b.parameter; } + + /* + * Pass 1: + * Calculate the intersections for all graphs + */ + var ix, loc, loc2, ixCount = 0; + for ( i = graph.length - 1; i >= 0; i--) { + var c1 = graph[i].getCurve(); + var v1 = c1.getValues(); + for ( j = i -1; j >= 0; j-- ) { + if( !resolveSelfIntersections && graph[j].id === graph[i].id ){ continue; } + var c2 = graph[j].getCurve(); + var v2 = c2.getValues(); + loc = []; + Curve.getIntersections( v1, v2, c1, loc ); + if( loc.length ){ + for (k = 0, l=loc.length; k= 0; i--) { + if( graph[i].intersections.length ){ + ix = graph[i].intersections; + // Sort the intersections if there is more than one + if( graph[i].intersections.length > 1 ){ ix.sort( ixSort ); } + // Remove the graph link, this link has to be split and replaced with the splits + lnk = graph.splice( i, 1 )[0]; + nix = lnk.nodeIn.point.x; niy = lnk.nodeIn.point.y; + nox = lnk.nodeOut.point.x; noy = lnk.nodeOut.point.y; + niho = lnk.nodeIn.handleOut; nohi = lnk.nodeOut.handleIn; + nihox = nihoy = nohix = nohiy = 0; + isLinear = true; + if( niho ){ nihox = niho.x; nihoy = niho.y; isLinear = false; } + if( nohi ){ nohix = nohi.x; nohiy = nohi.y; isLinear = false; } + values = [ nix, niy, nihox + nix, nihoy + niy, + nohix + nox, nohiy + noy, nox, noy ]; + for (j =0, l=ix.length; j discard cases 1 and 2 + * * Intersection -> discard case 3 + * * Path1-Path2 -> discard cases 2, 3[Path2] + */ + // step 1: discard invalid links according to the boolean operator + for ( i = graph.length - 1; i >= 0; i-- ) { + var insidePath1 = false, insidePath2 = false, contains; + lnk = graph[i]; + // if( lnk.SKIP_OPERATOR ) { continue; } + if( !lnk.INVALID ) { + crv = lnk.getCurve(); + // var midPoint = new Point(lnk.nodeIn.point); + var midPoint = crv.getPoint( 0.5 ); + // If on a base curve, consider points on the curve and inside, + // if not —for example a hole, points on the curve falls outside + if( lnk.id !== 1 ){ + contains = path1.contains( midPoint ); + insidePath1 = (lnk.winding === path1Clockwise)? contains : + contains && !this._testOnContour( path1, midPoint ); + } + if( lnk.id !== 2 ){ + contains = path2.contains( midPoint ); + insidePath2 = (lnk.winding === path2Clockwise)? contains : + contains && !this._testOnContour( path2, midPoint ); + } + } + if( lnk.INVALID || !operator( lnk, insidePath1, insidePath2 ) ){ + // lnk = graph.splice( i, 1 )[0]; + lnk.INVALID = true; + lnk.nodeIn.linkOut = null; + lnk.nodeOut.linkIn = null; + } + } + + // step 2: Match nodes according to their _intersectionID and merge them together + var len = graph.length; + while( len-- ){ + node = graph[len].nodeIn; + if( node.type === this._INTERSECTION_NODE ){ + var otherNode = null; + for (i = len - 1; i >= 0; i--) { + var tmpnode = graph[i].nodeIn; + if( tmpnode._intersectionID === node._intersectionID && + tmpnode.uniqueID !== node.uniqueID ) { + otherNode = tmpnode; + break; + } + } + if( otherNode ) { + //Check if it is a self-intersecting _Node + if( node.id === otherNode.id ){ + // Swap the outgoing links, this will resolve a knot and create two paths, + // the portion of the original path on one side of a self crossing is counter-clockwise, + // so one of the resulting paths will also be counter-clockwise + var tmp = otherNode.linkOut; + otherNode.linkOut = node.linkOut; + node.linkOut = tmp; + tmp = otherNode.handleOut; + otherNode.handleOut = node.handleOut; + node.handleOut = tmp; + node.type = otherNode.type = this._NORMAL_NODE; + node._intersectionID = null; + node._segment = otherNode._segment = null; + } else { + // Merge the nodes together, by adding this node's information to the other node + // this node becomes a four-way node, i.e. this node will have two sets of linkIns and linkOuts each. + // In this sense this is a multi-graph! + otherNode.idB = node.id; + otherNode.isBaseContourB = node.isBaseContour; + otherNode.handleBIn = node.handleIn; + otherNode.handleBOut = node.handleOut; + otherNode.linkBIn = node.linkIn; + otherNode.linkBOut = node.linkOut; + otherNode._segment = null; + if( node.linkIn ){ node.linkIn.nodeOut = otherNode; } + if( node.linkOut ){ node.linkOut.nodeIn = otherNode; } + // Clear this node's intersectionID, so that we won't iterate over it again + node._intersectionID = null; + } + } + } + } + + window.g = graph; + + // Final step: Retrieve the resulting paths from the graph + var boolResult = new CompoundPath(); + var firstNode = true, nextNode, foundBasePath = false; + while( firstNode ){ + firstNode = nextNode = null; + len = graph.length; + while( len-- ){ + lnk = graph[len]; + if( !lnk.INVALID && !lnk.nodeIn.visited && !firstNode ){ + if( !foundBasePath && lnk.isBaseContour ){ + firstNode = lnk.nodeIn; + foundBasePath = true; + break; + } else if(foundBasePath){ + firstNode = lnk.nodeIn; + break; + } + } + } + if( firstNode ){ + var path = new Path(); + path.add( firstNode.getSegment( true ) ); + firstNode.visited = true; + nextNode = firstNode.linkOut.nodeOut; + var linkCount = graph.length + 1; + while( firstNode.uniqueID !== nextNode.uniqueID && linkCount-- ){ + path.add( nextNode.getSegment( true ) ); + nextNode.visited = true; + if( !nextNode.linkOut ){ + throw { name: 'Boolean Error', message: 'No link found at node id: ' + nextNode.id }; + } + nextNode = nextNode.linkOut.nodeOut; + } + path.closed = true; + if( path.segments.length > 1 && linkCount >= 0 ){ // avoid stray segments and incomplete paths + if( path.segments.length > 2 || !path.curves[0].isLinear() ){ + boolResult.addChild( path ); + } + } + } + } + boolResult = boolResult.reduce(); + // Remove the paths we duplicated + path1.remove(); + path2.remove(); + + // I think, we're done. + return boolResult; + }, + + + /** + * _testOnContour + * Tests if the point lies on the countour of a path + */ + _testOnContour: function( path, point ){ + var res = 0; + var crv = path.getCurves(); + var i = 0; + var bounds = path._bounds; + if( bounds && bounds.contains( point ) ){ + for( i = 0; i < crv.length && !res; i++ ){ + var crvi = crv[i]; + if( crvi.bounds.contains( point ) && crvi.getParameterOf( point ) ){ + res = 1; + } + } + } + return res; } /** @@ -289,16 +899,16 @@ var PathItem = this.PathItem = Item.extend(/** @lends PathItem# */{ * if (myPath) { * myPath.remove(); * } - * + * * // Create a new path and add a segment point to it * // at {x: 150, y: 150): * myPath = new Path(); * myPath.add(150, 150); - * + * * // Draw a curve through the position of the mouse to 'toPoint' * var toPoint = new Point(350, 150); * myPath.curveTo(event.point, toPoint); - * + * * // Select the path, so we can see its segments: * myPath.selected = true; * } From f239a3980dbc1c9a21f923c88bc38ee77f34bd47 Mon Sep 17 00:00:00 2001 From: hkrish Date: Wed, 1 May 2013 13:29:02 +0200 Subject: [PATCH 2/3] Fix: Update the getIntersections method signature in recursive calls --- src/path/Curve.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 5dd6be5f..0db6c0ca 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -379,7 +379,7 @@ var Curve = this.Curve = Base.extend(/** @lends Curve# */{ * modified and becomes the first part, the second part is returned as a new * curve. If the modified curve belongs to a path item, the second part is * added to it. - * + * * @param parameter the position at which to split the curve as a value * between 0 and 1 {@default 0.5} * @return {Curve} the second part of the divided curve @@ -396,7 +396,7 @@ var Curve = this.Curve = Base.extend(/** @lends Curve# */{ right = parts[1], point1 = this._segment1._point, point2 = this._segment2._point; - + // Write back the results: if (!isLinear) { this._segment1._handleOut.set(left[2] - point1._x, @@ -413,7 +413,7 @@ var Curve = this.Curve = Base.extend(/** @lends Curve# */{ segment = new Segment(Point.create(x, y), isLinear ? null : Point.create(left[4] - x, left[5] - y), isLinear ? null : Point.create(right[2] - x, right[3] - y)); - + // Insert it in the segments list, if needed: if (this._path) { // Insert at the end if this curve is a closing curve of a @@ -670,7 +670,7 @@ statics: { /** * Private helper for both Curve.getBounds() and Path.getBounds(), which * finds the 0-crossings of the derivative of a bezier curve polynomial, to - * determine potential extremas when finding the bounds of a curve. + * determine potential extremas when finding the bounds of a curve. * Note: padding is only used for Path.getBounds(). */ _addBounds: function(v0, v1, v2, v3, coord, padding, min, max, roots) { @@ -757,7 +757,7 @@ statics: { .intersect(new Line(v2[0], v2[1], v2[6], v2[7], false)); if (point) { // Avoid duplicates when hitting segments (closed paths too) - var first = locations[0], + var first = locations[0], last = locations[locations.length - 1]; if ((!first || !point.equals(first._point)) && (!last || !point.equals(last._point))) @@ -773,7 +773,7 @@ statics: { v2s = this.subdivide(v2); for (var i = 0; i < 2; i++) for (var j = 0; j < 2; j++) - this.getIntersections(v1s[i], v2s[j], curve1, locations); + this.getIntersections(v1s[i], v2s[j], curve1, curve2, locations); } } return locations; @@ -852,7 +852,7 @@ statics: { /** * Calculates the curve time parameter of the specified offset on the path, * relative to the provided start parameter. If offset is a negative value, - * the parameter is searched to the left of the start parameter. If no start + * the parameter is searched to the left of the start parameter. If no start * parameter is provided, a default of {@code 0} for positive values of * {@code offset} and {@code 1} for negative values of {@code offset}. * @param {Number} offset @@ -880,7 +880,7 @@ statics: { * parameter. * @param {Number} offset the offset on the curve, or the curve time * parameter if {@code isParameter} is {@code true} - * @param {Boolean} [isParameter=false] pass {@code true} if {@code offset} + * @param {Boolean} [isParameter=false] pass {@code true} if {@code offset} * is a curve time parameter. * @return {CurveLocation} the curve location at the specified the offset. */ @@ -908,7 +908,7 @@ statics: { * @function * @param {Number} offset the offset on the curve, or the curve time * parameter if {@code isParameter} is {@code true} - * @param {Boolean} [isParameter=false] pass {@code true} if {@code offset} + * @param {Boolean} [isParameter=false] pass {@code true} if {@code offset} * is a curve time parameter. * @return {Point} the point on the curve at the specified offset. */ @@ -920,7 +920,7 @@ statics: { * @function * @param {Number} offset the offset on the curve, or the curve time * parameter if {@code isParameter} is {@code true} - * @param {Boolean} [isParameter=false] pass {@code true} if {@code offset} + * @param {Boolean} [isParameter=false] pass {@code true} if {@code offset} * is a curve time parameter. * @return {Point} the tangent of the curve at the specified offset. */ @@ -932,7 +932,7 @@ statics: { * @function * @param {Number} offset the offset on the curve, or the curve time * parameter if {@code isParameter} is {@code true} - * @param {Boolean} [isParameter=false] pass {@code true} if {@code offset} + * @param {Boolean} [isParameter=false] pass {@code true} if {@code offset} * is a curve time parameter. * @return {Point} the normal of the curve at the specified offset. */ @@ -944,7 +944,7 @@ statics: { * @function * @param {Number} offset the offset on the curve, or the curve time * parameter if {@code isParameter} is {@code true} - * @param {Boolean} [isParameter=false] pass {@code true} if {@code offset} + * @param {Boolean} [isParameter=false] pass {@code true} if {@code offset} * is a curve time parameter. * @return {Point} the curvature of the curve at the specified offset. */ From 73b018f0066df0db3932025a0a267a8c8433c598 Mon Sep 17 00:00:00 2001 From: hkrish Date: Thu, 2 May 2013 13:49:07 +0200 Subject: [PATCH 3/3] Updated boolean operation methods. The algorithm is based on paperjs' native segment and curve objects rather than the generic Node and Link objects. Also this is much smaller and faster! :) --- src/path/PathItem.js | 886 +++++++++++++++---------------------------- 1 file changed, 307 insertions(+), 579 deletions(-) diff --git a/src/path/PathItem.js b/src/path/PathItem.js index f3f3e43b..1edee67a 100644 --- a/src/path/PathItem.js +++ b/src/path/PathItem.js @@ -179,184 +179,131 @@ var PathItem = this.PathItem = Item.extend(/** @lends PathItem# */{ /** - * Calculates the Union of two paths - * Boolean API. - * @param {PathItem} path - * @return {CompoundPath} union of this & path - */ - unite: function( path ){ - function UnionOp( lnk, isInsidePath1, isInsidePath2 ){ - if( isInsidePath1 || isInsidePath2 ){ return false; } - return true; - } - return this._computeBoolean( this, path, UnionOp, 'unite' ); - }, - - /** - * Calculates the Intersection between two paths - * Boolean API. - * @param {PathItem} path - * @return {CompoundPath} Intersection of this & path - */ - intersect: function( path ){ - function IntersectionOp( lnk, isInsidePath1, isInsidePath2 ){ - if( !isInsidePath1 && !isInsidePath2 ){ - return false; - } - return true; - } - return this._computeBoolean( this, path, IntersectionOp, 'intersect' ); - }, - - /** - * Calculates this path - * Boolean API. - * @param {PathItem} path - * @return {CompoundPath} this path - */ - subtract: function( path ){ - function SubtractionOp( lnk, isInsidePath1, isInsidePath2 ){ - var lnkid = lnk.id; - if( lnkid === 1 && isInsidePath2 ){ - return false; - } else if( lnkid === 2 && !isInsidePath1 ){ - return false; - } - return true; - } - return this._computeBoolean( this, path, SubtractionOp, 'subtract' ); - }, - - // Some constants - // Need to find a home for these - // for _IntersectionID and _UNIQUE_ID, we could use the Base._uid? // tried; doesn't work. - _NORMAL_NODE: 1, - _INTERSECTION_NODE: 2, - _IntersectionID: 1, - _UNIQUE_ID: 1, - - /** - * The datastructure for boolean computation: - * _Node - Connects 2 Links, represents a Segment - * _Link - Connects 2 Nodes, represents a Curve - * Graph - List of Links - */ - /** - * Nodes in the graph are analogous to Segment objects - * with additional linkage information to track intersections etc. - * (enough to do a complete graph traversal) - * @param {Point} _point - * @param {Point} _handleIn - * @param {Point} _handleOut - * @param {Any} _id + * A boolean operator is a binary operator function of the form + * f( isPath1:boolean, isInsidePath1:Boolean, isInsidePath2:Boolean ) :Boolean + * + * Boolean operator determines whether a curve segment in the operands is part + * of the boolean result, and will be called for each curve segment in the graph after + * all the intersections between the operands are calculated and curves in the operands + * are split at intersections. + * + * These functions should have a name ( "union", "subtraction" etc. below ), if we need to + * do operator specific operations on paths inside the computeBoolean function. + * for example: if the name of the operator is "subtraction" then we need to reverse the second + * operand. Subtraction is neither associative nor commutative. + * + * The boolean operator should return a Boolean value indicating whether to keep the curve or not. + * return true - keep the curve + * return false - discard the curve */ - _Node: function( _point, _handleIn, _handleOut, _id, isBaseContour, _uid ){ - var _NORMAL_NODE = 1; - var _INTERSECTION_NODE = 2; - - this.id = _id; - this.isBaseContour = isBaseContour; - this.type = _NORMAL_NODE; - this.point = _point; - this.handleIn = _handleIn; // handleIn - this.handleOut = _handleOut; // handleOut - this.linkIn = null; // aka linkIn - this.linkOut = null; // linkOut - this.uniqueID = _uid; - - // In case of an intersection this will be a merged node. - // And we need space to save the "other _Node's" parameters before merging. - this.idB = null; - this.isBaseContourB = false; - // this.pointB = this.point; // point should be the same - this.handleBIn = null; - this.handleBOut = null; - this.linkBIn = null; - this.linkBOut = null; - - this._segment = null; - - this.getSegment = function( recalculate ){ - if( this.type === _INTERSECTION_NODE && recalculate ){ - // point this.linkIn and this.linkOut to those active ones - // also point this.handleIn and this.handleOut to correct in and out handles - // If a link is null, make sure the corresponding handle is also null - this.handleIn = (this.linkIn)? this.handleIn : null; - this.handleOut = (this.linkOut)? this.handleOut : null; - this.handleBIn = (this.linkBIn)? this.handleBIn : null; - this.handleBOut = (this.linkBOut)? this.handleBOut : null; - // Select the valid links - this.linkIn = this.linkIn || this.linkBIn; // linkIn - this.linkOut = this.linkOut || this.linkBOut; // linkOut - // Also update the references in links to point to "this" _Node - if( !this.linkIn || !this.linkOut ){ - throw { name: 'Boolean Error', message: 'No matching link found at ixID: ' + - this._intersectionID + " point: " + this.point.toString() }; - } - this.linkIn.nodeOut = this; // linkIn.nodeEnd - this.linkOut.nodeIn = this; // linkOut.nodeStart - this.handleIn = this.handleIn || this.handleBIn; - this.handleOut = this.handleOut || this.handleBOut; - this.isBaseContour = this.isBaseContour | this.isBaseContourB; - } - this._segment = this._segment || new Segment( this.point, this.handleIn, this.handleOut ); - return this._segment; - }; + unite: function( path, _cache ){ + var unionOp = function union( isPath1, isInsidePath1, isInsidePath2 ){ + return ( isInsidePath1 || isInsidePath2 )? false : true; + }; + return computeBoolean( this, path, unionOp, _cache ); }, - /** - * Links in the graph are analogous to CUrve objects - * @param {_Node} _nodeIn - * @param {_Node} _nodeOut - * @param {Any} _id - */ - _Link: function( _nodeIn, _nodeOut, _id, isBaseContour, _winding ) { - this.id = _id; - this.isBaseContour = isBaseContour; - this.winding = _winding; - this.nodeIn = _nodeIn; // nodeStart - this.nodeOut = _nodeOut; // nodeEnd - this.nodeIn.linkOut = this; // nodeStart.linkOut - this.nodeOut.linkIn = this; // nodeEnd.linkIn - this._curve = null; - this.intersections = []; - - // for reusing the paperjs function we need to (temperorily) build a Curve object from this _Link - // for performance reasons we cache it. - this.getCurve = function() { - this._curve = this._curve || new Curve( this.nodeIn.getSegment(), this.nodeOut.getSegment() ); - return this._curve; - }; + intersect: function( path, _cache ){ + var intersectionOp = function intersection( isPath1, isInsidePath1, isInsidePath2 ){ + return ( !isInsidePath1 && !isInsidePath2 )? false : true; + }; + return computeBoolean( this, path, intersectionOp, _cache ); }, - /** - * makes a graph. Only works on paths, for compound paths we need to - * make graphs for each of the child paths and merge them. - * @param {Path} path - * @param {Integer} id - * @return {Array} Links + subtract: function( path, _cache ){ + var subtractionOp = function subtraction( isPath1, isInsidePath1, isInsidePath2 ){ + return ( (isPath1 && isInsidePath2) || (!isPath1 && !isInsidePath1) )? false : true; + }; + return computeBoolean( this, path, subtractionOp, _cache ); + }, + + /* + * Compound boolean operators combine the basic boolean operations such as union, intersection, + * subtract etc. + * + * TODO: cache the split objects and find a way to properly clone them! */ - _makeGraph: function( path, id, isBaseContour ){ - var graph = []; - var segs = path.segments, prevNode = null, firstNode = null, nuLink, nuNode, - winding = path.clockwise; - for( i = 0, l = segs.length; i < l; i++ ){ - // var nuSeg = segs[i].clone(); - var nuSeg = segs[i]; - nuNode = new this._Node( nuSeg.point, nuSeg.handleIn, nuSeg.handleOut, id, isBaseContour, ++this._UNIQUE_ID ); - if( prevNode ) { - nuLink = new this._Link( prevNode, nuNode, id, isBaseContour, winding ); - graph.push( nuLink ); - } - prevNode = nuNode; - if( !firstNode ){ - firstNode = nuNode; - } - } - // the path is closed - nuLink = new this._Link( prevNode, firstNode, id, isBaseContour, winding ); - graph.push( nuLink ); - return graph; + // a.k.a. eXclusiveOR + exclude: function( path ){ + var res1 = this.subtract( path ); + var res2 = path.subtract( this ); + var res = new Group( [res1, res2] ); + return res; + }, + + // Divide path1 by path2 + divide: function( path ){ + var res1 = this.subtract( path ); + var res2 = this.intersect( path ); + var res = new Group( [res1, res2] ); + return res; + }, + + _splitPath: function( _ixs, other ) { + // Sort function for sorting intersections in the descending order + function sortIx( a, b ) { return b.parameter - a.parameter; } + other = other || false; + var i, j, k, l, len, ixs, ix, path, crv, vals; + var ixPoint, nuSeg; + var paths = {}, lastPathId = null; + for (i = 0, l = _ixs.length; i < l; i++) { + ix = ( other )? _ixs[i].getIntersection() : _ixs[i]; + if( !paths[ix.path.id] ){ + paths[ix.path.id] = ix.path; + } + if( !ix.curve._ixParams ){ix.curve._ixParams = []; } + ix.curve._ixParams.push( { parameter: ix.parameter, pair: ix.getIntersection() } ); + } + for (k in paths) { + if( !paths.hasOwnProperty( k ) ){ continue; } + path = paths[k]; + var lastNode = path.lastSegment, firstNode = path.firstSegment; + var nextNode = null, left = null, right = null, parts = null, isLinear; + var handleIn, handleOut; + while( nextNode !== firstNode){ + nextNode = ( nextNode )? nextNode.previous: lastNode; + if( nextNode.curve._ixParams ){ + ixs = nextNode.curve._ixParams; + ixs.sort( sortIx ); + crv = nextNode.getCurve(); + isLinear = crv.isLinear(); + crv = vals = null; + for (i = 0, l = ixs.length; i < l; i++) { + ix = ixs[i]; + crv = nextNode.getCurve(); + if( !vals ) vals = crv.getValues(); + if( ix.parameter === 0.0 || ix.parameter === 1.0 ){ + // Intersection is on an existing node + // no need to create a new segment, + // we just link the corresponding intersections together + nuSeg = ( ix.parameter === 0.0 )? crv.segment1 : crv.segment2; + nuSeg._ixPair = ix.pair; + nuSeg._ixPair._segment = nuSeg; + } else { + parts = Curve.subdivide( vals, ix.parameter ); + left = parts[0]; + right = parts[1]; + handleIn = handleOut = null; + ixPoint = new Point( right[0], right[1] ); + if( !isLinear ){ + crv.segment1.handleOut = new Point( left[2] - left[0], left[3] - left[1] ); + crv.segment2.handleIn = new Point( right[4] - right[6], right[5] - right[7] ); + handleIn = new Point( left[4] - ixPoint.x, left[5] - ixPoint.y ); + handleOut = new Point( right[2] - ixPoint.x, right[3] - ixPoint.y ); + } + nuSeg = new Segment( ixPoint, handleIn, handleOut ); + nuSeg._ixPair = ix.pair; + nuSeg._ixPair._segment = nuSeg; + path.insert( nextNode.index + 1, nuSeg ); + } + for (j = i + 1; j < l; j++) { + ixs[j].parameter = ixs[j].parameter / ix.parameter; + } + vals = left; + } + } + } + } }, /** @@ -373,420 +320,201 @@ var PathItem = this.PathItem = Item.extend(/** @lends PathItem# */{ * @return {boolean} the winding direction of the base contour( true if clockwise ) */ _reorientCompoundPath: function( path ){ - if( !(path instanceof CompoundPath) ){ return path.clockwise; } - var children = path.children, len = children.length, baseWinding; - var bounds = new Array( len ); - var tmparray = new Array( len ); - baseWinding = children[0].clockwise; - // Omit the first path - for (i = 0; i < len; i++) { - bounds[i] = children[i].bounds; - tmparray[i] = 0; - } - for (i = 0; i < len; i++) { - var p1 = children[i]; - for (j = 0; j < len; j++) { - var p2 = children[j]; - if( i !== j && bounds[i].contains( bounds[j] ) ){ - tmparray[j]++; - } - } - } - for (i = 1; i < len; i++) { - if ( tmparray[i] % 2 === 0 ) { - children[i].clockwise = baseWinding; - } - } - return baseWinding; + if( !(path instanceof CompoundPath) ){ + path.closed = true; + return path.clockwise; + } + var children = path.children, len = children.length, baseWinding; + var bounds = new Array( len ); + var tmparray = new Array( len ); + baseWinding = children[0].clockwise; + // Omit the first path + for (i = 0; i < len; i++) { + children[i].closed = true; + bounds[i] = children[i].bounds; + tmparray[i] = 0; + } + for (i = 0; i < len; i++) { + var p1 = children[i]; + for (j = 0; j < len; j++) { + var p2 = children[j]; + if( i !== j && bounds[i].contains( bounds[j] ) ){ + tmparray[j]++; + } + } + } + for (i = 1; i < len; i++) { + if ( tmparray[i] % 2 === 0 ) { + children[i].clockwise = baseWinding; + } + } + return baseWinding; }, - - _computeBoolean: function( _path1, _path2, operator, operatorName ){ - this._IntersectionID = 1; - this._UNIQUE_ID = 1; - // We work on duplicate paths since the algorithm may modify the original paths - var path1 = _path1.clone(); - var path2 = _path2.clone(); - var i, j, k, l, lnk, crv, node, nuNode, leftLink, rightLink; - var path1Clockwise = true, path2Clockwise = true; - // If one of the operands is empty, resolve self-intersections on the second operand - var childCount1 = (_path1 instanceof CompoundPath)? _path1.children.length : _path1.curves.length; - var childCount2 = (_path2 instanceof CompoundPath)? _path2.children.length : _path2.curves.length; - var resolveSelfIntersections = !childCount1 | !childCount2; - // Reorient the compound paths, i.e. make all the islands wind in the same direction - // and holes in the opposit direction. - // Do this only if we are not resolving selfIntersections: - // Resolving self-intersections work on compound paths, but, we might get different results! - if( !resolveSelfIntersections ){ - path1Clockwise = this._reorientCompoundPath( path1 ); - path2Clockwise = this._reorientCompoundPath( path2 ); - } - // Cache the bounding rectangle of paths - // so we can make the test for containment quite a bit faster - path1._bounds = (childCount1)? path1.bounds : null; - path2._bounds = (childCount2)? path2.bounds : null; - // Prepare the graphs. Graphs are list of Links that retains - // full connectivity information. The order of links in a graph is not important - // That allows us to sort and merge graphs and 'splice' links with their splits easily. - // Also, this is the place to resolve self-intersecting paths - var graph = [], path1Children, path2Children, base; - if( path1 instanceof CompoundPath ){ - path1Children = path1.children; - for (i = 0, base = true, l = path1Children.length; i < l; i++, base = false) { - path1Children[i].closed = true; - graph = graph.concat( this._makeGraph( path1Children[i], 1, base )); - } - } else { - path1.closed = true; - path1Clockwise = path1.clockwise; - graph = graph.concat( this._makeGraph( path1, 1, true ) ); - } - - // if operator is BooleanOps.Subtraction, then reverse path2 - // so that the nodes and links will link correctly - var reverse = ( operatorName === 'subtract' )? true: false; - path2Clockwise = (reverse)? !path2Clockwise : path2Clockwise; - if( path2 instanceof CompoundPath ){ - path2Children = path2.children; - for (i = 0, base = true, l = path2Children.length; i < l; i++, base = false) { - path2Children[i].closed = true; - if( reverse ){ path2Children[i].reverse(); } - graph = graph.concat( this._makeGraph( path2Children[i], 2, base )); - } - } else { - path2.closed = true; - if( reverse ){ path2.reverse(); } - path2Clockwise = path2.clockwise; - graph = graph.concat( this._makeGraph( path2, 2, true ) ); - } - - // Sort function to sort intersections according to the 'parameter'(t) in a link (curve) - function ixSort( a, b ){ return a.parameter - b.parameter; } - - /* - * Pass 1: - * Calculate the intersections for all graphs - */ - var ix, loc, loc2, ixCount = 0; - for ( i = graph.length - 1; i >= 0; i--) { - var c1 = graph[i].getCurve(); - var v1 = c1.getValues(); - for ( j = i -1; j >= 0; j-- ) { - if( !resolveSelfIntersections && graph[j].id === graph[i].id ){ continue; } - var c2 = graph[j].getCurve(); - var v2 = c2.getValues(); - loc = []; - Curve.getIntersections( v1, v2, c1, loc ); - if( loc.length ){ - for (k = 0, l=loc.length; k= 0; i--) { - if( graph[i].intersections.length ){ - ix = graph[i].intersections; - // Sort the intersections if there is more than one - if( graph[i].intersections.length > 1 ){ ix.sort( ixSort ); } - // Remove the graph link, this link has to be split and replaced with the splits - lnk = graph.splice( i, 1 )[0]; - nix = lnk.nodeIn.point.x; niy = lnk.nodeIn.point.y; - nox = lnk.nodeOut.point.x; noy = lnk.nodeOut.point.y; - niho = lnk.nodeIn.handleOut; nohi = lnk.nodeOut.handleIn; - nihox = nihoy = nohix = nohiy = 0; - isLinear = true; - if( niho ){ nihox = niho.x; nihoy = niho.y; isLinear = false; } - if( nohi ){ nohix = nohi.x; nohiy = nohi.y; isLinear = false; } - values = [ nix, niy, nihox + nix, nihoy + niy, - nohix + nox, nohiy + noy, nox, noy ]; - for (j =0, l=ix.length; j discard cases 1 and 2 - * * Intersection -> discard case 3 - * * Path1-Path2 -> discard cases 2, 3[Path2] - */ - // step 1: discard invalid links according to the boolean operator - for ( i = graph.length - 1; i >= 0; i-- ) { - var insidePath1 = false, insidePath2 = false, contains; - lnk = graph[i]; - // if( lnk.SKIP_OPERATOR ) { continue; } - if( !lnk.INVALID ) { - crv = lnk.getCurve(); - // var midPoint = new Point(lnk.nodeIn.point); - var midPoint = crv.getPoint( 0.5 ); - // If on a base curve, consider points on the curve and inside, - // if not —for example a hole, points on the curve falls outside - if( lnk.id !== 1 ){ - contains = path1.contains( midPoint ); - insidePath1 = (lnk.winding === path1Clockwise)? contains : - contains && !this._testOnContour( path1, midPoint ); - } - if( lnk.id !== 2 ){ - contains = path2.contains( midPoint ); - insidePath2 = (lnk.winding === path2Clockwise)? contains : - contains && !this._testOnContour( path2, midPoint ); - } - } - if( lnk.INVALID || !operator( lnk, insidePath1, insidePath2 ) ){ - // lnk = graph.splice( i, 1 )[0]; - lnk.INVALID = true; - lnk.nodeIn.linkOut = null; - lnk.nodeOut.linkIn = null; - } - } - - // step 2: Match nodes according to their _intersectionID and merge them together - var len = graph.length; - while( len-- ){ - node = graph[len].nodeIn; - if( node.type === this._INTERSECTION_NODE ){ - var otherNode = null; - for (i = len - 1; i >= 0; i--) { - var tmpnode = graph[i].nodeIn; - if( tmpnode._intersectionID === node._intersectionID && - tmpnode.uniqueID !== node.uniqueID ) { - otherNode = tmpnode; - break; - } - } - if( otherNode ) { - //Check if it is a self-intersecting _Node - if( node.id === otherNode.id ){ - // Swap the outgoing links, this will resolve a knot and create two paths, - // the portion of the original path on one side of a self crossing is counter-clockwise, - // so one of the resulting paths will also be counter-clockwise - var tmp = otherNode.linkOut; - otherNode.linkOut = node.linkOut; - node.linkOut = tmp; - tmp = otherNode.handleOut; - otherNode.handleOut = node.handleOut; - node.handleOut = tmp; - node.type = otherNode.type = this._NORMAL_NODE; - node._intersectionID = null; - node._segment = otherNode._segment = null; - } else { - // Merge the nodes together, by adding this node's information to the other node - // this node becomes a four-way node, i.e. this node will have two sets of linkIns and linkOuts each. - // In this sense this is a multi-graph! - otherNode.idB = node.id; - otherNode.isBaseContourB = node.isBaseContour; - otherNode.handleBIn = node.handleIn; - otherNode.handleBOut = node.handleOut; - otherNode.linkBIn = node.linkIn; - otherNode.linkBOut = node.linkOut; - otherNode._segment = null; - if( node.linkIn ){ node.linkIn.nodeOut = otherNode; } - if( node.linkOut ){ node.linkOut.nodeIn = otherNode; } - // Clear this node's intersectionID, so that we won't iterate over it again - node._intersectionID = null; - } - } - } - } - - window.g = graph; - - // Final step: Retrieve the resulting paths from the graph - var boolResult = new CompoundPath(); - var firstNode = true, nextNode, foundBasePath = false; - while( firstNode ){ - firstNode = nextNode = null; - len = graph.length; - while( len-- ){ - lnk = graph[len]; - if( !lnk.INVALID && !lnk.nodeIn.visited && !firstNode ){ - if( !foundBasePath && lnk.isBaseContour ){ - firstNode = lnk.nodeIn; - foundBasePath = true; - break; - } else if(foundBasePath){ - firstNode = lnk.nodeIn; - break; - } - } - } - if( firstNode ){ - var path = new Path(); - path.add( firstNode.getSegment( true ) ); - firstNode.visited = true; - nextNode = firstNode.linkOut.nodeOut; - var linkCount = graph.length + 1; - while( firstNode.uniqueID !== nextNode.uniqueID && linkCount-- ){ - path.add( nextNode.getSegment( true ) ); - nextNode.visited = true; - if( !nextNode.linkOut ){ - throw { name: 'Boolean Error', message: 'No link found at node id: ' + nextNode.id }; - } - nextNode = nextNode.linkOut.nodeOut; - } - path.closed = true; - if( path.segments.length > 1 && linkCount >= 0 ){ // avoid stray segments and incomplete paths - if( path.segments.length > 2 || !path.curves[0].isLinear() ){ - boolResult.addChild( path ); - } - } - } - } - boolResult = boolResult.reduce(); - // Remove the paths we duplicated - path1.remove(); - path2.remove(); - - // I think, we're done. - return boolResult; + reversePath: function( path ){ + var baseWinding; + if( path instanceof CompoundPath ){ + var children = path.children, i, len; + for (i = 0, len = children.length; i < len; i++) { + children[i].reverse(); + children[i]._curves = null; + } + baseWinding = children[0].clockwise; + } else { + path.reverse(); + baseWinding = path.clockwise; + path._curves = null; + } + return baseWinding; }, + _computeBoolean: function( path1, path2, operator, _splitCache ){ + var _path1, _path2, path1Clockwise, path2Clockwise; + var ixs, path1Id, path2Id; + // We do not modify the operands themselves + // The result might not belong to the same type + // i.e. subtraction( A:Path, B:Path ):CompoundPath etc. + _path1 = path1.clone(); + _path2 = path2.clone(); + _path1.style = _path2.style = null; + _path1.selected = _path2.selected = false; + path1Clockwise = _reorientCompoundPath( _path1 ); + path2Clockwise = _reorientCompoundPath( _path2 ); + path1Id = _path1.id; + path2Id = _path2.id; + // Calculate all the intersections + ixs = ( _splitCache && _splitCache.intersections )? + _splitCache.intersections : _path1.getIntersections( _path2 ); + // if we have a empty _splitCache object as an operand, + // skip calculating boolean and cache the intersections + if( _splitCache && !_splitCache.intersections ){ + _splitCache.intersections = ixs; + return; + } + _splitPath( ixs ); + _splitPath( ixs, true ); + path1Id = _path1.id; + path2Id = _path2.id; + // Do operator specific calculations before we begin + if( operator.name === "subtraction" ) { + path2Clockwise = _reversePath( _path2 ); + } - /** - * _testOnContour - * Tests if the point lies on the countour of a path - */ - _testOnContour: function( path, point ){ - var res = 0; - var crv = path.getCurves(); - var i = 0; - var bounds = path._bounds; - if( bounds && bounds.contains( point ) ){ - for( i = 0; i < crv.length && !res; i++ ){ - var crvi = crv[i]; - if( crvi.bounds.contains( point ) && crvi.getParameterOf( point ) ){ - res = 1; - } - } - } - return res; + var i, j, len, path, crv; + var paths = []; + if( _path1 instanceof CompoundPath ){ + paths = paths.concat( _path1.children ); + } else { + paths = [ _path1 ]; + } + if( _path2 instanceof CompoundPath ){ + paths = paths.concat( _path2.children ); + } else { + paths.push( _path2 ); + } + // step 1: discard invalid links according to the boolean operator + var lastNode, firstNode, nextNode, midPoint, insidePath1, insidePath2; + var thisId, thisWinding, contains, subtractionOp = (operator.name === 'subtraction'); + for (i = 0, len = paths.length; i < len; i++) { + insidePath1 = insidePath2 = false; + path = paths[i]; + thisId = ( path.parent instanceof CompoundPath )? path.parent.id : path.id; + thisWinding = path.clockwise; + lastNode = path.lastSegment; + firstNode = path.firstSegment; + nextNode = null; + while( nextNode !== firstNode){ + nextNode = ( nextNode )? nextNode.previous: lastNode; + crv = nextNode.curve; + midPoint = crv.getPoint( 0.5 ); + if( thisId !== path1Id ){ + contains = _path1.contains( midPoint ); + insidePath1 = (thisWinding === path1Clockwise || subtractionOp )? contains : + contains && !_testOnCurve( _path1, midPoint ); + } + if( thisId !== path2Id ){ + contains = _path2.contains( midPoint ); + insidePath2 = (thisWinding === path2Clockwise )? contains : + contains && !_testOnCurve( _path2, midPoint ); + } + if( !operator( thisId === path1Id, insidePath1, insidePath2 ) ){ + crv._INVALID = true; + // markPoint( midPoint, '+' ); + } + } + } + + // Final step: Retrieve the resulting paths from the graph + var boolResult = new CompoundPath(); + var node, nuNode, nuPath, nodeList = [], handle; + for (i = 0, len = paths.length; i < len; i++) { + nodeList = nodeList.concat( paths[i].segments ); + } + for (i = 0, len = nodeList.length; i < len; i++) { + node = nodeList[i]; + if( node.curve._INVALID || node._visited ){ continue; } + path = node.path; + thisId = ( path.parent instanceof CompoundPath )? path.parent.id : path.id; + thisWinding = path.clockwise; + nuPath = new Path(); + firstNode = null; + firstNode_ix = null; + if( node.previous.curve._INVALID ) { + node.handleIn = ( node._ixPair )? + node._ixPair.getIntersection()._segment.handleIn : [ 0, 0 ]; + } + while( node && !node._visited && ( node !== firstNode && node !== firstNode_ix ) ){ + node._visited = true; + firstNode = ( firstNode )? firstNode: node; + firstNode_ix = ( !firstNode_ix && firstNode._ixPair )? + firstNode._ixPair.getIntersection()._segment: firstNode_ix; + // node._ixPair is this node's intersection CurveLocation object + // node._ixPair.getIntersection() is the other CurveLocation object this node intersects with + nextNode = ( node._ixPair && node.curve._INVALID )? node._ixPair.getIntersection()._segment : node; + if( node._ixPair ) { + nextNode._visited = true; + nuNode = new Segment( node.point, node.handleIn, nextNode.handleOut ); + nuPath.add( nuNode ); + node = nextNode; + path = node.path; + thisWinding = path.clockwise; + } else { + nuPath.add( node ); + } + node = node.next; + } + if( nuPath.segments.length > 1 ) { + // avoid stray segments and incomplete paths + if( nuPath.segments.length > 2 || !nuPath.curves[0].isLinear() ){ + nuPath.closed = true; + boolResult.addChild( nuPath, true ); + } + } + } + // Delete the proxies + _path1.remove(); + _path2.remove(); + // And then, we are done. + return boolResult.reduce(); + }, + + _testOnCurve: function( path, point ){ + var res = 0; + var crv = path.getCurves(); + var i = 0; + var bounds = path.bounds; + if( bounds && bounds.contains( point ) ){ + for( i = 0; i < crv.length && !res; i++ ){ + var crvi = crv[i]; + if( crvi.bounds.contains( point ) && crvi.getParameterOf( point ) ){ + res = 1; + } + } + } + return res; } + /** * Smooth bezier curves without changing the amount of segments or their * points, by only smoothing and adjusting their handle points, for both