diff --git a/src/path/Curve.js b/src/path/Curve.js index 12455a9c..6cb87ad8 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) { @@ -759,7 +759,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))) @@ -857,7 +857,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 @@ -885,7 +885,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. */ @@ -913,7 +913,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. */ @@ -925,7 +925,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. */ @@ -937,7 +937,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. */ @@ -949,7 +949,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. */ diff --git a/src/path/PathItem.js b/src/path/PathItem.js index 8ffc87a0..29f8aa02 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, @@ -89,7 +89,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) @@ -175,8 +175,347 @@ var PathItem = this.PathItem = Item.extend(/** @lends PathItem# */{ break; } } + }, + + + /** + * 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 + */ + unite: function( path, _cache ){ + var unionOp = function union( isPath1, isInsidePath1, isInsidePath2 ){ + return ( isInsidePath1 || isInsidePath2 )? false : true; + }; + return this._computeBoolean( this, path, unionOp, _cache ); + }, + + intersect: function( path, _cache ){ + var intersectionOp = function intersection( isPath1, isInsidePath1, isInsidePath2 ){ + return ( !isInsidePath1 && !isInsidePath2 )? false : true; + }; + return this._computeBoolean( this, path, intersectionOp, _cache ); + }, + + subtract: function( path, _cache ){ + var subtractionOp = function subtraction( isPath1, isInsidePath1, isInsidePath2 ){ + return ( (isPath1 && isInsidePath2) || (!isPath1 && !isInsidePath1) )? false : true; + }; + return this._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! + */ + // 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; + } + } + } + } + }, + + /** + * 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) ){ + 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; + }, + + _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 = this._reorientCompoundPath( _path1 ); + path2Clockwise = this._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; + } + this._splitPath( ixs ); + this._splitPath( ixs, true ); + path1Id = _path1.id; + path2Id = _path2.id; + // Do operator specific calculations before we begin + if( operator.name === "subtraction" ) { + path2Clockwise = this._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 && !this._testOnCurve( _path1, midPoint ); + } + if( thisId !== path2Id ){ + contains = _path2.contains( midPoint ); + insidePath2 = (thisWinding === path2Clockwise )? contains : + contains && !this._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 @@ -290,16 +629,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; * }