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