Merge remote-tracking branch 'origin/improved-reorient' into develop

; Conflicts:
;	src/path/PathItem.Boolean.js
This commit is contained in:
Jürg Lehni 2017-02-04 20:20:21 +01:00
commit 69c124c36c
2 changed files with 199 additions and 148 deletions

View file

@ -38,10 +38,12 @@ PathItem.inject(new function() {
// contribution contributes to the final result or not. They are applied
// to for each segment after the paths are split at crossings.
operators = {
unite: { 1: true },
intersect: { 2: true },
subtract: { 1: true },
exclude: { 1: true }
unite: { '1': true, '2': true },
intersect: { '2': true },
subtract: { '1': true },
// exclude only needs -1 to support reorientPaths() when there are
// no crossings.
exclude: { '1': true, '-1': true }
};
/*
@ -54,8 +56,9 @@ PathItem.inject(new function() {
var res = path.clone(false).reduce({ simplify: true })
.transform(null, true, true);
return resolve
? res.resolveCrossings().reorient(res.getFillRule() === 'nonzero')
: res;
? res.resolveCrossings().reorient(
res.getFillRule() === 'nonzero', true)
: res;
}
function createResult(ctor, paths, reduce, path1, path2, options) {
@ -107,37 +110,6 @@ PathItem.inject(new function() {
curves = [],
paths;
// When there are no crossings, and the two paths are not contained
// within each other, the result can be known ahead of tracePaths(),
// largely simplifying the processing required:
if (!crossings.length) {
// If we have two operands, check their bounds to find cases where
// one path is fully contained in another. These cases cannot be
// simplified, we still need tracePaths() for them.
var ok = true;
if (paths2) {
for (var i1 = 0, l1 = paths1.length; i1 < l1 && ok; i1++) {
var bounds1 = paths1[i1].getBounds();
for (var i2 = 0, l2 = paths2.length; i2 < l2 && ok; i2++) {
var bounds2 = paths2[i2].getBounds();
// If either of the bounds fully contains the other,
// skip the simple approach and delegate to tracePaths()
ok = !bounds1._containsRectangle(bounds2) &&
!bounds2._containsRectangle(bounds1);
}
}
}
if (ok) {
// See #1113 for a description of how to deal with operators:
paths = operator.unite || operator.exclude ? [_path1, _path2]
: operator.subtract ? [_path1]
// No result, but let's return an empty path to keep
// chainability and transfer styles to the result.
: operator.intersect ? [new Path(Item.NO_INSERT)]
: null;
}
}
function collect(paths) {
for (var i = 0, l = paths.length; i < l; i++) {
var path = paths[i];
@ -149,7 +121,7 @@ PathItem.inject(new function() {
}
}
if (!paths) {
if (crossings.length) {
// Collect all segments and curves of both involved operands.
collect(paths1);
if (paths2)
@ -165,7 +137,7 @@ PathItem.inject(new function() {
for (var i = 0, l = segments.length; i < l; i++) {
var segment = segments[i],
inter = segment._intersection;
if (segment._winding == null) {
if (!segment._winding) {
propagateWinding(segment, _path1, _path2, curves, operator);
}
// See if all encountered segments in a path are overlaps.
@ -173,6 +145,13 @@ PathItem.inject(new function() {
segment._path._overlapsOnly = false;
}
paths = tracePaths(segments, operator);
} else {
// When there are no crossings, the result can be determined through
// a much faster call to reorientPaths():
paths = reorientPaths(paths2 ? paths1.concat(paths2) : paths1,
function(w) {
return !!operator[w];
});
}
return createResult(CompoundPath, paths, true, path1, path2, options);
@ -249,6 +228,87 @@ PathItem.inject(new function() {
curves[i].clearHandles();
}
/**
* Reorients the specified paths.
*
* @param {Item[]} paths the paths of which the orientation needs to be
* reoriented
* @param {Function} isInside determines if the inside of a path is filled.
* For non-zero fill rule this function would be implemented as follows:
*
* function isInside(w) {
* return w != 0;
* }
* @param {Boolean} [clockwise] if provided, the orientation of the root
* paths will be set to the orientation specified by `clockwise`,
* otherwise the orientation of the largest root child is used.
* @returns {Item[]} the reoriented paths
*/
function reorientPaths(paths, isInside, 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] = {
container: null,
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 abs(b.getArea()) - abs(a.getArea());
}),
// Get reference to the first, largest path and insert it
// already.
first = sorted[0];
if (clockwise == null)
clockwise = first.isClockwise();
// Now determine the winding for each path, from large to small.
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];
// As we run through the paths from largest to smallest, for
// any current path, all potentially containing paths have
// already been processed and their orientation fixed.
// To achieve correct orientation of contained paths based
// on winding, we have to find one containing path with
// different "insideness" and set opposite orientation.
if (path2.contains(point)) {
var entry2 = lookup[path2._id];
containerWinding = entry2.winding;
entry1.winding += containerWinding;
entry1.container = entry2.exclude ? entry2.container
: path2;
break;
}
}
// Only keep paths if the "insideness" changes when crossing the
// path, e.g. the inside of the path is filled and the outside
// is not, or vice versa.
if (isInside(entry1.winding) === isInside(containerWinding)) {
entry1.exclude = true;
// No need to delete excluded entries. Setting to null is
// enough, as #setChildren() can handle arrays with gaps.
paths[entry1.index] = null;
} else {
// If the containing path is not excluded, we're done
// searching for the orientation defining path.
var container = entry1.container;
path1.setClockwise(container ? !container.isClockwise()
: clockwise);
}
}
}
return paths;
}
/**
* Divides the path-items at the given locations.
*
@ -627,7 +687,6 @@ PathItem.inject(new function() {
windingL: windingL,
windingR: windingR,
quality: quality,
onContour: !windingL ^ !windingR,
onPath: onPath
};
}
@ -715,11 +774,14 @@ PathItem.inject(new function() {
function isValid(seg) {
var winding;
return !!(seg && !seg._visited && (!operator
|| operator[(winding = seg._winding || {}).winding]
// Unite operations need special handling of segments with a
// winding contribution of two (part of both involved areas)
// but which are also part of the contour of the result.
|| operator.unite && winding.onContour));
|| operator[(winding = seg._winding).winding]
// Unite operations need special handling of segments
// with a winding contribution of two (part of both
// areas), which are only valid if they are part of the
// result's contour, not contained inside another area.
&& !(operator.unite && winding.winding === 2
// No contour if both windings are non-zero.
&& winding.windingL && winding.windingR)));
}
function isStart(seg) {
@ -1163,78 +1225,22 @@ PathItem.inject(new function() {
* @param {Boolean} [nonZero=false] controls if the non-zero fill-rule
* is to be applied, by counting the winding of each nested path and
* discarding sub-paths that do not contribute to the final result
* @param {Boolean} [clockwise] if provided, the orientation of the root
* paths will be set to the orientation specified by `clockwise`,
* otherwise the orientation of the largest root child is used.
* @return {PahtItem} a reference to the item itself, reoriented
*/
reorient: function(nonZero) {
var children = this._children,
length = children && children.length;
if (length > 1) {
// Build a lookup table with information for each path's
// original index and winding contribution.
var lookup = Base.each(children, function(path, i) {
this[path._id] = {
winding: path.isClockwise() ? 1 : -1,
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;
}
}
isContained = true;
// If the containing path is not excluded, we're
// done searching for the orientation defining path.
container = !entry2.exclude && path2;
}
}
if (!exclude) {
// Set to the opposite orientation of containing path,
// 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);
reorient: function(nonZero, clockwise) {
var children = this._children;
if (children && children.length) {
this.setChildren(reorientPaths(this.removeChildren(),
function(w) {
// Handle both even-odd and non-zero rule.
return !!(nonZero ? w : w & 1);
},
clockwise));
} else if (clockwise !== undefined) {
this.setClockwise(clockwise);
}
return this;
},

View file

@ -12,6 +12,84 @@
QUnit.module('Path Boolean Operations');
function testOperations(path1, path2, results) {
compareBoolean(function() { return path1.unite(path2); }, results[0]);
compareBoolean(function() { return path2.unite(path1); }, results[0]);
compareBoolean(function() { return path1.subtract(path2); }, results[1]);
compareBoolean(function() { return path2.subtract(path1); }, results[2]);
compareBoolean(function() { return path1.intersect(path2); }, results[3]);
compareBoolean(function() { return path2.intersect(path1); }, results[3]);
compareBoolean(function() { return path1.exclude(path2); }, results[4]);
compareBoolean(function() { return path2.exclude(path1); }, results[4]);
}
test('Boolean operations without crossings', function() {
var path1 = new Path.Rectangle({
point: [0, 0],
size: [200, 200]
});
var path2 = new Path.Rectangle({
point: [50, 50],
size: [100, 100]
});
var path3 = new Path.Rectangle({
point: [250, 50],
size: [100, 100]
});
testOperations(path1, path2, [
'M0,200v-200h200v200z', // path1.unite(path2);
'M0,200v-200h200v200zM150,150v-100h-100v100z', // path1.subtract(path2);
'', // path2.subtract(path1);
'M50,150v-100h100v100z', // path1.intersect(path2);
'M0,200v-200h200v200zM150,150v-100h-100v100z' // path1.exclude(path2);
]);
testOperations(path1, path3, [
'M0,200v-200h200v200zM250,150v-100h100v100z', // path1.unite(path3);
'M0,200v-200h200v200z', // path1.subtract(path3);
'M350,150v-100h-100v100z', // path3.subtract(path1);
'', // path1.intersect(path3);
'M0,200v-200h200v200zM250,150v-100h100v100z' // path1.exclude(path3);
]);
});
test('frame.intersect(rect)', function() {
var frame = new CompoundPath();
frame.addChild(new Path.Rectangle(new Point(140, 10), [100, 300]));
frame.addChild(new Path.Rectangle(new Point(150, 80), [50, 80]));
var rect = new Path.Rectangle(new Point(50, 50), [100, 150]);
compareBoolean(function() { return frame.intersect(rect); },
'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,
@ -794,39 +872,6 @@ test('#1239', function() {
'M923.0175,265.805c21.3525,23.505 33.2725,54.305 33.2725,86.065c0,0.01833 0,0.03667 -0.00001,0.055h0.00001c-0.00005,0.10258 -0.00022,0.20515 -0.00052,0.3077c-0.06338,22.18242 -5.9393,43.88534 -16.78017,62.94682c-4.63138,8.14369 -10.16899,15.8051 -16.54682,22.81548l-11.8125,-10.75c8.97181,-9.86302 16.01692,-21.11585 20.93099,-33.22212c5.34364,-13.16533 8.16725,-27.34044 8.20856,-41.83592c0.0003,-0.10564 0.00044,-0.21129 0.00044,-0.31697c0,-27.9075 -10.32,-54.655 -29.0875,-75.315z');
});
test('frame.intersect(rect);', function() {
var frame = new CompoundPath();
frame.addChild(new Path.Rectangle(new Point(140, 10), [100, 300]));
frame.addChild(new Path.Rectangle(new Point(150, 80), [50, 80]));
var rect = new Path.Rectangle(new Point(50, 50), [100, 150]);
compareBoolean(function() { return frame.intersect(rect); },
'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('Selected edge-cases from @hari\'s boolean-test suite', function() {
var g = PathItem.create('M316.6,266.4Q332.6,266.4,343.8,272.8Q355,279.2,362,289.8Q369,300.4,372.2,313.6Q375.4,326.8,375.4,340.4Q375.4,354.8,372,369.2Q368.6,383.6,361.4,395Q354.2,406.4,342.4,413.4Q330.6,420.4,313.8,420.4Q297,420.4,285.8,413.4Q274.6,406.4,267.8,395Q261,383.6,258.2,369.6Q255.4,355.6,255.4,341.6Q255.4,326.8,258.8,313.2Q262.2,299.6,269.6,289.2Q277,278.8,288.6,272.6Q300.2,266.4,316.6,266.4Z M315,236.4Q288.2,236.4,269.8,246.6Q251.4,256.8,240.2,272.6Q229,288.4,224.2,307.8Q219.4,327.2,219.4,345.6Q219.4,366.8,225.2,385.8Q231,404.8,242.6,419Q254.2,433.2,271.4,441.6Q288.6,450,311.8,450Q331.8,450,349.6,441Q367.4,432,376.2,412.8L377,412.8L377,426.4Q377,443.6,373.6,458Q370.2,472.4,362.6,482.6Q355,492.8,343.4,498.6Q331.8,504.4,315,504.4Q306.6,504.4,297.4,502.6Q288.2,500.8,280.4,496.8Q272.6,492.8,267.2,486.4Q261.8,480,261.4,470.8L227.4,470.8Q228.2,487.6,236.2,499.2Q244.2,510.8,256.4,518Q268.6,525.2,283.6,528.4Q298.6,531.6,313,531.6Q362.6,531.6,385.8,506.4Q409,481.2,409,430.4L409,241.2L377,241.2L377,270.8L376.6,270.8Q367.4,253.6,351,245Q334.6,236.4,315,236.4Z');
var u = PathItem.create('M253,316.74Q242.25,316.74,232.77,318.39Q218.77,320.83,208.21,328.52Q197.65,336.21,191.32,349.4Q185,362.6,183.59,382.95Q182.01,405.69,189.83,423.08Q197.64,440.46,216.05,452.56L215.99,453.36L183.27,451.09L181.06,483.01L387.37,497.31L389.72,463.39L273.2,455.32Q259.23,454.35,247.72,449.74Q236.21,445.14,227.96,436.95Q219.7,428.76,215.7,417.05Q211.7,405.35,212.78,389.78Q214.14,370.23,226.09,359.83Q236.68,350.61,252.94,350.61Q255.02,350.61,257.19,350.76L396.85,360.44L399.2,326.52L263.53,317.12Q258.12,316.74,253,316.74Z');