Find a better implementation for exclude() boolean operations, requiring only one pass instead of two.

This commit is contained in:
Jürg Lehni 2015-01-04 01:50:24 +01:00
parent f0f98daf69
commit 390ef324f2

View file

@ -32,30 +32,51 @@
*/ */
PathItem.inject(new function() { PathItem.inject(new function() {
var operators = {
unite: function(w) {
return w === 1 || w === 0;
},
intersect: function(w) {
return w === 2;
},
subtract: function(w) {
return w === 1;
},
exclude: function(w) {
return w === 1;
}
};
// Boolean operators return true if a curve with the given winding // Boolean operators return true if a curve with the given winding
// contribution contributes to the final result or not. They are called // contribution contributes to the final result or not. They are called
// for each curve in the graph after curves in the operands are // for each curve in the graph after curves in the operands are
// split at intersections. // split at intersections.
function computeBoolean(path1, path2, operator, subtract) { function computeBoolean(path1, path2, operation) {
var operator = operators[operation];
// Creates a cloned version of the path that we can modify freely, with // Creates a cloned version of the path that we can modify freely, with
// its matrix applied to its geometry. Calls #reduce() to simplify // its matrix applied to its geometry. Calls #reduce() to simplify
// compound paths and remove empty curves, and #reorient() to make sure // compound paths and remove empty curves, and #reorient() to make sure
// all paths have correct winding direction. // all paths have correct winding direction.
function preparePath(path) { function preparePath(path) {
return path.clone(false).reduce().reorient().transform(null, true); return path.clone(false).reduce().reorient().transform(null, true,
true);
} }
// We do not modify the operands themselves // We do not modify the operands themselves, but create copies instead,
// The result might not belong to the same type // fas produced by the calls to preparePath().
// Note that the result paths might not belong to the same type
// i.e. subtraction(A:Path, B:Path):CompoundPath etc. // i.e. subtraction(A:Path, B:Path):CompoundPath etc.
var _path1 = preparePath(path1), var _path1 = preparePath(path1),
_path2 = path2 && path1 !== path2 && preparePath(path2); _path2 = path2 && path1 !== path2 && preparePath(path2);
// Do operator specific calculations before we begin // Give both paths clockwise orientation except for subtraction
// Make both paths at clockwise orientation, except when subtract = true // and exclusion, where we need them at opposite orientation.
// We need both paths at opposite orientation for subtraction.
if (!_path1.isClockwise()) if (!_path1.isClockwise())
_path1.reverse(); _path1.reverse();
if (_path2 && !(subtract ^ _path2.isClockwise())) if (_path2 && !(/^(subtract|exclude)$/.test(operation)
^ _path2.isClockwise()))
_path2.reverse(); _path2.reverse();
// Split curves at intersections on both paths. Note that for self // Split curves at intersections on both paths. Note that for self
// intersection, _path2 will be null and getIntersections() handles it. // intersection, _path2 will be null and getIntersections() handles it.
@ -134,7 +155,7 @@ PathItem.inject(new function() {
// While subtracting, we need to omit this curve if this // While subtracting, we need to omit this curve if this
// curve is contributing to the second operand and is // curve is contributing to the second operand and is
// outside the first operand. // outside the first operand.
windingSum += subtract && _path2 windingSum += operation === 'subtract' && _path2
&& (path === _path1 && _path2._getWinding(pt, hor) && (path === _path1 && _path2._getWinding(pt, hor)
|| path === _path2 && !_path1._getWinding(pt, hor)) || path === _path2 && !_path1._getWinding(pt, hor))
? 0 ? 0
@ -340,10 +361,6 @@ PathItem.inject(new function() {
* @return {Path[]} the contours traced * @return {Path[]} the contours traced
*/ */
function tracePaths(segments, operator, selfOp) { function tracePaths(segments, operator, selfOp) {
// Choose a default operator which will return all contours
operator = operator || function() {
return true;
};
var paths = [], var paths = [],
// Values for getTangentAt() that are almost 0 and 1. // Values for getTangentAt() that are almost 0 and 1.
// TODO: Correctly support getTangentAt(0) / (1)? // TODO: Correctly support getTangentAt(0) / (1)?
@ -481,9 +498,7 @@ PathItem.inject(new function() {
* @return {PathItem} the resulting path item * @return {PathItem} the resulting path item
*/ */
unite: function(path) { unite: function(path) {
return computeBoolean(this, path, function(w) { return computeBoolean(this, path, 'unite');
return w === 1 || w === 0;
}, false);
}, },
/** /**
@ -494,9 +509,7 @@ PathItem.inject(new function() {
* @return {PathItem} the resulting path item * @return {PathItem} the resulting path item
*/ */
intersect: function(path) { intersect: function(path) {
return computeBoolean(this, path, function(w) { return computeBoolean(this, path, 'intersect');
return w === 2;
}, false);
}, },
/** /**
@ -507,9 +520,7 @@ PathItem.inject(new function() {
* @return {PathItem} the resulting path item * @return {PathItem} the resulting path item
*/ */
subtract: function(path) { subtract: function(path) {
return computeBoolean(this, path, function(w) { return computeBoolean(this, path, 'subtract');
return w === 1;
}, true);
}, },
// Compound boolean operators combine the basic boolean operations such // Compound boolean operators combine the basic boolean operations such
@ -522,7 +533,7 @@ PathItem.inject(new function() {
* @return {Group} the resulting group item * @return {Group} the resulting group item
*/ */
exclude: function(path) { exclude: function(path) {
return new Group([this.subtract(path), path.subtract(this)]); return computeBoolean(this, path, 'exclude');
}, },
/** /**