Start work on removing resolveCrossings()

Relates to #1285
This commit is contained in:
Jürg Lehni 2017-03-18 17:22:44 +01:00
parent 374107c439
commit 8a0f0ad448
3 changed files with 20 additions and 162 deletions

View file

@ -49,16 +49,11 @@ PathItem.inject(new function() {
/*
* 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
* remove empty curves, #resolveCrossings() to resolve self-intersection
* make sure all paths have correct winding direction.
* remove empty curves.
*/
function preparePath(path, resolve) {
var res = path.clone(false).reduce({ simplify: true })
return path.clone(false).reduce({ simplify: true })
.transform(null, true, true);
return resolve
? res.resolveCrossings().reorient(
res.getFillRule() === 'nonzero', true)
: res;
}
function createResult(ctor, paths, reduce, path1, path2, options) {
@ -100,10 +95,13 @@ PathItem.inject(new function() {
if (_path2 && (operator.subtract || operator.exclude)
^ (_path2.isClockwise() ^ _path1.isClockwise()))
_path2.reverse();
// Split curves at crossings on both paths. Note that for self-
// intersection, path2 is null and getIntersections() handles it.
var crossings = divideLocations(
CurveLocation.expand(_path1.getCrossings(_path2))),
// Split curves in intersections and self-intersections on both paths.
var intersections = divideLocations(CurveLocation.expand(
Curve.getIntersections(
// Note that for self-intersection, path2 is null.
_path1.getCurves().concat(_path2 ? _path2.getCurves() : [])
)
)),
paths1 = _path1._children || [_path1],
paths2 = _path2 && (_path2._children || [_path2]),
segments = [],
@ -121,18 +119,18 @@ PathItem.inject(new function() {
}
}
if (crossings.length) {
if (intersections.length) {
// Collect all segments and curves of both involved operands.
collect(paths1);
if (paths2)
collect(paths2);
// Propagate the winding contribution. Winding contribution of
// curves does not change between two crossings.
// curves does not change between two intersections.
// First, propagate winding contributions for curve chains starting
// in all crossings:
for (var i = 0, l = crossings.length; i < l; i++) {
propagateWinding(crossings[i]._segment, _path1, _path2, curves,
operator);
// in all intersections:
for (var i = 0, l = intersections.length; i < l; i++) {
propagateWinding(intersections[i]._segment, _path1, _path2,
curves, operator);
}
for (var i = 0, l = segments.length; i < l; i++) {
var segment = segments[i],
@ -146,8 +144,8 @@ PathItem.inject(new function() {
}
paths = tracePaths(segments, operator);
} else {
// When there are no crossings, the result can be determined through
// a much faster call to reorientPaths():
// When there are no intersections, the result can be determined
// through a much faster call to reorientPaths():
paths = reorientPaths(
// Make sure reorientPaths() never works on original
// _children arrays by calling paths1.slice()
@ -358,7 +356,7 @@ PathItem.inject(new function() {
var loc = locations[i],
// Retrieve curve-time before calling include(), because it may
// be changed to the scaled value after splitting previously.
// See CurveLocation#getCurve(), #resolveCrossings()
// See CurveLocation#getCurve()
time = loc._time,
origTime = time,
exclude = include && !include(loc),
@ -1101,120 +1099,6 @@ PathItem.inject(new function() {
], true, this, path, options);
},
/*
* Resolves all crossings of a path item by splitting the path or
* compound-path in each self-intersection and tracing the result.
* 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.
*
* @return {PahtItem} the resulting path item
*/
resolveCrossings: function() {
var children = this._children,
// Support both path and compound-path items
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.hasOverlap() && (hasOverlaps = true) ||
inter.isCrossing() && (hasCrossings = true);
}),
// We only need to keep track of curves that need clearing
// outside of divideLocations() if two calls are necessary.
clearCurves = hasOverlaps && hasCrossings && [];
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.hasOverlap();
}, clearCurves);
for (var i = overlaps.length - 1; i >= 0; i--) {
var seg = overlaps[i]._segment,
prev = seg.getPrevious(),
next = seg.getNext();
if (hasOverlap(prev) && hasOverlap(next)) {
seg.remove();
prev._handleOut._set(0, 0);
next._handleIn._set(0, 0);
// If the curve that is left has no length, remove it
// altogether. Check for paths with only one segment
// before removal, since `prev.getCurve() == null`.
if (prev !== seg && !prev.getCurve().hasLength()) {
// Transfer handleIn when removing segment:
next._handleIn.set(prev._handleIn);
prev.remove();
}
}
}
}
if (hasCrossings) {
// Divide any remaining intersections that are still part of
// valid paths after the removal of overlaps.
divideLocations(intersections, hasOverlaps && 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(),
seg1 = inter.getSegment(),
// Do not call getCurve() and getSegment() on the other
// intersection yet, as it too is in the intersections
// array and will be divided later. But check if its
// current curve is valid, as required by some rare edge
// cases, related to intersections on the same curve.
other = inter._intersection,
curve2 = other._curve,
seg2 = other._segment;
if (curve1 && curve2 && curve1._path && curve2._path)
return true;
// Remove all intersections that were involved in the
// handling of overlaps, to not confuse tracePaths().
if (seg1)
seg1._intersection = null;
if (seg2)
seg2._intersection = null;
}, clearCurves);
if (clearCurves)
clearCurveHandles(clearCurves);
// Finally resolve self-intersections through tracePaths()
paths = tracePaths(Base.each(paths, function(path) {
this.push.apply(this, path._segments);
}, []));
}
// Determine how to return the paths: First try to recycle the
// current path / compound-path, if the amount of paths does not
// require a conversion.
var length = paths.length,
item;
if (length > 1 && children) {
if (paths !== children)
this.setChildren(paths);
item = this;
} else if (length === 1 && !children) {
if (paths[0] !== this)
this.setSegments(paths[0].removeSegments());
item = this;
}
// 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);
item.addChildren(paths);
item = item.reduce();
item.copyAttributes(this);
this.replaceWith(item);
}
return item;
},
/**
* Fixes the orientation of the sub-paths of a compound-path, assuming
* that non of its sub-paths intersect, by reorienting them so that they

View file

@ -351,9 +351,6 @@ var PathItem = Item.extend(/** @lends PathItem# */{
return this.getIntersections(path, function(inter) {
// TODO: Only return overlaps that are actually crossings! For this
// we need proper overlap range detection / merging first...
// But as we call #resolveCrossings() first in boolean operations,
// removing all self-touching areas in paths, this currently works
// as it should in the known use cases.
// The ideal implementation would deal with it in a way outlined in:
// https://github.com/paperjs/paper.js/issues/874#issuecomment-168332391
return inter.hasOverlap() || inter.isCrossing();

View file

@ -67,29 +67,6 @@ test('frame.intersect(rect)', function() {
'M140,50l10,0l0,150l-10,0z');
});
test('PathItem#resolveCrossings()', function() {
var paths = [
'M100,300l0,-50l50,-50l-50,0l150,0l-150,0l50,0l-50,0l100,0l-100,0l0,-100l200,0l0,200z',
'M50,300l0,-150l50,25l0,-75l200,0l0,200z M100,200l50,0l-50,-25z',
'M330.1,388.5l-65,65c0,0 -49.1,-14.5 -36.6,-36.6c12.5,-22.1 92.4,25.1 92.4,25.1c0,0 -33.3,-73.3 -23.2,-85.9c10,-12.8 32.4,32.4 32.4,32.4z',
'M570,290l5.8176000300452415,33.58556812220928l-28.17314339506561,-14.439003967264455l31.189735425395614,-4.568209255479985c-5.7225406635552645e-9,-3.907138079739525e-8 -59.366611385062015,8.695139599513823 -59.366611385062015,8.695139599513823z',
'M228.26666666666668,222.72h55.46666666666667c3.05499999999995,0 5.546666666666624,2.4916666666666742 5.546666666666624,5.546666666666681v55.46666666666667c0,3.05499999999995 -2.4916666666666742,5.546666666666624 -5.546666666666624,5.546666666666624h-55.46666666666667c-3.055000000000007,0 -5.546666666666681,-2.4916666666666742 -5.546666666666681,-5.546666666666624v-55.46666666666667c0,-3.055000000000007 2.4916666666666742,-5.546666666666681 5.546666666666681,-5.546666666666681zM283.73333399705655,289.2799999999998c-2.212411231994338e-7,1.1368683772161603e-13 2.212409526691772e-7,0 0,0z'
];
var results = [
'M100,300l0,-50l50,-50l-50,0l0,-100l200,0l0,200z',
'M50,300l0,-150l50,25l0,-75l200,0l0,200z M100,200l50,0l-50,-25z',
'M291.85631,426.74369l-26.75631,26.75631c0,0 -49.1,-14.5 -36.6,-36.6c7.48773,-13.23831 39.16013,-1.61018 63.35631,9.84369z M330.1,388.5l-22.09831,22.09831c-8.06306,-21.54667 -15.93643,-47.46883 -10.30169,-54.49831c10,-12.8 32.4,32.4 32.4,32.4z M320.9,442c0,0 -12.84682,-7.58911 -29.04369,-15.25631l16.14539,-16.14539c6.38959,17.07471 12.89831,31.40169 12.89831,31.40169z',
'M570,290l5.8176,33.58557l-28.17314,-14.439c-14.32289,2.0978 -28.17688,4.12693 -28.17688,4.12693z',
'M228.26666666666668,222.72h55.46666666666667c3.05499999999995,0 5.546666666666624,2.4916666666666742 5.546666666666624,5.546666666666681v55.46666666666667c0,3.05499999999995 -2.4916666666666742,5.546666666666624 -5.546666666666624,5.546666666666624h-55.46666666666667c-3.055000000000007,0 -5.546666666666681,-2.4916666666666742 -5.546666666666681,-5.546666666666624v-55.46666666666667c0,-3.055000000000007 2.4916666666666742,-5.546666666666681 5.546666666666681,-5.546666666666681z'
];
for (var i = 0; i < paths.length; i++) {
var path = PathItem.create(paths[i]),
result = PathItem.create(results[i]);
path.fillRule = 'evenodd';
compareBoolean(path.resolveCrossings(), result, 'path.resolveCrossings(); // Test ' + (i + 1));
}
});
test('#541', function() {
var shape0 = new Path.Rectangle({
insert: false,
@ -610,12 +587,12 @@ test('#973', function() {
path.segments[1].point.y += 60;
path.segments[3].point.y -= 60;
var resolved = path.resolveCrossings();
var resolved = path.unite();
var orientation = resolved.children.map(function(child) {
return child.isClockwise();
});
equals(orientation, [true, false, true],
'children orientation after calling path.resolveCrossings()');
'children orientation after calling path.unite()');
});
test('#1036', function() {