function sortIx( a, b ) { return b.parameter - a.parameter; } function splitPath( _ixs, other ) { 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; } } } } } /** * 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 ) */ function reorientCompoundPath( path ){ 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; } function reversePath( 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; } function computeBoolean( 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 ); } 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(); // nuPath.selected = true; 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 ); } // view.draw() node = node.next; } nuPath.closed = true; boolResult.addChild( nuPath, true ); } // if( operator.name === 'intersection' ){ // window.p = boolResult.reduce(); // } // window.a = _path1; // window.b = _path2; // Delete the proxies // _path1.remove(); // _path2.remove(); // And then, we are done. return boolResult.reduce(); } // Bottleneck no: 2 function testOnCurve( 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; } /** * 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 */ function unite( path1, path2, _cache ){ var unionOp = function union( isPath1, isInsidePath1, isInsidePath2 ){ return ( isInsidePath1 || isInsidePath2 )? false : true; }; return computeBoolean( path1, path2, unionOp, _cache ); } function intersect( path1, path2, _cache ){ var intersectionOp = function intersection( isPath1, isInsidePath1, isInsidePath2 ){ return ( !isInsidePath1 && !isInsidePath2 )? false : true; }; return computeBoolean( path1, path2, intersectionOp, _cache ); } function subtract( path1, path2, _cache ){ var subtractionOp = function subtraction( isPath1, isInsidePath1, isInsidePath2 ){ return ( (isPath1 && isInsidePath2) || (!isPath1 && !isInsidePath1) )? false : true; }; return computeBoolean( path1, path2, subtractionOp, _cache ); } // a.k.a. eXclusiveOR function exclude( path1, path2 ){ var cache = null; // computeBoolean( path1, path2, null, cache ); var res1 = subtract( path1, path2, cache ); var res2 = subtract( path2, path1, cache ); res1 = new CompoundPath( res1.children, res2.children ); return res1; }