mirror of
https://github.com/scratchfoundation/paper.js.git
synced 2025-01-05 20:32:00 -05:00
Implement improved reorientation of paths, that can also be used by non-crossing boolean operations.
This commit is contained in:
parent
4281800585
commit
61df327bc2
1 changed files with 132 additions and 74 deletions
|
@ -54,7 +54,7 @@ PathItem.inject(new function() {
|
||||||
var res = path.clone(false).reduce({ simplify: true })
|
var res = path.clone(false).reduce({ simplify: true })
|
||||||
.transform(null, true, true);
|
.transform(null, true, true);
|
||||||
return resolve
|
return resolve
|
||||||
? res.resolveCrossings().reorient(res.getFillRule() === 'nonzero')
|
? res.resolveCrossings().reorient(res.getFillRule() === 'nonzero', true)
|
||||||
: res;
|
: res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,9 +107,31 @@ PathItem.inject(new function() {
|
||||||
curves = [],
|
curves = [],
|
||||||
paths;
|
paths;
|
||||||
|
|
||||||
// When there are no crossings, and the two paths are not contained
|
// When there are no crossings, the result can be known ahead of tracePaths(),
|
||||||
// within each other, the result can be known ahead of tracePaths(),
|
|
||||||
// largely simplifying the processing required:
|
// largely simplifying the processing required:
|
||||||
|
if (!crossings.length) {
|
||||||
|
// the paths have been reoriented, therefore they have alternate
|
||||||
|
// windings.
|
||||||
|
var insideWindings =
|
||||||
|
operator.unite ? [1, 2] :
|
||||||
|
operator.subtract ? [1] :
|
||||||
|
operator.intersect ? [2] :
|
||||||
|
operator.exclude ? [1] :
|
||||||
|
[];
|
||||||
|
if (paths2 && operator.exclude) {
|
||||||
|
for (var i = 0; i < paths2.length; i++) {
|
||||||
|
paths2[i].reverse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var reorientedPaths = reorientPaths(
|
||||||
|
paths2 ? paths1.concat(paths2) : paths1,
|
||||||
|
function(w) {return insideWindings.indexOf(w) >= 0;}
|
||||||
|
);
|
||||||
|
paths = [
|
||||||
|
new CompoundPath({children: reorientedPaths, insert: false})
|
||||||
|
];
|
||||||
|
}
|
||||||
|
/*
|
||||||
if (!crossings.length) {
|
if (!crossings.length) {
|
||||||
// If we have two operands, check their bounds to find cases where
|
// If we have two operands, check their bounds to find cases where
|
||||||
// one path is fully contained in another. These cases cannot be
|
// one path is fully contained in another. These cases cannot be
|
||||||
|
@ -136,7 +158,7 @@ PathItem.inject(new function() {
|
||||||
: operator.intersect ? [new Path(Item.NO_INSERT)]
|
: operator.intersect ? [new Path(Item.NO_INSERT)]
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
|
||||||
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++) {
|
||||||
|
@ -243,6 +265,93 @@ PathItem.inject(new function() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reorients the specified paths.
|
||||||
|
*
|
||||||
|
* windingInsideFn is a function which determines if the inside of a path
|
||||||
|
* is filled. For non-zero fill rule this function would be implemented as
|
||||||
|
* follows:
|
||||||
|
*
|
||||||
|
* windingInsideFn = function(w) {
|
||||||
|
* return w != 0;
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* If clockwise is defined, the orientation of the root paths will be set to
|
||||||
|
* the orientation specified by clockwise. Otherwise the orientation of the
|
||||||
|
* first root child (which is the largest child) will be used.
|
||||||
|
*
|
||||||
|
* @param paths
|
||||||
|
* @param windingInsideFn
|
||||||
|
* @param clockwise (optional)
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
|
function reorientPaths(paths, windingInsideFn, clockwise) {
|
||||||
|
var length = paths && paths.length;
|
||||||
|
if (length) {
|
||||||
|
var lookup = Base.each(paths, function (path, i) {
|
||||||
|
// Build a lookup table with information for each path's
|
||||||
|
// original index and winding contribution.
|
||||||
|
this[path._id] = {
|
||||||
|
winding: path.isClockwise() ? 1 : -1,
|
||||||
|
index: i
|
||||||
|
};
|
||||||
|
}, {}),
|
||||||
|
// Now sort the paths by their areas, from large to small.
|
||||||
|
sorted = paths.slice().sort(function (a, b) {
|
||||||
|
return Math.abs(b.getArea()) - Math.abs(a.getArea());
|
||||||
|
}),
|
||||||
|
// Get reference to the first, largest path and insert it
|
||||||
|
// already.
|
||||||
|
first = sorted[0];
|
||||||
|
if (clockwise == null)
|
||||||
|
clockwise = first.isClockwise();
|
||||||
|
// determine winding for each path
|
||||||
|
for (var i = 0; i < length; i++) {
|
||||||
|
var path1 = sorted[i],
|
||||||
|
entry1 = lookup[path1._id],
|
||||||
|
point = path1.getInteriorPoint(),
|
||||||
|
containerWinding = 0;
|
||||||
|
for (var j = i - 1; j >= 0; j--) {
|
||||||
|
var path2 = sorted[j];
|
||||||
|
// We run through the paths from largest to smallest,
|
||||||
|
// meaning that for any current path, all potentially
|
||||||
|
// containing paths have already been processed and
|
||||||
|
// their orientation has been fixed. Since we want to
|
||||||
|
// achieve alternating orientation of contained paths,
|
||||||
|
// all we have to do is to find one include path that
|
||||||
|
// contains the current path, and then set the
|
||||||
|
// orientation to the opposite of the containing path.
|
||||||
|
if (path2.contains(point)) {
|
||||||
|
var entry2 = lookup[path2._id];
|
||||||
|
entry1.newContainer = entry2.exclude ? entry2.newContainer : path2;
|
||||||
|
containerWinding = entry2.winding;
|
||||||
|
entry1.winding += containerWinding;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// only keep paths if the insideness changes when crossing the
|
||||||
|
// path, e.g. the inside of the path is filled and the outside
|
||||||
|
// not filled (or vice versa).
|
||||||
|
if (windingInsideFn(entry1.winding) == windingInsideFn(containerWinding)) {
|
||||||
|
entry1.exclude = true;
|
||||||
|
} else {
|
||||||
|
// If the containing path is not excluded, we're
|
||||||
|
// done searching for the orientation defining path.
|
||||||
|
path1.setClockwise(entry1.newContainer ?
|
||||||
|
!entry1.newContainer.isClockwise() : clockwise);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// remove the excluded paths from the array
|
||||||
|
for (var i = length - 1; i >= 0; i--) {
|
||||||
|
if (lookup[paths[i]._id].exclude) {
|
||||||
|
paths.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Divides the path-items at the given locations.
|
* Divides the path-items at the given locations.
|
||||||
*
|
*
|
||||||
|
@ -1103,76 +1212,25 @@ PathItem.inject(new function() {
|
||||||
* discarding sub-paths that do not contribute to the final result
|
* discarding sub-paths that do not contribute to the final result
|
||||||
* @return {PahtItem} a reference to the item itself, reoriented
|
* @return {PahtItem} a reference to the item itself, reoriented
|
||||||
*/
|
*/
|
||||||
reorient: function(nonZero) {
|
reorient: function(nonZero, clockwise) {
|
||||||
var children = this._children,
|
var children = this._children;
|
||||||
length = children && children.length;
|
if (children && children.length) {
|
||||||
if (length > 1) {
|
children = this.removeChildren();
|
||||||
// Build a lookup table with information for each path's
|
reorientPaths(children,
|
||||||
// original index and winding contribution.
|
nonZero ?
|
||||||
var lookup = Base.each(children, function(path, i) {
|
function (w) {
|
||||||
this[path._id] = {
|
// true if winding is non-zero
|
||||||
winding: path.isClockwise() ? 1 : -1,
|
return !w
|
||||||
index: i
|
|
||||||
};
|
|
||||||
}, {}),
|
|
||||||
// Now sort the paths by their areas, from large to small.
|
|
||||||
sorted = this.removeChildren().sort(function (a, b) {
|
|
||||||
return abs(b.getArea()) - abs(a.getArea());
|
|
||||||
}),
|
|
||||||
// Get reference to the first, largest path and insert it
|
|
||||||
// already.
|
|
||||||
first = sorted[0],
|
|
||||||
paths = [];
|
|
||||||
// Always insert paths at their original index. With exclusion,
|
|
||||||
// this produces null entries, but #setChildren() handles those.
|
|
||||||
paths[lookup[first._id].index] = first;
|
|
||||||
// Walk through the sorted paths, from largest to smallest.
|
|
||||||
// Skip the first path, as it is already added.
|
|
||||||
for (var i1 = 1; i1 < length; i1++) {
|
|
||||||
var path1 = sorted[i1],
|
|
||||||
entry1 = lookup[path1._id],
|
|
||||||
point = path1.getInteriorPoint(),
|
|
||||||
isContained = false,
|
|
||||||
container = null,
|
|
||||||
exclude = false;
|
|
||||||
for (var i2 = i1 - 1; i2 >= 0 && !container; i2--) {
|
|
||||||
var path2 = sorted[i2];
|
|
||||||
// We run through the paths from largest to smallest,
|
|
||||||
// meaning that for any current path, all potentially
|
|
||||||
// containing paths have already been processed and
|
|
||||||
// their orientation has been fixed. Since we want to
|
|
||||||
// achieve alternating orientation of contained paths,
|
|
||||||
// all we have to do is to find one include path that
|
|
||||||
// contains the current path, and then set the
|
|
||||||
// orientation to the opposite of the containing path.
|
|
||||||
if (path2.contains(point)) {
|
|
||||||
var entry2 = lookup[path2._id];
|
|
||||||
if (nonZero && !isContained) {
|
|
||||||
entry1.winding += entry2.winding;
|
|
||||||
// Remove path if rule is nonzero and winding
|
|
||||||
// of path and containing path is not zero.
|
|
||||||
if (entry1.winding && entry2.winding) {
|
|
||||||
exclude = entry1.exclude = true;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
: function (w) {
|
||||||
isContained = true;
|
// true if winding is even
|
||||||
// If the containing path is not excluded, we're
|
return !(w % 2)
|
||||||
// done searching for the orientation defining path.
|
},
|
||||||
container = !entry2.exclude && path2;
|
clockwise
|
||||||
}
|
);
|
||||||
}
|
this.setChildren(children);
|
||||||
if (!exclude) {
|
} else if (clockwise != null) {
|
||||||
// Set to the opposite orientation of containing path,
|
this.setClockwise(clockwise);
|
||||||
// or the same orientation as the first path if the path
|
|
||||||
// is not contained in any other path.
|
|
||||||
path1.setClockwise(container
|
|
||||||
? !container.isClockwise()
|
|
||||||
: first.isClockwise());
|
|
||||||
paths[entry1.index] = path1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.setChildren(paths);
|
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue