2013-05-03 19:16:52 -04:00
|
|
|
/*
|
|
|
|
* Paper.js - The Swiss Army Knife of Vector Graphics Scripting.
|
|
|
|
* http://paperjs.org/
|
|
|
|
*
|
|
|
|
* Copyright (c) 2011 - 2013, Juerg Lehni & Jonathan Puckey
|
|
|
|
* http://lehni.org/ & http://jonathanpuckey.com/
|
|
|
|
*
|
|
|
|
* Distributed under the MIT license. See LICENSE file for details.
|
|
|
|
*
|
|
|
|
* All rights reserved.
|
|
|
|
*/
|
|
|
|
|
|
|
|
/*
|
2013-05-03 19:31:36 -04:00
|
|
|
* Boolean Geometric Path Operations
|
2013-05-03 19:16:52 -04:00
|
|
|
*
|
|
|
|
* This is mostly written for clarity and compatibility, not optimised for
|
|
|
|
* performance, and has to be tested heavily for stability.
|
|
|
|
*
|
|
|
|
* Supported
|
2013-05-05 19:38:18 -04:00
|
|
|
* - Path and CompoundPath items
|
2013-05-03 19:16:52 -04:00
|
|
|
* - Boolean Union
|
|
|
|
* - Boolean Intersection
|
|
|
|
* - Boolean Subtraction
|
|
|
|
* - Resolving a self-intersecting Path
|
|
|
|
*
|
|
|
|
* Not supported yet
|
|
|
|
* - Boolean operations on self-intersecting Paths
|
|
|
|
* - Paths are clones of each other that ovelap exactly on top of each other!
|
|
|
|
*
|
|
|
|
* @author Harikrishnan Gopalakrishnan
|
|
|
|
* http://hkrish.com/playground/paperjs/booleanStudy.html
|
|
|
|
*/
|
|
|
|
|
2013-05-04 00:21:53 -04:00
|
|
|
PathItem.inject(new function() {
|
2013-05-03 19:16:52 -04:00
|
|
|
/**
|
2013-05-04 02:25:26 -04:00
|
|
|
* To deal with a HTML5 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 paper.js)
|
|
|
|
* - islands have to have the same winding direction as the first child
|
2013-05-03 19:16:52 -04:00
|
|
|
*
|
2013-05-04 02:25:26 -04:00
|
|
|
* NOTE: Does NOT handle self-intersecting CompoundPaths.
|
2013-05-03 19:16:52 -04:00
|
|
|
*/
|
2013-05-04 02:03:00 -04:00
|
|
|
function reorientPath(path) {
|
|
|
|
if (path instanceof CompoundPath) {
|
2013-11-24 07:32:01 -05:00
|
|
|
var children = path.removeChildren(),
|
2013-05-04 02:03:00 -04:00
|
|
|
length = children.length,
|
|
|
|
bounds = new Array(length),
|
|
|
|
counters = new Array(length),
|
2013-11-24 07:32:01 -05:00
|
|
|
clockwise;
|
2013-12-03 15:54:36 -05:00
|
|
|
children.sort(function(a, b) {
|
|
|
|
return b.getBounds().getArea() - a.getBounds().getArea();
|
2013-11-24 07:32:01 -05:00
|
|
|
});
|
|
|
|
path.addChildren(children);
|
|
|
|
clockwise = children[0].isClockwise();
|
2013-05-04 02:03:00 -04:00
|
|
|
for (var i = 0; i < length; i++) {
|
|
|
|
bounds[i] = children[i].getBounds();
|
|
|
|
counters[i] = 0;
|
2013-05-03 19:21:44 -04:00
|
|
|
}
|
2013-05-04 02:03:00 -04:00
|
|
|
for (var i = 0; i < length; i++) {
|
|
|
|
for (var j = 1; j < length; j++) {
|
2013-12-29 07:36:43 -05:00
|
|
|
if (i !== j && bounds[i].intersects(bounds[j]))
|
2013-05-04 02:03:00 -04:00
|
|
|
counters[j]++;
|
|
|
|
}
|
2013-05-04 02:25:26 -04:00
|
|
|
// Omit the first child
|
|
|
|
if (i > 0 && counters[i] % 2 === 0)
|
2013-05-04 02:03:00 -04:00
|
|
|
children[i].setClockwise(clockwise);
|
2013-05-03 19:21:44 -04:00
|
|
|
}
|
|
|
|
}
|
2013-05-04 02:03:00 -04:00
|
|
|
return path;
|
2013-05-04 00:21:53 -04:00
|
|
|
}
|
2013-05-03 19:16:52 -04:00
|
|
|
|
2013-12-29 07:38:04 -05:00
|
|
|
function computeBoolean(path1, path2, operator, reverse, subtract, res) {
|
|
|
|
function calculateWinding (curve, t, monoCurves, subtract) {
|
|
|
|
var wind,
|
|
|
|
v = curve.getValues(),
|
|
|
|
midPoint = Curve.evaluate(v, t, 0),
|
|
|
|
length = Curve.getLength(v),
|
|
|
|
vDiff = Math.abs(v[1] - v[7]),
|
|
|
|
tolerance = /*#=*/ Numerical.TOLERANCE,
|
|
|
|
linear = Curve.isLinear(v) || Curve.isFlatEnough(v, tolerance);
|
|
|
|
horizontal = (linear && vDiff < tolerance) ||
|
|
|
|
(length < 1 && vDiff < 0.01),
|
|
|
|
parent = segment._path;
|
|
|
|
if (parent._parent instanceof CompoundPath)
|
|
|
|
parent = parent._parent;
|
|
|
|
// Find the winding contribution of this curve to
|
|
|
|
// the resulting path
|
|
|
|
wind = PathItem._getWindingNumber(midPoint, monoCurves, horizontal);
|
|
|
|
// While subtracting, we need to omit this curve if this
|
|
|
|
// curve is contributing to the second operand exclusively.
|
|
|
|
if (subtract && (parent._id === path2._id &&
|
|
|
|
!path1._getWinding(midPoint) ||
|
|
|
|
(parent._id === path1._id &&
|
|
|
|
path2._getWinding(midPoint)))) {
|
|
|
|
wind = 0;
|
|
|
|
}
|
|
|
|
return wind;
|
|
|
|
}
|
2013-05-03 19:21:44 -04:00
|
|
|
// 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.
|
2013-12-08 15:39:56 -05:00
|
|
|
// Also apply matrices to both paths in case they were transformed.
|
|
|
|
path1 = reorientPath(path1.clone(false).applyMatrix());
|
|
|
|
path2 = reorientPath(path2.clone(false).applyMatrix());
|
2013-05-03 19:21:44 -04:00
|
|
|
// Do operator specific calculations before we begin
|
2013-12-29 07:38:04 -05:00
|
|
|
// Make both paths at clockwise orientation, except when @subtract = true
|
|
|
|
// We need both paths at opposit orientation for subtraction
|
|
|
|
if (!path1.isClockwise())
|
2013-09-22 21:18:22 -04:00
|
|
|
path1.reverse();
|
2013-12-29 07:38:04 -05:00
|
|
|
if (!(reverse ^ path2.isClockwise()))
|
2013-09-22 21:18:22 -04:00
|
|
|
path2.reverse();
|
2013-12-29 07:38:04 -05:00
|
|
|
var intersections, i, j, l, segment, wind,
|
|
|
|
startSeg, crv, v, length,
|
|
|
|
// Minimum length of the path and minimum number of curves to
|
|
|
|
// confirm, to determine the winding contribution with a
|
|
|
|
// good enough confidence.
|
|
|
|
minCurveLenY = 15,
|
|
|
|
minCurveNum = 2,
|
|
|
|
curveChain = [],
|
|
|
|
windings = [],
|
|
|
|
windCommon, windConfident, lenCurves, numCurves, slowPath,
|
|
|
|
paths = [],
|
2013-05-04 06:08:43 -04:00
|
|
|
segments = [],
|
2013-12-29 07:38:04 -05:00
|
|
|
// Aggregate of all curves in both operands, monotonic in y
|
|
|
|
monoCurves = [],
|
|
|
|
result = new CompoundPath(),
|
|
|
|
abs = Math.abs;
|
|
|
|
// Split curves at intersections on both paths.
|
|
|
|
intersections = path1.getIntersections(path2);
|
|
|
|
PathItem._splitPath(intersections);
|
|
|
|
// Collect all sub paths and segments
|
|
|
|
paths.push.apply(paths, path1._children || [path1]);
|
|
|
|
paths.push.apply(paths, path2._children || [path2]);
|
|
|
|
for (i = 0, l = paths.length; i < l; i++){
|
|
|
|
segments.push.apply(segments, paths[i].getSegments());
|
|
|
|
monoCurves.push.apply(monoCurves, paths[i]._getMonotoneCurves());
|
2013-05-03 19:21:44 -04:00
|
|
|
}
|
2013-12-29 07:38:04 -05:00
|
|
|
|
|
|
|
//DEBUG:---------NOTE: delete ret arg. from unite etc. below------------------
|
|
|
|
if(res){
|
|
|
|
var cPath = new CompoundPath();
|
|
|
|
cPath.addChildren(paths, true);
|
|
|
|
return cPath;
|
|
|
|
}
|
|
|
|
//DEBUG:----------------------------------------------
|
|
|
|
|
|
|
|
// Propagate the winding contribution. Winding contribution of curves
|
|
|
|
// does not change between two intersections.
|
|
|
|
// First, sort all segments with an intersection to the begining.
|
|
|
|
segments.sort(function(a, b) {
|
|
|
|
var ixa = a._intersection,
|
|
|
|
ixb = b._intersection;
|
|
|
|
if ((!ixa && !ixb) || (ixa && ixb))
|
|
|
|
return 0;
|
|
|
|
return ixa ? -1 : 1;
|
|
|
|
});
|
|
|
|
for (i = 0, l = segments.length; i < l; i++) {
|
|
|
|
segment = segments[i];
|
|
|
|
if(segment._winding != null)
|
2013-05-04 02:03:00 -04:00
|
|
|
continue;
|
2013-12-29 07:38:04 -05:00
|
|
|
// Here we try to determine the most probable winding number
|
|
|
|
// contribution for this curve-chain. Once we have enough
|
|
|
|
// confidence in the winding contribution, we can propagate it
|
|
|
|
// until the intersection or end of a curve chain.
|
|
|
|
curveChain.length = 0;
|
|
|
|
windings.length = 0;
|
|
|
|
lenCurves = numCurves = 0;
|
|
|
|
windConfident = windCommon = null;
|
|
|
|
slowPath = false;
|
|
|
|
startSeg = segment;
|
|
|
|
// var check = startSeg.point.equals([262.80000000000007, 259.97641876494305])
|
|
|
|
// check && console.log("check")
|
2013-05-04 05:50:18 -04:00
|
|
|
do {
|
2013-12-29 07:38:04 -05:00
|
|
|
curveChain.push(segment);
|
|
|
|
if (windConfident === null || slowPath) {
|
|
|
|
crv = segment.getCurve();
|
|
|
|
// Determine the winding contribution of this curve
|
|
|
|
wind = calculateWinding(crv, 0.5, monoCurves, subtract);
|
|
|
|
// Record the length covered by this winding number,
|
|
|
|
// we need this if we were to revert to a slow path later
|
|
|
|
length = crv.getLength();
|
|
|
|
windings[wind] = windings[wind]
|
|
|
|
? windings[wind] + length : length;
|
|
|
|
// Check if we can declare a probable winding direction for
|
|
|
|
// this curve chain or not
|
|
|
|
if (!slowPath) {
|
|
|
|
if (windCommon === null) {
|
|
|
|
windCommon = wind;
|
|
|
|
} else if (windCommon !== wind) {
|
|
|
|
slowPath = true;
|
|
|
|
}
|
|
|
|
++numCurves;
|
|
|
|
lenCurves += length;
|
|
|
|
if (lenCurves > minCurveLenY && numCurves > minCurveNum)
|
|
|
|
windConfident = windCommon;
|
|
|
|
}
|
2013-05-03 19:21:44 -04:00
|
|
|
}
|
2013-12-29 07:38:04 -05:00
|
|
|
// Continue with next curve
|
2013-05-04 06:00:31 -04:00
|
|
|
segment = segment.getNext();
|
2013-12-29 07:38:04 -05:00
|
|
|
} while(segment && !segment._intersection && segment !== startSeg);
|
|
|
|
// If didn't manage to find a consistent winding value,
|
|
|
|
// we revert to a slower path.
|
|
|
|
if (!windConfident) {
|
|
|
|
if (curveChain.length && curveChain.length < 3) {
|
|
|
|
// If don't have enough curves beween intersections, we
|
|
|
|
// cannot use any method of ranking to determine a reliable
|
|
|
|
// winding number. Split the midpoint into three points
|
|
|
|
// along the curve and select the median winding.
|
|
|
|
// TODO: Find the lasrgest of the curves
|
|
|
|
crv = curveChain[0].getCurve();
|
|
|
|
windings.length = 0;
|
|
|
|
windings.push(1/3, 1/2, 2/3);
|
|
|
|
for (j = windings.length - 1; j >= 0; j--)
|
|
|
|
windings[j] = calculateWinding(crv, windings[j],
|
|
|
|
monoCurves, subtract);
|
|
|
|
windings.sort();
|
|
|
|
windConfident = windings[1];
|
|
|
|
} else {
|
|
|
|
// select the winding number from the accumulated values,
|
|
|
|
// that covers most of the curve chain by arc length
|
|
|
|
length = 0;
|
|
|
|
for (j = windings.length - 1; j >= 0; j--) {
|
|
|
|
wind = windings[j];
|
|
|
|
if (wind != null && wind > length) {
|
|
|
|
length = wind;
|
|
|
|
windConfident = j;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2013-05-03 19:21:44 -04:00
|
|
|
}
|
2013-12-29 07:38:04 -05:00
|
|
|
// DEBUG: ------------------------------------------------
|
|
|
|
// console.log(windConfident, curveChain.length, startSeg.point.x, startSeg.point.y)
|
|
|
|
// DEBUG: ------------------------------------------------
|
|
|
|
|
|
|
|
// Assign the winding to the entire curve chain
|
|
|
|
for (j = curveChain.length - 1; j >= 0; j--)
|
|
|
|
curveChain[j]._winding = windConfident;
|
2013-05-03 19:21:44 -04:00
|
|
|
}
|
2013-12-29 07:38:04 -05:00
|
|
|
// Trace closed contours and insert them into the result;
|
|
|
|
paths = PathItem._tracePaths(segments, operator);
|
|
|
|
for (i = 0, l = paths.length; i < l; i++)
|
|
|
|
result.addChild(paths[i], true);
|
2013-05-03 19:21:44 -04:00
|
|
|
// Delete the proxies
|
2013-05-04 06:38:19 -04:00
|
|
|
path1.remove();
|
|
|
|
path2.remove();
|
2013-05-03 19:21:44 -04:00
|
|
|
// And then, we are done.
|
2013-05-04 01:38:29 -04:00
|
|
|
return result.reduce();
|
2013-05-04 00:21:53 -04:00
|
|
|
}
|
2013-05-03 19:16:52 -04:00
|
|
|
|
2013-12-29 07:38:33 -05:00
|
|
|
// Boolean operators return true if a curve with the given winding
|
|
|
|
// contribution contributes to the final result or not. They are called
|
|
|
|
// for each curve in the graph after curves in the operands are
|
|
|
|
// split at intersections.
|
2013-05-04 13:58:50 -04:00
|
|
|
return /** @lends Path# */{
|
|
|
|
/**
|
|
|
|
* Merges the geometry of the specified path from this path's
|
|
|
|
* geometry and returns the result as a new path item.
|
2013-09-21 09:26:14 -04:00
|
|
|
*
|
2013-05-04 13:58:50 -04:00
|
|
|
* @param {PathItem} path the path to unite with
|
|
|
|
* @return {PathItem} the resulting path item
|
|
|
|
*/
|
2013-05-05 19:38:18 -04:00
|
|
|
unite: function(path) {
|
2013-05-04 00:21:53 -04:00
|
|
|
return computeBoolean(this, path,
|
|
|
|
function(isPath1, isInPath1, isInPath2) {
|
|
|
|
return isInPath1 || isInPath2;
|
2013-05-05 19:38:18 -04:00
|
|
|
});
|
2013-05-04 00:21:53 -04:00
|
|
|
},
|
|
|
|
|
2013-05-04 13:58:50 -04:00
|
|
|
/**
|
|
|
|
* Intersects the geometry of the specified path with this path's
|
|
|
|
* geometry and returns the result as a new path item.
|
2013-09-21 09:26:14 -04:00
|
|
|
*
|
2013-05-04 13:58:50 -04:00
|
|
|
* @param {PathItem} path the path to intersect with
|
|
|
|
* @return {PathItem} the resulting path item
|
|
|
|
*/
|
2013-05-05 19:38:18 -04:00
|
|
|
intersect: function(path) {
|
2013-05-04 00:21:53 -04:00
|
|
|
return computeBoolean(this, path,
|
|
|
|
function(isPath1, isInPath1, isInPath2) {
|
|
|
|
return !(isInPath1 || isInPath2);
|
2013-05-05 19:38:18 -04:00
|
|
|
});
|
2013-05-04 00:21:53 -04:00
|
|
|
},
|
|
|
|
|
2013-05-04 13:58:50 -04:00
|
|
|
/**
|
|
|
|
* Subtracts the geometry of the specified path from this path's
|
|
|
|
* geometry and returns the result as a new path item.
|
2013-09-21 09:26:14 -04:00
|
|
|
*
|
2013-05-04 13:58:50 -04:00
|
|
|
* @param {PathItem} path the path to subtract
|
|
|
|
* @return {PathItem} the resulting path item
|
|
|
|
*/
|
2013-05-05 19:38:18 -04:00
|
|
|
subtract: function(path) {
|
2013-05-04 00:21:53 -04:00
|
|
|
return computeBoolean(this, path,
|
|
|
|
function(isPath1, isInPath1, isInPath2) {
|
|
|
|
return isPath1 && isInPath2 || !isPath1 && !isInPath1;
|
2013-05-05 19:38:18 -04:00
|
|
|
}, true);
|
2013-05-04 00:21:53 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
// Compound boolean operators combine the basic boolean operations such
|
2013-09-21 09:26:14 -04:00
|
|
|
// as union, intersection, subtract etc.
|
2013-05-04 00:21:53 -04:00
|
|
|
// TODO: cache the split objects and find a way to properly clone them!
|
2013-05-04 13:58:50 -04:00
|
|
|
/**
|
|
|
|
* Excludes the intersection of the geometry of the specified path with
|
|
|
|
* this path's geometry and returns the result as a new group item.
|
2013-09-21 09:26:14 -04:00
|
|
|
*
|
2013-05-04 13:58:50 -04:00
|
|
|
* @param {PathItem} path the path to exclude the intersection of
|
|
|
|
* @return {Group} the resulting group item
|
|
|
|
*/
|
2013-05-04 00:21:53 -04:00
|
|
|
exclude: function(path) {
|
|
|
|
return new Group([this.subtract(path), path.subtract(this)]);
|
|
|
|
},
|
|
|
|
|
2013-05-04 13:58:50 -04:00
|
|
|
/**
|
|
|
|
* Splits the geometry of this path along the geometry of the specified
|
|
|
|
* path returns the result as a new group item.
|
2013-09-21 09:26:14 -04:00
|
|
|
*
|
2013-05-04 13:58:50 -04:00
|
|
|
* @param {PathItem} path the path to divide by
|
|
|
|
* @return {Group} the resulting group item
|
|
|
|
*/
|
2013-05-04 00:21:53 -04:00
|
|
|
divide: function(path) {
|
|
|
|
return new Group([this.subtract(path), this.intersect(path)]);
|
|
|
|
}
|
|
|
|
};
|
2013-05-03 19:16:52 -04:00
|
|
|
});
|