mirror of
https://github.com/scratchfoundation/paper.js.git
synced 2025-01-03 19:45:44 -05:00
Updated boolean operation methods.
The algorithm is based on paperjs' native segment and curve objects rather than the generic Node and Link objects. Also this is much smaller and faster! :)
This commit is contained in:
parent
934ec8df7e
commit
381ee98cbc
1 changed files with 307 additions and 579 deletions
|
@ -179,184 +179,131 @@ var PathItem = this.PathItem = Item.extend(/** @lends PathItem# */{
|
|||
|
||||
|
||||
/**
|
||||
* 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 <minus> path
|
||||
* Boolean API.
|
||||
* @param {PathItem} path
|
||||
* @return {CompoundPath} this <minus> 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
|
||||
* 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
|
||||
*/
|
||||
_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;
|
||||
};
|
||||
unite: function( path, _cache ){
|
||||
var unionOp = function union( isPath1, isInsidePath1, isInsidePath2 ){
|
||||
return ( isInsidePath1 || isInsidePath2 )? false : true;
|
||||
};
|
||||
return computeBoolean( this, path, unionOp, _cache );
|
||||
},
|
||||
|
||||
/**
|
||||
* 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;
|
||||
};
|
||||
intersect: function( path, _cache ){
|
||||
var intersectionOp = function intersection( isPath1, isInsidePath1, isInsidePath2 ){
|
||||
return ( !isInsidePath1 && !isInsidePath2 )? false : true;
|
||||
};
|
||||
return computeBoolean( this, path, intersectionOp, _cache );
|
||||
},
|
||||
|
||||
/**
|
||||
* 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
|
||||
subtract: function( path, _cache ){
|
||||
var subtractionOp = function subtraction( isPath1, isInsidePath1, isInsidePath2 ){
|
||||
return ( (isPath1 && isInsidePath2) || (!isPath1 && !isInsidePath1) )? false : true;
|
||||
};
|
||||
return 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!
|
||||
*/
|
||||
_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;
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -373,420 +320,201 @@ var PathItem = this.PathItem = Item.extend(/** @lends PathItem# */{
|
|||
* @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;
|
||||
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;
|
||||
},
|
||||
|
||||
|
||||
_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<l; k++) {
|
||||
// Ignore segment overlaps if both curve are part of same contour
|
||||
// This is a degenerate case while resolving self-intersections,
|
||||
// after paperjs rev#8d35d92
|
||||
if( graph[j].id === graph[i].id &&
|
||||
( loc[k].parameter === 0.0 || loc[k].parameter === 1.0 )) {
|
||||
continue;
|
||||
}
|
||||
graph[i].intersections.push( loc[k] );
|
||||
loc2 = new CurveLocation( c2, null, loc[k].point );
|
||||
loc2._id = loc[k]._id;
|
||||
graph[j].intersections.push( loc2 );
|
||||
loc[k]._ixpair = loc2;
|
||||
loc2._ixpair = loc[k];
|
||||
++ixCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/*
|
||||
* Avoid duplicate intersections when a curve that belongs to one contour
|
||||
* passes through a segment on another contour
|
||||
*/
|
||||
len = graph.length;
|
||||
while( len-- ){
|
||||
ix = graph[len].intersections;
|
||||
for (i =0, l=ix.length; i<l; i++) {
|
||||
// In case of an over lap over the first segment on a link we
|
||||
// look for duplicates and mark them INVALID
|
||||
loc = ix[i];
|
||||
if ( loc.parameter === 0.0 ){
|
||||
j = graph.length;
|
||||
while( j-- ) {
|
||||
var ix2 = graph[j].intersections;
|
||||
k = ix2.length;
|
||||
while ( k-- ) {
|
||||
loc2 = ix2[k];
|
||||
if( !loc2.INVALID && loc._id !== loc2._id && loc2.parameter !== 1.0 &&
|
||||
loc2.point.equals( loc.point ) ) {
|
||||
loc2.INVALID = loc2._ixpair.INVALID = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} // if( loc.parameter === 0.0 ) {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Pass 2:
|
||||
* Walk the graph, sort the intersections on each individual link.
|
||||
* for each link that intersects with another one, replace it with new split links.
|
||||
*/
|
||||
var ixPoint, ixHandleI, ixHandleOut, param, isLinear, parts, left, right;
|
||||
// variable names are (sort of) acronyms of what thay are relative to the link
|
||||
// niho - link.NodeInHandleOut, for example.
|
||||
var values, nix, niy,nox, noy, niho, nohi, nihox, nihoy, nohix, nohiy;
|
||||
for ( i = graph.length - 1; i >= 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<l && lnk; j++) {
|
||||
if( ix[j].INVALID ){ continue; }
|
||||
param = ix[j].parameter;
|
||||
if( param === 0.0 || param === 1.0) {
|
||||
// Intersection falls on an existing node
|
||||
// there is no need to split the link
|
||||
nuNode = ( param === 0.0 )? lnk.nodeIn : lnk.nodeOut;
|
||||
nuNode.type = this._INTERSECTION_NODE;
|
||||
nuNode._intersectionID = ix[j]._id;
|
||||
if( param === 1.0 ){
|
||||
leftLink = null;
|
||||
rightLink = lnk;
|
||||
} else {
|
||||
leftLink = lnk;
|
||||
rightLink = null;
|
||||
}
|
||||
} else {
|
||||
parts = Curve.subdivide(values, param);
|
||||
left = parts[0];
|
||||
right = parts[1];
|
||||
// Make new link and convert handles from absolute to relative
|
||||
ixPoint = new Point( left[6], left[7] );
|
||||
if( !isLinear ){
|
||||
ixHandleIn = new Point(left[4] - ixPoint.x, left[5] - ixPoint.y);
|
||||
ixHandleOut = new Point(right[2] - ixPoint.x, right[3] - ixPoint.y);
|
||||
} else {
|
||||
ixHandleIn = ixHandleOut = null;
|
||||
right[2] = right[0];
|
||||
right[3] = right[1];
|
||||
}
|
||||
nuNode = new this._Node( ixPoint, ixHandleIn, ixHandleOut, lnk.id, lnk.isBaseContour, ++this._UNIQUE_ID );
|
||||
nuNode.type = this._INTERSECTION_NODE;
|
||||
nuNode._intersectionID = ix[j]._id;
|
||||
// clear the cached Segment on original end nodes and Update their handles
|
||||
lnk.nodeIn._segment = null;
|
||||
lnk.nodeOut._segment = null;
|
||||
if( !isLinear ){
|
||||
var tmppnt = lnk.nodeIn.point;
|
||||
lnk.nodeIn.handleOut = new Point( left[2] - tmppnt.x, left[3] - tmppnt.y );
|
||||
tmppnt = lnk.nodeOut.point;
|
||||
lnk.nodeOut.handleIn = new Point( right[4] - tmppnt.x, right[5] - tmppnt.y );
|
||||
}
|
||||
// Make new links after the split
|
||||
leftLink = new this._Link( lnk.nodeIn, nuNode, lnk.id, lnk.isBaseContour, lnk.winding );
|
||||
rightLink = new this._Link( nuNode, lnk.nodeOut, lnk.id, lnk.isBaseContour, lnk.winding );
|
||||
values = right;
|
||||
}
|
||||
// Add the first split link back to the graph, since we sorted the intersections
|
||||
// already, this link should contain no more intersections to the left.
|
||||
if( leftLink ){
|
||||
graph.splice( i, 0, leftLink );
|
||||
}
|
||||
// continue with the second split link, to see if
|
||||
// there are more intersections to deal with
|
||||
lnk = rightLink;
|
||||
// Interpolate the rest of the parameters
|
||||
if( lnk ) {
|
||||
var one_minus_param = (1.0 - param);
|
||||
for (k =j + 1, l=ix.length; k<l; k++) {
|
||||
ix[k]._parameter = ( ix[k].parameter - param ) / one_minus_param;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Add the last split link back to the graph
|
||||
if( lnk ){
|
||||
graph.splice( i, 0, lnk );
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Pass 3:
|
||||
* Merge matching intersection _Node Pairs (type is _INTERSECTION_NODE &&
|
||||
* a._intersectionID == b._intersectionID )
|
||||
*
|
||||
* Mark each _Link(Curve) according to whether it is
|
||||
* case 1. inside Path1 ( and only Path1 )
|
||||
* 2. inside Path2 ( and only Path2 )
|
||||
* 3. outside (normal case)
|
||||
*
|
||||
* Take a test function "operator" which will discard links
|
||||
* according to the above
|
||||
* * Union -> 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;
|
||||
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 = _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 );
|
||||
}
|
||||
|
||||
/**
|
||||
* _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;
|
||||
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();
|
||||
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
|
||||
|
|
Loading…
Reference in a new issue