From 61df327bc2d9dd019eb9d4d1f6ef9ec94c8025f9 Mon Sep 17 00:00:00 2001
From: iconexperience <jan.boesenberg@incors.com>
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?= <juerg@scratchdisk.com>
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?= <juerg@scratchdisk.com>
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?= <juerg@scratchdisk.com>
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?= <juerg@scratchdisk.com>
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?= <juerg@scratchdisk.com>
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');