Merge pull request #211 from hkrish/master

Merge in the possibly smallest Boolean Operations code ever written = )
This commit is contained in:
Jürg Lehni 2013-05-03 15:16:23 -07:00
commit 7d54bb36b1
2 changed files with 358 additions and 19 deletions

View file

@ -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.
*/

View file

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