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
|
|
|
|
* - paperjs Path and CompoundPath objects
|
|
|
|
* - 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 00:21:53 -04:00
|
|
|
function splitPath(intersections, collectOthers) {
|
2013-05-04 00:05:44 -04:00
|
|
|
// Sort intersections by paths ids, curve index and parameter, so we
|
|
|
|
// can loop through all intersections, divide paths and never need to
|
|
|
|
// readjust indices.
|
|
|
|
intersections.sort(function(loc1, loc2) {
|
|
|
|
var path1 = loc1.getPath(),
|
|
|
|
path2 = loc2.getPath();
|
|
|
|
return path1 === path2
|
|
|
|
// We can add parameter (0 <= t <= 1) to index (a integer)
|
|
|
|
// to compare both at the same time
|
|
|
|
? (loc1.getIndex() + loc1.getParameter())
|
|
|
|
- (loc2.getIndex() + loc2.getParameter())
|
|
|
|
: path1._id - path2._id;
|
|
|
|
});
|
|
|
|
var others = collectOthers && [];
|
|
|
|
for (var i = intersections.length - 1; i >= 0; i--) {
|
|
|
|
var loc = intersections[i],
|
|
|
|
other = loc.getIntersection(),
|
|
|
|
curve = loc.divide(),
|
|
|
|
// When the curve doesn't need to be divided since t = 0, 1,
|
|
|
|
// #divide() returns null and we can use the existing segment.
|
|
|
|
segment = curve && curve.getSegment1() || loc.getSegment();
|
|
|
|
if (others)
|
|
|
|
others.push(other);
|
|
|
|
other.__segment = segment;
|
|
|
|
segment._ixPair = other;
|
2013-05-03 19:21:44 -04:00
|
|
|
}
|
2013-05-04 00:05:44 -04:00
|
|
|
return others;
|
2013-05-04 00:21:53 -04:00
|
|
|
}
|
2013-05-03 19:16:52 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
2013-05-03 19:21:44 -04:00
|
|
|
* the holes have opposit winding direction (already handled by paperjs)
|
|
|
|
* islands has to have same winding direction (as the first child of the path)
|
2013-05-03 19:16:52 -04:00
|
|
|
*
|
|
|
|
* Does NOT handle selfIntersecting CompoundPaths.
|
|
|
|
*
|
|
|
|
* @param {CompoundPath} path - Input CompoundPath, Note: This path could be modified if need be.
|
2013-05-03 19:21:44 -04:00
|
|
|
* @return {boolean} the winding direction of the base contour(true if clockwise)
|
2013-05-03 19:16:52 -04:00
|
|
|
*/
|
2013-05-04 02:03:00 -04:00
|
|
|
function reorientPath(path) {
|
|
|
|
if (path instanceof CompoundPath) {
|
|
|
|
var children = path._children,
|
|
|
|
length = children.length,
|
|
|
|
bounds = new Array(length),
|
|
|
|
counters = new Array(length),
|
|
|
|
clockwise = children[0].isClockwise();
|
|
|
|
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++) {
|
|
|
|
if (i !== j && bounds[i].contains(bounds[j]))
|
|
|
|
counters[j]++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Omit the first child
|
|
|
|
for (var i = 1; i < length; i++) {
|
|
|
|
if (counters[i] % 2 === 0) {
|
|
|
|
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-05-04 00:21:53 -04:00
|
|
|
function computeBoolean(path1, path2, operator, subtract, _cache) {
|
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-05-04 02:03:00 -04:00
|
|
|
var _path1 = reorientPath(path1.clone()),
|
|
|
|
_path2 = reorientPath(path2.clone()),
|
|
|
|
path1Clockwise = _path1.isClockwise(),
|
|
|
|
path2Clockwise = _path2.isClockwise(),
|
2013-05-04 01:38:29 -04:00
|
|
|
path1Id = _path1.id,
|
|
|
|
path2Id = _path2.id,
|
|
|
|
// Calculate all the intersections
|
|
|
|
intersections = _cache && _cache.intersections
|
|
|
|
|| _path1.getIntersections(_path2);
|
|
|
|
// if we have a empty _cache object as an operand, skip calculating
|
|
|
|
// boolean and cache the intersections
|
|
|
|
if (_cache && !_cache.intersections) {
|
|
|
|
// TODO: Don't we need to clear up and remove _path1 & _path2 again?
|
|
|
|
return _cache.intersections = intersections;
|
|
|
|
}
|
|
|
|
// Now split intersections on both curves, by asking the first call to
|
|
|
|
// collect the 'other' intersections for us and passing that on to the
|
|
|
|
// second call.
|
|
|
|
splitPath(splitPath(intersections, true));
|
2013-05-03 19:21:44 -04:00
|
|
|
// Do operator specific calculations before we begin
|
2013-05-04 00:41:22 -04:00
|
|
|
if (subtract) {
|
|
|
|
_path2.reverse();
|
|
|
|
path2Clockwise = !path2Clockwise;
|
|
|
|
}
|
2013-05-03 19:16:52 -04:00
|
|
|
|
2013-05-04 02:03:00 -04:00
|
|
|
var paths = []
|
|
|
|
.concat(_path1._children || [_path1])
|
|
|
|
.concat(_path2._children || [_path2]),
|
2013-05-04 01:38:29 -04:00
|
|
|
nodes = [],
|
2013-05-04 02:03:00 -04:00
|
|
|
result = new CompoundPath();
|
2013-05-04 01:38:29 -04:00
|
|
|
// Step 1: Discard invalid links according to the boolean operator
|
|
|
|
for (var i = 0, l = paths.length; i < l; i++) {
|
|
|
|
var path = paths[i],
|
2013-05-04 02:03:00 -04:00
|
|
|
parent = path._parent,
|
|
|
|
id = parent instanceof CompoundPath ? parent._id : path._id,
|
2013-05-04 01:38:29 -04:00
|
|
|
clockwise = path.isClockwise(),
|
2013-05-04 02:03:00 -04:00
|
|
|
segments = path._segments,
|
|
|
|
insidePath1 = false,
|
|
|
|
insidePath2 = false;
|
|
|
|
for (var j = segments.length - 1; j >= 0; j--) {
|
2013-05-04 01:38:29 -04:00
|
|
|
var segment = segments[j],
|
2013-05-04 02:03:00 -04:00
|
|
|
midPoint = segment.getCurve().getPoint(0.5);
|
|
|
|
if (id !== path1Id) {
|
2013-05-04 01:38:29 -04:00
|
|
|
insidePath1 = _path1.contains(midPoint)
|
|
|
|
&& (clockwise === path1Clockwise || subtract
|
|
|
|
|| !testOnCurve(_path1, midPoint));
|
2013-05-03 19:21:44 -04:00
|
|
|
}
|
2013-05-04 02:03:00 -04:00
|
|
|
if (id !== path2Id) {
|
2013-05-04 01:38:29 -04:00
|
|
|
insidePath2 = _path2.contains(midPoint)
|
|
|
|
&& (clockwise === path2Clockwise
|
|
|
|
|| !testOnCurve(_path2, midPoint));
|
2013-05-03 19:21:44 -04:00
|
|
|
}
|
2013-05-04 02:03:00 -04:00
|
|
|
if (operator(id === path1Id, insidePath1, insidePath2)) {
|
|
|
|
segment._INVALID = true;
|
2013-05-03 19:21:44 -04:00
|
|
|
// markPoint(midPoint, '+');
|
2013-05-04 02:03:00 -04:00
|
|
|
} else {
|
|
|
|
nodes.push(segment);
|
2013-05-03 19:21:44 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2013-05-04 01:38:29 -04:00
|
|
|
// Step 2: Retrieve the resulting paths from the graph
|
|
|
|
for (var i = 0, l = nodes.length; i < l; i++) {
|
|
|
|
var node = nodes[i];
|
2013-05-04 02:03:00 -04:00
|
|
|
if (node._visited)
|
|
|
|
continue;
|
2013-05-04 01:38:29 -04:00
|
|
|
var path = node.path,
|
|
|
|
nuPath = new Path(),
|
2013-05-04 02:03:00 -04:00
|
|
|
firstNode = node,
|
2013-05-04 01:38:29 -04:00
|
|
|
firstNode_ix = null;
|
2013-05-04 02:03:00 -04:00
|
|
|
if (node.getPrevious()._INVALID) {
|
2013-05-04 01:38:29 -04:00
|
|
|
node.setHandleIn(node._ixPair
|
|
|
|
? node._ixPair.getIntersection().__segment._handleIn
|
|
|
|
: Point.create(0, 0));
|
2013-05-03 19:21:44 -04:00
|
|
|
}
|
2013-05-04 02:03:00 -04:00
|
|
|
while (node && !node._visited && node !== firstNode_ix) {
|
2013-05-03 19:21:44 -04:00
|
|
|
node._visited = true;
|
2013-05-04 02:03:00 -04:00
|
|
|
firstNode_ix = firstNode_ix || firstNode._ixPair
|
|
|
|
&& firstNode._ixPair.getIntersection().__segment;
|
2013-05-03 19:21:44 -04:00
|
|
|
// node._ixPair is this node's intersection CurveLocation object
|
|
|
|
// node._ixPair.getIntersection() is the other CurveLocation object this node intersects with
|
2013-05-04 02:03:00 -04:00
|
|
|
var nextNode = node._ixPair && node._INVALID
|
|
|
|
? node._ixPair.getIntersection().__segment
|
|
|
|
: node;
|
2013-05-03 19:21:44 -04:00
|
|
|
if (node._ixPair) {
|
2013-05-04 01:38:29 -04:00
|
|
|
nuPath.add(new Segment(node._point, node._handleIn,
|
|
|
|
nextNode._handleOut));
|
2013-05-04 02:03:00 -04:00
|
|
|
nextNode._visited = true;
|
2013-05-03 19:21:44 -04:00
|
|
|
node = nextNode;
|
|
|
|
} else {
|
|
|
|
nuPath.add(node);
|
|
|
|
}
|
2013-05-04 02:03:00 -04:00
|
|
|
node = node.getNext();
|
2013-05-03 19:21:44 -04:00
|
|
|
}
|
2013-05-04 01:38:29 -04:00
|
|
|
// Avoid stray segments and incomplete paths
|
2013-05-04 02:03:00 -04:00
|
|
|
if (nuPath._segments.length > 2) {
|
|
|
|
nuPath.setClosed(true);
|
2013-05-04 01:38:29 -04:00
|
|
|
result.addChild(nuPath, true);
|
2013-05-04 02:03:00 -04:00
|
|
|
} else {
|
|
|
|
nuPath.remove();
|
2013-05-03 19:21:44 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
// Delete the proxies
|
|
|
|
_path1.remove();
|
|
|
|
_path2.remove();
|
|
|
|
// 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-05-04 00:21:53 -04:00
|
|
|
function testOnCurve(path, point) {
|
2013-05-04 00:24:02 -04:00
|
|
|
var curves = path.getCurves(),
|
|
|
|
bounds = path.getBounds();
|
|
|
|
if (bounds.contains(point)) {
|
|
|
|
for (var i = 0, l = curves.length; i < l; i++) {
|
|
|
|
var curve = curves[i];
|
|
|
|
if (curve.getBounds().contains(point)
|
|
|
|
&& curve.getParameterOf(point))
|
|
|
|
return true;
|
2013-05-03 19:21:44 -04:00
|
|
|
}
|
|
|
|
}
|
2013-05-04 00:24:02 -04:00
|
|
|
return false;
|
2013-05-03 19:16:52 -04:00
|
|
|
}
|
2013-05-04 00:21:53 -04:00
|
|
|
|
|
|
|
// A boolean operator is a binary operator function of the form
|
|
|
|
// function(isPath1, isInPath1, isInPath2)
|
|
|
|
//
|
|
|
|
// Operators return true if a curve in the operands is to be removed,
|
|
|
|
// and they aare called for each curve segment in the graph after all the
|
|
|
|
// intersections between the operands are calculated and curves in the
|
|
|
|
// operands were split at intersections.
|
|
|
|
//
|
|
|
|
// The boolean operator return a Boolean value indicating whether to
|
|
|
|
// keep the curve or not.
|
|
|
|
// return true - discard the curve
|
|
|
|
// return false - keep the curve
|
|
|
|
|
|
|
|
return {
|
|
|
|
unite: function(path, _cache) {
|
|
|
|
return computeBoolean(this, path,
|
|
|
|
function(isPath1, isInPath1, isInPath2) {
|
|
|
|
return isInPath1 || isInPath2;
|
|
|
|
}, false, _cache);
|
|
|
|
},
|
|
|
|
|
|
|
|
intersect: function(path, _cache) {
|
|
|
|
return computeBoolean(this, path,
|
|
|
|
function(isPath1, isInPath1, isInPath2) {
|
|
|
|
return !(isInPath1 || isInPath2);
|
|
|
|
}, false, _cache);
|
|
|
|
},
|
|
|
|
|
|
|
|
subtract: function(path, _cache) {
|
|
|
|
return computeBoolean(this, path,
|
|
|
|
function(isPath1, isInPath1, isInPath2) {
|
|
|
|
return isPath1 && isInPath2 || !isPath1 && !isInPath1;
|
|
|
|
}, true, _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) {
|
|
|
|
return new Group([this.subtract(path), path.subtract(this)]);
|
|
|
|
},
|
|
|
|
|
|
|
|
// Divide path1 by path2
|
|
|
|
divide: function(path) {
|
|
|
|
return new Group([this.subtract(path), this.intersect(path)]);
|
|
|
|
}
|
|
|
|
};
|
2013-05-03 19:16:52 -04:00
|
|
|
});
|