2013-05-03 16:16:52 -07:00
|
|
|
/*
|
|
|
|
* Paper.js - The Swiss Army Knife of Vector Graphics Scripting.
|
|
|
|
* http://paperjs.org/
|
|
|
|
*
|
2015-12-27 18:09:25 +01:00
|
|
|
* Copyright (c) 2011 - 2016, Juerg Lehni & Jonathan Puckey
|
2014-01-04 01:47:16 +01:00
|
|
|
* http://scratchdisk.com/ & http://jonathanpuckey.com/
|
2013-05-03 16:16:52 -07:00
|
|
|
*
|
|
|
|
* Distributed under the MIT license. See LICENSE file for details.
|
|
|
|
*
|
|
|
|
* All rights reserved.
|
|
|
|
*/
|
|
|
|
|
|
|
|
/*
|
2013-05-03 16:31:36 -07:00
|
|
|
* Boolean Geometric Path Operations
|
2013-05-03 16:16:52 -07:00
|
|
|
*
|
|
|
|
* Supported
|
2013-05-05 16:38:18 -07:00
|
|
|
* - Path and CompoundPath items
|
2013-05-03 16:16:52 -07:00
|
|
|
* - Boolean Union
|
|
|
|
* - Boolean Intersection
|
|
|
|
* - Boolean Subtraction
|
2015-09-12 11:58:17 +02:00
|
|
|
* - Boolean Exclusion
|
|
|
|
* - Resolving a self-intersecting Path items
|
|
|
|
* - Boolean operations on self-intersecting Paths items
|
2013-05-03 16:16:52 -07:00
|
|
|
*
|
|
|
|
* @author Harikrishnan Gopalakrishnan
|
|
|
|
* http://hkrish.com/playground/paperjs/booleanStudy.html
|
|
|
|
*/
|
2014-02-20 20:24:16 +01:00
|
|
|
PathItem.inject(new function() {
|
2016-01-05 11:06:06 +01:00
|
|
|
// Set up lookup tables for each operator, to decide if a given segment is
|
|
|
|
// to be considered a part of the solution, or to be discarded, based on its
|
|
|
|
// winding contribution, as calculated by propagateWinding().
|
|
|
|
// Boolean operators return true if a segment with the given winding
|
|
|
|
// contribution contributes to the final result or not. They are applied to
|
|
|
|
// for each segment after the paths are split at crossings.
|
2015-01-04 01:50:24 +01:00
|
|
|
var operators = {
|
2016-01-05 10:30:33 +01:00
|
|
|
unite: { 0: true, 1: true },
|
|
|
|
intersect: { 2: true },
|
|
|
|
subtract: { 1: true },
|
2016-01-05 11:06:06 +01:00
|
|
|
exclude: { 1: true }
|
2015-01-04 01:50:24 +01:00
|
|
|
};
|
|
|
|
|
2015-10-24 23:13:13 +02:00
|
|
|
/*
|
|
|
|
* Creates a clone of the path that we can modify freely, with its matrix
|
|
|
|
* applied to its geometry. Calls #reduce() to simplify compound paths and
|
2015-12-26 12:52:32 +01:00
|
|
|
* remove empty curves, #resolveCrossings() to resolve self-intersection
|
|
|
|
* make sure all paths have correct winding direction.
|
2015-10-24 23:13:13 +02:00
|
|
|
*/
|
2015-10-24 22:41:51 +02:00
|
|
|
function preparePath(path, resolve) {
|
2016-01-06 10:53:50 +01:00
|
|
|
var res = path.clone(false).reduce({ simplify: true })
|
|
|
|
.transform(null, true, true);
|
2015-12-26 12:52:32 +01:00
|
|
|
return resolve ? res.resolveCrossings() : res;
|
2015-09-13 22:12:04 +02:00
|
|
|
}
|
|
|
|
|
2016-01-06 11:50:32 +01:00
|
|
|
function finishResult(result, path1, path2) {
|
2015-09-15 14:11:27 +02:00
|
|
|
// Insert the resulting path above whichever of the two paths appear
|
|
|
|
// further up in the stack.
|
|
|
|
result.insertAbove(path2 && path1.isSibling(path2)
|
2016-01-06 11:50:32 +01:00
|
|
|
&& path1.getIndex() < path2.getIndex() ? path2 : path1);
|
2015-12-26 22:44:48 +01:00
|
|
|
// Copy over the input path attributes, excluding matrix and we're done.
|
|
|
|
result.copyAttributes(path1, true);
|
2015-09-15 14:11:27 +02:00
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2016-01-06 11:50:32 +01:00
|
|
|
function createResult(ctor, paths, reduce) {
|
|
|
|
var result = new ctor(Item.NO_INSERT);
|
|
|
|
result.addChildren(paths, true);
|
|
|
|
// See if the item can be reduced to just a simple Path.
|
|
|
|
if (reduce)
|
|
|
|
result = result.reduce({ simplify: true });
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2015-01-04 01:50:24 +01:00
|
|
|
function computeBoolean(path1, path2, operation) {
|
2015-10-24 22:41:51 +02:00
|
|
|
// If path1 is open, delegate to computeOpenBoolean()
|
|
|
|
if (!path1._children && !path1._closed)
|
|
|
|
return computeOpenBoolean(path1, path2, operation);
|
2015-01-04 01:50:24 +01: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 15:33:23 +01:00
|
|
|
// i.e. subtraction(A:Path, B:Path):CompoundPath etc.
|
2015-10-24 22:41:51 +02:00
|
|
|
var _path1 = preparePath(path1, true),
|
2016-01-05 10:30:33 +01:00
|
|
|
_path2 = path2 && path1 !== path2 && preparePath(path2, true),
|
2016-01-05 11:06:06 +01:00
|
|
|
// Retrieve the operator lookup table for winding numbers.
|
2016-01-05 11:10:59 +01:00
|
|
|
operator = operators[operation];
|
|
|
|
// Add a simple boolean property to check for a given operation,
|
|
|
|
// e.g. `if (operator.unite)`
|
2016-01-05 10:30:33 +01:00
|
|
|
operator[operation] = true;
|
2015-01-04 01:51:27 +01:00
|
|
|
// Give both paths the same orientation except for subtraction
|
2015-01-04 01:50:24 +01:00
|
|
|
// and exclusion, where we need them at opposite orientation.
|
2016-01-05 10:30:33 +01:00
|
|
|
if (_path2 && (operator.subtract || operator.exclude)
|
2015-01-04 01:51:27 +01:00
|
|
|
^ (_path2.isClockwise() !== _path1.isClockwise()))
|
2015-01-02 15:33:23 +01:00
|
|
|
_path2.reverse();
|
2015-11-04 01:19:20 +01:00
|
|
|
// Split curves at crossings on both paths. Note that for self-
|
|
|
|
// intersection, path2 is null and getIntersections() handles it.
|
2016-01-07 11:18:46 +01:00
|
|
|
var crossings = divideLocations(
|
|
|
|
CurveLocation.expand(_path1.getCrossings(_path2))),
|
2016-01-06 10:53:50 +01:00
|
|
|
segments = [],
|
|
|
|
// Aggregate of all curves in both operands, monotonic in y.
|
2016-01-05 10:30:33 +01:00
|
|
|
monoCurves = [],
|
2016-01-06 10:53:50 +01:00
|
|
|
// Keep track if all encountered segments are overlaps.
|
2016-01-06 11:50:32 +01:00
|
|
|
overlapsOnly = true,
|
|
|
|
validOverlapsOnly = true;
|
2014-01-26 05:39:51 +01:00
|
|
|
|
2015-01-02 15:33:23 +01: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 19:10:46 +01:00
|
|
|
|
2015-01-02 15:33:23 +01: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-11-04 01:19:20 +01:00
|
|
|
// does not change between two crossings.
|
2015-09-13 13:06:01 +02:00
|
|
|
// First, propagate winding contributions for curve chains starting in
|
2015-11-04 01:19:20 +01:00
|
|
|
// all crossings:
|
|
|
|
for (var i = 0, l = crossings.length; i < l; i++) {
|
|
|
|
propagateWinding(crossings[i]._segment, _path1, _path2, monoCurves,
|
2016-01-05 10:30:33 +01:00
|
|
|
operator);
|
2015-09-13 13:06:01 +02:00
|
|
|
}
|
|
|
|
// Now process the segments that are not part of any intersecting chains
|
2015-01-02 15:33:23 +01:00
|
|
|
for (var i = 0, l = segments.length; i < l; i++) {
|
2016-01-05 10:30:33 +01:00
|
|
|
var segment = segments[i],
|
|
|
|
inter = segment._intersection;
|
2015-09-13 13:06:01 +02:00
|
|
|
if (segment._winding == null) {
|
2016-01-05 10:30:33 +01:00
|
|
|
propagateWinding(segment, _path1, _path2, monoCurves, operator);
|
2015-08-23 21:19:19 +02:00
|
|
|
}
|
2016-01-06 10:53:50 +01:00
|
|
|
// See if there are any valid segments that aren't part of overlaps.
|
|
|
|
// This information is used further down to determine where to start
|
|
|
|
// tracing the path, and how to treat encountered invalid segments.
|
2016-01-06 11:50:32 +01:00
|
|
|
if (!(inter && inter._overlap)) {
|
2016-01-06 10:53:50 +01:00
|
|
|
overlapsOnly = false;
|
2016-01-06 11:50:32 +01:00
|
|
|
if (operator[segment._winding])
|
|
|
|
validOverlapsOnly = false;
|
|
|
|
}
|
2015-01-02 15:33:23 +01:00
|
|
|
}
|
2016-01-06 11:50:32 +01:00
|
|
|
// If all encountered are overlaps (regardless if valid or not), we have
|
|
|
|
// two fully overlapping paths and can just return a clone of the first.
|
|
|
|
return finishResult(overlapsOnly
|
|
|
|
? path1.getArea() ? path1.clone() : new Path(Item.NO_INSERT)
|
|
|
|
: createResult(CompoundPath,
|
|
|
|
tracePaths(segments, operator, validOverlapsOnly),
|
|
|
|
!overlapsOnly), path1, path2);
|
2015-01-02 15:33:23 +01:00
|
|
|
}
|
2014-02-20 19:50:37 +01:00
|
|
|
|
2016-01-05 10:30:33 +01:00
|
|
|
function computeOpenBoolean(path1, path2, operator) {
|
2015-10-24 22:41:51 +02:00
|
|
|
// Only support subtract and intersect operations between an open
|
|
|
|
// and a closed path. Assume that compound-paths are closed.
|
|
|
|
// TODO: Should we complain about not supported operations?
|
|
|
|
if (!path2 || !path2._children && !path2._closed
|
2016-01-05 10:30:33 +01:00
|
|
|
|| !operator.subtract && !operator.intersect)
|
2015-10-24 22:41:51 +02:00
|
|
|
return null;
|
|
|
|
var _path1 = preparePath(path1, false),
|
|
|
|
_path2 = preparePath(path2, false),
|
2016-01-07 11:18:46 +01:00
|
|
|
crossings = _path1.getCrossings(_path2),
|
2016-01-05 10:30:33 +01:00
|
|
|
sub = operator.subtract,
|
2015-10-24 22:41:51 +02:00
|
|
|
paths = [];
|
|
|
|
|
|
|
|
function addPath(path) {
|
|
|
|
// Simple see if the point halfway across the open path is inside
|
2016-01-05 10:30:33 +01:00
|
|
|
// path2, and include / exclude the path based on the operator.
|
2015-10-24 22:41:51 +02:00
|
|
|
if (_path2.contains(path.getPointAt(path.getLength() / 2)) ^ sub) {
|
|
|
|
paths.unshift(path);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-11-04 01:19:20 +01:00
|
|
|
// Now loop backwards through all crossings, split the path and check
|
|
|
|
// the new path that was split off for inclusion.
|
|
|
|
for (var i = crossings.length - 1; i >= 0; i--) {
|
|
|
|
var path = crossings[i].split();
|
2015-10-24 22:41:51 +02:00
|
|
|
if (path) {
|
|
|
|
// See if we can add the path, and if so, clear the first handle
|
|
|
|
// at the split, because it might have been a curve.
|
|
|
|
if (addPath(path))
|
|
|
|
path.getFirstSegment().setHandleIn(0, 0);
|
|
|
|
// Clear the other side of the split too, which is always the
|
|
|
|
// end of the remaining _path1.
|
|
|
|
_path1.getLastSegment().setHandleOut(0, 0);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// At the end, check what's left from our path after all the splitting.
|
|
|
|
addPath(_path1);
|
2016-01-06 11:50:32 +01:00
|
|
|
return finishResult(createResult(Group, paths), path1, path2);
|
2015-10-24 22:41:51 +02:00
|
|
|
}
|
|
|
|
|
2015-10-06 21:09:35 +02:00
|
|
|
/*
|
2015-10-21 02:36:43 +02:00
|
|
|
* Creates linked lists between intersections through their _next and _prev
|
|
|
|
* properties.
|
2015-10-06 21:09:35 +02:00
|
|
|
*
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
function linkIntersections(from, to) {
|
2015-10-07 17:20:08 +02:00
|
|
|
// Only create the link if it's not already in the existing chain, to
|
2015-10-08 23:54:00 +02:00
|
|
|
// avoid endless recursions. First walk to the beginning of the chain,
|
|
|
|
// and abort if we find `to`.
|
2015-10-07 17:20:08 +02:00
|
|
|
var prev = from;
|
|
|
|
while (prev) {
|
|
|
|
if (prev === to)
|
|
|
|
return;
|
|
|
|
prev = prev._prev;
|
|
|
|
}
|
2015-10-08 23:54:00 +02:00
|
|
|
// Now walk to the end of the existing chain to find an empty spot, but
|
|
|
|
// stop if we find `to`, to avoid adding it again.
|
2015-10-07 17:20:08 +02:00
|
|
|
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) {
|
|
|
|
// Go back to beginning of the other chain, and link the two up.
|
|
|
|
while (to._prev)
|
|
|
|
to = to._prev;
|
|
|
|
from._next = to;
|
|
|
|
to._prev = from;
|
2015-10-06 21:09:35 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-01-02 15:33:23 +01:00
|
|
|
/**
|
2015-10-24 22:41:51 +02:00
|
|
|
* Divides the path-items at the given locations.
|
2015-06-16 17:50:37 +02:00
|
|
|
*
|
2015-10-06 21:09:35 +02:00
|
|
|
* @param {CurveLocation[]} locations an array of the locations to split the
|
|
|
|
* path-item at.
|
|
|
|
* @private
|
2015-01-02 15:33:23 +01:00
|
|
|
*/
|
2016-01-06 10:53:50 +01:00
|
|
|
function divideLocations(locations, include) {
|
|
|
|
var results = include && [],
|
|
|
|
tMin = /*#=*/Numerical.CURVETIME_EPSILON,
|
2015-01-04 17:37:15 +01:00
|
|
|
tMax = 1 - tMin,
|
2015-09-06 12:47:35 +02:00
|
|
|
noHandles = false,
|
2015-12-27 16:38:56 +01:00
|
|
|
clearCurves = [],
|
2015-10-01 09:41:57 -05:00
|
|
|
prevCurve,
|
2015-09-16 18:34:35 +02:00
|
|
|
prevT;
|
2014-02-20 19:50:37 +01:00
|
|
|
|
2015-09-20 14:16:47 +02:00
|
|
|
for (var i = locations.length - 1; i >= 0; i--) {
|
|
|
|
var loc = locations[i],
|
2015-10-01 09:41:57 -05:00
|
|
|
curve = loc._curve,
|
2015-09-16 18:34:35 +02:00
|
|
|
t = loc._parameter,
|
2016-01-06 10:53:50 +01:00
|
|
|
origT = t,
|
|
|
|
segment;
|
|
|
|
if (include) {
|
|
|
|
if (!include(loc))
|
|
|
|
continue;
|
|
|
|
results.unshift(loc);
|
|
|
|
}
|
2015-10-01 09:41:57 -05:00
|
|
|
if (curve !== prevCurve) {
|
|
|
|
// This is a new curve, update noHandles setting.
|
2015-09-06 12:47:35 +02:00
|
|
|
noHandles = !curve.hasHandles();
|
2015-10-01 09:41:57 -05: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 15:33:23 +01:00
|
|
|
}
|
2015-08-22 14:24:31 +02:00
|
|
|
if (t < tMin) {
|
|
|
|
segment = curve._segment1;
|
|
|
|
} else if (t > tMax) {
|
|
|
|
segment = curve._segment2;
|
|
|
|
} else {
|
2015-10-01 05:55:22 -05: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-12-27 16:38:56 +01:00
|
|
|
var newCurve = curve.divide(t, true, true);
|
|
|
|
// Keep track of curves without handles, so they can be cleared
|
|
|
|
// again at the end.
|
2015-09-06 12:47:35 +02:00
|
|
|
if (noHandles)
|
2015-12-27 16:38:56 +01:00
|
|
|
clearCurves.push(curve, newCurve);
|
|
|
|
segment = newCurve._segment1;
|
2015-01-02 15:33:23 +01:00
|
|
|
}
|
2015-10-04 02:25:33 +02:00
|
|
|
loc._setSegment(segment);
|
2015-10-06 21:09:35 +02: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 19:07:44 +02:00
|
|
|
if (inter) {
|
2015-10-06 21:09:35 +02: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-17 01:15:41 +02:00
|
|
|
} else {
|
2015-10-06 21:09:35 +02:00
|
|
|
segment._intersection = dest;
|
2015-09-17 01:15:41 +02:00
|
|
|
}
|
2015-10-01 09:41:57 -05:00
|
|
|
prevCurve = curve;
|
|
|
|
prevT = origT;
|
2015-01-02 15:33:23 +01:00
|
|
|
}
|
2015-09-06 12:47:35 +02:00
|
|
|
// Clear segment handles if they were part of a curve with no handles,
|
|
|
|
// once we are done with the entire curve.
|
2015-12-27 16:38:56 +01:00
|
|
|
for (var i = 0, l = clearCurves.length; i < l; i++) {
|
|
|
|
clearCurves[i].clearHandles();
|
2015-08-22 22:06:42 +02:00
|
|
|
}
|
2016-01-06 10:53:50 +01:00
|
|
|
return results || locations;
|
2015-01-02 15:33:23 +01:00
|
|
|
}
|
2014-02-20 19:50:37 +01:00
|
|
|
|
2015-01-02 15:33:23 +01:00
|
|
|
/**
|
2015-08-18 22:36:10 +02:00
|
|
|
* Private method that returns the winding contribution of the given point
|
2015-01-02 15:33:23 +01:00
|
|
|
* with respect to a given set of monotone curves.
|
|
|
|
*/
|
2015-12-30 20:13:55 +01:00
|
|
|
function getWinding(point, curves, horizontal) {
|
2015-10-21 01:16:52 +02:00
|
|
|
var epsilon = /*#=*/Numerical.WINDING_EPSILON,
|
2015-09-12 22:55:58 +02:00
|
|
|
tMin = /*#=*/Numerical.CURVETIME_EPSILON,
|
2015-01-02 23:47:26 +01:00
|
|
|
tMax = 1 - tMin,
|
2015-01-05 00:13:30 +01:00
|
|
|
px = point.x,
|
|
|
|
py = point.y,
|
2015-01-02 15:33:23 +01:00
|
|
|
windLeft = 0,
|
|
|
|
windRight = 0,
|
2015-12-27 21:05:55 +01:00
|
|
|
length = curves.length,
|
2015-01-02 15:33:23 +01:00
|
|
|
roots = [],
|
2015-01-02 21:19:18 +01:00
|
|
|
abs = Math.abs;
|
2015-01-02 15:33:23 +01: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 22:20:31 +02:00
|
|
|
yBefore = py - epsilon,
|
|
|
|
yAfter = py + epsilon;
|
2015-01-02 15:33:23 +01:00
|
|
|
// Find the closest top and bottom intercepts for the same vertical
|
|
|
|
// line.
|
2015-12-27 21:05:55 +01:00
|
|
|
for (var i = 0; i < length; i++) {
|
|
|
|
var values = curves[i].values,
|
|
|
|
count = Curve.solveCubic(values, 0, px, roots, 0, 1);
|
|
|
|
for (var j = count - 1; j >= 0; j--) {
|
|
|
|
var y = Curve.getPoint(values, roots[j]).y;
|
|
|
|
if (y < yBefore && y > yTop) {
|
|
|
|
yTop = y;
|
|
|
|
} else if (y > yAfter && y < yBottom) {
|
|
|
|
yBottom = y;
|
2015-01-02 15:33:23 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Shift the point lying on the horizontal curves by
|
|
|
|
// half of closest top and bottom intercepts.
|
2015-01-05 00:13:30 +01:00
|
|
|
yTop = (yTop + py) / 2;
|
|
|
|
yBottom = (yBottom + py) / 2;
|
2015-01-02 15:33:23 +01:00
|
|
|
if (yTop > -Infinity)
|
2015-12-30 20:13:55 +01:00
|
|
|
windLeft = getWinding(new Point(px, yTop), curves);
|
2015-01-02 15:33:23 +01:00
|
|
|
if (yBottom < Infinity)
|
2015-12-30 20:13:55 +01:00
|
|
|
windRight = getWinding(new Point(px, yBottom), curves);
|
2015-01-02 15:33:23 +01:00
|
|
|
} else {
|
2015-09-12 22:20:31 +02:00
|
|
|
var xBefore = px - epsilon,
|
2015-12-27 20:55:57 +01:00
|
|
|
xAfter = px + epsilon,
|
|
|
|
start,
|
2015-12-27 21:05:55 +01:00
|
|
|
end = 0;
|
2015-12-27 20:55:57 +01:00
|
|
|
while (end < length) {
|
2015-12-27 21:05:55 +01:00
|
|
|
start = end;
|
2016-01-07 11:02:51 +01:00
|
|
|
// The first curve of a loop holds information about its length
|
|
|
|
// and the first / last curve with non-zero winding.
|
|
|
|
// Retrieve and use it here (See _getMonoCurve()).
|
|
|
|
var curve = curves[start],
|
|
|
|
firstWinding = curve.firstWinding,
|
|
|
|
lastWinding = curve.lastWinding;
|
|
|
|
end = start + curve.length;
|
2015-12-27 20:55:57 +01:00
|
|
|
// Walk through one single loop of curves.
|
2015-12-14 10:48:04 +01:00
|
|
|
var startCounted = false,
|
2015-12-27 20:55:57 +01:00
|
|
|
prevCurve, // non-horizontal curve before the current curve.
|
|
|
|
nextCurve, // non-horizontal curve after the current curve.
|
|
|
|
prevT = null,
|
|
|
|
curve = null;
|
|
|
|
for (var i = start; i < end; i++) {
|
2015-12-14 10:48:04 +01:00
|
|
|
if (!curve) {
|
2016-01-07 11:02:51 +01:00
|
|
|
prevCurve = lastWinding;
|
|
|
|
nextCurve = firstWinding;
|
2015-12-14 10:48:04 +01:00
|
|
|
} else if (curve.winding) {
|
2015-12-27 20:55:57 +01:00
|
|
|
prevCurve = curve;
|
2015-12-14 10:48:04 +01:00
|
|
|
}
|
2015-12-27 20:55:57 +01:00
|
|
|
curve = curves[i];
|
|
|
|
if (curve === nextCurve) {
|
|
|
|
nextCurve = curve.next;
|
|
|
|
while (nextCurve && !nextCurve.winding) {
|
|
|
|
nextCurve = nextCurve.next;
|
2015-12-14 10:48:04 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
var values = curve.values,
|
|
|
|
winding = curve.winding;
|
|
|
|
// 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-12-27 21:05:55 +01:00
|
|
|
if (winding && (winding === 1
|
2015-12-27 20:55:57 +01:00
|
|
|
&& py >= values[1] && py <= values[7]
|
|
|
|
|| py >= values[7] && py <= values[1])
|
2015-12-27 21:05:55 +01:00
|
|
|
&& Curve.solveCubic(values, 1, py, roots, 0, 1) === 1) {
|
2015-12-14 10:48:04 +01:00
|
|
|
var t = roots[0];
|
2015-12-27 20:55:57 +01: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. 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-12-14 10:48:04 +01:00
|
|
|
if (!( // = the following conditions will be excluded:
|
2015-12-27 20:55:57 +01:00
|
|
|
// Detect and exclude intercepts at 'end' of loops
|
|
|
|
// if the start of the loop was already counted.
|
2016-01-07 11:02:51 +01:00
|
|
|
t > tMax && startCounted && curve === lastWinding
|
2015-12-27 20:55:57 +01:00
|
|
|
// Detect 2nd case of a consecutive intercept
|
2015-12-14 10:48:04 +01:00
|
|
|
|| t < tMin && prevT > tMax)) {
|
|
|
|
var x = Curve.getPoint(values, t).x,
|
|
|
|
counted = false;
|
2015-12-27 20:55:57 +01: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.
|
2016-01-07 11:02:51 +01:00
|
|
|
if (Numerical.isZero(Curve.getTangent(values, t).y)
|
2015-12-27 20:55:57 +01:00
|
|
|
&& !Curve.isStraight(values)
|
2016-01-07 10:02:43 +01:00
|
|
|
// Does the winding over the edges change?
|
2015-12-27 20:55:57 +01:00
|
|
|
|| t < tMin && prevCurve
|
2016-01-07 10:02:43 +01:00
|
|
|
&& winding * prevCurve.winding < 0
|
2015-12-27 20:55:57 +01:00
|
|
|
|| t > tMax && nextCurve
|
2016-01-07 10:02:43 +01:00
|
|
|
&& winding * nextCurve.winding < 0) {
|
2015-12-30 20:13:55 +01:00
|
|
|
if (x > xBefore && x < xAfter) {
|
2015-12-14 10:48:04 +01:00
|
|
|
++windLeft;
|
|
|
|
++windRight;
|
|
|
|
counted = true;
|
|
|
|
}
|
|
|
|
} else if (x <= xBefore) {
|
|
|
|
windLeft += winding;
|
|
|
|
counted = true;
|
|
|
|
} else if (x >= xAfter) {
|
|
|
|
windRight += winding;
|
2015-08-18 22:36:10 +02:00
|
|
|
counted = true;
|
2015-01-04 22:37:27 +01:00
|
|
|
}
|
2015-12-27 20:55:57 +01:00
|
|
|
// Mark the start of the path as counted.
|
2016-01-07 11:02:51 +01:00
|
|
|
if (curve === firstWinding) {
|
2015-12-14 10:48:04 +01:00
|
|
|
startCounted = t < tMin && counted;
|
2015-12-27 20:55:57 +01:00
|
|
|
}
|
2015-12-14 10:48:04 +01:00
|
|
|
}
|
|
|
|
prevT = t;
|
|
|
|
} else if (!winding) {
|
2015-12-27 20:55:57 +01:00
|
|
|
// If the point is on a horizontal curve and winding
|
|
|
|
// changes between before and after the curve, we treat
|
|
|
|
// this as a 'touch point'.
|
2015-12-30 20:13:55 +01:00
|
|
|
if (py === values[1]
|
2015-12-27 20:55:57 +01:00
|
|
|
&& (values[0] < xAfter && values[6] > xBefore
|
|
|
|
|| values[6] < xAfter && values[0] > xBefore)
|
|
|
|
&& prevCurve && nextCurve
|
|
|
|
&& prevCurve.winding * nextCurve.winding < 0) {
|
2015-12-14 10:48:04 +01:00
|
|
|
++windLeft;
|
|
|
|
++windRight;
|
2015-01-02 15:33:23 +01:00
|
|
|
}
|
2015-12-27 20:55:57 +01:00
|
|
|
// We keep the value for prevT to avoid double counting
|
|
|
|
// of intersections at the end of a curve and the start
|
|
|
|
// of the next curve, even if any number of horizontal
|
|
|
|
// curves is between both curves.
|
2015-12-14 10:48:04 +01:00
|
|
|
} else {
|
|
|
|
prevT = null;
|
2015-01-02 15:33:23 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return Math.max(abs(windLeft), abs(windRight));
|
|
|
|
}
|
2014-02-20 19:50:37 +01:00
|
|
|
|
2016-01-05 10:30:33 +01:00
|
|
|
function propagateWinding(segment, path1, path2, monoCurves, operator) {
|
2015-09-13 13:06:01 +02:00
|
|
|
// 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-10-21 09:35:17 +02:00
|
|
|
var epsilon = /*#=*/Numerical.GEOMETRIC_EPSILON,
|
|
|
|
chain = [],
|
2015-09-15 19:39:35 +02:00
|
|
|
start = segment,
|
2015-09-13 13:06:01 +02:00
|
|
|
totalLength = 0,
|
|
|
|
windingSum = 0;
|
|
|
|
do {
|
2015-09-13 14:19:56 +02:00
|
|
|
var curve = segment.getCurve(),
|
|
|
|
length = curve.getLength();
|
|
|
|
chain.push({ segment: segment, curve: curve, length: length });
|
2015-09-13 13:06:01 +02:00
|
|
|
totalLength += length;
|
|
|
|
segment = segment.getNext();
|
2015-09-15 19:39:35 +02:00
|
|
|
} while (segment && !segment._intersection && segment !== start);
|
2015-09-13 13:06:01 +02: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++) {
|
2015-10-21 02:40:12 +02:00
|
|
|
// Sample the points at 1/4, 2/4 and 3/4 of the total length:
|
2015-09-13 13:06:01 +02:00
|
|
|
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) {
|
2015-10-21 09:35:17 +02:00
|
|
|
// 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 14:19:56 +02:00
|
|
|
var curve = node.curve,
|
|
|
|
path = curve._path,
|
|
|
|
parent = path._parent,
|
2015-12-29 23:16:20 +01:00
|
|
|
t = curve.getParameterAt(length),
|
|
|
|
pt = curve.getPointAt(t, true),
|
2015-12-30 20:13:55 +01:00
|
|
|
hor = Math.abs(curve.getTangentAt(t, true).y)
|
|
|
|
< /*#=*/Numerical.TRIGONOMETRIC_EPSILON;
|
2015-09-13 14:19:56 +02:00
|
|
|
if (parent instanceof CompoundPath)
|
|
|
|
path = parent;
|
2015-12-30 20:13:55 +01:00
|
|
|
// While subtracting, we need to omit this curve if it is
|
|
|
|
// contributing to the second operand and is outside the
|
|
|
|
// first operand.
|
2016-01-05 10:30:33 +01:00
|
|
|
windingSum += operator.subtract && path2
|
2015-09-13 13:06:01 +02:00
|
|
|
&& (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 15:50:26 +02:00
|
|
|
for (var j = chain.length - 1; j >= 0; j--)
|
|
|
|
chain[j].segment._winding = winding;
|
2015-09-13 13:06:01 +02:00
|
|
|
}
|
|
|
|
|
2015-01-02 15:33:23 +01: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
|
2015-12-28 17:54:07 +01:00
|
|
|
* indicating whether the curve should be included in the final contour or
|
2015-01-02 15:33:23 +01:00
|
|
|
* not
|
|
|
|
* @return {Path[]} the contours traced
|
|
|
|
*/
|
2016-01-06 11:50:32 +01:00
|
|
|
function tracePaths(segments, operator, validOverlapsOnly) {
|
2016-01-06 11:14:38 +01:00
|
|
|
var paths = [],
|
2015-09-19 19:07:44 +02:00
|
|
|
start,
|
2016-01-05 10:30:33 +01:00
|
|
|
otherStart;
|
2015-09-20 15:50:26 +02:00
|
|
|
|
2016-01-05 10:30:33 +01:00
|
|
|
function isValid(seg) {
|
2016-01-06 10:53:50 +01:00
|
|
|
return !!(!seg._visited && (!operator || operator[seg._winding]));
|
2015-09-20 15:50:26 +02:00
|
|
|
}
|
2015-09-19 19:07:44 +02:00
|
|
|
|
2015-10-09 10:18:45 +02:00
|
|
|
function isStart(seg) {
|
2016-01-03 01:13:03 +01:00
|
|
|
return seg === start || seg === otherStart;
|
2015-10-09 10:18:45 +02:00
|
|
|
}
|
|
|
|
|
2015-11-04 01:19:20 +01: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-08 23:13:37 +02:00
|
|
|
function findBestIntersection(inter, strict) {
|
|
|
|
if (!inter._next)
|
|
|
|
return inter;
|
2015-10-06 21:30:51 +02:00
|
|
|
while (inter) {
|
|
|
|
var seg = inter._segment,
|
|
|
|
nextSeg = seg.getNext(),
|
|
|
|
nextInter = nextSeg._intersection;
|
|
|
|
// See if this segment and the next are both not visited yet, or
|
|
|
|
// are bringing us back to the beginning, and are both part of
|
|
|
|
// the boolean result.
|
2015-10-21 02:24:54 +02:00
|
|
|
// Handling overlaps correctly here is tricky, requiring two
|
|
|
|
// passes, first with strict = true, then false:
|
|
|
|
// In strict mode, the current and the next segment are both
|
|
|
|
// checked for validity, and only the current one is allowed to
|
2016-01-05 10:30:33 +01:00
|
|
|
// be an overlap.
|
2015-10-21 02:24:54 +02:00
|
|
|
// If this pass does not yield a result, the non-strict mode is
|
|
|
|
// used, in which invalid current segments are tolerated, and
|
2016-01-05 10:30:33 +01:00
|
|
|
// overlaps for the next segment are allowed.
|
2016-01-03 01:08:11 +01:00
|
|
|
if (isStart(seg) || isStart(nextSeg)
|
2015-10-06 21:30:51 +02:00
|
|
|
|| !seg._visited && !nextSeg._visited
|
2015-10-06 22:23:43 +02:00
|
|
|
// Self-intersections (!operator) don't need isValid() calls
|
|
|
|
&& (!operator
|
2015-10-21 15:02:53 +02:00
|
|
|
|| (!strict || isValid(seg))
|
2015-10-06 22:23:43 +02:00
|
|
|
// Do not consider 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.
|
2016-01-06 10:53:50 +01:00
|
|
|
&& (!(strict && nextInter && nextInter._overlap)
|
2015-10-21 15:02:53 +02:00
|
|
|
&& isValid(nextSeg)
|
2015-10-06 22:23:43 +02:00
|
|
|
// If the next segment isn't valid, its intersection
|
|
|
|
// to which we may switch might be, so check that.
|
|
|
|
|| !strict && nextInter
|
2015-10-21 15:02:53 +02:00
|
|
|
&& isValid(nextInter._segment))
|
2015-10-06 22:23:43 +02:00
|
|
|
))
|
2015-10-06 21:30:51 +02:00
|
|
|
return inter;
|
2015-10-06 21:09:35 +02:00
|
|
|
// If it's no match, continue with the next linked intersection.
|
2015-10-06 21:30:51 +02:00
|
|
|
inter = inter._next;
|
|
|
|
}
|
|
|
|
return null;
|
2015-09-19 19:07:44 +02:00
|
|
|
}
|
2015-10-06 22:23:43 +02:00
|
|
|
|
2015-09-13 22:12:04 +02:00
|
|
|
for (var i = 0, l = segments.length; i < l; i++) {
|
2016-01-05 10:30:33 +01:00
|
|
|
var path = null,
|
2016-01-03 01:08:11 +01:00
|
|
|
finished = false,
|
2016-01-05 10:30:33 +01:00
|
|
|
seg = segments[i],
|
|
|
|
inter = seg._intersection,
|
2016-01-03 01:08:11 +01:00
|
|
|
handleIn;
|
2016-01-05 10:30:33 +01:00
|
|
|
// Do not start paths with invalid segments (segments that were
|
|
|
|
// already visited, or that are not going to be part of the result).
|
|
|
|
// Also don't start in overlaps, unless all segments are part of
|
|
|
|
// overlaps, in which case we have no other choice.
|
2016-01-06 11:50:32 +01:00
|
|
|
if (!isValid(seg) || !validOverlapsOnly
|
2016-01-06 10:53:50 +01:00
|
|
|
&& inter && seg._winding && inter._overlap)
|
2015-09-19 19:07:44 +02:00
|
|
|
continue;
|
2015-10-09 10:18:45 +02:00
|
|
|
start = otherStart = null;
|
2016-01-05 10:30:33 +01:00
|
|
|
while (true) {
|
2016-01-03 01:08:11 +01:00
|
|
|
handleIn = path && seg._handleIn;
|
2016-01-05 10:30:33 +01:00
|
|
|
// For each segment we encounter, see if there are multiple
|
2015-09-19 19:07:44 +02:00
|
|
|
// intersections, and if so, pick the best one:
|
2015-10-08 23:13:37 +02:00
|
|
|
inter = inter && (findBestIntersection(inter, true)
|
|
|
|
|| findBestIntersection(inter, false)) || inter;
|
2016-01-05 10:30:33 +01:00
|
|
|
// Get the reference to the other segment on the intersection.
|
2015-09-21 09:42:47 -04:00
|
|
|
var other = inter && inter._segment;
|
2016-01-03 01:08:11 +01:00
|
|
|
if (isStart(seg)) {
|
|
|
|
finished = true;
|
|
|
|
} else if (other) {
|
|
|
|
if (isStart(other)) {
|
|
|
|
finished = true;
|
|
|
|
// Switch the segment, but do not update handleIn
|
|
|
|
seg = other;
|
|
|
|
} else if (isValid(other)) {
|
|
|
|
// We are at a crossing and the other segment is part of
|
|
|
|
// the boolean result, switch over.
|
|
|
|
// We need to mark overlap segments as visited when
|
|
|
|
// processing intersection.
|
2016-01-06 10:53:50 +01:00
|
|
|
if (operator && operator.intersect && inter._overlap)
|
2016-01-03 01:08:11 +01:00
|
|
|
seg._visited = true;
|
|
|
|
seg = other;
|
2015-10-09 10:22:54 +02:00
|
|
|
}
|
2016-01-03 01:08:11 +01:00
|
|
|
}
|
|
|
|
// Bail out if we're done, or if we encounter an already visited
|
|
|
|
// next segment.
|
|
|
|
if (finished || seg._visited) {
|
|
|
|
// It doesn't hurt to set again to share some code.
|
|
|
|
seg._visited = true;
|
2015-10-20 23:02:19 +02:00
|
|
|
break;
|
2015-09-15 16:31:05 +02:00
|
|
|
}
|
2016-01-06 10:53:50 +01:00
|
|
|
// If there are only valid overlap segments and we encounter
|
|
|
|
// and invalid segment, bail out immediately. Otherwise we need
|
|
|
|
// to be more tolerant due to complex situations of crossing.
|
2016-01-06 11:50:32 +01:00
|
|
|
if (validOverlapsOnly && !isValid(seg))
|
2016-01-06 10:53:50 +01:00
|
|
|
break;
|
2015-10-01 21:09:30 -05:00
|
|
|
if (!path) {
|
|
|
|
path = new Path(Item.NO_INSERT);
|
|
|
|
start = seg;
|
2015-10-09 10:18:45 +02:00
|
|
|
otherStart = other;
|
2015-10-01 21:09:30 -05:00
|
|
|
}
|
2015-10-11 16:56:41 +02:00
|
|
|
// Add the segment to the path, and mark it as visited.
|
2015-09-15 16:31:05 +02:00
|
|
|
path.add(new Segment(seg._point, handleIn, seg._handleOut));
|
2015-10-01 21:09:30 -05:00
|
|
|
seg._visited = true;
|
2015-09-15 16:31:05 +02:00
|
|
|
seg = seg.getNext();
|
2016-01-05 10:30:33 +01:00
|
|
|
inter = seg._intersection;
|
2015-09-19 19:07:44 +02:00
|
|
|
}
|
2015-10-09 10:18:45 +02:00
|
|
|
if (finished) {
|
2016-01-05 10:30:33 +01:00
|
|
|
// Finish with closing the paths, and carrying over the last
|
|
|
|
// handleIn to the first segment.
|
2016-01-03 01:08:11 +01:00
|
|
|
path.firstSegment.setHandleIn(handleIn);
|
2015-08-26 17:36:20 +02:00
|
|
|
path.setClosed(true);
|
2015-10-21 15:02:53 +02:00
|
|
|
} else if (path) {
|
2015-11-02 18:21:19 +01:00
|
|
|
var length = path.getLength();
|
|
|
|
// Only complain about open paths if they are long enough.
|
|
|
|
if (length >= /*#=*/Numerical.GEOMETRIC_EPSILON) {
|
|
|
|
// This path wasn't finished and is hence invalid.
|
|
|
|
// Report the error to the console for the time being.
|
|
|
|
console.error('Boolean operation resulted in open path',
|
|
|
|
'segments =', path._segments.length,
|
|
|
|
'length =', length);
|
|
|
|
}
|
2015-08-26 17:36:20 +02:00
|
|
|
path = null;
|
2015-01-02 15:33:23 +01:00
|
|
|
}
|
|
|
|
// Add the path to the result, while avoiding stray segments and
|
2015-09-06 16:35:15 +02:00
|
|
|
// paths that are incomplete or cover no area.
|
2015-10-21 02:24:54 +02:00
|
|
|
// As an optimization, only check paths with 8 or less segments
|
2015-09-06 16:35:15 +02:00
|
|
|
// for their area, and assume that they cover an area when more.
|
2015-10-21 02:24:54 +02:00
|
|
|
if (path && (path._segments.length > 8
|
2015-10-01 21:09:30 -05:00
|
|
|
|| !Numerical.isZero(path.getArea()))) {
|
2015-01-02 15:33:23 +01:00
|
|
|
paths.push(path);
|
2015-10-01 21:09:30 -05:00
|
|
|
path = null;
|
|
|
|
}
|
2015-01-02 15:33:23 +01:00
|
|
|
}
|
|
|
|
return paths;
|
|
|
|
}
|
2014-02-20 20:24:16 +01:00
|
|
|
|
2015-01-02 15:33:23 +01: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
|
|
|
|
* @return {Number} the winding number
|
|
|
|
*/
|
2015-12-30 20:13:55 +01:00
|
|
|
_getWinding: function(point, horizontal) {
|
|
|
|
return getWinding(point, this._getMonoCurves(), horizontal);
|
2015-01-02 15:33:23 +01:00
|
|
|
},
|
2014-02-20 20:24:16 +01:00
|
|
|
|
2015-01-02 15:33:23 +01:00
|
|
|
/**
|
|
|
|
* {@grouptitle Boolean Path Operations}
|
|
|
|
*
|
2015-10-25 09:41:43 +01:00
|
|
|
* Merges the geometry of the specified path with this path's geometry
|
|
|
|
* and returns the result as a new path item.
|
2015-01-02 15:33:23 +01:00
|
|
|
*
|
|
|
|
* @param {PathItem} path the path to unite with
|
|
|
|
* @return {PathItem} the resulting path item
|
|
|
|
*/
|
|
|
|
unite: function(path) {
|
2015-01-04 01:50:24 +01:00
|
|
|
return computeBoolean(this, path, 'unite');
|
2015-01-02 15:33:23 +01:00
|
|
|
},
|
2014-02-20 20:24:16 +01:00
|
|
|
|
2015-01-02 15:33:23 +01: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-04 01:50:24 +01:00
|
|
|
return computeBoolean(this, path, 'intersect');
|
2015-01-02 15:33:23 +01:00
|
|
|
},
|
2014-02-20 20:24:16 +01:00
|
|
|
|
2015-01-02 15:33:23 +01: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-04 01:50:24 +01:00
|
|
|
return computeBoolean(this, path, 'subtract');
|
2015-01-02 15:33:23 +01:00
|
|
|
},
|
2014-02-20 20:24:16 +01:00
|
|
|
|
2015-01-02 15:33:23 +01:00
|
|
|
/**
|
|
|
|
* Excludes the intersection of the geometry of the specified path with
|
2015-10-25 09:41:43 +01:00
|
|
|
* this path's geometry and returns the result as a new path item.
|
2015-01-02 15:33:23 +01:00
|
|
|
*
|
|
|
|
* @param {PathItem} path the path to exclude the intersection of
|
2015-10-25 09:41:43 +01:00
|
|
|
* @return {PathItem} the resulting group item
|
2015-01-02 15:33:23 +01:00
|
|
|
*/
|
|
|
|
exclude: function(path) {
|
2015-01-04 01:50:24 +01:00
|
|
|
return computeBoolean(this, path, 'exclude');
|
2015-01-02 15:33:23 +01:00
|
|
|
},
|
2014-04-06 13:48:03 +02:00
|
|
|
|
2015-01-02 15:33:23 +01:00
|
|
|
/**
|
|
|
|
* Splits the geometry of this path along the geometry of the specified
|
2015-10-25 09:41:43 +01:00
|
|
|
* path returns the result as a new group item. This is equivalent to
|
|
|
|
* calling {@link #subtract(path)} and {@link #subtract(path)} and
|
|
|
|
* putting the results into a new group.
|
2015-01-02 15:33:23 +01:00
|
|
|
*
|
|
|
|
* @param {PathItem} path the path to divide by
|
|
|
|
* @return {Group} the resulting group item
|
|
|
|
*/
|
|
|
|
divide: function(path) {
|
2016-01-06 11:50:32 +01:00
|
|
|
return finishResult(createResult(Group,
|
|
|
|
[this.subtract(path), this.intersect(path)], true), this, path);
|
2015-09-18 17:51:03 +02:00
|
|
|
},
|
|
|
|
|
2015-12-26 12:52:32 +01:00
|
|
|
/*
|
|
|
|
* Resolves all crossings of a path item, first by splitting the path or
|
|
|
|
* compound-path in each self-intersection and tracing the result, then
|
|
|
|
* fixing the orientation of the resulting sub-paths by making sure that
|
|
|
|
* all sub-paths are of different winding direction than the first path,
|
|
|
|
* except for when individual sub-paths are disjoint, i.e. islands,
|
|
|
|
* which are reoriented so that:
|
|
|
|
* - The holes have opposite winding direction.
|
|
|
|
* - Islands have to have the same winding direction as the first child.
|
|
|
|
* If possible, the existing path / compound-path is modified if the
|
|
|
|
* amount of resulting paths allows so, otherwise a new path /
|
|
|
|
* compound-path is created, replacing the current one.
|
|
|
|
*/
|
2015-09-18 17:51:03 +02:00
|
|
|
resolveCrossings: function() {
|
2015-12-26 12:52:32 +01:00
|
|
|
var children = this._children,
|
|
|
|
// Support both path and compound-path items
|
2016-01-06 10:53:50 +01:00
|
|
|
paths = children || [this];
|
|
|
|
|
|
|
|
function hasOverlap(seg) {
|
|
|
|
var inter = seg && seg._intersection;
|
|
|
|
return inter && inter._overlap;
|
|
|
|
}
|
|
|
|
|
|
|
|
// First collect all overlaps and crossings while taking not of the
|
|
|
|
// existence of both.
|
|
|
|
var hasOverlaps = false,
|
|
|
|
hasCrossings = false,
|
|
|
|
intersections = this.getIntersections(null, function(inter) {
|
|
|
|
return inter._overlap && (hasOverlaps = true)
|
|
|
|
|| inter.isCrossing() && (hasCrossings = true);
|
|
|
|
});
|
|
|
|
intersections = CurveLocation.expand(intersections);
|
|
|
|
if (hasOverlaps) {
|
|
|
|
// First divide in all overlaps, and then remove the inside of
|
|
|
|
// the resulting overlap ranges.
|
|
|
|
var overlaps = divideLocations(intersections, function(inter) {
|
|
|
|
return inter._overlap;
|
|
|
|
});
|
|
|
|
for (var i = overlaps.length - 1; i >= 0; i--) {
|
|
|
|
var seg = overlaps[i]._segment,
|
|
|
|
prev = seg.getPrevious(),
|
|
|
|
next = seg.getNext();
|
|
|
|
if (seg._path && hasOverlap(prev) && hasOverlap(next)) {
|
|
|
|
seg.remove();
|
|
|
|
prev._handleOut.set(0, 0);
|
|
|
|
next._handleIn.set(0, 0);
|
|
|
|
var curve = prev.getCurve();
|
|
|
|
if (curve.isStraight() && curve.getLength() === 0)
|
|
|
|
prev.remove();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (hasCrossings) {
|
2016-01-07 09:50:07 +01:00
|
|
|
// Divide any remaining intersections that are still part of
|
2016-01-06 10:53:50 +01:00
|
|
|
// valid paths after the removal of overlaps.
|
|
|
|
divideLocations(intersections, function(inter) {
|
|
|
|
// Check both involved curves to see if they're still valid,
|
|
|
|
// meaning they are still part of their paths.
|
|
|
|
var curve1 = inter.getCurve(),
|
2016-01-07 09:50:07 +01:00
|
|
|
// Do not call getCurve() on the other intersection yet,
|
|
|
|
// as it too is in the intersections array and will be
|
|
|
|
// divided later. But do check if its current curve is
|
|
|
|
// still valid. This is required by some very rare edge
|
|
|
|
// cases, related to intersections on the same curve.
|
|
|
|
curve2 = inter._intersection._curve,
|
2016-01-06 10:53:50 +01:00
|
|
|
seg = inter._segment;
|
|
|
|
if (curve1 && curve2 && curve1._path && curve2._path) {
|
|
|
|
return true;
|
|
|
|
} else if (seg) {
|
|
|
|
// Remove all intersections that were involved in the
|
|
|
|
// handling of overlaps, to not confuse tracePaths().
|
|
|
|
seg._intersection = null;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
// Finally resolve self-intersections through tracePaths()
|
2015-12-26 12:52:32 +01:00
|
|
|
paths = tracePaths(Base.each(paths, function(path) {
|
|
|
|
this.push.apply(this, path._segments);
|
|
|
|
}, []));
|
|
|
|
}
|
|
|
|
// By now, all paths are non-overlapping, but might be fully
|
|
|
|
// contained inside each other.
|
|
|
|
// Next we adjust their orientation based on on further checks:
|
|
|
|
var length = paths.length,
|
|
|
|
item;
|
|
|
|
if (length > 1) {
|
|
|
|
// First order the paths by the area of their bounding boxes.
|
2015-12-28 01:54:49 +01:00
|
|
|
// Make a clone of paths as it may still be the children array.
|
2015-12-26 12:52:32 +01:00
|
|
|
paths = paths.slice().sort(function (a, b) {
|
|
|
|
return b.getBounds().getArea() - a.getBounds().getArea();
|
|
|
|
});
|
2015-12-28 19:01:56 +01:00
|
|
|
var first = paths[0],
|
|
|
|
items = [first],
|
2015-12-26 12:52:32 +01:00
|
|
|
excluded = {},
|
|
|
|
isNonZero = this.getFillRule() === 'nonzero',
|
|
|
|
windings = isNonZero && Base.each(paths, function(path) {
|
|
|
|
this.push(path.isClockwise() ? 1 : -1);
|
|
|
|
}, []);
|
|
|
|
// Walk through paths, from largest to smallest.
|
2015-12-28 19:01:56 +01:00
|
|
|
// The first, largest child can be skipped.
|
|
|
|
for (var i = 1; i < length; i++) {
|
2015-12-26 12:52:32 +01:00
|
|
|
var path = paths[i],
|
|
|
|
point = path.getInteriorPoint(),
|
2015-12-28 11:43:28 +01:00
|
|
|
isContained = false,
|
2015-12-30 19:28:32 +01:00
|
|
|
container = null,
|
2015-12-28 11:43:28 +01:00
|
|
|
exclude = false;
|
2015-12-30 19:28:32 +01:00
|
|
|
for (var j = i - 1; j >= 0 && !container; j--) {
|
2015-12-28 11:43:28 +01:00
|
|
|
// We run through the paths from largest to smallest,
|
|
|
|
// meaning that for any current path, all potentially
|
|
|
|
// containing paths have already been processed and
|
|
|
|
// their orientation has been fixed. Since we want to
|
|
|
|
// achieve alternating orientation of contained paths,
|
|
|
|
// all we have to do is to find one include path that
|
|
|
|
// contains the current path, and then set the
|
|
|
|
// orientation to the opposite of the containing path.
|
2015-12-26 12:52:32 +01:00
|
|
|
if (paths[j].contains(point)) {
|
2015-12-28 11:43:28 +01:00
|
|
|
if (isNonZero && !isContained) {
|
2015-12-26 12:52:32 +01:00
|
|
|
windings[i] += windings[j];
|
|
|
|
// Remove path if rule is nonzero and winding
|
2015-12-26 19:45:55 +01:00
|
|
|
// of path and containing path is not zero.
|
2015-12-26 12:52:32 +01:00
|
|
|
if (windings[i] && windings[j]) {
|
|
|
|
exclude = excluded[i] = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2015-12-28 11:43:28 +01:00
|
|
|
isContained = true;
|
2015-12-30 19:28:32 +01:00
|
|
|
// If the containing path is not excluded, we're
|
|
|
|
// done searching for the orientation defining path.
|
|
|
|
container = !excluded[j] && paths[j];
|
2015-12-26 12:52:32 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!exclude) {
|
2015-12-30 19:28:32 +01:00
|
|
|
// Set to the opposite orientation of containing path,
|
|
|
|
// or the same orientation as the first path if the path
|
|
|
|
// is not contained in any other path.
|
|
|
|
path.setClockwise(container ? !container.isClockwise()
|
|
|
|
: first.isClockwise());
|
2015-12-26 12:52:32 +01:00
|
|
|
items.push(path);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Replace paths with the processed items list:
|
|
|
|
paths = items;
|
|
|
|
length = items.length;
|
|
|
|
}
|
|
|
|
// First try to recycle the current path / compound-path, if the
|
|
|
|
// amount of paths do not require a conversion.
|
|
|
|
if (length > 1 && children) {
|
2015-12-28 11:43:28 +01:00
|
|
|
if (paths !== children) {
|
|
|
|
// TODO: Fix automatic child-orientation in CompoundPath,
|
|
|
|
// and stop passing true for _preserve.
|
|
|
|
this.setChildren(paths, true); // Preserve orientation
|
|
|
|
}
|
2015-12-26 12:52:32 +01:00
|
|
|
item = this;
|
|
|
|
} else if (length === 1 && !children) {
|
|
|
|
if (paths[0] !== this)
|
|
|
|
this.setSegments(paths[0].removeSegments());
|
|
|
|
item = this;
|
2015-09-18 17:51:03 +02:00
|
|
|
}
|
2015-12-26 12:52:32 +01:00
|
|
|
// Otherwise create a new compound-path and see if we can reduce it,
|
|
|
|
// and attempt to replace this item with it.
|
|
|
|
if (!item) {
|
|
|
|
item = new CompoundPath(Item.NO_INSERT);
|
2015-12-28 11:43:28 +01:00
|
|
|
item.addChildren(paths, true); // Preserve orientation
|
2015-12-26 12:52:32 +01:00
|
|
|
item = item.reduce();
|
2015-12-26 21:46:36 +01:00
|
|
|
item.copyAttributes(this);
|
2015-12-26 12:52:32 +01:00
|
|
|
this.replaceWith(item);
|
|
|
|
}
|
|
|
|
return item;
|
2015-01-02 15:33:23 +01:00
|
|
|
}
|
|
|
|
};
|
2014-02-20 20:24:16 +01:00
|
|
|
});
|
2014-02-20 20:00:46 +01:00
|
|
|
|
|
|
|
Path.inject(/** @lends Path# */{
|
2015-01-02 15:33:23 +01:00
|
|
|
/**
|
2015-08-24 12:59:10 +02: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 15:33:23 +01:00
|
|
|
* Used by getWinding().
|
|
|
|
*/
|
|
|
|
_getMonoCurves: function() {
|
|
|
|
var monoCurves = this._monoCurves,
|
2016-01-07 11:02:51 +01:00
|
|
|
prevCurve,
|
|
|
|
firstWinding = null,
|
|
|
|
lastWinding;
|
2014-02-20 20:00:46 +01:00
|
|
|
|
2015-01-02 15:33:23 +01:00
|
|
|
// Insert curve values into a cached array
|
|
|
|
function insertCurve(v) {
|
|
|
|
var y0 = v[1],
|
|
|
|
y1 = v[7],
|
2016-01-07 11:02:51 +01:00
|
|
|
winding = y0 === y1
|
|
|
|
? 0 // Horizontal
|
|
|
|
: y0 > y1
|
|
|
|
? -1 // Decreasing
|
|
|
|
: 1, // Increasing
|
2015-01-02 15:33:23 +01:00
|
|
|
curve = {
|
|
|
|
values: v,
|
2016-01-07 11:02:51 +01:00
|
|
|
winding: winding,
|
2015-01-02 15:33:23 +01:00
|
|
|
// 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;
|
2016-01-07 11:02:51 +01:00
|
|
|
// Keep track of the first and last curves with winding numbers.
|
|
|
|
if (winding) {
|
|
|
|
if (!firstWinding)
|
|
|
|
firstWinding = curve;
|
|
|
|
lastWinding = curve;
|
|
|
|
}
|
2015-01-02 15:33:23 +01:00
|
|
|
}
|
2014-02-20 20:00:46 +01:00
|
|
|
|
2015-12-28 17:54:07 +01:00
|
|
|
// Handle bezier curves. We need to chop them into smaller curves with
|
2015-01-02 15:33:23 +01:00
|
|
|
// 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 17:35:27 +02:00
|
|
|
if (Curve.isStraight(v)) {
|
|
|
|
// Handling straight curves is easy.
|
2015-01-02 15:33:23 +01: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 22:55:58 +02:00
|
|
|
tMin = /*#=*/Numerical.CURVETIME_EPSILON,
|
2015-09-12 22:14:04 +02: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 15:33:23 +01:00
|
|
|
insertCurve(v);
|
|
|
|
} else {
|
|
|
|
roots.sort();
|
|
|
|
var t = roots[0],
|
|
|
|
parts = Curve.subdivide(v, t);
|
|
|
|
insertCurve(parts[0]);
|
2015-09-12 22:14:04 +02:00
|
|
|
if (n > 1) {
|
2015-01-02 15:33:23 +01: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 20:00:46 +01:00
|
|
|
|
2015-01-02 15:33:23 +01: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;
|
2016-01-07 11:02:51 +01:00
|
|
|
// Add information about the loop length and the first / last
|
|
|
|
// curve with non-zero winding (Used in getWinding()).
|
|
|
|
first.length = monoCurves.length;
|
|
|
|
first.firstWinding = firstWinding;
|
|
|
|
first.lastWinding = lastWinding;
|
2015-01-02 15:33:23 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return monoCurves;
|
|
|
|
},
|
2014-03-17 09:48:00 +01:00
|
|
|
|
2015-01-02 15:33:23 +01: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,
|
2015-12-27 21:05:55 +01:00
|
|
|
intercepts = [];
|
2015-01-02 15:33:23 +01:00
|
|
|
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]
|
2015-12-27 21:05:55 +01:00
|
|
|
|| y >= values[7] && y <= values[1])) {
|
|
|
|
var count = Curve.solveCubic(values, 1, y, roots, 0, 1);
|
|
|
|
for (var j = count - 1; j >= 0; j--) {
|
|
|
|
intercepts.push(Curve.getPoint(values, roots[j]).x);
|
|
|
|
}
|
2015-01-02 15:33:23 +01:00
|
|
|
}
|
2015-12-27 21:05:55 +01:00
|
|
|
if (intercepts.length > 1)
|
2015-01-02 15:33:23 +01:00
|
|
|
break;
|
|
|
|
}
|
2015-12-27 21:05:55 +01:00
|
|
|
point.x = (intercepts[0] + intercepts[1]) / 2;
|
2015-01-02 15:33:23 +01:00
|
|
|
}
|
|
|
|
return point;
|
|
|
|
}
|
2014-02-20 20:00:46 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
CompoundPath.inject(/** @lends CompoundPath# */{
|
2015-01-02 15:33:23 +01: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-12 13:34:43 +01:00
|
|
|
});
|