/*! * * Vector boolean operations on paperjs objects * This is mostly written for clarity (I hope it is clear) and compatibility, * not optimised for performance, and has to be tested heavily for stability. * (Looking up to Java's Area path boolean algorithms for stability, * but the code is too complex —mainly because the operations are stored and * enumerable, such as quadraticCurveTo, cubicCurveTo etc.; and is largely * undocumented to directly adapt from) * * Supported * - paperjs Path objects * - Boolean Union operations * - Boolean Intersection operations * - handles path complexity quite nicely * * Not supported yet ( which I would like to see supported ) * - Compound Paths as input ( however compound paths are correctly handled in the output ) * - Self-intersecting Paths * - Boolean Subtraction operation ( depends on compound paths as input ) * - Paths are clones of each other that ovelap exactly on top of each other! * * In the Not-supported-yet list, the first three can be easily implemented, * as for the last point, I need help! Thanks! :) * * ------ * Harikrishnan Gopalakrishnan * http://hkrish.com/playground/paperbool.html * * ------ * Paperjs * Copyright (c) 2011, Juerg Lehni & Jonathan Puckey * http://paperjs.org/license/ * */ /** * BooleanOps defines the boolean operator functions to use. * A boolean operator is a function f( link:Link, isInsidePath1:Boolean, isInsidePath2:Boolean ) : * should return a Boolean value indicating whether to keep the link or not. * return true - keep the path * return false - discard the path */ var BooleanOps = { Union: function( lnk, isInsidePath1, isInsidePath2 ){ if( isInsidePath1 || isInsidePath2 ){ return false; } return true; }, Intersection: function( lnk, isInsidePath1, isInsidePath2 ){ if( !isInsidePath1 && !isInsidePath2 ){ return false; } return true; } }; /** * The datastructure for boolean computation: * Graph - List of Links * Link - Connects 2 Nodes, represents a Curve * Node - Connects 2 Links, represents a Segment */ var NORMAL_NODE = 1; var INTERSECTION_NODE = 2; var IntersectionID = 1; var UNIQUE_ID = 1; /** * 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 */ function Node( _point, _handleIn, _handleOut, _id, _childId ){ this.id = _id; this.childId = _childId; 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 = ++UNIQUE_ID; // 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.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 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._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 */ function Link( _nodeIn, _nodeOut, _id, _childId ) { this.id = _id; this.childId = _childId; 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 */ function makeGraph( path, id, childId ){ var graph = []; var segs = path.segments, prevNode = null, firstNode = null, nuLink, nuNode; for( i = 0, l = segs.length; i < l; i++ ){ var nuSeg = segs[i].clone(); nuNode = new Node( nuSeg.point, nuSeg.handleIn, nuSeg.handleOut, id, childId ); if( prevNode ) { nuLink = new Link( prevNode, nuNode, id, childId ); graph.push( nuLink ); } prevNode = nuNode; if( !firstNode ){ firstNode = nuNode; } } // the path is closed nuLink = new Link( prevNode, firstNode, id, childId ); graph.push( nuLink ); return graph; } /** * Calculates the Union of two paths * Boolean API. * @param {Path} path1 * @param {Path} path2 * @return {CompoundPath} union of path1 & path2 */ function boolUnion( path1, path2 ){ return computeBoolean( path1, path2, BooleanOps.Union ); } /** * Calculates the Intersection between two paths * Boolean API. * @param {Path} path1 * @param {Path} path2 * @return {CompoundPath} Intersection of path1 & path2 */ function boolIntersection( path1, path2 ){ return computeBoolean( path1, path2, BooleanOps.Intersection ); } /** * Actual function that computes the boolean * @param {Path} _path1 (cannot be self-intersecting at the moment) * @param {Path} _path2 (cannot be self-intersecting at the moment) * @param {BooleanOps type} operator * @return {CompoundPath} boolean result */ function computeBoolean( _path1, _path2, operator ){ IntersectionID = 1; UNIQUE_ID = 1; // The boolean operation may modify the original paths var path1 = _path1.clone(); var path2 = _path2.clone(); // if( !path1.clockwise ){ path1.reverse(); } // if( !path2.clockwise ){ path2.reverse(); } // var i, j, k, l, lnk, crv, node, nuNode, leftLink, rightLink; // 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; if( path1 instanceof CompoundPath ){ path1Children = path1.children; for (i = 0, l = path1Children.length; i < l; i++) { path1Children[i].closed = true; graph = graph.concat( makeGraph( path1Children[i], 1, i + 1 ) ); } } else { path1.closed = true; path1.clockwise = true; graph = graph.concat( makeGraph( path1, 1, 1 ) ); } // TODO: if operator === BooleanOps.subtract, then for path2, clockwise must be false if( path2 instanceof CompoundPath ){ path2Children = path2.children; for (i = 0, l = path2Children.length; i < l; i++) { path2Children[i].closed = true; graph = graph.concat( makeGraph( path2Children[i], 2, i + 1 ) ); } } else { path2.closed = true; path2.clockwise = true; graph = graph.concat( makeGraph( path2, 2, 1 ) ); } window.g = graph // 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 * TODO: test if this takes are of self intersecting paths - NO * And since it doesn't take self-intersecting curves, we need to only calculate * intersections if the "id" of the links differ. * The rest of the algorithm can easily be modified to resolve self-intersections */ 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( graph[j].id === graph[i].id ){ continue; } var c2 = graph[j].getCurve(); var v2 = c2.getValues(); var loc = []; Curve._addIntersections( v1, v2, loc ); if( loc.length ){ for (k = 0, l=loc.length; k= 0; i--) { if( graph[i].intersections.length ){ var ix = graph[i].intersections; ix.sort( ixSort ); // Remove the graph link, this link has to be split and replaced with the splits lnk = graph.splice( i, 1 )[0]; for (j =0, l=ix.length; j discard cases 1, 2 and 3 * * Intersection -> discard case 4 * * Path1-Path2 -> discard cases 2, 3[Path1] and 4[Path2]‡ * * Path2-Path1 -> discard cases 1, 3[Path2] and 4[Path1] * ‡ - 4[Path2] means curves of case 4 that belongs to Path2 */ // step 1: discard invalid links according to the boolean operator for ( i = graph.length - 1; i >= 0; i--) { lnk = graph[i]; crv = lnk.getCurve(); // var midPoint = new Point(lnk.nodeIn.point); var midPoint = crv.getPoint( 0.5 ); var insidePath1 = (lnk.id === 1 )? false : path1.contains( midPoint ); var insidePath2 = (lnk.id === 2 )? false : path2.contains( midPoint ); if( !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 === 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 = 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 otherNode.idB = node.id; 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; } } } } // Final step: Retrieve the resulting paths from the graph // TODO: start from a path where childId === 1 var boolResult = new CompoundPath(); var firstNode = true, nextNode, foundBasePath = false; while( firstNode ){ firstNode = nextNode = null; len = graph.length; while( len-- ){ if( !graph[len].INVALID && !graph[len].nodeIn.visited && !firstNode ){ if( !foundBasePath && graph[len].childId === 1 ){ firstNode = graph[len].nodeIn; foundBasePath = true; break; } else if(foundBasePath){ firstNode = graph[len].nodeIn; break; } } } if( firstNode ){ var path = new Path(); path.add( firstNode.getSegment( true ) ); firstNode.visited = true; nextNode = firstNode.linkOut.nodeOut; while( firstNode.uniqueID !== nextNode.uniqueID ){ path.add( nextNode.getSegment( true ) ); nextNode.visited = true; nextNode = nextNode.linkOut.nodeOut; } path.closed = true; // path.clockwise = true; boolResult.addChild( path ); } } boolResult = boolResult.reduce(); return boolResult; } function markPoint( pnt, t, c, tc, remove ) { if( !pnt ) return; c = c || '#000'; if( remove === undefined ){ remove = true; } var cir = new Path.Circle( pnt, 2 ); cir.style.fillColor = c; cir.style.strokeColor = tc; if( t !== undefined || t !== null ){ var text = new PointText( pnt.add([0, -3]) ); text.justification = 'center'; text.fillColor = c; text.content = t; if( remove ){ text.removeOnMove(); } } if( remove ) { cir.removeOnMove(); } } // Same as the paperjs' Numerical class, // added here because I can't access the original from this scope var Numerical = { TOLERANCE : 10e-6 }; // paperjs' Curve._addIntersections modified to return just intersection Point with a // unique id. paper.Curve._addIntersections = function(v1, v2, locations) { var bounds1 = Curve.getBounds(v1), bounds2 = Curve.getBounds(v2); if (bounds1.touches(bounds2)) { // See if both curves are flat enough to be treated as lines. if (Curve.isFlatEnough(v1, /*#=*/ Numerical.TOLERANCE) && Curve.isFlatEnough(v2, /*#=*/ Numerical.TOLERANCE)) { // See if the parametric equations of the lines interesct. var point = new Line(v1[0], v1[1], v1[6], v1[7], false) .intersect(new Line(v2[0], v2[1], v2[6], v2[7], false), // Filter out beginnings of the curves, to avoid // duplicate solutions where curves join. true, false); if (point){ point._intersectionID = IntersectionID++; locations.push( point ); } } else { // Subdivide both curves, and see if they intersect. var v1s = Curve.subdivide(v1), v2s = Curve.subdivide(v2); for (var i = 0; i < 2; i++) for (var j = 0; j < 2; j++) this._addIntersections(v1s[i], v2s[j], locations); } } return locations; };