2013-05-03 19:16:52 -04:00
|
|
|
/*
|
|
|
|
* 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/
|
2013-05-03 19:16:52 -04:00
|
|
|
*
|
|
|
|
* 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
|
|
|
*
|
|
|
|
* 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
|
2015-09-12 05:58:17 -04:00
|
|
|
* - Boolean Exclusion
|
|
|
|
* - Resolving a self-intersecting Path items
|
|
|
|
* - Boolean operations on self-intersecting Paths items
|
2013-05-03 19:16:52 -04:00
|
|
|
*
|
|
|
|
* @author Harikrishnan Gopalakrishnan
|
|
|
|
* http://hkrish.com/playground/paperjs/booleanStudy.html
|
|
|
|
*/
|
2014-02-20 14:24:16 -05:00
|
|
|
PathItem.inject(new function() {
|
2015-01-03 19:50:24 -05:00
|
|
|
var operators = {
|
|
|
|
unite: function(w) {
|
|
|
|
return w === 1 || w === 0;
|
|
|
|
},
|
|
|
|
|
|
|
|
intersect: function(w) {
|
|
|
|
return w === 2;
|
|
|
|
},
|
|
|
|
|
|
|
|
subtract: function(w) {
|
|
|
|
return w === 1;
|
|
|
|
},
|
|
|
|
|
|
|
|
exclude: function(w) {
|
|
|
|
return w === 1;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2015-09-13 16:12:04 -04:00
|
|
|
// Creates a cloned version of the path that we can modify freely, with its
|
|
|
|
// matrix applied to its geometry. Calls #reduce() to simplify compound
|
|
|
|
// paths and remove empty curves, and #reorient() to make sure all paths
|
|
|
|
// have correct winding direction.
|
|
|
|
function preparePath(path) {
|
2015-09-18 11:51:03 -04:00
|
|
|
return path.clone(false).reduce().resolveCrossings()
|
|
|
|
.transform(null, true, true);
|
2015-09-13 16:12:04 -04:00
|
|
|
}
|
|
|
|
|
2015-09-18 11:51:57 -04:00
|
|
|
function finishBoolean(paths, path1, path2, reduce) {
|
|
|
|
var result = new CompoundPath(Item.NO_INSERT);
|
2015-09-15 08:11:27 -04:00
|
|
|
result.addChildren(paths, true);
|
|
|
|
// See if the CompoundPath can be reduced to just a simple Path.
|
2015-09-18 11:51:57 -04:00
|
|
|
if (reduce)
|
|
|
|
result = result.reduce();
|
2015-09-15 08:11:27 -04:00
|
|
|
// Insert the resulting path above whichever of the two paths appear
|
|
|
|
// further up in the stack.
|
|
|
|
result.insertAbove(path2 && path1.isSibling(path2)
|
|
|
|
&& path1.getIndex() < path2.getIndex()
|
|
|
|
? path2 : path1);
|
|
|
|
// Copy over the left-hand item's style and we're done.
|
|
|
|
// TODO: Consider using Item#_clone() for this, but find a way to not
|
|
|
|
// clone children / name (content).
|
|
|
|
result.setStyle(path1._style);
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2015-10-05 11:42:01 -04:00
|
|
|
var scaleFactor = 0.1;
|
|
|
|
var textAngle = 0;
|
2015-09-18 11:33:42 -04:00
|
|
|
var fontSize = 5;
|
|
|
|
|
|
|
|
var segmentOffset;
|
|
|
|
var pathIndices;
|
|
|
|
var pathIndex;
|
|
|
|
var pathCount;
|
|
|
|
|
2015-01-02 09:33:23 -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.
|
2015-01-03 19:50:24 -05:00
|
|
|
function computeBoolean(path1, path2, operation) {
|
2015-09-18 11:33:42 -04:00
|
|
|
segmentOffset = {};
|
|
|
|
pathIndices = {};
|
|
|
|
|
2015-01-03 19:50:24 -05:00
|
|
|
// We do not modify the operands themselves, but create copies instead,
|
|
|
|
// fas produced by the calls to preparePath().
|
|
|
|
// Note that the result paths might not belong to the same type
|
2015-01-02 09:33:23 -05:00
|
|
|
// i.e. subtraction(A:Path, B:Path):CompoundPath etc.
|
|
|
|
var _path1 = preparePath(path1),
|
|
|
|
_path2 = path2 && path1 !== path2 && preparePath(path2);
|
2015-01-03 19:51:27 -05:00
|
|
|
// Give both paths the same orientation except for subtraction
|
2015-01-03 19:50:24 -05:00
|
|
|
// and exclusion, where we need them at opposite orientation.
|
2015-01-03 19:51:27 -05:00
|
|
|
if (_path2 && /^(subtract|exclude)$/.test(operation)
|
|
|
|
^ (_path2.isClockwise() !== _path1.isClockwise()))
|
2015-01-02 09:33:23 -05:00
|
|
|
_path2.reverse();
|
2015-10-03 17:17:12 -04:00
|
|
|
// Split curves at crossings and overlaps on both paths. Note that for
|
|
|
|
// self-intersection, path2 is null and getIntersections() handles it.
|
2015-09-12 04:24:19 -04:00
|
|
|
// console.time('intersection');
|
2015-10-03 17:17:12 -04:00
|
|
|
var intersections = CurveLocation.expand(
|
|
|
|
_path1.getIntersections(_path2, function(inter) {
|
|
|
|
// Only handle overlaps when not self-intersecting
|
|
|
|
return inter.isCrossing() || _path2 && inter.isOverlap();
|
|
|
|
})
|
|
|
|
);
|
2015-09-12 04:24:19 -04:00
|
|
|
// console.timeEnd('intersection');
|
2015-10-03 17:17:12 -04:00
|
|
|
splitPath(intersections);
|
2015-09-09 02:24:02 -04:00
|
|
|
|
2015-09-13 07:06:01 -04:00
|
|
|
var segments = [],
|
2015-01-02 09:33:23 -05:00
|
|
|
// Aggregate of all curves in both operands, monotonic in y
|
2015-09-13 07:06:01 -04:00
|
|
|
monoCurves = [];
|
2014-01-25 23:39:51 -05:00
|
|
|
|
2015-01-02 09:33:23 -05:00
|
|
|
function collect(paths) {
|
|
|
|
for (var i = 0, l = paths.length; i < l; i++) {
|
|
|
|
var path = paths[i];
|
|
|
|
segments.push.apply(segments, path._segments);
|
|
|
|
monoCurves.push.apply(monoCurves, path._getMonoCurves());
|
|
|
|
}
|
|
|
|
}
|
2014-02-20 13:10:46 -05:00
|
|
|
|
2015-01-02 09:33:23 -05:00
|
|
|
// Collect all segments and monotonic curves
|
|
|
|
collect(_path1._children || [_path1]);
|
|
|
|
if (_path2)
|
|
|
|
collect(_path2._children || [_path2]);
|
|
|
|
// Propagate the winding contribution. Winding contribution of curves
|
2015-10-03 17:17:12 -04:00
|
|
|
// does not change between two intersections.
|
2015-09-13 07:06:01 -04:00
|
|
|
// First, propagate winding contributions for curve chains starting in
|
2015-10-03 17:17:12 -04:00
|
|
|
// all intersections:
|
|
|
|
for (var i = 0, l = intersections.length; i < l; i++) {
|
|
|
|
propagateWinding(intersections[i]._segment, _path1, _path2,
|
|
|
|
monoCurves, operation);
|
2015-09-13 07:06:01 -04:00
|
|
|
}
|
|
|
|
// Now process the segments that are not part of any intersecting chains
|
2015-01-02 09:33:23 -05:00
|
|
|
for (var i = 0, l = segments.length; i < l; i++) {
|
|
|
|
var segment = segments[i];
|
2015-09-13 07:06:01 -04:00
|
|
|
if (segment._winding == null) {
|
|
|
|
propagateWinding(segment, _path1, _path2, monoCurves,
|
|
|
|
operation);
|
2015-08-23 15:19:19 -04:00
|
|
|
}
|
2015-01-02 09:33:23 -05:00
|
|
|
}
|
2015-09-18 11:51:57 -04:00
|
|
|
return finishBoolean(tracePaths(segments, operation), path1, path2,
|
|
|
|
true);
|
2015-01-02 09:33:23 -05:00
|
|
|
}
|
2014-02-20 13:50:37 -05:00
|
|
|
|
2015-10-03 11:40:33 -04:00
|
|
|
function logIntersection(title, inter) {
|
|
|
|
var other = inter._intersection;
|
|
|
|
var log = [title, inter._id, 'id', inter.getPath()._id,
|
|
|
|
'i', inter.getIndex(), 't', inter._parameter,
|
|
|
|
'o', !!inter._overlap, 'p', inter.getPoint(),
|
|
|
|
'Other', other._id, 'id', other.getPath()._id,
|
|
|
|
'i', other.getIndex(), 't', other._parameter,
|
|
|
|
'o', !!other._overlap, 'p', other.getPoint()];
|
|
|
|
console.log(log.map(function(v) {
|
|
|
|
return v == null ? '-' : v
|
|
|
|
}).join(' '));
|
|
|
|
}
|
|
|
|
|
2015-10-06 15:09:35 -04:00
|
|
|
/*
|
|
|
|
* Creates linked lists between intersections through their _next property.
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
function linkIntersections(from, to) {
|
|
|
|
// Only create links if they are not the same, to avoid endless
|
|
|
|
// recursions.
|
|
|
|
if (from !== to) {
|
|
|
|
// Loop through the existing linked list until we find an
|
|
|
|
// empty spot, but stop if we find `to`, to avoid adding it
|
|
|
|
// again.
|
|
|
|
while (from._next && from._next !== to)
|
|
|
|
from = from._next;
|
|
|
|
// If we're reached the end of the list, we can add it.
|
|
|
|
if (!from._next)
|
|
|
|
from._next = to;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-01-02 09:33:23 -05:00
|
|
|
/**
|
2015-10-06 15:09:35 -04:00
|
|
|
* Splits a path-item at the given locations.
|
2015-06-16 11:50:37 -04:00
|
|
|
*
|
2015-10-06 15:09:35 -04:00
|
|
|
* @param {CurveLocation[]} locations an array of the locations to split the
|
|
|
|
* path-item at.
|
|
|
|
* @private
|
2015-01-02 09:33:23 -05:00
|
|
|
*/
|
2015-09-20 08:16:47 -04:00
|
|
|
function splitPath(locations) {
|
2015-09-13 18:51:46 -04:00
|
|
|
if (window.reportIntersections) {
|
2015-09-20 08:16:47 -04:00
|
|
|
console.log('Crossings', locations.length / 2);
|
|
|
|
locations.forEach(function(inter) {
|
2015-09-12 16:55:58 -04:00
|
|
|
if (inter._other)
|
|
|
|
return;
|
2015-10-03 11:40:33 -04:00
|
|
|
logIntersection('Intersection', inter);
|
2015-09-12 16:55:58 -04:00
|
|
|
new Path.Circle({
|
|
|
|
center: inter.point,
|
2015-09-18 11:33:42 -04:00
|
|
|
radius: 2 * scaleFactor,
|
2015-09-21 10:54:33 -04:00
|
|
|
strokeColor: 'red',
|
2015-09-14 09:16:52 -04:00
|
|
|
strokeScaling: false
|
2015-09-12 16:55:58 -04:00
|
|
|
});
|
2015-08-30 08:38:18 -04:00
|
|
|
});
|
|
|
|
}
|
2015-08-28 10:18:14 -04:00
|
|
|
|
2015-08-23 15:19:19 -04:00
|
|
|
// TODO: Make public in API, since useful!
|
2015-09-12 16:55:58 -04:00
|
|
|
var tMin = /*#=*/Numerical.CURVETIME_EPSILON,
|
2015-01-04 11:37:15 -05:00
|
|
|
tMax = 1 - tMin,
|
2015-09-06 06:47:35 -04:00
|
|
|
noHandles = false,
|
2015-09-16 12:34:35 -04:00
|
|
|
clearSegments = [],
|
2015-10-01 10:41:57 -04:00
|
|
|
prevCurve,
|
2015-09-16 12:34:35 -04:00
|
|
|
prevT;
|
2014-02-20 13:50:37 -05:00
|
|
|
|
2015-09-20 08:16:47 -04:00
|
|
|
for (var i = locations.length - 1; i >= 0; i--) {
|
|
|
|
var loc = locations[i],
|
2015-10-01 10:41:57 -04:00
|
|
|
curve = loc._curve,
|
2015-09-16 12:34:35 -04:00
|
|
|
t = loc._parameter,
|
2015-10-01 10:41:57 -04:00
|
|
|
origT = t;
|
|
|
|
if (curve !== prevCurve) {
|
|
|
|
// This is a new curve, update noHandles setting.
|
2015-09-06 06:47:35 -04:00
|
|
|
noHandles = !curve.hasHandles();
|
2015-10-01 10:41:57 -04:00
|
|
|
} else if (prevT > 0) {
|
|
|
|
// Scale parameter when we are splitting same curve multiple
|
|
|
|
// times, but avoid dividing by zero.
|
|
|
|
t /= prevT;
|
2015-01-02 09:33:23 -05:00
|
|
|
}
|
2015-08-22 08:24:31 -04:00
|
|
|
var segment;
|
|
|
|
if (t < tMin) {
|
|
|
|
segment = curve._segment1;
|
|
|
|
} else if (t > tMax) {
|
|
|
|
segment = curve._segment2;
|
|
|
|
} else {
|
2015-10-01 06:55:22 -04:00
|
|
|
// Split the curve at t, passing true for _setHandles to always
|
|
|
|
// set the handles on the sub-curves even if the original curve
|
|
|
|
// had no handles.
|
2015-10-01 07:52:08 -04:00
|
|
|
segment = curve.divide(t, true, true)._segment1;
|
2015-10-01 10:41:57 -04:00
|
|
|
// Keep track of segments of curves without handles, so they can
|
|
|
|
// be cleared again at the end.
|
2015-09-06 06:47:35 -04:00
|
|
|
if (noHandles)
|
|
|
|
clearSegments.push(segment);
|
2015-01-02 09:33:23 -05:00
|
|
|
}
|
2015-10-03 20:25:33 -04:00
|
|
|
loc._setSegment(segment);
|
2015-10-06 15:09:35 -04:00
|
|
|
|
|
|
|
// Create links from the new segment to the intersection on the
|
|
|
|
// other curve, as well as from there back. If there are multiple
|
|
|
|
// intersections on the same segment, we create linked lists between
|
|
|
|
// the intersections through linkIntersections(), linking both ways.
|
|
|
|
var inter = segment._intersection,
|
|
|
|
dest = loc._intersection;
|
2015-09-19 13:07:44 -04:00
|
|
|
if (inter) {
|
2015-10-06 15:09:35 -04:00
|
|
|
linkIntersections(inter, dest);
|
|
|
|
// Each time we add a new link to the linked list, we need to
|
|
|
|
// add links from all the other entries to the new entry.
|
|
|
|
var other = inter;
|
|
|
|
while (other) {
|
|
|
|
linkIntersections(other._intersection, inter);
|
|
|
|
other = other._next;
|
2015-09-21 09:43:19 -04:00
|
|
|
}
|
2015-09-16 19:15:41 -04:00
|
|
|
} else {
|
2015-10-06 15:09:35 -04:00
|
|
|
segment._intersection = dest;
|
2015-09-16 19:15:41 -04:00
|
|
|
}
|
2015-10-01 10:41:57 -04:00
|
|
|
prevCurve = curve;
|
|
|
|
prevT = origT;
|
2015-01-02 09:33:23 -05:00
|
|
|
}
|
2015-09-06 06:47:35 -04:00
|
|
|
// Clear segment handles if they were part of a curve with no handles,
|
|
|
|
// once we are done with the entire curve.
|
|
|
|
for (var i = 0, l = clearSegments.length; i < l; i++) {
|
|
|
|
clearSegments[i].clearHandles();
|
2015-08-22 16:06:42 -04:00
|
|
|
}
|
2015-01-02 09:33:23 -05:00
|
|
|
}
|
2014-02-20 13:50:37 -05:00
|
|
|
|
2015-01-02 09:33:23 -05:00
|
|
|
/**
|
2015-08-18 16:36:10 -04:00
|
|
|
* Private method that returns the winding contribution of the given point
|
2015-01-02 09:33:23 -05:00
|
|
|
* with respect to a given set of monotone curves.
|
|
|
|
*/
|
|
|
|
function getWinding(point, curves, horizontal, testContains) {
|
2015-09-12 16:20:31 -04:00
|
|
|
var epsilon = /*#=*/Numerical.GEOMETRIC_EPSILON,
|
2015-09-12 16:55:58 -04:00
|
|
|
tMin = /*#=*/Numerical.CURVETIME_EPSILON,
|
2015-01-02 17:47:26 -05:00
|
|
|
tMax = 1 - tMin,
|
2015-01-04 18:13:30 -05:00
|
|
|
px = point.x,
|
|
|
|
py = point.y,
|
2015-01-02 09:33:23 -05:00
|
|
|
windLeft = 0,
|
|
|
|
windRight = 0,
|
|
|
|
roots = [],
|
2015-01-02 15:19:18 -05:00
|
|
|
abs = Math.abs;
|
2015-01-02 09:33:23 -05:00
|
|
|
// Absolutely horizontal curves may return wrong results, since
|
|
|
|
// the curves are monotonic in y direction and this is an
|
|
|
|
// indeterminate state.
|
|
|
|
if (horizontal) {
|
|
|
|
var yTop = -Infinity,
|
|
|
|
yBottom = Infinity,
|
2015-09-12 16:20:31 -04:00
|
|
|
yBefore = py - epsilon,
|
|
|
|
yAfter = py + epsilon;
|
2015-01-02 09:33:23 -05:00
|
|
|
// Find the closest top and bottom intercepts for the same vertical
|
|
|
|
// line.
|
|
|
|
for (var i = 0, l = curves.length; i < l; i++) {
|
|
|
|
var values = curves[i].values;
|
2015-01-04 18:13:30 -05:00
|
|
|
if (Curve.solveCubic(values, 0, px, roots, 0, 1) > 0) {
|
2015-01-02 09:33:23 -05:00
|
|
|
for (var j = roots.length - 1; j >= 0; j--) {
|
2015-08-19 11:15:41 -04:00
|
|
|
var y = Curve.getPoint(values, roots[j]).y;
|
2015-01-04 18:13:30 -05:00
|
|
|
if (y < yBefore && y > yTop) {
|
|
|
|
yTop = y;
|
|
|
|
} else if (y > yAfter && y < yBottom) {
|
|
|
|
yBottom = y;
|
2015-01-02 09:33:23 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Shift the point lying on the horizontal curves by
|
|
|
|
// half of closest top and bottom intercepts.
|
2015-01-04 18:13:30 -05:00
|
|
|
yTop = (yTop + py) / 2;
|
|
|
|
yBottom = (yBottom + py) / 2;
|
2015-08-18 16:36:10 -04:00
|
|
|
// TODO: Don't we need to pass on testContains here?
|
2015-01-02 09:33:23 -05:00
|
|
|
if (yTop > -Infinity)
|
2015-01-04 18:13:30 -05:00
|
|
|
windLeft = getWinding(new Point(px, yTop), curves);
|
2015-01-02 09:33:23 -05:00
|
|
|
if (yBottom < Infinity)
|
2015-01-04 18:13:30 -05:00
|
|
|
windRight = getWinding(new Point(px, yBottom), curves);
|
2015-01-02 09:33:23 -05:00
|
|
|
} else {
|
2015-09-12 16:20:31 -04:00
|
|
|
var xBefore = px - epsilon,
|
|
|
|
xAfter = px + epsilon;
|
2015-01-02 09:33:23 -05:00
|
|
|
// Find the winding number for right side of the curve, inclusive of
|
|
|
|
// the curve itself, while tracing along its +-x direction.
|
2015-08-18 16:36:10 -04:00
|
|
|
var startCounted = false,
|
|
|
|
prevCurve,
|
|
|
|
prevT;
|
2015-01-02 09:33:23 -05:00
|
|
|
for (var i = 0, l = curves.length; i < l; i++) {
|
|
|
|
var curve = curves[i],
|
|
|
|
values = curve.values,
|
2015-08-18 16:36:10 -04:00
|
|
|
winding = curve.winding;
|
2015-01-04 17:59:25 -05:00
|
|
|
// Since the curves are monotone in y direction, we can just
|
|
|
|
// compare the endpoints of the curve to determine if the
|
|
|
|
// ray from query point along +-x direction will intersect
|
|
|
|
// the monotone curve. Results in quite significant speedup.
|
2015-01-02 09:33:23 -05:00
|
|
|
if (winding && (winding === 1
|
2015-01-04 18:13:30 -05:00
|
|
|
&& py >= values[1] && py <= values[7]
|
|
|
|
|| py >= values[7] && py <= values[1])
|
|
|
|
&& Curve.solveCubic(values, 1, py, roots, 0, 1) === 1) {
|
2015-08-18 16:36:10 -04:00
|
|
|
var t = roots[0];
|
2015-01-02 09:33:23 -05:00
|
|
|
// Due to numerical precision issues, two consecutive curves
|
|
|
|
// may register an intercept twice, at t = 1 and 0, if y is
|
|
|
|
// almost equal to one of the endpoints of the curves.
|
2015-01-04 17:59:25 -05:00
|
|
|
// But since curves may contain more than one loop of curves
|
|
|
|
// and the end point on the last curve of a loop would not
|
|
|
|
// be registered as a double, we need to filter these cases:
|
2015-08-18 16:36:10 -04:00
|
|
|
if (!( // = the following conditions will be excluded:
|
|
|
|
// Detect and exclude intercepts at 'end' of loops
|
|
|
|
// if the start of the loop was already counted.
|
|
|
|
// This also works for the last curve: [i + 1] == null
|
|
|
|
t > tMax && startCounted && curve.next !== curves[i + 1]
|
2015-01-04 17:59:25 -05:00
|
|
|
// Detect 2nd case of a consecutive intercept, but make
|
2015-08-18 16:36:10 -04:00
|
|
|
// sure we're still on the same loop.
|
|
|
|
|| t < tMin && prevT > tMax
|
|
|
|
&& curve.previous === prevCurve)) {
|
2015-08-19 11:15:41 -04:00
|
|
|
var x = Curve.getPoint(values, t).x,
|
|
|
|
slope = Curve.getTangent(values, t).y,
|
2015-08-18 16:36:10 -04:00
|
|
|
counted = false;
|
2015-01-04 16:37:27 -05:00
|
|
|
// Take care of cases where the curve and the preceding
|
|
|
|
// curve merely touches the ray towards +-x direction,
|
|
|
|
// but proceeds to the same side of the ray.
|
|
|
|
// This essentially is not a crossing.
|
2015-09-06 11:35:27 -04:00
|
|
|
if (Numerical.isZero(slope) && !Curve.isStraight(values)
|
2015-01-04 17:28:39 -05:00
|
|
|
// Does the slope over curve beginning change?
|
2015-08-19 11:15:41 -04:00
|
|
|
|| t < tMin && slope * Curve.getTangent(
|
|
|
|
curve.previous.values, 1).y < 0
|
2015-01-04 17:28:39 -05:00
|
|
|
// Does the slope over curve end change?
|
2015-08-19 11:15:41 -04:00
|
|
|
|| t > tMax && slope * Curve.getTangent(
|
|
|
|
curve.next.values, 0).y < 0) {
|
2015-01-04 18:13:30 -05:00
|
|
|
if (testContains && x >= xBefore && x <= xAfter) {
|
2015-01-04 16:37:27 -05:00
|
|
|
++windLeft;
|
|
|
|
++windRight;
|
2015-08-18 16:36:10 -04:00
|
|
|
counted = true;
|
2015-01-04 16:37:27 -05:00
|
|
|
}
|
2015-01-04 18:13:30 -05:00
|
|
|
} else if (x <= xBefore) {
|
2015-01-04 16:37:27 -05:00
|
|
|
windLeft += winding;
|
2015-08-18 16:36:10 -04:00
|
|
|
counted = true;
|
2015-01-04 18:13:30 -05:00
|
|
|
} else if (x >= xAfter) {
|
2015-01-04 16:37:27 -05:00
|
|
|
windRight += winding;
|
2015-08-18 16:36:10 -04:00
|
|
|
counted = true;
|
2015-01-02 09:33:23 -05:00
|
|
|
}
|
2015-08-18 16:36:10 -04:00
|
|
|
// Detect the beginning of a new loop by comparing with
|
|
|
|
// the previous curve, and set startCounted accordingly.
|
|
|
|
// This also works for the first loop where i - 1 == -1
|
|
|
|
if (curve.previous !== curves[i - 1])
|
|
|
|
startCounted = t < tMin && counted;
|
2015-01-02 09:33:23 -05:00
|
|
|
}
|
2015-08-18 16:36:10 -04:00
|
|
|
prevCurve = curve;
|
2015-01-04 18:09:34 -05:00
|
|
|
prevT = t;
|
2015-01-02 09:33:23 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return Math.max(abs(windLeft), abs(windRight));
|
|
|
|
}
|
2014-02-20 13:50:37 -05:00
|
|
|
|
2015-09-13 07:06:01 -04:00
|
|
|
function propagateWinding(segment, path1, path2, monoCurves, operation) {
|
|
|
|
// Here we try to determine the most probable winding number
|
|
|
|
// contribution for the curve-chain starting with this segment. Once we
|
|
|
|
// have enough confidence in the winding contribution, we can propagate
|
|
|
|
// it until the next intersection or end of a curve chain.
|
2015-09-16 12:15:26 -04:00
|
|
|
var epsilon = /*#=*/Numerical.GEOMETRIC_EPSILON,
|
2015-09-13 07:06:01 -04:00
|
|
|
chain = [],
|
2015-09-15 13:39:35 -04:00
|
|
|
start = segment,
|
2015-09-13 07:06:01 -04:00
|
|
|
totalLength = 0,
|
|
|
|
windingSum = 0;
|
|
|
|
do {
|
2015-09-13 08:19:56 -04:00
|
|
|
var curve = segment.getCurve(),
|
|
|
|
length = curve.getLength();
|
|
|
|
chain.push({ segment: segment, curve: curve, length: length });
|
2015-09-13 07:06:01 -04:00
|
|
|
totalLength += length;
|
|
|
|
segment = segment.getNext();
|
2015-09-15 13:39:35 -04:00
|
|
|
} while (segment && !segment._intersection && segment !== start);
|
2015-09-13 07:06:01 -04:00
|
|
|
// Calculate the average winding among three evenly distributed
|
|
|
|
// points along this curve chain as a representative winding number.
|
|
|
|
// This selection gives a better chance of returning a correct
|
|
|
|
// winding than equally dividing the curve chain, with the same
|
|
|
|
// (amortised) time.
|
|
|
|
for (var i = 0; i < 3; i++) {
|
|
|
|
// Try the points at 1/4, 2/4 and 3/4 of the total length:
|
|
|
|
var length = totalLength * (i + 1) / 4;
|
|
|
|
for (var k = 0, m = chain.length; k < m; k++) {
|
|
|
|
var node = chain[k],
|
|
|
|
curveLength = node.length;
|
|
|
|
if (length <= curveLength) {
|
|
|
|
// If the selected location on the curve falls onto its
|
|
|
|
// beginning or end, use the curve's center instead.
|
|
|
|
if (length < epsilon || curveLength - length < epsilon)
|
|
|
|
length = curveLength / 2;
|
2015-09-13 08:19:56 -04:00
|
|
|
var curve = node.curve,
|
|
|
|
path = curve._path,
|
|
|
|
parent = path._parent,
|
2015-09-13 07:06:01 -04:00
|
|
|
pt = curve.getPointAt(length),
|
2015-09-13 08:19:56 -04:00
|
|
|
hor = curve.isHorizontal();
|
|
|
|
if (parent instanceof CompoundPath)
|
|
|
|
path = parent;
|
2015-09-13 07:06:01 -04:00
|
|
|
// While subtracting, we need to omit this curve if this
|
|
|
|
// curve is contributing to the second operand and is
|
|
|
|
// outside the first operand.
|
|
|
|
windingSum += operation === 'subtract' && path2
|
|
|
|
&& (path === path1 && path2._getWinding(pt, hor)
|
|
|
|
|| path === path2 && !path1._getWinding(pt, hor))
|
|
|
|
? 0
|
|
|
|
: getWinding(pt, monoCurves, hor);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
length -= curveLength;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Assign the average winding to the entire curve chain.
|
|
|
|
var winding = Math.round(windingSum / 3);
|
2015-09-20 09:50:26 -04:00
|
|
|
for (var j = chain.length - 1; j >= 0; j--)
|
|
|
|
chain[j].segment._winding = winding;
|
2015-09-13 07:06:01 -04:00
|
|
|
}
|
|
|
|
|
2015-01-02 09:33:23 -05:00
|
|
|
/**
|
|
|
|
* Private method to trace closed contours from a set of segments according
|
|
|
|
* to a set of constraints-winding contribution and a custom operator.
|
|
|
|
*
|
|
|
|
* @param {Segment[]} segments Array of 'seed' segments for tracing closed
|
|
|
|
* contours
|
|
|
|
* @param {Function} the operator function that receives as argument the
|
|
|
|
* winding number contribution of a curve and returns a boolean value
|
|
|
|
* indicating whether the curve should be included in the final contour or
|
|
|
|
* not
|
|
|
|
* @return {Path[]} the contours traced
|
|
|
|
*/
|
2015-09-18 11:51:57 -04:00
|
|
|
function tracePaths(segments, operation) {
|
2015-09-19 07:21:29 -04:00
|
|
|
pathIndex = 0;
|
|
|
|
pathCount = 1;
|
2015-08-23 15:19:19 -04:00
|
|
|
|
|
|
|
function labelSegment(seg, text, color) {
|
|
|
|
var point = seg.point;
|
2015-10-01 22:12:15 -04:00
|
|
|
var key = Math.round(point.x / (10 * scaleFactor))
|
|
|
|
+ ',' + Math.round(point.y / (10 * scaleFactor));
|
2015-08-23 15:19:19 -04:00
|
|
|
var offset = segmentOffset[key] || 0;
|
|
|
|
segmentOffset[key] = offset + 1;
|
2015-09-14 09:16:52 -04:00
|
|
|
var size = fontSize * scaleFactor;
|
2015-08-23 15:19:19 -04:00
|
|
|
var text = new PointText({
|
2015-09-14 09:16:52 -04:00
|
|
|
point: point.add(
|
|
|
|
new Point(size, size / 2).add(0, offset * size * 1.2)
|
|
|
|
.rotate(textAngle)),
|
2015-08-23 15:19:19 -04:00
|
|
|
content: text,
|
|
|
|
justification: 'left',
|
2015-08-23 22:36:49 -04:00
|
|
|
fillColor: color,
|
2015-08-26 11:36:20 -04:00
|
|
|
fontSize: fontSize
|
2015-08-23 15:19:19 -04:00
|
|
|
});
|
2015-09-13 16:12:04 -04:00
|
|
|
// TODO! PointText should have pivot in #point by default!
|
2015-08-23 15:19:19 -04:00
|
|
|
text.pivot = text.globalToLocal(text.point);
|
2015-09-14 09:16:52 -04:00
|
|
|
text.scale(scaleFactor);
|
|
|
|
text.rotate(textAngle);
|
2015-10-01 07:21:17 -04:00
|
|
|
new Path.Line({
|
|
|
|
from: text.point,
|
|
|
|
to: seg.point,
|
|
|
|
strokeColor: color,
|
|
|
|
strokeScaling: false
|
|
|
|
});
|
|
|
|
return text;
|
2015-08-23 15:19:19 -04:00
|
|
|
}
|
|
|
|
|
2015-09-21 09:42:47 -04:00
|
|
|
function drawSegment(seg, other, text, index, color) {
|
2015-09-13 18:51:46 -04:00
|
|
|
if (!window.reportSegments)
|
2015-08-23 15:19:19 -04:00
|
|
|
return;
|
|
|
|
new Path.Circle({
|
|
|
|
center: seg.point,
|
2015-09-14 09:16:52 -04:00
|
|
|
radius: fontSize / 2 * scaleFactor,
|
2015-08-26 11:36:20 -04:00
|
|
|
strokeColor: color,
|
|
|
|
strokeScaling: false
|
2015-08-23 15:19:19 -04:00
|
|
|
});
|
|
|
|
var inter = seg._intersection;
|
2015-09-15 10:31:05 -04:00
|
|
|
labelSegment(seg, '#' + pathCount + '.'
|
2015-08-26 11:36:20 -04:00
|
|
|
+ (path ? path._segments.length + 1 : 1)
|
2015-09-18 11:33:42 -04:00
|
|
|
+ ' (' + (index + 1) + '): ' + text
|
|
|
|
+ ' id: ' + seg._path._id + '.' + seg._index
|
2015-09-21 09:42:47 -04:00
|
|
|
+ (other ? ' -> ' + other._path._id + '.' + other._index : '')
|
2015-09-16 12:34:35 -04:00
|
|
|
+ ' v: ' + (seg._visited ? 1 : 0)
|
|
|
|
+ ' p: ' + seg._point
|
2015-09-20 09:50:26 -04:00
|
|
|
+ ' op: ' + isValid(seg)
|
2015-09-20 08:17:23 -04:00
|
|
|
+ ' ov: ' + !!(inter && inter._overlap)
|
2015-09-16 12:34:35 -04:00
|
|
|
+ ' wi: ' + seg._winding
|
2015-09-19 07:21:29 -04:00
|
|
|
+ ' mu: ' + !!(inter && inter._next)
|
2015-08-23 15:19:19 -04:00
|
|
|
, color);
|
|
|
|
}
|
|
|
|
|
2015-09-19 16:47:57 -04:00
|
|
|
for (var i = 0, j = 0;
|
|
|
|
i < (window.reportWindings ? segments.length : 0);
|
|
|
|
i++, j++) {
|
2015-08-23 15:19:19 -04:00
|
|
|
var seg = segments[i];
|
2015-08-28 10:18:14 -04:00
|
|
|
path = seg._path,
|
|
|
|
id = path._id,
|
2015-08-23 16:42:57 -04:00
|
|
|
point = seg.point,
|
2015-10-04 22:57:12 -04:00
|
|
|
inter = seg._intersection,
|
2015-10-06 15:09:35 -04:00
|
|
|
ix = inter,
|
|
|
|
ixs = ix && ix._segment,
|
|
|
|
n1x = inter && inter._next,
|
|
|
|
n1xs = n1x && n1x._segment,
|
|
|
|
n2x = n1x && n1x._next,
|
|
|
|
n2xs = n2x && n2x._segment,
|
|
|
|
n3x = n2x && n2x._next,
|
|
|
|
n3xs = n3x && n3x._segment,
|
|
|
|
item = path instanceof Path ? path : path._parent;
|
2015-09-19 16:47:57 -04:00
|
|
|
if (!(id in pathIndices)) {
|
2015-08-28 10:18:14 -04:00
|
|
|
pathIndices[id] = ++pathIndex;
|
2015-09-19 16:47:57 -04:00
|
|
|
j = 0;
|
|
|
|
}
|
|
|
|
labelSegment(seg, '#' + pathIndex + '.' + (j + 1)
|
2015-09-18 11:33:42 -04:00
|
|
|
+ ' id: ' + seg._path._id + '.' + seg._index
|
2015-10-06 15:09:35 -04:00
|
|
|
+ ' ix: ' + (ixs && ixs._path._id + '.' + ixs._index
|
|
|
|
+ '(' + ix._id + ')' || '--')
|
|
|
|
+ ' n1x: ' + (n1xs && n1xs._path._id + '.' + n1xs._index
|
|
|
|
+ '(' + n1x._id + ')' || '--')
|
|
|
|
+ ' n2x: ' + (n2xs && n2xs._path._id + '.' + n2xs._index
|
|
|
|
+ '(' + n2x._id + ')' || '--')
|
|
|
|
+ ' n3x: ' + (n3xs && n3xs._path._id + '.' + n3xs._index
|
|
|
|
+ '(' + n3x._id + ')' || '--')
|
2015-09-16 12:34:35 -04:00
|
|
|
+ ' pt: ' + seg._point
|
2015-09-20 08:17:23 -04:00
|
|
|
+ ' ov: ' + !!(inter && inter._overlap)
|
2015-09-16 12:34:35 -04:00
|
|
|
+ ' wi: ' + seg._winding
|
2015-10-06 15:09:35 -04:00
|
|
|
, item.strokeColor || item.fillColor || 'black');
|
2015-08-23 15:19:19 -04:00
|
|
|
}
|
|
|
|
|
2015-08-24 06:30:14 -04:00
|
|
|
var paths = [],
|
2015-09-19 13:07:44 -04:00
|
|
|
start,
|
2015-09-20 09:50:26 -04:00
|
|
|
otherStart,
|
|
|
|
operator = operators[operation],
|
|
|
|
// Adjust winding contributions for specific operations on overlaps:
|
|
|
|
overlapWinding = {
|
|
|
|
unite: { 1: 2 },
|
|
|
|
intersect: { 2: 1 }
|
|
|
|
}[operation];
|
|
|
|
|
|
|
|
function isValid(seg, unadjusted) {
|
|
|
|
if (!operator) // For self-intersection, we're always valid!
|
|
|
|
return true;
|
|
|
|
var winding = seg._winding,
|
|
|
|
inter = seg._intersection;
|
|
|
|
if (inter && !unadjusted && overlapWinding && inter._overlap)
|
|
|
|
winding = overlapWinding[winding] || winding;
|
|
|
|
return operator(winding);
|
|
|
|
}
|
2015-09-19 13:07:44 -04:00
|
|
|
|
2015-10-06 15:09:35 -04:00
|
|
|
/**
|
|
|
|
* Checks if the curve from seg1 to seg2 is part of an overlap, by
|
|
|
|
* getting a curve-point somewhere along the curve (t = 0.5), and
|
|
|
|
* checking if it is part of the overlap curve.
|
|
|
|
*/
|
|
|
|
function isOverlap(seg1, seg2) {
|
|
|
|
var inter = seg2._intersection,
|
|
|
|
overlap = inter && inter._overlap;
|
|
|
|
if (overlap) {
|
|
|
|
var pt = Curve.getPoint(Curve.getValues(seg1, seg2), 0.5);
|
|
|
|
if (Curve.getParameterOf(overlap, pt.x, pt.y) !== null)
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2015-09-19 13:07:44 -04:00
|
|
|
// If there are multiple possible intersections, find the one
|
|
|
|
// that's either connecting back to start or is not visited yet,
|
|
|
|
// and will be part of the boolean result:
|
2015-10-06 15:09:35 -04:00
|
|
|
function getIntersection(inter, strict) {
|
2015-09-19 13:07:44 -04:00
|
|
|
if (!inter)
|
|
|
|
return null;
|
2015-09-19 16:47:57 -04:00
|
|
|
var seg = inter._segment,
|
2015-10-04 22:24:04 -04:00
|
|
|
nextSeg = seg.getNext(),
|
|
|
|
nextInter = nextSeg._intersection;
|
2015-09-19 13:07:44 -04:00
|
|
|
if (window.reportSegments) {
|
2015-09-21 09:44:17 -04:00
|
|
|
console.log('getIntersection(' + strict + ')'
|
2015-09-19 13:07:44 -04:00
|
|
|
+ ', seg: ' + seg._path._id + '.' +seg._index
|
2015-10-04 22:24:04 -04:00
|
|
|
+ ', next: ' + nextSeg._path._id + '.' + nextSeg._index
|
2015-09-20 08:17:23 -04:00
|
|
|
+ ', seg vis:' + !!seg._visited
|
2015-10-04 22:24:04 -04:00
|
|
|
+ ', next vis:' + !!nextSeg._visited
|
|
|
|
+ ', next start:' + (nextSeg === start
|
|
|
|
|| nextSeg === otherStart)
|
2015-09-21 09:44:17 -04:00
|
|
|
+ ', seg wi:' + seg._winding
|
2015-10-04 22:24:04 -04:00
|
|
|
+ ', next wi:' + nextSeg._winding
|
2015-09-20 09:50:26 -04:00
|
|
|
+ ', seg op:' + isValid(seg, true)
|
2015-10-06 15:09:35 -04:00
|
|
|
+ ', next op:' + ((!strict || !isOverlap(seg, nextSeg))
|
|
|
|
&& isValid(nextSeg, true)
|
|
|
|
|| !strict && nextInter
|
|
|
|
&& isValid(nextInter._segment, true))
|
|
|
|
+ ', seg ov: ' + !!(seg._intersection
|
2015-09-21 09:44:17 -04:00
|
|
|
&& seg._intersection._overlap)
|
2015-10-06 15:09:35 -04:00
|
|
|
+ ', next ov: ' + !!(nextSeg._intersection
|
2015-10-04 22:24:04 -04:00
|
|
|
&& nextSeg._intersection._overlap)
|
2015-09-21 09:44:17 -04:00
|
|
|
+ ', more: ' + (!!inter._next));
|
2015-09-19 13:07:44 -04:00
|
|
|
}
|
2015-10-04 22:24:04 -04:00
|
|
|
// See if this segment and the next are both not visited yet, or are
|
2015-09-20 08:17:23 -04:00
|
|
|
// bringing us back to the beginning, and are both part of the
|
|
|
|
// boolean result.
|
2015-09-21 09:44:17 -04:00
|
|
|
// Handling overlaps correctly here is a bit tricky business, and
|
|
|
|
// requires two passes, first with `strict = true`, then `false`:
|
|
|
|
// In strict mode, the current segment and the next segment are both
|
|
|
|
// checked for validity, and only the current one is allowed to be
|
|
|
|
// an overlap (passing true for `unadjusted` in isValid()). If this
|
|
|
|
// pass does not yield a result, the non-strict mode is used, in
|
|
|
|
// which invalid current segments are tolerated, and overlaps for
|
|
|
|
// the next segment are allowed as long as they are valid when not
|
|
|
|
// adjusted.
|
2015-10-04 22:24:04 -04:00
|
|
|
return nextSeg === start || nextSeg === otherStart
|
|
|
|
|| !seg._visited && !nextSeg._visited
|
|
|
|
// Self-intersections (!operator) don't need isValid() calls
|
|
|
|
&& (!operator
|
|
|
|
// NOTE: We need to use the unadjusted winding here
|
|
|
|
// since an overlap crossing might have brought us here,
|
|
|
|
// in which case isValid(seg, false) might be false.
|
|
|
|
|| (!strict || isValid(seg, true))
|
2015-10-06 15:09:35 -04:00
|
|
|
// Do not consider the nextSeg in strict mode if it is
|
|
|
|
// part of an overlap, in order to give non-overlapping
|
|
|
|
// options that might follow the priority over overlaps.
|
|
|
|
&& (!(strict && isOverlap(seg, nextSeg))
|
|
|
|
&& isValid(nextSeg, true)
|
|
|
|
// If next segment is not valid, its intersection to
|
|
|
|
// which we may switch might be, so allow that too!
|
|
|
|
|| !strict && nextInter
|
|
|
|
&& isValid(nextInter._segment, true))
|
2015-10-04 22:24:04 -04:00
|
|
|
)
|
2015-09-20 09:50:26 -04:00
|
|
|
? inter
|
2015-10-06 15:09:35 -04:00
|
|
|
// If it's no match, continue with the next linked intersection.
|
|
|
|
: getIntersection(inter._next, strict)
|
2015-09-19 13:07:44 -04:00
|
|
|
}
|
2015-09-13 16:12:04 -04:00
|
|
|
for (var i = 0, l = segments.length; i < l; i++) {
|
2015-09-19 07:21:29 -04:00
|
|
|
var seg = segments[i],
|
2015-10-01 22:09:30 -04:00
|
|
|
path = null;
|
2015-09-19 13:07:44 -04:00
|
|
|
// Do not start a chain with already visited segments, and segments
|
|
|
|
// that are not going to be part of the resulting operation.
|
2015-09-20 09:50:26 -04:00
|
|
|
if (seg._visited || !isValid(seg))
|
2015-09-19 13:07:44 -04:00
|
|
|
continue;
|
|
|
|
start = otherStart = null;
|
|
|
|
while (true) {
|
|
|
|
var inter = seg._intersection;
|
|
|
|
// Once we started a chain, see if there are multiple
|
|
|
|
// intersections, and if so, pick the best one:
|
2015-09-21 12:44:53 -04:00
|
|
|
if (inter && window.reportSegments) {
|
2015-09-21 09:44:17 -04:00
|
|
|
console.log('-----\n'
|
|
|
|
+'#' + pathCount + '.'
|
|
|
|
+ (path ? path._segments.length + 1 : 1)
|
|
|
|
+ ', Before getIntersection()'
|
|
|
|
+ ', seg: ' + seg._path._id + '.' + seg._index
|
|
|
|
+ ', other: ' + inter._segment._path._id + '.'
|
|
|
|
+ inter._segment._index);
|
2015-09-19 16:47:57 -04:00
|
|
|
}
|
2015-10-06 15:09:35 -04:00
|
|
|
inter = inter && (getIntersection(inter, true)
|
|
|
|
|| getIntersection(inter, false)) || inter;
|
2015-09-21 09:42:47 -04:00
|
|
|
var other = inter && inter._segment;
|
2015-09-19 13:07:44 -04:00
|
|
|
// A switched intersection means we may have changed the segment
|
|
|
|
// Point to the other segment in the selected intersection.
|
2015-09-21 12:44:53 -04:00
|
|
|
if (inter && window.reportSegments) {
|
2015-09-21 09:44:17 -04:00
|
|
|
console.log('After getIntersection()'
|
|
|
|
+ ', seg: '
|
|
|
|
+ seg._path._id + '.' + seg._index
|
|
|
|
+ ', other: ' + inter._segment._path._id + '.'
|
|
|
|
+ inter._segment._index);
|
2015-09-20 09:29:54 -04:00
|
|
|
}
|
2015-10-01 22:09:30 -04:00
|
|
|
if (seg === start || seg === otherStart) {
|
2015-09-19 13:07:44 -04:00
|
|
|
// We've come back to the start, bail out as we're done.
|
2015-09-21 09:42:47 -04:00
|
|
|
drawSegment(seg, null, 'done', i, 'red');
|
2015-09-19 13:07:44 -04:00
|
|
|
break;
|
2015-09-20 09:50:26 -04:00
|
|
|
} else if (!inter && !isValid(seg)) {
|
2015-09-19 13:07:44 -04:00
|
|
|
// Intersections are always part of the resulting path, for
|
|
|
|
// all other segments check the winding contribution to see
|
|
|
|
// if they are to be kept. If not, the chain has to end here
|
2015-09-21 09:42:47 -04:00
|
|
|
drawSegment(seg, null, 'discard', i, 'red');
|
2015-09-19 13:07:44 -04:00
|
|
|
console.error('Excluded segment encountered, aborting #'
|
|
|
|
+ pathCount + '.' +
|
|
|
|
(path ? path._segments.length + 1 : 1));
|
|
|
|
break;
|
|
|
|
}
|
2015-10-01 22:09:30 -04:00
|
|
|
var handleIn = path && seg._handleIn;
|
2015-10-01 22:11:51 -04:00
|
|
|
if (!path || !other) {
|
2015-09-15 13:39:35 -04:00
|
|
|
// Just add the first segment and all segments that have no
|
|
|
|
// intersection.
|
2015-09-21 09:42:47 -04:00
|
|
|
drawSegment(seg, null, 'add', i, 'black');
|
2015-09-18 11:51:03 -04:00
|
|
|
} else if (!operator) { // Resolve self-intersections
|
2015-09-21 09:42:47 -04:00
|
|
|
drawSegment(seg, other, 'self-int', i, 'purple');
|
2015-09-15 13:39:35 -04:00
|
|
|
// Switch to the intersecting segment, as we need to
|
|
|
|
// resolving self-Intersections.
|
|
|
|
seg = other;
|
2015-09-18 16:29:29 -04:00
|
|
|
} else if (inter._overlap && operation !== 'intersect') {
|
|
|
|
// Switch to the overlapping intersecting segment if it is
|
2015-09-20 09:50:26 -04:00
|
|
|
// part of the boolean result. Do not adjust for overlap!
|
|
|
|
if (isValid(other, true)) {
|
2015-09-21 09:42:47 -04:00
|
|
|
drawSegment(seg, other, 'overlap-cross', i, 'orange');
|
2015-09-18 16:29:29 -04:00
|
|
|
seg = other;
|
|
|
|
} else {
|
2015-09-24 07:47:39 -04:00
|
|
|
drawSegment(seg, other, 'overlap-stay', i, 'orange');
|
2015-09-18 16:29:29 -04:00
|
|
|
}
|
2015-09-15 13:39:35 -04:00
|
|
|
} else if (operation === 'exclude') {
|
|
|
|
// We need to handle exclusion separately, as we want to
|
2015-09-18 11:51:43 -04:00
|
|
|
// switch at each crossing.
|
2015-09-21 09:42:47 -04:00
|
|
|
drawSegment(seg, other, 'exclude-cross', i, 'green');
|
2015-09-20 08:16:47 -04:00
|
|
|
seg = other;
|
2015-10-02 19:57:45 -04:00
|
|
|
} else if (!other._visited && isValid(other)) {
|
2015-09-16 03:52:41 -04:00
|
|
|
// The other segment is part of the boolean result, and we
|
|
|
|
// are at crossing, switch over.
|
2015-09-21 09:42:47 -04:00
|
|
|
drawSegment(seg, other, 'cross', i, 'green');
|
2015-09-16 03:52:41 -04:00
|
|
|
seg = other;
|
2015-08-23 15:19:19 -04:00
|
|
|
} else {
|
2015-09-16 03:52:41 -04:00
|
|
|
// Keep on truckin'
|
2015-09-21 09:42:47 -04:00
|
|
|
drawSegment(seg, null, 'stay', i, 'blue');
|
2015-01-02 09:33:23 -05:00
|
|
|
}
|
2015-09-15 10:31:05 -04:00
|
|
|
if (seg._visited) {
|
2015-10-04 23:34:22 -04:00
|
|
|
// TODO: || !isValid(seg) ?
|
2015-09-15 10:31:05 -04:00
|
|
|
// We didn't manage to switch, so stop right here.
|
2015-09-21 12:44:53 -04:00
|
|
|
console.error('Visited segment encountered, aborting #'
|
|
|
|
+ pathCount + '.'
|
2015-09-18 11:33:42 -04:00
|
|
|
+ (path ? path._segments.length + 1 : 1)
|
2015-09-19 07:21:29 -04:00
|
|
|
+ ', id: ' + seg._path._id + '.' + seg._index
|
2015-10-04 22:24:04 -04:00
|
|
|
+ ', multiple: ' + !!(inter && inter._next));
|
2015-09-15 10:31:05 -04:00
|
|
|
break;
|
|
|
|
}
|
2015-10-01 22:09:30 -04:00
|
|
|
if (!path) {
|
|
|
|
path = new Path(Item.NO_INSERT);
|
|
|
|
start = seg;
|
|
|
|
otherStart = other;
|
|
|
|
}
|
2015-01-02 09:33:23 -05:00
|
|
|
// Add the current segment to the path, and mark the added
|
|
|
|
// segment as visited.
|
2015-09-15 10:31:05 -04:00
|
|
|
path.add(new Segment(seg._point, handleIn, seg._handleOut));
|
2015-10-01 22:09:30 -04:00
|
|
|
seg._visited = true;
|
2015-09-15 10:31:05 -04:00
|
|
|
seg = seg.getNext();
|
2015-09-19 13:07:44 -04:00
|
|
|
}
|
2015-10-01 22:09:30 -04:00
|
|
|
if (!path)
|
2015-09-19 13:07:44 -04:00
|
|
|
continue;
|
2015-01-02 09:33:23 -05:00
|
|
|
// Finish with closing the paths if necessary, correctly linking up
|
|
|
|
// curves etc.
|
2015-09-15 13:39:35 -04:00
|
|
|
if (seg === start || seg === otherStart) {
|
2015-09-13 16:12:04 -04:00
|
|
|
path.firstSegment.setHandleIn(seg._handleIn);
|
2015-08-26 11:36:20 -04:00
|
|
|
path.setClosed(true);
|
2015-09-13 18:51:46 -04:00
|
|
|
if (window.reportSegments) {
|
2015-08-26 11:36:20 -04:00
|
|
|
console.log('Boolean operation completed',
|
2015-09-15 10:31:05 -04:00
|
|
|
'#' + pathCount + '.' +
|
2015-08-26 11:36:20 -04:00
|
|
|
(path ? path._segments.length + 1 : 1));
|
|
|
|
}
|
2015-01-02 09:33:23 -05:00
|
|
|
} else {
|
2015-08-26 11:36:20 -04:00
|
|
|
// path.lastSegment._handleOut.set(0, 0);
|
|
|
|
console.error('Boolean operation results in open path, segs =',
|
|
|
|
path._segments.length, 'length = ', path.getLength(),
|
2015-09-15 10:31:05 -04:00
|
|
|
'#' + pathCount + '.' +
|
2015-08-26 11:36:20 -04:00
|
|
|
(path ? path._segments.length + 1 : 1));
|
2015-10-05 11:42:01 -04:00
|
|
|
paper.project.activeLayer.addChild(path);
|
|
|
|
path.strokeColor = 'cyan';
|
|
|
|
path.strokeWidth = 2;
|
|
|
|
path.strokeScaling = false;
|
2015-08-26 11:36:20 -04:00
|
|
|
path = null;
|
2015-01-02 09:33:23 -05:00
|
|
|
}
|
|
|
|
// Add the path to the result, while avoiding stray segments and
|
2015-09-06 10:35:15 -04:00
|
|
|
// paths that are incomplete or cover no area.
|
|
|
|
// As an optimization, only check paths with 4 or less segments
|
|
|
|
// for their area, and assume that they cover an area when more.
|
|
|
|
if (path && (path._segments.length > 4
|
2015-10-01 22:09:30 -04:00
|
|
|
|| !Numerical.isZero(path.getArea()))) {
|
2015-01-02 09:33:23 -05:00
|
|
|
paths.push(path);
|
2015-10-01 22:09:30 -04:00
|
|
|
path = null;
|
|
|
|
}
|
2015-09-15 10:31:05 -04:00
|
|
|
pathCount++;
|
2015-01-02 09:33:23 -05:00
|
|
|
}
|
|
|
|
return paths;
|
|
|
|
}
|
2014-02-20 14:24:16 -05:00
|
|
|
|
2015-01-02 09:33:23 -05:00
|
|
|
return /** @lends PathItem# */{
|
|
|
|
/**
|
|
|
|
* Returns the winding contribution of the given point with respect to
|
|
|
|
* this PathItem.
|
|
|
|
*
|
|
|
|
* @param {Point} point the location for which to determine the winding
|
|
|
|
* direction
|
|
|
|
* @param {Boolean} horizontal whether we need to consider this point as
|
|
|
|
* part of a horizontal curve
|
|
|
|
* @param {Boolean} testContains whether we need to consider this point
|
|
|
|
* as part of stationary points on the curve itself, used when checking
|
2015-06-16 11:50:37 -04:00
|
|
|
* the winding about a point
|
2015-01-02 09:33:23 -05:00
|
|
|
* @return {Number} the winding number
|
|
|
|
*/
|
|
|
|
_getWinding: function(point, horizontal, testContains) {
|
|
|
|
return getWinding(point, this._getMonoCurves(),
|
|
|
|
horizontal, testContains);
|
|
|
|
},
|
2014-02-20 14:24:16 -05:00
|
|
|
|
2015-01-02 09:33:23 -05:00
|
|
|
/**
|
|
|
|
* {@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.
|
|
|
|
*
|
|
|
|
* @param {PathItem} path the path to unite with
|
|
|
|
* @return {PathItem} the resulting path item
|
|
|
|
*/
|
|
|
|
unite: function(path) {
|
2015-01-03 19:50:24 -05:00
|
|
|
return computeBoolean(this, path, 'unite');
|
2015-01-02 09:33:23 -05:00
|
|
|
},
|
2014-02-20 14:24:16 -05:00
|
|
|
|
2015-01-02 09:33:23 -05:00
|
|
|
/**
|
|
|
|
* Intersects the geometry of the specified path with this path's
|
|
|
|
* geometry and returns the result as a new path item.
|
|
|
|
*
|
|
|
|
* @param {PathItem} path the path to intersect with
|
|
|
|
* @return {PathItem} the resulting path item
|
|
|
|
*/
|
|
|
|
intersect: function(path) {
|
2015-01-03 19:50:24 -05:00
|
|
|
return computeBoolean(this, path, 'intersect');
|
2015-01-02 09:33:23 -05:00
|
|
|
},
|
2014-02-20 14:24:16 -05:00
|
|
|
|
2015-01-02 09:33:23 -05:00
|
|
|
/**
|
|
|
|
* Subtracts the geometry of the specified path from this path's
|
|
|
|
* geometry and returns the result as a new path item.
|
|
|
|
*
|
|
|
|
* @param {PathItem} path the path to subtract
|
|
|
|
* @return {PathItem} the resulting path item
|
|
|
|
*/
|
|
|
|
subtract: function(path) {
|
2015-01-03 19:50:24 -05:00
|
|
|
return computeBoolean(this, path, 'subtract');
|
2015-01-02 09:33:23 -05:00
|
|
|
},
|
2014-02-20 14:24:16 -05:00
|
|
|
|
2015-01-02 09:33:23 -05:00
|
|
|
// 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.
|
|
|
|
*
|
|
|
|
* @param {PathItem} path the path to exclude the intersection of
|
|
|
|
* @return {Group} the resulting group item
|
|
|
|
*/
|
|
|
|
exclude: function(path) {
|
2015-01-03 19:50:24 -05:00
|
|
|
return computeBoolean(this, path, 'exclude');
|
2015-09-18 11:51:57 -04:00
|
|
|
// return finishBoolean([this.subtract(path), path.subtract(this)],
|
|
|
|
// this, path, true);
|
2015-01-02 09:33:23 -05:00
|
|
|
},
|
2014-04-06 07:48:03 -04:00
|
|
|
|
2015-01-02 09:33:23 -05:00
|
|
|
/**
|
|
|
|
* Splits the geometry of this path along the geometry of the specified
|
|
|
|
* path returns the result as a new group item.
|
|
|
|
*
|
|
|
|
* @param {PathItem} path the path to divide by
|
|
|
|
* @return {Group} the resulting group item
|
|
|
|
*/
|
|
|
|
divide: function(path) {
|
2015-09-18 11:51:57 -04:00
|
|
|
return finishBoolean([this.subtract(path), this.intersect(path)],
|
|
|
|
this, path, true);
|
2015-09-18 11:51:03 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
resolveCrossings: function() {
|
|
|
|
var reportSegments = window.reportSegments;
|
2015-09-20 08:16:47 -04:00
|
|
|
var reportWindings = window.reportWindings;
|
2015-09-18 11:51:03 -04:00
|
|
|
var reportIntersections = window.reportIntersections;
|
|
|
|
window.reportSegments = false;
|
2015-09-20 08:16:47 -04:00
|
|
|
window.reportWindings = false;
|
2015-09-18 11:51:03 -04:00
|
|
|
window.reportIntersections = false;
|
2015-09-21 10:54:33 -04:00
|
|
|
var crossings = this.getCrossings();
|
|
|
|
if (!crossings.length) {
|
|
|
|
window.reportSegments = reportSegments;
|
|
|
|
window.reportWindings = reportWindings;
|
|
|
|
window.reportIntersections = reportIntersections;
|
|
|
|
return this.reorient();
|
|
|
|
}
|
2015-09-20 08:16:47 -04:00
|
|
|
splitPath(CurveLocation.expand(crossings));
|
2015-09-20 16:39:28 -04:00
|
|
|
var paths = this._children || [this],
|
2015-09-18 11:51:03 -04:00
|
|
|
segments = [];
|
|
|
|
for (var i = 0, l = paths.length; i < l; i++) {
|
|
|
|
segments.push.apply(segments, paths[i]._segments);
|
|
|
|
}
|
|
|
|
var res = finishBoolean(tracePaths(segments), this, null, false)
|
|
|
|
.reorient();
|
|
|
|
window.reportSegments = reportSegments;
|
2015-09-20 08:16:47 -04:00
|
|
|
window.reportWindings = reportWindings;
|
2015-09-18 11:51:03 -04:00
|
|
|
window.reportIntersections = reportIntersections;
|
|
|
|
return res;
|
2015-01-02 09:33:23 -05:00
|
|
|
}
|
|
|
|
};
|
2014-02-20 14:24:16 -05:00
|
|
|
});
|
2014-02-20 14:00:46 -05:00
|
|
|
|
|
|
|
Path.inject(/** @lends Path# */{
|
2015-01-02 09:33:23 -05:00
|
|
|
/**
|
2015-08-24 06:59:10 -04:00
|
|
|
* Private method that returns and caches all the curves in this Path,
|
|
|
|
* which are monotonically decreasing or increasing in the y-direction.
|
2015-01-02 09:33:23 -05:00
|
|
|
* Used by getWinding().
|
|
|
|
*/
|
|
|
|
_getMonoCurves: function() {
|
|
|
|
var monoCurves = this._monoCurves,
|
|
|
|
prevCurve;
|
2014-02-20 14:00:46 -05:00
|
|
|
|
2015-01-02 09:33:23 -05:00
|
|
|
// Insert curve values into a cached array
|
|
|
|
function insertCurve(v) {
|
|
|
|
var y0 = v[1],
|
|
|
|
y1 = v[7],
|
|
|
|
curve = {
|
|
|
|
values: v,
|
|
|
|
winding: y0 === y1
|
|
|
|
? 0 // Horizontal
|
|
|
|
: y0 > y1
|
|
|
|
? -1 // Decreasing
|
|
|
|
: 1, // Increasing
|
|
|
|
// Add a reference to neighboring curves.
|
|
|
|
previous: prevCurve,
|
|
|
|
next: null // Always set it for hidden class optimization.
|
|
|
|
};
|
|
|
|
if (prevCurve)
|
|
|
|
prevCurve.next = curve;
|
|
|
|
monoCurves.push(curve);
|
|
|
|
prevCurve = curve;
|
|
|
|
}
|
2014-02-20 14:00:46 -05:00
|
|
|
|
2015-01-02 09:33:23 -05:00
|
|
|
// Handle bezier curves. We need to chop them into smaller curves with
|
|
|
|
// defined orientation, by solving the derivative curve for y extrema.
|
|
|
|
function handleCurve(v) {
|
|
|
|
// Filter out curves of zero length.
|
|
|
|
// TODO: Do not filter this here.
|
|
|
|
if (Curve.getLength(v) === 0)
|
|
|
|
return;
|
|
|
|
var y0 = v[1],
|
|
|
|
y1 = v[3],
|
|
|
|
y2 = v[5],
|
|
|
|
y3 = v[7];
|
2015-09-06 11:35:27 -04:00
|
|
|
if (Curve.isStraight(v)) {
|
|
|
|
// Handling straight curves is easy.
|
2015-01-02 09:33:23 -05:00
|
|
|
insertCurve(v);
|
|
|
|
} else {
|
|
|
|
// Split the curve at y extrema, to get bezier curves with clear
|
|
|
|
// orientation: Calculate the derivative and find its roots.
|
|
|
|
var a = 3 * (y1 - y2) - y0 + y3,
|
|
|
|
b = 2 * (y0 + y2) - 4 * y1,
|
|
|
|
c = y1 - y0,
|
2015-09-12 16:55:58 -04:00
|
|
|
tMin = /*#=*/Numerical.CURVETIME_EPSILON,
|
2015-09-12 16:14:04 -04:00
|
|
|
tMax = 1 - tMin,
|
|
|
|
roots = [],
|
|
|
|
// Keep then range to 0 .. 1 (excluding) in the search for y
|
|
|
|
// extrema.
|
|
|
|
n = Numerical.solveQuadratic(a, b, c, roots, tMin, tMax);
|
|
|
|
if (n === 0) {
|
2015-01-02 09:33:23 -05:00
|
|
|
insertCurve(v);
|
|
|
|
} else {
|
|
|
|
roots.sort();
|
|
|
|
var t = roots[0],
|
|
|
|
parts = Curve.subdivide(v, t);
|
|
|
|
insertCurve(parts[0]);
|
2015-09-12 16:14:04 -04:00
|
|
|
if (n > 1) {
|
2015-01-02 09:33:23 -05:00
|
|
|
// If there are two extrema, renormalize t to the range
|
|
|
|
// of the second range and split again.
|
|
|
|
t = (roots[1] - t) / (1 - t);
|
|
|
|
// Since we already processed parts[0], we can override
|
|
|
|
// the parts array with the new pair now.
|
|
|
|
parts = Curve.subdivide(parts[1], t);
|
|
|
|
insertCurve(parts[0]);
|
|
|
|
}
|
|
|
|
insertCurve(parts[1]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2014-02-20 14:00:46 -05:00
|
|
|
|
2015-01-02 09:33:23 -05:00
|
|
|
if (!monoCurves) {
|
|
|
|
// Insert curves that are monotonic in y direction into cached array
|
|
|
|
monoCurves = this._monoCurves = [];
|
|
|
|
var curves = this.getCurves(),
|
|
|
|
segments = this._segments;
|
|
|
|
for (var i = 0, l = curves.length; i < l; i++)
|
|
|
|
handleCurve(curves[i].getValues());
|
|
|
|
// If the path is not closed, we need to join the end points with a
|
|
|
|
// straight line, just like how filling open paths works.
|
|
|
|
if (!this._closed && segments.length > 1) {
|
|
|
|
var p1 = segments[segments.length - 1]._point,
|
|
|
|
p2 = segments[0]._point,
|
|
|
|
p1x = p1._x, p1y = p1._y,
|
|
|
|
p2x = p2._x, p2y = p2._y;
|
|
|
|
handleCurve([p1x, p1y, p1x, p1y, p2x, p2y, p2x, p2y]);
|
|
|
|
}
|
|
|
|
if (monoCurves.length > 0) {
|
|
|
|
// Link first and last curves
|
|
|
|
var first = monoCurves[0],
|
|
|
|
last = monoCurves[monoCurves.length - 1];
|
|
|
|
first.previous = last;
|
|
|
|
last.next = first;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return monoCurves;
|
|
|
|
},
|
2014-03-17 04:48:00 -04:00
|
|
|
|
2015-01-02 09:33:23 -05:00
|
|
|
/**
|
|
|
|
* Returns a point that is guaranteed to be inside the path.
|
|
|
|
*
|
|
|
|
* @type Point
|
|
|
|
* @bean
|
|
|
|
*/
|
|
|
|
getInteriorPoint: function() {
|
|
|
|
var bounds = this.getBounds(),
|
|
|
|
point = bounds.getCenter(true);
|
|
|
|
if (!this.contains(point)) {
|
|
|
|
// Since there is no guarantee that a poly-bezier path contains
|
|
|
|
// the center of its bounding rectangle, we shoot a ray in
|
|
|
|
// +x direction from the center and select a point between
|
|
|
|
// consecutive intersections of the ray
|
|
|
|
var curves = this._getMonoCurves(),
|
|
|
|
roots = [],
|
|
|
|
y = point.y,
|
|
|
|
xIntercepts = [];
|
|
|
|
for (var i = 0, l = curves.length; i < l; i++) {
|
|
|
|
var values = curves[i].values;
|
|
|
|
if ((curves[i].winding === 1
|
|
|
|
&& y >= values[1] && y <= values[7]
|
|
|
|
|| y >= values[7] && y <= values[1])
|
|
|
|
&& Curve.solveCubic(values, 1, y, roots, 0, 1) > 0) {
|
|
|
|
for (var j = roots.length - 1; j >= 0; j--)
|
2015-08-19 11:15:41 -04:00
|
|
|
xIntercepts.push(Curve.getPoint(values, roots[j]).x);
|
2015-01-02 09:33:23 -05:00
|
|
|
}
|
|
|
|
if (xIntercepts.length > 1)
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
point.x = (xIntercepts[0] + xIntercepts[1]) / 2;
|
|
|
|
}
|
|
|
|
return point;
|
|
|
|
},
|
2014-03-17 05:04:09 -04:00
|
|
|
|
2015-01-02 09:33:23 -05:00
|
|
|
reorient: function() {
|
|
|
|
// Paths that are not part of compound paths should never be counter-
|
|
|
|
// clockwise for boolean operations.
|
|
|
|
this.setClockwise(true);
|
|
|
|
return this;
|
|
|
|
}
|
2014-02-20 14:00:46 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
CompoundPath.inject(/** @lends CompoundPath# */{
|
2015-01-02 09:33:23 -05:00
|
|
|
/**
|
|
|
|
* Private method that returns all the curves in this CompoundPath, which
|
|
|
|
* are monotonically decreasing or increasing in the 'y' direction.
|
|
|
|
* Used by getWinding().
|
|
|
|
*/
|
|
|
|
_getMonoCurves: function() {
|
|
|
|
var children = this._children,
|
|
|
|
monoCurves = [];
|
|
|
|
for (var i = 0, l = children.length; i < l; i++)
|
|
|
|
monoCurves.push.apply(monoCurves, children[i]._getMonoCurves());
|
|
|
|
return monoCurves;
|
|
|
|
},
|
2014-03-17 04:48:00 -04:00
|
|
|
|
2015-01-02 09:33:23 -05:00
|
|
|
/*
|
|
|
|
* Fixes the orientation of a CompoundPath's child paths by first ordering
|
|
|
|
* them according to their area, and then making sure that all children are
|
|
|
|
* of different winding direction than the first child, except for when
|
|
|
|
* some individual contours are disjoint, i.e. islands, they are reoriented
|
|
|
|
* so that:
|
|
|
|
* - The holes have opposite winding direction.
|
|
|
|
* - Islands have to have the same winding direction as the first child.
|
|
|
|
*/
|
|
|
|
// NOTE: Does NOT handle self-intersecting CompoundPaths.
|
|
|
|
reorient: function() {
|
|
|
|
var children = this.removeChildren().sort(function(a, b) {
|
|
|
|
return b.getBounds().getArea() - a.getBounds().getArea();
|
|
|
|
});
|
2015-01-02 18:46:24 -05:00
|
|
|
if (children.length > 0) {
|
|
|
|
this.addChildren(children);
|
|
|
|
var clockwise = children[0].isClockwise();
|
|
|
|
// Skip the first child
|
|
|
|
for (var i = 1, l = children.length; i < l; i++) {
|
|
|
|
var point = children[i].getInteriorPoint(),
|
|
|
|
counters = 0;
|
|
|
|
for (var j = i - 1; j >= 0; j--) {
|
|
|
|
if (children[j].contains(point))
|
|
|
|
counters++;
|
|
|
|
}
|
|
|
|
children[i].setClockwise(counters % 2 === 0 && clockwise);
|
2015-01-02 09:33:23 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return this;
|
|
|
|
}
|
2014-03-12 08:34:43 -04:00
|
|
|
});
|