mirror of
https://github.com/scratchfoundation/paper.js.git
synced 2025-01-03 19:45:44 -05:00
Implement proper handling of self-touching paths in resolveCrossings().
Closes #874, #887
This commit is contained in:
parent
d89995a781
commit
5a16d0cd01
6 changed files with 116 additions and 49 deletions
|
@ -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);
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
}, []));
|
}, []));
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue