paper.js/src/path/PathItem.Boolean.js

254 lines
8.5 KiB
JavaScript
Raw Normal View History

/*
* Paper.js - The Swiss Army Knife of Vector Graphics Scripting.
* http://paperjs.org/
*
2014-01-03 19:47:16 -05:00
* Copyright (c) 2011 - 2014, Juerg Lehni & Jonathan Puckey
* http://scratchdisk.com/ & 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
*
* 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
* - 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
*/
PathItem.inject(new function() {
2014-01-05 09:59:21 -05: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
*
* NOTE: Does NOT handle self-intersecting CompoundPaths.
*/
function reorientPath(path) {
if (path instanceof CompoundPath) {
var children = path.removeChildren(),
2014-02-20 12:44:38 -05:00
length = children.length,
bounds = new Array(length),
counters = new Array(length),
clockwise;
children.sort(function(a, b) {
return b.getBounds().getArea() - a.getBounds().getArea();
});
path.addChildren(children);
clockwise = children[0].isClockwise();
for (var i = 0; i < length; i++) {
bounds[i] = children[i].getBounds();
counters[i] = 0;
}
for (var i = 0; i < length; i++) {
for (var j = 1; j < length; j++) {
if (i !== j && bounds[i].intersects(bounds[j]))
counters[j]++;
}
2013-05-04 02:25:26 -04:00
// Omit the first child
if (i > 0 && counters[i] % 2 === 0)
children[i].setClockwise(clockwise);
}
}
return path;
2014-01-05 09:59:21 -05:00
}
function computeBoolean(path1, path2, operator, subtract) {
// 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.
// We call reduce() on both cloned paths to simplify compound paths and
// remove empty curves. We also apply matrices to both paths in case
// they were transformed.
var selfOp = path1 === path2;
path1 = reorientPath(path1.clone(false).reduce().applyMatrix());
path2 = selfOp ? path1
: reorientPath(path2.clone(false).reduce().applyMatrix());
// 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();
if (!selfOp && !(subtract ^ path2.isClockwise()))
2013-09-22 21:18:22 -04:00
path2.reverse();
2014-02-20 12:44:38 -05:00
var chain = [],
2014-02-17 14:59:38 -05:00
windings = [],
lengths = [],
paths = [],
segments = [],
// Aggregate of all curves in both operands, monotonic in y
monoCurves = [],
2014-02-20 12:44:38 -05:00
TOLERANCE = /*#=*/ Numerical.TOLERANCE,
intersections = path1.getIntersections(path2, true);
// Split curves at intersections on both paths.
PathItem._splitPath(intersections);
2013-12-29 07:38:04 -05:00
// Collect all sub paths and segments
paths.push.apply(paths, path1._children || [path1]);
if (!selfOp)
paths.push.apply(paths, path2._children || [path2]);
2014-01-25 23:39:51 -05:00
2014-02-20 12:44:38 -05:00
for (var i = 0, l = paths.length; i < l; i++) {
2013-12-29 07:38:04 -05:00
segments.push.apply(segments, paths[i].getSegments());
monoCurves.push.apply(monoCurves, paths[i]._getMonoCurves());
}
2013-12-29 07:38:04 -05:00
// 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) {
2014-02-19 18:32:15 -05:00
var _a = a._intersection,
_b = b._intersection;
return !_a && !_b || _a && _b ? 0 : _a ? -1 : 1;
2013-12-29 07:38:04 -05:00
});
2014-02-20 12:44:38 -05:00
for (var i = 0, l = segments.length; i < l; i++) {
var segment = segments[i];
2014-02-17 14:59:38 -05:00
if (segment._winding != null)
continue;
2013-12-29 07:38:04 -05:00
// Here we try to determine the most probable winding number
2014-02-20 12:44:38 -05:00
// 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.
chain.length = windings.length = lengths.length = 0;
var totalLength = 0,
startSeg = segment;
do {
2014-02-20 12:44:38 -05:00
chain.push(segment);
lengths.push(totalLength += segment.getCurve().getLength());
segment = segment.getNext();
2014-02-17 14:59:38 -05:00
} while (segment && !segment._intersection && segment !== startSeg);
2014-02-20 12:44:38 -05:00
// Select the median winding of three random points along this curve
// chain, as a representative winding number. The random selection
// gives a better chance of returning a correct winding than equally
// dividing the curve chain, with the same (amortised) time.
for (var j = 0; j < 3; j++) {
var length = totalLength * Math.random(),
amount = lengths.length;
k = 0;
do {
if (lengths[k] >= length) {
if (k > 0)
length -= lengths[k - 1];
break;
2013-12-29 07:38:04 -05:00
}
2014-02-20 12:44:38 -05:00
} while (++k < amount);
var curve = chain[k].getCurve(),
point = curve.getPointAt(length),
hor = curve.isHorizontal(),
path = curve._path;
if (path._parent instanceof CompoundPath)
path = path._parent;
// While subtracting, we need to omit this curve if this
// curve is contributing to the second operand and is outside
// the first operand.
2014-02-20 12:44:38 -05:00
windings[j] = subtract
&& (path === path1 && path2._getWinding(point, hor)
|| path === path2 && !path1._getWinding(point, hor))
? 0
: PathItem._getWinding(point, monoCurves, hor);
}
windings.sort();
2014-02-20 12:44:38 -05:00
// Assign the median winding to the entire curve chain.
var winding = windings[1];
for (var j = chain.length - 1; j >= 0; j--)
chain[j]._winding = winding;
}
2014-02-20 12:44:38 -05:00
// Trace closed contours and insert them into the result.
var result = new CompoundPath();
result.addChildren(PathItem._tracePaths(segments, operator), true);
// Delete the proxies
2013-05-04 06:38:19 -04:00
path1.remove();
if (!selfOp)
path2.remove();
// And then, we are done.
return result.reduce();
2014-01-05 09:59:21 -05:00
}
2014-01-05 09:59:21 -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.
2014-02-20 12:44:38 -05:00
return /** @lends PathItem# */{
/**
* {@grouptitle Boolean Path Operations}
*
* 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
*
* @param {PathItem} path the path to unite with
* @return {PathItem} the resulting path item
*/
2014-01-25 23:44:55 -05:00
unite: function(path) {
2014-02-20 12:44:38 -05:00
return computeBoolean(this, path, function(w) {
return w === 1 || w === 0;
}, false);
},
/**
* 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
*
* @param {PathItem} path the path to intersect with
* @return {PathItem} the resulting path item
*/
2014-01-25 23:44:55 -05:00
intersect: function(path) {
2014-02-20 12:44:38 -05:00
return computeBoolean(this, path, function(w) {
return w === 2;
}, false);
},
/**
* 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
*
* @param {PathItem} path the path to subtract
* @return {PathItem} the resulting path item
*/
2014-01-25 23:44:55 -05:00
subtract: function(path) {
2014-02-20 12:44:38 -05:00
return computeBoolean(this, path, function(w) {
return w === 1;
}, true);
},
// Compound boolean operators combine the basic boolean operations such
// as union, intersection, subtract etc.
/**
* 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
*
* @param {PathItem} path the path to exclude the intersection of
* @return {Group} the resulting group item
*/
exclude: function(path) {
return new Group([this.subtract(path), path.subtract(this)]);
},
/**
* 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
*
* @param {PathItem} path the path to divide by
* @return {Group} the resulting group item
*/
divide: function(path) {
return new Group([this.subtract(path), this.intersect(path)]);
}
2014-01-05 09:59:21 -05:00
};
});