Implement proper handling of self-touching paths in resolveCrossings().

Closes #874, #887
This commit is contained in:
Jürg Lehni 2016-01-06 10:53:50 +01:00
parent d89995a781
commit 5a16d0cd01
6 changed files with 116 additions and 49 deletions

View file

@ -2336,10 +2336,10 @@ var Item = Base.extend(Emitter, /** @lends Item# */{
* *
* @return {Item} the reduced item * @return {Item} the reduced item
*/ */
reduce: function() { reduce: function(options) {
var children = this._children; var children = this._children;
if (children && children.length === 1) { if (children && children.length === 1) {
var child = children[0].reduce(); var child = children[0].reduce(options);
// Make sure the reduced item has the same parent as the original. // Make sure the reduced item has the same parent as the original.
if (this._parent) { if (this._parent) {
child.insertAbove(this); child.insertAbove(this);

View file

@ -143,10 +143,10 @@ var CompoundPath = PathItem.extend(/** @lends CompoundPath# */{
// DOCS: reduce() // DOCS: reduce()
// TEST: reduce() // TEST: reduce()
reduce: function reduce() { reduce: function reduce(options) {
var children = this._children; var children = this._children;
for (var i = children.length - 1; i >= 0; i--) { for (var i = children.length - 1; i >= 0; i--) {
var path = children[i].reduce(); var path = children[i].reduce(options);
if (path.isEmpty()) if (path.isEmpty())
path.remove(); path.remove();
} }

View file

@ -507,7 +507,7 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{
* @see #isCrossing() * @see #isCrossing()
* @see #isTouching() * @see #isTouching()
*/ */
isOverlap: function() { hasOverlap: function() {
return !!this._overlap; return !!this._overlap;
} }
}, Base.each(Curve.evaluateMethods, function(name) { }, Base.each(Curve.evaluateMethods, function(name) {
@ -597,7 +597,7 @@ new function() { // Scope for statics
// Create a copy since insert() keeps modifying the array and // Create a copy since insert() keeps modifying the array and
// inserting at sorted indices. // inserting at sorted indices.
var expanded = locations.slice(); var expanded = locations.slice();
for (var i = 0, l = locations.length; i < l; i++) { for (var i = locations.length - 1; i >= 0; i--) {
insert(expanded, locations[i]._intersection, false); insert(expanded, locations[i]._intersection, false);
} }
return expanded; return expanded;

View file

@ -756,6 +756,9 @@ var Path = PathItem.extend(/** @lends Path# */{
? from - 1 ? from - 1
: from, : from,
curves = curves.splice(index, amount); curves = curves.splice(index, amount);
// Unlink the removed curves from the path.
for (var i = curves.length - 1; i >= 0; i--)
curves[i]._path = null;
// Return the removed curves as well, if we're asked to include // Return the removed curves as well, if we're asked to include
// them, but exclude the first curve, since that's shared with the // them, but exclude the first curve, since that's shared with the
// previous segment and does not connect the returned segments. // previous segment and does not connect the returned segments.
@ -1026,18 +1029,20 @@ var Path = PathItem.extend(/** @lends Path# */{
* Reduces the path by removing curves that have a length of 0, * Reduces the path by removing curves that have a length of 0,
* and unnecessary segments between two collinear curves. * and unnecessary segments between two collinear curves.
*/ */
reduce: function() { reduce: function(options) {
var curves = this.getCurves(); var curves = this.getCurves(),
simplify = options && options.simplify,
// When not simplifying, only remove curves if their length is
// absolutely 0.
tolerance = simplify ? /*#=*/Numerical.GEOMETRIC_EPSILON : 0;
for (var i = curves.length - 1; i >= 0; i--) { for (var i = curves.length - 1; i >= 0; i--) {
var curve = curves[i]; var curve = curves[i];
if (!curve.hasHandles() // When simplifying, compare curves with isCollinear() will remove
&& (curve.getLength() < /*#=*/Numerical.GEOMETRIC_EPSILON // any collinear neighboring curves regardless of their orientation.
// Pass true for sameDir, as we can only remove straight // This serves as a reliable way to remove linear overlaps but only
// curves if they point in the same direction as the next // as long as the lines are truly overlapping.
// curve, not 180° in the opposite direction. if (!curve.hasHandles() && (curve.getLength() < tolerance
// NOTE: sameDir is temporarily deactivate until overlaps || simplify && curve.isCollinear(curve.getNext())))
// are handled properly.
|| curve.isCollinear(curve.getNext(), false)))
curve.remove(); curve.remove();
} }
return this; return this;

View file

@ -46,7 +46,8 @@ PathItem.inject(new function() {
* make sure all paths have correct winding direction. * make sure all paths have correct winding direction.
*/ */
function preparePath(path, resolve) { function preparePath(path, resolve) {
var res = path.clone(false).reduce().transform(null, true, true); var res = path.clone(false).reduce({ simplify: true })
.transform(null, true, true);
return resolve ? res.resolveCrossings() : res; return resolve ? res.resolveCrossings() : res;
} }
@ -55,7 +56,7 @@ PathItem.inject(new function() {
result.addChildren(paths, true); result.addChildren(paths, true);
// See if the item can be reduced to just a simple Path. // See if the item can be reduced to just a simple Path.
if (reduce) if (reduce)
result = result.reduce(); result = result.reduce({ simplify: true });
// Insert the resulting path above whichever of the two paths appear // Insert the resulting path above whichever of the two paths appear
// further up in the stack. // further up in the stack.
result.insertAbove(path2 && path1.isSibling(path2) result.insertAbove(path2 && path1.isSibling(path2)
@ -88,15 +89,14 @@ PathItem.inject(new function() {
_path2.reverse(); _path2.reverse();
// Split curves at crossings on both paths. Note that for self- // Split curves at crossings on both paths. Note that for self-
// intersection, path2 is null and getIntersections() handles it. // intersection, path2 is null and getIntersections() handles it.
var crossings = CurveLocation.expand(_path1.getCrossings(_path2, var crossings = divideLocations(CurveLocation.expand(
// Only handle overlaps when not self-intersecting // Only handle overlaps when not self-intersecting
!!_path2)); _path1.getCrossings(_path2, !!_path2))),
divideLocations(crossings); segments = [],
// Aggregate of all curves in both operands, monotonic in y.
var segments = [],
// Aggregate of all curves in both operands, monotonic in y
monoCurves = [], monoCurves = [],
startInOverlaps = true; // Keep track if all encountered segments are overlaps.
overlapsOnly = true;
function collect(paths) { function collect(paths) {
for (var i = 0, l = paths.length; i < l; i++) { for (var i = 0, l = paths.length; i < l; i++) {
@ -125,14 +125,14 @@ PathItem.inject(new function() {
if (segment._winding == null) { if (segment._winding == null) {
propagateWinding(segment, _path1, _path2, monoCurves, operator); propagateWinding(segment, _path1, _path2, monoCurves, operator);
} }
// If there are any valid segments that are not part of overlaps, // See if there are any valid segments that aren't part of overlaps.
// prefer these to start tracing boolean paths from. But if all // This information is used further down to determine where to start
// segments are part of overlaps, we need to start there. // tracing the path, and how to treat encountered invalid segments.
if (!(inter && inter.isOverlap()) && operator[segment._winding]) if (!(inter && inter._overlap) && operator[segment._winding])
startInOverlaps = false; overlapsOnly = false;
} }
return finishBoolean(CompoundPath, return finishBoolean(CompoundPath,
tracePaths(segments, operator, startInOverlaps), tracePaths(segments, operator, overlapsOnly),
path1, path2, true); path1, path2, true);
} }
@ -214,8 +214,9 @@ PathItem.inject(new function() {
* path-item at. * path-item at.
* @private * @private
*/ */
function divideLocations(locations) { function divideLocations(locations, include) {
var tMin = /*#=*/Numerical.CURVETIME_EPSILON, var results = include && [],
tMin = /*#=*/Numerical.CURVETIME_EPSILON,
tMax = 1 - tMin, tMax = 1 - tMin,
noHandles = false, noHandles = false,
clearCurves = [], clearCurves = [],
@ -226,7 +227,13 @@ PathItem.inject(new function() {
var loc = locations[i], var loc = locations[i],
curve = loc._curve, curve = loc._curve,
t = loc._parameter, t = loc._parameter,
origT = t; origT = t,
segment;
if (include) {
if (!include(loc))
continue;
results.unshift(loc);
}
if (curve !== prevCurve) { if (curve !== prevCurve) {
// This is a new curve, update noHandles setting. // This is a new curve, update noHandles setting.
noHandles = !curve.hasHandles(); noHandles = !curve.hasHandles();
@ -235,7 +242,6 @@ PathItem.inject(new function() {
// times, but avoid dividing by zero. // times, but avoid dividing by zero.
t /= prevT; t /= prevT;
} }
var segment;
if (t < tMin) { if (t < tMin) {
segment = curve._segment1; segment = curve._segment1;
} else if (t > tMax) { } else if (t > tMax) {
@ -278,6 +284,7 @@ PathItem.inject(new function() {
for (var i = 0, l = clearCurves.length; i < l; i++) { for (var i = 0, l = clearCurves.length; i < l; i++) {
clearCurves[i].clearHandles(); clearCurves[i].clearHandles();
} }
return results || locations;
} }
/** /**
@ -528,13 +535,13 @@ PathItem.inject(new function() {
* not * not
* @return {Path[]} the contours traced * @return {Path[]} the contours traced
*/ */
function tracePaths(segments, operator, startInOverlaps) { function tracePaths(segments, operator, overlapsOnly) {
var paths = [], var paths = [],
start, start,
otherStart; otherStart;
function isValid(seg) { function isValid(seg) {
return !seg._visited && (!operator || operator[seg._winding]); return !!(!seg._visited && (!operator || operator[seg._winding]));
} }
function isStart(seg) { function isStart(seg) {
@ -570,7 +577,7 @@ PathItem.inject(new function() {
// Do not consider nextSeg in strict mode if it is part // Do not consider nextSeg in strict mode if it is part
// of an overlap, in order to give non-overlapping // of an overlap, in order to give non-overlapping
// options that might follow the priority over overlaps. // options that might follow the priority over overlaps.
&& (!(strict && nextInter && nextInter.isOverlap()) && (!(strict && nextInter && nextInter._overlap)
&& isValid(nextSeg) && isValid(nextSeg)
// If the next segment isn't valid, its intersection // If the next segment isn't valid, its intersection
// to which we may switch might be, so check that. // to which we may switch might be, so check that.
@ -594,8 +601,8 @@ PathItem.inject(new function() {
// already visited, or that are not going to be part of the result). // 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 // Also don't start in overlaps, unless all segments are part of
// overlaps, in which case we have no other choice. // overlaps, in which case we have no other choice.
if (!isValid(seg) || !startInOverlaps if (!isValid(seg) || !overlapsOnly
&& inter && seg._winding && inter.isOverlap()) && inter && seg._winding && inter._overlap)
continue; continue;
start = otherStart = null; start = otherStart = null;
while (true) { while (true) {
@ -618,7 +625,7 @@ PathItem.inject(new function() {
// the boolean result, switch over. // the boolean result, switch over.
// We need to mark overlap segments as visited when // We need to mark overlap segments as visited when
// processing intersection. // processing intersection.
if (operator && operator.intersect && inter.isOverlap()) if (operator && operator.intersect && inter._overlap)
seg._visited = true; seg._visited = true;
seg = other; seg = other;
} }
@ -630,6 +637,11 @@ PathItem.inject(new function() {
seg._visited = true; seg._visited = true;
break; break;
} }
// 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.
if (overlapsOnly && !isValid(seg))
break;
if (!path) { if (!path) {
path = new Path(Item.NO_INSERT); path = new Path(Item.NO_INSERT);
start = seg; start = seg;
@ -763,12 +775,60 @@ PathItem.inject(new function() {
resolveCrossings: function() { resolveCrossings: function() {
var children = this._children, var children = this._children,
// Support both path and compound-path items // Support both path and compound-path items
paths = children || [this], paths = children || [this];
crossings = this.getCrossings();
// First resolve all self-intersections function hasOverlap(seg) {
if (crossings.length) { var inter = seg && seg._intersection;
divideLocations(CurveLocation.expand(crossings)); return inter && inter._overlap;
// Resolve self-intersections through tracePaths() }
// 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) {
// Split any remaining intersections that are still part of
// 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(),
curve2 = inter._intersection.getCurve(true),
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()
paths = tracePaths(Base.each(paths, function(path) { paths = tracePaths(Base.each(paths, function(path) {
this.push.apply(this, path._segments); this.push.apply(this, path._segments);
}, [])); }, []));

View file

@ -152,8 +152,10 @@ var PathItem = Item.extend(/** @lends PathItem# */{
*/ */
getCrossings: function(path, includeOverlaps) { getCrossings: function(path, includeOverlaps) {
return this.getIntersections(path, function(inter) { return this.getIntersections(path, function(inter) {
// TODO: Only return overlaps that are actually crossings! For this
// we need proper overlap range detection first.
// Check overlap first since it's the cheaper test between the two. // Check overlap first since it's the cheaper test between the two.
return includeOverlaps && inter.isOverlap() || inter.isCrossing(); return includeOverlaps && inter._overlap || inter.isCrossing();
}); });
}, },