mirror of
https://github.com/scratchfoundation/paper.js.git
synced 2024-12-29 09:22:22 -05:00
Implement new version of #reorient() and merge with #resolveCrossings()
As proposed by @iconexperience in #854
This commit is contained in:
parent
48c0988546
commit
386632b0be
3 changed files with 113 additions and 54 deletions
|
@ -256,8 +256,6 @@
|
||||||
// pathA.style = pathStyleBoolean;
|
// pathA.style = pathStyleBoolean;
|
||||||
// pathB.style = pathStyleBoolean;
|
// pathB.style = pathStyleBoolean;
|
||||||
|
|
||||||
// // reorientCompoundPath(pathB)
|
|
||||||
|
|
||||||
// // var ixs = pathA.getIntersections(pathB);
|
// // var ixs = pathA.getIntersections(pathB);
|
||||||
// // ixs.map(function(a) { console.log("(" + a.path.id + " , " + a.curve.index + " , "+ a.parameter +")");
|
// // ixs.map(function(a) { console.log("(" + a.path.id + " , " + a.curve.index + " , "+ a.parameter +")");
|
||||||
// // markPoint(a.point, " ") });
|
// // markPoint(a.point, " ") });
|
||||||
|
|
|
@ -153,6 +153,8 @@ var CompoundPath = PathItem.extend(/** @lends CompoundPath# */{
|
||||||
if (children.length === 0) { // Replace with a simple empty Path
|
if (children.length === 0) { // Replace with a simple empty Path
|
||||||
var path = new Path(Item.NO_INSERT);
|
var path = new Path(Item.NO_INSERT);
|
||||||
path.insertAbove(this);
|
path.insertAbove(this);
|
||||||
|
// TODO: Consider using Item#_clone() for this, but find a way to
|
||||||
|
// not clone children / name (content).
|
||||||
path.setStyle(this._style);
|
path.setStyle(this._style);
|
||||||
this.remove();
|
this.remove();
|
||||||
return path;
|
return path;
|
||||||
|
|
|
@ -47,12 +47,12 @@ PathItem.inject(new function() {
|
||||||
/*
|
/*
|
||||||
* Creates a clone of the path that we can modify freely, with its matrix
|
* 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
|
* applied to its geometry. Calls #reduce() to simplify compound paths and
|
||||||
* remove empty curves, #resolveCrossings() to resolve self- intersection
|
* remove empty curves, #resolveCrossings() to resolve self-intersection
|
||||||
* and #reorient() to 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().transform(null, true, true);
|
||||||
return resolve ? res.resolveCrossings().reorient() : res;
|
return resolve ? res.resolveCrossings() : res;
|
||||||
}
|
}
|
||||||
|
|
||||||
function finishBoolean(ctor, paths, path1, path2, reduce) {
|
function finishBoolean(ctor, paths, path1, path2, reduce) {
|
||||||
|
@ -758,18 +758,116 @@ PathItem.inject(new function() {
|
||||||
this, path, true);
|
this, path, true);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
resolveCrossings: function() {
|
resolveCrossings: function() {
|
||||||
var crossings = this.getCrossings();
|
var children = this._children,
|
||||||
if (!crossings.length)
|
// Support both path and compound-path items
|
||||||
return this;
|
paths = children || [this],
|
||||||
divideLocations(CurveLocation.expand(crossings));
|
crossings = this.getCrossings();
|
||||||
var paths = this._children || [this],
|
// First resolve all self-intersections
|
||||||
segments = [];
|
if (crossings.length) {
|
||||||
for (var i = 0, l = paths.length; i < l; i++) {
|
divideLocations(CurveLocation.expand(crossings));
|
||||||
segments.push.apply(segments, paths[i]._segments);
|
// Resolve self-intersections through tracePaths()
|
||||||
|
paths = tracePaths(Base.each(paths, function(path) {
|
||||||
|
this.push.apply(this, path._segments);
|
||||||
|
}, []));
|
||||||
}
|
}
|
||||||
return finishBoolean(CompoundPath, tracePaths(segments),
|
// By now, all paths are non-overlapping, but might be fully
|
||||||
this, null, false);
|
// 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.
|
||||||
|
paths = paths.slice().sort(function (a, b) {
|
||||||
|
return b.getBounds().getArea() - a.getBounds().getArea();
|
||||||
|
});
|
||||||
|
var first = paths[0],
|
||||||
|
clockwise = first.isClockwise(),
|
||||||
|
items = [first],
|
||||||
|
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.
|
||||||
|
// The first, largest path can be skipped.
|
||||||
|
for (var i = 1; i < length; i++) {
|
||||||
|
var path = paths[i],
|
||||||
|
point = path.getInteriorPoint(),
|
||||||
|
isOverlapping = false,
|
||||||
|
exclude = false,
|
||||||
|
counter = 0;
|
||||||
|
for (var j = i - 1; j >= 0; j--) {
|
||||||
|
if (paths[j].contains(point)) {
|
||||||
|
if (isNonZero && !isOverlapping) {
|
||||||
|
windings[i] += windings[j];
|
||||||
|
// Remove path if rule is nonzero and winding
|
||||||
|
// changes from nonzero to zero or from zero to
|
||||||
|
// nonzero between containing path and path.
|
||||||
|
if (windings[i] && windings[j]) {
|
||||||
|
exclude = excluded[i] = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isOverlapping = true;
|
||||||
|
// Increase counter for containing paths only if
|
||||||
|
// path will not be excluded.
|
||||||
|
if (!excluded[j])
|
||||||
|
counter++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!exclude) {
|
||||||
|
// Set correct orientation and add to final items.
|
||||||
|
path.setClockwise((counter % 2 === 0) == clockwise);
|
||||||
|
items.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Replace paths with the processed items list:
|
||||||
|
paths = items;
|
||||||
|
length = items.length;
|
||||||
|
} else if (length === 1) {
|
||||||
|
// TODO: Is this really required? We don't do the same for
|
||||||
|
// compound-paths:
|
||||||
|
// Paths that are not part of compound paths should never be
|
||||||
|
// counter- clockwise for boolean operations.
|
||||||
|
paths[0].setClockwise(true);
|
||||||
|
}
|
||||||
|
// First try to recycle the current path / compound-path, if the
|
||||||
|
// amount of paths do not require a conversion.
|
||||||
|
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.setChildren(paths);
|
||||||
|
item = item.reduce();
|
||||||
|
// TODO: Consider using Item#_clone() for this, but find a way to
|
||||||
|
// not clone children / name (content).
|
||||||
|
item.setStyle(this._style);
|
||||||
|
this.replaceWith(item);
|
||||||
|
}
|
||||||
|
return item;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -912,13 +1010,6 @@ Path.inject(/** @lends Path# */{
|
||||||
point.x = (xIntercepts[0] + xIntercepts[1]) / 2;
|
point.x = (xIntercepts[0] + xIntercepts[1]) / 2;
|
||||||
}
|
}
|
||||||
return point;
|
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;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -934,37 +1025,5 @@ CompoundPath.inject(/** @lends CompoundPath# */{
|
||||||
for (var i = 0, l = children.length; i < l; i++)
|
for (var i = 0, l = children.length; i < l; i++)
|
||||||
monoCurves.push.apply(monoCurves, children[i]._getMonoCurves());
|
monoCurves.push.apply(monoCurves, children[i]._getMonoCurves());
|
||||||
return monoCurves;
|
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 on itself, but
|
|
||||||
// the boolean code above resolves these before calling reorient().
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue