From 61df327bc2d9dd019eb9d4d1f6ef9ec94c8025f9 Mon Sep 17 00:00:00 2001 From: iconexperience Date: Sat, 17 Dec 2016 18:43:48 +0100 Subject: [PATCH 1/6] Implement improved reorientation of paths, that can also be used by non-crossing boolean operations. --- src/path/PathItem.Boolean.js | 206 ++++++++++++++++++++++------------- 1 file changed, 132 insertions(+), 74 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 5ada25db..0dd7b03a 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -54,7 +54,7 @@ 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.resolveCrossings().reorient(res.getFillRule() === 'nonzero', true) : res; } @@ -107,9 +107,31 @@ 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(), + // When there are no crossings, the result can be known ahead of tracePaths(), // 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 we have two operands, check their bounds to find cases where // 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)] : null; } - } + }*/ function collect(paths) { 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. * @@ -615,7 +724,7 @@ PathItem.inject(new function() { // from the point (horizontal or vertical), based on the // curve's direction at that point. If the tangent is less // than 45°, cast the ray vertically, else horizontally. - dir = abs(curve.getTangentAtTime(t).normalize().y) + dir = abs(curve.getTangentAtTime(t).normalize().y) < Math.SQRT1_2 ? 1 : 0; if (parent instanceof CompoundPath) path = parent; @@ -1103,76 +1212,25 @@ PathItem.inject(new function() { * discarding sub-paths that do not contribute to the final result * @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; + reorient: function(nonZero, clockwise) { + var children = this._children; + if (children && children.length) { + children = this.removeChildren(); + reorientPaths(children, + nonZero ? + function (w) { + // true if winding is non-zero + return !w } - } - 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); + : function (w) { + // true if winding is even + return !(w % 2) + }, + clockwise + ); + this.setChildren(children); + } else if (clockwise != null) { + this.setClockwise(clockwise); } return this; }, From f77621f67d333c94e51d0c9689a51f0395b00c73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 22 Jan 2017 11:44:40 -0500 Subject: [PATCH 2/6] Various improvements to new reorient() code - Merge insideWindings object with operators lookup - Optimize handling of excluded paths - Improve contour handling in unite operations --- src/path/PathItem.Boolean.js | 155 +++++++++++++---------------------- 1 file changed, 55 insertions(+), 100 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 0dd7b03a..13e45a7b 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -38,7 +38,7 @@ 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 }, + unite: { 1: true, 2: true }, intersect: { 2: true }, subtract: { 1: true }, exclude: { 1: true } @@ -54,8 +54,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', true) - : res; + ? res.resolveCrossings().reorient( + res.getFillRule() === 'nonzero', true) + : res; } function createResult(ctor, paths, reduce, path1, path2, options) { @@ -92,6 +93,7 @@ PathItem.inject(new function() { // Add a simple boolean property to check for a given operation, // e.g. `if (operator.unite)` operator[operation] = true; + operator.name = operation; // Give both paths the same orientation except for subtraction // and exclusion, where we need them at opposite orientation. if (_path2 && (operator.subtract || operator.exclude) @@ -110,55 +112,16 @@ PathItem.inject(new function() { // When there are no crossings, the result can be known ahead of tracePaths(), // 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}) - ]; + paths = reorientPaths(paths2 ? paths1.concat(paths2) : paths1, + function(w) { + return !!operator[w]; + }); } - /* - 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++) { @@ -268,37 +231,34 @@ 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: + * @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: * - * 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 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, windingInsideFn, clockwise) { + 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 Math.abs(b.getArea()) - Math.abs(a.getArea()); + return abs(b.getArea()) - abs(a.getArea()); }), // Get reference to the first, largest path and insert it // already. @@ -315,39 +275,37 @@ PathItem.inject(new function() { 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. + // 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; + entry1.container = entry2.exclude ? entry2.container + : path2; + entry1.winding += (containerWinding = entry2.winding); 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)) { + 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. - path1.setClockwise(entry1.newContainer ? - !entry1.newContainer.isClockwise() : clockwise); + // 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); } } } - // 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; } @@ -688,7 +646,6 @@ PathItem.inject(new function() { winding: max(windingL, windingR), windingL: windingL, windingR: windingR, - onContour: !windingL ^ !windingR, onPathCount: onPathCount }; } @@ -768,8 +725,11 @@ PathItem.inject(new function() { || 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)); + // which are only valid if they are part of the contour of + // the result, 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) { @@ -1210,26 +1170,21 @@ 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, clockwise) { var children = this._children; if (children && children.length) { - children = this.removeChildren(); - reorientPaths(children, - nonZero ? - function (w) { - // true if winding is non-zero - return !w - } - : function (w) { - // true if winding is even - return !(w % 2) + this.setChildren(reorientPaths(this.removeChildren(), + function(w) { + // Handle both even-odd and non-zero rule. + return !!(nonZero ? w : w & 1); }, - clockwise - ); - this.setChildren(children); - } else if (clockwise != null) { + clockwise)); + } else if (clockwise !== undefined) { this.setClockwise(clockwise); } return this; From 8bbbe149eac1df88e56fd6d8ea77fd1e66170e42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 22 Jan 2017 12:08:54 -0500 Subject: [PATCH 3/6] More simplifications related to reorientPaths() --- src/path/PathItem.Boolean.js | 55 ++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 31 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 13e45a7b..edb3894d 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -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, 2: 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 } }; /* @@ -109,20 +111,6 @@ PathItem.inject(new function() { curves = [], paths; - // When there are no crossings, the result can be known ahead of tracePaths(), - // largely simplifying the processing required: - if (!crossings.length) { - if (paths2 && operator.exclude) { - for (var i = 0; i < paths2.length; i++) { - paths2[i].reverse(); - } - } - paths = reorientPaths(paths2 ? paths1.concat(paths2) : paths1, - function(w) { - return !!operator[w]; - }); - } - function collect(paths) { for (var i = 0, l = paths.length; i < l; i++) { var path = paths[i]; @@ -134,7 +122,7 @@ PathItem.inject(new function() { } } - if (!paths) { + if (crossings.length) { // Collect all segments and curves of both involved operands. collect(paths1); if (paths2) @@ -158,6 +146,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); @@ -265,7 +260,7 @@ PathItem.inject(new function() { first = sorted[0]; if (clockwise == null) clockwise = first.isClockwise(); - // determine winding for each path + // 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], @@ -273,14 +268,12 @@ PathItem.inject(new function() { 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. + // 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]; entry1.container = entry2.exclude ? entry2.container @@ -289,10 +282,10 @@ PathItem.inject(new function() { break; } } - // only keep paths if the insideness changes when crossing the + // 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 (isInside(entry1.winding) == isInside(containerWinding)) { + // 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. From a410aafaf23721aef9d618e7df0dd71755308bd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 24 Jan 2017 06:52:27 -0500 Subject: [PATCH 4/6] Remove unused property. --- src/path/PathItem.Boolean.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index edb3894d..00dc57f9 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -95,7 +95,6 @@ PathItem.inject(new function() { // Add a simple boolean property to check for a given operation, // e.g. `if (operator.unite)` operator[operation] = true; - operator.name = operation; // Give both paths the same orientation except for subtraction // and exclusion, where we need them at opposite orientation. if (_path2 && (operator.subtract || operator.exclude) From 9af936514ec4b9c03045d04e5ea980637bd2dcbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sat, 4 Feb 2017 20:14:35 +0100 Subject: [PATCH 5/6] Minor code cleanups. --- src/path/PathItem.Boolean.js | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 00dc57f9..94b36fbf 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -137,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. @@ -275,9 +275,10 @@ PathItem.inject(new function() { // 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; - entry1.winding += (containerWinding = entry2.winding); break; } } @@ -713,15 +714,17 @@ 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) - // which are only valid if they are part of the contour of - // the result, not contained inside another area. - && !(operator.unite && winding.winding === 2 - // No contour if both windings are non-zero. - && winding.windingL && winding.windingR))); + var res = !!(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 + // 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))); + console.log(seg && seg._winding); + return res; } function isStart(seg) { From 535607931c4cd23983410e6524ca6a4cd78391a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sat, 4 Feb 2017 20:15:23 +0100 Subject: [PATCH 6/6] Unit tests for boolean operations without crossings. Closes #1113 --- test/tests/Path_Boolean.js | 111 ++++++++++++++++++++++++++----------- 1 file changed, 78 insertions(+), 33 deletions(-) diff --git a/test/tests/Path_Boolean.js b/test/tests/Path_Boolean.js index 25bee58c..61b5c42f 100644 --- a/test/tests/Path_Boolean.js +++ b/test/tests/Path_Boolean.js @@ -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, @@ -766,39 +844,6 @@ test('#1091', function() { 'M91.24228,396.894h132.42802c-25.19365,0 -45.62,20.42407 -45.62,45.62c0,-25.19364 20.42635,-45.62 45.62,-45.62c80.62581,0 139.14228,-64.27063 139.14328,-152.82472l0,-0.00228c-0.001,-88.55097 -58.51636,-152.82351 -139.141,-152.82472l-0.00228,0c-25.1926,-0.00123 -45.61772,-20.42483 -45.61772,-45.62c0,-25.1955 20.42566,-45.62158 45.61871,-45.62228h1.61624c0.4166,0 0.83093,0.00454 1.24526,0.0159c0.00234,0.00002 0.00467,0.00004 0.00701,0.00007c0.00058,0.00002 0.00116,0.00003 0.00173,0.00005c129.91593,1.5144 227.51285,105.92259 227.51433,244.05012c0,0.00029 0,0.00057 0,0.00086c0,0.00012 0,0.00024 0,0.00036l0,0.00192c-0.00107,138.0792 -97.54084,242.46347 -227.38605,244.04875c-0.43111,0.0114 -0.8645,0.01825 -1.30017,0.01825h-1.69934c-12.59632,0 -24.00091,-5.10618 -32.25663,-13.36168c8.2555,8.25572 19.65987,13.36168 32.25663,13.36168l-178.04574,0c-0.00076,0 -0.00152,0 -0.00228,0c-0.00076,0 -0.00152,0 -0.00228,0h0c-25.19716,-0.00123 -45.61772,-20.42483 -45.61772,-45.62228v-396.88944c0,-25.19821 20.42179,-45.62228 45.62,-45.62228c14.89455,0 28.12203,7.13863 36.44812,18.18156c-8.3258,-11.04405 -21.55413,-18.18384 -36.4504,-18.18384h178.04802c-25.19365,0 -45.62,20.42407 -45.62,45.62228c0.00456,25.19593 20.42864,45.62 45.62228,45.62l-132.42802,0zM45.62,488.13628c-25.19821,0 -45.62,-20.42407 -45.62,-45.62228c0,25.19593 20.42179,45.62228 45.62,45.62228zM226.51682,0.01575c-0.93686,-0.01114 -1.88465,-0.01567 -2.82377,-0.01575c0.93909,0.0001 1.88688,0.0068 2.82377,0.01575zM362.81358,244.06928c0.00123,25.19716 20.42483,45.61772 45.62228,45.61772c-25.19745,0 -45.62105,-20.42056 -45.62228,-45.61772z'); }); -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');