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

922 lines
39 KiB
JavaScript
Raw Normal View History

/*
* Paper.js - The Swiss Army Knife of Vector Graphics Scripting.
* http://paperjs.org/
*
2014-01-03 19:47:16 -05:00
* Copyright (c) 2011 - 2014, Juerg Lehni & Jonathan Puckey
* http://scratchdisk.com/ & http://jonathanpuckey.com/
*
* Distributed under the MIT license. See LICENSE file for details.
*
* All rights reserved.
*/
/*
2013-05-03 19:31:36 -04:00
* Boolean Geometric Path Operations
*
* Supported
2013-05-05 19:38:18 -04:00
* - Path and CompoundPath items
* - Boolean Union
* - Boolean Intersection
* - Boolean Subtraction
* - Boolean Exclusion
* - Resolving a self-intersecting Path items
* - Boolean operations on self-intersecting Paths items
*
* @author Harikrishnan Gopalakrishnan
* http://hkrish.com/playground/paperjs/booleanStudy.html
*/
PathItem.inject(new function() {
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;
}
};
// 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) {
return path.clone(false).reduce().reorient().transform(null, true, true);
}
function finishBoolean(ctor, paths, path1, path2) {
var result = new ctor(Item.NO_INSERT);
result.addChildren(paths, true);
// See if the CompoundPath can be reduced to just a simple Path.
result = result.reduce();
// 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;
}
// 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.
function computeBoolean(path1, path2, operation) {
// 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
// i.e. subtraction(A:Path, B:Path):CompoundPath etc.
var _path1 = preparePath(path1),
_path2 = path2 && path1 !== path2 && preparePath(path2);
// Give both paths the same orientation except for subtraction
// and exclusion, where we need them at opposite orientation.
if (_path2 && /^(subtract|exclude)$/.test(operation)
^ (_path2.isClockwise() !== _path1.isClockwise()))
_path2.reverse();
// Split curves at intersections on both paths. Note that for self
// intersection, _path2 will be null and getIntersections() handles it.
// console.time('intersection');
var locations = _path1._getIntersections(_path2, null, []);
// console.timeEnd('inter');
if (_path2) {
// console.time('self-intersection');
// Resolve self-intersections on both source paths and add them to
// the locations too:
// var self = [];
_path1._getIntersections(null, null, locations);
_path2._getIntersections(null, null, locations);
/*
self.forEach(function(inter) {
new Path.Circle({
center: inter.point,
2015-09-14 09:16:52 -04:00
radius: fontSize / 2 * scaleFactor,
fillColor: 'red'
});
});
console.log(self);
*/
// console.timeEnd('self-intersection');
}
// console.timeEnd('intersection');
splitPath(Curve._filterIntersections(locations, true));
var segments = [],
// Aggregate of all curves in both operands, monotonic in y
monoCurves = [];
2014-01-25 23:39:51 -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());
}
}
// Collect all segments and monotonic curves
collect(_path1._children || [_path1]);
if (_path2)
collect(_path2._children || [_path2]);
// Propagate the winding contribution. Winding contribution of curves
// does not change between two intersections.
// First, propagate winding contributions for curve chains starting in
// all intersections:
for (var i = 0, l = locations.length; i < l; i++) {
propagateWinding(locations[i]._segment, _path1, _path2, monoCurves,
operation);
}
// Now process the segments that are not part of any intersecting chains
for (var i = 0, l = segments.length; i < l; i++) {
var segment = segments[i];
if (segment._winding == null) {
propagateWinding(segment, _path1, _path2, monoCurves,
operation);
}
}
return finishBoolean(CompoundPath,
tracePaths(segments, monoCurves, operation, _path1, _path2),
path1, path2);
}
var scaleFactor = 0.25; // 1 / 3000;
var textAngle = 33;
2015-09-14 09:16:52 -04:00
var fontSize = 5;
/**
* Private method for splitting a PathItem at the given intersections.
* The routine works for both self intersections and intersections
* between PathItems.
2015-06-16 11:50:37 -04:00
*
* @param {CurveLocation[]} intersections Array of CurveLocation objects
*/
function splitPath(intersections) {
if (window.reportIntersections) {
2015-09-14 09:16:52 -04:00
console.log('Intersections', intersections.length / 2);
2015-08-30 08:38:18 -04:00
intersections.forEach(function(inter) {
if (inter._other)
return;
var other = inter._intersection;
2015-08-30 08:38:18 -04:00
var log = ['CurveLocation', inter._id, 'p', inter.getPath()._id,
'i', inter.getIndex(), 't', inter._parameter,
'o', !!inter._overlap,
'Other', other._id, 'p', other.getPath()._id,
'i', other.getIndex(), 't', other._parameter,
'o', !!other._overlap];
new Path.Circle({
center: inter.point,
2015-09-14 09:16:52 -04:00
radius: fontSize / 2 * scaleFactor,
strokeColor: 'green',
strokeScaling: false
});
2015-08-30 08:38:18 -04:00
console.log(log.map(function(v) {
return v == null ? '-' : v
}).join(' '));
});
}
// TODO: Make public in API, since useful!
var tMin = /*#=*/Numerical.CURVETIME_EPSILON,
2015-01-04 11:37:15 -05:00
tMax = 1 - tMin,
noHandles = false,
clearSegments = [];
for (var i = intersections.length - 1, curve, prev; i >= 0; i--) {
var loc = intersections[i],
t = loc._parameter;
2015-01-04 11:37:15 -05:00
// Check if we are splitting same curve multiple times, but avoid
// dividing with zero.
if (prev && prev._curve === loc._curve && prev._parameter > 0) {
// Scale parameter after previous split.
t /= prev._parameter;
} else {
curve = loc._curve;
noHandles = !curve.hasHandles();
}
var segment;
if (t < tMin) {
segment = curve._segment1;
} else if (t > tMax) {
segment = curve._segment2;
} else {
// Split the curve at t, passing true for ignoreStraight to not
// force the result of splitting straight curves straight.
var newCurve = curve.divide(t, true, true);
segment = newCurve._segment1;
curve = newCurve.getPrevious();
// Keep track of segments of once straight curves, so they can
// be set back straight at the end.
if (noHandles)
clearSegments.push(segment);
}
// Link the new segment with the intersection on the other curve
segment._intersection = loc._intersection;
// loc._setCurve(segment.getCurve());
loc._segment = segment;
prev = loc;
}
// 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();
}
}
/**
* Private method that returns the winding contribution of the given point
* with respect to a given set of monotone curves.
*/
function getWinding(point, curves, horizontal, testContains) {
var epsilon = /*#=*/Numerical.GEOMETRIC_EPSILON,
tMin = /*#=*/Numerical.CURVETIME_EPSILON,
tMax = 1 - tMin,
2015-01-04 18:13:30 -05:00
px = point.x,
py = point.y,
windLeft = 0,
windRight = 0,
roots = [],
abs = Math.abs;
// 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,
yBefore = py - epsilon,
yAfter = py + epsilon;
// 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) {
for (var j = roots.length - 1; j >= 0; j--) {
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;
}
}
}
}
// 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;
// TODO: Don't we need to pass on testContains here?
if (yTop > -Infinity)
2015-01-04 18:13:30 -05:00
windLeft = getWinding(new Point(px, yTop), curves);
if (yBottom < Infinity)
2015-01-04 18:13:30 -05:00
windRight = getWinding(new Point(px, yBottom), curves);
} else {
var xBefore = px - epsilon,
xAfter = px + epsilon;
// Find the winding number for right side of the curve, inclusive of
// the curve itself, while tracing along its +-x direction.
var startCounted = false,
prevCurve,
prevT;
for (var i = 0, l = curves.length; i < l; i++) {
var curve = curves[i],
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.
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) {
var t = roots[0];
// 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:
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]
// Detect 2nd case of a consecutive intercept, but make
// sure we're still on the same loop.
|| t < tMin && prevT > tMax
&& curve.previous === prevCurve)) {
var x = Curve.getPoint(values, t).x,
slope = Curve.getTangent(values, t).y,
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.
if (Numerical.isZero(slope) && !Curve.isStraight(values)
// Does the slope over curve beginning change?
|| t < tMin && slope * Curve.getTangent(
curve.previous.values, 1).y < 0
// Does the slope over curve end change?
|| 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;
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;
counted = true;
2015-01-04 18:13:30 -05:00
} else if (x >= xAfter) {
2015-01-04 16:37:27 -05:00
windRight += winding;
counted = true;
}
// 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;
}
prevCurve = curve;
2015-01-04 18:09:34 -05:00
prevT = t;
}
}
}
return Math.max(abs(windLeft), abs(windRight));
}
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.
var epsilon = /*#=*/Numerical.GEOMETRIC_EPSILON;
chain = [],
start = segment,
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 });
totalLength += length;
segment = segment.getNext();
} while (segment && !segment._intersection && segment !== start);
// 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,
pt = curve.getPointAt(length),
2015-09-13 08:19:56 -04:00
hor = curve.isHorizontal();
if (parent instanceof CompoundPath)
path = parent;
// While subtracting, we need to omit this curve if this
// curve is contributing to the second operand and is
// outside the first operand.
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);
for (var j = chain.length - 1; j >= 0; j--) {
var seg = chain[j].segment,
inter = seg._intersection,
wind = winding;
// We need to handle the edge cases of overlapping curves
// differently based on the type of operation, and adjust the
// winding number accordingly:
if (inter && inter._overlap) {
switch (operation) {
case 'unite':
if (wind === 1)
wind = 2;
break;
case 'intersect':
if (wind === 2)
wind = 1;
break;
}
}
seg._winding = wind;
}
}
var segmentOffset = {};
var pathIndices = {};
var pathIndex = 0;
/**
* 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
*/
function tracePaths(segments, monoCurves, operation, path1, path2) {
var segmentCount = 0;
var pathCount = 1;
function labelSegment(seg, text, color) {
var point = seg.point;
2015-09-14 09:16:52 -04:00
var key = Math.round(point.x / scaleFactor) + ',' + Math.round(point.y / scaleFactor);
var offset = segmentOffset[key] || 0;
segmentOffset[key] = offset + 1;
2015-09-14 09:16:52 -04:00
var size = fontSize * scaleFactor;
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)),
content: text,
justification: 'left',
fillColor: color,
fontSize: fontSize
});
// TODO! PointText should have pivot in #point by default!
text.pivot = text.globalToLocal(text.point);
2015-09-14 09:16:52 -04:00
text.scale(scaleFactor);
text.rotate(textAngle);
}
function drawSegment(seg, text, index, color) {
if (!window.reportSegments)
return;
new Path.Circle({
center: seg.point,
2015-09-14 09:16:52 -04:00
radius: fontSize / 2 * scaleFactor,
strokeColor: color,
strokeScaling: false
});
var inter = seg._intersection;
labelSegment(seg, '#' + pathCount + '.'
+ (path ? path._segments.length + 1 : 1)
+ ' ' + (segmentCount++) + '/' + index + ': ' + text
+ ' v: ' + !!seg._visited
+ ' p: ' + seg._path._id
+ ' x: ' + seg._point.x
+ ' y: ' + seg._point.y
+ ' op: ' + operator(seg._winding)
+ ' o: ' + (inter && inter._overlap || 0)
+ ' w: ' + seg._winding
, color);
}
for (var i = 0; i < (window.reportWindings ? segments.length : 0); i++) {
var seg = segments[i];
path = seg._path,
id = path._id,
point = seg.point,
inter = seg._intersection;
if (!(id in pathIndices))
pathIndices[id] = ++pathIndex;
labelSegment(seg, '#' + pathIndex + '.' + (i + 1)
+ ' i: ' + !!inter
+ ' p: ' + seg._path._id
+ ' x: ' + seg._point.x
+ ' y: ' + seg._point.y
+ ' o: ' + (inter && inter._overlap || 0)
+ ' w: ' + seg._winding
, 'green');
}
var paths = [],
operator = operators[operation];
for (var i = 0, l = segments.length; i < l; i++) {
var seg = segments[i];
if (seg._visited || !operator(seg._winding))
continue;
var path = new Path(Item.NO_INSERT),
start = seg,
inter = seg._intersection,
other = inter && inter._segment,
otherStart = other,
added = false; // Whether a first segment as added already
do {
var handleIn = added && seg._handleIn;
if (!added || !other || other === start) {
// TODO: Is (other === start) check really required?
// Does that ever occur?
// Just add the first segment and all segments that have no
// intersection.
drawSegment(seg, 'add', i, 'black');
} else if (other._path === seg._path) { // Self-intersection
drawSegment(seg, 'self-int', i, 'red');
// Switch to the intersecting segment, as we need to
// resolving self-Intersections.
seg = other;
} else if (operation !== 'intersect' && inter._overlap) {
// Switch to the overlapping intersecting segment if its
// winding number along the curve is 1, meaning we leave the
// overlapping area.
// NOTE: We cannot check the next (overlapping) segment
// since its winding number will always be 2.
var curve = other.getCurve();
if (getWinding(curve.getPointAt(0.5, true),
monoCurves, curve.isHorizontal()) === 1) {
drawSegment(seg, 'overlap-cross', i, 'orange');
seg = other;
} else {
drawSegment(seg, 'overlap-stay', i, 'orange');
}
} else if (operation === 'exclude') {
// We need to handle exclusion separately, as we want to
// switch at each crossing, and at each intersection within
// the exclusion area even if it is not a crossing.
if (inter.isCrossing() || path2.contains(seg._point)) {
drawSegment(seg, 'exclude-cross', i, 'green');
seg = other;
} else {
drawSegment(seg, 'exclude-stay', i, 'orange');
}
} else if (operator(seg._winding)) {
// Do not switch to the intersecting segment as this segment
// is part of the the boolean result.
drawSegment(seg, 'ignore-keep', i, 'black');
} else if (operator(other._winding) && inter.isCrossing()) {
// The other segment is part of the boolean result, and we
// are at crossing, switch over.
drawSegment(seg, 'cross', i, 'green');
seg = other;
} else {
// Keep on truckin'
drawSegment(seg, 'stay', i, 'orange');
}
if (seg._visited) {
// We didn't manage to switch, so stop right here.
console.error('Unable to switch to intersecting segment, '
+ 'aborting #' + pathCount + '.' +
(path ? path._segments.length + 1 : 1));
break;
}
// Add the current segment to the path, and mark the added
// segment as visited.
path.add(new Segment(seg._point, handleIn, seg._handleOut));
seg._visited = added = true;
seg = seg.getNext();
inter = seg && seg._intersection;
other = inter && inter._segment;
if (seg === start || seg === otherStart) {
drawSegment(seg, 'close', i, 'red');
}
if (seg._visited && (!other || other._visited)) {
drawSegment(seg, 'visited', i, 'red');
}
if (!inter && !operator(seg._winding)) {
// TODO: We really should find a way to go backwards perhaps
// and try another path when this happens?
drawSegment(seg, 'discard', i, 'red');
console.error('Excluded segment encountered, aborting #'
+ pathCount + '.' +
(path ? path._segments.length + 1 : 1));
}
} while (seg && seg !== start && seg !== otherStart
// If we're about to switch, try to see if we can carry on
// if the other segment wasn't visited yet.
&& (!seg._visited || other && !other._visited)
// 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
&& (inter || operator(seg._winding)));
// Finish with closing the paths if necessary, correctly linking up
// curves etc.
if (seg === start || seg === otherStart) {
path.firstSegment.setHandleIn(seg._handleIn);
path.setClosed(true);
if (window.reportSegments) {
console.log('Boolean operation completed',
'#' + pathCount + '.' +
(path ? path._segments.length + 1 : 1));
}
} else {
// path.lastSegment._handleOut.set(0, 0);
console.error('Boolean operation results in open path, segs =',
path._segments.length, 'length = ', path.getLength(),
'#' + pathCount + '.' +
(path ? path._segments.length + 1 : 1));
path = null;
}
// Add the path to the result, while avoiding stray segments and
// 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
|| !Numerical.isZero(path.getArea())))
paths.push(path);
pathCount++;
}
return paths;
}
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
* @return {Number} the winding number
*/
_getWinding: function(point, horizontal, testContains) {
return getWinding(point, this._getMonoCurves(),
horizontal, testContains);
},
/**
* {@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) {
return computeBoolean(this, path, 'unite');
},
/**
* 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) {
return computeBoolean(this, path, 'intersect');
},
/**
* 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) {
return computeBoolean(this, path, 'subtract');
},
// 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) {
return computeBoolean(this, path, 'exclude');
},
/**
* 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) {
return finishBoolean(Group,
[this.subtract(path), this.intersect(path)], this, path);
}
};
});
Path.inject(/** @lends Path# */{
/**
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.
* Used by getWinding().
*/
_getMonoCurves: function() {
var monoCurves = this._monoCurves,
prevCurve;
// 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;
}
// 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];
if (Curve.isStraight(v)) {
// Handling straight curves is easy.
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,
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) {
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) {
// 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]);
}
}
}
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;
},
/**
* 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--)
xIntercepts.push(Curve.getPoint(values, roots[j]).x);
}
if (xIntercepts.length > 1)
break;
}
point.x = (xIntercepts[0] + xIntercepts[1]) / 2;
}
return point;
},
reorient: function() {
// Paths that are not part of compound paths should never be counter-
// clockwise for boolean operations.
this.setClockwise(true);
return this;
}
});
CompoundPath.inject(/** @lends CompoundPath# */{
/**
* 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;
},
/*
* 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();
});
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);
}
}
return this;
}
});