From 0e26b530530b8e1a010e102c01bab9d89d2c9d0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Fri, 28 Aug 2015 16:17:54 +0200 Subject: [PATCH 001/280] Improve CurveLocation.sort() to handle more edge cases. Relates to #648 --- src/path/CurveLocation.js | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index 504f5a23..63e7fcce 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -55,6 +55,8 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ this._point = point || curve.getPointAt(parameter, true); this._curve2 = _curve2; this._parameter2 = _parameter2; + if (_parameter2 == 0.19410221115440937) + debugger; this._point2 = _point2; this._distance = _distance; // Also store references to segment1 and segment2, in case path @@ -217,6 +219,7 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ this._parameter2, this._point2 || this._point); intersection._overlap = this._overlap; intersection._intersection = this; + intersection._other = true; } return intersection; }, @@ -320,19 +323,32 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ var curve1 = l1._curve, curve2 = l2._curve, path1 = curve1._path, - path2 = curve2._path; + path2 = curve2._path, + res; // Sort by path-id, curve, parameter, curve2, parameter2 so we // can easily remove duplicates with calls to equals() after. - return path1 === path2 - ? curve1 === curve2 - ? Math.abs(l1._parameter - l2._parameter) < tolerance - ? l1._curve2 === l2._curve2 + if (path1 === path2) { + if (curve1 === curve2) { + var diff = l1._parameter - l2._parameter; + if (Math.abs(diff) < tolerance) { + var curve21 = l1._curve2, + curve22 = l2._curve2; + res = curve21 === curve22 // equal or both null ? l1._parameter2 - l2._parameter2 - : l1._curve2.getIndex() - l2._curve2.getIndex() - : l1._parameter - l2._parameter - : curve1.getIndex() - curve2.getIndex() + : curve21 && curve22 + ? curve21.getIndex() - curve22.getIndex() + : curve21 ? 1 : -1; + } else { + res = diff; + } + } else { + res = curve1.getIndex() - curve2.getIndex(); + } + } else { // Sort by path id to group all locs on the same path. - : path1._id - path2._id; + res = path1._id - path2._id; + } + return res; }); } } From e07d8f55ea35a77d22e6c01ecb0cbcbaa645eeb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Fri, 28 Aug 2015 16:18:14 +0200 Subject: [PATCH 002/280] Add debug logging for intersections again. --- src/path/PathItem.Boolean.js | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index a1d722a1..d97e08eb 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -232,6 +232,24 @@ PathItem.inject(new function() { * @param {CurveLocation[]} intersections Array of CurveLocation objects */ function splitPath(intersections) { + console.log('splitting', intersections.length); + intersections.forEach(function(inter) { + var log = ['CurveLocation', inter._id, 'p', inter.getPath()._id, + 'i', inter.getIndex(), 't', inter._parameter, + 'i2', inter._curve2 ? inter._curve2.getIndex() : null, + 't2', inter._parameter2, 'o', !!inter._overlap]; + if (inter._other) { + inter = inter.getIntersection(); + log.push('Other', inter._id, 'p', inter.getPath()._id, + 'i', inter.getIndex(), 't', inter._parameter, + 'i2', inter._curve2 ? inter._curve2.getIndex() : null, + 't2', inter._parameter2, 'o', !!inter._overlap); + } + console.log(log.map(function(v) { + return v == null ? '-' : v + }).join(' ')); + }); + // TODO: Make public in API, since useful! var tMin = /*#=*/Numerical.TOLERANCE, tMax = 1 - tMin, @@ -417,6 +435,10 @@ PathItem.inject(new function() { return Math.max(abs(windLeft), abs(windRight)); } + var segmentOffset = {}; + var pathIndices = {}; + var pathIndex = 0; + /** * Private method to trace closed contours from a set of segments according * to a set of constraints-winding contribution and a custom operator. @@ -431,9 +453,8 @@ PathItem.inject(new function() { */ function tracePaths(segments, monoCurves, operation) { var segmentCount = 0; - var segmentOffset = {}; var reportSegments = false && !window.silent; - var reportWindings = false && !window.silent; + var reportWindings = true && !window.silent; var textAngle = 0; var fontSize = 4 / paper.project.activeLayer.scaling.x; @@ -476,13 +497,19 @@ PathItem.inject(new function() { , color); } + + for (var i = 0; i < (reportWindings ? segments.length : 0); i++) { var seg = segments[i]; + path = seg._path, + id = path._id, point = seg.point, inter = seg._intersection; - labelSegment(seg, i + if (!(id in pathIndices)) + pathIndices[id] = ++pathIndex; + + labelSegment(seg, '#' + pathIndex + '.' + (i + 1) + ' i: ' + !!inter - + ' p: ' + seg._path._id + ' x: ' + seg._point.x + ' y: ' + seg._point.y + ' o: ' + (inter && inter._overlap || 0) From 0cbce044aaf47e9cdf975f4788209f80315b5c7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Fri, 28 Aug 2015 16:18:28 +0200 Subject: [PATCH 003/280] Define Curve#_serialize() --- src/path/Curve.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/path/Curve.js b/src/path/Curve.js index eb0bdd23..ab581585 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -97,6 +97,15 @@ var Curve = Base.extend(/** @lends Curve# */{ } }, + _serialize: function(options) { + // If it is straight, only serialize point, otherwise handles too. + return Base.serialize(this.isStraight() + ? [this.getPoint1(), this.getPoint2()] + : [this.getPoint1(), this.getHandle1(), this.getHandle2(), + this.getPoint2()], + options, true); + }, + _changed: function() { // Clear cached values. this._length = this._bounds = undefined; From 8f13fa54fcbcdf7a3f5109149c46c9cee6266a5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 30 Aug 2015 13:52:13 +0200 Subject: [PATCH 004/280] Remove debugger statement. --- src/path/CurveLocation.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index 63e7fcce..2bec1df0 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -55,8 +55,6 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ this._point = point || curve.getPointAt(parameter, true); this._curve2 = _curve2; this._parameter2 = _parameter2; - if (_parameter2 == 0.19410221115440937) - debugger; this._point2 = _point2; this._distance = _distance; // Also store references to segment1 and segment2, in case path From 27aae8b261e2404e41537826c5d777d37dcf0805 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 30 Aug 2015 14:14:52 +0200 Subject: [PATCH 005/280] Add support for values array and toString object format to Curve constructor. --- src/path/Curve.js | 74 +++++++++++++++++++++++++++++---------------- src/path/Segment.js | 2 +- 2 files changed, 49 insertions(+), 27 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index ab581585..9bd2fb13 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -58,43 +58,65 @@ var Curve = Base.extend(/** @lends Curve# */{ * @param {Number} y2 */ initialize: function Curve(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7) { - var count = arguments.length; + var count = arguments.length, + values, + seg1, seg2, + point1, point2, + handle1, handle2; + // The following code has to either set seg1 & seg2, + // or point1, point2, handle1 & handle2. At the end, the internal + // segments are created accordingly. if (count === 3) { // Undocumented internal constructor, used by Path#getCurves() // new Segment(path, segment1, segment2); this._path = arg0; - this._segment1 = arg1; - this._segment2 = arg2; + seg1 = arg1; + seg2 = arg2; } else if (count === 0) { - this._segment1 = new Segment(); - this._segment2 = new Segment(); + seg1 = new Segment(); + seg2 = new Segment(); } else if (count === 1) { // new Segment(segment); // Note: This copies from existing segments through bean getters - this._segment1 = new Segment(arg0.segment1); - this._segment2 = new Segment(arg0.segment2); + if ('segment1' in arg0) { + seg1 = new Segment(arg0.segment1); + seg2 = new Segment(arg0.segment2); + } else if ('point1' in arg0) { + // As printed by #toString() + point1 = arg0.point1; + handle1 = arg0.handle1; + handle2 = arg0.handle2; + point2 = arg0.point2; + } else if (Array.isArray(arg0)) { + // Convert getValues() array back to points and handles so we + // can create segments for those. + point1 = [arg0[0], arg0[1]]; + point2 = [arg0[6], arg0[7]]; + handle1 = [arg0[2] - arg0[0], arg0[3] - arg0[1]]; + handle2 = [arg0[4] - arg0[6], arg0[5] - arg0[7]]; + } } else if (count === 2) { // new Segment(segment1, segment2); - this._segment1 = new Segment(arg0); - this._segment2 = new Segment(arg1); - } else { - var point1, handle1, handle2, point2; - if (count === 4) { - point1 = arg0; - handle1 = arg1; - handle2 = arg2; - point2 = arg3; - } else if (count === 8) { - // Convert getValue() array back to points and handles so we - // can create segments for those. - point1 = [arg0, arg1]; - point2 = [arg6, arg7]; - handle1 = [arg2 - arg0, arg3 - arg1]; - handle2 = [arg4 - arg6, arg5 - arg7]; - } - this._segment1 = new Segment(point1, null, handle1); - this._segment2 = new Segment(point2, handle2, null); + seg1 = new Segment(arg0); + seg2 = new Segment(arg1); + } else if (count === 4) { + point1 = arg0; + handle1 = arg1; + handle2 = arg2; + point2 = arg3; + } else if (count === 8) { + // Convert getValues() array from arguments list back to points and + // handles so we can create segments for those. + // NOTE: This could be merged with the above code after the array + // check through the `arguments` object, but it would break JS + // optimizations. + point1 = [arg0, arg1]; + point2 = [arg6, arg7]; + handle1 = [arg2 - arg0, arg3 - arg1]; + handle2 = [arg4 - arg6, arg5 - arg7]; } + this._segment1 = seg1 || new Segment(point1, null, handle1); + this._segment2 = seg2 || new Segment(point2, handle2, null); }, _serialize: function(options) { diff --git a/src/path/Segment.js b/src/path/Segment.js index 3690b02c..27af6723 100644 --- a/src/path/Segment.js +++ b/src/path/Segment.js @@ -119,7 +119,7 @@ var Segment = Base.extend(/** @lends Segment# */{ // Nothing } else if (count === 1) { // Note: This copies from existing segments through accessors. - if (arg0.point) { + if ('point' in arg0) { point = arg0.point; handleIn = arg0.handleIn; handleOut = arg0.handleOut; From bd61390f9dd0e4363a7063785688408cd65234e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 30 Aug 2015 14:37:21 +0200 Subject: [PATCH 006/280] improve break-off condition in curve interesection code. To prevent arbitrary incorrect solutions occuring when tDiff is very close to zero. Closes #762. --- src/path/Curve.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 9bd2fb13..b6903d19 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1335,7 +1335,7 @@ new function() { // Scope for methods that require private functions curve1, t1, Curve.getPoint(v1, t1), curve2, t2, Curve.getPoint(v2, t2)); } - } else if (tDiff > 0) { // Iterate + } else if (tDiff > /*#=*/Numerical.EPSILON) { // Iterate addCurveIntersections(v2, v1, curve2, curve1, locations, include, uMin, uMax, tMinNew, tMaxNew, tDiff, !reverse, ++recursion); } From b4755ea699a645bd1a89f8d8f212733c71d017cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 30 Aug 2015 14:38:18 +0200 Subject: [PATCH 007/280] Deactivate debug logging code. --- src/path/PathItem.Boolean.js | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index d97e08eb..efa78650 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -232,23 +232,25 @@ PathItem.inject(new function() { * @param {CurveLocation[]} intersections Array of CurveLocation objects */ function splitPath(intersections) { - console.log('splitting', intersections.length); - intersections.forEach(function(inter) { - var log = ['CurveLocation', inter._id, 'p', inter.getPath()._id, - 'i', inter.getIndex(), 't', inter._parameter, - 'i2', inter._curve2 ? inter._curve2.getIndex() : null, - 't2', inter._parameter2, 'o', !!inter._overlap]; - if (inter._other) { - inter = inter.getIntersection(); - log.push('Other', inter._id, 'p', inter.getPath()._id, + if (false) { + console.log('Intersections', intersections.length); + intersections.forEach(function(inter) { + var log = ['CurveLocation', inter._id, 'p', inter.getPath()._id, 'i', inter.getIndex(), 't', inter._parameter, - 'i2', inter._curve2 ? inter._curve2.getIndex() : null, - 't2', inter._parameter2, 'o', !!inter._overlap); - } - console.log(log.map(function(v) { - return v == null ? '-' : v - }).join(' ')); - }); + 'i2', inter._curve2 ? inter._curve2.getIndex() : null, + 't2', inter._parameter2, 'o', !!inter._overlap]; + if (inter._other) { + inter = inter.getIntersection(); + log.push('Other', inter._id, 'p', inter.getPath()._id, + 'i', inter.getIndex(), 't', inter._parameter, + 'i2', inter._curve2 ? inter._curve2.getIndex() : null, + 't2', inter._parameter2, 'o', !!inter._overlap); + } + console.log(log.map(function(v) { + return v == null ? '-' : v + }).join(' ')); + }); + } // TODO: Make public in API, since useful! var tMin = /*#=*/Numerical.TOLERANCE, @@ -454,7 +456,7 @@ PathItem.inject(new function() { function tracePaths(segments, monoCurves, operation) { var segmentCount = 0; var reportSegments = false && !window.silent; - var reportWindings = true && !window.silent; + var reportWindings = false && !window.silent; var textAngle = 0; var fontSize = 4 / paper.project.activeLayer.scaling.x; From 31771aa01d634fd4aba5131d2c56700482b7b2cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 30 Aug 2015 14:47:46 +0200 Subject: [PATCH 008/280] Insert results of boolean operations above whichever of the two paths appear further up in the stack. --- src/item/Item.js | 10 ++++++++++ src/path/PathItem.Boolean.js | 6 +++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/item/Item.js b/src/item/Item.js index 8e3718d5..122f4f2f 100644 --- a/src/item/Item.js +++ b/src/item/Item.js @@ -2664,6 +2664,16 @@ var Item = Base.extend(Emitter, /** @lends Item# */{ return item ? item.isDescendant(this) : false; }, + /** + * Checks if the item is an a sibling of the specified item. + * + * @param {Item} item the item to check against + * @return {Boolean} {@true if the item is aa sibling of the specified item} + */ + isSibling: function(item) { + return this._parent === item._parent; + }, + /** * Checks whether the item is grouped with the specified item. * diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index efa78650..ad92aed1 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -216,7 +216,11 @@ PathItem.inject(new function() { true); // See if the CompoundPath can be reduced to just a simple Path. result = result.reduce(); - result.insertAbove(path1); + // Insert the resulting path above whichever of the two paths appear + // further up in the stack. + result.insertAbove(path2 && path1.isSibling(path2) + && path1.getIndex() < path2.getIndex() + ? path2 : path1); // Copy over the left-hand item's style and we're done. // TODO: Consider using Item#_clone() for this, but find a way to not // clone children / name (content). From 4379e0b0f0a4c0b59d0118821437b315288f3711 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 30 Aug 2015 19:56:17 +0200 Subject: [PATCH 009/280] Improve boolean debug code. --- src/path/PathItem.Boolean.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index ad92aed1..125c42cb 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -79,7 +79,6 @@ PathItem.inject(new function() { // intersection, _path2 will be null and getIntersections() handles it. splitPath(Curve.filterIntersections( _path1._getIntersections(_path2, null, []), true)); - /* console.time('inter'); var locations = _path1._getIntersections(_path2, null, []); @@ -459,10 +458,11 @@ PathItem.inject(new function() { */ function tracePaths(segments, monoCurves, operation) { var segmentCount = 0; + var pathCount = 0; var reportSegments = false && !window.silent; var reportWindings = false && !window.silent; var textAngle = 0; - var fontSize = 4 / paper.project.activeLayer.scaling.x; + var fontSize = 1 / paper.project.activeLayer.scaling.x; function labelSegment(seg, text, color) { var point = seg.point; @@ -470,7 +470,8 @@ PathItem.inject(new function() { var offset = segmentOffset[key] || 0; segmentOffset[key] = offset + 1; var text = new PointText({ - point: point.add(new Point(fontSize, fontSize / 2).rotate(textAngle).add(0, offset * fontSize * 1.2)), + point: point.add(new Point(fontSize, fontSize / 2) + .rotate(textAngle).add(0, offset * fontSize * 1.2)), content: text, justification: 'left', fillColor: color, @@ -490,7 +491,7 @@ PathItem.inject(new function() { strokeScaling: false }); var inter = seg._intersection; - labelSegment(seg, '#' + (paths.length + 1) + '.' + labelSegment(seg, '#' + (pathCount + 1) + '.' + (path ? path._segments.length + 1 : 1) + ' ' + (segmentCount++) + '/' + index + ': ' + text + ' v: ' + !!seg._visited @@ -658,14 +659,14 @@ PathItem.inject(new function() { path.setClosed(true); if (reportSegments) { console.log('Boolean operation completed', - '#' + (paths.length + 1) + '.' + + '#' + (pathCount + 1) + '.' + (path ? path._segments.length + 1 : 1)); } } else { // path.lastSegment._handleOut.set(0, 0); console.error('Boolean operation results in open path, segs =', path._segments.length, 'length = ', path.getLength(), - '#' + (paths.length + 1) + '.' + + '#' + (pathCount + 1) + '.' + (path ? path._segments.length + 1 : 1)); path = null; } @@ -676,6 +677,9 @@ PathItem.inject(new function() { // - Closed paths with curves can consist of only one segment if (path && path._segments.length > path.isLinear() ? 2 : 0) paths.push(path); + if (reportSegments) { + pathCount++; + } } return paths; } From 0651eee0c273d777e5483da292c4a9ecffe813d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 30 Aug 2015 19:58:32 +0200 Subject: [PATCH 010/280] No more need for special handling of 'subtract' overlaps. This is now taken care of in the code that handles overlaps itself, and the additional code was causing additional issues. --- src/path/PathItem.Boolean.js | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 125c42cb..ba9fc09e 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -175,6 +175,9 @@ PathItem.inject(new function() { var seg = chain[j].segment, inter = seg._intersection, wind = winding; + // We need to handle the edge cases of overlapping curves + // differently based on the type of operation, and adjust the + // winding number accordingly: if (inter && inter._overlap) { switch (operation) { case 'unite': @@ -185,25 +188,6 @@ PathItem.inject(new function() { if (wind === 2) wind = 1; break; - case 'subtract': - // When subtracting, we need to reverse the winding - // number along overlaps. - // Calculate the new winding number based on current - // number and role in the operation. - var path = getMainPath(seg), - newWind = wind === 0 && path === _path1 ? 1 - : wind === 1 && path === _path2 ? 2 - : null; - if (newWind != null) { - // Check against the winding of the intersecting - // path, to exclude islands in compound paths, where - // the reversal of winding numbers below in overlaps - // is not required: - var pt = inter._segment._path.getInteriorPoint(); - if (getWinding(pt, monoCurves) === 1) - wind = newWind; - } - break; } } seg._winding = wind; From 20222be5c6346fa337b5afdfcef8c984673da642 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 30 Aug 2015 19:59:13 +0200 Subject: [PATCH 011/280] Minor clean-up in fat-line code. --- src/path/Curve.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index b6903d19..ebf1ffe8 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1251,7 +1251,7 @@ new function() { // Scope for methods that require private functions // below when determining which curve converges the least. He also // recommended a threshold of 0.5 instead of the initial 0.8 // See: https://github.com/paperjs/paper.js/issues/565 - if (recursion > 32) + if (++recursion >= 32) return; // Let P be the first curve and Q be the second var q0x = v2[0], q0y = v2[1], q3x = v2[6], q3y = v2[7], @@ -1272,8 +1272,8 @@ new function() { // Scope for methods that require private functions dp2 = getSignedDistance(q0x, q0y, q3x, q3y, v1[4], v1[5]), dp3 = getSignedDistance(q0x, q0y, q3x, q3y, v1[6], v1[7]), tMinNew, tMaxNew, tDiff; - if (q0x === q3x && uMax - uMin < tolerance && recursion > 3) { - // The fatline of Q has converged to a point, the clipping is not + if (q0x === q3x && uMax - uMin < tolerance && recursion >= 3) { + // The fat-line of Q has converged to a point, the clipping is not // reliable. Return the value we have even though we will miss the // precision. tMaxNew = tMinNew = (tMax + tMin) / 2; @@ -1292,7 +1292,7 @@ new function() { // Scope for methods that require private functions // No intersections if one of the tvalues are null or 'undefined' if (tMinClip == null || tMaxClip == null) return; - // Clip P with the fatline for Q + // Clip P with the fat-line for Q v1 = Curve.getPart(v1, tMinClip, tMaxClip); tDiff = tMaxClip - tMinClip; // tMin and tMax are within the range (0, 1). We need to project it @@ -1308,7 +1308,7 @@ new function() { // Scope for methods that require private functions t = tMinNew + (tMaxNew - tMinNew) / 2; addCurveIntersections( v2, parts[0], curve2, curve1, locations, include, - uMin, uMax, tMinNew, t, tDiff, !reverse, ++recursion); + uMin, uMax, tMinNew, t, tDiff, !reverse, recursion); addCurveIntersections( v2, parts[1], curve2, curve1, locations, include, uMin, uMax, t, tMaxNew, tDiff, !reverse, recursion); @@ -1317,7 +1317,7 @@ new function() { // Scope for methods that require private functions t = uMin + (uMax - uMin) / 2; addCurveIntersections( parts[0], v1, curve2, curve1, locations, include, - uMin, t, tMinNew, tMaxNew, tDiff, !reverse, ++recursion); + uMin, t, tMinNew, tMaxNew, tDiff, !reverse, recursion); addCurveIntersections( parts[1], v1, curve2, curve1, locations, include, t, uMax, tMinNew, tMaxNew, tDiff, !reverse, recursion); @@ -1337,7 +1337,7 @@ new function() { // Scope for methods that require private functions } } else if (tDiff > /*#=*/Numerical.EPSILON) { // Iterate addCurveIntersections(v2, v1, curve2, curve1, locations, include, - uMin, uMax, tMinNew, tMaxNew, tDiff, !reverse, ++recursion); + uMin, uMax, tMinNew, tMaxNew, tDiff, !reverse, recursion); } } From 215bbe2e8e960f57c74e8659d6a3829cdbc6d5c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 30 Aug 2015 22:57:33 +0200 Subject: [PATCH 012/280] Fix issue in Numerical.solveCubic() / solveQuadratic() We need to include EPSILON tolerance in the comparison with bounds values. --- src/util/Numerical.js | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/util/Numerical.js b/src/util/Numerical.js index a970267a..ed5cc1eb 100644 --- a/src/util/Numerical.js +++ b/src/util/Numerical.js @@ -64,6 +64,10 @@ var Numerical = new function() { EPSILON = 1e-12, MACHINE_EPSILON = 1.12e-16; + function clip(value, min, max) { + return value < min ? min : value > max ? max : value; + } + return /** @lends Numerical */{ TOLERANCE: TOLERANCE, // Precision when comparing against 0 @@ -174,6 +178,8 @@ var Numerical = new function() { */ solveQuadratic: function(a, b, c, roots, min, max) { var count = 0, + eMin = min - EPSILON, + eMax = max + EPSILON, x1, x2 = Infinity, B = b, D; @@ -222,11 +228,13 @@ var Numerical = new function() { // count = D > MACHINE_EPSILON ? 2 : 1; } } - if (isFinite(x1) && (min == null || x1 >= min && x1 <= max)) - roots[count++] = x1; + // We need to include EPSILON in the comparisons with min / max, + // as some solutions are ever so lightly out of bounds. + if (isFinite(x1) && (min == null || x1 > eMin && x1 < eMax)) + roots[count++] = min == null ? x1 : clip(x1, min, max); if (x2 !== x1 - && isFinite(x2) && (min == null || x2 >= min && x2 <= max)) - roots[count++] = x2; + && isFinite(x2) && (min == null || x2 > eMin && x2 < eMax)) + roots[count++] = min == null ? x2 : clip(x2, min, max); return count; }, @@ -321,8 +329,8 @@ var Numerical = new function() { // The cubic has been deflated to a quadratic. var count = Numerical.solveQuadratic(a, b1, c2, roots, min, max); if (isFinite(x) && (count === 0 || x !== roots[count - 1]) - && (min == null || x >= min && x <= max)) - roots[count++] = x; + && (min == null || x > min - EPSILON && x < max + EPSILON)) + roots[count++] = min == null ? x : clip(x, min, max); return count; } }; From b9a07ca538b83dfb07d4fff0f481b94aa2369ba6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Mon, 31 Aug 2015 22:01:18 +0200 Subject: [PATCH 013/280] Address code comments by @iconexperience in #762. --- src/path/Curve.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index ebf1ffe8..efe205e6 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1271,7 +1271,8 @@ new function() { // Scope for methods that require private functions dp1 = getSignedDistance(q0x, q0y, q3x, q3y, v1[2], v1[3]), dp2 = getSignedDistance(q0x, q0y, q3x, q3y, v1[4], v1[5]), dp3 = getSignedDistance(q0x, q0y, q3x, q3y, v1[6], v1[7]), - tMinNew, tMaxNew, tDiff; + tMinNew, tMaxNew, + tDiff; if (q0x === q3x && uMax - uMin < tolerance && recursion >= 3) { // The fat-line of Q has converged to a point, the clipping is not // reliable. Return the value we have even though we will miss the @@ -1284,13 +1285,11 @@ new function() { // Scope for methods that require private functions top = hull[0], bottom = hull[1], tMinClip, tMaxClip; - // Clip the convex-hull with dMin and dMax - tMinClip = clipConvexHull(top, bottom, dMin, dMax); - top.reverse(); - bottom.reverse(); - tMaxClip = clipConvexHull(top, bottom, dMin, dMax); - // No intersections if one of the tvalues are null or 'undefined' - if (tMinClip == null || tMaxClip == null) + // Clip the convex-hull with dMin and dMax, taking into account that + // there will be no intersections if one of the tvalues are null. + if ((tMinClip = clipConvexHull(top, bottom, dMin, dMax)) == null || + (tMaxClip = clipConvexHull(top.reverse(), bottom.reverse(), + dMin, dMax)) == null) return; // Clip P with the fat-line for Q v1 = Curve.getPart(v1, tMinClip, tMaxClip); From 041c31a88aa452e001a2e3fe3346cb1ffff9d900 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 2 Sep 2015 15:54:14 +0200 Subject: [PATCH 014/280] Prevent variable leackage. --- src/path/Curve.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index efe205e6..e873723a 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1653,7 +1653,8 @@ new function() { // Scope for methods that require private functions // Merge intersections very close to the end of a curve to the // beginning of the next curve. for (var i = last; i >= 0; i--) { - var loc = locations[i]; + var loc = locations[i], + next; if (loc._parameter >= tMax && (next = loc._curve.getNext())) { loc._parameter = 0; loc._curve = next; From af355dc82c87b55cdf4c7ddec88dec7fc0ef5f4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Thu, 3 Sep 2015 09:01:07 +0200 Subject: [PATCH 015/280] Fix false positives in Curve#isLinear() and Segment#isLinear(). --- src/path/Curve.js | 8 +++++--- src/path/Segment.js | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index e873723a..90977dfe 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -666,9 +666,11 @@ statics: { // See Segment#isLinear(): var p1x = v[0], p1y = v[1], p2x = v[6], p2y = v[7], - l = new Point(p2x - p1x, p2y - p1y); - return l.isCollinear(new Point(v[2] - p1x, v[3] - p1y)) - && l.isCollinear(new Point(v[4] - p2x, v[5] - p2y)); + l = new Point(p2x - p1x, p2y - p1y), + h1 = new Point(v[2] - p1x, v[3] - p1y), + h2 = new Point(v[4] - p2x, v[5] - p2y); + return l.isZero() ? h1.isZero() && h2.isZero() + : l.isCollinear(h2) && l.isCollinear(h2); }, isFlatEnough: function(v, tolerance) { diff --git a/src/path/Segment.js b/src/path/Segment.js index 27af6723..21b16871 100644 --- a/src/path/Segment.js +++ b/src/path/Segment.js @@ -571,9 +571,11 @@ var Segment = Base.extend(/** @lends Segment# */{ // these methods that are implemented in both places. isLinear: function(seg1, seg2) { - var l = seg2._point.subtract(seg1._point); - return l.isCollinear(seg1._handleOut) - && l.isCollinear(seg2._handleIn); + var l = seg2._point.subtract(seg1._point), + h1 = seg1._handleOut, + h2 = seg2._handleIn; + return l.isZero() ? h1.isZero() && h2.isZero() + : l.isCollinear(h2) && l.isCollinear(h2); }, isCollinear: function(seg1, seg2, seg3, seg4) { From 3fa385ac7cdb3627770af2e0e0da94e63da5db28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sat, 5 Sep 2015 09:56:37 +0200 Subject: [PATCH 016/280] Fix typo in previous commit. --- src/path/Curve.js | 2 +- src/path/Segment.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 90977dfe..aed9b659 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -670,7 +670,7 @@ statics: { h1 = new Point(v[2] - p1x, v[3] - p1y), h2 = new Point(v[4] - p2x, v[5] - p2y); return l.isZero() ? h1.isZero() && h2.isZero() - : l.isCollinear(h2) && l.isCollinear(h2); + : l.isCollinear(h1) && l.isCollinear(h2); }, isFlatEnough: function(v, tolerance) { diff --git a/src/path/Segment.js b/src/path/Segment.js index 21b16871..cb56c7a8 100644 --- a/src/path/Segment.js +++ b/src/path/Segment.js @@ -575,7 +575,7 @@ var Segment = Base.extend(/** @lends Segment# */{ h1 = seg1._handleOut, h2 = seg2._handleIn; return l.isZero() ? h1.isZero() && h2.isZero() - : l.isCollinear(h2) && l.isCollinear(h2); + : l.isCollinear(h1) && l.isCollinear(h2); }, isCollinear: function(seg1, seg2, seg3, seg4) { From bfbe0b31478fedf66975ecd01760df70a6e05ceb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 6 Sep 2015 12:33:41 +0200 Subject: [PATCH 017/280] Fix PointText size test on new Safari... --- test/tests/TextItem.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/tests/TextItem.js b/test/tests/TextItem.js index b3d218b4..77cbc5c9 100644 --- a/test/tests/TextItem.js +++ b/test/tests/TextItem.js @@ -14,7 +14,7 @@ module('TextItem'); test('PointText', function() { var text = new PointText({ - fontFamily: 'Arial', + fontFamily: 'Helvetica, Arial', fontSize: 14, point: [100, 100], content: 'Hello World!' @@ -22,7 +22,7 @@ test('PointText', function() { equals(text.fillColor, new Color(0, 0, 0), 'text.fillColor should be black by default'); equals(text.point, new Point(100, 100), 'text.point'); equals(text.bounds.point, new Point(100, 87.4), 'text.bounds.point'); - equals(text.bounds.size, new Size(77, 16.8), 'text.bounds.size', { tolerance: 1.0 }); + equals(text.bounds.size, new Size(76, 16.8), 'text.bounds.size', { tolerance: 1.0 }); equals(function() { return text.hitTest(text.bounds.center) != null; }, true); From 8b67d8a1dcb9deed6b8e952f14341549f2504c82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 6 Sep 2015 12:47:35 +0200 Subject: [PATCH 018/280] Remove #isStraight() in favor of #hasHandles() and implement #clearHandles() Relates to #652 --- src/path/Curve.js | 29 ++++++++++++----------------- src/path/Path.js | 10 ++++++++++ src/path/PathItem.Boolean.js | 22 +++++++++------------- src/path/Segment.js | 29 ++++++++++++----------------- test/tests/Path_Curves.js | 8 ++++---- 5 files changed, 47 insertions(+), 51 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index aed9b659..89dca428 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -120,11 +120,11 @@ var Curve = Base.extend(/** @lends Curve# */{ }, _serialize: function(options) { - // If it is straight, only serialize point, otherwise handles too. - return Base.serialize(this.isStraight() - ? [this.getPoint1(), this.getPoint2()] - : [this.getPoint1(), this.getHandle1(), this.getHandle2(), - this.getPoint2()], + // If it has no handles, only serialize points, otherwise handles too. + return Base.serialize(this.hasHandles() + ? [this.getPoint1(), this.getHandle1(), this.getHandle2(), + this.getPoint2()] + : [this.getPoint1(), this.getPoint2()], options, true); }, @@ -329,22 +329,17 @@ var Curve = Base.extend(/** @lends Curve# */{ * @see Path#hasHandles() */ hasHandles: function() { - return !this.isStraight(); + return !this._segment1._handleOut.isZero() + || !this._segment2._handleIn.isZero(); }, /** - * Checks whether the curve is straight, meaning it has no curve handles - * defined and thus appears as a line. - * Note that this is not the same as {@link #isLinear()}, which performs a - * full linearity check that includes handles running collinear to the line - * direction. - * - * @return {Boolean} {@true if the curve is straight} - * @see Segment#isStraight() + * Clears the curve's handles by setting their coordinates to zero, + * turning the curve into a straight line. */ - isStraight: function() { - return this._segment1._handleOut.isZero() - && this._segment2._handleIn.isZero(); + clearHandles: function() { + this._segment1._handleOut.set(0, 0); + this._segment2._handleIn.set(0, 0); }, /** diff --git a/src/path/Path.js b/src/path/Path.js index b61c3c0d..e645f9a0 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -402,6 +402,16 @@ var Path = PathItem.extend(/** @lends Path# */{ return false; }, + /** + * Clears the path's handles by setting their coordinates to zero, + * turning the path into a polygon (or a polyline if it isn't closed). + */ + clearHandles: function() { + var segments = this._segments; + for (var i = 0, l = segments.length; i < l; i++) + segments[i].clearHandles(); + }, + _transformContent: function(matrix) { var coords = new Array(6); for (var i = 0, l = this._segments.length; i < l; i++) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index ba9fc09e..decd58d0 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -242,8 +242,8 @@ PathItem.inject(new function() { // TODO: Make public in API, since useful! var tMin = /*#=*/Numerical.TOLERANCE, tMax = 1 - tMin, - isStraight = false, - straightSegments = []; + noHandles = false, + clearSegments = []; for (var i = intersections.length - 1, curve, prev; i >= 0; i--) { var loc = intersections[i], @@ -255,7 +255,7 @@ PathItem.inject(new function() { t /= prev._parameter; } else { curve = loc._curve; - isStraight = curve.isStraight(); + noHandles = !curve.hasHandles(); } var segment; if (t < tMin) { @@ -270,22 +270,18 @@ PathItem.inject(new function() { curve = newCurve.getPrevious(); // Keep track of segments of once straight curves, so they can // be set back straight at the end. - if (isStraight) - straightSegments.push(segment); + if (noHandles) + clearSegments.push(segment); } // Link the new segment with the intersection on the other curve segment._intersection = loc.getIntersection(); loc._segment = segment; prev = loc; } - // Reset linear segments if they were part of a linear curve - // and if we are done with the entire curve. - for (var i = 0, l = straightSegments.length; i < l; i++) { - var segment = straightSegments[i]; - // TODO: Implement Segment#makeStraight(), - // or #adjustHandles({ straight: true })) - segment._handleIn.set(0, 0); - segment._handleOut.set(0, 0); + // Clear segment handles if they were part of a curve with no handles, + // once we are done with the entire curve. + for (var i = 0, l = clearSegments.length; i < l; i++) { + clearSegments[i].clearHandles(); } } diff --git a/src/path/Segment.js b/src/path/Segment.js index cb56c7a8..575992bc 100644 --- a/src/path/Segment.js +++ b/src/path/Segment.js @@ -145,9 +145,10 @@ var Segment = Base.extend(/** @lends Segment# */{ }, _serialize: function(options) { - // If it is straight, only serialize point, otherwise handles too. - return Base.serialize(this.isStraight() ? this._point - : [this._point, this._handleIn, this._handleOut], + // If it is has no handles, only serialize point, otherwise handles too. + return Base.serialize(this.hasHandles() + ? [this._point, this._handleIn, this._handleOut] + : this._point, options, true); }, @@ -237,22 +238,16 @@ var Segment = Base.extend(/** @lends Segment# */{ * @see Path#hasHandles() */ hasHandles: function() { - return !this.isStraight(); + return !this._handleIn.isZero() || !this._handleOut.isZero(); }, /** - * Checks whether the segment is straight, meaning it has no curve handles - * defined. If two straight segments follow each each other in a path, the - * curve between them will appear as a straight line. - * Note that this is not the same as {@link #isLinear()}, which performs a - * full linearity check that includes handles running collinear to the line - * direction. - * - * @return {Boolean} {@true if the segment is straight} - * @see Curve#isStraight() + * Clears the segment's handles by setting their coordinates to zero, + * turning the segment into a corner. */ - isStraight: function() { - return this._handleIn.isZero() && this._handleOut.isZero(); + clearHandles: function() { + this._handleIn.set(0, 0); + this._handleOut.set(0, 0); }, /** @@ -579,7 +574,7 @@ var Segment = Base.extend(/** @lends Segment# */{ }, isCollinear: function(seg1, seg2, seg3, seg4) { - // TODO: This assumes isStraight(), while isLinear() allows handles! + // TODO: This assumes !hasHandles(), while isLinear() allows handles! return seg1._handleOut.isZero() && seg2._handleIn.isZero() && seg3._handleOut.isZero() && seg4._handleIn.isZero() && seg2._point.subtract(seg1._point).isCollinear( @@ -587,7 +582,7 @@ var Segment = Base.extend(/** @lends Segment# */{ }, isOrthogonal: function(seg1, seg2, seg3) { - // TODO: This assumes isStraight(), while isLinear() allows handles! + // TODO: This assumes !hasHandles(), while isLinear() allows handles! return seg1._handleOut.isZero() && seg2._handleIn.isZero() && seg2._handleOut.isZero() && seg3._handleIn.isZero() && seg2._point.subtract(seg1._point).isOrthogonal( diff --git a/test/tests/Path_Curves.js b/test/tests/Path_Curves.js index c697e9b1..faebbd47 100644 --- a/test/tests/Path_Curves.js +++ b/test/tests/Path_Curves.js @@ -107,10 +107,10 @@ test('Curve list after removing a segment - 2', function() { }, 1, 'After removing the last segment, we should be left with one curve'); }); -test('Splitting a straight path should produce straight segments', function() { - var path = new Path.Line([0, 0], [50, 50]); - var path2 = path.split(0, 0.5); +test('Splitting a straight path should produce segments without handles', function() { + var path1 = new Path.Line([0, 0], [50, 50]); + var path2 = path1.split(0, 0.5); equals(function() { - return path2.firstSegment.isStraight(); + return !path1.lastSegment.hasHandles() && !path2.firstSegment.hasHandles(); }, true); }); From fe5916766afaedaa65ba704cb7e0fa68bb0c859d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 6 Sep 2015 13:20:29 +0200 Subject: [PATCH 019/280] Implement various tests for Curve#isLinear() Some are currently failing. --- test/tests/Curve.js | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/test/tests/Curve.js b/test/tests/Curve.js index f125f1c3..37f57b92 100644 --- a/test/tests/Curve.js +++ b/test/tests/Curve.js @@ -178,3 +178,36 @@ test('Curve#getLocationAt()', function() { 'Should return null when offset is out of range.'); // 'Should return null when point is not on the curve.'); }); + +test('Curve#isLinear()', function() { + equals(function() { + return new Curve([100, 100], null, null, [200, 200]).isLinear(); + }, true); + equals(function() { + return new Curve([100, 100], [-50, -50], null, [200, 200]).isLinear(); + }, false); + equals(function() { + return new Curve([100, 100], [50, 50], null, [200, 200]).isLinear(); + }, true); + equals(function() { + return new Curve([100, 100], [50, 50], [-50, -50], [200, 200]).isLinear(); + }, true); + equals(function() { + return new Curve([100, 100], [50, 50], [50, 50], [200, 200]).isLinear(); + }, false); + equals(function() { + return new Curve([100, 100], null, [-50, -50], [200, 200]).isLinear(); + }, true); + equals(function() { + return new Curve([100, 100], null, [50, 50], [200, 200]).isLinear(); + }, false); + equals(function() { + return new Curve([100, 100], null, null, [100, 100]).isLinear(); + }, true); + equals(function() { + return new Curve([100, 100], [50, 50], null, [100, 100]).isLinear(); + }, false); + equals(function() { + return new Curve([100, 100], null, [-50, -50], [100, 100]).isLinear(); + }, false); +}); From 26e35322a4fdcdf1f6bf3d706b9288c7e651bb08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 6 Sep 2015 13:20:57 +0200 Subject: [PATCH 020/280] Some reworking of code and comments. --- src/path/Curve.js | 6 ++-- src/path/Path.js | 88 ++++++++++++++++++++++----------------------- src/path/Segment.js | 13 ++++--- 3 files changed, 55 insertions(+), 52 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 89dca428..848e65af 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -300,7 +300,7 @@ var Curve = Base.extend(/** @lends Curve# */{ */ getLength: function() { if (this._length == null) { - // Use simple point distance for linear curves + // Use simple point distance for straight curves this._length = this.isLinear() ? this._segment2._point.getDistance(this._segment1._point) : Curve.getLength(this.getValues(), 0, 1); @@ -343,8 +343,8 @@ var Curve = Base.extend(/** @lends Curve# */{ }, /** - * Checks if this curve appears as a line. This can mean that it has no - * handles defined, or that the handles run collinear with the line. + * Checks if this curve appears as a straight line. This can mean that it + * has no handles defined, or that the handles run collinear with the line. * * @return {Boolean} {@true if the curve is linear} * @see Segment#isLinear() diff --git a/src/path/Path.js b/src/path/Path.js index e645f9a0..a03ec7f0 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -368,50 +368,6 @@ var Path = PathItem.extend(/** @lends Path# */{ return this._segments.length === 0; }, - /** - * Checks if this path consists of only linear curves. This can mean that - * the curves have no handles defined, or that the handles run collinear - * with the line. - * - * @return {Boolean} {@true if the path is entirely linear} - * @see Segment#isLinear() - * @see Curve#isLinear() - */ - isLinear: function() { - var segments = this._segments; - for (var i = 0, l = segments.length; i < l; i++) { - if (!segments[i].isLinear()) - return false; - } - return true; - }, - - /** - * Checks if none of the curves in the path define any curve handles. - * - * @return {Boolean} {@true if the path contains no curve handles} - * @see Segment#hasHandles() - * @see Curve#hasHandles() - */ - hasHandles: function() { - var segments = this._segments; - for (var i = 0, l = segments.length; i < l; i++) { - if (segments[i].hasHandles()) - return true; - } - return false; - }, - - /** - * Clears the path's handles by setting their coordinates to zero, - * turning the path into a polygon (or a polyline if it isn't closed). - */ - clearHandles: function() { - var segments = this._segments; - for (var i = 0, l = segments.length; i < l; i++) - segments[i].clearHandles(); - }, - _transformContent: function(matrix) { var coords = new Array(6); for (var i = 0, l = this._segments.length; i < l; i++) @@ -819,6 +775,50 @@ var Path = PathItem.extend(/** @lends Path# */{ // DOCS Path#clear() clear: '#removeSegments', + /** + * Checks if none of the curves in the path define any curve handles. + * + * @return {Boolean} {@true if the path contains no curve handles} + * @see Segment#hasHandles() + * @see Curve#hasHandles() + */ + hasHandles: function() { + var segments = this._segments; + for (var i = 0, l = segments.length; i < l; i++) { + if (segments[i].hasHandles()) + return true; + } + return false; + }, + + /** + * Clears the path's handles by setting their coordinates to zero, + * turning the path into a polygon (or a polyline if it isn't closed). + */ + clearHandles: function() { + var segments = this._segments; + for (var i = 0, l = segments.length; i < l; i++) + segments[i].clearHandles(); + }, + + /** + * Checks if this path consists of only straight curves. This can mean that + * the curves have no handles defined, or that the handles run collinear + * with the line. + * + * @return {Boolean} {@true if the path is entirely linear} + * @see Segment#isLinear() + * @see Curve#isLinear() + */ + isLinear: function() { + var segments = this._segments; + for (var i = 0, l = segments.length; i < l; i++) { + if (!segments[i].isLinear()) + return false; + } + return true; + }, + /** * The approximate length of the path in points. * diff --git a/src/path/Segment.js b/src/path/Segment.js index 575992bc..81065070 100644 --- a/src/path/Segment.js +++ b/src/path/Segment.js @@ -251,11 +251,11 @@ var Segment = Base.extend(/** @lends Segment# */{ }, /** - * Checks if the curve that starts in this segment appears as a line. This - * can mean that it has no handles defined, or that the handles run - * collinear with the line. + * Checks if the curve that starts in this segment appears as a straight + * line. This can mean that it has no handles defined, or that the handles + * run collinear with the line. * - * @return {Boolean} {@true if the curve is linear} + * @return {Boolean} {@true if the curve starting in this segment is linear} * @see Curve#isLinear() * @see Path#isLinear() */ @@ -563,7 +563,10 @@ var Segment = Base.extend(/** @lends Segment# */{ statics: { // These statics are shared between Segment and Curve, for versions of - // these methods that are implemented in both places. + // these methods that are implemented in both places. Most of these + // methods relate more to the nature of curves than segments, but since + // curves are made out of segments, and segments are the main path data + // structure, while curves are 2nd class citizens, they are defined here isLinear: function(seg1, seg2) { var l = seg2._point.subtract(seg1._point), From d7fb5cd51285d56db8c7b501a4a32313c6b02d69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 6 Sep 2015 13:21:08 +0200 Subject: [PATCH 021/280] Do not reduce linear curves with handles defined. --- src/path/Path.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/path/Path.js b/src/path/Path.js index a03ec7f0..6590c1f3 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -1021,7 +1021,7 @@ var Path = PathItem.extend(/** @lends Path# */{ for (var i = curves.length - 1; i >= 0; i--) { var curve = curves[i], next; - if (curve.isLinear() && (curve.getLength() === 0 + if (!curve.hasHandles() && (curve.getLength() === 0 || (next = curve.getNext()) && curve.isCollinear(next))) curve.remove(); } From f91373efd8d4346852046ef81265ddcf436672eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 6 Sep 2015 14:01:04 +0200 Subject: [PATCH 022/280] Simplify Point#project() --- src/basic/Point.js | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/basic/Point.js b/src/basic/Point.js index f9af8718..d96e08e8 100644 --- a/src/basic/Point.js +++ b/src/basic/Point.js @@ -721,6 +721,7 @@ var Point = Base.extend(/** @lends Point# */{ isOrthogonal: function(point) { // NOTE: Numerical.EPSILON is too small, breaking shape-path-shape // conversion test. + // TODO: Test if 1e-10 works here too? See #isCollinear() return Math.abs(this.dot(point)) < /*#=*/Numerical.TOLERANCE; }, @@ -767,23 +768,19 @@ var Point = Base.extend(/** @lends Point# */{ }, /** - * Returns the projection of the point on another point. + * Returns the projection of the point onto another point. * Both points are interpreted as vectors. * * @param {Point} point - * @return {Point} the projection of the point on another point + * @return {Point} the projection of the point onto another point */ project: function(/* point */) { - var point = Point.read(arguments); - if (point.isZero()) { - return new Point(0, 0); - } else { - var scale = this.dot(point) / point.dot(point); - return new Point( - point.x * scale, - point.y * scale - ); - } + var point = Point.read(arguments), + scale = point.isZero() ? 0 : this.dot(point) / point.dot(point); + return new Point( + point.x * scale, + point.y * scale + ); }, /** From 66717868cd2b71b32133cf9737b81baab501f868 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 6 Sep 2015 14:10:15 +0200 Subject: [PATCH 023/280] Address failing #isLinear() tests. --- src/path/Curve.js | 17 ++++++++++++++--- src/path/Segment.js | 14 +++++++++++--- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 848e65af..58f179a3 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -344,7 +344,9 @@ var Curve = Base.extend(/** @lends Curve# */{ /** * Checks if this curve appears as a straight line. This can mean that it - * has no handles defined, or that the handles run collinear with the line. + * has no handles defined, or that the handles run collinear with the line + * that connects the curve's start and end point, not falling outside of + * the line. * * @return {Boolean} {@true if the curve is linear} * @see Segment#isLinear() @@ -664,8 +666,17 @@ statics: { l = new Point(p2x - p1x, p2y - p1y), h1 = new Point(v[2] - p1x, v[3] - p1y), h2 = new Point(v[4] - p2x, v[5] - p2y); - return l.isZero() ? h1.isZero() && h2.isZero() - : l.isCollinear(h1) && l.isCollinear(h2); + if (l.isZero()) { + return h1.isZero() && h2.isZero(); + } else if (h1.isCollinear(l) && h2.isCollinear(l)) { + // Get the scalar projection of h1 and h2 onto l, and make sure they + // lie within l (note that h2 is reversed) + var div = l.dot(l), + p1 = l.dot(h1) / div, + p2 = l.dot(h2) / div; + return p1 >= 0 && p1 <= 1 && p2 <= 0 && p2 >= -1; + } + return false; }, isFlatEnough: function(v, tolerance) { diff --git a/src/path/Segment.js b/src/path/Segment.js index 81065070..863d2770 100644 --- a/src/path/Segment.js +++ b/src/path/Segment.js @@ -253,7 +253,8 @@ var Segment = Base.extend(/** @lends Segment# */{ /** * Checks if the curve that starts in this segment appears as a straight * line. This can mean that it has no handles defined, or that the handles - * run collinear with the line. + * run collinear with the line that connects the curve's start and end + * point, not falling outside of the line. * * @return {Boolean} {@true if the curve starting in this segment is linear} * @see Curve#isLinear() @@ -572,8 +573,15 @@ var Segment = Base.extend(/** @lends Segment# */{ var l = seg2._point.subtract(seg1._point), h1 = seg1._handleOut, h2 = seg2._handleIn; - return l.isZero() ? h1.isZero() && h2.isZero() - : l.isCollinear(h1) && l.isCollinear(h2); + if (l.isZero()) { + return h1.isZero() && h2.isZero(); + } else if (h1.isCollinear(l) && h2.isCollinear(l)) { + var div = l.dot(l), + p1 = l.dot(h1) / div, + p2 = l.dot(h2) / div; + return p1 >= 0 && p1 <= 1 && p2 <= 0 && p2 >= -1; + } + return false; }, isCollinear: function(seg1, seg2, seg3, seg4) { From 3d89cd71bd1ea51170d1ddac1dadfed656c86684 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 6 Sep 2015 15:54:11 +0200 Subject: [PATCH 024/280] Some clean-up work on documentation. --- src/path/Curve.js | 17 +++++++++++++---- src/path/Path.js | 4 ++-- src/path/PathItem.js | 2 -- src/path/Segment.js | 9 +++++---- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 58f179a3..26f5b797 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -322,9 +322,11 @@ var Curve = Base.extend(/** @lends Curve# */{ }, /** - * Checks if this curve defines any curve handle. + * Checks if this curve has any curve handles set. * - * @return {Boolean} {@true if the curve has handles defined} + * @return {Boolean} {@true if the curve has handles set} + * @see Curve#getHandle1() + * @see Curve#getHandle2() * @see Segment#hasHandles() * @see Path#hasHandles() */ @@ -360,7 +362,7 @@ var Curve = Base.extend(/** @lends Curve# */{ * Checks if the the two curves describe lines that are collinear, meaning * they run in parallel. * - * @param {Curve} the other curve to check against + * @param {Curve} curve the other curve to check against * @return {Boolean} {@true if the two lines are collinear} * @see Segment#isCollinear(segment) */ @@ -380,7 +382,14 @@ var Curve = Base.extend(/** @lends Curve# */{ return Segment.isOrthogonalArc(this._segment1, this._segment2); }, - // DOCS: Curve#getIntersections() + /** + * Returns all intersections between two {@link Curve} objects as an array + * of {@link CurveLocation} objects. + * + * @param {Curve} curve the other curve to find the intersections with + * @return {CurveLocation[]} the locations of all intersection between the + * curves + */ getIntersections: function(curve) { return Curve.filterIntersections(Curve.getIntersections( this.getValues(), curve.getValues(), this, curve, [])); diff --git a/src/path/Path.js b/src/path/Path.js index 6590c1f3..3297d620 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -776,9 +776,9 @@ var Path = PathItem.extend(/** @lends Path# */{ clear: '#removeSegments', /** - * Checks if none of the curves in the path define any curve handles. + * Checks if any of the curves in the path have curve handles set. * - * @return {Boolean} {@true if the path contains no curve handles} + * @return {Boolean} {@true if the path has curve handles set} * @see Segment#hasHandles() * @see Curve#hasHandles() */ diff --git a/src/path/PathItem.js b/src/path/PathItem.js index b44c23b9..2a43f215 100644 --- a/src/path/PathItem.js +++ b/src/path/PathItem.js @@ -32,8 +32,6 @@ var PathItem = Item.extend(/** @lends PathItem# */{ * supported. * * @param {PathItem} path the other item to find the intersections with - * @param {Boolean} [sorted=false] specifies whether the returned - * {@link CurveLocation} objects should be sorted by path and offset * @return {CurveLocation[]} the locations of all intersection between the * paths * @example {@paperscript} // Finding the intersections between two paths diff --git a/src/path/Segment.js b/src/path/Segment.js index 863d2770..84092720 100644 --- a/src/path/Segment.js +++ b/src/path/Segment.js @@ -230,10 +230,11 @@ var Segment = Base.extend(/** @lends Segment# */{ }, /** - * Checks whether the segment has curve handles defined, meaning it is not - * a straight segment. + * Checks if the segment has any curve handles set. * - * @return {Boolean} {@true if the segment has handles defined} + * @return {Boolean} {@true if the segment has handles set} + * @see Segment#getHandleIn() + * @see Segment#getHandleOut() * @see Curve#hasHandles() * @see Path#hasHandles() */ @@ -268,7 +269,7 @@ var Segment = Base.extend(/** @lends Segment# */{ * Checks if the the two segments are the beginning of two lines that are * collinear, meaning they run in parallel. * - * @param {Segment} the other segment to check against + * @param {Segment} segment the other segment to check against * @return {Boolean} {@true if the two lines are collinear} * @see Curve#isCollinear(curve) */ From dd1f5ba3d168954770aa1f04e01397081e8f359c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 6 Sep 2015 16:35:15 +0200 Subject: [PATCH 025/280] Remove Path#isLinear() and use Path#getArea() instaed in boolean code. --- src/path/Path.js | 18 ------------------ src/path/PathItem.Boolean.js | 10 +++++----- 2 files changed, 5 insertions(+), 23 deletions(-) diff --git a/src/path/Path.js b/src/path/Path.js index 3297d620..a5d44c7b 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -801,24 +801,6 @@ var Path = PathItem.extend(/** @lends Path# */{ segments[i].clearHandles(); }, - /** - * Checks if this path consists of only straight curves. This can mean that - * the curves have no handles defined, or that the handles run collinear - * with the line. - * - * @return {Boolean} {@true if the path is entirely linear} - * @see Segment#isLinear() - * @see Curve#isLinear() - */ - isLinear: function() { - var segments = this._segments; - for (var i = 0, l = segments.length; i < l; i++) { - if (!segments[i].isLinear()) - return false; - } - return true; - }, - /** * The approximate length of the path in points. * diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 0824b5b1..ed975df6 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -650,11 +650,11 @@ PathItem.inject(new function() { path = null; } // Add the path to the result, while avoiding stray segments and - // incomplete paths. The amount of segments for valid paths depend - // on their geometry: - // - Closed paths with only straight lines need more than 2 segments - // - Closed paths with curves can consist of only one segment - if (path && path._segments.length > path.isLinear() ? 2 : 0) + // paths that are incomplete or cover no area. + // As an optimization, only check paths with 4 or less segments + // for their area, and assume that they cover an area when more. + if (path && (path._segments.length > 4 + || !Numerical.isZero(path.getArea()))) paths.push(path); if (reportSegments) { pathCount++; From cf813faa751c0a4b2bbba260e1fe37894bd9a1ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 6 Sep 2015 16:37:10 +0200 Subject: [PATCH 026/280] Remove all mention of points and square points as units. And some other doc clean-ups. --- src/basic/Line.js | 12 ++++++------ src/basic/Rectangle.js | 2 +- src/item/Item.js | 2 +- src/path/CompoundPath.js | 4 ++-- src/path/Curve.js | 28 +++++++++++++++++++++++++++- src/path/Path.js | 6 +++--- src/project/Project.js | 2 +- 7 files changed, 41 insertions(+), 15 deletions(-) diff --git a/src/basic/Line.js b/src/basic/Line.js index 3d143109..28c6883c 100644 --- a/src/basic/Line.js +++ b/src/basic/Line.js @@ -48,30 +48,30 @@ var Line = Base.extend(/** @lends Line# */{ }, /** - * The starting point of the line + * The starting point of the line. * - * @name Line#point * @type Point + * @bean */ getPoint: function() { return new Point(this._px, this._py); }, /** - * The vector of the line + * The direction of the line as a vector. * - * @name Line#vector * @type Point + * @bean */ getVector: function() { return new Point(this._vx, this._vy); }, /** - * The length of the line + * The length of the line. * - * @name Line#length * @type Number + * @bean */ getLength: function() { return this.getVector().getLength(); diff --git a/src/basic/Rectangle.js b/src/basic/Rectangle.js index e26f27f7..c6b89ba0 100644 --- a/src/basic/Rectangle.js +++ b/src/basic/Rectangle.js @@ -475,7 +475,7 @@ var Rectangle = Base.extend(/** @lends Rectangle# */{ */ /** - * The area of the rectangle in square points. + * The area of the rectangle. * * @type Number * @bean diff --git a/src/item/Item.js b/src/item/Item.js index 122f4f2f..722947e8 100644 --- a/src/item/Item.js +++ b/src/item/Item.js @@ -1698,7 +1698,7 @@ var Item = Base.extend(Emitter, /** @lends Item# */{ * and may contain a combination of the following values: * * @option [options.tolerance={@link PaperScope#settings}.hitTolerance] - * {Number} the tolerance of the hit-test in points + * {Number} the tolerance of the hit-test * @option options.class {Function} only hit-test again a certain item class * and its sub-classes: {@code Group, Layer, Path, CompoundPath, * Shape, Raster, PlacedSymbol, PointText}, etc diff --git a/src/path/CompoundPath.js b/src/path/CompoundPath.js index fb285478..53e4a359 100644 --- a/src/path/CompoundPath.js +++ b/src/path/CompoundPath.js @@ -220,8 +220,8 @@ var CompoundPath = PathItem.extend(/** @lends CompoundPath# */{ }, /** - * The area of the path in square points. Self-intersecting paths can - * contain sub-areas that cancel each other out. + * The area that the path's geometry is covering. Self-intersecting paths + * can contain sub-areas that cancel each other out. * * @type Number * @bean diff --git a/src/path/Curve.js b/src/path/Curve.js index 26f5b797..1efe9ef7 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -279,10 +279,30 @@ var Curve = Base.extend(/** @lends Curve# */{ this.getPoint2().setSelected(selected); }, + /** + * An array of 8 float values, describing this curve's geometry in four + * absolute x/y pairs (point1, handle1, handle2, point2). This format is + * used internally for efficient processing of curve geometries, e.g. when + * calculating intersections or bounds. + * + * Note that the handles are converted to absolute coordinates. + * + * @type Number[] + * @bean + */ getValues: function(matrix) { return Curve.getValues(this._segment1, this._segment2, matrix); }, + /** + * An array of 4 point objects, describing this curve's geometry in absolute + * coordinates (point1, handle1, handle2, point2). + * + * Note that the handles are converted to absolute coordinates. + * + * @type Point[] + * @bean + */ getPoints: function() { // Convert to array of absolute points var coords = this.getValues(), @@ -293,7 +313,7 @@ var Curve = Base.extend(/** @lends Curve# */{ }, /** - * The approximated length of the curve in points. + * The approximated length of the curve. * * @type Number * @bean @@ -308,6 +328,12 @@ var Curve = Base.extend(/** @lends Curve# */{ return this._length; }, + /** + * The area that the curve's geometry is covering. + * + * @type Number + * @bean + */ getArea: function() { return Curve.getArea(this.getValues()); }, diff --git a/src/path/Path.js b/src/path/Path.js index a5d44c7b..b895e990 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -802,7 +802,7 @@ var Path = PathItem.extend(/** @lends Path# */{ }, /** - * The approximate length of the path in points. + * The approximate length of the path. * * @type Number * @bean @@ -818,8 +818,8 @@ var Path = PathItem.extend(/** @lends Path# */{ }, /** - * The area of the path in square points. Self-intersecting paths can - * contain sub-areas that cancel each other out. + * The area that the path's geometry is covering. Self-intersecting paths + * can contain sub-areas that cancel each other out. * * @type Number * @bean diff --git a/src/project/Project.js b/src/project/Project.js index b5a96710..e9316366 100644 --- a/src/project/Project.js +++ b/src/project/Project.js @@ -304,7 +304,7 @@ var Project = PaperScopeItem.extend(/** @lends Project# */{ * and may contain a combination of the following values: * * @option [options.tolerance={@link PaperScope#settings}.hitTolerance] - * {Number} the tolerance of the hit-test in points + * {Number} the tolerance of the hit-test * @option options.class {Function} only hit-test again a certain item class * and its sub-classes: {@code Group, Layer, Path, CompoundPath, * Shape, Raster, PlacedSymbol, PointText}, etc From b52d343527a95e96399ad0ab8ee13fd4964476c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 6 Sep 2015 16:48:23 +0200 Subject: [PATCH 027/280] Use same notation for all injection scopes. --- src/basic/Rectangle.js | 3 ++- src/item/Shape.js | 1 - src/path/CompoundPath.js | 3 ++- src/path/Curve.js | 4 +++- src/path/Path.js | 11 ++++++----- src/style/Color.js | 3 ++- src/view/CanvasView.js | 4 ++-- src/view/View.js | 4 ++-- 8 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/basic/Rectangle.js b/src/basic/Rectangle.js index c6b89ba0..75a43e30 100644 --- a/src/basic/Rectangle.js +++ b/src/basic/Rectangle.js @@ -857,7 +857,8 @@ var LinkedRectangle = Rectangle.extend({ this._owner[this._setter](this); return this; } -}, new function() { +}, +new function() { var proto = Rectangle.prototype; return Base.each(['x', 'y', 'width', 'height'], function(key) { diff --git a/src/item/Shape.js b/src/item/Shape.js index 69c033e3..e17621a9 100644 --- a/src/item/Shape.js +++ b/src/item/Shape.js @@ -277,7 +277,6 @@ var Shape = Item.extend(/** @lends Shape# */{ } }, new function() { // Scope for _contains() and _hitTestSelf() code. - // Returns the center of the quarter corner ellipse for rounded rectangle, // if the point lies within its bounding box. function getCornerCenter(that, point, expand) { diff --git a/src/path/CompoundPath.js b/src/path/CompoundPath.js index 53e4a359..c7c8c622 100644 --- a/src/path/CompoundPath.js +++ b/src/path/CompoundPath.js @@ -298,7 +298,8 @@ var CompoundPath = PathItem.extend(/** @lends CompoundPath# */{ : matrix.chain(mx)); } } -}, new function() { // Injection scope for PostScript-like drawing functions +}, +new function() { // Injection scope for PostScript-like drawing functions /** * Helper method that returns the current path and checks if a moveTo() * command is required first. diff --git a/src/path/Curve.js b/src/path/Curve.js index 1efe9ef7..9f940516 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1050,6 +1050,7 @@ statics: { */ }, new function() { // // Scope to inject various curve evaluation methods + var methods = ['getPoint', 'getTangent', 'getNormal', 'getWeightedTangent', 'getWeightedNormal', 'getCurvature']; return Base.each(methods, @@ -1274,7 +1275,8 @@ new function() { // Scope for methods that require private functions return evaluate(v, t, 3, false).x; } }; -}, new function() { // Scope for intersection using bezier fat-line clipping +}, +new function() { // Scope for intersection using bezier fat-line clipping function addLocation(locations, include, curve1, t1, point1, curve2, t2, point2) { var loc = new CurveLocation(curve1, t1, point1, curve2, t2, point2); diff --git a/src/path/Path.js b/src/path/Path.js index b895e990..f6d8313f 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -2020,8 +2020,8 @@ var Path = PathItem.extend(/** @lends Path# */{ getNearestPoint: function(/* point */) { return this.getNearestLocation.apply(this, arguments).getPoint(); } -}), new function() { // Scope for drawing - +}), +new function() { // Scope for drawing // Note that in the code below we're often accessing _x and _y on point // objects that were read from segments. This is because the SegmentPoint // class overrides the plain x / y properties with getter / setters and @@ -2220,8 +2220,8 @@ var Path = PathItem.extend(/** @lends Path# */{ drawHandles(ctx, this._segments, matrix, paper.settings.handleSize); } }; -}, new function() { // Path Smoothing - +}, +new function() { // Path Smoothing /** * Solves a tri-diagonal system for one of coordinates (x or y) of first * bezier control points. @@ -2346,7 +2346,8 @@ var Path = PathItem.extend(/** @lends Path# */{ } } }; -}, new function() { // PostScript-style drawing commands +}, +new function() { // PostScript-style drawing commands /** * Helper method that returns the current segment and checks if a moveTo() * command is required first. diff --git a/src/style/Color.js b/src/style/Color.js index 3e1155f2..07d9966d 100644 --- a/src/style/Color.js +++ b/src/style/Color.js @@ -1112,7 +1112,8 @@ var Color = Base.extend(new function() { } } }); -}, new function() { +}, +new function() { var operators = { add: function(a, b) { return a + b; diff --git a/src/view/CanvasView.js b/src/view/CanvasView.js index 996ad97e..910ffbe0 100644 --- a/src/view/CanvasView.js +++ b/src/view/CanvasView.js @@ -144,8 +144,8 @@ var CanvasView = View.extend(/** @lends CanvasView# */{ project._needsUpdate = false; return true; } -}, new function() { // Item based mouse handling: - +}, +new function() { // Item based mouse handling: var downPoint, lastPoint, overPoint, diff --git a/src/view/View.js b/src/view/View.js index a497a87f..75a16d22 100644 --- a/src/view/View.js +++ b/src/view/View.js @@ -673,8 +673,8 @@ var View = Base.extend(Emitter, /** @lends View# */{ return new CanvasView(project, element); } } -}, new function() { - // Injection scope for mouse events on the browser +}, +new function() { // Injection scope for mouse events on the browser /*#*/ if (__options.environment == 'browser') { var tool, prevFocus, From 71a7cc37e63f2ad6e5e822ccf02ba711f6fe8e4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 6 Sep 2015 17:20:01 +0200 Subject: [PATCH 028/280] Many documentation clean-ups. --- src/item/Item.js | 2 +- src/item/Raster.js | 2 +- src/item/Shape.js | 2 +- src/path/Path.js | 4 ++-- src/style/Style.js | 8 ++++---- src/text/TextItem.js | 8 ++++---- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/item/Item.js b/src/item/Item.js index 722947e8..a4e9f4c1 100644 --- a/src/item/Item.js +++ b/src/item/Item.js @@ -1190,7 +1190,7 @@ var Item = Base.extend(Emitter, /** @lends Item# */{ /** * @bean - * @deprecated use {@link #getApplyMatrix()} instead. + * @deprecated use {@link #applyMatrix} instead. */ getTransformContent: '#getApplyMatrix', setTransformContent: '#setApplyMatrix', diff --git a/src/item/Raster.js b/src/item/Raster.js index 57961a1f..a0ef9a41 100644 --- a/src/item/Raster.js +++ b/src/item/Raster.js @@ -207,7 +207,7 @@ var Raster = Item.extend(/** @lends Raster# */{ /** * @private * @bean - * @deprecated use {@link #getResolution()} instead. + * @deprecated use {@link #resolution} instead. */ getPpi: '#getResolution', diff --git a/src/item/Shape.js b/src/item/Shape.js index e17621a9..a9c2c75d 100644 --- a/src/item/Shape.js +++ b/src/item/Shape.js @@ -64,7 +64,7 @@ var Shape = Item.extend(/** @lends Shape# */{ /** * @private * @bean - * @deprecated use {@link #getType()} instead. + * @deprecated use {@link #type} instead. */ getShape: '#getType', setShape: '#setType', diff --git a/src/path/Path.js b/src/path/Path.js index f6d8313f..f2ad755e 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -1966,7 +1966,7 @@ var Path = PathItem.extend(/** @lends Path# */{ * Returns the nearest location on the path to the specified point. * * @function - * @param point {Point} the point for which we search the nearest location + * @param {Point} point the point for which we search the nearest location * @return {CurveLocation} the location on the path that's the closest to * the specified point */ @@ -1989,7 +1989,7 @@ var Path = PathItem.extend(/** @lends Path# */{ * Returns the nearest point on the path to the specified point. * * @function - * @param point {Point} the point for which we search the nearest point + * @param {Point} point the point for which we search the nearest point * @return {Point} the point on the path that's the closest to the specified * point * diff --git a/src/style/Style.js b/src/style/Style.js index 0ebe5bff..d4d0db31 100644 --- a/src/style/Style.js +++ b/src/style/Style.js @@ -309,7 +309,7 @@ var Style = Base.extend(new function() { /** * @private * @bean - * @deprecated use {@link #getFontFamily()} instead. + * @deprecated use {@link #fontFamily} instead. */ getFont: '#getFontFamily', setFont: '#setFontFamily', @@ -582,7 +582,7 @@ var Style = Base.extend(new function() { */ /** - * The font size of text content, as {@Number} in pixels, or as {@String} + * The font size of text content, as a number in pixels, or as a string * with optional units {@code 'px'}, {@code 'pt'} and {@code 'em'}. * * @name Style#fontSize @@ -592,12 +592,12 @@ var Style = Base.extend(new function() { /** * - * The font-family to be used in text content, as one {@String}. - * @deprecated use {@link #fontFamily} instead. + * The font-family to be used in text content, as one string. * * @name Style#font * @default 'sans-serif' * @type String + * @deprecated use {@link #fontFamily} instead. */ /** diff --git a/src/text/TextItem.js b/src/text/TextItem.js index 824bf561..8aeb6168 100644 --- a/src/text/TextItem.js +++ b/src/text/TextItem.js @@ -120,7 +120,7 @@ var TextItem = Item.extend(/** @lends TextItem# */{ */ /** - * The font size of text content, as {@Number} in pixels, or as {@String} + * The font size of text content, as a number in pixels, or as a string * with optional units {@code 'px'}, {@code 'pt'} and {@code 'em'}. * * @name TextItem#fontSize @@ -130,7 +130,7 @@ var TextItem = Item.extend(/** @lends TextItem# */{ /** * - * The font-family to be used in text content, as one {@String}. + * The font-family to be used in text content, as one string. * @deprecated use {@link #fontFamily} instead. * * @name TextItem#font @@ -159,7 +159,7 @@ var TextItem = Item.extend(/** @lends TextItem# */{ /** * @private * @bean - * @deprecated use {@link #getStyle()} instead. + * @deprecated use {@link #style} instead. */ getCharacterStyle: '#getStyle', setCharacterStyle: '#setStyle', @@ -167,7 +167,7 @@ var TextItem = Item.extend(/** @lends TextItem# */{ /** * @private * @bean - * @deprecated use {@link #getStyle()} instead. + * @deprecated use {@link #style} instead. */ getParagraphStyle: '#getStyle', setParagraphStyle: '#setStyle' From 9dab662a1f28bb077e4e0465f4498b176c88689d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 6 Sep 2015 17:27:33 +0200 Subject: [PATCH 029/280] Clean-up various Segment and Curve tests. Moving functionality back to Path#toShape() since it was too specific, and missleading as part of the exposed Segment API. --- src/path/Curve.js | 276 ++++++++++++++++++++++++-------------------- src/path/Path.js | 38 +++++- src/path/Segment.js | 116 ------------------- 3 files changed, 188 insertions(+), 242 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 9f940516..da2e750b 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -133,6 +133,45 @@ var Curve = Base.extend(/** @lends Curve# */{ this._length = this._bounds = undefined; }, + /** + * Returns a copy of the curve. + * + * @return {Curve} + */ + clone: function() { + return new Curve(this._segment1, this._segment2); + }, + + /** + * @return {String} a string representation of the curve + */ + toString: function() { + var parts = [ 'point1: ' + this._segment1._point ]; + if (!this._segment1._handleOut.isZero()) + parts.push('handle1: ' + this._segment1._handleOut); + if (!this._segment2._handleIn.isZero()) + parts.push('handle2: ' + this._segment2._handleIn); + parts.push('point2: ' + this._segment2._point); + return '{ ' + parts.join(', ') + ' }'; + }, + + /** + * Removes the curve from the path that it belongs to, by removing its + * second segment and merging its handle with the first segment. + * @return {Boolean} {@true if the curve was removed} + */ + remove: function() { + var removed = false; + if (this._path) { + var segment2 = this._segment2, + handleOut = segment2._handleOut; + removed = segment2.remove(); + if (removed) + this._segment1._handleOut.set(handleOut.x, handleOut.y); + } + return removed; + }, + /** * The first anchor point of the curve. * @@ -322,7 +361,7 @@ var Curve = Base.extend(/** @lends Curve# */{ if (this._length == null) { // Use simple point distance for straight curves this._length = this.isLinear() - ? this._segment2._point.getDistance(this._segment1._point) + ? this.getVector().getLength() : Curve.getLength(this.getValues(), 0, 1); } return this._length; @@ -338,6 +377,17 @@ var Curve = Base.extend(/** @lends Curve# */{ return Curve.getArea(this.getValues()); }, + /** + * The total direction of the curve as a vector pointing from + * {@link #point1} to {@link #point2}. + * + * @type Point + * @bean + */ + getVector: function() { + return this._segment2._point.subtract(this._segment1._point); + }, + getPart: function(from, to) { return new Curve(Curve.getPart(this.getValues(), from, to)); }, @@ -347,67 +397,6 @@ var Curve = Base.extend(/** @lends Curve# */{ return Curve.getLength(this.getValues(), from, to); }, - /** - * Checks if this curve has any curve handles set. - * - * @return {Boolean} {@true if the curve has handles set} - * @see Curve#getHandle1() - * @see Curve#getHandle2() - * @see Segment#hasHandles() - * @see Path#hasHandles() - */ - hasHandles: function() { - return !this._segment1._handleOut.isZero() - || !this._segment2._handleIn.isZero(); - }, - - /** - * Clears the curve's handles by setting their coordinates to zero, - * turning the curve into a straight line. - */ - clearHandles: function() { - this._segment1._handleOut.set(0, 0); - this._segment2._handleIn.set(0, 0); - }, - - /** - * Checks if this curve appears as a straight line. This can mean that it - * has no handles defined, or that the handles run collinear with the line - * that connects the curve's start and end point, not falling outside of - * the line. - * - * @return {Boolean} {@true if the curve is linear} - * @see Segment#isLinear() - * @see Path#isLinear() - */ - isLinear: function() { - return Segment.isLinear(this._segment1, this._segment2); - }, - - /** - * Checks if the the two curves describe lines that are collinear, meaning - * they run in parallel. - * - * @param {Curve} curve the other curve to check against - * @return {Boolean} {@true if the two lines are collinear} - * @see Segment#isCollinear(segment) - */ - isCollinear: function(curve) { - return Segment.isCollinear(this._segment1, this._segment2, - curve._segment1, curve._segment2); - }, - - /** - * Checks if the curve describes an orthogonal arc, as used in the - * construction of circles and ellipses. - * - * @return {Boolean} {@true if the curve describes an orthogonal arc} - * @see Segment#isOrthogonalArc() - */ - isOrthogonalArc: function() { - return Segment.isOrthogonalArc(this._segment1, this._segment2); - }, - /** * Returns all intersections between two {@link Curve} objects as an array * of {@link CurveLocation} objects. @@ -541,45 +530,14 @@ var Curve = Base.extend(/** @lends Curve# */{ }, /** - * Removes the curve from the path that it belongs to, by removing its - * second segment and merging its handle with the first segment. - * @return {Boolean} {@true if the curve was removed} + * Clears the curve's handles by setting their coordinates to zero, + * turning the curve into a straight line. */ - remove: function() { - var removed = false; - if (this._path) { - var segment2 = this._segment2, - handleOut = segment2._handleOut; - removed = segment2.remove(); - if (removed) - this._segment1._handleOut.set(handleOut.x, handleOut.y); - } - return removed; + clearHandles: function() { + this._segment1._handleOut.set(0, 0); + this._segment2._handleIn.set(0, 0); }, - /** - * Returns a copy of the curve. - * - * @return {Curve} - */ - clone: function() { - return new Curve(this._segment1, this._segment2); - }, - - /** - * @return {String} a string representation of the curve - */ - toString: function() { - var parts = [ 'point1: ' + this._segment1._point ]; - if (!this._segment1._handleOut.isZero()) - parts.push('handle1: ' + this._segment1._handleOut); - if (!this._segment2._handleIn.isZero()) - parts.push('handle2: ' + this._segment2._handleIn); - parts.push('point2: ' + this._segment2._point); - return '{ ' + parts.join(', ') + ' }'; - }, - -// Mess with indentation in order to get more line-space below... statics: { getValues: function(segment1, segment2, matrix) { var p1 = segment1._point, @@ -694,26 +652,6 @@ statics: { && isZero(v[4] - v[6]) && isZero(v[5] - v[7])); }, - isLinear: function(v) { - // See Segment#isLinear(): - var p1x = v[0], p1y = v[1], - p2x = v[6], p2y = v[7], - l = new Point(p2x - p1x, p2y - p1y), - h1 = new Point(v[2] - p1x, v[3] - p1y), - h2 = new Point(v[4] - p2x, v[5] - p2y); - if (l.isZero()) { - return h1.isZero() && h2.isZero(); - } else if (h1.isCollinear(l) && h2.isCollinear(l)) { - // Get the scalar projection of h1 and h2 onto l, and make sure they - // lie within l (note that h2 is reversed) - var div = l.dot(l), - p1 = l.dot(h1) / div, - p2 = l.dot(h2) / div; - return p1 >= 0 && p1 <= 1 && p2 <= 0 && p2 >= -1; - } - return false; - }, - isFlatEnough: function(v, tolerance) { // Thanks to Kaspar Fischer and Roger Willcocks for the following: // http://hcklbrrfnn.files.wordpress.com/2012/08/bez.pdf @@ -833,6 +771,8 @@ statics: { }, /** @lends Curve# */{ /** + * {@grouptitle Bounding Boxes} + * * The bounding rectangle of the curve excluding stroke width. * * @name Curve#bounds @@ -861,13 +801,88 @@ statics: { * @type Rectangle * @ignore */ -}), /** @lends Curve# */{ +}), new function() { // Injection scope for tests + function isLinear(l, h1, h2) { + if (h1.isZero() && h2.isZero()) { + // No handles. + return true; + } else if (l.isZero()) { + // Zero-length line, with some handles defined. + return false; + } else if (h1.isCollinear(l) && h2.isCollinear(l)) { + // Collinear handles. Project them onto line to see if they are + // within the line's range: + var div = l.dot(l), + p1 = l.dot(h1) / div, + p2 = l.dot(h2) / div; + return p1 >= 0 && p1 <= 1 && p2 <= 0 && p2 >= -1; + } + return false; + } + + return /** @lends Curve# */{ + /** + * {@grouptitle Tests} + * + * Checks if this curve has any curve handles set. + * + * @return {Boolean} {@true if the curve has handles set} + * @see Curve#handle1 + * @see Curve#handle2 + * @see Segment#hasHandles() + * @see Path#hasHandles() + */ + hasHandles: function() { + return !this._segment1._handleOut.isZero() + || !this._segment2._handleIn.isZero(); + }, + + /** + * Checks if this curve appears as a straight line. This can mean that + * it has no handles defined, or that the handles run collinear with the + * line that connects the curve's start and end point, not falling + * outside of the line. + * + * @return {Boolean} {@true if the curve is linear} + */ + isLinear: function() { + var seg1 = this._segment1, + seg2 = this._segment2; + return isLinear(seg2._point.subtract(seg1._point), + seg1._handleOut, seg2._handleIn); + }, + + /** + * Checks if the the two curves describe straight lines that are + * collinear, meaning they run in parallel. + * + * @param {Curve} curve the other curve to check against + * @return {Boolean} {@true if the two lines are collinear} + */ + isCollinear: function(curve) { + return this.isLinear() && curve.isLinear() + && this.getVector().isCollinear(curve.getVector()); + }, + + statics: { + isLinear: function(v) { + var p1x = v[0], p1y = v[1], + p2x = v[6], p2y = v[7]; + return isLinear(new Point(p2x - p1x, p2y - p1y), + new Point(v[2] - p1x, v[3] - p1y), + new Point(v[4] - p2x, v[5] - p2y)); + } + } + } +}, /** @lends Curve# */{ // Explicitly deactivate the creation of beans, as we have functions here // that look like bean getters but actually read arguments. // See #getParameterOf(), #getLocationOf(), #getNearestLocation(), ... beans: false, /** + * {@grouptitle Positions on Curves} + * * Calculates the curve time parameter of the specified offset on the path, * relative to the provided start parameter. If offset is a negative value, * the parameter is searched to the left of the start parameter. If no start @@ -935,6 +950,14 @@ statics: { return loc ? loc.getOffset() : null; }, + /** + * Returns the nearest location on the curve to the specified point. + * + * @function + * @param {Point} point the point for which we search the nearest location + * @return {CurveLocation} the location on the curve that's the closest to + * the specified point + */ getNearestLocation: function(/* point */) { var point = Point.read(arguments), values = this.getValues(), @@ -967,6 +990,14 @@ statics: { point.getDistance(pt)); }, + /** + * Returns the nearest point on the curve to the specified point. + * + * @function + * @param {Point} point the point for which we search the nearest point + * @return {Point} the point on the curve that's the closest to the + * specified point + */ getNearestPoint: function(/* point */) { return this.getNearestLocation.apply(this, arguments).getPoint(); } @@ -1050,7 +1081,6 @@ statics: { */ }, new function() { // // Scope to inject various curve evaluation methods - var methods = ['getPoint', 'getTangent', 'getNormal', 'getWeightedTangent', 'getWeightedNormal', 'getCurvature']; return Base.each(methods, @@ -1183,8 +1213,7 @@ new function() { // Scope for methods that require private functions return type === 2 ? new Point(y, -x) : new Point(x, y); } - return { - statics: true, + return { statics: { getLength: function(v, a, b) { if (a === undefined) @@ -1274,9 +1303,10 @@ new function() { // Scope for methods that require private functions getCurvature: function(v, t) { return evaluate(v, t, 3, false).x; } - }; + }}; }, new function() { // Scope for intersection using bezier fat-line clipping + function addLocation(locations, include, curve1, t1, point1, curve2, t2, point2) { var loc = new CurveLocation(curve1, t1, point1, curve2, t2, point2); diff --git a/src/path/Path.js b/src/path/Path.js index f2ad755e..0bc06d55 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -1406,15 +1406,47 @@ var Path = PathItem.extend(/** @lends Path# */{ topCenter; function isCollinear(i, j) { - return segments[i].isCollinear(segments[j]); + var seg1 = segments[i], + seg2 = seg1.getNext(), + seg3 = segments[j], + seg4 = seg3.getNext(); + return seg1._handleOut.isZero() && seg2._handleIn.isZero() + && seg3._handleOut.isZero() && seg4._handleIn.isZero() + && seg2._point.subtract(seg1._point).isCollinear( + seg4._point.subtract(seg3._point)); } function isOrthogonal(i) { - return segments[i].isOrthogonal(); + var seg2 = segments[i], + seg1 = seg2.getPrevious(), + seg3 = seg2.getNext(); + return seg1._handleOut.isZero() && seg2._handleIn.isZero() + && seg2._handleOut.isZero() && seg3._handleIn.isZero() + && seg2._point.subtract(seg1._point).isOrthogonal( + seg3._point.subtract(seg2._point)); } function isArc(i) { - return segments[i].isOrthogonalArc(); + var seg1 = segments[i], + seg2 = seg1.getNext(), + handle1 = seg1._handleOut, + handle2 = seg2._handleIn, + kappa = /*#=*/Numerical.KAPPA; + // Look at handle length and the distance to the imaginary corner + // point and see if it their relation is kappa. + if (handle1.isOrthogonal(handle2)) { + var pt1 = seg1._point, + pt2 = seg2._point, + // Find the corner point by intersecting the lines described + // by both handles: + corner = new Line(pt1, handle1, true).intersect( + new Line(pt2, handle2, true), true); + return corner && Numerical.isZero(handle1.getLength() / + corner.subtract(pt1).getLength() - kappa) + && Numerical.isZero(handle2.getLength() / + corner.subtract(pt2).getLength() - kappa); + } + return false; } function getDistance(i, j) { diff --git a/src/path/Segment.js b/src/path/Segment.js index 84092720..14cd1aa6 100644 --- a/src/path/Segment.js +++ b/src/path/Segment.js @@ -251,62 +251,6 @@ var Segment = Base.extend(/** @lends Segment# */{ this._handleOut.set(0, 0); }, - /** - * Checks if the curve that starts in this segment appears as a straight - * line. This can mean that it has no handles defined, or that the handles - * run collinear with the line that connects the curve's start and end - * point, not falling outside of the line. - * - * @return {Boolean} {@true if the curve starting in this segment is linear} - * @see Curve#isLinear() - * @see Path#isLinear() - */ - isLinear: function() { - return Segment.isLinear(this, this.getNext()); - }, - - /** - * Checks if the the two segments are the beginning of two lines that are - * collinear, meaning they run in parallel. - * - * @param {Segment} segment the other segment to check against - * @return {Boolean} {@true if the two lines are collinear} - * @see Curve#isCollinear(curve) - */ - isCollinear: function(segment) { - return Segment.isCollinear(this, this.getNext(), - segment, segment.getNext()); - }, - - // TODO: Remove version with typo after a while (deprecated June 2015) - isColinear: '#isCollinear', - - /** - * Checks if the segment is connecting two lines that are orthogonal, - * meaning they connect at an 90° angle. - * - * @return {Boolean} {@true if the two lines connected by this segment are - * orthogonal} - */ - isOrthogonal: function() { - return Segment.isOrthogonal(this.getPrevious(), this, this.getNext()); - }, - - /** - * Checks if the segment is the beginning of an orthogonal arc, as used in - * the construction of circles and ellipses. - * - * @return {Boolean} {@true if the segment is the beginning of an orthogonal - * arc} - * @see Curve#isOrthogonalArc() - */ - isOrthogonalArc: function() { - return Segment.isOrthogonalArc(this, this.getNext()); - }, - - // TODO: Remove a while (deprecated August 2015) - isArc: '#isOrthogonalArc', - _selectionState: 0, /** @@ -561,65 +505,5 @@ var Segment = Base.extend(/** @lends Segment# */{ } } return coords; - }, - - statics: { - // These statics are shared between Segment and Curve, for versions of - // these methods that are implemented in both places. Most of these - // methods relate more to the nature of curves than segments, but since - // curves are made out of segments, and segments are the main path data - // structure, while curves are 2nd class citizens, they are defined here - - isLinear: function(seg1, seg2) { - var l = seg2._point.subtract(seg1._point), - h1 = seg1._handleOut, - h2 = seg2._handleIn; - if (l.isZero()) { - return h1.isZero() && h2.isZero(); - } else if (h1.isCollinear(l) && h2.isCollinear(l)) { - var div = l.dot(l), - p1 = l.dot(h1) / div, - p2 = l.dot(h2) / div; - return p1 >= 0 && p1 <= 1 && p2 <= 0 && p2 >= -1; - } - return false; - }, - - isCollinear: function(seg1, seg2, seg3, seg4) { - // TODO: This assumes !hasHandles(), while isLinear() allows handles! - return seg1._handleOut.isZero() && seg2._handleIn.isZero() - && seg3._handleOut.isZero() && seg4._handleIn.isZero() - && seg2._point.subtract(seg1._point).isCollinear( - seg4._point.subtract(seg3._point)); - }, - - isOrthogonal: function(seg1, seg2, seg3) { - // TODO: This assumes !hasHandles(), while isLinear() allows handles! - return seg1._handleOut.isZero() && seg2._handleIn.isZero() - && seg2._handleOut.isZero() && seg3._handleIn.isZero() - && seg2._point.subtract(seg1._point).isOrthogonal( - seg3._point.subtract(seg2._point)); - }, - - isOrthogonalArc: function(seg1, seg2) { - var handle1 = seg1._handleOut, - handle2 = seg2._handleIn, - kappa = /*#=*/Numerical.KAPPA; - // Look at handle length and the distance to the imaginary corner - // point and see if it their relation is kappa. - if (handle1.isOrthogonal(handle2)) { - var pt1 = seg1._point, - pt2 = seg2._point, - // Find the corner point by intersecting the lines described - // by both handles: - corner = new Line(pt1, handle1, true).intersect( - new Line(pt2, handle2, true), true); - return corner && Numerical.isZero(handle1.getLength() / - corner.subtract(pt1).getLength() - kappa) - && Numerical.isZero(handle2.getLength() / - corner.subtract(pt2).getLength() - kappa); - } - return false; - }, } }); From 9d12a0a82c84aad4b26223dbce466d0276a2d557 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 6 Sep 2015 17:35:27 +0200 Subject: [PATCH 030/280] Rename Curve#isLinear() to #isStraight() Relates to #652 --- src/path/Curve.js | 56 ++++++++++++++++-------------------- src/path/PathItem.Boolean.js | 10 +++---- test/tests/Curve.js | 22 +++++++------- 3 files changed, 40 insertions(+), 48 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index da2e750b..e2e36cad 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -358,12 +358,8 @@ var Curve = Base.extend(/** @lends Curve# */{ * @bean */ getLength: function() { - if (this._length == null) { - // Use simple point distance for straight curves - this._length = this.isLinear() - ? this.getVector().getLength() - : Curve.getLength(this.getValues(), 0, 1); - } + if (this._length == null) + this._length = Curve.getLength(this.getValues(), 0, 1); return this._length; }, @@ -802,7 +798,7 @@ statics: { * @ignore */ }), new function() { // Injection scope for tests - function isLinear(l, h1, h2) { + function isStraight(l, h1, h2) { if (h1.isZero() && h2.isZero()) { // No handles. return true; @@ -843,12 +839,12 @@ statics: { * line that connects the curve's start and end point, not falling * outside of the line. * - * @return {Boolean} {@true if the curve is linear} + * @return {Boolean} {@true if the curve is straight} */ - isLinear: function() { + isStraight: function() { var seg1 = this._segment1, seg2 = this._segment2; - return isLinear(seg2._point.subtract(seg1._point), + return isStraight(seg2._point.subtract(seg1._point), seg1._handleOut, seg2._handleIn); }, @@ -860,15 +856,15 @@ statics: { * @return {Boolean} {@true if the two lines are collinear} */ isCollinear: function(curve) { - return this.isLinear() && curve.isLinear() + return this.isStraight() && curve.isStraight() && this.getVector().isCollinear(curve.getVector()); }, statics: { - isLinear: function(v) { + isStraight: function(v) { var p1x = v[0], p1y = v[1], p2x = v[6], p2y = v[7]; - return isLinear(new Point(p2x - p1x, p2y - p1y), + return isStraight(new Point(p2x - p1x, p2y - p1y), new Point(v[2] - p1x, v[3] - p1y), new Point(v[4] - p2x, v[5] - p2y)); } @@ -1220,12 +1216,8 @@ new function() { // Scope for methods that require private functions a = 0; if (b === undefined) b = 1; - var isZero = Numerical.isZero; - // See if the curve is linear by checking p1 == c1 and p2 == c2 - if (a === 0 && b === 1 - && isZero(v[0] - v[2]) && isZero(v[1] - v[3]) - && isZero(v[6] - v[4]) && isZero(v[7] - v[5])) { - // Straight line + if (a === 0 && b === 1 && Curve.isStraight(v)) { + // The length of straight curves can be calculated more easily. var dx = v[6] - v[0], // p2x - p1x dy = v[7] - v[1]; // p2y - p1y return Math.sqrt(dx * dx + dy * dy); @@ -1525,7 +1517,7 @@ new function() { // Scope for intersection using bezier fat-line clipping */ function addCurveLineIntersections(v1, v2, curve1, curve2, locations, include) { - var flip = Curve.isLinear(v1), + var flip = Curve.isStraight(v1), vc = flip ? v2 : v1, vl = flip ? v1 : v2, lx1 = vl[0], ly1 = vl[1], @@ -1594,10 +1586,10 @@ new function() { // Scope for intersection using bezier fat-line clipping var abs = Math.abs, tolerance = /*#=*/Numerical.TOLERANCE, epsilon = /*#=*/Numerical.EPSILON, - linear1 = Curve.isLinear(v1), - linear2 = Curve.isLinear(v2), - linear = linear1 && linear2; - if (linear) { + straight1 = Curve.isStraight(v1), + straight2 = Curve.isStraight(v2), + straight = straight1 && straight2; + if (straight) { // Linear curves can only overlap if they are collinear, which means // they must be are collinear and any point of curve 1 must be on // curve 2 @@ -1606,8 +1598,8 @@ new function() { // Scope for intersection using bezier fat-line clipping if (!line1.isCollinear(line2) || line1.getDistance(line2.getPoint()) > epsilon) return false; - } else if (linear1 ^ linear2) { - // If one curve is linear, the other curve must be linear, too, + } else if (straight1 ^ straight2) { + // If one curve is straight, the other curve must be straight, too, // otherwise they cannot overlap. return false; } @@ -1650,7 +1642,7 @@ new function() { // Scope for intersection using bezier fat-line clipping // Check if handles of overlapping paths are similar enough. // We could do another check for curve identity here if we find a // better criteria. - if (linear || + if (straight || abs(c2[0] - c1[0]) < epsilon && abs(c2[1] - c1[1]) < epsilon && abs(c2[1] - c1[1]) < epsilon && @@ -1687,8 +1679,8 @@ new function() { // Scope for intersection using bezier fat-line clipping getIntersections: function(v1, v2, c1, c2, locations, include) { if (addOverlap(v1, v2, c1, c2, locations, include)) return locations; - var linear1 = Curve.isLinear(v1), - linear2 = Curve.isLinear(v2), + var straight1 = Curve.isStraight(v1), + straight2 = Curve.isStraight(v2), c1p1 = c1.getPoint1(), c1p2 = c1.getPoint2(), c2p1 = c2.getPoint1(), @@ -1702,10 +1694,10 @@ new function() { // Scope for intersection using bezier fat-line clipping if (c1p1.isClose(c2p2, tolerance)) addLocation(locations, include, c1, 0, c1p1, c2, 1, c1p1); // Determine the correct intersection method based on values of - // linear1 & 2: - (linear1 && linear2 + // straight1 & 2: + (straight1 && straight2 ? addLineIntersection - : linear1 || linear2 + : straight1 || straight2 ? addCurveLineIntersections : addCurveIntersections)( v1, v2, c1, c2, locations, include, diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index ed975df6..8c3328f7 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -291,9 +291,9 @@ PathItem.inject(new function() { } function isHorizontal(curve) { - // Determine if the curve is a horizontal linear curve by checking the + // Determine if the curve is a horizontal straight curve by checking the // slope of it's tangent. - return curve.isLinear() && Math.abs(curve.getTangentAt(0.5, true).y) + return curve.isStraight() && Math.abs(curve.getTangentAt(0.5, true).y) < /*#=*/Numerical.TOLERANCE; } @@ -386,7 +386,7 @@ PathItem.inject(new function() { // curve merely touches the ray towards +-x direction, // but proceeds to the same side of the ray. // This essentially is not a crossing. - if (Numerical.isZero(slope) && !Curve.isLinear(values) + if (Numerical.isZero(slope) && !Curve.isStraight(values) // Does the slope over curve beginning change? || t < tMin && slope * Curve.getTangent( curve.previous.values, 1).y < 0 @@ -785,8 +785,8 @@ Path.inject(/** @lends Path# */{ y1 = v[3], y2 = v[5], y3 = v[7]; - if (Curve.isLinear(v)) { - // Handling linear curves is easy. + if (Curve.isStraight(v)) { + // Handling straight curves is easy. insertCurve(v); } else { // Split the curve at y extrema, to get bezier curves with clear diff --git a/test/tests/Curve.js b/test/tests/Curve.js index 37f57b92..a154dc5e 100644 --- a/test/tests/Curve.js +++ b/test/tests/Curve.js @@ -179,35 +179,35 @@ test('Curve#getLocationAt()', function() { // 'Should return null when point is not on the curve.'); }); -test('Curve#isLinear()', function() { +test('Curve#isStraight()', function() { equals(function() { - return new Curve([100, 100], null, null, [200, 200]).isLinear(); + return new Curve([100, 100], null, null, [200, 200]).isStraight(); }, true); equals(function() { - return new Curve([100, 100], [-50, -50], null, [200, 200]).isLinear(); + return new Curve([100, 100], [-50, -50], null, [200, 200]).isStraight(); }, false); equals(function() { - return new Curve([100, 100], [50, 50], null, [200, 200]).isLinear(); + return new Curve([100, 100], [50, 50], null, [200, 200]).isStraight(); }, true); equals(function() { - return new Curve([100, 100], [50, 50], [-50, -50], [200, 200]).isLinear(); + return new Curve([100, 100], [50, 50], [-50, -50], [200, 200]).isStraight(); }, true); equals(function() { - return new Curve([100, 100], [50, 50], [50, 50], [200, 200]).isLinear(); + return new Curve([100, 100], [50, 50], [50, 50], [200, 200]).isStraight(); }, false); equals(function() { - return new Curve([100, 100], null, [-50, -50], [200, 200]).isLinear(); + return new Curve([100, 100], null, [-50, -50], [200, 200]).isStraight(); }, true); equals(function() { - return new Curve([100, 100], null, [50, 50], [200, 200]).isLinear(); + return new Curve([100, 100], null, [50, 50], [200, 200]).isStraight(); }, false); equals(function() { - return new Curve([100, 100], null, null, [100, 100]).isLinear(); + return new Curve([100, 100], null, null, [100, 100]).isStraight(); }, true); equals(function() { - return new Curve([100, 100], [50, 50], null, [100, 100]).isLinear(); + return new Curve([100, 100], [50, 50], null, [100, 100]).isStraight(); }, false); equals(function() { - return new Curve([100, 100], null, [-50, -50], [100, 100]).isLinear(); + return new Curve([100, 100], null, [-50, -50], [100, 100]).isStraight(); }, false); }); From 31d9e1cd6eb847e53c766d82313cfdc7e17d24d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 6 Sep 2015 17:56:12 +0200 Subject: [PATCH 031/280] Implement Curve#isLinear() to check for parametrical linearity. Along with some unit tests for it. --- src/path/Curve.js | 128 +++++++++++++++++++++++++------------------- test/tests/Curve.js | 12 +++++ 2 files changed, 84 insertions(+), 56 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index e2e36cad..03558218 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -797,8 +797,8 @@ statics: { * @type Rectangle * @ignore */ -}), new function() { // Injection scope for tests - function isStraight(l, h1, h2) { +}), Base.each({ // Injection scope for tests both as instance and static methods + isStraight: function(l, h1, h2) { if (h1.isZero() && h2.isZero()) { // No handles. return true; @@ -814,63 +814,79 @@ statics: { return p1 >= 0 && p1 <= 1 && p2 <= 0 && p2 >= -1; } return false; + }, + + isLinear: function(l, h1, h2) { + var third = l.divide(3); + return h1.equals(third) && h2.negate().equals(third); } +}, function(test, name) { + this[name] = function() { + var seg1 = this._segment1, + seg2 = this._segment2; + return test(seg2._point.subtract(seg1._point), + seg1._handleOut, seg2._handleIn); + }; - return /** @lends Curve# */{ - /** - * {@grouptitle Tests} - * - * Checks if this curve has any curve handles set. - * - * @return {Boolean} {@true if the curve has handles set} - * @see Curve#handle1 - * @see Curve#handle2 - * @see Segment#hasHandles() - * @see Path#hasHandles() - */ - hasHandles: function() { - return !this._segment1._handleOut.isZero() - || !this._segment2._handleIn.isZero(); - }, - - /** - * Checks if this curve appears as a straight line. This can mean that - * it has no handles defined, or that the handles run collinear with the - * line that connects the curve's start and end point, not falling - * outside of the line. - * - * @return {Boolean} {@true if the curve is straight} - */ - isStraight: function() { - var seg1 = this._segment1, - seg2 = this._segment2; - return isStraight(seg2._point.subtract(seg1._point), - seg1._handleOut, seg2._handleIn); - }, - - /** - * Checks if the the two curves describe straight lines that are - * collinear, meaning they run in parallel. - * - * @param {Curve} curve the other curve to check against - * @return {Boolean} {@true if the two lines are collinear} - */ - isCollinear: function(curve) { - return this.isStraight() && curve.isStraight() - && this.getVector().isCollinear(curve.getVector()); - }, - - statics: { - isStraight: function(v) { - var p1x = v[0], p1y = v[1], - p2x = v[6], p2y = v[7]; - return isStraight(new Point(p2x - p1x, p2y - p1y), - new Point(v[2] - p1x, v[3] - p1y), - new Point(v[4] - p2x, v[5] - p2y)); - } - } - } + this.statics[name] = function(v) { + var p1x = v[0], p1y = v[1], + p2x = v[6], p2y = v[7]; + return test(new Point(p2x - p1x, p2y - p1y), + new Point(v[2] - p1x, v[3] - p1y), + new Point(v[4] - p2x, v[5] - p2y)); + }; }, /** @lends Curve# */{ + statics: {}, // Filled in the loop above + + /** + * {@grouptitle Tests} + * + * Checks if this curve has any curve handles set. + * + * @return {Boolean} {@true if the curve has handles set} + * @see Curve#handle1 + * @see Curve#handle2 + * @see Segment#hasHandles() + * @see Path#hasHandles() + */ + hasHandles: function() { + return !this._segment1._handleOut.isZero() + || !this._segment2._handleIn.isZero(); + }, + + /** + * Checks if this curve appears as a straight line. This can mean that + * it has no handles defined, or that the handles run collinear with the + * line that connects the curve's start and end point, not falling + * outside of the line. + * + * @name Curve#isStraight + * @function + * @return {Boolean} {@true if the curve is straight} + */ + + /** + * Checks if this curve is parametrically linear, meaning that its + * handles are positioned at 1/3 and 2/3 of the total length of the + * straight curve. + * + * @name Curve#isLinear + * @function + * @return {Boolean} {@true if the curve is parametrically linear} + */ + + /** + * Checks if the the two curves describe straight lines that are + * collinear, meaning they run in parallel. + * + * @param {Curve} curve the other curve to check against + * @return {Boolean} {@true if the two lines are collinear} + */ + isCollinear: function(curve) { + return this.isStraight() && curve.isStraight() + && this.getVector().isCollinear(curve.getVector()); + } +}), /** @lends Curve# */{ // Explicitly deactivate the creation of beans, as we have functions here // that look like bean getters but actually read arguments. // See #getParameterOf(), #getLocationOf(), #getNearestLocation(), ... diff --git a/test/tests/Curve.js b/test/tests/Curve.js index a154dc5e..2e79f27e 100644 --- a/test/tests/Curve.js +++ b/test/tests/Curve.js @@ -211,3 +211,15 @@ test('Curve#isStraight()', function() { return new Curve([100, 100], null, [-50, -50], [100, 100]).isStraight(); }, false); }); + +test('Curve#isLinear()', function() { + equals(function() { + return new Curve([100, 100], [100 / 3, 100 / 3], [-100 / 3, -100 / 3], [200, 200]).isLinear(); + }, true); + equals(function() { + return new Curve([100, 100], null, null, [100, 100]).isLinear(); + }, true); + equals(function() { + return new Curve([100, 100], null, null, [200, 200]).isLinear(); + }, false); +}); From 98d7703b5c55c91079690c0d8ba5f4d3497eb74b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 6 Sep 2015 18:02:15 +0200 Subject: [PATCH 032/280] Finish implementing Curve#isStraight and #isLinear() Closes #652 --- src/path/Curve.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 03558218..b7187025 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -821,6 +821,7 @@ statics: { return h1.equals(third) && h2.negate().equals(third); } }, function(test, name) { + // Produce the instance version that is called on curve object. this[name] = function() { var seg1 = this._segment1, seg2 = this._segment2; @@ -828,6 +829,7 @@ statics: { seg1._handleOut, seg2._handleIn); }; + // Produce the static version that handles a curve values array. this.statics[name] = function(v) { var p1x = v[0], p1y = v[1], p2x = v[6], p2y = v[7]; @@ -836,10 +838,10 @@ statics: { new Point(v[4] - p2x, v[5] - p2y)); }; }, /** @lends Curve# */{ - statics: {}, // Filled in the loop above + statics: {}, // Filled in the Base.each loop above. /** - * {@grouptitle Tests} + * {@grouptitle Curve Tests} * * Checks if this curve has any curve handles set. * @@ -866,9 +868,9 @@ statics: { */ /** - * Checks if this curve is parametrically linear, meaning that its - * handles are positioned at 1/3 and 2/3 of the total length of the - * straight curve. + * Checks if this curve is parametrically linear, meaning that it is + * straight and its handles are positioned at 1/3 and 2/3 of the total + * length of the curve. * * @name Curve#isLinear * @function From b96036fb032b2f73e99d34d51739f7ad3b6edcbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 9 Sep 2015 06:25:37 +0200 Subject: [PATCH 033/280] Fix strange curve check. Something went wrong in prior refactoring here. Also, no need to check curve beginnings and ends again, just handles. --- src/path/Curve.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index b7187025..22303c1b 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1661,14 +1661,10 @@ new function() { // Scope for intersection using bezier fat-line clipping // We could do another check for curve identity here if we find a // better criteria. if (straight || - abs(c2[0] - c1[0]) < epsilon && - abs(c2[1] - c1[1]) < epsilon && - abs(c2[1] - c1[1]) < epsilon && - abs(c2[3] - c1[3]) < epsilon && abs(c2[2] - c1[2]) < epsilon && - abs(c2[5] - c1[5]) < epsilon && abs(c2[3] - c1[3]) < epsilon && - abs(c2[7] - c1[7]) < epsilon) { + abs(c2[4] - c1[4]) < epsilon && + abs(c2[5] - c1[5]) < epsilon) { // Overlapping parts are identical var t11 = pairs[0][0], t12 = pairs[0][1], From 3f53aa78ce358c3aaddb23bc5fd0deaebc9e78c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 9 Sep 2015 07:26:39 +0200 Subject: [PATCH 034/280] Do not access curve objects for geometry since they might be subdivided. Partial fix for #765 --- src/path/Curve.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 22303c1b..bb7694e1 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1695,10 +1695,10 @@ new function() { // Scope for intersection using bezier fat-line clipping return locations; var straight1 = Curve.isStraight(v1), straight2 = Curve.isStraight(v2), - c1p1 = c1.getPoint1(), - c1p2 = c1.getPoint2(), - c2p1 = c2.getPoint1(), - c2p2 = c2.getPoint2(), + c1p1 = new Point(v1[0], v1[1]), + c1p2 = new Point(v1[6], v1[7]), + c2p1 = new Point(v2[0], v2[1]), + c2p2 = new Point(v2[6], v2[7]), tolerance = /*#=*/Numerical.TOLERANCE; // Handle a special case where if both curves start or end at the // same point, the same end-point case will be handled after we From 1c1e19614eb7ee66394d940c6ce6fd2bf6d07b52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 9 Sep 2015 07:28:08 +0200 Subject: [PATCH 035/280] Avoid matching connected start- and end points when self-intersecting curves. Partial fix for #765. --- src/path/Curve.js | 84 +++++++++++++++++++----------------- src/path/PathItem.Boolean.js | 4 +- src/path/PathItem.js | 50 ++++++++++----------- 3 files changed, 71 insertions(+), 67 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index bb7694e1..eab994ff 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -402,8 +402,8 @@ var Curve = Base.extend(/** @lends Curve# */{ * curves */ getIntersections: function(curve) { - return Curve.filterIntersections(Curve.getIntersections( - this.getValues(), curve.getValues(), this, curve, [])); + return Curve._filterIntersections(Curve._getIntersections( + this.getValues(), curve.getValues(), this, curve, [], {})); }, // TODO: adjustThroughPoint @@ -1317,18 +1317,22 @@ new function() { // Scope for methods that require private functions }, new function() { // Scope for intersection using bezier fat-line clipping - function addLocation(locations, include, curve1, t1, point1, curve2, t2, + function addLocation(locations, param, curve1, t1, point1, curve2, t2, point2) { - var loc = new CurveLocation(curve1, t1, point1, curve2, t2, point2); - if (!include || include(loc)) { + var loc = null, + tMin = /*#=*/Numerical.TOLERANCE, + tMax = 1 - tMin; + if (t1 >= (param.startConnected ? tMin : 0) + && t1 <= (param.endConnected ? tMax : 1)) { + loc = new CurveLocation(curve1, t1, point1, curve2, t2, point2); + if (param.adjust) + param.adjust(loc); locations.push(loc); - } else { - loc = null; } return loc; } - function addCurveIntersections(v1, v2, curve1, curve2, locations, include, + function addCurveIntersections(v1, v2, curve1, curve2, locations, param, tMin, tMax, uMin, uMax, oldTDiff, reverse, recursion) { // Avoid deeper recursion. // NOTE: @iconexperience determined that more than 20 recursions are @@ -1391,19 +1395,19 @@ new function() { // Scope for intersection using bezier fat-line clipping var parts = Curve.subdivide(v1, 0.5), t = tMinNew + (tMaxNew - tMinNew) / 2; addCurveIntersections( - v2, parts[0], curve2, curve1, locations, include, + v2, parts[0], curve2, curve1, locations, param, uMin, uMax, tMinNew, t, tDiff, !reverse, recursion); addCurveIntersections( - v2, parts[1], curve2, curve1, locations, include, + v2, parts[1], curve2, curve1, locations, param, uMin, uMax, t, tMaxNew, tDiff, !reverse, recursion); } else { var parts = Curve.subdivide(v2, 0.5), t = uMin + (uMax - uMin) / 2; addCurveIntersections( - parts[0], v1, curve2, curve1, locations, include, + parts[0], v1, curve2, curve1, locations, param, uMin, t, tMinNew, tMaxNew, tDiff, !reverse, recursion); addCurveIntersections( - parts[1], v1, curve2, curve1, locations, include, + parts[1], v1, curve2, curve1, locations, param, t, uMax, tMinNew, tMaxNew, tDiff, !reverse, recursion); } } else if (Math.max(uMax - uMin, tMaxNew - tMinNew) < tolerance) { @@ -1411,16 +1415,16 @@ new function() { // Scope for intersection using bezier fat-line clipping var t1 = tMinNew + (tMaxNew - tMinNew) / 2, t2 = uMin + (uMax - uMin) / 2; if (reverse) { - addLocation(locations, include, + addLocation(locations, param, curve2, t2, Curve.getPoint(v2, t2), curve1, t1, Curve.getPoint(v1, t1)); } else { - addLocation(locations, include, + addLocation(locations, param, curve1, t1, Curve.getPoint(v1, t1), curve2, t2, Curve.getPoint(v2, t2)); } } else if (tDiff > /*#=*/Numerical.EPSILON) { // Iterate - addCurveIntersections(v2, v1, curve2, curve1, locations, include, + addCurveIntersections(v2, v1, curve2, curve1, locations, param, uMin, uMax, tMinNew, tMaxNew, tDiff, !reverse, recursion); } } @@ -1534,7 +1538,7 @@ new function() { // Scope for intersection using bezier fat-line clipping * and the curve. */ function addCurveLineIntersections(v1, v2, curve1, curve2, locations, - include) { + param) { var flip = Curve.isStraight(v1), vc = flip ? v2 : v1, vl = flip ? v1 : v2, @@ -1574,14 +1578,14 @@ new function() { // Scope for intersection using bezier fat-line clipping var tl = Curve.getParameterOf(rvl, x, 0), t1 = flip ? tl : tc, t2 = flip ? tc : tl; - addLocation(locations, include, + addLocation(locations, param, curve1, t1, Curve.getPoint(v1, t1), curve2, t2, Curve.getPoint(v2, t2)); } } } - function addLineIntersection(v1, v2, curve1, curve2, locations, include) { + function addLineIntersection(v1, v2, curve1, curve2, locations, param) { var point = Line.intersect( v1[0], v1[1], v1[6], v1[7], v2[0], v2[1], v2[6], v2[7]); @@ -1590,7 +1594,7 @@ new function() { // Scope for intersection using bezier fat-line clipping // since they will be used for sorting var x = point.x, y = point.y; - addLocation(locations, include, + addLocation(locations, param, curve1, Curve.getParameterOf(v1, x, y), point, curve2, Curve.getParameterOf(v2, x, y), point); } @@ -1600,7 +1604,7 @@ new function() { // Scope for intersection using bezier fat-line clipping * Code to detect overlaps of intersecting curves by @iconexperience: * https://github.com/paperjs/paper.js/issues/648 */ - function addOverlap(v1, v2, curve1, curve2, locations, include) { + function addOverlap(v1, v2, curve1, curve2, locations, param) { var abs = Math.abs, tolerance = /*#=*/Numerical.TOLERANCE, epsilon = /*#=*/Numerical.EPSILON, @@ -1670,10 +1674,10 @@ new function() { // Scope for intersection using bezier fat-line clipping t12 = pairs[0][1], t21 = pairs[1][0], t22 = pairs[1][1], - loc1 = addLocation(locations, include, + loc1 = addLocation(locations, param, curve1, t11, Curve.getPoint(v1, t11), curve2, t12, Curve.getPoint(v2, t12), true), - loc2 = addLocation(locations, include, + loc2 = addLocation(locations, param, curve1, t21, Curve.getPoint(v1, t21), curve2, t22, Curve.getPoint(v2, t22), true); if (loc1) @@ -1690,8 +1694,9 @@ new function() { // Scope for intersection using bezier fat-line clipping // We need to provide the original left curve reference to the // #getIntersections() calls as it is required to create the resulting // CurveLocation objects. - getIntersections: function(v1, v2, c1, c2, locations, include) { - if (addOverlap(v1, v2, c1, c2, locations, include)) + _getIntersections: function(v1, v2, curve1, curve2, locations, param) { + if (!param.startConnected && !param.endConnected + && addOverlap(v1, v2, curve1, curve2, locations, param)) return locations; var straight1 = Curve.isStraight(v1), straight2 = Curve.isStraight(v2), @@ -1700,39 +1705,38 @@ new function() { // Scope for intersection using bezier fat-line clipping c2p1 = new Point(v2[0], v2[1]), c2p2 = new Point(v2[6], v2[7]), tolerance = /*#=*/Numerical.TOLERANCE; - // Handle a special case where if both curves start or end at the - // same point, the same end-point case will be handled after we - // calculate other intersections within the curve. + // Handle the special case where the first curve's stat-point + // overlaps with the second curve's start- or end-points. if (c1p1.isClose(c2p1, tolerance)) - addLocation(locations, include, c1, 0, c1p1, c2, 0, c1p1); - if (c1p1.isClose(c2p2, tolerance)) - addLocation(locations, include, c1, 0, c1p1, c2, 1, c1p1); - // Determine the correct intersection method based on values of - // straight1 & 2: + addLocation(locations, param, curve1, 0, c1p1, curve2, 0, c1p1); + if (!param.startConnected && c1p1.isClose(c2p2, tolerance)) + addLocation(locations, param, curve1, 0, c1p1, curve2, 1, c1p1); + // Determine the correct intersection method based on whether one or + // curves are straight lines: (straight1 && straight2 ? addLineIntersection : straight1 || straight2 ? addCurveLineIntersections : addCurveIntersections)( - v1, v2, c1, c2, locations, include, + v1, v2, curve1, curve2, locations, param, // Define the defaults for these parameters of // addCurveIntersections(): // tMin, tMax, uMin, uMax, oldTDiff, reverse, recursion 0, 1, 0, 1, 0, false, 0); - // Handle the special case where c1's end-point overlap with - // c2's points. - if (c1p2.isClose(c2p1, tolerance)) - addLocation(locations, include, c1, 1, c1p2, c2, 0, c1p2); + // Handle the special case where the first curve's end-point + // overlaps with the second curve's start- or end-points. + if (!param.endConnected && c1p2.isClose(c2p1, tolerance)) + addLocation(locations, param, curve1, 1, c1p2, curve2, 0, c1p2); if (c1p2.isClose(c2p2, tolerance)) - addLocation(locations, include, c1, 1, c1p2, c2, 1, c1p2); + addLocation(locations, param, curve1, 1, c1p2, curve2, 1, c1p2); return locations; }, - filterIntersections: function(locations, expand) { + _filterIntersections: function(locations, expand) { var last = locations.length - 1, tMax = 1 - /*#=*/Numerical.TOLERANCE; // Merge intersections very close to the end of a curve to the - // beginning of the next curve. + // beginning of the next curve, so we can compare them. for (var i = last; i >= 0; i--) { var loc = locations[i], next; diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 8c3328f7..039c5abb 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -76,7 +76,7 @@ PathItem.inject(new function() { _path2.reverse(); // Split curves at intersections on both paths. Note that for self // intersection, _path2 will be null and getIntersections() handles it. - splitPath(Curve.filterIntersections( + splitPath(Curve._filterIntersections( _path1._getIntersections(_path2, null, []), true)); /* console.time('inter'); @@ -88,7 +88,7 @@ PathItem.inject(new function() { _path2._getIntersections(null, null, locations); console.timeEnd('self'); } - splitPath(Curve.filterIntersections(locations, true)); + splitPath(Curve._filterIntersections(locations, true)); */ var chain = [], segments = [], diff --git a/src/path/PathItem.js b/src/path/PathItem.js index 2a43f215..3dac84a7 100644 --- a/src/path/PathItem.js +++ b/src/path/PathItem.js @@ -63,31 +63,30 @@ var PathItem = Item.extend(/** @lends PathItem# */{ // intersections. // NOTE: The hidden argument _matrix is used internally to override the // passed path's transformation matrix. - return Curve.filterIntersections(this._getIntersections( + return Curve._filterIntersections(this._getIntersections( this !== path ? path : null, _matrix, [])); }, _getIntersections: function(path, matrix, locations, returnFirst) { - var curves1 = this.getCurves(), - curves2 = path ? path.getCurves() : curves1, + var self = !path, // self-intersections? + curves1 = this.getCurves(), + curves2 = self ? curves1 : path.getCurves(), matrix1 = this._matrix.orNullIfIdentity(), - matrix2 = path ? (matrix || path._matrix).orNullIfIdentity() - : matrix1, + matrix2 = self ? matrix1 + : (matrix || path._matrix).orNullIfIdentity(), length1 = curves1.length, length2 = path ? curves2.length : length1, - values2 = [], - tMin = /*#=*/Numerical.TOLERANCE, - tMax = 1 - tMin; + values2 = []; // First check the bounds of the two paths. If they don't intersect, // we don't need to iterate through their curves. if (path && !this.getBounds(matrix1).touches(path.getBounds(matrix2))) - return []; + return locations; for (var i = 0; i < length2; i++) values2[i] = curves2[i].getValues(matrix2); for (var i = 0; i < length1; i++) { var curve1 = curves1[i], - values1 = path ? curve1.getValues(matrix1) : values2[i]; - if (!path) { + values1 = self ? values2[i] : curve1.getValues(matrix1); + if (self) { // First check for self-intersections within the same curve var seg1 = curve1.getSegment1(), seg2 = curve1.getSegment2(), @@ -103,15 +102,17 @@ var PathItem = Item.extend(/** @lends PathItem# */{ // Self intersecting is found by dividing the curve in two // and and then applying the normal curve intersection code. var parts = Curve.subdivide(values1); - Curve.getIntersections( - parts[0], parts[1], curve1, curve1, locations, - function(loc) { - if (loc._parameter <= tMax) { + Curve._getIntersections( + parts[0], parts[1], curve1, curve1, locations, { + // Only possible if there is only one curve: + startConnected: length1 === 1, + // After splitting, the end is always connected: + endConnected: true, + adjust: function(loc) { // Since the curve was split above, we need to // adjust the parameters for both locations. loc._parameter /= 2; loc._parameter2 = 0.5 + loc._parameter2 / 2; - return true; } } ); @@ -119,20 +120,19 @@ var PathItem = Item.extend(/** @lends PathItem# */{ } // Check for intersections with other curves. For self intersection, // we can start at i + 1 instead of 0 - for (var j = path ? 0 : i + 1; j < length2; j++) { + for (var j = self ? i + 1 : 0; j < length2; j++) { // There might be already one location from the above // self-intersection check: if (returnFirst && locations.length) break; - Curve.getIntersections( + // Avoid end point intersections on consecutive curves when + // self intersecting. + Curve._getIntersections( values1, values2[j], curve1, curves2[j], locations, - // Avoid end point intersections on consecutive curves when - // self intersecting. - !path && (j === i + 1 || j === length2 - 1 && i === 0) - && function(loc) { - var t = loc._parameter; - return t >= tMin && t <= tMax; - } + self ? { + startConnected: j === length2 - 1 && i === 0, + endConnected: j === i + 1 + } : {} ); } } From dae8bb630b5966e5d14426d070e60e1319c00ccb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 9 Sep 2015 07:34:28 +0200 Subject: [PATCH 036/280] Avoid checking curves if completely out of control bounds. This leads to a huge speed increase! Relates to #765 --- src/path/Curve.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index eab994ff..2f33791a 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1695,7 +1695,20 @@ new function() { // Scope for intersection using bezier fat-line clipping // #getIntersections() calls as it is required to create the resulting // CurveLocation objects. _getIntersections: function(v1, v2, curve1, curve2, locations, param) { - if (!param.startConnected && !param.endConnected + var min = Math.min, + max = Math.max; + // Avoid checking curves if completely out of control bounds. + // Also detect and handle overlaps. + if (!( + max(v1[0], v1[2], v1[4], v1[6]) >= + min(v2[0], v2[2], v2[4], v2[6]) && + max(v1[1], v1[3], v1[5], v1[7]) >= + min(v2[1], v2[3], v2[5], v2[7]) && + min(v1[0], v1[2], v1[4], v1[6]) <= + max(v2[0], v2[2], v2[4], v2[6]) && + min(v1[1], v1[3], v1[5], v1[7]) <= + max(v2[1], v2[3], v2[5], v2[7]) + ) || !param.startConnected && !param.endConnected && addOverlap(v1, v2, curve1, curve2, locations, param)) return locations; var straight1 = Curve.isStraight(v1), From 332b09c5345134daa8c7cd1b35ab8bc52fac9cd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 9 Sep 2015 07:46:48 +0200 Subject: [PATCH 037/280] More curve interesection optimizations. Only evaluate points if locations are actually added. --- src/path/Curve.js | 45 ++++++++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 2f33791a..23035c83 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1317,14 +1317,17 @@ new function() { // Scope for methods that require private functions }, new function() { // Scope for intersection using bezier fat-line clipping - function addLocation(locations, param, curve1, t1, point1, curve2, t2, - point2) { + function addLocation(locations, param, + curve1, t1, point1, v1, + curve2, t2, point2, v2) { var loc = null, tMin = /*#=*/Numerical.TOLERANCE, tMax = 1 - tMin; if (t1 >= (param.startConnected ? tMin : 0) && t1 <= (param.endConnected ? tMax : 1)) { - loc = new CurveLocation(curve1, t1, point1, curve2, t2, point2); + loc = new CurveLocation( + curve1, t1, point1 || Curve.getPoint(v1, t1), + curve2, t2, point2 || Curve.getPoint(v2, t2)); if (param.adjust) param.adjust(loc); locations.push(loc); @@ -1416,12 +1419,12 @@ new function() { // Scope for intersection using bezier fat-line clipping t2 = uMin + (uMax - uMin) / 2; if (reverse) { addLocation(locations, param, - curve2, t2, Curve.getPoint(v2, t2), - curve1, t1, Curve.getPoint(v1, t1)); + curve2, t2, null, v2, + curve1, t1, null, v1); } else { addLocation(locations, param, - curve1, t1, Curve.getPoint(v1, t1), - curve2, t2, Curve.getPoint(v2, t2)); + curve1, t1, null, v1, + curve2, t2, null, v2); } } else if (tDiff > /*#=*/Numerical.EPSILON) { // Iterate addCurveIntersections(v2, v1, curve2, curve1, locations, param, @@ -1579,8 +1582,8 @@ new function() { // Scope for intersection using bezier fat-line clipping t1 = flip ? tl : tc, t2 = flip ? tc : tl; addLocation(locations, param, - curve1, t1, Curve.getPoint(v1, t1), - curve2, t2, Curve.getPoint(v2, t2)); + curve1, t1, null, v1, + curve2, t2, null, v1); } } } @@ -1595,8 +1598,8 @@ new function() { // Scope for intersection using bezier fat-line clipping var x = point.x, y = point.y; addLocation(locations, param, - curve1, Curve.getParameterOf(v1, x, y), point, - curve2, Curve.getParameterOf(v2, x, y), point); + curve1, Curve.getParameterOf(v1, x, y), point, null, + curve2, Curve.getParameterOf(v2, x, y), point, null); } } @@ -1675,11 +1678,11 @@ new function() { // Scope for intersection using bezier fat-line clipping t21 = pairs[1][0], t22 = pairs[1][1], loc1 = addLocation(locations, param, - curve1, t11, Curve.getPoint(v1, t11), - curve2, t12, Curve.getPoint(v2, t12), true), + curve1, t11, null, v1, + curve2, t12, null, v2), loc2 = addLocation(locations, param, - curve1, t21, Curve.getPoint(v1, t21), - curve2, t22, Curve.getPoint(v2, t22), true); + curve1, t21, null, v1, + curve2, t22, null, v2); if (loc1) loc1._overlap = true; if (loc2) @@ -1721,9 +1724,11 @@ new function() { // Scope for intersection using bezier fat-line clipping // Handle the special case where the first curve's stat-point // overlaps with the second curve's start- or end-points. if (c1p1.isClose(c2p1, tolerance)) - addLocation(locations, param, curve1, 0, c1p1, curve2, 0, c1p1); + addLocation(locations, param, curve1, 0, c1p1, null, + curve2, 0, c1p1, null); if (!param.startConnected && c1p1.isClose(c2p2, tolerance)) - addLocation(locations, param, curve1, 0, c1p1, curve2, 1, c1p1); + addLocation(locations, param, curve1, 0, c1p1, null, + curve2, 1, c1p1, null); // Determine the correct intersection method based on whether one or // curves are straight lines: (straight1 && straight2 @@ -1739,9 +1744,11 @@ new function() { // Scope for intersection using bezier fat-line clipping // Handle the special case where the first curve's end-point // overlaps with the second curve's start- or end-points. if (!param.endConnected && c1p2.isClose(c2p1, tolerance)) - addLocation(locations, param, curve1, 1, c1p2, curve2, 0, c1p2); + addLocation(locations, param, curve1, 1, c1p2, null, + curve2, 0, c1p2, null); if (c1p2.isClose(c2p2, tolerance)) - addLocation(locations, param, curve1, 1, c1p2, curve2, 1, c1p2); + addLocation(locations, param, curve1, 1, c1p2, null, + curve2, 1, c1p2, null); return locations; }, From 70f8f1912fceee7b30f2636c7e4845ef3ce48494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 9 Sep 2015 08:06:59 +0200 Subject: [PATCH 038/280] Some code clean-up. --- src/path/Curve.js | 103 +++++++++++++++++++--------------------------- 1 file changed, 43 insertions(+), 60 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 23035c83..a51ee2dd 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1317,17 +1317,19 @@ new function() { // Scope for methods that require private functions }, new function() { // Scope for intersection using bezier fat-line clipping - function addLocation(locations, param, - curve1, t1, point1, v1, - curve2, t2, point2, v2) { + function addLocation(locations, param, v1, c1, t1, p1, v2, c2, t2, p2) { var loc = null, tMin = /*#=*/Numerical.TOLERANCE, tMax = 1 - tMin; + if (t1 == null) + t1 = Curve.getParameterOf(v1, p1.x, p1.y); if (t1 >= (param.startConnected ? tMin : 0) && t1 <= (param.endConnected ? tMax : 1)) { + if (t2 == null) + t2 = Curve.getParameterOf(v2, p2.x, p2.y); loc = new CurveLocation( - curve1, t1, point1 || Curve.getPoint(v1, t1), - curve2, t2, point2 || Curve.getPoint(v2, t2)); + c1, t1, p1 || Curve.getPoint(v1, t1), + c2, t2, p2 || Curve.getPoint(v2, t2)); if (param.adjust) param.adjust(loc); locations.push(loc); @@ -1335,7 +1337,7 @@ new function() { // Scope for intersection using bezier fat-line clipping return loc; } - function addCurveIntersections(v1, v2, curve1, curve2, locations, param, + function addCurveIntersections(v1, v2, c1, c2, locations, param, tMin, tMax, uMin, uMax, oldTDiff, reverse, recursion) { // Avoid deeper recursion. // NOTE: @iconexperience determined that more than 20 recursions are @@ -1398,36 +1400,30 @@ new function() { // Scope for intersection using bezier fat-line clipping var parts = Curve.subdivide(v1, 0.5), t = tMinNew + (tMaxNew - tMinNew) / 2; addCurveIntersections( - v2, parts[0], curve2, curve1, locations, param, + v2, parts[0], c2, c1, locations, param, uMin, uMax, tMinNew, t, tDiff, !reverse, recursion); addCurveIntersections( - v2, parts[1], curve2, curve1, locations, param, + v2, parts[1], c2, c1, locations, param, uMin, uMax, t, tMaxNew, tDiff, !reverse, recursion); } else { var parts = Curve.subdivide(v2, 0.5), t = uMin + (uMax - uMin) / 2; addCurveIntersections( - parts[0], v1, curve2, curve1, locations, param, + parts[0], v1, c2, c1, locations, param, uMin, t, tMinNew, tMaxNew, tDiff, !reverse, recursion); addCurveIntersections( - parts[1], v1, curve2, curve1, locations, param, + parts[1], v1, c2, c1, locations, param, t, uMax, tMinNew, tMaxNew, tDiff, !reverse, recursion); } } else if (Math.max(uMax - uMin, tMaxNew - tMinNew) < tolerance) { // We have isolated the intersection with sufficient precision var t1 = tMinNew + (tMaxNew - tMinNew) / 2, t2 = uMin + (uMax - uMin) / 2; - if (reverse) { - addLocation(locations, param, - curve2, t2, null, v2, - curve1, t1, null, v1); - } else { - addLocation(locations, param, - curve1, t1, null, v1, - curve2, t2, null, v2); - } + addLocation(locations, param, + reverse ? v2 : v1, reverse ? c2 : c1, reverse ? t2 : t1, null, + reverse ? v1 : v2, reverse ? c1 : c2, reverse ? t1 : t2, null); } else if (tDiff > /*#=*/Numerical.EPSILON) { // Iterate - addCurveIntersections(v2, v1, curve2, curve1, locations, param, + addCurveIntersections(v2, v1, c2, c1, locations, param, uMin, uMax, tMinNew, tMaxNew, tDiff, !reverse, recursion); } } @@ -1540,7 +1536,7 @@ new function() { // Scope for intersection using bezier fat-line clipping * line is on the X axis, and solve the implicit equations for the X axis * and the curve. */ - function addCurveLineIntersections(v1, v2, curve1, curve2, locations, + function addCurveLineIntersections(v1, v2, c1, c2, locations, param) { var flip = Curve.isStraight(v1), vc = flip ? v2 : v1, @@ -1581,25 +1577,18 @@ new function() { // Scope for intersection using bezier fat-line clipping var tl = Curve.getParameterOf(rvl, x, 0), t1 = flip ? tl : tc, t2 = flip ? tc : tl; - addLocation(locations, param, - curve1, t1, null, v1, - curve2, t2, null, v1); + addLocation(locations, param, v1, c1, t1, null, + v2, c2, t2, null); } } } - function addLineIntersection(v1, v2, curve1, curve2, locations, param) { - var point = Line.intersect( + function addLineIntersection(v1, v2, c1, c2, locations, param) { + var pt = Line.intersect( v1[0], v1[1], v1[6], v1[7], v2[0], v2[1], v2[6], v2[7]); - if (point) { - // We need to return the parameters for the intersection, - // since they will be used for sorting - var x = point.x, - y = point.y; - addLocation(locations, param, - curve1, Curve.getParameterOf(v1, x, y), point, null, - curve2, Curve.getParameterOf(v2, x, y), point, null); + if (pt) { + addLocation(locations, param, v1, c1, null, pt, v2, c2, null, pt); } } @@ -1607,7 +1596,7 @@ new function() { // Scope for intersection using bezier fat-line clipping * Code to detect overlaps of intersecting curves by @iconexperience: * https://github.com/paperjs/paper.js/issues/648 */ - function addOverlap(v1, v2, curve1, curve2, locations, param) { + function addOverlap(v1, v2, c1, c2, locations, param) { var abs = Math.abs, tolerance = /*#=*/Numerical.TOLERANCE, epsilon = /*#=*/Numerical.EPSILON, @@ -1656,33 +1645,31 @@ new function() { // Scope for intersection using bezier fat-line clipping // We only have to check if the handles are the same, too. if (pairs.length === 2) { // create values for overlapping part of each curve - var c1 = Curve.getPart(v[0], pairs[0][0], pairs[1][0]), - c2 = Curve.getPart(v[1], Math.min(pairs[0][1], pairs[1][1]), + var p1 = Curve.getPart(v[0], pairs[0][0], pairs[1][0]), + p2 = Curve.getPart(v[1], Math.min(pairs[0][1], pairs[1][1]), Math.max(pairs[0][1], pairs[1][1])); // Reverse values of second curve if necessary - // if (abs(c1[0] - c2[6]) < epsilon && abs(c1[1] - c2[7]) < epsilon) { + // if (abs(p1[0] - p2[6]) < epsilon && abs(p1[1] - p2[7]) < epsilon) { if (pairs[0][1] > pairs[1][1]) { - c2 = [c2[6], c2[7], c2[4], c2[5], c2[2], c2[3], c2[0], c2[1]]; + p2 = [p2[6], p2[7], p2[4], p2[5], p2[2], p2[3], p2[0], p2[1]]; } // Check if handles of overlapping paths are similar enough. // We could do another check for curve identity here if we find a // better criteria. if (straight || - abs(c2[2] - c1[2]) < epsilon && - abs(c2[3] - c1[3]) < epsilon && - abs(c2[4] - c1[4]) < epsilon && - abs(c2[5] - c1[5]) < epsilon) { + abs(p2[2] - p1[2]) < epsilon && + abs(p2[3] - p1[3]) < epsilon && + abs(p2[4] - p1[4]) < epsilon && + abs(p2[5] - p1[5]) < epsilon) { // Overlapping parts are identical var t11 = pairs[0][0], t12 = pairs[0][1], t21 = pairs[1][0], t22 = pairs[1][1], - loc1 = addLocation(locations, param, - curve1, t11, null, v1, - curve2, t12, null, v2), - loc2 = addLocation(locations, param, - curve1, t21, null, v1, - curve2, t22, null, v2); + loc1 = addLocation(locations, param, v1, c1, t11, null, + v2, c2, t12, null), + loc2 = addLocation(locations, param, v1, c1, t21, null, + v2, c2, t22, null); if (loc1) loc1._overlap = true; if (loc2) @@ -1697,7 +1684,7 @@ new function() { // Scope for intersection using bezier fat-line clipping // We need to provide the original left curve reference to the // #getIntersections() calls as it is required to create the resulting // CurveLocation objects. - _getIntersections: function(v1, v2, curve1, curve2, locations, param) { + _getIntersections: function(v1, v2, c1, c2, locations, param) { var min = Math.min, max = Math.max; // Avoid checking curves if completely out of control bounds. @@ -1712,7 +1699,7 @@ new function() { // Scope for intersection using bezier fat-line clipping min(v1[1], v1[3], v1[5], v1[7]) <= max(v2[1], v2[3], v2[5], v2[7]) ) || !param.startConnected && !param.endConnected - && addOverlap(v1, v2, curve1, curve2, locations, param)) + && addOverlap(v1, v2, c1, c2, locations, param)) return locations; var straight1 = Curve.isStraight(v1), straight2 = Curve.isStraight(v2), @@ -1724,11 +1711,9 @@ new function() { // Scope for intersection using bezier fat-line clipping // Handle the special case where the first curve's stat-point // overlaps with the second curve's start- or end-points. if (c1p1.isClose(c2p1, tolerance)) - addLocation(locations, param, curve1, 0, c1p1, null, - curve2, 0, c1p1, null); + addLocation(locations, param, v1, c1, 0, c1p1, v2, c2, 0, c1p1); if (!param.startConnected && c1p1.isClose(c2p2, tolerance)) - addLocation(locations, param, curve1, 0, c1p1, null, - curve2, 1, c1p1, null); + addLocation(locations, param, v1, c1, 0, c1p1, v2, c2, 1, c1p1); // Determine the correct intersection method based on whether one or // curves are straight lines: (straight1 && straight2 @@ -1736,7 +1721,7 @@ new function() { // Scope for intersection using bezier fat-line clipping : straight1 || straight2 ? addCurveLineIntersections : addCurveIntersections)( - v1, v2, curve1, curve2, locations, param, + v1, v2, c1, c2, locations, param, // Define the defaults for these parameters of // addCurveIntersections(): // tMin, tMax, uMin, uMax, oldTDiff, reverse, recursion @@ -1744,11 +1729,9 @@ new function() { // Scope for intersection using bezier fat-line clipping // Handle the special case where the first curve's end-point // overlaps with the second curve's start- or end-points. if (!param.endConnected && c1p2.isClose(c2p1, tolerance)) - addLocation(locations, param, curve1, 1, c1p2, null, - curve2, 0, c1p2, null); + addLocation(locations, param, v1, c1, 1, c1p2, v2, c2, 0, c1p2); if (c1p2.isClose(c2p2, tolerance)) - addLocation(locations, param, curve1, 1, c1p2, null, - curve2, 1, c1p2, null); + addLocation(locations, param, v1, c1, 1, c1p2, v2, c2, 1, c1p2); return locations; }, From 155442e70615fa28bc4b29596d9f7993f4cab495 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 9 Sep 2015 08:12:03 +0200 Subject: [PATCH 039/280] Increase readability of convex-hull check in self-intersection code. --- src/path/PathItem.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/path/PathItem.js b/src/path/PathItem.js index 3dac84a7..bd8c9772 100644 --- a/src/path/PathItem.js +++ b/src/path/PathItem.js @@ -90,15 +90,17 @@ var PathItem = Item.extend(/** @lends PathItem# */{ // First check for self-intersections within the same curve var seg1 = curve1.getSegment1(), seg2 = curve1.getSegment2(), + p1 = seg1._point, + p2 = seg2._point, h1 = seg1._handleOut, - h2 = seg2._handleIn; + h2 = seg2._handleIn, + l1 = new Line(p1.subtract(h1), p1.add(h1)), + l2 = new Line(p2.subtract(h2), p1.add(h2)); // Check if extended handles of endpoints of this curve // intersects each other. We cannot have a self intersection // within this curve if they don't intersect due to convex-hull // property. - if (new Line(seg1._point.subtract(h1), h1.multiply(2), true) - .intersect(new Line(seg2._point.subtract(h2), - h2.multiply(2), true), false)) { + if (l1.intersect(l2, false)) { // Self intersecting is found by dividing the curve in two // and and then applying the normal curve intersection code. var parts = Curve.subdivide(values1); From abf70378fe3aba34cc98bfe88b9713465c9d0314 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 9 Sep 2015 08:15:43 +0200 Subject: [PATCH 040/280] Some more code fixes. One was breaking unit tests. --- src/path/Curve.js | 2 +- src/path/PathItem.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index a51ee2dd..32b6eb25 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1328,7 +1328,7 @@ new function() { // Scope for intersection using bezier fat-line clipping if (t2 == null) t2 = Curve.getParameterOf(v2, p2.x, p2.y); loc = new CurveLocation( - c1, t1, p1 || Curve.getPoint(v1, t1), + c1, t1, p1 || Curve.getPoint(v1, t1), c2, t2, p2 || Curve.getPoint(v2, t2)); if (param.adjust) param.adjust(loc); diff --git a/src/path/PathItem.js b/src/path/PathItem.js index bd8c9772..a4346539 100644 --- a/src/path/PathItem.js +++ b/src/path/PathItem.js @@ -104,8 +104,8 @@ var PathItem = Item.extend(/** @lends PathItem# */{ // Self intersecting is found by dividing the curve in two // and and then applying the normal curve intersection code. var parts = Curve.subdivide(values1); - Curve._getIntersections( - parts[0], parts[1], curve1, curve1, locations, { + Curve._getIntersections(parts[0], parts[1], curve1, curve1, + locations, { // Only possible if there is only one curve: startConnected: length1 === 1, // After splitting, the end is always connected: From 78e0bae6aae34dbc16fa423a8a884524dc929eca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 9 Sep 2015 08:24:02 +0200 Subject: [PATCH 041/280] Activate code that handles self-intersection directly now. Relates to #765, #761 --- src/path/PathItem.Boolean.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 039c5abb..35e64017 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -76,20 +76,22 @@ PathItem.inject(new function() { _path2.reverse(); // Split curves at intersections on both paths. Note that for self // intersection, _path2 will be null and getIntersections() handles it. - splitPath(Curve._filterIntersections( - _path1._getIntersections(_path2, null, []), true)); - /* - console.time('inter'); + + // Without support for self-intersection + // splitPath(Curve._filterIntersections( + // _path1._getIntersections(_path2, null, []), true)); + + // console.time('inter'); var locations = _path1._getIntersections(_path2, null, []); - console.timeEnd('inter'); + // console.timeEnd('inter'); if (_path2 && false) { - console.time('self'); + // console.time('self'); _path1._getIntersections(null, null, locations); _path2._getIntersections(null, null, locations); - console.timeEnd('self'); + // console.timeEnd('self'); } splitPath(Curve._filterIntersections(locations, true)); - */ + var chain = [], segments = [], // Aggregate of all curves in both operands, monotonic in y From 04452730dd37cce383450dc602b8c2c35e134045 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 9 Sep 2015 17:17:49 +0200 Subject: [PATCH 042/280] Simplify CurveLocation data structures. Directly creating and linking intersections simplifies things a lot. --- examples/Scripts/BooleanOperations.html | 2 +- src/path/Curve.js | 72 ++++++++++--------------- src/path/CurveLocation.js | 72 +++++++++++++------------ src/path/PathItem.Boolean.js | 10 ++-- src/path/PathItem.js | 8 +-- 5 files changed, 74 insertions(+), 90 deletions(-) diff --git a/examples/Scripts/BooleanOperations.html b/examples/Scripts/BooleanOperations.html index e12356ff..7520ab6c 100644 --- a/examples/Scripts/BooleanOperations.html +++ b/examples/Scripts/BooleanOperations.html @@ -269,7 +269,7 @@ // // annotatePath(pathB) // // pathB.translate([ 300, 0 ]); // // pathB.segments.filter(function(a) { return a._ixPair; }).map( - // // function(a) { a._ixPair.getIntersection()._segment.selected = true; }); + // // function(a) { a._ixPair.intersection._segment.selected = true; }); // console.time('unite'); // var nup = unite(pathA, pathB); diff --git a/src/path/Curve.js b/src/path/Curve.js index 32b6eb25..e0b47412 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1000,8 +1000,7 @@ statics: { step /= 2; } var pt = Curve.getPoint(values, minT); - return new CurveLocation(this, minT, pt, null, null, null, - point.getDistance(pt)); + return new CurveLocation(this, minT, pt, point.getDistance(pt)); }, /** @@ -1317,7 +1316,8 @@ new function() { // Scope for methods that require private functions }, new function() { // Scope for intersection using bezier fat-line clipping - function addLocation(locations, param, v1, c1, t1, p1, v2, c2, t2, p2) { + function addLocation(locations, param, v1, c1, t1, p1, v2, c2, t2, p2, + overlap) { var loc = null, tMin = /*#=*/Numerical.TOLERANCE, tMax = 1 - tMin; @@ -1327,11 +1327,16 @@ new function() { // Scope for intersection using bezier fat-line clipping && t1 <= (param.endConnected ? tMax : 1)) { if (t2 == null) t2 = Curve.getParameterOf(v2, p2.x, p2.y); - loc = new CurveLocation( - c1, t1, p1 || Curve.getPoint(v1, t1), - c2, t2, p2 || Curve.getPoint(v2, t2)); - if (param.adjust) - param.adjust(loc); + var reparametrize = param.reparametrize; + if (reparametrize) { + var res = reparametrize(t1, t2); + t1 = res.t1; + t2 = res.t2; + } + loc = new CurveLocation(c1, t1, p1 || Curve.getPoint(v1, t1), + null, overlap, + new CurveLocation(c2, t2, p2 || Curve.getPoint(v2, t2), + null, overlap)); locations.push(loc); } return loc; @@ -1662,18 +1667,10 @@ new function() { // Scope for intersection using bezier fat-line clipping abs(p2[4] - p1[4]) < epsilon && abs(p2[5] - p1[5]) < epsilon) { // Overlapping parts are identical - var t11 = pairs[0][0], - t12 = pairs[0][1], - t21 = pairs[1][0], - t22 = pairs[1][1], - loc1 = addLocation(locations, param, v1, c1, t11, null, - v2, c2, t12, null), - loc2 = addLocation(locations, param, v1, c1, t21, null, - v2, c2, t22, null); - if (loc1) - loc1._overlap = true; - if (loc2) - loc2._overlap = true; + addLocation(locations, param, v1, c1, pairs[0][0], null, + v2, c2, pairs[0][1], null, true), + addLocation(locations, param, v1, c1, pairs[1][0], null, + v2, c2, pairs[1][1], null, true); return true; } } @@ -1736,37 +1733,22 @@ new function() { // Scope for intersection using bezier fat-line clipping }, _filterIntersections: function(locations, expand) { - var last = locations.length - 1, - tMax = 1 - /*#=*/Numerical.TOLERANCE; - // Merge intersections very close to the end of a curve to the - // beginning of the next curve, so we can compare them. - for (var i = last; i >= 0; i--) { - var loc = locations[i], - next; - if (loc._parameter >= tMax && (next = loc._curve.getNext())) { - loc._parameter = 0; - loc._curve = next; - } - if (loc._parameter2 >= tMax && (next = loc._curve2.getNext())) { - loc._parameter2 = 0; - loc._curve2 = next; - } - } - + var last = locations.length - 1; if (last > 0) { CurveLocation.sort(locations); - // Filter out duplicate locations, but preserve _overlap setting - // among all duplicated (only one of them will have it defined). + // Filter out duplicate locations, but preserve _overlap among + // all duplicated (only one of them will have it defined). var i = last, loc = locations[i]; while(--i >= 0) { var prev = locations[i]; if (prev.equals(loc)) { - locations.splice(i + 1, 1); // Remove loc. - // Preserve overlap setting. - var overlap = loc._overlap; - if (overlap) - prev._overlap = overlap; + locations.splice(i + 1, 1); // Remove location. + // Preserve _overlap for both linked intersections. + var over = loc._overlap; + if (over) { + prev._overlap = prev._intersection._overlap = over; + } last--; } loc = prev; @@ -1774,7 +1756,7 @@ new function() { // Scope for intersection using bezier fat-line clipping } if (expand) { for (var i = last; i >= 0; i--) - locations.push(locations[i].getIntersection()); + locations.push(locations[i]._intersection); CurveLocation.sort(locations); } return locations; diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index 2bec1df0..fd3e056c 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -41,8 +41,17 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ * @param {Number} parameter * @param {Point} [point] */ - initialize: function CurveLocation(curve, parameter, point, _curve2, - _parameter2, _point2, _distance) { + initialize: function CurveLocation(curve, parameter, point, + _distance, _overlap, _intersection) { + // Merge intersections very close to the end of a curve to the + // beginning of the next curve. + if (parameter >= 1 - /*#=*/Numerical.TOLERANCE) { + var next = curve.getNext(); + if (next) { + parameter = 0; + curve = next; + } + } // Define this CurveLocation's unique id. // NOTE: We do not use the same pool as the rest of the library here, // since this is only required to be unique at runtime among other @@ -53,10 +62,14 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ this._curve = curve; this._parameter = parameter; this._point = point || curve.getPointAt(parameter, true); - this._curve2 = _curve2; - this._parameter2 = _parameter2; - this._point2 = _point2; this._distance = _distance; + this._overlap = _overlap; + this._intersection = _intersection; + this._other = false; + if (_intersection) { + _intersection._intersection = this; + _intersection._other = true; + } // Also store references to segment1 and segment2, in case path // splitting / dividing is going to happen, in which case the segments // can be used to determine the new curves, see #getCurve(true) @@ -209,17 +222,7 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ * @bean */ getIntersection: function() { - var intersection = this._intersection; - if (!intersection && this._curve2) { - // If we have the parameter on the other curve use that for - // intersection rather than the point. - this._intersection = intersection = new CurveLocation(this._curve2, - this._parameter2, this._point2 || this._point); - intersection._overlap = this._overlap; - intersection._intersection = this; - intersection._other = true; - } - return intersection; + return this._intersection; }, /** @@ -275,23 +278,20 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ * @param {CurveLocation} location * @return {Boolean} {@true if the locations are equal} */ - equals: function(loc) { - var abs = Math.abs, - // Use the same tolerance for curve time parameter comparisons as - // in Curve.js when considering two locations the same. - tolerance = /*#=*/Numerical.TOLERANCE; + equals: function(loc, _ignoreIntersection) { return this === loc - || loc instanceof CurveLocation - // Call getCurve() and getParameter() to keep in sync - && this.getCurve() === loc.getCurve() - && abs(this.getParameter() - loc.getParameter()) < tolerance - // _curve2/_parameter2 are only used for Boolean operations - // and don't need syncing there. - // TODO: That's not quite true though... Rework this! - && this._curve2 === loc._curve2 - && abs((this._parameter2 || 0) - (loc._parameter2 || 0)) - < tolerance - || false; + || loc instanceof CurveLocation + // Call getCurve() and getParameter() to keep in sync + && this.getCurve() === loc.getCurve() + // Use the same tolerance for curve time parameter + // comparisons as in Curve.js + && Math.abs(this.getParameter() - loc.getParameter()) + < /*#=*/Numerical.TOLERANCE + && (_ignoreIntersection + || (!this._intersection && !loc._intersection + || this._intersection && this._intersection.equals( + loc._intersection, true))) + || false; }, /** @@ -329,10 +329,12 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ if (curve1 === curve2) { var diff = l1._parameter - l2._parameter; if (Math.abs(diff) < tolerance) { - var curve21 = l1._curve2, - curve22 = l2._curve2; + var i1 = l1._intersection, + i2 = l2._intersection, + curve21 = i1 && i1._curve, + curve22 = i2 && l2._curve; res = curve21 === curve22 // equal or both null - ? l1._parameter2 - l2._parameter2 + ? (i1 ? i1._parameter : 0) - (i2 ? i2._parameter : 0) : curve21 && curve22 ? curve21.getIndex() - curve22.getIndex() : curve21 ? 1 : -1; diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 35e64017..04238033 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -225,14 +225,12 @@ PathItem.inject(new function() { intersections.forEach(function(inter) { var log = ['CurveLocation', inter._id, 'p', inter.getPath()._id, 'i', inter.getIndex(), 't', inter._parameter, - 'i2', inter._curve2 ? inter._curve2.getIndex() : null, - 't2', inter._parameter2, 'o', !!inter._overlap]; + 'o', !!inter._overlap]; if (inter._other) { - inter = inter.getIntersection(); + inter = inter._intersection; log.push('Other', inter._id, 'p', inter.getPath()._id, 'i', inter.getIndex(), 't', inter._parameter, - 'i2', inter._curve2 ? inter._curve2.getIndex() : null, - 't2', inter._parameter2, 'o', !!inter._overlap); + 'o', !!inter._overlap); } console.log(log.map(function(v) { return v == null ? '-' : v @@ -275,7 +273,7 @@ PathItem.inject(new function() { clearSegments.push(segment); } // Link the new segment with the intersection on the other curve - segment._intersection = loc.getIntersection(); + segment._intersection = loc._intersection; loc._segment = segment; prev = loc; } diff --git a/src/path/PathItem.js b/src/path/PathItem.js index a4346539..c370c98a 100644 --- a/src/path/PathItem.js +++ b/src/path/PathItem.js @@ -110,11 +110,13 @@ var PathItem = Item.extend(/** @lends PathItem# */{ startConnected: length1 === 1, // After splitting, the end is always connected: endConnected: true, - adjust: function(loc) { + reparametrize: function(t1, t2) { // Since the curve was split above, we need to // adjust the parameters for both locations. - loc._parameter /= 2; - loc._parameter2 = 0.5 + loc._parameter2 / 2; + return { + t1: t1 / 2, + t2: (1 + t2) / 2 + }; } } ); From 4770cfe2f844bc1e93b8315592bf87626f6fcb1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Thu, 10 Sep 2015 05:18:56 +0200 Subject: [PATCH 043/280] Minor intersection refactoring clean up. --- src/path/CurveLocation.js | 7 +++++-- src/path/PathItem.js | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index fd3e056c..2c13bc86 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -65,9 +65,9 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ this._distance = _distance; this._overlap = _overlap; this._intersection = _intersection; - this._other = false; if (_intersection) { _intersection._intersection = this; + // TODO: Remove this once debug logging is removed. _intersection._other = true; } // Also store references to segment1 and segment2, in case path @@ -325,6 +325,9 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ res; // Sort by path-id, curve, parameter, curve2, parameter2 so we // can easily remove duplicates with calls to equals() after. + // NOTE: We don't call getCurve() / getParameter() here, since + // this code is used internally in boolean operations where all + // this information remains valid during processing. if (path1 === path2) { if (curve1 === curve2) { var diff = l1._parameter - l2._parameter; @@ -334,7 +337,7 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ curve21 = i1 && i1._curve, curve22 = i2 && l2._curve; res = curve21 === curve22 // equal or both null - ? (i1 ? i1._parameter : 0) - (i2 ? i2._parameter : 0) + ? i1 && i2 ? i1._parameter - i2._parameter : 0 : curve21 && curve22 ? curve21.getIndex() - curve22.getIndex() : curve21 ? 1 : -1; diff --git a/src/path/PathItem.js b/src/path/PathItem.js index c370c98a..d6d2b7a6 100644 --- a/src/path/PathItem.js +++ b/src/path/PathItem.js @@ -106,8 +106,8 @@ var PathItem = Item.extend(/** @lends PathItem# */{ var parts = Curve.subdivide(values1); Curve._getIntersections(parts[0], parts[1], curve1, curve1, locations, { - // Only possible if there is only one curve: - startConnected: length1 === 1, + // Only possible if there is only one closed curve: + startConnected: length1 === 1 && p1.equals(p2), // After splitting, the end is always connected: endConnected: true, reparametrize: function(t1, t2) { From c69ea345da211085c707f48bf74fe94d1c19b543 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Thu, 10 Sep 2015 05:21:47 +0200 Subject: [PATCH 044/280] Correctly handle self-intersections when deciding to switch segments. Closes #765 --- src/path/PathItem.Boolean.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 04238033..6d7de00a 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -496,6 +496,7 @@ PathItem.inject(new function() { labelSegment(seg, '#' + pathIndex + '.' + (i + 1) + ' i: ' + !!inter + + ' p: ' + seg._path._id + ' x: ' + seg._point.x + ' y: ' + seg._point.y + ' o: ' + (inter && inter._overlap || 0) @@ -526,20 +527,23 @@ PathItem.inject(new function() { do { var handleIn = dir > 0 ? seg._handleIn : seg._handleOut, handleOut = dir > 0 ? seg._handleOut : seg._handleIn, - interSeg; + inter = seg._intersection, + interSeg = inter && inter._segment; // If the intersection segment is valid, try switching to // it, with an appropriate direction to continue traversal. // Else, stay on the same contour. - if (added && (!operator(seg._winding)) - && (inter = seg._intersection) - && (interSeg = inter._segment) - && interSeg !== startSeg) { - if (interSeg._path === seg._path) { - // Switch to the intersection segment, if we are + if (added && interSeg && interSeg !== startSeg) { + if (interSeg._path === seg._path) { // Self-intersection + drawSegment(seg, 'self-int ' + dir, i, 'red'); + // Switch to the intersection segment, as we need to // resolving self-Intersections. seg._visited = interSeg._visited; seg = interSeg; dir = 1; + } else if (operator(seg._winding)) { + // Do not switch to the intersection as the segment is + // part of the boolean result. + drawSegment(seg, 'keep', i, 'black'); } else if (inter._overlap && operation !== 'intersect') { // Switch to the overlapping intersection segment // if its winding number along the curve is 1, to From 86f404123ed0efae9d356b68d356fe3f8ae520f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Fri, 11 Sep 2015 12:07:27 +0200 Subject: [PATCH 045/280] Minor code tweaks. --- src/path/Curve.js | 4 ++-- src/path/PathItem.js | 7 ++----- src/path/PathIterator.js | 2 +- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index e0b47412..fd5f6d17 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1330,8 +1330,8 @@ new function() { // Scope for intersection using bezier fat-line clipping var reparametrize = param.reparametrize; if (reparametrize) { var res = reparametrize(t1, t2); - t1 = res.t1; - t2 = res.t2; + t1 = res[0]; + t2 = res[1]; } loc = new CurveLocation(c1, t1, p1 || Curve.getPoint(v1, t1), null, overlap, diff --git a/src/path/PathItem.js b/src/path/PathItem.js index d6d2b7a6..e473b384 100644 --- a/src/path/PathItem.js +++ b/src/path/PathItem.js @@ -103,7 +103,7 @@ var PathItem = Item.extend(/** @lends PathItem# */{ if (l1.intersect(l2, false)) { // Self intersecting is found by dividing the curve in two // and and then applying the normal curve intersection code. - var parts = Curve.subdivide(values1); + var parts = Curve.subdivide(values1, 0.5); Curve._getIntersections(parts[0], parts[1], curve1, curve1, locations, { // Only possible if there is only one closed curve: @@ -113,10 +113,7 @@ var PathItem = Item.extend(/** @lends PathItem# */{ reparametrize: function(t1, t2) { // Since the curve was split above, we need to // adjust the parameters for both locations. - return { - t1: t1 / 2, - t2: (1 + t2) / 2 - }; + return [t1 / 2, (1 + t2) / 2]; } } ); diff --git a/src/path/PathIterator.js b/src/path/PathIterator.js index 1a997798..e610cf53 100644 --- a/src/path/PathIterator.js +++ b/src/path/PathIterator.js @@ -58,7 +58,7 @@ var PathIterator = Base.extend({ // appears to offer a good trade-off between speed and // precision for display purposes. && !Curve.isFlatEnough(curve, tolerance || 0.25)) { - var split = Curve.subdivide(curve), + var split = Curve.subdivide(curve, 0.5), halfT = (minT + maxT) / 2; // Recursively subdivide and compute parts again. computeParts(split[0], index, minT, halfT); From 35f3ac87bfb4bcae38819b5f7807a34932f17df8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sat, 12 Sep 2015 10:12:17 +0200 Subject: [PATCH 046/280] Change checks for startConnected / endConnected to support compound-paths. Closes #778 --- src/path/Curve.js | 20 +++++++++----------- src/path/PathItem.js | 11 ++++++++--- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index fd5f6d17..b9bbea51 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1333,13 +1333,12 @@ new function() { // Scope for intersection using bezier fat-line clipping t1 = res[0]; t2 = res[1]; } - loc = new CurveLocation(c1, t1, p1 || Curve.getPoint(v1, t1), - null, overlap, - new CurveLocation(c2, t2, p2 || Curve.getPoint(v2, t2), - null, overlap)); - locations.push(loc); + locations.push( + new CurveLocation(c1, t1, p1 || Curve.getPoint(v1, t1), + null, overlap, + new CurveLocation(c2, t2, p2 || Curve.getPoint(v2, t2), + null, overlap))); } - return loc; } function addCurveIntersections(v1, v2, c1, c2, locations, param, @@ -1612,10 +1611,10 @@ new function() { // Scope for intersection using bezier fat-line clipping // Linear curves can only overlap if they are collinear, which means // they must be are collinear and any point of curve 1 must be on // curve 2 - var line1 = new Line(v1[0], v1[1], v1[6], v1[7], false), - line2 = new Line(v2[0], v2[1], v2[6], v2[7], false); - if (!line1.isCollinear(line2) || - line1.getDistance(line2.getPoint()) > epsilon) + var line1 = new Line(v1[0], v1[1], v1[6], v1[7]), + line2 = new Line(v2[0], v2[1], v2[6], v2[7]); + if (!line1.isCollinear(line2) || line1.getDistance(line2.getPoint()) + > /*#=*/Numerical.GEOMETRY_TOLERANCE) return false; } else if (straight1 ^ straight2) { // If one curve is straight, the other curve must be straight, too, @@ -1654,7 +1653,6 @@ new function() { // Scope for intersection using bezier fat-line clipping p2 = Curve.getPart(v[1], Math.min(pairs[0][1], pairs[1][1]), Math.max(pairs[0][1], pairs[1][1])); // Reverse values of second curve if necessary - // if (abs(p1[0] - p2[6]) < epsilon && abs(p1[1] - p2[7]) < epsilon) { if (pairs[0][1] > pairs[1][1]) { p2 = [p2[6], p2[7], p2[4], p2[5], p2[2], p2[3], p2[0], p2[1]]; } diff --git a/src/path/PathItem.js b/src/path/PathItem.js index e473b384..0aaa03f3 100644 --- a/src/path/PathItem.js +++ b/src/path/PathItem.js @@ -81,6 +81,7 @@ var PathItem = Item.extend(/** @lends PathItem# */{ // we don't need to iterate through their curves. if (path && !this.getBounds(matrix1).touches(path.getBounds(matrix2))) return locations; + // Cache values for curves2 as we re-iterate them for each in curves1. for (var i = 0; i < length2; i++) values2[i] = curves2[i].getValues(matrix2); for (var i = 0; i < length1; i++) { @@ -126,13 +127,17 @@ var PathItem = Item.extend(/** @lends PathItem# */{ // self-intersection check: if (returnFirst && locations.length) break; + var curve2 = curves2[j]; // Avoid end point intersections on consecutive curves when // self intersecting. Curve._getIntersections( - values1, values2[j], curve1, curves2[j], locations, + values1, values2[j], curve1, curve2, locations, self ? { - startConnected: j === length2 - 1 && i === 0, - endConnected: j === i + 1 + // Do not compare indices here to determine connection, + // since one array of curves can contain curves from + // separate sup-paths of a compound path. + startConnected: curve1.getPrevious() === curve2, + endConnected: curve1.getNext() === curve2 } : {} ); } From cdd0cee623f9e6022dd3b57ccf200c9b27309ca3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sat, 12 Sep 2015 10:24:19 +0200 Subject: [PATCH 047/280] Activate resolving of self-intersections in boolean code. Relates to #779 --- src/path/PathItem.Boolean.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 6d7de00a..c7a07a77 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -76,20 +76,18 @@ PathItem.inject(new function() { _path2.reverse(); // Split curves at intersections on both paths. Note that for self // intersection, _path2 will be null and getIntersections() handles it. - - // Without support for self-intersection - // splitPath(Curve._filterIntersections( - // _path1._getIntersections(_path2, null, []), true)); - - // console.time('inter'); + // console.time('intersection'); var locations = _path1._getIntersections(_path2, null, []); // console.timeEnd('inter'); - if (_path2 && false) { - // console.time('self'); + if (_path2) { + // console.time('self-intersection'); + // Resolve self-intersections on both source paths and add them to + // the locations too: _path1._getIntersections(null, null, locations); _path2._getIntersections(null, null, locations); - // console.timeEnd('self'); + // console.timeEnd('self-intersection'); } + // console.timeEnd('intersection'); splitPath(Curve._filterIntersections(locations, true)); var chain = [], @@ -438,8 +436,8 @@ PathItem.inject(new function() { function tracePaths(segments, monoCurves, operation) { var segmentCount = 0; var pathCount = 0; - var reportSegments = false && !window.silent; var reportWindings = false && !window.silent; + var reportSegments = false && !window.silent; var textAngle = 0; var fontSize = 1 / paper.project.activeLayer.scaling.x; From fb5f8c011501a7b55ba2dd0e4c6abd1e288eb23e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sat, 12 Sep 2015 10:35:47 +0200 Subject: [PATCH 048/280] Introduce GEOMETRIC_EPSILON, for isOrthogonal(), isCollinear() and overlap checks. Relates to #777 --- src/basic/Line.js | 4 +--- src/basic/Point.js | 10 ++-------- src/path/Curve.js | 2 +- src/path/Path.js | 3 +-- src/util/Numerical.js | 1 + 5 files changed, 6 insertions(+), 14 deletions(-) diff --git a/src/basic/Line.js b/src/basic/Line.js index 28c6883c..a31b2f28 100644 --- a/src/basic/Line.js +++ b/src/basic/Line.js @@ -113,10 +113,8 @@ var Line = Base.extend(/** @lends Line# */{ }, isCollinear: function(line) { - // TODO: Tests showed that 1e-10 might work well here, but we want to - // keep it in sync with Point#isCollinear() return this._vx * line._vy - this._vy * line._vx - < /*#=*/Numerical.TOLERANCE; + < /*#=*/Numerical.GEOMETRIC_EPSILON; }, statics: /** @lends Line */{ diff --git a/src/basic/Point.js b/src/basic/Point.js index d96e08e8..e2cd6b43 100644 --- a/src/basic/Point.js +++ b/src/basic/Point.js @@ -702,10 +702,7 @@ var Point = Base.extend(/** @lends Point# */{ * @return {Boolean} {@true it is collinear} */ isCollinear: function(point) { - // NOTE: Numerical.EPSILON is too small, breaking shape-path-shape - // conversion test. But tolerance is probably too large? - // TODO: Tests showed that 1e-10 might work well here. - return Math.abs(this.cross(point)) < /*#=*/Numerical.TOLERANCE; + return Math.abs(this.cross(point)) < /*#=*/Numerical.GEOMETRIC_EPSILON; }, // TODO: Remove version with typo after a while (deprecated June 2015) @@ -719,10 +716,7 @@ var Point = Base.extend(/** @lends Point# */{ * @return {Boolean} {@true it is orthogonal} */ isOrthogonal: function(point) { - // NOTE: Numerical.EPSILON is too small, breaking shape-path-shape - // conversion test. - // TODO: Test if 1e-10 works here too? See #isCollinear() - return Math.abs(this.dot(point)) < /*#=*/Numerical.TOLERANCE; + return Math.abs(this.dot(point)) < /*#=*/Numerical.GEOMETRIC_EPSILON; }, /** diff --git a/src/path/Curve.js b/src/path/Curve.js index b9bbea51..cd4c76e3 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1614,7 +1614,7 @@ new function() { // Scope for intersection using bezier fat-line clipping var line1 = new Line(v1[0], v1[1], v1[6], v1[7]), line2 = new Line(v2[0], v2[1], v2[6], v2[7]); if (!line1.isCollinear(line2) || line1.getDistance(line2.getPoint()) - > /*#=*/Numerical.GEOMETRY_TOLERANCE) + > /*#=*/Numerical.GEOMETRIC_EPSILON) return false; } else if (straight1 ^ straight2) { // If one curve is straight, the other curve must be straight, too, diff --git a/src/path/Path.js b/src/path/Path.js index 0bc06d55..e56137a5 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -2502,7 +2502,6 @@ new function() { // PostScript-style drawing commands x = pt.x, y = pt.y, abs = Math.abs, - epsilon = /*#=*/Numerical.EPSILON, rx = abs(radius.width), ry = abs(radius.height), rxSq = rx * rx, @@ -2519,7 +2518,7 @@ new function() { // PostScript-style drawing commands } factor = (rxSq * rySq - rxSq * ySq - rySq * xSq) / (rxSq * ySq + rySq * xSq); - if (abs(factor) < epsilon) + if (abs(factor) < /*#=*/Numerical.EPSILON) factor = 0; if (factor < 0) throw new Error( diff --git a/src/util/Numerical.js b/src/util/Numerical.js index ed5cc1eb..8bfbc0a5 100644 --- a/src/util/Numerical.js +++ b/src/util/Numerical.js @@ -81,6 +81,7 @@ var Numerical = new function() { * range (see MACHINE_EPSILON). */ EPSILON: EPSILON, + GEOMETRIC_EPSILON: 1e-9, /** * MACHINE_EPSILON for a double precision (Javascript Number) is * 2.220446049250313e-16. (try this in the js console) From 3da921a0b080d15e266c84b4273c18daedb68f02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sat, 12 Sep 2015 11:43:41 +0200 Subject: [PATCH 049/280] Improve CompoundPath#reduce() to properly reduce suppaths. Relates to #779 --- src/path/CompoundPath.js | 13 ++++++++++--- src/path/Curve.js | 2 +- src/path/Path.js | 5 ++--- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/path/CompoundPath.js b/src/path/CompoundPath.js index c7c8c622..343eacab 100644 --- a/src/path/CompoundPath.js +++ b/src/path/CompoundPath.js @@ -131,16 +131,23 @@ var CompoundPath = PathItem.extend(/** @lends CompoundPath# */{ this._children[i].smooth(); }, + // DOCS: reduce() + // TEST: reduce() reduce: function reduce() { - if (this._children.length === 0) { // Replace with a simple empty Path + var children = this._children; + for (var i = children.length - 1; i >= 0; i--) { + var path = children[i].reduce(); + if (path.isEmpty()) + children.splice(i, 1); + } + if (children.length === 0) { // Replace with a simple empty Path var path = new Path(Item.NO_INSERT); path.insertAbove(this); path.setStyle(this._style); this.remove(); return path; - } else { - return reduce.base.call(this); } + return reduce.base.call(this); }, /** diff --git a/src/path/Curve.js b/src/path/Curve.js index cd4c76e3..2507e88e 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -885,7 +885,7 @@ statics: { * @return {Boolean} {@true if the two lines are collinear} */ isCollinear: function(curve) { - return this.isStraight() && curve.isStraight() + return curve && this.isStraight() && curve.isStraight() && this.getVector().isCollinear(curve.getVector()); } }), /** @lends Curve# */{ diff --git a/src/path/Path.js b/src/path/Path.js index e56137a5..014337fb 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -1001,10 +1001,9 @@ var Path = PathItem.extend(/** @lends Path# */{ reduce: function() { var curves = this.getCurves(); for (var i = curves.length - 1; i >= 0; i--) { - var curve = curves[i], - next; + var curve = curves[i]; if (!curve.hasHandles() && (curve.getLength() === 0 - || (next = curve.getNext()) && curve.isCollinear(next))) + || curve.isCollinear(curve.getNext()))) curve.remove(); } return this; From 2fb203ddd16752d99a919d17e3d1bb737ea64811 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sat, 12 Sep 2015 11:58:17 +0200 Subject: [PATCH 050/280] Adjust notes since we now support boolean operations on self-intersecting Paths items --- src/path/PathItem.Boolean.js | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index c7a07a77..e929d647 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -13,23 +13,18 @@ /* * Boolean Geometric Path Operations * - * This is mostly written for clarity and compatibility, not optimised for - * performance, and has to be tested heavily for stability. - * * Supported * - Path and CompoundPath items * - Boolean Union * - Boolean Intersection * - Boolean Subtraction - * - Resolving a self-intersecting Path - * - * Not supported yet - * - Boolean operations on self-intersecting Paths + * - Boolean Exclusion + * - Resolving a self-intersecting Path items + * - Boolean operations on self-intersecting Paths items * * @author Harikrishnan Gopalakrishnan * http://hkrish.com/playground/paperjs/booleanStudy.html */ - PathItem.inject(new function() { var operators = { unite: function(w) { @@ -83,8 +78,19 @@ PathItem.inject(new function() { // console.time('self-intersection'); // Resolve self-intersections on both source paths and add them to // the locations too: + // var self = []; _path1._getIntersections(null, null, locations); _path2._getIntersections(null, null, locations); + /* + self.forEach(function(inter) { + new Path.Circle({ + center: inter.point, + radius: 3, + fillColor: 'red' + }); + }); + console.log(self); + */ // console.timeEnd('self-intersection'); } // console.timeEnd('intersection'); From 085cdd74a2c49de1228aa0b996261b33743a0959 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sat, 12 Sep 2015 21:56:53 +0200 Subject: [PATCH 051/280] Use GEOMETRIC_EPSILON when comparing curve start / end points. Relates to #777 --- src/path/Curve.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 2507e88e..c686f0c5 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1702,12 +1702,12 @@ new function() { // Scope for intersection using bezier fat-line clipping c1p2 = new Point(v1[6], v1[7]), c2p1 = new Point(v2[0], v2[1]), c2p2 = new Point(v2[6], v2[7]), - tolerance = /*#=*/Numerical.TOLERANCE; + epsilon = /*#=*/Numerical.GEOMETRIC_EPSILON; // Handle the special case where the first curve's stat-point // overlaps with the second curve's start- or end-points. - if (c1p1.isClose(c2p1, tolerance)) + if (c1p1.isClose(c2p1, epsilon)) addLocation(locations, param, v1, c1, 0, c1p1, v2, c2, 0, c1p1); - if (!param.startConnected && c1p1.isClose(c2p2, tolerance)) + if (!param.startConnected && c1p1.isClose(c2p2, epsilon)) addLocation(locations, param, v1, c1, 0, c1p1, v2, c2, 1, c1p1); // Determine the correct intersection method based on whether one or // curves are straight lines: @@ -1723,9 +1723,9 @@ new function() { // Scope for intersection using bezier fat-line clipping 0, 1, 0, 1, 0, false, 0); // Handle the special case where the first curve's end-point // overlaps with the second curve's start- or end-points. - if (!param.endConnected && c1p2.isClose(c2p1, tolerance)) + if (!param.endConnected && c1p2.isClose(c2p1, epsilon)) addLocation(locations, param, v1, c1, 1, c1p2, v2, c2, 0, c1p2); - if (c1p2.isClose(c2p2, tolerance)) + if (c1p2.isClose(c2p2, epsilon)) addLocation(locations, param, v1, c1, 1, c1p2, v2, c2, 1, c1p2); return locations; }, From a0730756c12545dc1494ba386f3a3c0323713018 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sat, 12 Sep 2015 22:13:18 +0200 Subject: [PATCH 052/280] Use correct term for curve parameter renormalization. --- src/path/Curve.js | 6 +++--- src/path/PathItem.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index c686f0c5..eb4c4a67 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1327,9 +1327,9 @@ new function() { // Scope for intersection using bezier fat-line clipping && t1 <= (param.endConnected ? tMax : 1)) { if (t2 == null) t2 = Curve.getParameterOf(v2, p2.x, p2.y); - var reparametrize = param.reparametrize; - if (reparametrize) { - var res = reparametrize(t1, t2); + var renormalize = param.renormalize; + if (renormalize) { + var res = renormalize(t1, t2); t1 = res[0]; t2 = res[1]; } diff --git a/src/path/PathItem.js b/src/path/PathItem.js index 0aaa03f3..1f931dd0 100644 --- a/src/path/PathItem.js +++ b/src/path/PathItem.js @@ -111,7 +111,7 @@ var PathItem = Item.extend(/** @lends PathItem# */{ startConnected: length1 === 1 && p1.equals(p2), // After splitting, the end is always connected: endConnected: true, - reparametrize: function(t1, t2) { + renormalize: function(t1, t2) { // Since the curve was split above, we need to // adjust the parameters for both locations. return [t1 / 2, (1 + t2) / 2]; From ef45a5f62c9fd17d94457b05f58ce369b2bcfac2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sat, 12 Sep 2015 22:14:04 +0200 Subject: [PATCH 053/280] Clean up tMin / tMax uses. --- src/path/PathItem.Boolean.js | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index e929d647..372eb41c 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -510,12 +510,11 @@ PathItem.inject(new function() { var paths = [], operator = operators[operation], - tolerance = /*#=*/Numerical.TOLERANCE, // Values for getTangentAt() that are almost 0 and 1. // NOTE: Even though getTangentAt() supports 0 and 1 instead of // tMin and tMax, we still need to use this instead, as other issues // emerge from switching to 0 and 1 in edge cases. - tMin = tolerance, + tMin = /*#=*/Numerical.TOLERANCE, tMax = 1 - tMin; for (var i = 0, seg, startSeg, l = segments.length; i < l; i++) { seg = startSeg = segments[i]; @@ -802,20 +801,20 @@ Path.inject(/** @lends Path# */{ var a = 3 * (y1 - y2) - y0 + y3, b = 2 * (y0 + y2) - 4 * y1, c = y1 - y0, - tolerance = /*#=*/Numerical.TOLERANCE, - roots = []; - // Keep then range to 0 .. 1 (excluding) in the search for y - // extrema. - var count = Numerical.solveQuadratic(a, b, c, roots, tolerance, - 1 - tolerance); - if (count === 0) { + tMin = /*#=*/Numerical.TOLERANCE, + tMax = 1 - tMin, + roots = [], + // Keep then range to 0 .. 1 (excluding) in the search for y + // extrema. + n = Numerical.solveQuadratic(a, b, c, roots, tMin, tMax); + if (n === 0) { insertCurve(v); } else { roots.sort(); var t = roots[0], parts = Curve.subdivide(v, t); insertCurve(parts[0]); - if (count > 1) { + if (n > 1) { // If there are two extrema, renormalize t to the range // of the second range and split again. t = (roots[1] - t) / (1 - t); From 8047f90ccf51c1a0175d4902f638f7afdfd2e859 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sat, 12 Sep 2015 22:20:31 +0200 Subject: [PATCH 054/280] Switch to using GEOMETRIC_EPSILON in getWinding() code. --- src/path/PathItem.Boolean.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 372eb41c..df37ea6f 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -306,8 +306,8 @@ PathItem.inject(new function() { * with respect to a given set of monotone curves. */ function getWinding(point, curves, horizontal, testContains) { - var tolerance = /*#=*/Numerical.TOLERANCE, - tMin = tolerance, + var epsilon = /*#=*/Numerical.GEOMETRIC_EPSILON, + tMin = /*#=*/Numerical.TOLERANCE, tMax = 1 - tMin, px = point.x, py = point.y, @@ -321,8 +321,8 @@ PathItem.inject(new function() { if (horizontal) { var yTop = -Infinity, yBottom = Infinity, - yBefore = py - tolerance, - yAfter = py + tolerance; + yBefore = py - epsilon, + yAfter = py + epsilon; // Find the closest top and bottom intercepts for the same vertical // line. for (var i = 0, l = curves.length; i < l; i++) { @@ -348,8 +348,8 @@ PathItem.inject(new function() { if (yBottom < Infinity) windRight = getWinding(new Point(px, yBottom), curves); } else { - var xBefore = px - tolerance, - xAfter = px + tolerance; + var xBefore = px - epsilon, + xAfter = px + epsilon; // Find the winding number for right side of the curve, inclusive of // the curve itself, while tracing along its +-x direction. var startCounted = false, From 4f04dae20f08f545becde5e58b1191ac8f4da493 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sat, 12 Sep 2015 22:26:16 +0200 Subject: [PATCH 055/280] Use the correct GEOMETRIC_EPSILON when matching beginnings and ends of curve in Curve.getParameterOf() Relates to #777 --- src/path/Curve.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index eb4c4a67..85019fa7 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -592,11 +592,11 @@ statics: { getParameterOf: function(v, x, y) { // Handle beginnings and end separately, as they are not detected // sometimes. - var tolerance = /*#=*/Numerical.TOLERANCE, + var epsilon = /*#=*/Numerical.GEOMETRIC_EPSILON, abs = Math.abs; - if (abs(v[0] - x) < tolerance && abs(v[1] - y) < tolerance) + if (abs(v[0] - x) < epsilon && abs(v[1] - y) < epsilon) return 0; - if (abs(v[6] - x) < tolerance && abs(v[7] - y) < tolerance) + if (abs(v[6] - x) < epsilon && abs(v[7] - y) < epsilon) return 1; var txs = [], tys = [], @@ -619,7 +619,7 @@ statics: { ty = tx; } // Use average if we're within tolerance - if (abs(tx - ty) < tolerance) + if (abs(tx - ty) < /*#=*/Numerical.TOLERANCE) return (tx + ty) * 0.5; } } From d62caf6faa99cd6f47d269567432a312c368044a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sat, 12 Sep 2015 22:55:58 +0200 Subject: [PATCH 056/280] Introduce CURVETIME_EPSILON, to be used when handling curve time parameters. Relates to #777 --- src/path/Curve.js | 60 ++++++++++++++++++------------------ src/path/CurveLocation.js | 7 ++--- src/path/Path.js | 2 +- src/path/PathItem.Boolean.js | 34 +++++++++++--------- src/util/Numerical.js | 1 + test/tests/Curve.js | 2 +- 6 files changed, 55 insertions(+), 51 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 85019fa7..58eb50b3 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -442,10 +442,11 @@ var Curve = Base.extend(/** @lends Curve# */{ // TODO: Rename to divideAt()? divide: function(offset, isParameter, ignoreStraight) { var parameter = this._getParameter(offset, isParameter), - tolerance = /*#=*/Numerical.TOLERANCE, + tMin = /*#=*/Numerical.CURVETIME_EPSILON, + tMax = 1 - tMin, res = null; // Only divide if not at the beginning or end. - if (parameter >= tolerance && parameter <= 1 - tolerance) { + if (parameter >= tMin && parameter <= tMax) { var parts = Curve.subdivide(this.getValues(), parameter), setHandles = ignoreStraight || this.hasHandles(), left = parts[0], @@ -618,8 +619,8 @@ statics: { } else if (sy === -1) { ty = tx; } - // Use average if we're within tolerance - if (abs(tx - ty) < /*#=*/Numerical.TOLERANCE) + // Use average if we're within epsilon + if (abs(tx - ty) < /*#=*/Numerical.CURVETIME_EPSILON) return (tx + ty) * 0.5; } } @@ -726,7 +727,7 @@ statics: { // Add some tolerance for good roots, as t = 0, 1 are added // separately anyhow, and we don't want joins to be added with radii // in getStrokeBounds() - tMin = /*#=*/Numerical.TOLERANCE, + tMin = /*#=*/Numerical.CURVETIME_EPSILON, tMax = 1 - tMin; // Only add strokeWidth to bounds for points which lie within 0 < t < 1 // The corner cases for cap and join are handled in getStrokeBounds() @@ -995,7 +996,7 @@ statics: { // Now iteratively refine solution until we reach desired precision. var step = 1 / (count * 2); - while (step > /*#=*/Numerical.TOLERANCE) { + while (step > /*#=*/Numerical.CURVETIME_EPSILON) { if (!refine(minT - step) && !refine(minT + step)) step /= 2; } @@ -1155,12 +1156,13 @@ new function() { // Scope for methods that require private functions c1x = v[2], c1y = v[3], c2x = v[4], c2y = v[5], p2x = v[6], p2y = v[7], - tolerance = /*#=*/Numerical.TOLERANCE, + tMin = /*#=*/Numerical.CURVETIME_EPSILON, + tMax = 1 - tMin, x, y; // Handle special case at beginning / end of curve - if (type === 0 && (t < tolerance || t > 1 - tolerance)) { - var isZero = t < tolerance; + if (type === 0 && (t < tMin || t > tMax)) { + var isZero = t < tMin; x = isZero ? p1x : p2x; y = isZero ? p1y : p2y; } else { @@ -1184,10 +1186,10 @@ new function() { // Scope for methods that require private functions // the x and y coordinates: // Prevent tangents and normals of length 0: // http://stackoverflow.com/questions/10506868/ - if (t < tolerance) { + if (t < tMin) { x = cx; y = cy; - } else if (t > 1 - tolerance) { + } else if (t > tMax) { x = 3 * (p2x - c2x); y = 3 * (p2y - c2y); } else { @@ -1198,8 +1200,7 @@ new function() { // Scope for methods that require private functions // When the tangent at t is zero and we're at the beginning // or the end, we can use the vector between the handles, // but only when normalizing as its weighted length is 0. - if (x === 0 && y === 0 - && (t < tolerance || t > 1 - tolerance)) { + if (x === 0 && y === 0 && (t < tMin || t > tMax)) { x = c2x - c1x; y = c2y - c1y; } @@ -1250,8 +1251,7 @@ new function() { // Scope for methods that require private functions return start; // See if we're going forward or backward, and handle cases // differently - var tolerance = /*#=*/Numerical.TOLERANCE, - abs = Math.abs, + var abs = Math.abs, forward = offset > 0, a = forward ? start : 0, b = forward ? 1 : start, @@ -1261,7 +1261,7 @@ new function() { // Scope for methods that require private functions // Get length of total range rangeLength = Numerical.integrate(ds, a, b, getIterations(a, b)); - if (abs(offset - rangeLength) < tolerance) { + if (abs(offset - rangeLength) < /*#=*/Numerical.GEOMETRIC_EPSILON) { // Matched the end: return forward ? b : a; } else if (abs(offset) > rangeLength) { @@ -1286,7 +1286,7 @@ new function() { // Scope for methods that require private functions // Start with out initial guess for x. // NOTE: guess is a negative value when not looking forward. return Numerical.findRoot(f, ds, start + guess, a, b, 16, - tolerance); + /*#=*/Numerical.CURVETIME_EPSILON); }, getPoint: function(v, t) { @@ -1319,7 +1319,7 @@ new function() { // Scope for intersection using bezier fat-line clipping function addLocation(locations, param, v1, c1, t1, p1, v2, c2, t2, p2, overlap) { var loc = null, - tMin = /*#=*/Numerical.TOLERANCE, + tMin = /*#=*/Numerical.CURVETIME_EPSILON, tMax = 1 - tMin; if (t1 == null) t1 = Curve.getParameterOf(v1, p1.x, p1.y); @@ -1353,7 +1353,7 @@ new function() { // Scope for intersection using bezier fat-line clipping return; // Let P be the first curve and Q be the second var q0x = v2[0], q0y = v2[1], q3x = v2[6], q3y = v2[7], - tolerance = /*#=*/Numerical.TOLERANCE, + epsilon = /*#=*/Numerical.CURVETIME_EPSILON, getSignedDistance = Line.getSignedDistance, // Calculate the fat-line L for Q is the baseline l and two // offsets which completely encloses the curve P. @@ -1371,7 +1371,7 @@ new function() { // Scope for intersection using bezier fat-line clipping dp3 = getSignedDistance(q0x, q0y, q3x, q3y, v1[6], v1[7]), tMinNew, tMaxNew, tDiff; - if (q0x === q3x && uMax - uMin < tolerance && recursion >= 3) { + if (q0x === q3x && uMax - uMin < epsilon && recursion >= 3) { // The fat-line of Q has converged to a point, the clipping is not // reliable. Return the value we have even though we will miss the // precision. @@ -1419,7 +1419,7 @@ new function() { // Scope for intersection using bezier fat-line clipping parts[1], v1, c2, c1, locations, param, t, uMax, tMinNew, tMaxNew, tDiff, !reverse, recursion); } - } else if (Math.max(uMax - uMin, tMaxNew - tMinNew) < tolerance) { + } else if (Math.max(uMax - uMin, tMaxNew - tMinNew) < epsilon) { // We have isolated the intersection with sufficient precision var t1 = tMinNew + (tMaxNew - tMinNew) / 2, t2 = uMin + (uMax - uMin) / 2; @@ -1602,8 +1602,8 @@ new function() { // Scope for intersection using bezier fat-line clipping */ function addOverlap(v1, v2, c1, c2, locations, param) { var abs = Math.abs, - tolerance = /*#=*/Numerical.TOLERANCE, - epsilon = /*#=*/Numerical.EPSILON, + timeEpsilon = /*#=*/Numerical.CURVETIME_EPSILON, + geomEpsilon = /*#=*/Numerical.GEOMETRIC_EPSILON, straight1 = Curve.isStraight(v1), straight2 = Curve.isStraight(v2), straight = straight1 && straight2; @@ -1614,7 +1614,7 @@ new function() { // Scope for intersection using bezier fat-line clipping var line1 = new Line(v1[0], v1[1], v1[6], v1[7]), line2 = new Line(v2[0], v2[1], v2[6], v2[7]); if (!line1.isCollinear(line2) || line1.getDistance(line2.getPoint()) - > /*#=*/Numerical.GEOMETRIC_EPSILON) + > geomEpsilon) return false; } else if (straight1 ^ straight2) { // If one curve is straight, the other curve must be straight, too, @@ -1636,8 +1636,8 @@ new function() { // Scope for intersection using bezier fat-line clipping if (pairs.length === 1 && pair[0] < pairs[0][0]) { pairs.unshift(pair); } else if (pairs.length === 0 - || abs(pair[0] - pairs[0][0]) > tolerance - || abs(pair[1] - pairs[0][1]) > tolerance) { + || abs(pair[0] - pairs[0][0]) > timeEpsilon + || abs(pair[1] - pairs[0][1]) > timeEpsilon) { pairs.push(pair); } } @@ -1660,10 +1660,10 @@ new function() { // Scope for intersection using bezier fat-line clipping // We could do another check for curve identity here if we find a // better criteria. if (straight || - abs(p2[2] - p1[2]) < epsilon && - abs(p2[3] - p1[3]) < epsilon && - abs(p2[4] - p1[4]) < epsilon && - abs(p2[5] - p1[5]) < epsilon) { + abs(p2[2] - p1[2]) < geomEpsilon && + abs(p2[3] - p1[3]) < geomEpsilon && + abs(p2[4] - p1[4]) < geomEpsilon && + abs(p2[5] - p1[5]) < geomEpsilon) { // Overlapping parts are identical addLocation(locations, param, v1, c1, pairs[0][0], null, v2, c2, pairs[0][1], null, true), diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index 2c13bc86..a93acc43 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -45,7 +45,7 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ _distance, _overlap, _intersection) { // Merge intersections very close to the end of a curve to the // beginning of the next curve. - if (parameter >= 1 - /*#=*/Numerical.TOLERANCE) { + if (parameter >= 1 - /*#=*/Numerical.CURVETIME_EPSILON) { var next = curve.getNext(); if (next) { parameter = 0; @@ -286,7 +286,7 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ // Use the same tolerance for curve time parameter // comparisons as in Curve.js && Math.abs(this.getParameter() - loc.getParameter()) - < /*#=*/Numerical.TOLERANCE + < /*#=*/Numerical.CURVETIME_EPSILON && (_ignoreIntersection || (!this._intersection && !loc._intersection || this._intersection && this._intersection.equals( @@ -316,7 +316,6 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ statics: { sort: function(locations) { - var tolerance = /*#=*/Numerical.TOLERANCE; locations.sort(function compare(l1, l2) { var curve1 = l1._curve, curve2 = l2._curve, @@ -331,7 +330,7 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ if (path1 === path2) { if (curve1 === curve2) { var diff = l1._parameter - l2._parameter; - if (Math.abs(diff) < tolerance) { + if (Math.abs(diff) < /*#=*/Numerical.CURVETIME_EPSILON){ var i1 = l1._intersection, i2 = l2._intersection, curve21 = i1 && i1._curve, diff --git a/src/path/Path.js b/src/path/Path.js index 014337fb..69132a17 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -1182,7 +1182,7 @@ var Path = PathItem.extend(/** @lends Path# */{ index = arg.index; parameter = arg.parameter; } - var tMin = /*#=*/Numerical.TOLERANCE, + var tMin = /*#=*/Numerical.CURVETIME_EPSILON, tMax = 1 - tMin; if (parameter >= tMax) { // t == 1 is the same as t == 0 and index ++ diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index df37ea6f..0dc56b86 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -100,7 +100,7 @@ PathItem.inject(new function() { segments = [], // Aggregate of all curves in both operands, monotonic in y monoCurves = [], - tolerance = /*#=*/Numerical.TOLERANCE; + epsilon = /*#=*/Numerical.GEOMETRIC_EPSILON; function collect(paths) { for (var i = 0, l = paths.length; i < l; i++) { @@ -154,8 +154,7 @@ PathItem.inject(new function() { if (length <= curveLength) { // If the selected location on the curve falls onto its // beginning or end, use the curve's center instead. - if (length < tolerance - || curveLength - length < tolerance) + if (length < epsilon || curveLength - length < epsilon) length = curveLength / 2; var curve = node.segment.getCurve(), pt = curve.getPointAt(length), @@ -227,15 +226,20 @@ PathItem.inject(new function() { if (false) { console.log('Intersections', intersections.length); intersections.forEach(function(inter) { + if (inter._other) + return; + var other = inter._intersection; var log = ['CurveLocation', inter._id, 'p', inter.getPath()._id, 'i', inter.getIndex(), 't', inter._parameter, - 'o', !!inter._overlap]; - if (inter._other) { - inter = inter._intersection; - log.push('Other', inter._id, 'p', inter.getPath()._id, - 'i', inter.getIndex(), 't', inter._parameter, - 'o', !!inter._overlap); - } + 'o', !!inter._overlap, + 'Other', other._id, 'p', other.getPath()._id, + 'i', other.getIndex(), 't', other._parameter, + 'o', !!other._overlap]; + new Path.Circle({ + center: inter.point, + radius: 3, + strokeColor: 'green' + }); console.log(log.map(function(v) { return v == null ? '-' : v }).join(' ')); @@ -243,7 +247,7 @@ PathItem.inject(new function() { } // TODO: Make public in API, since useful! - var tMin = /*#=*/Numerical.TOLERANCE, + var tMin = /*#=*/Numerical.CURVETIME_EPSILON, tMax = 1 - tMin, noHandles = false, clearSegments = []; @@ -298,7 +302,7 @@ PathItem.inject(new function() { // Determine if the curve is a horizontal straight curve by checking the // slope of it's tangent. return curve.isStraight() && Math.abs(curve.getTangentAt(0.5, true).y) - < /*#=*/Numerical.TOLERANCE; + < /*#=*/Numerical.GEOMETRIC_EPSILON; } /** @@ -307,7 +311,7 @@ PathItem.inject(new function() { */ function getWinding(point, curves, horizontal, testContains) { var epsilon = /*#=*/Numerical.GEOMETRIC_EPSILON, - tMin = /*#=*/Numerical.TOLERANCE, + tMin = /*#=*/Numerical.CURVETIME_EPSILON, tMax = 1 - tMin, px = point.x, py = point.y, @@ -514,7 +518,7 @@ PathItem.inject(new function() { // NOTE: Even though getTangentAt() supports 0 and 1 instead of // tMin and tMax, we still need to use this instead, as other issues // emerge from switching to 0 and 1 in edge cases. - tMin = /*#=*/Numerical.TOLERANCE, + tMin = /*#=*/Numerical.CURVETIME_EPSILON, tMax = 1 - tMin; for (var i = 0, seg, startSeg, l = segments.length; i < l; i++) { seg = startSeg = segments[i]; @@ -801,7 +805,7 @@ Path.inject(/** @lends Path# */{ var a = 3 * (y1 - y2) - y0 + y3, b = 2 * (y0 + y2) - 4 * y1, c = y1 - y0, - tMin = /*#=*/Numerical.TOLERANCE, + tMin = /*#=*/Numerical.CURVETIME_EPSILON, tMax = 1 - tMin, roots = [], // Keep then range to 0 .. 1 (excluding) in the search for y diff --git a/src/util/Numerical.js b/src/util/Numerical.js index 8bfbc0a5..5e6481ee 100644 --- a/src/util/Numerical.js +++ b/src/util/Numerical.js @@ -81,6 +81,7 @@ var Numerical = new function() { * range (see MACHINE_EPSILON). */ EPSILON: EPSILON, + CURVETIME_EPSILON: 1e-6, GEOMETRIC_EPSILON: 1e-9, /** * MACHINE_EPSILON for a double precision (Javascript Number) is diff --git a/test/tests/Curve.js b/test/tests/Curve.js index 2e79f27e..87383ec5 100644 --- a/test/tests/Curve.js +++ b/test/tests/Curve.js @@ -161,7 +161,7 @@ test('Curve#getParameterAt()', function() { var t2 = curve.getParameterAt(o2); equals(t1, t2, 'Curve parameter at offset ' + o1 + ' should be the same value as at offset' + o2, - Numerical.TOLERANCE); + Numerical.CURVETIME_EPSILON); } equals(curve.getParameterAt(curve.length + 1), null, From 19c9a0e7221db760de3e0e06297f114f5c7ba4f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 13 Sep 2015 11:52:17 +0200 Subject: [PATCH 057/280] Use the correct points on curve2 when checking intersections at beginnings and ends. --- src/path/Curve.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 58eb50b3..0de4a154 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1706,9 +1706,9 @@ new function() { // Scope for intersection using bezier fat-line clipping // Handle the special case where the first curve's stat-point // overlaps with the second curve's start- or end-points. if (c1p1.isClose(c2p1, epsilon)) - addLocation(locations, param, v1, c1, 0, c1p1, v2, c2, 0, c1p1); + addLocation(locations, param, v1, c1, 0, c1p1, v2, c2, 0, c2p1); if (!param.startConnected && c1p1.isClose(c2p2, epsilon)) - addLocation(locations, param, v1, c1, 0, c1p1, v2, c2, 1, c1p1); + addLocation(locations, param, v1, c1, 0, c1p1, v2, c2, 1, c2p2); // Determine the correct intersection method based on whether one or // curves are straight lines: (straight1 && straight2 @@ -1724,9 +1724,9 @@ new function() { // Scope for intersection using bezier fat-line clipping // Handle the special case where the first curve's end-point // overlaps with the second curve's start- or end-points. if (!param.endConnected && c1p2.isClose(c2p1, epsilon)) - addLocation(locations, param, v1, c1, 1, c1p2, v2, c2, 0, c1p2); + addLocation(locations, param, v1, c1, 1, c1p2, v2, c2, 0, c2p1); if (c1p2.isClose(c2p2, epsilon)) - addLocation(locations, param, v1, c1, 1, c1p2, v2, c2, 1, c1p2); + addLocation(locations, param, v1, c1, 1, c1p2, v2, c2, 1, c2p2); return locations; }, From d84a84c67f335d22cc80fa078898296c37fde377 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 13 Sep 2015 13:06:01 +0200 Subject: [PATCH 058/280] Change the way winding contributions are propagated The new approach preserves segment sequence. Relates to #777 --- src/path/Curve.js | 8 +- src/path/CurveLocation.js | 1 + src/path/PathItem.Boolean.js | 172 ++++++++++++++++++----------------- 3 files changed, 96 insertions(+), 85 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 0de4a154..711e0e6d 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -619,7 +619,7 @@ statics: { } else if (sy === -1) { ty = tx; } - // Use average if we're within epsilon + // Use average if we're within curve-time epsilon if (abs(tx - ty) < /*#=*/Numerical.CURVETIME_EPSILON) return (tx + ty) * 0.5; } @@ -1333,6 +1333,12 @@ new function() { // Scope for intersection using bezier fat-line clipping t1 = res[0]; t2 = res[1]; } + /* + var d1 = p1 ? p1.getDistance(Curve.getPoint(v1, t1)) : 0, + d2 = p2 ? p2.getDistance(Curve.getPoint(v2, t2)) : 0; + if (!Numerical.isZero(d1) || !Numerical.isZero(d2)) + debugger; + */ locations.push( new CurveLocation(c1, t1, p1 || Curve.getPoint(v1, t1), null, overlap, diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index a93acc43..e77cb7fd 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -70,6 +70,7 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ // TODO: Remove this once debug logging is removed. _intersection._other = true; } + this._segment = null; // To be determined, see #getSegment() // Also store references to segment1 and segment2, in case path // splitting / dividing is going to happen, in which case the segments // can be used to determine the new curves, see #getCurve(true) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 0dc56b86..dc87d606 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -96,11 +96,9 @@ PathItem.inject(new function() { // console.timeEnd('intersection'); splitPath(Curve._filterIntersections(locations, true)); - var chain = [], - segments = [], + var segments = [], // Aggregate of all curves in both operands, monotonic in y - monoCurves = [], - epsilon = /*#=*/Numerical.GEOMETRIC_EPSILON; + monoCurves = []; function collect(paths) { for (var i = 0, l = paths.length; i < l; i++) { @@ -116,91 +114,23 @@ PathItem.inject(new function() { collect(_path2._children || [_path2]); // Propagate the winding contribution. Winding contribution of curves // does not change between two intersections. - // First, sort all segments with an intersection to the beginning. - segments.sort(function(a, b) { - var _a = a._intersection, - _b = b._intersection; - return !_a && !_b || _a && _b ? 0 : _a ? -1 : 1; - }); + // First, propagate winding contributions for curve chains starting in + // all intersections: + for (var i = 0, l = locations.length; i < l; i++) { + propagateWinding(locations[i]._segment, _path1, _path2, monoCurves, + operation); + } + // Now process the segments that are not part of any intersecting chains for (var i = 0, l = segments.length; i < l; i++) { var segment = segments[i]; - if (segment._winding != null) - continue; - // Here we try to determine the most probable winding number - // contribution for this curve-chain. Once we have enough confidence - // in the winding contribution, we can propagate it until the - // intersection or end of a curve chain. - chain.length = 0; - var startSeg = segment, - totalLength = 0, - windingSum = 0; - do { - var length = segment.getCurve().getLength(); - chain.push({ segment: segment, length: length }); - totalLength += length; - segment = segment.getNext(); - } while (segment && !segment._intersection && segment !== startSeg); - // Calculate the average winding among three evenly distributed - // points along this curve chain as a representative winding number. - // This selection gives a better chance of returning a correct - // winding than equally dividing the curve chain, with the same - // (amortised) time. - for (var j = 0; j < 3; j++) { - // Try the points at 1/4, 2/4 and 3/4 of the total length: - var length = totalLength * (j + 1) / 4; - for (var k = 0, m = chain.length; k < m; k++) { - var node = chain[k], - curveLength = node.length; - if (length <= curveLength) { - // If the selected location on the curve falls onto its - // beginning or end, use the curve's center instead. - if (length < epsilon || curveLength - length < epsilon) - length = curveLength / 2; - var curve = node.segment.getCurve(), - pt = curve.getPointAt(length), - hor = isHorizontal(curve), - path = getMainPath(curve); - // While subtracting, we need to omit this curve if this - // curve is contributing to the second operand and is - // outside the first operand. - windingSum += operation === 'subtract' && _path2 - && (path === _path1 && _path2._getWinding(pt, hor) - || path === _path2 && !_path1._getWinding(pt, hor)) - ? 0 - : getWinding(pt, monoCurves, hor); - break; - } - length -= curveLength; - } - } - // Assign the average winding to the entire curve chain. - var winding = Math.round(windingSum / 3); - for (var j = chain.length - 1; j >= 0; j--) { - var seg = chain[j].segment, - inter = seg._intersection, - wind = winding; - // We need to handle the edge cases of overlapping curves - // differently based on the type of operation, and adjust the - // winding number accordingly: - if (inter && inter._overlap) { - switch (operation) { - case 'unite': - if (wind === 1) - wind = 2; - break; - case 'intersect': - if (wind === 2) - wind = 1; - break; - } - } - seg._winding = wind; + if (segment._winding == null) { + propagateWinding(segment, _path1, _path2, monoCurves, + operation); } } // Trace closed contours and insert them into the result. var result = new CompoundPath(Item.NO_INSERT); - result.addChildren(tracePaths(segments, monoCurves, operation, !_path2), - true); + result.addChildren(tracePaths(segments, monoCurves, operation), true); // See if the CompoundPath can be reduced to just a simple Path. result = result.reduce(); // Insert the resulting path above whichever of the two paths appear @@ -427,6 +357,80 @@ PathItem.inject(new function() { return Math.max(abs(windLeft), abs(windRight)); } + function propagateWinding(segment, path1, path2, monoCurves, operation) { + // Here we try to determine the most probable winding number + // contribution for the curve-chain starting with this segment. Once we + // have enough confidence in the winding contribution, we can propagate + // it until the next intersection or end of a curve chain. + var epsilon = /*#=*/Numerical.GEOMETRIC_EPSILON; + chain = [], + startSeg = segment, + totalLength = 0, + windingSum = 0; + do { + var length = segment.getCurve().getLength(); + chain.push({ segment: segment, length: length }); + totalLength += length; + segment = segment.getNext(); + } while (segment && !segment._intersection && segment !== startSeg); + // Calculate the average winding among three evenly distributed + // points along this curve chain as a representative winding number. + // This selection gives a better chance of returning a correct + // winding than equally dividing the curve chain, with the same + // (amortised) time. + for (var i = 0; i < 3; i++) { + // Try the points at 1/4, 2/4 and 3/4 of the total length: + var length = totalLength * (i + 1) / 4; + for (var k = 0, m = chain.length; k < m; k++) { + var node = chain[k], + curveLength = node.length; + if (length <= curveLength) { + // If the selected location on the curve falls onto its + // beginning or end, use the curve's center instead. + if (length < epsilon || curveLength - length < epsilon) + length = curveLength / 2; + var curve = node.segment.getCurve(), + pt = curve.getPointAt(length), + hor = isHorizontal(curve), + path = getMainPath(curve); + // While subtracting, we need to omit this curve if this + // curve is contributing to the second operand and is + // outside the first operand. + windingSum += operation === 'subtract' && path2 + && (path === path1 && path2._getWinding(pt, hor) + || path === path2 && !path1._getWinding(pt, hor)) + ? 0 + : getWinding(pt, monoCurves, hor); + break; + } + length -= curveLength; + } + } + // Assign the average winding to the entire curve chain. + var winding = Math.round(windingSum / 3); + for (var j = chain.length - 1; j >= 0; j--) { + var seg = chain[j].segment, + inter = seg._intersection, + wind = winding; + // We need to handle the edge cases of overlapping curves + // differently based on the type of operation, and adjust the + // winding number accordingly: + if (inter && inter._overlap) { + switch (operation) { + case 'unite': + if (wind === 1) + wind = 2; + break; + case 'intersect': + if (wind === 2) + wind = 1; + break; + } + } + seg._winding = wind; + } + } + var segmentOffset = {}; var pathIndices = {}; var pathIndex = 0; @@ -523,7 +527,7 @@ PathItem.inject(new function() { for (var i = 0, seg, startSeg, l = segments.length; i < l; i++) { seg = startSeg = segments[i]; if (seg._visited || !operator(seg._winding)) { - drawSegment(seg, 'ignore', i, 'red'); + drawSegment(seg, seg._visited ? 'visited' : 'ignore', i, 'red'); continue; } var path = new Path(Item.NO_INSERT), From 52c0e3e22578e2c5a4cef7da2a5c60394dc00bf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 13 Sep 2015 13:26:08 +0200 Subject: [PATCH 059/280] Fix boolean test to adress shifted segment sequence. We really need a circular check for closed path geometry. --- test/tests/Path_Boolean.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/tests/Path_Boolean.js b/test/tests/Path_Boolean.js index 4293c6b0..4967693b 100644 --- a/test/tests/Path_Boolean.js +++ b/test/tests/Path_Boolean.js @@ -51,5 +51,5 @@ test('ring.subtract(square); #610', function() { var ring = outer.subtract(inner); var result = ring.subtract(square); - equals(result.pathData, 'M-10,131.62689c-68.2302,-5.11075 -122,-62.08951 -122,-131.62689c0,-69.53737 53.7698,-126.51614 122,-131.62689l0,32.12064c-50.53323,5.01724 -90,47.65277 -90,99.50625c0,51.85348 39.46677,94.489 90,99.50625z'); + equals(result.pathData, 'M-132,0c0,-69.53737 53.7698,-126.51614 122,-131.62689l0,32.12064c-50.53323,5.01724 -90,47.65277 -90,99.50625c0,51.85348 39.46677,94.489 90,99.50625l0,32.12064c-68.2302,-5.11075 -122,-62.08951 -122,-131.62689z'); }); From f029d5f9dabd174cc28eb89bf3a01d5eed70969c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 13 Sep 2015 13:41:53 +0200 Subject: [PATCH 060/280] Write docs for the new EPSILON values. --- src/util/Numerical.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/util/Numerical.js b/src/util/Numerical.js index 5e6481ee..00a9ce2b 100644 --- a/src/util/Numerical.js +++ b/src/util/Numerical.js @@ -81,7 +81,18 @@ var Numerical = new function() { * range (see MACHINE_EPSILON). */ EPSILON: EPSILON, + /** + * The epsilon to be used when handling curve-time parameters. This + * cannot be smaller, because errors add up to about 1e-7 in the bezier + * fat-line clipping code as a result of recursive sub-division. + */ CURVETIME_EPSILON: 1e-6, + /** + * The epsilon to be used when performing "geometric" checks, such as + * point distances and examining cross products to check for + * collinearity. This value is somewhat arbitrary and was chosen by + * trial and error. + */ GEOMETRIC_EPSILON: 1e-9, /** * MACHINE_EPSILON for a double precision (Javascript Number) is From 56da70c0304bd22f0274ccaa6c1551b6087549dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 13 Sep 2015 13:43:50 +0200 Subject: [PATCH 061/280] No need to pass TOLERANCE. We're comparing with a default tolerance of 1e-5. --- test/tests/Matrix.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/test/tests/Matrix.js b/test/tests/Matrix.js index 350a1c3d..6a5c1967 100644 --- a/test/tests/Matrix.js +++ b/test/tests/Matrix.js @@ -16,8 +16,7 @@ test('Decomposition: rotate()', function() { var m = new Matrix().rotate(a), s = 'new Matrix().rotate(' + a + ')'; equals(m.getRotation(), Base.pick(ea, a), - s + '.getRotation()', - Numerical.TOLERANCE); + s + '.getRotation()'); equals(m.getScaling(), new Point(1, 1), s + '.getScaling()'); } @@ -42,8 +41,7 @@ test('Decomposition: scale()', function() { var m = new Matrix().scale(sx, sy), s = 'new Matrix().scale(' + sx + ', ' + sy + ')'; equals(m.getRotation(), ea || 0, - s + '.getRotation()', - Numerical.TOLERANCE); + s + '.getRotation()'); equals(m.getScaling(), new Point(Base.pick(ex, sx), Base.pick(ey, sy)), s + '.getScaling()'); } @@ -64,8 +62,7 @@ test('Decomposition: rotate() & scale()', function() { var m = new Matrix().scale(sx, sy).rotate(a), s = 'new Matrix().scale(' + sx + ', ' + sy + ').rotate(' + a + ')'; equals(m.getRotation(), ea || a, - s + '.getRotation()', - Numerical.TOLERANCE); + s + '.getRotation()'); equals(m.getScaling(), new Point(Base.pick(ex, sx), Base.pick(ey, sy)), s + '.getScaling()'); } From ea2ff5ec289b023643efdc5054cb022f7aba9554 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 13 Sep 2015 13:45:20 +0200 Subject: [PATCH 062/280] Increase precision in Curve.getParameterOf() Usually only requires 0-1 more iteration. --- src/path/Curve.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 711e0e6d..09844fe5 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1285,8 +1285,8 @@ new function() { // Scope for methods that require private functions } // Start with out initial guess for x. // NOTE: guess is a negative value when not looking forward. - return Numerical.findRoot(f, ds, start + guess, a, b, 16, - /*#=*/Numerical.CURVETIME_EPSILON); + return Numerical.findRoot(f, ds, start + guess, a, b, 32, + /*#=*/Numerical.EPSILON); }, getPoint: function(v, t) { From e2d2c836e5a107772719711bbed109a08d42d16f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 13 Sep 2015 14:19:56 +0200 Subject: [PATCH 063/280] Some boolean code clean-up. --- src/path/Curve.js | 20 ++++++++++++++++++++ src/path/PathItem.Boolean.js | 29 ++++++++++------------------- 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 09844fe5..0b5772c1 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -888,6 +888,26 @@ statics: { isCollinear: function(curve) { return curve && this.isStraight() && curve.isStraight() && this.getVector().isCollinear(curve.getVector()); + }, + + /** + * Checks if the curve is a straight horizontal line. + * + * @return {Boolean} {@true if the line is horizontal} + */ + isHorizontal: function() { + return this.isStraight() && Math.abs(this.getTangentAt(0.5, true).y) + < /*#=*/Numerical.GEOMETRIC_EPSILON; + }, + + /** + * Checks if the curve is a straight vertical line. + * + * @return {Boolean} {@true if the line is vertical} + */ + isVertical: function() { + return this.isStraight() && Math.abs(this.getTangentAt(0.5, true).x) + < /*#=*/Numerical.GEOMETRIC_EPSILON; } }), /** @lends Curve# */{ // Explicitly deactivate the creation of beans, as we have functions here diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index dc87d606..df705d73 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -222,19 +222,6 @@ PathItem.inject(new function() { } } - function getMainPath(item) { - var path = item._path, - parent = path._parent; - return parent instanceof CompoundPath ? parent : path; - } - - function isHorizontal(curve) { - // Determine if the curve is a horizontal straight curve by checking the - // slope of it's tangent. - return curve.isStraight() && Math.abs(curve.getTangentAt(0.5, true).y) - < /*#=*/Numerical.GEOMETRIC_EPSILON; - } - /** * Private method that returns the winding contribution of the given point * with respect to a given set of monotone curves. @@ -368,8 +355,9 @@ PathItem.inject(new function() { totalLength = 0, windingSum = 0; do { - var length = segment.getCurve().getLength(); - chain.push({ segment: segment, length: length }); + var curve = segment.getCurve(), + length = curve.getLength(); + chain.push({ segment: segment, curve: curve, length: length }); totalLength += length; segment = segment.getNext(); } while (segment && !segment._intersection && segment !== startSeg); @@ -389,10 +377,13 @@ PathItem.inject(new function() { // beginning or end, use the curve's center instead. if (length < epsilon || curveLength - length < epsilon) length = curveLength / 2; - var curve = node.segment.getCurve(), + var curve = node.curve, + path = curve._path, + parent = path._parent, pt = curve.getPointAt(length), - hor = isHorizontal(curve), - path = getMainPath(curve); + hor = curve.isHorizontal(); + if (parent instanceof CompoundPath) + path = parent; // While subtracting, we need to omit this curve if this // curve is contributing to the second operand and is // outside the first operand. @@ -564,7 +555,7 @@ PathItem.inject(new function() { drawSegment(seg, 'overlap ' + dir, i, 'orange'); var curve = interSeg.getCurve(); if (getWinding(curve.getPointAt(0.5, true), - monoCurves, isHorizontal(curve)) === 1) { + monoCurves, curve.isHorizontal()) === 1) { seg._visited = interSeg._visited; seg = interSeg; dir = 1; From b231e9b2a84762941edfd5ed00d23377dcd8acf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 13 Sep 2015 16:06:24 +0200 Subject: [PATCH 064/280] Accept CompoundPath items as children of CompoundPath items. Just add their children and remove the parent. Closes #541 --- src/path/CompoundPath.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/path/CompoundPath.js b/src/path/CompoundPath.js index 343eacab..69c2df01 100644 --- a/src/path/CompoundPath.js +++ b/src/path/CompoundPath.js @@ -102,6 +102,16 @@ var CompoundPath = PathItem.extend(/** @lends CompoundPath# */{ }, insertChildren: function insertChildren(index, items, _preserve) { + // Convert CompoundPath items in the children list by adding their + // children to the list and removing their parent. + var before = items.slice(); + for (var i = items.length - 1; i >= 0; i--) { + var item = items[i]; + if (item instanceof CompoundPath) { + items.splice.apply(items, [i, 1].concat(item.removeChildren())); + item.remove(); + } + } // Pass on 'path' for _type, to make sure that only paths are added as // children. items = insertChildren.base.call(this, index, items, _preserve, Path); From fbb0f3f37d4ecaca50919002830fca40846ad7f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 13 Sep 2015 21:50:35 +0200 Subject: [PATCH 065/280] Remove left-over debugging code. --- src/path/CompoundPath.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/path/CompoundPath.js b/src/path/CompoundPath.js index 69c2df01..aca47247 100644 --- a/src/path/CompoundPath.js +++ b/src/path/CompoundPath.js @@ -104,7 +104,6 @@ var CompoundPath = PathItem.extend(/** @lends CompoundPath# */{ insertChildren: function insertChildren(index, items, _preserve) { // Convert CompoundPath items in the children list by adding their // children to the list and removing their parent. - var before = items.slice(); for (var i = items.length - 1; i >= 0; i--) { var item = items[i]; if (item instanceof CompoundPath) { From b532c9cce2c064a237b2f392b3d8ae4df3e73a12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 13 Sep 2015 22:12:04 +0200 Subject: [PATCH 066/280] Handle boolean exclusion as a special case. Switching each time an intersection is encountered. Closes #781 --- src/path/PathItem.Boolean.js | 108 +++++++++++++++++------------------ 1 file changed, 53 insertions(+), 55 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index df705d73..2f53f76e 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -44,20 +44,19 @@ PathItem.inject(new function() { } }; + // Creates a cloned version of the path that we can modify freely, with its + // matrix applied to its geometry. Calls #reduce() to simplify compound + // paths and remove empty curves, and #reorient() to make sure all paths + // have correct winding direction. + function preparePath(path) { + return path.clone(false).reduce().reorient().transform(null, true, true); + } + // Boolean operators return true if a curve with the given winding // contribution contributes to the final result or not. They are called // for each curve in the graph after curves in the operands are // split at intersections. function computeBoolean(path1, path2, operation) { - // Creates a cloned version of the path that we can modify freely, with - // its matrix applied to its geometry. Calls #reduce() to simplify - // compound paths and remove empty curves, and #reorient() to make sure - // all paths have correct winding direction. - function preparePath(path) { - return path.clone(false).reduce().reorient().transform(null, true, - true); - } - // We do not modify the operands themselves, but create copies instead, // fas produced by the calls to preparePath(). // Note that the result paths might not belong to the same type @@ -153,7 +152,7 @@ PathItem.inject(new function() { * @param {CurveLocation[]} intersections Array of CurveLocation objects */ function splitPath(intersections) { - if (false) { + if (window.report) { console.log('Intersections', intersections.length); intersections.forEach(function(inter) { if (inter._other) @@ -443,12 +442,12 @@ PathItem.inject(new function() { var pathCount = 0; var reportWindings = false && !window.silent; var reportSegments = false && !window.silent; - var textAngle = 0; - var fontSize = 1 / paper.project.activeLayer.scaling.x; + var textAngle = 20; + var fontSize = 5 / paper.project.activeLayer.scaling.x; function labelSegment(seg, text, color) { var point = seg.point; - var key = Math.round(point.x / 10) + ',' + Math.round(point.y / 10); + var key = Math.round(point.x) + ',' + Math.round(point.y); var offset = segmentOffset[key] || 0; segmentOffset[key] = offset + 1; var text = new PointText({ @@ -459,6 +458,7 @@ PathItem.inject(new function() { fillColor: color, fontSize: fontSize }); + // TODO! PointText should have pivot in #point by default! text.pivot = text.globalToLocal(text.point); text.rotation = textAngle; } @@ -515,37 +515,44 @@ PathItem.inject(new function() { // emerge from switching to 0 and 1 in edge cases. tMin = /*#=*/Numerical.CURVETIME_EPSILON, tMax = 1 - tMin; - for (var i = 0, seg, startSeg, l = segments.length; i < l; i++) { - seg = startSeg = segments[i]; + for (var i = 0, l = segments.length; i < l; i++) { + var seg = segments[i]; if (seg._visited || !operator(seg._winding)) { - drawSegment(seg, seg._visited ? 'visited' : 'ignore', i, 'red'); + // drawSegment(seg, seg._visited ? 'visited' : 'filtered', i, 'red'); continue; } var path = new Path(Item.NO_INSERT), + startSeg = seg, inter = seg._intersection, - startInterSeg = inter && inter._segment, + otherSeg = inter && inter._segment, + startOtherSeg = otherSeg, added = false, // Whether a first segment as added already dir = 1; do { var handleIn = dir > 0 ? seg._handleIn : seg._handleOut, - handleOut = dir > 0 ? seg._handleOut : seg._handleIn, - inter = seg._intersection, - interSeg = inter && inter._segment; + handleOut = dir > 0 ? seg._handleOut : seg._handleIn; // If the intersection segment is valid, try switching to // it, with an appropriate direction to continue traversal. // Else, stay on the same contour. - if (added && interSeg && interSeg !== startSeg) { - if (interSeg._path === seg._path) { // Self-intersection + if (added && otherSeg && otherSeg !== startSeg) { + if (otherSeg._path === seg._path) { // Self-intersection drawSegment(seg, 'self-int ' + dir, i, 'red'); // Switch to the intersection segment, as we need to // resolving self-Intersections. - seg._visited = interSeg._visited; - seg = interSeg; + seg = otherSeg; dir = 1; } else if (operator(seg._winding)) { - // Do not switch to the intersection as the segment is - // part of the boolean result. - drawSegment(seg, 'keep', i, 'black'); + // We need to handle exclusion separately and switch on + // every intersection that's part of the result. + if (operation === 'exclude') { + seg = otherSeg; + dir = 1; + drawSegment(seg, 'exclude', i, 'green'); + } else { + // Do not switch to the intersection as the segment + // is part of the boolean result. + drawSegment(seg, 'keep', i, 'black'); + } } else if (inter._overlap && operation !== 'intersect') { // Switch to the overlapping intersection segment // if its winding number along the curve is 1, to @@ -553,11 +560,10 @@ PathItem.inject(new function() { // NOTE: We cannot check the next (overlapping) // segment since its winding number will always be 2 drawSegment(seg, 'overlap ' + dir, i, 'orange'); - var curve = interSeg.getCurve(); + var curve = otherSeg.getCurve(); if (getWinding(curve.getPointAt(0.5, true), monoCurves, curve.isHorizontal()) === 1) { - seg._visited = interSeg._visited; - seg = interSeg; + seg = otherSeg; dir = 1; } } else { @@ -567,7 +573,7 @@ PathItem.inject(new function() { var t1 = c1.getTangentAt(dir < 0 ? tMin : tMax, true), // Get both curves at the intersection // (except the entry curves). - c4 = interSeg.getCurve(), + c4 = otherSeg.getCurve(), c3 = c4.getPrevious(), // Calculate their winding values and tangents. t3 = c3.getTangentAt(tMax, true), @@ -594,11 +600,11 @@ PathItem.inject(new function() { dir = 1; } else { // Switch to the intersection segment. - seg._visited = interSeg._visited; - seg = interSeg; - drawSegment(seg, 'switch', i, 'green'); + seg = otherSeg; + // TODO:Why is this necessary, why not always 1? if (nextSeg._visited) dir = 1; + drawSegment(seg, 'switch', i, 'green'); } } else { drawSegment(seg, 'no cross', i, 'blue'); @@ -612,35 +618,27 @@ PathItem.inject(new function() { // Add the current segment to the path, and mark the added // segment as visited. path.add(new Segment(seg._point, added && handleIn, handleOut)); - added = true; seg._visited = true; + added = true; // Move to the next segment according to the traversal direction seg = dir > 0 ? seg.getNext() : seg. getPrevious(); + inter = seg && seg._intersection; + otherSeg = inter && inter._segment; if (reportSegments) { console.log(seg, seg && !seg._visited, - seg !== startSeg, seg !== startInterSeg, - seg && seg._intersection, seg && operator(seg._winding)); - if (!(seg && !seg._visited - && seg !== startSeg && seg !== startInterSeg - && (seg._intersection || operator(seg._winding)))) { - if (seg) { - new Path.Circle({ - center: seg.point, - radius: fontSize / 2, - fillColor: 'red', - strokeScaling: false - }); - } - } + seg !== startSeg, seg !== startOtherSeg, + inter, seg && operator(seg._winding)); } - } while (seg && !seg._visited - && seg !== startSeg && seg !== startInterSeg - && (seg._intersection || operator(seg._winding))); + } while (seg && seg !== startSeg && seg !== startOtherSeg + // Exclusion switches on each intersection, we need to look + // ahead & carry on if the other segment wasn't visited yet. + && (!seg._visited || operation === 'exclude' + && otherSeg && !otherSeg._visited) + && (inter || operator(seg._winding))); // Finish with closing the paths if necessary, correctly linking up // curves etc. - if (seg && (seg === startSeg || seg === startInterSeg)) { - path.firstSegment.setHandleIn((seg === startInterSeg - ? startInterSeg : seg)._handleIn); + if (seg === startSeg || seg === startOtherSeg) { + path.firstSegment.setHandleIn(seg._handleIn); path.setClosed(true); if (reportSegments) { console.log('Boolean operation completed', From e85586d0fe9f35adec31ef4415d3d59f83ac721d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Mon, 14 Sep 2015 00:51:46 +0200 Subject: [PATCH 067/280] Improve handling of exclude() operations. Determine wether to switch to other intersection or not based on tangents. Closes #781 again. --- src/path/PathItem.Boolean.js | 64 +++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 2f53f76e..60376fff 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -152,7 +152,7 @@ PathItem.inject(new function() { * @param {CurveLocation[]} intersections Array of CurveLocation objects */ function splitPath(intersections) { - if (window.report) { + if (window.reportIntersections) { console.log('Intersections', intersections.length); intersections.forEach(function(inter) { if (inter._other) @@ -440,8 +440,6 @@ PathItem.inject(new function() { function tracePaths(segments, monoCurves, operation) { var segmentCount = 0; var pathCount = 0; - var reportWindings = false && !window.silent; - var reportSegments = false && !window.silent; var textAngle = 20; var fontSize = 5 / paper.project.activeLayer.scaling.x; @@ -464,7 +462,7 @@ PathItem.inject(new function() { } function drawSegment(seg, text, index, color) { - if (!reportSegments) + if (!window.reportSegments) return; new Path.Circle({ center: seg.point, @@ -488,7 +486,7 @@ PathItem.inject(new function() { - for (var i = 0; i < (reportWindings ? segments.length : 0); i++) { + for (var i = 0; i < (window.reportWindings ? segments.length : 0); i++) { var seg = segments[i]; path = seg._path, id = path._id, @@ -509,6 +507,7 @@ PathItem.inject(new function() { var paths = [], operator = operators[operation], + epsilon = /*#=*/Numerical.EPSILON, // Values for getTangentAt() that are almost 0 and 1. // NOTE: Even though getTangentAt() supports 0 and 1 instead of // tMin and tMax, we still need to use this instead, as other issues @@ -525,7 +524,7 @@ PathItem.inject(new function() { startSeg = seg, inter = seg._intersection, otherSeg = inter && inter._segment, - startOtherSeg = otherSeg, + otherStartSeg = otherSeg, added = false, // Whether a first segment as added already dir = 1; do { @@ -545,9 +544,17 @@ PathItem.inject(new function() { // We need to handle exclusion separately and switch on // every intersection that's part of the result. if (operation === 'exclude') { - seg = otherSeg; - dir = 1; - drawSegment(seg, 'exclude', i, 'green'); + // Look at the crossing tangents to decide whether + // to switch over or not. + var t1 = seg.getCurve().getTangentAt(tMin, true), + t2 = otherSeg.getCurve().getTangentAt(tMin, true); + if (Math.abs(t1.cross(t2)) > epsilon) { + seg = otherSeg; + drawSegment(seg, 'exclude:switch', i, 'green'); + dir = 1; + } else { + drawSegment(seg, 'exclude:no cross', i, 'blue'); + } } else { // Do not switch to the intersection as the segment // is part of the boolean result. @@ -567,31 +574,28 @@ PathItem.inject(new function() { dir = 1; } } else { - var c1 = seg.getCurve(); - if (dir > 0) - c1 = c1.getPrevious(); - var t1 = c1.getTangentAt(dir < 0 ? tMin : tMax, true), + var t = seg.getCurve().getTangentAt(tMin, true), // Get both curves at the intersection // (except the entry curves). - c4 = otherSeg.getCurve(), - c3 = c4.getPrevious(), + c2 = otherSeg.getCurve(), + c1 = c2.getPrevious(), // Calculate their winding values and tangents. - t3 = c3.getTangentAt(tMax, true), - t4 = c4.getTangentAt(tMin, true), + t1 = c1.getTangentAt(tMax, true), + t2 = c2.getTangentAt(tMin, true), // Cross product of the entry and exit tangent // vectors at the intersection, will let us select // the correct contour to traverse next. - w3 = t1.cross(t3), - w4 = t1.cross(t4); - if (Math.abs(w3 * w4) > /*#=*/Numerical.EPSILON) { + w1 = t.cross(t1), + w2 = t.cross(t2); + if (Math.abs(w1 * w2) > epsilon) { // Do not attempt to switch contours if we aren't // sure that there is a possible candidate. - var curve = w3 < w4 ? c3 : c4, + var curve = w1 < w2 ? c1 : c2, nextCurve = operator(curve._segment1._winding) ? curve - : w3 < w4 ? c4 : c3, + : w1 < w2 ? c2 : c1, nextSeg = nextCurve._segment1; - dir = nextCurve === c3 ? -1 : 1; + dir = nextCurve === c1 ? -1 : 1; // If we didn't find a suitable direction for next // contour to traverse, stay on the same contour. if (nextSeg._visited && seg._path !== nextSeg._path @@ -601,7 +605,7 @@ PathItem.inject(new function() { } else { // Switch to the intersection segment. seg = otherSeg; - // TODO:Why is this necessary, why not always 1? + // TODO: Find a case that actually requires this if (nextSeg._visited) dir = 1; drawSegment(seg, 'switch', i, 'green'); @@ -624,12 +628,12 @@ PathItem.inject(new function() { seg = dir > 0 ? seg.getNext() : seg. getPrevious(); inter = seg && seg._intersection; otherSeg = inter && inter._segment; - if (reportSegments) { + if (window.reportSegments) { console.log(seg, seg && !seg._visited, - seg !== startSeg, seg !== startOtherSeg, + seg !== startSeg, seg !== otherStartSeg, inter, seg && operator(seg._winding)); } - } while (seg && seg !== startSeg && seg !== startOtherSeg + } while (seg && seg !== startSeg && seg !== otherStartSeg // Exclusion switches on each intersection, we need to look // ahead & carry on if the other segment wasn't visited yet. && (!seg._visited || operation === 'exclude' @@ -637,10 +641,10 @@ PathItem.inject(new function() { && (inter || operator(seg._winding))); // Finish with closing the paths if necessary, correctly linking up // curves etc. - if (seg === startSeg || seg === startOtherSeg) { + if (seg === startSeg || seg === otherStartSeg) { path.firstSegment.setHandleIn(seg._handleIn); path.setClosed(true); - if (reportSegments) { + if (window.reportSegments) { console.log('Boolean operation completed', '#' + (pathCount + 1) + '.' + (path ? path._segments.length + 1 : 1)); @@ -660,7 +664,7 @@ PathItem.inject(new function() { if (path && (path._segments.length > 4 || !Numerical.isZero(path.getArea()))) paths.push(path); - if (reportSegments) { + if (window.reportSegments) { pathCount++; } } From a665175a8957c1e4f75c2ad97a630b56a1ae0aa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Mon, 14 Sep 2015 01:20:03 +0200 Subject: [PATCH 068/280] Substantial simplifications in boolean code. These were probably made possible thanks to increased precision elsewhere in the lib. --- src/path/PathItem.Boolean.js | 74 ++++++++---------------------------- 1 file changed, 15 insertions(+), 59 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 60376fff..aa0d03e8 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -525,53 +525,33 @@ PathItem.inject(new function() { inter = seg._intersection, otherSeg = inter && inter._segment, otherStartSeg = otherSeg, - added = false, // Whether a first segment as added already - dir = 1; + added = false; // Whether a first segment as added already do { - var handleIn = dir > 0 ? seg._handleIn : seg._handleOut, - handleOut = dir > 0 ? seg._handleOut : seg._handleIn; + var handleIn = added && seg._handleIn; // If the intersection segment is valid, try switching to // it, with an appropriate direction to continue traversal. // Else, stay on the same contour. if (added && otherSeg && otherSeg !== startSeg) { if (otherSeg._path === seg._path) { // Self-intersection - drawSegment(seg, 'self-int ' + dir, i, 'red'); + drawSegment(seg, 'self-int', i, 'red'); // Switch to the intersection segment, as we need to // resolving self-Intersections. seg = otherSeg; - dir = 1; - } else if (operator(seg._winding)) { - // We need to handle exclusion separately and switch on - // every intersection that's part of the result. - if (operation === 'exclude') { - // Look at the crossing tangents to decide whether - // to switch over or not. - var t1 = seg.getCurve().getTangentAt(tMin, true), - t2 = otherSeg.getCurve().getTangentAt(tMin, true); - if (Math.abs(t1.cross(t2)) > epsilon) { - seg = otherSeg; - drawSegment(seg, 'exclude:switch', i, 'green'); - dir = 1; - } else { - drawSegment(seg, 'exclude:no cross', i, 'blue'); - } - } else { - // Do not switch to the intersection as the segment - // is part of the boolean result. - drawSegment(seg, 'keep', i, 'black'); - } - } else if (inter._overlap && operation !== 'intersect') { + } else if (operation !== 'exclude' && operator(seg._winding)) { + // Do not switch to the intersection as the segment + // is part of the boolean result. + drawSegment(seg, 'keep', i, 'black'); + } else if (operation !== 'intersect' && inter._overlap) { // Switch to the overlapping intersection segment // if its winding number along the curve is 1, to // leave the overlapping area. // NOTE: We cannot check the next (overlapping) // segment since its winding number will always be 2 - drawSegment(seg, 'overlap ' + dir, i, 'orange'); + drawSegment(seg, 'overlap', i, 'orange'); var curve = otherSeg.getCurve(); if (getWinding(curve.getPointAt(0.5, true), monoCurves, curve.isHorizontal()) === 1) { seg = otherSeg; - dir = 1; } } else { var t = seg.getCurve().getTangentAt(tMin, true), @@ -581,51 +561,27 @@ PathItem.inject(new function() { c1 = c2.getPrevious(), // Calculate their winding values and tangents. t1 = c1.getTangentAt(tMax, true), - t2 = c2.getTangentAt(tMin, true), + t2 = c2.getTangentAt(tMin, true); // Cross product of the entry and exit tangent // vectors at the intersection, will let us select // the correct contour to traverse next. - w1 = t.cross(t1), - w2 = t.cross(t2); - if (Math.abs(w1 * w2) > epsilon) { - // Do not attempt to switch contours if we aren't - // sure that there is a possible candidate. - var curve = w1 < w2 ? c1 : c2, - nextCurve = operator(curve._segment1._winding) - ? curve - : w1 < w2 ? c2 : c1, - nextSeg = nextCurve._segment1; - dir = nextCurve === c1 ? -1 : 1; - // If we didn't find a suitable direction for next - // contour to traverse, stay on the same contour. - if (nextSeg._visited && seg._path !== nextSeg._path - || !operator(nextSeg._winding)) { - drawSegment(nextSeg, 'not suitable', i, 'orange'); - dir = 1; - } else { - // Switch to the intersection segment. - seg = otherSeg; - // TODO: Find a case that actually requires this - if (nextSeg._visited) - dir = 1; - drawSegment(seg, 'switch', i, 'green'); - } + if (Math.abs(t.cross(t1) * t.cross(t2)) > epsilon) { + seg = otherSeg; + drawSegment(seg, 'switch', i, 'green'); } else { drawSegment(seg, 'no cross', i, 'blue'); - dir = 1; } } - handleOut = dir > 0 ? seg._handleOut : seg._handleIn; } else { drawSegment(seg, 'keep', i, 'black'); } // Add the current segment to the path, and mark the added // segment as visited. - path.add(new Segment(seg._point, added && handleIn, handleOut)); + path.add(new Segment(seg._point, handleIn, seg._handleOut)); seg._visited = true; added = true; // Move to the next segment according to the traversal direction - seg = dir > 0 ? seg.getNext() : seg. getPrevious(); + seg = seg.getNext(); inter = seg && seg._intersection; otherSeg = inter && inter._segment; if (window.reportSegments) { From fec479167ce889674a73c5f2e5f108407d180f98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Mon, 14 Sep 2015 15:16:52 +0200 Subject: [PATCH 069/280] Improve debug logging and drawing. --- src/path/PathItem.Boolean.js | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index aa0d03e8..dc28a591 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -84,7 +84,7 @@ PathItem.inject(new function() { self.forEach(function(inter) { new Path.Circle({ center: inter.point, - radius: 3, + radius: fontSize / 2 * scaleFactor, fillColor: 'red' }); }); @@ -144,6 +144,10 @@ PathItem.inject(new function() { return result; } + var scaleFactor = 1 / 3000; // 0.5; // 1 / 3000; + var textAngle = 60; + var fontSize = 5; + /** * Private method for splitting a PathItem at the given intersections. * The routine works for both self intersections and intersections @@ -153,7 +157,7 @@ PathItem.inject(new function() { */ function splitPath(intersections) { if (window.reportIntersections) { - console.log('Intersections', intersections.length); + console.log('Intersections', intersections.length / 2); intersections.forEach(function(inter) { if (inter._other) return; @@ -166,8 +170,9 @@ PathItem.inject(new function() { 'o', !!other._overlap]; new Path.Circle({ center: inter.point, - radius: 3, - strokeColor: 'green' + radius: fontSize / 2 * scaleFactor, + strokeColor: 'green', + strokeScaling: false }); console.log(log.map(function(v) { return v == null ? '-' : v @@ -440,17 +445,17 @@ PathItem.inject(new function() { function tracePaths(segments, monoCurves, operation) { var segmentCount = 0; var pathCount = 0; - var textAngle = 20; - var fontSize = 5 / paper.project.activeLayer.scaling.x; function labelSegment(seg, text, color) { var point = seg.point; - var key = Math.round(point.x) + ',' + Math.round(point.y); + var key = Math.round(point.x / scaleFactor) + ',' + Math.round(point.y / scaleFactor); var offset = segmentOffset[key] || 0; segmentOffset[key] = offset + 1; + var size = fontSize * scaleFactor; var text = new PointText({ - point: point.add(new Point(fontSize, fontSize / 2) - .rotate(textAngle).add(0, offset * fontSize * 1.2)), + point: point.add( + new Point(size, size / 2).add(0, offset * size * 1.2) + .rotate(textAngle)), content: text, justification: 'left', fillColor: color, @@ -458,7 +463,8 @@ PathItem.inject(new function() { }); // TODO! PointText should have pivot in #point by default! text.pivot = text.globalToLocal(text.point); - text.rotation = textAngle; + text.scale(scaleFactor); + text.rotate(textAngle); } function drawSegment(seg, text, index, color) { @@ -466,7 +472,7 @@ PathItem.inject(new function() { return; new Path.Circle({ center: seg.point, - radius: fontSize / 2, + radius: fontSize / 2 * scaleFactor, strokeColor: color, strokeScaling: false }); From 7aef20ae6b2441c5ba439bd8d70146eb699eaf61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Mon, 14 Sep 2015 15:18:44 +0200 Subject: [PATCH 070/280] Compare intersection points instead of curve time when deciding to merge. Use same precision indepenent of curve length. --- src/path/Curve.js | 1 + src/path/CurveLocation.js | 6 ++---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 0b5772c1..5ad00858 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1662,6 +1662,7 @@ new function() { // Scope for intersection using bezier fat-line clipping if (pairs.length === 1 && pair[0] < pairs[0][0]) { pairs.unshift(pair); } else if (pairs.length === 0 + // TODO: Compare distance of points instead! || abs(pair[0] - pairs[0][0]) > timeEpsilon || abs(pair[1] - pairs[0][1]) > timeEpsilon) { pairs.push(pair); diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index e77cb7fd..5c3a9f08 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -284,10 +284,8 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ || loc instanceof CurveLocation // Call getCurve() and getParameter() to keep in sync && this.getCurve() === loc.getCurve() - // Use the same tolerance for curve time parameter - // comparisons as in Curve.js - && Math.abs(this.getParameter() - loc.getParameter()) - < /*#=*/Numerical.CURVETIME_EPSILON + && this.getPoint().isClose(loc.getPoint(), + /*#=*/Numerical.GEOMETRIC_EPSILON) && (_ignoreIntersection || (!this._intersection && !loc._intersection || this._intersection && this._intersection.equals( From 5e327f7a48115daf1041d5a02ad336ce28605978 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Mon, 14 Sep 2015 15:23:46 +0200 Subject: [PATCH 071/280] Revert "Substantial simplifications in boolean code." This reverts commit a665175a8957c1e4f75c2ad97a630b56a1ae0aa2. --- src/path/PathItem.Boolean.js | 74 ++++++++++++++++++++++++++++-------- 1 file changed, 59 insertions(+), 15 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index dc28a591..36273485 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -531,33 +531,53 @@ PathItem.inject(new function() { inter = seg._intersection, otherSeg = inter && inter._segment, otherStartSeg = otherSeg, - added = false; // Whether a first segment as added already + added = false, // Whether a first segment as added already + dir = 1; do { - var handleIn = added && seg._handleIn; + var handleIn = dir > 0 ? seg._handleIn : seg._handleOut, + handleOut = dir > 0 ? seg._handleOut : seg._handleIn; // If the intersection segment is valid, try switching to // it, with an appropriate direction to continue traversal. // Else, stay on the same contour. if (added && otherSeg && otherSeg !== startSeg) { if (otherSeg._path === seg._path) { // Self-intersection - drawSegment(seg, 'self-int', i, 'red'); + drawSegment(seg, 'self-int ' + dir, i, 'red'); // Switch to the intersection segment, as we need to // resolving self-Intersections. seg = otherSeg; - } else if (operation !== 'exclude' && operator(seg._winding)) { - // Do not switch to the intersection as the segment - // is part of the boolean result. - drawSegment(seg, 'keep', i, 'black'); - } else if (operation !== 'intersect' && inter._overlap) { + dir = 1; + } else if (operator(seg._winding)) { + // We need to handle exclusion separately and switch on + // every intersection that's part of the result. + if (operation === 'exclude') { + // Look at the crossing tangents to decide whether + // to switch over or not. + var t1 = seg.getCurve().getTangentAt(tMin, true), + t2 = otherSeg.getCurve().getTangentAt(tMin, true); + if (Math.abs(t1.cross(t2)) > epsilon) { + seg = otherSeg; + drawSegment(seg, 'exclude:switch', i, 'green'); + dir = 1; + } else { + drawSegment(seg, 'exclude:no cross', i, 'blue'); + } + } else { + // Do not switch to the intersection as the segment + // is part of the boolean result. + drawSegment(seg, 'keep', i, 'black'); + } + } else if (inter._overlap && operation !== 'intersect') { // Switch to the overlapping intersection segment // if its winding number along the curve is 1, to // leave the overlapping area. // NOTE: We cannot check the next (overlapping) // segment since its winding number will always be 2 - drawSegment(seg, 'overlap', i, 'orange'); + drawSegment(seg, 'overlap ' + dir, i, 'orange'); var curve = otherSeg.getCurve(); if (getWinding(curve.getPointAt(0.5, true), monoCurves, curve.isHorizontal()) === 1) { seg = otherSeg; + dir = 1; } } else { var t = seg.getCurve().getTangentAt(tMin, true), @@ -567,27 +587,51 @@ PathItem.inject(new function() { c1 = c2.getPrevious(), // Calculate their winding values and tangents. t1 = c1.getTangentAt(tMax, true), - t2 = c2.getTangentAt(tMin, true); + t2 = c2.getTangentAt(tMin, true), // Cross product of the entry and exit tangent // vectors at the intersection, will let us select // the correct contour to traverse next. - if (Math.abs(t.cross(t1) * t.cross(t2)) > epsilon) { - seg = otherSeg; - drawSegment(seg, 'switch', i, 'green'); + w1 = t.cross(t1), + w2 = t.cross(t2); + if (Math.abs(w1 * w2) > epsilon) { + // Do not attempt to switch contours if we aren't + // sure that there is a possible candidate. + var curve = w1 < w2 ? c1 : c2, + nextCurve = operator(curve._segment1._winding) + ? curve + : w1 < w2 ? c2 : c1, + nextSeg = nextCurve._segment1; + dir = nextCurve === c1 ? -1 : 1; + // If we didn't find a suitable direction for next + // contour to traverse, stay on the same contour. + if (nextSeg._visited && seg._path !== nextSeg._path + || !operator(nextSeg._winding)) { + drawSegment(nextSeg, 'not suitable', i, 'orange'); + dir = 1; + } else { + // Switch to the intersection segment. + seg = otherSeg; + // TODO: Find a case that actually requires this + if (nextSeg._visited) + dir = 1; + drawSegment(seg, 'switch', i, 'green'); + } } else { drawSegment(seg, 'no cross', i, 'blue'); + dir = 1; } } + handleOut = dir > 0 ? seg._handleOut : seg._handleIn; } else { drawSegment(seg, 'keep', i, 'black'); } // Add the current segment to the path, and mark the added // segment as visited. - path.add(new Segment(seg._point, handleIn, seg._handleOut)); + path.add(new Segment(seg._point, added && handleIn, handleOut)); seg._visited = true; added = true; // Move to the next segment according to the traversal direction - seg = seg.getNext(); + seg = dir > 0 ? seg.getNext() : seg. getPrevious(); inter = seg && seg._intersection; otherSeg = inter && inter._segment; if (window.reportSegments) { From e4b403772151e13fc522caa0c02ba8cdefce28f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 15 Sep 2015 13:01:52 +0200 Subject: [PATCH 072/280] Revert "Improve handling of exclude() operations." This reverts commit e85586d0fe9f35adec31ef4415d3d59f83ac721d. --- src/path/PathItem.Boolean.js | 42 ++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 36273485..5d3f423a 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -513,7 +513,6 @@ PathItem.inject(new function() { var paths = [], operator = operators[operation], - epsilon = /*#=*/Numerical.EPSILON, // Values for getTangentAt() that are almost 0 and 1. // NOTE: Even though getTangentAt() supports 0 and 1 instead of // tMin and tMax, we still need to use this instead, as other issues @@ -550,17 +549,9 @@ PathItem.inject(new function() { // We need to handle exclusion separately and switch on // every intersection that's part of the result. if (operation === 'exclude') { - // Look at the crossing tangents to decide whether - // to switch over or not. - var t1 = seg.getCurve().getTangentAt(tMin, true), - t2 = otherSeg.getCurve().getTangentAt(tMin, true); - if (Math.abs(t1.cross(t2)) > epsilon) { - seg = otherSeg; - drawSegment(seg, 'exclude:switch', i, 'green'); - dir = 1; - } else { - drawSegment(seg, 'exclude:no cross', i, 'blue'); - } + seg = otherSeg; + dir = 1; + drawSegment(seg, 'exclude', i, 'green'); } else { // Do not switch to the intersection as the segment // is part of the boolean result. @@ -580,28 +571,31 @@ PathItem.inject(new function() { dir = 1; } } else { - var t = seg.getCurve().getTangentAt(tMin, true), + var c1 = seg.getCurve(); + if (dir > 0) + c1 = c1.getPrevious(); + var t1 = c1.getTangentAt(dir < 0 ? tMin : tMax, true), // Get both curves at the intersection // (except the entry curves). - c2 = otherSeg.getCurve(), - c1 = c2.getPrevious(), + c4 = otherSeg.getCurve(), + c3 = c4.getPrevious(), // Calculate their winding values and tangents. - t1 = c1.getTangentAt(tMax, true), - t2 = c2.getTangentAt(tMin, true), + t3 = c3.getTangentAt(tMax, true), + t4 = c4.getTangentAt(tMin, true), // Cross product of the entry and exit tangent // vectors at the intersection, will let us select // the correct contour to traverse next. - w1 = t.cross(t1), - w2 = t.cross(t2); - if (Math.abs(w1 * w2) > epsilon) { + w3 = t1.cross(t3), + w4 = t1.cross(t4); + if (Math.abs(w3 * w4) > /*#=*/Numerical.EPSILON) { // Do not attempt to switch contours if we aren't // sure that there is a possible candidate. - var curve = w1 < w2 ? c1 : c2, + var curve = w3 < w4 ? c3 : c4, nextCurve = operator(curve._segment1._winding) ? curve - : w1 < w2 ? c2 : c1, + : w3 < w4 ? c4 : c3, nextSeg = nextCurve._segment1; - dir = nextCurve === c1 ? -1 : 1; + dir = nextCurve === c3 ? -1 : 1; // If we didn't find a suitable direction for next // contour to traverse, stay on the same contour. if (nextSeg._visited && seg._path !== nextSeg._path @@ -611,7 +605,7 @@ PathItem.inject(new function() { } else { // Switch to the intersection segment. seg = otherSeg; - // TODO: Find a case that actually requires this + // TODO:Why is this necessary, why not always 1? if (nextSeg._visited) dir = 1; drawSegment(seg, 'switch', i, 'green'); From 7b3f8598f46b308a37f1d19aa4584dff3c23591e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 15 Sep 2015 14:11:27 +0200 Subject: [PATCH 073/280] Make sure all results of boolean operations are styled and inserted in the sample way. --- src/path/PathItem.Boolean.js | 37 ++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 5d3f423a..2c85d416 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -52,6 +52,23 @@ PathItem.inject(new function() { return path.clone(false).reduce().reorient().transform(null, true, true); } + function finishBoolean(ctor, paths, path1, path2) { + var result = new ctor(Item.NO_INSERT); + result.addChildren(paths, true); + // See if the CompoundPath can be reduced to just a simple Path. + result = result.reduce(); + // Insert the resulting path above whichever of the two paths appear + // further up in the stack. + result.insertAbove(path2 && path1.isSibling(path2) + && path1.getIndex() < path2.getIndex() + ? path2 : path1); + // Copy over the left-hand item's style and we're done. + // TODO: Consider using Item#_clone() for this, but find a way to not + // clone children / name (content). + result.setStyle(path1._style); + return result; + } + // Boolean operators return true if a curve with the given winding // contribution contributes to the final result or not. They are called // for each curve in the graph after curves in the operands are @@ -127,21 +144,8 @@ PathItem.inject(new function() { operation); } } - // Trace closed contours and insert them into the result. - var result = new CompoundPath(Item.NO_INSERT); - result.addChildren(tracePaths(segments, monoCurves, operation), true); - // See if the CompoundPath can be reduced to just a simple Path. - result = result.reduce(); - // Insert the resulting path above whichever of the two paths appear - // further up in the stack. - result.insertAbove(path2 && path1.isSibling(path2) - && path1.getIndex() < path2.getIndex() - ? path2 : path1); - // Copy over the left-hand item's style and we're done. - // TODO: Consider using Item#_clone() for this, but find a way to not - // clone children / name (content). - result.setStyle(path1._style); - return result; + return finishBoolean(CompoundPath, + tracePaths(segments, monoCurves, operation), path1, path2); } var scaleFactor = 1 / 3000; // 0.5; // 1 / 3000; @@ -746,7 +750,8 @@ PathItem.inject(new function() { * @return {Group} the resulting group item */ divide: function(path) { - return new Group([this.subtract(path), this.intersect(path)]); + return finishBoolean(Group, + [this.subtract(path), this.intersect(path)], this, path); } }; }); From 089738478c67e07c467f4e7747754b4ba48ca9d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 15 Sep 2015 15:03:12 +0200 Subject: [PATCH 074/280] Bring back boolean exclusion handling to reverted code. --- src/path/PathItem.Boolean.js | 71 ++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 39 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 2c85d416..bc328005 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -537,8 +537,7 @@ PathItem.inject(new function() { added = false, // Whether a first segment as added already dir = 1; do { - var handleIn = dir > 0 ? seg._handleIn : seg._handleOut, - handleOut = dir > 0 ? seg._handleOut : seg._handleIn; + var handleIn = dir > 0 ? seg._handleIn : seg._handleOut; // If the intersection segment is valid, try switching to // it, with an appropriate direction to continue traversal. // Else, stay on the same contour. @@ -549,19 +548,11 @@ PathItem.inject(new function() { // resolving self-Intersections. seg = otherSeg; dir = 1; - } else if (operator(seg._winding)) { - // We need to handle exclusion separately and switch on - // every intersection that's part of the result. - if (operation === 'exclude') { - seg = otherSeg; - dir = 1; - drawSegment(seg, 'exclude', i, 'green'); - } else { - // Do not switch to the intersection as the segment - // is part of the boolean result. - drawSegment(seg, 'keep', i, 'black'); - } - } else if (inter._overlap && operation !== 'intersect') { + } else if (operation !== 'exclude' && operator(seg._winding)) { + // Do not switch to the intersection as the segment is + // part of the boolean result. + drawSegment(seg, 'keep', i, 'black'); + } else if (operation !== 'intersect' && inter._overlap) { // Switch to the overlapping intersection segment // if its winding number along the curve is 1, to // leave the overlapping area. @@ -591,52 +582,54 @@ PathItem.inject(new function() { // the correct contour to traverse next. w3 = t1.cross(t3), w4 = t1.cross(t4); - if (Math.abs(w3 * w4) > /*#=*/Numerical.EPSILON) { + if (Math.abs(w3 * w4) < /*#=*/Numerical.EPSILON) { + drawSegment(seg, 'no cross', i, 'blue'); + dir = 1; // TODO: Why? Is this correct? + } else if (operation === 'exclude') { // Do not attempt to switch contours if we aren't // sure that there is a possible candidate. - var curve = w3 < w4 ? c3 : c4, - nextCurve = operator(curve._segment1._winding) - ? curve - : w3 < w4 ? c4 : c3, - nextSeg = nextCurve._segment1; - dir = nextCurve === c3 ? -1 : 1; // If we didn't find a suitable direction for next // contour to traverse, stay on the same contour. - if (nextSeg._visited && seg._path !== nextSeg._path - || !operator(nextSeg._winding)) { - drawSegment(nextSeg, 'not suitable', i, 'orange'); + // Switch to the intersection segment. + seg = otherSeg; + drawSegment(seg, 'exclude:switch', i, 'green'); + dir = 1; + } else { + // Do not attempt to switch contours if we aren't + // sure that there is a possible candidate. + var curve = w3 < w4 ? c3 : c4; + if (!operator(curve._segment1._winding)) + curve = curve === c3 ? c4 : c3; + dir = curve === c3 ? -1 : 1; + var next = curve._segment1; + // If we didn't find a suitable direction for next + // contour to traverse, stay on the same contour. + if (next._visited && seg._path !== next._path + || !operator(next._winding)) { + drawSegment(next, 'not suitable', i, 'orange'); dir = 1; } else { // Switch to the intersection segment. seg = otherSeg; - // TODO:Why is this necessary, why not always 1? - if (nextSeg._visited) - dir = 1; + if (next._visited) + dir = -dir; drawSegment(seg, 'switch', i, 'green'); } - } else { - drawSegment(seg, 'no cross', i, 'blue'); - dir = 1; } } - handleOut = dir > 0 ? seg._handleOut : seg._handleIn; } else { drawSegment(seg, 'keep', i, 'black'); } // Add the current segment to the path, and mark the added // segment as visited. - path.add(new Segment(seg._point, added && handleIn, handleOut)); + path.add(new Segment(seg._point, added && handleIn, + dir > 0 ? seg._handleOut : seg._handleIn)); seg._visited = true; added = true; // Move to the next segment according to the traversal direction - seg = dir > 0 ? seg.getNext() : seg. getPrevious(); + seg = dir > 0 ? seg.getNext() : seg.getPrevious(); inter = seg && seg._intersection; otherSeg = inter && inter._segment; - if (window.reportSegments) { - console.log(seg, seg && !seg._visited, - seg !== startSeg, seg !== otherStartSeg, - inter, seg && operator(seg._winding)); - } } while (seg && seg !== startSeg && seg !== otherStartSeg // Exclusion switches on each intersection, we need to look // ahead & carry on if the other segment wasn't visited yet. From 3ce7d88347c49117f2187341167aee24848adc4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 15 Sep 2015 16:31:05 +0200 Subject: [PATCH 075/280] Second attempt at simplifying boolean code. This time without endless loops. --- src/path/PathItem.Boolean.js | 123 +++++++++++++---------------------- 1 file changed, 44 insertions(+), 79 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index bc328005..a7ccad22 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -148,7 +148,7 @@ PathItem.inject(new function() { tracePaths(segments, monoCurves, operation), path1, path2); } - var scaleFactor = 1 / 3000; // 0.5; // 1 / 3000; + var scaleFactor = 1; // 1 / 3000; var textAngle = 60; var fontSize = 5; @@ -448,7 +448,7 @@ PathItem.inject(new function() { */ function tracePaths(segments, monoCurves, operation) { var segmentCount = 0; - var pathCount = 0; + var pathCount = 1; function labelSegment(seg, text, color) { var point = seg.point; @@ -481,7 +481,7 @@ PathItem.inject(new function() { strokeScaling: false }); var inter = seg._intersection; - labelSegment(seg, '#' + (pathCount + 1) + '.' + labelSegment(seg, '#' + pathCount + '.' + (path ? path._segments.length + 1 : 1) + ' ' + (segmentCount++) + '/' + index + ': ' + text + ' v: ' + !!seg._visited @@ -534,107 +534,74 @@ PathItem.inject(new function() { inter = seg._intersection, otherSeg = inter && inter._segment, otherStartSeg = otherSeg, - added = false, // Whether a first segment as added already - dir = 1; + added = false; // Whether a first segment as added already do { - var handleIn = dir > 0 ? seg._handleIn : seg._handleOut; - // If the intersection segment is valid, try switching to - // it, with an appropriate direction to continue traversal. - // Else, stay on the same contour. + var handleIn = added && seg._handleIn; if (added && otherSeg && otherSeg !== startSeg) { + // There is an intersection. If the intersecting segment is + // valid, try switching to it to continue traversal. + // Otherwise stay on the same contour. if (otherSeg._path === seg._path) { // Self-intersection - drawSegment(seg, 'self-int ' + dir, i, 'red'); - // Switch to the intersection segment, as we need to + drawSegment(seg, 'self-int', i, 'red'); + // Switch to the intersecting segment, as we need to // resolving self-Intersections. seg = otherSeg; - dir = 1; - } else if (operation !== 'exclude' && operator(seg._winding)) { - // Do not switch to the intersection as the segment is - // part of the boolean result. - drawSegment(seg, 'keep', i, 'black'); + } else if (operation !== 'exclude' + && operator(seg._winding)) { + // Do not switch to the intersecting segment as it is + // contained inside the boolean result. + drawSegment(seg, 'ignore-keep', i, 'black'); } else if (operation !== 'intersect' && inter._overlap) { // Switch to the overlapping intersection segment // if its winding number along the curve is 1, to // leave the overlapping area. // NOTE: We cannot check the next (overlapping) // segment since its winding number will always be 2 - drawSegment(seg, 'overlap ' + dir, i, 'orange'); + drawSegment(seg, 'overlap', i, 'orange'); var curve = otherSeg.getCurve(); if (getWinding(curve.getPointAt(0.5, true), monoCurves, curve.isHorizontal()) === 1) { seg = otherSeg; - dir = 1; } - } else { - var c1 = seg.getCurve(); - if (dir > 0) - c1 = c1.getPrevious(); - var t1 = c1.getTangentAt(dir < 0 ? tMin : tMax, true), - // Get both curves at the intersection - // (except the entry curves). - c4 = otherSeg.getCurve(), - c3 = c4.getPrevious(), - // Calculate their winding values and tangents. - t3 = c3.getTangentAt(tMax, true), - t4 = c4.getTangentAt(tMin, true), - // Cross product of the entry and exit tangent - // vectors at the intersection, will let us select - // the correct contour to traverse next. - w3 = t1.cross(t3), - w4 = t1.cross(t4); - if (Math.abs(w3 * w4) < /*#=*/Numerical.EPSILON) { - drawSegment(seg, 'no cross', i, 'blue'); - dir = 1; // TODO: Why? Is this correct? - } else if (operation === 'exclude') { + } else if (operation === 'exclude' + || operator(otherSeg._winding)) { + // Look at both tangents at the intersection to + // determine if this intersection is actually a crossing + var t1 = seg.getPrevious().getCurve().getTangentAt(tMax, true), + t2 = otherSeg.getCurve().getTangentAt(tMin, true); + if (t1.isCollinear(t2)) { // Do not attempt to switch contours if we aren't // sure that there is a possible candidate. - // If we didn't find a suitable direction for next - // contour to traverse, stay on the same contour. + drawSegment(seg, 'no cross', i, 'blue'); + } else { // Switch to the intersection segment. seg = otherSeg; - drawSegment(seg, 'exclude:switch', i, 'green'); - dir = 1; - } else { - // Do not attempt to switch contours if we aren't - // sure that there is a possible candidate. - var curve = w3 < w4 ? c3 : c4; - if (!operator(curve._segment1._winding)) - curve = curve === c3 ? c4 : c3; - dir = curve === c3 ? -1 : 1; - var next = curve._segment1; - // If we didn't find a suitable direction for next - // contour to traverse, stay on the same contour. - if (next._visited && seg._path !== next._path - || !operator(next._winding)) { - drawSegment(next, 'not suitable', i, 'orange'); - dir = 1; - } else { - // Switch to the intersection segment. - seg = otherSeg; - if (next._visited) - dir = -dir; - drawSegment(seg, 'switch', i, 'green'); - } + drawSegment(seg, 'switch', i, 'green'); } + } else { + drawSegment(otherSeg, 'not suitable', i, 'orange'); } } else { drawSegment(seg, 'keep', i, 'black'); } + if (seg._visited) { + // We didn't manage to switch, so stop right here. + console.error('Unable to switch to intersecting segment, ' + + 'aborting #' + pathCount + '.' + + (path ? path._segments.length + 1 : 1)); + break; + } // Add the current segment to the path, and mark the added // segment as visited. - path.add(new Segment(seg._point, added && handleIn, - dir > 0 ? seg._handleOut : seg._handleIn)); - seg._visited = true; - added = true; - // Move to the next segment according to the traversal direction - seg = dir > 0 ? seg.getNext() : seg.getPrevious(); + path.add(new Segment(seg._point, handleIn, seg._handleOut)); + seg._visited = added = true; + seg = seg.getNext(); inter = seg && seg._intersection; otherSeg = inter && inter._segment; } while (seg && seg !== startSeg && seg !== otherStartSeg - // Exclusion switches on each intersection, we need to look - // ahead & carry on if the other segment wasn't visited yet. - && (!seg._visited || operation === 'exclude' - && otherSeg && !otherSeg._visited) + // If we're about to switch, try to see if we can carry on + // if the other segment wasn't visited yet. + && (!seg._visited || otherSeg && !otherSeg._visited) && (inter || operator(seg._winding))); // Finish with closing the paths if necessary, correctly linking up // curves etc. @@ -643,14 +610,14 @@ PathItem.inject(new function() { path.setClosed(true); if (window.reportSegments) { console.log('Boolean operation completed', - '#' + (pathCount + 1) + '.' + + '#' + pathCount + '.' + (path ? path._segments.length + 1 : 1)); } } else { // path.lastSegment._handleOut.set(0, 0); console.error('Boolean operation results in open path, segs =', path._segments.length, 'length = ', path.getLength(), - '#' + (pathCount + 1) + '.' + + '#' + pathCount + '.' + (path ? path._segments.length + 1 : 1)); path = null; } @@ -661,9 +628,7 @@ PathItem.inject(new function() { if (path && (path._segments.length > 4 || !Numerical.isZero(path.getArea()))) paths.push(path); - if (window.reportSegments) { - pathCount++; - } + pathCount++; } return paths; } From 60a725b527251a960085da3a90e6220182c6fb50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 15 Sep 2015 19:38:28 +0200 Subject: [PATCH 076/280] Implement correct CurveLocation#isCrossing() check. And improve curve caching. Still needs work. --- src/path/CurveLocation.js | 141 +++++++++++++++++++++++++++++--------- 1 file changed, 108 insertions(+), 33 deletions(-) diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index 5c3a9f08..06617d8c 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -57,19 +57,24 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ // since this is only required to be unique at runtime among other // CurveLocation objects. this._id = UID.get(CurveLocation); - var path = curve._path; - this._version = path ? path._version : 0; - this._curve = curve; + this._setCurve(curve); this._parameter = parameter; this._point = point || curve.getPointAt(parameter, true); this._distance = _distance; this._overlap = _overlap; + this._crossing = null; this._intersection = _intersection; if (_intersection) { _intersection._intersection = this; // TODO: Remove this once debug logging is removed. _intersection._other = true; } + }, + + _setCurve: function(curve) { + var path = curve._path; + this._version = path ? path._version : 0; + this._curve = curve; this._segment = null; // To be determined, see #getSegment() // Also store references to segment1 and segment2, in case path // splitting / dividing is going to happen, in which case the segments @@ -84,25 +89,26 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ * @type Segment * @bean */ - getSegment: function(_preferFirst) { - if (!this._segment) { - var curve = this.getCurve(), - parameter = this.getParameter(); - if (parameter === 1) { - this._segment = curve._segment2; - } else if (parameter === 0 || _preferFirst) { - this._segment = curve._segment1; - } else if (parameter == null) { - return null; - } else { + getSegment: function() { + // Request curve first, so _segment gets invalidated if it's out of sync + var curve = this.getCurve(), + segment = this._segment; + if (!segment) { + var parameter = this.getParameter(); + if (parameter === 0) { + segment = curve._segment1; + } else if (parameter === 1) { + segment = curve._segment2; + } else if (parameter != null) { // Determine the closest segment by comparing curve lengths - this._segment = curve.getPartLength(0, parameter) + segment = curve.getPartLength(0, parameter) < curve.getPartLength(parameter, 1) ? curve._segment1 : curve._segment2; } + this._segment = segment; } - return this._segment; + return segment; }, /** @@ -113,29 +119,34 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ */ getCurve: function() { var curve = this._curve, - path = curve && curve._path; + path = curve && curve._path, + that = this; if (path && path._version !== this._version) { // If the path's segments have changed in the meantime, clear the // internal _parameter value and force refetching of the correct // curve again here. - curve = null; - this._parameter = null; + curve = this._parameter = this._curve = null; } - if (!curve) { - // If we're asked to get the curve uncached, access current curve - // objects through segment1 / segment2. Since path splitting or - // dividing might have happened in the meantime, try segment1's - // curve, and see if _point lies on it still, otherwise assume it's - // the curve before segment2. - curve = this._segment1.getCurve(); - if (curve.getParameterOf(this._point) == null) - curve = this._segment2.getPrevious().getCurve(); - this._curve = curve; - // Fetch path again as it could be on a new one through split() - path = curve._path; - this._version = path ? path._version : 0; + + // If path is out of sync, access current curve objects through segment1 + // / segment2. Since path splitting or dividing might have happened in + // the meantime, try segment1's curve, and see if _point lies on it + // still, otherwise assume it's the curve before segment2. + function trySegment(segment) { + var curve = segment && segment.getCurve(); + if (curve && (that._parameter = curve.getParameterOf(that._point)) + != null) { + // Fetch path again as it could be on a new one through split() + that._setCurve(curve); + that._segment = segment; + return curve; + } } - return curve; + + return curve + || trySegment(this._segment) + || trySegment(this._segment1) + || trySegment(this._segment2.getPrevious()); }, /** @@ -271,6 +282,70 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ return curve && curve.split(this.getParameter(), true); }, + isCrossing: function(report) { + // Implementation based on work by Andy Finnell: + // http://losingfight.com/blog/2011/07/09/how-to-implement-boolean-operations-on-bezier-paths-part-3/ + // https://bitbucket.org/andyfinnell/vectorboolean + var intersection = this._intersection, + crossing = this._crossing; + if (crossing != null || !intersection) + return crossing || false; + // TODO: isTangent() ? + // TODO: isAtEndPoint() ? + // -> Return if it's a tangent, or if not at an end point, only end + // point intersections need more checking! + var tMin = /*#=*/Numerical.CURVETIME_EPSILON, + tMax = 1 - tMin, + PI = Math.PI, + PI_2 = PI * 2, + // TODO: Make getCurve() sync work in boolean ops after splitting!!! + c2 = this._curve, + c1 = c2.getPrevious(), + c4 = intersection._curve, + c3 = c4.getPrevious(); + if (!c1 || !c3) + return this._crossing = false; + if (report) { + new Path.Circle({ + center: this.getPoint(), + radius: 10, + strokeColor: 'red' + }); + new Path({ + segments: [c1.getSegment1(), c1.getSegment2(), c2.getSegment2()], + strokeColor: 'red' + }); + new Path({ + segments: [c3.getSegment1(), c3.getSegment2(), c4.getSegment2()], + strokeColor: 'orange' + }); + console.log(c1.getValues(), c2.getValues(), c3.getValues(), c4.getValues()); + } + + function getAngle(tangent) { + var a = tangent.getAngleInRadians(); + return a < -PI ? a + PI_2 : a >= PI ? a - PI_2 : a; + } + + function isInRange(angle, min, max) { + return min < max + ? angle > min && angle < max + // The range wraps around 0: + : angle > min && angle <= PI || angle >= -PI && angle < max; + } + + // Calculate angles for all four tangents at the intersection point + var a1 = getAngle(c1.getTangentAt(tMax, true).negate()), + a2 = getAngle(c2.getTangentAt(tMin, true)), + a3 = getAngle(c3.getTangentAt(tMax, true).negate()), + a4 = getAngle(c4.getTangentAt(tMin, true)); + + // Count how many times curve2 angles appear between the curve1 angles + // If each pair of angles split the other two, then the edges cross. + return (isInRange(a3, a1, a2) ^ isInRange(a4, a1, a2)) + && (isInRange(a3, a2, a1) ^ isInRange(a4, a2, a1)); + }, + /** * Checks whether tow CurveLocation objects are describing the same location * on a path, by applying the same tolerances as elsewhere when dealing with From ad276ba46aaeab34aa58327f563852e691e716f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 15 Sep 2015 19:39:35 +0200 Subject: [PATCH 077/280] More improvements in tracePaths() - Use new isCrossing() check - Correctly switch crossings in exclusion --- src/path/Path.js | 7 +-- src/path/PathItem.Boolean.js | 112 +++++++++++++++++------------------ 2 files changed, 56 insertions(+), 63 deletions(-) diff --git a/src/path/Path.js b/src/path/Path.js index 69132a17..244bee8b 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -150,7 +150,8 @@ var Path = PathItem.extend(/** @lends Path# */{ if (parent) parent._currentPath = undefined; // Clockwise state becomes undefined as soon as geometry changes. - this._length = this._clockwise = undefined; + // Also clear cached mono curves used for winding calculations. + this._length = this._clockwise = this._monoCurves = undefined; if (flags & /*#=*/ChangeFlag.SEGMENTS) { this._version++; // See CurveLocation } else if (this._curves) { @@ -159,10 +160,6 @@ var Path = PathItem.extend(/** @lends Path# */{ for (var i = 0, l = this._curves.length; i < l; i++) this._curves[i]._changed(); } - // Clear cached curves used for winding direction and containment - // calculation. - // NOTE: This is only needed with __options.booleanOperations - this._monoCurves = undefined; } else if (flags & /*#=*/ChangeFlag.STROKE) { // TODO: We could preserve the purely geometric bounds that are not // affected by stroke: _bounds.bounds and _bounds.handleBounds diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index a7ccad22..4068df81 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -145,7 +145,8 @@ PathItem.inject(new function() { } } return finishBoolean(CompoundPath, - tracePaths(segments, monoCurves, operation), path1, path2); + tracePaths(segments, monoCurves, operation, _path1, _path2), + path1, path2); } var scaleFactor = 1; // 1 / 3000; @@ -220,6 +221,7 @@ PathItem.inject(new function() { } // Link the new segment with the intersection on the other curve segment._intersection = loc._intersection; + // loc._setCurve(segment.getCurve()); loc._segment = segment; prev = loc; } @@ -359,7 +361,7 @@ PathItem.inject(new function() { // it until the next intersection or end of a curve chain. var epsilon = /*#=*/Numerical.GEOMETRIC_EPSILON; chain = [], - startSeg = segment, + start = segment, totalLength = 0, windingSum = 0; do { @@ -368,7 +370,7 @@ PathItem.inject(new function() { chain.push({ segment: segment, curve: curve, length: length }); totalLength += length; segment = segment.getNext(); - } while (segment && !segment._intersection && segment !== startSeg); + } while (segment && !segment._intersection && segment !== start); // Calculate the average winding among three evenly distributed // points along this curve chain as a representative winding number. // This selection gives a better chance of returning a correct @@ -446,7 +448,7 @@ PathItem.inject(new function() { * not * @return {Path[]} the contours traced */ - function tracePaths(segments, monoCurves, operation) { + function tracePaths(segments, monoCurves, operation, path1, path2) { var segmentCount = 0; var pathCount = 1; @@ -525,64 +527,58 @@ PathItem.inject(new function() { tMax = 1 - tMin; for (var i = 0, l = segments.length; i < l; i++) { var seg = segments[i]; - if (seg._visited || !operator(seg._winding)) { - // drawSegment(seg, seg._visited ? 'visited' : 'filtered', i, 'red'); + if (seg._visited || !operator(seg._winding)) continue; - } var path = new Path(Item.NO_INSERT), - startSeg = seg, + start = seg, inter = seg._intersection, - otherSeg = inter && inter._segment, - otherStartSeg = otherSeg, + other = inter && inter._segment, + otherStart = other, added = false; // Whether a first segment as added already do { var handleIn = added && seg._handleIn; - if (added && otherSeg && otherSeg !== startSeg) { - // There is an intersection. If the intersecting segment is - // valid, try switching to it to continue traversal. - // Otherwise stay on the same contour. - if (otherSeg._path === seg._path) { // Self-intersection - drawSegment(seg, 'self-int', i, 'red'); - // Switch to the intersecting segment, as we need to - // resolving self-Intersections. - seg = otherSeg; - } else if (operation !== 'exclude' - && operator(seg._winding)) { - // Do not switch to the intersecting segment as it is - // contained inside the boolean result. - drawSegment(seg, 'ignore-keep', i, 'black'); - } else if (operation !== 'intersect' && inter._overlap) { - // Switch to the overlapping intersection segment - // if its winding number along the curve is 1, to - // leave the overlapping area. - // NOTE: We cannot check the next (overlapping) - // segment since its winding number will always be 2 - drawSegment(seg, 'overlap', i, 'orange'); - var curve = otherSeg.getCurve(); - if (getWinding(curve.getPointAt(0.5, true), - monoCurves, curve.isHorizontal()) === 1) { - seg = otherSeg; - } - } else if (operation === 'exclude' - || operator(otherSeg._winding)) { - // Look at both tangents at the intersection to - // determine if this intersection is actually a crossing - var t1 = seg.getPrevious().getCurve().getTangentAt(tMax, true), - t2 = otherSeg.getCurve().getTangentAt(tMin, true); - if (t1.isCollinear(t2)) { - // Do not attempt to switch contours if we aren't - // sure that there is a possible candidate. - drawSegment(seg, 'no cross', i, 'blue'); - } else { - // Switch to the intersection segment. - seg = otherSeg; - drawSegment(seg, 'switch', i, 'green'); - } - } else { - drawSegment(otherSeg, 'not suitable', i, 'orange'); + if (!added || !other || other === start) { + // TODO: Is (other === start) check really required? + // Does that ever occur? + // Just add the first segment and all segments that have no + // intersection. + drawSegment(seg, 'add', i, 'black'); + } else if (other._path === seg._path) { // Self-intersection + drawSegment(seg, 'self-int', i, 'red'); + // Switch to the intersecting segment, as we need to + // resolving self-Intersections. + seg = other; + } else if (operation !== 'intersect' && inter._overlap) { + // Switch to the overlapping intersection segment + // if its winding number along the curve is 1, to + // leave the overlapping area. + // NOTE: We cannot check the next (overlapping) + // segment since its winding number will always be 2 + drawSegment(seg, 'overlap', i, 'orange'); + var curve = other.getCurve(); + if (getWinding(curve.getPointAt(0.5, true), + monoCurves, curve.isHorizontal()) === 1) { + seg = other; } + } else if (operation === 'exclude') { + // We need to handle exclusion separately, as we want to + // switch at each crossing, and at each intersection within + // the exclusion area even if it is not crossing. + if (inter.isCrossing() || path2.contains(seg._point)) { + seg = other; + drawSegment(seg, 'exclude-cross', i, 'green'); + } else { + drawSegment(other, 'exclude-no-cross', i, 'orange'); + } + } else if (operator(seg._winding)) { + // Do not switch to the intersecting segment as it is + // contained inside the boolean result. + drawSegment(seg, 'ignore-keep', i, 'black'); + } else if (operator(other._winding) && inter.isCrossing()) { + seg = other; + drawSegment(seg, 'cross', i, 'green'); } else { - drawSegment(seg, 'keep', i, 'black'); + drawSegment(other, 'no-cross', i, 'orange'); } if (seg._visited) { // We didn't manage to switch, so stop right here. @@ -597,15 +593,15 @@ PathItem.inject(new function() { seg._visited = added = true; seg = seg.getNext(); inter = seg && seg._intersection; - otherSeg = inter && inter._segment; - } while (seg && seg !== startSeg && seg !== otherStartSeg + other = inter && inter._segment; + } while (seg && seg !== start && seg !== otherStart // If we're about to switch, try to see if we can carry on // if the other segment wasn't visited yet. - && (!seg._visited || otherSeg && !otherSeg._visited) + && (!seg._visited || other && !other._visited) && (inter || operator(seg._winding))); // Finish with closing the paths if necessary, correctly linking up // curves etc. - if (seg === startSeg || seg === otherStartSeg) { + if (seg === start || seg === otherStart) { path.firstSegment.setHandleIn(seg._handleIn); path.setClosed(true); if (window.reportSegments) { From 8f9549dd12d76c50e8e5c39d9c0230c5f7239815 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 16 Sep 2015 02:29:17 +0200 Subject: [PATCH 078/280] Fix non-breaking spaces. --- src/path/CurveLocation.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index 06617d8c..035f4061 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -288,8 +288,8 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ // https://bitbucket.org/andyfinnell/vectorboolean var intersection = this._intersection, crossing = this._crossing; - if (crossing != null || !intersection) - return crossing || false; + if (crossing != null || !intersection) + return crossing || false; // TODO: isTangent() ? // TODO: isAtEndPoint() ? // -> Return if it's a tangent, or if not at an end point, only end From 1e5c1bafaf91e894c8e72314291dd00c553f88e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 16 Sep 2015 02:31:37 +0200 Subject: [PATCH 079/280] Increase geometric epsilon to better match collinear lines. Needs more testing, but seems to perform well. Relates to #786 --- src/util/Numerical.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/Numerical.js b/src/util/Numerical.js index 00a9ce2b..30f28ec5 100644 --- a/src/util/Numerical.js +++ b/src/util/Numerical.js @@ -93,7 +93,7 @@ var Numerical = new function() { * collinearity. This value is somewhat arbitrary and was chosen by * trial and error. */ - GEOMETRIC_EPSILON: 1e-9, + GEOMETRIC_EPSILON: 1e-8, /** * MACHINE_EPSILON for a double precision (Javascript Number) is * 2.220446049250313e-16. (try this in the js console) From e54839127680e0f8273cabce5aeff98797cb81d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 16 Sep 2015 02:33:56 +0200 Subject: [PATCH 080/280] Use the zero-epsilon when checking beginnings and ends of curves for overlaps. Relates to #786 and #777 --- src/path/Curve.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 5ad00858..be30826c 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1729,7 +1729,7 @@ new function() { // Scope for intersection using bezier fat-line clipping c1p2 = new Point(v1[6], v1[7]), c2p1 = new Point(v2[0], v2[1]), c2p2 = new Point(v2[6], v2[7]), - epsilon = /*#=*/Numerical.GEOMETRIC_EPSILON; + epsilon = /*#=*/Numerical.EPSILON; // Handle the special case where the first curve's stat-point // overlaps with the second curve's start- or end-points. if (c1p1.isClose(c2p1, epsilon)) From 2026e5571ea421190fe31b5b7c6d12aa87e161d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 16 Sep 2015 02:54:25 +0200 Subject: [PATCH 081/280] Some code cleanup and comments in isCrossing(). --- src/path/CurveLocation.js | 6 ++++++ src/path/PathItem.Boolean.js | 8 +------- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index 035f4061..a6c9d655 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -294,6 +294,12 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ // TODO: isAtEndPoint() ? // -> Return if it's a tangent, or if not at an end point, only end // point intersections need more checking! + // Values for getTangentAt() that are almost 0 and 1. + // NOTE: Even though getTangentAt() has code to support 0 and 1 instead + // of tMin and tMax, we still need to use this instead, as other issues + // emerge from switching to 0 and 1 in edge cases. + // NOTE: VectorBoolean has code that slowly shifts these points inwards + // until the resulting tangents are not ambiguous. Do we need this too? var tMin = /*#=*/Numerical.CURVETIME_EPSILON, tMax = 1 - tMin, PI = Math.PI, diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 4068df81..9c6ac55a 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -518,13 +518,7 @@ PathItem.inject(new function() { } var paths = [], - operator = operators[operation], - // Values for getTangentAt() that are almost 0 and 1. - // NOTE: Even though getTangentAt() supports 0 and 1 instead of - // tMin and tMax, we still need to use this instead, as other issues - // emerge from switching to 0 and 1 in edge cases. - tMin = /*#=*/Numerical.CURVETIME_EPSILON, - tMax = 1 - tMin; + operator = operators[operation]; for (var i = 0, l = segments.length; i < l; i++) { var seg = segments[i]; if (seg._visited || !operator(seg._winding)) From 197aa4b4cfcac5fedfbaa0cbf9693e30f35cc74a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 16 Sep 2015 02:56:24 +0200 Subject: [PATCH 082/280] No need to wrap angles as they're always -PI < a < PI. --- src/path/CurveLocation.js | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index a6c9d655..df84267e 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -328,11 +328,6 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ console.log(c1.getValues(), c2.getValues(), c3.getValues(), c4.getValues()); } - function getAngle(tangent) { - var a = tangent.getAngleInRadians(); - return a < -PI ? a + PI_2 : a >= PI ? a - PI_2 : a; - } - function isInRange(angle, min, max) { return min < max ? angle > min && angle < max @@ -341,10 +336,10 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ } // Calculate angles for all four tangents at the intersection point - var a1 = getAngle(c1.getTangentAt(tMax, true).negate()), - a2 = getAngle(c2.getTangentAt(tMin, true)), - a3 = getAngle(c3.getTangentAt(tMax, true).negate()), - a4 = getAngle(c4.getTangentAt(tMin, true)); + var a1 = c1.getTangentAt(tMax, true).negate().getAngleInRadians(), + a2 = c2.getTangentAt(tMin, true).getAngleInRadians(), + a3 = c3.getTangentAt(tMax, true).negate().getAngleInRadians(), + a4 = c4.getTangentAt(tMin, true).getAngleInRadians(); // Count how many times curve2 angles appear between the curve1 angles // If each pair of angles split the other two, then the edges cross. From f8bd7a2005cfb0e9475644af828680ab7f6c4a71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 16 Sep 2015 09:52:41 +0200 Subject: [PATCH 083/280] Improve debug logging and drawing. And add more descriptive comments to tracePath(). --- src/path/CurveLocation.js | 8 +++--- src/path/PathItem.Boolean.js | 52 +++++++++++++++++++++++++----------- 2 files changed, 40 insertions(+), 20 deletions(-) diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index df84267e..e8f17c4c 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -282,7 +282,7 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ return curve && curve.split(this.getParameter(), true); }, - isCrossing: function(report) { + isCrossing: function(_report) { // Implementation based on work by Andy Finnell: // http://losingfight.com/blog/2011/07/09/how-to-implement-boolean-operations-on-bezier-paths-part-3/ // https://bitbucket.org/andyfinnell/vectorboolean @@ -303,7 +303,6 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ var tMin = /*#=*/Numerical.CURVETIME_EPSILON, tMax = 1 - tMin, PI = Math.PI, - PI_2 = PI * 2, // TODO: Make getCurve() sync work in boolean ops after splitting!!! c2 = this._curve, c1 = c2.getPrevious(), @@ -311,7 +310,7 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ c3 = c4.getPrevious(); if (!c1 || !c3) return this._crossing = false; - if (report) { + if (_report) { new Path.Circle({ center: this.getPoint(), radius: 10, @@ -325,13 +324,12 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ segments: [c3.getSegment1(), c3.getSegment2(), c4.getSegment2()], strokeColor: 'orange' }); - console.log(c1.getValues(), c2.getValues(), c3.getValues(), c4.getValues()); } function isInRange(angle, min, max) { return min < max ? angle > min && angle < max - // The range wraps around 0: + // The range wraps around -PI / PI: : angle > min && angle <= PI || angle >= -PI && angle < max; } diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 9c6ac55a..dc4640da 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -149,8 +149,8 @@ PathItem.inject(new function() { path1, path2); } - var scaleFactor = 1; // 1 / 3000; - var textAngle = 60; + var scaleFactor = 0.25; // 1 / 3000; + var textAngle = 33; var fontSize = 5; /** @@ -543,36 +543,41 @@ PathItem.inject(new function() { // resolving self-Intersections. seg = other; } else if (operation !== 'intersect' && inter._overlap) { - // Switch to the overlapping intersection segment - // if its winding number along the curve is 1, to - // leave the overlapping area. - // NOTE: We cannot check the next (overlapping) - // segment since its winding number will always be 2 - drawSegment(seg, 'overlap', i, 'orange'); + // Switch to the overlapping intersecting segment if its + // winding number along the curve is 1, meaning we leave the + // overlapping area. + // NOTE: We cannot check the next (overlapping) segment + // since its winding number will always be 2. var curve = other.getCurve(); if (getWinding(curve.getPointAt(0.5, true), monoCurves, curve.isHorizontal()) === 1) { + drawSegment(seg, 'overlap-cross', i, 'orange'); seg = other; + } else { + drawSegment(seg, 'overlap-stay', i, 'orange'); } } else if (operation === 'exclude') { // We need to handle exclusion separately, as we want to // switch at each crossing, and at each intersection within - // the exclusion area even if it is not crossing. + // the exclusion area even if it is not a crossing. if (inter.isCrossing() || path2.contains(seg._point)) { - seg = other; drawSegment(seg, 'exclude-cross', i, 'green'); + seg = other; } else { - drawSegment(other, 'exclude-no-cross', i, 'orange'); + drawSegment(seg, 'exclude-stay', i, 'orange'); } } else if (operator(seg._winding)) { - // Do not switch to the intersecting segment as it is - // contained inside the boolean result. + // Do not switch to the intersecting segment as this segment + // is part of the the boolean result. drawSegment(seg, 'ignore-keep', i, 'black'); } else if (operator(other._winding) && inter.isCrossing()) { - seg = other; + // The other segment is part of the boolean result, and we + // are at crossing, switch over. drawSegment(seg, 'cross', i, 'green'); + seg = other; } else { - drawSegment(other, 'no-cross', i, 'orange'); + // Keep on truckin' + drawSegment(seg, 'stay', i, 'orange'); } if (seg._visited) { // We didn't manage to switch, so stop right here. @@ -588,10 +593,27 @@ PathItem.inject(new function() { seg = seg.getNext(); inter = seg && seg._intersection; other = inter && inter._segment; + if (seg === start || seg === otherStart) { + drawSegment(seg, 'close', i, 'red'); + } + if (seg._visited && (!other || other._visited)) { + drawSegment(seg, 'visited', i, 'red'); + } + if (!inter && !operator(seg._winding)) { + // TODO: We really should find a way to go backwards perhaps + // and try another path when this happens? + drawSegment(seg, 'discard', i, 'red'); + console.error('Excluded segment encountered, aborting #' + + pathCount + '.' + + (path ? path._segments.length + 1 : 1)); + } } while (seg && seg !== start && seg !== otherStart // If we're about to switch, try to see if we can carry on // if the other segment wasn't visited yet. && (!seg._visited || other && !other._visited) + // Intersections are always part of the resulting path, for + // all other segments check the winding contribution to see + // if they are to be kept. If not, the chain has to end here && (inter || operator(seg._winding))); // Finish with closing the paths if necessary, correctly linking up // curves etc. From 0980ad3fe9883f503c2dd764bd1cb7c1adeea9bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 16 Sep 2015 10:44:41 +0200 Subject: [PATCH 084/280] Fix remaining issues with curve location sorting. Relates to #787 --- src/path/CurveLocation.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index e8f17c4c..a9fe06f6 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -403,16 +403,22 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ if (path1 === path2) { if (curve1 === curve2) { var diff = l1._parameter - l2._parameter; + // TODO: Compare points instead of parameter like in + // equals? or curve time there too? Why was it changed? if (Math.abs(diff) < /*#=*/Numerical.CURVETIME_EPSILON){ var i1 = l1._intersection, i2 = l2._intersection, curve21 = i1 && i1._curve, - curve22 = i2 && l2._curve; + curve22 = i2 && i2._curve, + path21 = curve21 && curve21._path, + path22 = curve22 && curve22._path; res = curve21 === curve22 // equal or both null ? i1 && i2 ? i1._parameter - i2._parameter : 0 : curve21 && curve22 - ? curve21.getIndex() - curve22.getIndex() - : curve21 ? 1 : -1; + ? path21 === path22 + ? curve21.getIndex() - curve22.getIndex() + : curve21 ? 1 : -1 + : path21._id - path22._id; } else { res = diff; } From 7061bc0e0a77a39f371531d778077db1606fbd6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 16 Sep 2015 10:52:51 +0200 Subject: [PATCH 085/280] Simplify CurveLocation.sort() code Relates to #787 --- src/path/CurveLocation.js | 57 +++++++++++++++------------------------ 1 file changed, 22 insertions(+), 35 deletions(-) diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index a9fe06f6..3b12fd60 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -353,14 +353,14 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ * @param {CurveLocation} location * @return {Boolean} {@true if the locations are equal} */ - equals: function(loc, _ignoreIntersection) { + equals: function(loc, _ignoreOther) { return this === loc || loc instanceof CurveLocation // Call getCurve() and getParameter() to keep in sync && this.getCurve() === loc.getCurve() && this.getPoint().isClose(loc.getPoint(), /*#=*/Numerical.GEOMETRIC_EPSILON) - && (_ignoreIntersection + && (_ignoreOther || (!this._intersection && !loc._intersection || this._intersection && this._intersection.equals( loc._intersection, true))) @@ -389,48 +389,35 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ statics: { sort: function(locations) { - locations.sort(function compare(l1, l2) { + function compare(l1, l2, _ignoreOther) { + if (!l1 || !l2) + return l1 ? -1 : 0; var curve1 = l1._curve, curve2 = l2._curve, path1 = curve1._path, path2 = curve2._path, - res; + diff; // Sort by path-id, curve, parameter, curve2, parameter2 so we // can easily remove duplicates with calls to equals() after. // NOTE: We don't call getCurve() / getParameter() here, since // this code is used internally in boolean operations where all // this information remains valid during processing. - if (path1 === path2) { - if (curve1 === curve2) { - var diff = l1._parameter - l2._parameter; - // TODO: Compare points instead of parameter like in - // equals? or curve time there too? Why was it changed? - if (Math.abs(diff) < /*#=*/Numerical.CURVETIME_EPSILON){ - var i1 = l1._intersection, - i2 = l2._intersection, - curve21 = i1 && i1._curve, - curve22 = i2 && i2._curve, - path21 = curve21 && curve21._path, - path22 = curve22 && curve22._path; - res = curve21 === curve22 // equal or both null - ? i1 && i2 ? i1._parameter - i2._parameter : 0 - : curve21 && curve22 - ? path21 === path22 - ? curve21.getIndex() - curve22.getIndex() - : curve21 ? 1 : -1 - : path21._id - path22._id; - } else { - res = diff; - } - } else { - res = curve1.getIndex() - curve2.getIndex(); - } - } else { - // Sort by path id to group all locs on the same path. - res = path1._id - path2._id; - } - return res; - }); + return path1 === path2 + ? curve1 === curve2 + // TODO: Compare points instead of parameter like in + // equals? Or time there too? Why was it changed? + ? Math.abs((diff = l1._parameter - l2._parameter)) + < /*#=*/Numerical.CURVETIME_EPSILON + ? _ignoreOther + ? 0 + : compare(l1._intersection, + l2._intersection, true) + : diff + : curve1.getIndex() - curve2.getIndex() + // Sort by path id to group all locs on the same path. + : path1._id - path2._id; + } + locations.sort(compare); } } }, Base.each(Curve.evaluateMethods, function(name) { From 857e27e3a8a4a3f5b5dc773e31c0a1b3c3dc03df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 16 Sep 2015 18:15:26 +0200 Subject: [PATCH 086/280] Fix accidental variable leackage. --- src/path/PathItem.Boolean.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index dc4640da..26f3788c 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -359,7 +359,7 @@ PathItem.inject(new function() { // contribution for the curve-chain starting with this segment. Once we // have enough confidence in the winding contribution, we can propagate // it until the next intersection or end of a curve chain. - var epsilon = /*#=*/Numerical.GEOMETRIC_EPSILON; + var epsilon = /*#=*/Numerical.GEOMETRIC_EPSILON, chain = [], start = segment, totalLength = 0, From d0332f843f940aac3b381b3b140848599e93998f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 16 Sep 2015 18:16:48 +0200 Subject: [PATCH 087/280] Renamed Curve#reverse() and Segment#reverse() to #reversed() Since they don't modify the object. Also introduce new Segment#reverse(), which does. --- src/path/Curve.js | 4 ++-- src/path/Segment.js | 16 +++++++++++++++- test/tests/Segment.js | 2 +- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index be30826c..38405343 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -522,8 +522,8 @@ var Curve = Base.extend(/** @lends Curve# */{ * * @return {Curve} a reversed version of the curve */ - reverse: function() { - return new Curve(this._segment2.reverse(), this._segment1.reverse()); + reversed: function() { + return new Curve(this._segment2.reversed(), this._segment1.reversed()); }, /** diff --git a/src/path/Segment.js b/src/path/Segment.js index 14cd1aa6..61b7d610 100644 --- a/src/path/Segment.js +++ b/src/path/Segment.js @@ -393,10 +393,24 @@ var Segment = Base.extend(/** @lends Segment# */{ }, /** - * Returns the reversed the segment, without modifying the segment itself. + * Reverses the {@link #handleIn} and {@link #handleOut} vectors of this + * segment. Note: the actual segment is modified, no copy is created. * @return {Segment} the reversed segment */ reverse: function() { + var handleIn = this._handleIn, + handleOut = this._handleOut, + inX = handleIn._x, + inY = handleIn._y; + handleIn.set(handleOut._x, handleOut._y); + handleOut.set(inX, inY); + }, + + /** + * Returns the reversed the segment, without modifying the segment itself. + * @return {Segment} the reversed segment + */ + reversed: function() { return new Segment(this._point, this._handleOut, this._handleIn); }, diff --git a/test/tests/Segment.js b/test/tests/Segment.js index 3c8eb957..b28de7b7 100644 --- a/test/tests/Segment.js +++ b/test/tests/Segment.js @@ -43,7 +43,7 @@ test('new Segment(size)', function() { test('segment.reverse()', function() { var segment = new Segment(new Point(10, 10), new Point(5, 5), new Point(15, 15)); - segment = segment.reverse(); + segment.reverse(); equals(segment.toString(), '{ point: { x: 10, y: 10 }, handleIn: { x: 15, y: 15 }, handleOut: { x: 5, y: 5 } }'); }); From 30f1441c269245c890e7ac17e2cc42746472ca3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 16 Sep 2015 18:34:35 +0200 Subject: [PATCH 088/280] Various boolean code clean-ups. --- src/path/Curve.js | 2 +- src/path/PathItem.Boolean.js | 55 ++++++++++++++++++++---------------- test/js/helpers.js | 2 +- 3 files changed, 32 insertions(+), 27 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 38405343..da2a3766 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1662,7 +1662,7 @@ new function() { // Scope for intersection using bezier fat-line clipping if (pairs.length === 1 && pair[0] < pairs[0][0]) { pairs.unshift(pair); } else if (pairs.length === 0 - // TODO: Compare distance of points instead! + // TODO: Compare distance of the actual points instead! || abs(pair[0] - pairs[0][0]) > timeEpsilon || abs(pair[1] - pairs[0][1]) > timeEpsilon) { pairs.push(pair); diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 26f3788c..a07592ea 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -149,7 +149,7 @@ PathItem.inject(new function() { path1, path2); } - var scaleFactor = 0.25; // 1 / 3000; + var scaleFactor = 1; // 1 / 3000; var textAngle = 33; var fontSize = 5; @@ -167,12 +167,12 @@ PathItem.inject(new function() { if (inter._other) return; var other = inter._intersection; - var log = ['CurveLocation', inter._id, 'p', inter.getPath()._id, + var log = ['CurveLocation', inter._id, 'id', inter.getPath()._id, 'i', inter.getIndex(), 't', inter._parameter, - 'o', !!inter._overlap, - 'Other', other._id, 'p', other.getPath()._id, + 'o', !!inter._overlap, 'p', inter.getPoint(), + 'Other', other._id, 'id', other.getPath()._id, 'i', other.getIndex(), 't', other._parameter, - 'o', !!other._overlap]; + 'o', !!other._overlap, 'p', other.getPoint()]; new Path.Circle({ center: inter.point, radius: fontSize / 2 * scaleFactor, @@ -189,16 +189,20 @@ PathItem.inject(new function() { var tMin = /*#=*/Numerical.CURVETIME_EPSILON, tMax = 1 - tMin, noHandles = false, - clearSegments = []; + clearSegments = [], + curve, + prev, + prevT; - for (var i = intersections.length - 1, curve, prev; i >= 0; i--) { + for (var i = intersections.length - 1; i >= 0; i--) { var loc = intersections[i], - t = loc._parameter; + t = loc._parameter, + locT = t; // Check if we are splitting same curve multiple times, but avoid // dividing with zero. - if (prev && prev._curve === loc._curve && prev._parameter > 0) { + if (prev && prev._curve === loc._curve && prevT > 0) { // Scale parameter after previous split. - t /= prev._parameter; + t /= prevT; } else { curve = loc._curve; noHandles = !curve.hasHandles(); @@ -220,10 +224,14 @@ PathItem.inject(new function() { clearSegments.push(segment); } // Link the new segment with the intersection on the other curve + if (segment._intersection) + console.log('Oh dear there was one already: ' + segment._intersection); segment._intersection = loc._intersection; // loc._setCurve(segment.getCurve()); loc._segment = segment; + loc._parameter = t; prev = loc; + prevT = locT; } // Clear segment handles if they were part of a curve with no handles, // once we are done with the entire curve. @@ -486,18 +494,15 @@ PathItem.inject(new function() { labelSegment(seg, '#' + pathCount + '.' + (path ? path._segments.length + 1 : 1) + ' ' + (segmentCount++) + '/' + index + ': ' + text - + ' v: ' + !!seg._visited - + ' p: ' + seg._path._id - + ' x: ' + seg._point.x - + ' y: ' + seg._point.y + + ' id: ' + seg._path._id + + ' v: ' + (seg._visited ? 1 : 0) + + ' p: ' + seg._point + ' op: ' + operator(seg._winding) - + ' o: ' + (inter && inter._overlap || 0) - + ' w: ' + seg._winding + + ' ov: ' + (inter && inter._overlap || 0) + + ' wi: ' + seg._winding , color); } - - for (var i = 0; i < (window.reportWindings ? segments.length : 0); i++) { var seg = segments[i]; path = seg._path, @@ -509,11 +514,10 @@ PathItem.inject(new function() { labelSegment(seg, '#' + pathIndex + '.' + (i + 1) + ' i: ' + !!inter - + ' p: ' + seg._path._id - + ' x: ' + seg._point.x - + ' y: ' + seg._point.y - + ' o: ' + (inter && inter._overlap || 0) - + ' w: ' + seg._winding + + ' id: ' + seg._path._id + + ' pt: ' + seg._point + + ' ov: ' + (inter && inter._overlap || 0) + + ' wi: ' + seg._winding , 'green'); } @@ -560,7 +564,8 @@ PathItem.inject(new function() { // We need to handle exclusion separately, as we want to // switch at each crossing, and at each intersection within // the exclusion area even if it is not a crossing. - if (inter.isCrossing() || path2.contains(seg._point)) { + if (inter.isCrossing() + || path2 && path2.contains(seg._point)) { drawSegment(seg, 'exclude-cross', i, 'green'); seg = other; } else { @@ -593,7 +598,7 @@ PathItem.inject(new function() { seg = seg.getNext(); inter = seg && seg._intersection; other = inter && inter._segment; - if (seg === start || seg === otherStart) { + if (seg === start || seg === otherStart) { drawSegment(seg, 'close', i, 'red'); } if (seg._visited && (!other || other._visited)) { diff --git a/test/js/helpers.js b/test/js/helpers.js index c483fb27..cb2d5b03 100644 --- a/test/js/helpers.js +++ b/test/js/helpers.js @@ -286,7 +286,7 @@ function asyncTest(testName, expected) { var project = new Project(); expected(function() { project.remove(); - start(); + QUnit.start(); }); }); } From 2750c34090ad56908ad1e5c4e576491c968a5f0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Thu, 17 Sep 2015 01:03:13 +0200 Subject: [PATCH 089/280] Improve the way intersections are sorted and merged. Use a binary search to determine insertion index and compare with neighbours to eliminate doubles. --- src/path/Curve.js | 8 ++-- src/path/CurveLocation.js | 79 ++++++++++++++++++++++-------------- src/path/PathItem.Boolean.js | 2 +- src/path/PathItem.js | 3 +- 4 files changed, 55 insertions(+), 37 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index da2a3766..581d2427 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -402,8 +402,8 @@ var Curve = Base.extend(/** @lends Curve# */{ * curves */ getIntersections: function(curve) { - return Curve._filterIntersections(Curve._getIntersections( - this.getValues(), curve.getValues(), this, curve, [], {})); + return Curve._getIntersections(this.getValues(), curve.getValues(), + this, curve, [], {}); }, // TODO: adjustThroughPoint @@ -1359,11 +1359,11 @@ new function() { // Scope for intersection using bezier fat-line clipping if (!Numerical.isZero(d1) || !Numerical.isZero(d2)) debugger; */ - locations.push( + CurveLocation.add(locations, new CurveLocation(c1, t1, p1 || Curve.getPoint(v1, t1), null, overlap, new CurveLocation(c2, t2, p2 || Curve.getPoint(v2, t2), - null, overlap))); + null, overlap)), true); } } diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index 3b12fd60..aa30785f 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -358,8 +358,8 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ || loc instanceof CurveLocation // Call getCurve() and getParameter() to keep in sync && this.getCurve() === loc.getCurve() - && this.getPoint().isClose(loc.getPoint(), - /*#=*/Numerical.GEOMETRIC_EPSILON) + && Math.abs(this.getParameter() - loc.getParameter()) + < /*#=*/Numerical.CURVETIME_EPSILON && (_ignoreOther || (!this._intersection && !loc._intersection || this._intersection && this._intersection.equals( @@ -388,36 +388,55 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ }, statics: { - sort: function(locations) { - function compare(l1, l2, _ignoreOther) { - if (!l1 || !l2) - return l1 ? -1 : 0; - var curve1 = l1._curve, - curve2 = l2._curve, + add: function(locations, loc, merge) { + // Insert-sort by path-id, curve, parameter so we can easily merge + // duplicates with calls to equals() after. + // NOTE: We don't call getCurve() / getParameter() here, since this + // code is used internally in boolean operations where all this + // information remains valid during processing. + var l = 0, + r = locations.length - 1; + while (l <= r) { + var m = (l + r) >>> 1, + loc2 = locations[m], + curve1 = loc._curve, + curve2 = loc2._curve, path1 = curve1._path, - path2 = curve2._path, - diff; - // Sort by path-id, curve, parameter, curve2, parameter2 so we - // can easily remove duplicates with calls to equals() after. - // NOTE: We don't call getCurve() / getParameter() here, since - // this code is used internally in boolean operations where all - // this information remains valid during processing. - return path1 === path2 - ? curve1 === curve2 - // TODO: Compare points instead of parameter like in - // equals? Or time there too? Why was it changed? - ? Math.abs((diff = l1._parameter - l2._parameter)) - < /*#=*/Numerical.CURVETIME_EPSILON - ? _ignoreOther - ? 0 - : compare(l1._intersection, - l2._intersection, true) - : diff - : curve1.getIndex() - curve2.getIndex() - // Sort by path id to group all locs on the same path. - : path1._id - path2._id; + path2 = curve2._path; + diff = path1 === path2 + ? curve1.getIndex() + loc._parameter + - curve2.getIndex() - loc2._parameter + // Sort by path id to group all locs on same path. + : path1._id - path2._id; + // Only compare location with equals() if diff is small enough + // NOTE: equals() takes the intersection location into account, + // while the above calculation of diff doesn't! + if (merge && Math.abs(diff) < /*#=*/Numerical.CURVETIME_EPSILON + && loc.equals(loc2)) { + // Carry over overlap setting! + if (loc._overlap) { + loc2._overlap = loc2._intersection._overlap = true; + } + // We're done, don't insert + return; + } + if (diff < 0) { + r = m - 1; + } else { + l = m + 1; + } } - locations.sort(compare); + locations.splice(l, 0, loc); + }, + + expand: function(locations) { + // Create a copy since add() keeps modifying the array and inserting + // at sorted indices. + var copy = locations.slice(); + for (var i = 0, l = locations.length; i < l; i++) { + this.add(copy, locations[i]._intersection, false); + } + return copy; } } }, Base.each(Curve.evaluateMethods, function(name) { diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index a07592ea..44e390e3 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -110,7 +110,7 @@ PathItem.inject(new function() { // console.timeEnd('self-intersection'); } // console.timeEnd('intersection'); - splitPath(Curve._filterIntersections(locations, true)); + splitPath(CurveLocation.expand(locations)); var segments = [], // Aggregate of all curves in both operands, monotonic in y diff --git a/src/path/PathItem.js b/src/path/PathItem.js index 1f931dd0..00ad1b92 100644 --- a/src/path/PathItem.js +++ b/src/path/PathItem.js @@ -63,8 +63,7 @@ var PathItem = Item.extend(/** @lends PathItem# */{ // intersections. // NOTE: The hidden argument _matrix is used internally to override the // passed path's transformation matrix. - return Curve._filterIntersections(this._getIntersections( - this !== path ? path : null, _matrix, [])); + return this._getIntersections(this !== path ? path : null, _matrix, []); }, _getIntersections: function(path, matrix, locations, returnFirst) { From 1508b8fc75195452b34a644eac93edeb8cf6510f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Thu, 17 Sep 2015 01:15:41 +0200 Subject: [PATCH 090/280] Improve debug logging. --- src/path/PathItem.Boolean.js | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 44e390e3..1d2af4b0 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -224,9 +224,12 @@ PathItem.inject(new function() { clearSegments.push(segment); } // Link the new segment with the intersection on the other curve - if (segment._intersection) - console.log('Oh dear there was one already: ' + segment._intersection); - segment._intersection = loc._intersection; + if (segment._intersection) { + console.log('Segment already has an intersection: ' + + segment._intersection + ', ' + loc._intersection); + } else { + segment._intersection = loc._intersection; + } // loc._setCurve(segment.getCurve()); loc._segment = segment; loc._parameter = t; @@ -542,7 +545,7 @@ PathItem.inject(new function() { // intersection. drawSegment(seg, 'add', i, 'black'); } else if (other._path === seg._path) { // Self-intersection - drawSegment(seg, 'self-int', i, 'red'); + drawSegment(seg, 'self-int', i, 'purple'); // Switch to the intersecting segment, as we need to // resolving self-Intersections. seg = other; @@ -569,12 +572,12 @@ PathItem.inject(new function() { drawSegment(seg, 'exclude-cross', i, 'green'); seg = other; } else { - drawSegment(seg, 'exclude-stay', i, 'orange'); + drawSegment(seg, 'exclude-stay', i, 'blue'); } } else if (operator(seg._winding)) { // Do not switch to the intersecting segment as this segment // is part of the the boolean result. - drawSegment(seg, 'ignore-keep', i, 'black'); + drawSegment(seg, 'keep', i, 'black'); } else if (operator(other._winding) && inter.isCrossing()) { // The other segment is part of the boolean result, and we // are at crossing, switch over. @@ -582,7 +585,7 @@ PathItem.inject(new function() { seg = other; } else { // Keep on truckin' - drawSegment(seg, 'stay', i, 'orange'); + drawSegment(seg, 'stay', i, 'blue'); } if (seg._visited) { // We didn't manage to switch, so stop right here. @@ -599,9 +602,8 @@ PathItem.inject(new function() { inter = seg && seg._intersection; other = inter && inter._segment; if (seg === start || seg === otherStart) { - drawSegment(seg, 'close', i, 'red'); - } - if (seg._visited && (!other || other._visited)) { + drawSegment(seg, 'done', i, 'red'); + } else if (seg._visited && (!other || other._visited)) { drawSegment(seg, 'visited', i, 'red'); } if (!inter && !operator(seg._winding)) { From 9c812335e5011d65cb779108d048352d1f732b08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Thu, 17 Sep 2015 09:39:22 +0200 Subject: [PATCH 091/280] Curve._filterIntersections() is now without a job. --- src/path/Curve.js | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 581d2427..9e98081e 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1755,36 +1755,6 @@ new function() { // Scope for intersection using bezier fat-line clipping if (c1p2.isClose(c2p2, epsilon)) addLocation(locations, param, v1, c1, 1, c1p2, v2, c2, 1, c2p2); return locations; - }, - - _filterIntersections: function(locations, expand) { - var last = locations.length - 1; - if (last > 0) { - CurveLocation.sort(locations); - // Filter out duplicate locations, but preserve _overlap among - // all duplicated (only one of them will have it defined). - var i = last, - loc = locations[i]; - while(--i >= 0) { - var prev = locations[i]; - if (prev.equals(loc)) { - locations.splice(i + 1, 1); // Remove location. - // Preserve _overlap for both linked intersections. - var over = loc._overlap; - if (over) { - prev._overlap = prev._intersection._overlap = over; - } - last--; - } - loc = prev; - } - } - if (expand) { - for (var i = last; i >= 0; i--) - locations.push(locations[i]._intersection); - CurveLocation.sort(locations); - } - return locations; } }}; }); From 17356637acdf663493f318a42ca9e856abad2ff2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Thu, 17 Sep 2015 10:18:45 +0200 Subject: [PATCH 092/280] Clean up new CurveLocation code. --- src/path/CurveLocation.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index aa30785f..4b45fb41 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -395,13 +395,13 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ // code is used internally in boolean operations where all this // information remains valid during processing. var l = 0, - r = locations.length - 1; + r = locations.length - 1, + curve1 = loc._curve, + path1 = curve1._path; while (l <= r) { var m = (l + r) >>> 1, loc2 = locations[m], - curve1 = loc._curve, curve2 = loc2._curve, - path1 = curve1._path, path2 = curve2._path; diff = path1 === path2 ? curve1.getIndex() + loc._parameter @@ -417,8 +417,8 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ if (loc._overlap) { loc2._overlap = loc2._intersection._overlap = true; } - // We're done, don't insert - return; + // We're done, don't insert, merge with loc2 instead + return loc2; } if (diff < 0) { r = m - 1; @@ -427,16 +427,17 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ } } locations.splice(l, 0, loc); + return loc; }, expand: function(locations) { // Create a copy since add() keeps modifying the array and inserting // at sorted indices. - var copy = locations.slice(); + var expanded = locations.slice(); for (var i = 0, l = locations.length; i < l; i++) { - this.add(copy, locations[i]._intersection, false); + this.add(expanded, locations[i]._intersection, false); } - return copy; + return expanded; } } }, Base.each(Curve.evaluateMethods, function(name) { From 85311cfb296baf3a0cce51302780093889039dca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Fri, 18 Sep 2015 17:27:29 +0200 Subject: [PATCH 093/280] Improve Path#getArea() and #isClockwise() --- src/path/Path.js | 97 +++++++++++++++++++++++++----------------------- 1 file changed, 50 insertions(+), 47 deletions(-) diff --git a/src/path/Path.js b/src/path/Path.js index 244bee8b..dc9f0a10 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -151,7 +151,8 @@ var Path = PathItem.extend(/** @lends Path# */{ parent._currentPath = undefined; // Clockwise state becomes undefined as soon as geometry changes. // Also clear cached mono curves used for winding calculations. - this._length = this._clockwise = this._monoCurves = undefined; + this._length = this._area = this._clockwise = this._monoCurves = + undefined; if (flags & /*#=*/ChangeFlag.SEGMENTS) { this._version++; // See CurveLocation } else if (this._curves) { @@ -806,10 +807,11 @@ var Path = PathItem.extend(/** @lends Path# */{ */ getLength: function() { if (this._length == null) { - var curves = this.getCurves(); - this._length = 0; + var curves = this.getCurves(), + length = 0; for (var i = 0, l = curves.length; i < l; i++) - this._length += curves[i].getLength(); + length += curves[i].getLength(); + this._length = length; } return this._length; }, @@ -822,11 +824,50 @@ var Path = PathItem.extend(/** @lends Path# */{ * @bean */ getArea: function() { - var curves = this.getCurves(); - var area = 0; - for (var i = 0, l = curves.length; i < l; i++) - area += curves[i].getArea(); - return area; + if (this._area == null) { + var segments = this._segments, + count = segments.length, + last = count - 1, + area = 0; + for (var i = 0, l = this._closed ? count : last; i < l; i++) { + area += Curve.getArea(Curve.getValues( + segments[i], segments[i < last ? i + 1 : 0])); + } + this._area = area; + } + return this._area; + }, + + /** + * Specifies whether the path is oriented clock-wise. + * + * @type Boolean + * @bean + */ + isClockwise: function() { + if (this._clockwise !== undefined) + return this._clockwise; + var segments = this._segments, + count = segments.length, + last = count - 1, + sum = 0; + // TODO: Check if this works correctly for all open paths. + for (var i = 0, l = this._closed ? count : last; i < l; i++) { + sum += Curve.getEdgeSum(Curve.getValues( + segments[i], segments[i < last ? i + 1 : 0])); + } + return sum > 0; + }, + + setClockwise: function(clockwise) { + // Only revers the path if its clockwise orientation is not the same + // as what it is now demanded to be. + // On-the-fly conversion to boolean: + if (this.isClockwise() != (clockwise = !!clockwise)) + this.reverse(); + // Reverse only flips _clockwise state if it was already set, so let's + // always set this here now. + this._clockwise = clockwise; }, /** @@ -1223,29 +1264,6 @@ var Path = PathItem.extend(/** @lends Path# */{ return null; }, - /** - * Specifies whether the path is oriented clock-wise. - * - * @type Boolean - * @bean - */ - isClockwise: function() { - if (this._clockwise !== undefined) - return this._clockwise; - return Path.isClockwise(this._segments); - }, - - setClockwise: function(clockwise) { - // Only revers the path if its clockwise orientation is not the same - // as what it is now demanded to be. - // On-the-fly conversion to boolean: - if (this.isClockwise() != (clockwise = !!clockwise)) - this.reverse(); - // Reverse only flips _clockwise state if it was already set, so let's - // always set this here now. - this._clockwise = clockwise; - }, - /** * Reverses the orientation of the path, by reversing all its segments. */ @@ -2684,21 +2702,6 @@ new function() { // PostScript-style drawing commands // Mess with indentation in order to get more line-space below: statics: { - /** - * Determines whether the segments describe a path in clockwise or counter- - * clockwise orientation. - * - * @private - */ - isClockwise: function(segments) { - var sum = 0; - // TODO: Check if this works correctly for all open paths. - for (var i = 0, l = segments.length; i < l; i++) - sum += Curve.getEdgeSum(Curve.getValues( - segments[i], segments[i + 1 < l ? i + 1 : 0])); - return sum > 0; - }, - /** * Returns the bounding rectangle of the item excluding stroke width. * From ae93652b56efd22530e90087d95c2d69eb561dd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Fri, 18 Sep 2015 17:31:23 +0200 Subject: [PATCH 094/280] Clean up getIntersection() methods. Now that they filter the results on the fly. --- src/item/Item.js | 6 +++--- src/path/Curve.js | 4 ++-- src/path/PathItem.js | 31 +++++++++++++++++++------------ 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/src/item/Item.js b/src/item/Item.js index a4e9f4c1..d380ebbf 100644 --- a/src/item/Item.js +++ b/src/item/Item.js @@ -1684,10 +1684,10 @@ var Item = Base.extend(Emitter, /** @lends Item# */{ intersects: function(item, _matrix) { if (!(item instanceof Item)) return false; - // Tell _getIntersections to return as soon as some intersections are + // Tell getIntersections() to return as soon as some intersections are // found, because all we care for here is there are some or none: - return this._asPathItem()._getIntersections(item._asPathItem(), - _matrix || item._matrix, [], true).length > 0; + return this._asPathItem().getIntersections(item._asPathItem(), + _matrix || item._matrix, true).length > 0; }, /** diff --git a/src/path/Curve.js b/src/path/Curve.js index 9e98081e..35857926 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -402,7 +402,7 @@ var Curve = Base.extend(/** @lends Curve# */{ * curves */ getIntersections: function(curve) { - return Curve._getIntersections(this.getValues(), curve.getValues(), + return Curve.getIntersections(this.getValues(), curve.getValues(), this, curve, [], {}); }, @@ -1706,7 +1706,7 @@ new function() { // Scope for intersection using bezier fat-line clipping // We need to provide the original left curve reference to the // #getIntersections() calls as it is required to create the resulting // CurveLocation objects. - _getIntersections: function(v1, v2, c1, c2, locations, param) { + getIntersections: function(v1, v2, c1, c2, locations, param) { var min = Math.min, max = Math.max; // Avoid checking curves if completely out of control bounds. diff --git a/src/path/PathItem.js b/src/path/PathItem.js index 00ad1b92..0e4fa9ae 100644 --- a/src/path/PathItem.js +++ b/src/path/PathItem.js @@ -57,28 +57,25 @@ var PathItem = Item.extend(/** @lends PathItem# */{ * } * } */ - getIntersections: function(path, _matrix) { + getIntersections: function(path, _matrix, _returnFirst) { // NOTE: For self-intersection, path is null. This means you can also // just call path.getIntersections() without an argument to get self // intersections. // NOTE: The hidden argument _matrix is used internally to override the // passed path's transformation matrix. - return this._getIntersections(this !== path ? path : null, _matrix, []); - }, - - _getIntersections: function(path, matrix, locations, returnFirst) { - var self = !path, // self-intersections? + var self = this === path || !path, // self-intersections? curves1 = this.getCurves(), curves2 = self ? curves1 : path.getCurves(), matrix1 = this._matrix.orNullIfIdentity(), matrix2 = self ? matrix1 - : (matrix || path._matrix).orNullIfIdentity(), + : (_matrix || path._matrix).orNullIfIdentity(), length1 = curves1.length, - length2 = path ? curves2.length : length1, + length2 = self ? length1 : curves2.length, + locations = [], values2 = []; // First check the bounds of the two paths. If they don't intersect, // we don't need to iterate through their curves. - if (path && !this.getBounds(matrix1).touches(path.getBounds(matrix2))) + if (!self && !this.getBounds(matrix1).touches(path.getBounds(matrix2))) return locations; // Cache values for curves2 as we re-iterate them for each in curves1. for (var i = 0; i < length2; i++) @@ -104,7 +101,7 @@ var PathItem = Item.extend(/** @lends PathItem# */{ // Self intersecting is found by dividing the curve in two // and and then applying the normal curve intersection code. var parts = Curve.subdivide(values1, 0.5); - Curve._getIntersections(parts[0], parts[1], curve1, curve1, + Curve.getIntersections(parts[0], parts[1], curve1, curve1, locations, { // Only possible if there is only one closed curve: startConnected: length1 === 1 && p1.equals(p2), @@ -124,12 +121,12 @@ var PathItem = Item.extend(/** @lends PathItem# */{ for (var j = self ? i + 1 : 0; j < length2; j++) { // There might be already one location from the above // self-intersection check: - if (returnFirst && locations.length) + if (_returnFirst && locations.length) break; var curve2 = curves2[j]; // Avoid end point intersections on consecutive curves when // self intersecting. - Curve._getIntersections( + Curve.getIntersections( values1, values2[j], curve1, curve2, locations, self ? { // Do not compare indices here to determine connection, @@ -144,6 +141,16 @@ var PathItem = Item.extend(/** @lends PathItem# */{ return locations; }, + getCrossings: function(path) { + var locations = this.getIntersections(path); + for (var i = locations.length - 1; i >= 0; i--) { + // TODO: An overlap could be either a crossing or a tangent! + if (!locations[i].isCrossing() && !locations[i]._overlap) + locations.splice(i, 1); + } + return locations; + }, + _asPathItem: function() { // See Item#_asPathItem() return this; From 73a9989261f923c7ae89ad876f632b3b090c7739 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Fri, 18 Sep 2015 17:32:19 +0200 Subject: [PATCH 095/280] Fix CurveLocation#isCrossing() for locations in the middle of curves. --- src/path/CurveLocation.js | 57 +++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 32 deletions(-) diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index 4b45fb41..c9fddaea 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -282,49 +282,42 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ return curve && curve.split(this.getParameter(), true); }, - isCrossing: function(_report) { + isTangent: function() { + var t1 = this.getTangent(), + inter = this._intersection, + t2 = inter && inter.getTangent(); + return t1 && t2 ? t1.isCollinear(t2) : false; + }, + + isCrossing: function() { // Implementation based on work by Andy Finnell: // http://losingfight.com/blog/2011/07/09/how-to-implement-boolean-operations-on-bezier-paths-part-3/ // https://bitbucket.org/andyfinnell/vectorboolean - var intersection = this._intersection, - crossing = this._crossing; - if (crossing != null || !intersection) - return crossing || false; - // TODO: isTangent() ? - // TODO: isAtEndPoint() ? - // -> Return if it's a tangent, or if not at an end point, only end - // point intersections need more checking! + var inter = this._intersection; + if (!inter) + return false; + var t = this._parameter, + tMin = /*#=*/Numerical.CURVETIME_EPSILON, + tMax = 1 - tMin; + // If the intersection is in the middle of the path, it is either a + // tangent or a crossing, no need for the detailed corner check below. + // But we do need a check for the edge case of tangents? + if (t >= tMin && t <= tMax) + return !this.isTangent(); // Values for getTangentAt() that are almost 0 and 1. // NOTE: Even though getTangentAt() has code to support 0 and 1 instead // of tMin and tMax, we still need to use this instead, as other issues // emerge from switching to 0 and 1 in edge cases. // NOTE: VectorBoolean has code that slowly shifts these points inwards // until the resulting tangents are not ambiguous. Do we need this too? - var tMin = /*#=*/Numerical.CURVETIME_EPSILON, - tMax = 1 - tMin, - PI = Math.PI, - // TODO: Make getCurve() sync work in boolean ops after splitting!!! - c2 = this._curve, + // TODO: Make getCurve() sync work in boolean ops after splitting!!! + var c2 = this._curve, c1 = c2.getPrevious(), - c4 = intersection._curve, - c3 = c4.getPrevious(); + c4 = inter._curve, + c3 = c4.getPrevious(), + PI = Math.PI; if (!c1 || !c3) - return this._crossing = false; - if (_report) { - new Path.Circle({ - center: this.getPoint(), - radius: 10, - strokeColor: 'red' - }); - new Path({ - segments: [c1.getSegment1(), c1.getSegment2(), c2.getSegment2()], - strokeColor: 'red' - }); - new Path({ - segments: [c3.getSegment1(), c3.getSegment2(), c4.getSegment2()], - strokeColor: 'orange' - }); - } + return false; function isInRange(angle, min, max) { return min < max From 59a23fdd3f20aa7e22812415257ad230628b0a69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Fri, 18 Sep 2015 17:33:42 +0200 Subject: [PATCH 096/280] Improve debug logging. --- src/path/PathItem.Boolean.js | 39 +++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 1d2af4b0..2aa45ea8 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -69,11 +69,25 @@ PathItem.inject(new function() { return result; } + var scaleFactor = 0.5; // 1 / 3000; + var textAngle = 33; + var fontSize = 5; + + var segmentOffset; + var pathIndices; + var pathIndex; + var pathCount; + // Boolean operators return true if a curve with the given winding // contribution contributes to the final result or not. They are called // for each curve in the graph after curves in the operands are // split at intersections. function computeBoolean(path1, path2, operation) { + segmentOffset = {}; + pathIndices = {}; + pathIndex = 0; + pathCount = 1; + // We do not modify the operands themselves, but create copies instead, // fas produced by the calls to preparePath(). // Note that the result paths might not belong to the same type @@ -149,10 +163,6 @@ PathItem.inject(new function() { path1, path2); } - var scaleFactor = 1; // 1 / 3000; - var textAngle = 33; - var fontSize = 5; - /** * Private method for splitting a PathItem at the given intersections. * The routine works for both self intersections and intersections @@ -175,8 +185,8 @@ PathItem.inject(new function() { 'o', !!other._overlap, 'p', other.getPoint()]; new Path.Circle({ center: inter.point, - radius: fontSize / 2 * scaleFactor, - strokeColor: 'green', + radius: 2 * scaleFactor, + fillColor: inter.isCrossing() ? 'red' : 'green', strokeScaling: false }); console.log(log.map(function(v) { @@ -443,10 +453,6 @@ PathItem.inject(new function() { } } - var segmentOffset = {}; - var pathIndices = {}; - var pathIndex = 0; - /** * Private method to trace closed contours from a set of segments according * to a set of constraints-winding contribution and a custom operator. @@ -496,11 +502,11 @@ PathItem.inject(new function() { var inter = seg._intersection; labelSegment(seg, '#' + pathCount + '.' + (path ? path._segments.length + 1 : 1) - + ' ' + (segmentCount++) + '/' + index + ': ' + text - + ' id: ' + seg._path._id + + ' (' + (index + 1) + '): ' + text + + ' id: ' + seg._path._id + '.' + seg._index + ' v: ' + (seg._visited ? 1 : 0) + ' p: ' + seg._point - + ' op: ' + operator(seg._winding) + + ' op: ' + (operator && operator(seg._winding)) + ' ov: ' + (inter && inter._overlap || 0) + ' wi: ' + seg._winding , color); @@ -517,7 +523,7 @@ PathItem.inject(new function() { labelSegment(seg, '#' + pathIndex + '.' + (i + 1) + ' i: ' + !!inter - + ' id: ' + seg._path._id + + ' id: ' + seg._path._id + '.' + seg._index + ' pt: ' + seg._point + ' ov: ' + (inter && inter._overlap || 0) + ' wi: ' + seg._winding @@ -590,8 +596,9 @@ PathItem.inject(new function() { if (seg._visited) { // We didn't manage to switch, so stop right here. console.error('Unable to switch to intersecting segment, ' - + 'aborting #' + pathCount + '.' + - (path ? path._segments.length + 1 : 1)); + + 'aborting #' + pathCount + '.' + + (path ? path._segments.length + 1 : 1) + + ' id: ' + seg._path._id + '.' + seg._index); break; } // Add the current segment to the path, and mark the added From c70f8cb3cc56e4e2cf58a4f190697ac950b9eeb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Fri, 18 Sep 2015 17:46:46 +0200 Subject: [PATCH 097/280] Simplify overlap calculations by keeping the original winding value. --- src/path/PathItem.Boolean.js | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 2aa45ea8..353c95f5 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -449,6 +449,7 @@ PathItem.inject(new function() { break; } } + seg._originalWinding = winding; seg._winding = wind; } } @@ -555,20 +556,6 @@ PathItem.inject(new function() { // Switch to the intersecting segment, as we need to // resolving self-Intersections. seg = other; - } else if (operation !== 'intersect' && inter._overlap) { - // Switch to the overlapping intersecting segment if its - // winding number along the curve is 1, meaning we leave the - // overlapping area. - // NOTE: We cannot check the next (overlapping) segment - // since its winding number will always be 2. - var curve = other.getCurve(); - if (getWinding(curve.getPointAt(0.5, true), - monoCurves, curve.isHorizontal()) === 1) { - drawSegment(seg, 'overlap-cross', i, 'orange'); - seg = other; - } else { - drawSegment(seg, 'overlap-stay', i, 'orange'); - } } else if (operation === 'exclude') { // We need to handle exclusion separately, as we want to // switch at each crossing, and at each intersection within @@ -580,6 +567,16 @@ PathItem.inject(new function() { } else { drawSegment(seg, 'exclude-stay', i, 'blue'); } + } else if (inter._overlap + && /^(unite|subtract)$/.test(operation)) { + // Switch to the overlapping intersecting segment if it is + // part of the boolean result. + if (operator(other._originalWinding)) { + drawSegment(seg, 'overlap-cross', i, 'orange'); + seg = other; + } else { + drawSegment(seg, 'overlap-stay', i, 'orange'); + } } else if (operator(seg._winding)) { // Do not switch to the intersecting segment as this segment // is part of the the boolean result. From 87687d816b359e8e20448f49576646d903a67c38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Fri, 18 Sep 2015 17:51:03 +0200 Subject: [PATCH 098/280] Implement PathItem#resolveCrossings() based on the new #getCrossings() method. --- src/path/PathItem.Boolean.js | 56 +++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 353c95f5..4ce2ba5d 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -49,7 +49,8 @@ PathItem.inject(new function() { // paths and remove empty curves, and #reorient() to make sure all paths // have correct winding direction. function preparePath(path) { - return path.clone(false).reduce().reorient().transform(null, true, true); + return path.clone(false).reduce().resolveCrossings() + .transform(null, true, true); } function finishBoolean(ctor, paths, path1, path2) { @@ -102,29 +103,9 @@ PathItem.inject(new function() { // Split curves at intersections on both paths. Note that for self // intersection, _path2 will be null and getIntersections() handles it. // console.time('intersection'); - var locations = _path1._getIntersections(_path2, null, []); - // console.timeEnd('inter'); - if (_path2) { - // console.time('self-intersection'); - // Resolve self-intersections on both source paths and add them to - // the locations too: - // var self = []; - _path1._getIntersections(null, null, locations); - _path2._getIntersections(null, null, locations); - /* - self.forEach(function(inter) { - new Path.Circle({ - center: inter.point, - radius: fontSize / 2 * scaleFactor, - fillColor: 'red' - }); - }); - console.log(self); - */ - // console.timeEnd('self-intersection'); - } + var locations = CurveLocation.expand(_path1.getCrossings(_path2)); // console.timeEnd('intersection'); - splitPath(CurveLocation.expand(locations)); + splitPath(locations); var segments = [], // Aggregate of all curves in both operands, monotonic in y @@ -535,7 +516,7 @@ PathItem.inject(new function() { operator = operators[operation]; for (var i = 0, l = segments.length; i < l; i++) { var seg = segments[i]; - if (seg._visited || !operator(seg._winding)) + if (seg._visited || operator && !operator(seg._winding)) continue; var path = new Path(Item.NO_INSERT), start = seg, @@ -551,7 +532,7 @@ PathItem.inject(new function() { // Just add the first segment and all segments that have no // intersection. drawSegment(seg, 'add', i, 'black'); - } else if (other._path === seg._path) { // Self-intersection + } else if (!operator) { // Resolve self-intersections drawSegment(seg, 'self-int', i, 'purple'); // Switch to the intersecting segment, as we need to // resolving self-Intersections. @@ -610,7 +591,7 @@ PathItem.inject(new function() { } else if (seg._visited && (!other || other._visited)) { drawSegment(seg, 'visited', i, 'red'); } - if (!inter && !operator(seg._winding)) { + if (!inter && operator && !operator(seg._winding)) { // TODO: We really should find a way to go backwards perhaps // and try another path when this happens? drawSegment(seg, 'discard', i, 'red'); @@ -625,7 +606,7 @@ PathItem.inject(new function() { // Intersections are always part of the resulting path, for // all other segments check the winding contribution to see // if they are to be kept. If not, the chain has to end here - && (inter || operator(seg._winding))); + && (inter || !operator || operator(seg._winding))); // Finish with closing the paths if necessary, correctly linking up // curves etc. if (seg === start || seg === otherStart) { @@ -733,6 +714,27 @@ PathItem.inject(new function() { divide: function(path) { return finishBoolean(Group, [this.subtract(path), this.intersect(path)], this, path); + }, + + resolveCrossings: function() { + var locations = this.getCrossings(); + if (!locations.length) + return this.reorient(); + var reportSegments = window.reportSegments; + var reportIntersections = window.reportIntersections; + window.reportSegments = false; + window.reportIntersections = false; + splitPath(CurveLocation.expand(locations)); + var paths = this._children || [this], + segments = []; + for (var i = 0, l = paths.length; i < l; i++) { + segments.push.apply(segments, paths[i]._segments); + } + var res = finishBoolean(tracePaths(segments), this, null, false) + .reorient(); + window.reportSegments = reportSegments; + window.reportIntersections = reportIntersections; + return res; } }; }); From 66b01973f04737d1548e04cc8e30667f2bf72731 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Fri, 18 Sep 2015 17:51:43 +0200 Subject: [PATCH 099/280] Simplify exclusion handling for new boolean code. --- src/path/PathItem.Boolean.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 4ce2ba5d..0ecde008 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -539,10 +539,8 @@ PathItem.inject(new function() { seg = other; } else if (operation === 'exclude') { // We need to handle exclusion separately, as we want to - // switch at each crossing, and at each intersection within - // the exclusion area even if it is not a crossing. - if (inter.isCrossing() - || path2 && path2.contains(seg._point)) { + // switch at each crossing. + if (inter.isCrossing()) { drawSegment(seg, 'exclude-cross', i, 'green'); seg = other; } else { From 23443dc8f483109b3eebc08b5643887ac305ad71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Fri, 18 Sep 2015 17:51:57 +0200 Subject: [PATCH 100/280] Clean up boolean code. --- 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 0ecde008..18b0f76d 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -53,11 +53,12 @@ PathItem.inject(new function() { .transform(null, true, true); } - function finishBoolean(ctor, paths, path1, path2) { - var result = new ctor(Item.NO_INSERT); + function finishBoolean(paths, path1, path2, reduce) { + var result = new CompoundPath(Item.NO_INSERT); result.addChildren(paths, true); // See if the CompoundPath can be reduced to just a simple Path. - result = result.reduce(); + if (reduce) + result = result.reduce(); // Insert the resulting path above whichever of the two paths appear // further up in the stack. result.insertAbove(path2 && path1.isSibling(path2) @@ -139,9 +140,8 @@ PathItem.inject(new function() { operation); } } - return finishBoolean(CompoundPath, - tracePaths(segments, monoCurves, operation, _path1, _path2), - path1, path2); + return finishBoolean(tracePaths(segments, operation), path1, path2, + true); } /** @@ -213,6 +213,8 @@ PathItem.inject(new function() { // be set back straight at the end. if (noHandles) clearSegments.push(segment); + // TODO: Figure out the right value for t + t = 0; // Since it's split (might be 1 also?) } // Link the new segment with the intersection on the other curve if (segment._intersection) { @@ -221,6 +223,7 @@ PathItem.inject(new function() { } else { segment._intersection = loc._intersection; } + // TODO: Figure out why setCurves doesn't work: // loc._setCurve(segment.getCurve()); loc._segment = segment; loc._parameter = t; @@ -447,9 +450,7 @@ PathItem.inject(new function() { * not * @return {Path[]} the contours traced */ - function tracePaths(segments, monoCurves, operation, path1, path2) { - var segmentCount = 0; - var pathCount = 1; + function tracePaths(segments, operation) { function labelSegment(seg, text, color) { var point = seg.point; @@ -700,6 +701,8 @@ PathItem.inject(new function() { */ exclude: function(path) { return computeBoolean(this, path, 'exclude'); + // return finishBoolean([this.subtract(path), path.subtract(this)], + // this, path, true); }, /** @@ -710,8 +713,8 @@ PathItem.inject(new function() { * @return {Group} the resulting group item */ divide: function(path) { - return finishBoolean(Group, - [this.subtract(path), this.intersect(path)], this, path); + return finishBoolean([this.subtract(path), this.intersect(path)], + this, path, true); }, resolveCrossings: function() { From 5af391d3333d8d655690915f49af96a45d3ccc39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Fri, 18 Sep 2015 17:52:14 +0200 Subject: [PATCH 101/280] Fix errors in Boolean Operations example. --- examples/Scripts/BooleanOperations.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/Scripts/BooleanOperations.html b/examples/Scripts/BooleanOperations.html index 7520ab6c..f9017c69 100644 --- a/examples/Scripts/BooleanOperations.html +++ b/examples/Scripts/BooleanOperations.html @@ -360,10 +360,9 @@ function disperse(path, distance) { distance = distance || 10; - if (! path instanceof CompoundPath || ! path instanceof Group) { return; } var center = path.bounds.center; var children = path.children, i ,len; - for (var i = 0, len = children.length; i < len; i++) { + for (var i = 0, len = children && children.length; i < len; i++) { var cCenter = children[i].bounds.center; var vec = cCenter.subtract(center); vec = (vec.isClose([0,0], 0.5))? vec : vec.normalize(distance); From c6a38589e9747204d537012f32eb7778859cde07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Fri, 18 Sep 2015 18:06:15 +0200 Subject: [PATCH 102/280] Remove special handling of winding contribution on overlaps Looks like the new code handles this correclty now! --- src/path/PathItem.Boolean.js | 23 ++--------------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 18b0f76d..3c00f450 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -415,26 +415,7 @@ PathItem.inject(new function() { // Assign the average winding to the entire curve chain. var winding = Math.round(windingSum / 3); for (var j = chain.length - 1; j >= 0; j--) { - var seg = chain[j].segment, - inter = seg._intersection, - wind = winding; - // We need to handle the edge cases of overlapping curves - // differently based on the type of operation, and adjust the - // winding number accordingly: - if (inter && inter._overlap) { - switch (operation) { - case 'unite': - if (wind === 1) - wind = 2; - break; - case 'intersect': - if (wind === 2) - wind = 1; - break; - } - } - seg._originalWinding = winding; - seg._winding = wind; + chain[j].segment._winding = winding; } } @@ -551,7 +532,7 @@ PathItem.inject(new function() { && /^(unite|subtract)$/.test(operation)) { // Switch to the overlapping intersecting segment if it is // part of the boolean result. - if (operator(other._originalWinding)) { + if (operator(other._winding)) { drawSegment(seg, 'overlap-cross', i, 'orange'); seg = other; } else { From 17dc5eb51aee866ac41079acadcbaea38fc6aaef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Fri, 18 Sep 2015 20:15:18 +0200 Subject: [PATCH 103/280] Allow gettings of unstyled bounds on curves without paths. --- src/path/Curve.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 35857926..8ad13b20 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -760,8 +760,10 @@ statics: { if (!bounds) { // Calculate the curve bounds by passing a segment list for the // curve to the static Path.get*Boudns methods. - bounds = this._bounds[name] = Path[name]([this._segment1, - this._segment2], false, this._path.getStyle()); + var path = this._path; + bounds = this._bounds[name] = Path[name]( + [this._segment1, this._segment2], false, + path && path.getStyle()); } return bounds.clone(); }; From 10eafccd1aebfee182d0a9d3cfad8abf16adad37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Fri, 18 Sep 2015 21:09:57 +0200 Subject: [PATCH 104/280] Implement 0.75 * handle scaling in curve bounds checks. --- src/path/Curve.js | 48 +++++++++++++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 8ad13b20..ab9dee2f 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1709,28 +1709,44 @@ new function() { // Scope for intersection using bezier fat-line clipping // #getIntersections() calls as it is required to create the resulting // CurveLocation objects. getIntersections: function(v1, v2, c1, c2, locations, param) { - var min = Math.min, - max = Math.max; // Avoid checking curves if completely out of control bounds. - // Also detect and handle overlaps. + // As a little optimization, we can scale the handles with 0.75 + // before calculating the control bounds and still be sure that the + // curve is fully contained. + var c1p1x = v1[0], c1p1y = v1[1], + c1p2x = v1[6], c1p2y = v1[7], + c2p1x = v2[0], c2p1y = v2[1], + c2p2x = v2[6], c2p2y = v2[7], + c1h1x = (3 * v1[2] + c1p1x) / 4, + c1h1y = (3 * v1[3] + c1p1y) / 4, + c1h2x = (3 * v1[4] + c1p2x) / 4, + c1h2y = (3 * v1[5] + c1p2y) / 4, + c2h1x = (3 * v2[2] + c2p1x) / 4, + c2h1y = (3 * v2[3] + c2p1y) / 4, + c2h2x = (3 * v2[4] + c2p2x) / 4, + c2h2y = (3 * v2[5] + c2p2y) / 4, + min = Math.min, + max = Math.max; if (!( - max(v1[0], v1[2], v1[4], v1[6]) >= - min(v2[0], v2[2], v2[4], v2[6]) && - max(v1[1], v1[3], v1[5], v1[7]) >= - min(v2[1], v2[3], v2[5], v2[7]) && - min(v1[0], v1[2], v1[4], v1[6]) <= - max(v2[0], v2[2], v2[4], v2[6]) && - min(v1[1], v1[3], v1[5], v1[7]) <= - max(v2[1], v2[3], v2[5], v2[7]) - ) || !param.startConnected && !param.endConnected + max(c1p1x, c1h1x, c1h2x, c1p2x) >= + min(c2p1x, c2h1x, c2h2x, c2p2x) && + min(c1p1x, c1h1x, c1h2x, c1p2x) <= + max(c2p1x, c2h1x, c2h2x, c2p2x) && + max(c1p1y, c1h1y, c1h2y, c1p2y) >= + min(c2p1y, c2h1y, c2h2y, c2p2y) && + min(c1p1y, c1h1y, c1h2y, c1p2y) <= + max(c2p1y, c2h1y, c2h2y, c2p2y) + ) + // Also detect and handle overlaps: + || !param.startConnected && !param.endConnected && addOverlap(v1, v2, c1, c2, locations, param)) return locations; var straight1 = Curve.isStraight(v1), straight2 = Curve.isStraight(v2), - c1p1 = new Point(v1[0], v1[1]), - c1p2 = new Point(v1[6], v1[7]), - c2p1 = new Point(v2[0], v2[1]), - c2p2 = new Point(v2[6], v2[7]), + c1p1 = new Point(c1p1x, c1p1y), + c1p2 = new Point(c1p2x, c1p2y), + c2p1 = new Point(c2p1x, c2p1y), + c2p2 = new Point(c2p2x, c2p2y), epsilon = /*#=*/Numerical.EPSILON; // Handle the special case where the first curve's stat-point // overlaps with the second curve's start- or end-points. From 8cf562c57bb3e4773527a81c14cd133cf08a302d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Fri, 18 Sep 2015 21:10:58 +0200 Subject: [PATCH 105/280] Revert "Remove special handling of winding contribution on overlaps" This reverts commit c6a38589e9747204d537012f32eb7778859cde07. The special handling seems to be still necessary in some edge cases, e.g. in BooleanOperations.html --- src/path/PathItem.Boolean.js | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 3c00f450..18b0f76d 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -415,7 +415,26 @@ PathItem.inject(new function() { // Assign the average winding to the entire curve chain. var winding = Math.round(windingSum / 3); for (var j = chain.length - 1; j >= 0; j--) { - chain[j].segment._winding = winding; + var seg = chain[j].segment, + inter = seg._intersection, + wind = winding; + // We need to handle the edge cases of overlapping curves + // differently based on the type of operation, and adjust the + // winding number accordingly: + if (inter && inter._overlap) { + switch (operation) { + case 'unite': + if (wind === 1) + wind = 2; + break; + case 'intersect': + if (wind === 2) + wind = 1; + break; + } + } + seg._originalWinding = winding; + seg._winding = wind; } } @@ -532,7 +551,7 @@ PathItem.inject(new function() { && /^(unite|subtract)$/.test(operation)) { // Switch to the overlapping intersecting segment if it is // part of the boolean result. - if (operator(other._winding)) { + if (operator(other._originalWinding)) { drawSegment(seg, 'overlap-cross', i, 'orange'); seg = other; } else { From b5af47a7b1681414ad2961f872dbfc8b6b62d482 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Fri, 18 Sep 2015 21:41:54 +0200 Subject: [PATCH 106/280] Implement a better approach to calculate Path#clockwise... ...merging code with Path#area. Closes #788 --- src/path/Curve.js | 33 +++++++++++++++------------------ src/path/Path.js | 11 +---------- 2 files changed, 16 insertions(+), 28 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index ab9dee2f..f9d26c50 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -665,29 +665,26 @@ statics: { }, getArea: function(v) { - var p1x = v[0], p1y = v[1], - c1x = v[2], c1y = v[3], - c2x = v[4], c2y = v[5], - p2x = v[6], p2y = v[7]; + // This is a combination of the methods to decide if a path is clockwise + // and to calculate the area, as described here: // http://objectmix.com/graphics/133553-area-closed-bezier-curve.html - return ( 3.0 * c1y * p1x - 1.5 * c1y * c2x - - 1.5 * c1y * p2x - 3.0 * p1y * c1x - - 1.5 * p1y * c2x - 0.5 * p1y * p2x - + 1.5 * c2y * p1x + 1.5 * c2y * c1x - - 3.0 * c2y * p2x + 0.5 * p2y * p1x - + 1.5 * p2y * c1x + 3.0 * p2y * c2x) / 10; - }, - - getEdgeSum: function(v) { - // Method derived from: // http://stackoverflow.com/questions/1165647 // We treat the curve points and handles as the outline of a polygon of // which we determine the orientation using the method of calculating // the sum over the edges. This will work even with non-convex polygons, - // telling you whether it's mostly clockwise - return (v[0] - v[2]) * (v[3] + v[1]) - + (v[2] - v[4]) * (v[5] + v[3]) - + (v[4] - v[6]) * (v[7] + v[5]); + // telling you whether it's mostly clockwise. + // With bezier curves, the trick appears to be to calculate edge sum + // with half the handles' lengths, and then: + // area = 6 * edge-sum / 10 + var p1x = v[0], p1y = v[1], + p2x = v[6], p2y = v[7], + h1x = (v[2] + p1x) / 2, + h1y = (v[3] + p1y) / 2, + h2x = (v[4] + v[6]) / 2, + h2y = (v[5] + v[7]) / 2; + return 6 * ((p1x - h1x) * (h1y + p1y) + + (h1x - h2x) * (h2y + h1y) + + (h2x - p2x) * (p2y + h2y)) / 10; }, getBounds: function(v) { diff --git a/src/path/Path.js b/src/path/Path.js index dc9f0a10..fdc601a2 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -847,16 +847,7 @@ var Path = PathItem.extend(/** @lends Path# */{ isClockwise: function() { if (this._clockwise !== undefined) return this._clockwise; - var segments = this._segments, - count = segments.length, - last = count - 1, - sum = 0; - // TODO: Check if this works correctly for all open paths. - for (var i = 0, l = this._closed ? count : last; i < l; i++) { - sum += Curve.getEdgeSum(Curve.getValues( - segments[i], segments[i < last ? i + 1 : 0])); - } - return sum > 0; + return this.getArea() >= 0; }, setClockwise: function(clockwise) { From a95ba12bc3f5c359c6a1d8e83139f5427f417b81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Fri, 18 Sep 2015 22:26:09 +0200 Subject: [PATCH 107/280] isCrossing() needs parameter checks on both curves. --- src/path/CurveLocation.js | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index c9fddaea..f9d75563 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -289,20 +289,23 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ return t1 && t2 ? t1.isCollinear(t2) : false; }, - isCrossing: function() { + isCrossing: function(_report) { // Implementation based on work by Andy Finnell: // http://losingfight.com/blog/2011/07/09/how-to-implement-boolean-operations-on-bezier-paths-part-3/ // https://bitbucket.org/andyfinnell/vectorboolean var inter = this._intersection; if (!inter) return false; - var t = this._parameter, + // TODO: Make getCurve() and getParameter() sync work in boolean ops + // before and after splitting!!! + var t1 = this._parameter, + t2 = inter._parameter, tMin = /*#=*/Numerical.CURVETIME_EPSILON, tMax = 1 - tMin; // If the intersection is in the middle of the path, it is either a // tangent or a crossing, no need for the detailed corner check below. // But we do need a check for the edge case of tangents? - if (t >= tMin && t <= tMax) + if (t1 >= tMin && t1 <= tMax || t2 >= tMin && t2 <= tMax) return !this.isTangent(); // Values for getTangentAt() that are almost 0 and 1. // NOTE: Even though getTangentAt() has code to support 0 and 1 instead @@ -310,7 +313,6 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ // emerge from switching to 0 and 1 in edge cases. // NOTE: VectorBoolean has code that slowly shifts these points inwards // until the resulting tangents are not ambiguous. Do we need this too? - // TODO: Make getCurve() sync work in boolean ops after splitting!!! var c2 = this._curve, c1 = c2.getPrevious(), c4 = inter._curve, @@ -319,6 +321,24 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ if (!c1 || !c3) return false; + if (_report) { + new Path.Circle({ + center: this.getPoint(), + radius: 10, + strokeColor: 'red' + }); + new Path({ + segments: [c1.getSegment1(), c1.getSegment2(), c2.getSegment2()], + strokeColor: 'red', + strokeWidth: 4 + }); + new Path({ + segments: [c3.getSegment1(), c3.getSegment2(), c4.getSegment2()], + strokeColor: 'orange', + strokeWidth: 4 + }); + } + function isInRange(angle, min, max) { return min < max ? angle > min && angle < max From 081de1d12a178006a163b792b65307c43ed0558e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Fri, 18 Sep 2015 22:29:29 +0200 Subject: [PATCH 108/280] 'exclude' operation needs overlap handling too. --- src/path/PathItem.Boolean.js | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 18b0f76d..6e517e65 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -538,6 +538,15 @@ PathItem.inject(new function() { // Switch to the intersecting segment, as we need to // resolving self-Intersections. seg = other; + } else if (inter._overlap && operation !== 'intersect') { + // Switch to the overlapping intersecting segment if it is + // part of the boolean result. + if (operator(other._originalWinding)) { + drawSegment(seg, 'overlap-cross', i, 'orange'); + seg = other; + } else { + drawSegment(seg, 'overlap-stay', i, 'orange'); + } } else if (operation === 'exclude') { // We need to handle exclusion separately, as we want to // switch at each crossing. @@ -547,16 +556,6 @@ PathItem.inject(new function() { } else { drawSegment(seg, 'exclude-stay', i, 'blue'); } - } else if (inter._overlap - && /^(unite|subtract)$/.test(operation)) { - // Switch to the overlapping intersecting segment if it is - // part of the boolean result. - if (operator(other._originalWinding)) { - drawSegment(seg, 'overlap-cross', i, 'orange'); - seg = other; - } else { - drawSegment(seg, 'overlap-stay', i, 'orange'); - } } else if (operator(seg._winding)) { // Do not switch to the intersecting segment as this segment // is part of the the boolean result. From 18c5a06f45076757f904fb71414d027eae5a737e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Fri, 18 Sep 2015 23:00:47 +0200 Subject: [PATCH 109/280] Fix colors in animated boolean operations demo. --- examples/Animated/BooleanOperations.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/Animated/BooleanOperations.html b/examples/Animated/BooleanOperations.html index 9c271a99..83fd01c0 100644 --- a/examples/Animated/BooleanOperations.html +++ b/examples/Animated/BooleanOperations.html @@ -92,7 +92,7 @@ text.content = 'ring.' + operation + '(square)'; } result.selected = true; - result.fillColor = colors[curIndex % operations.length]; + result.fillColor = colors[curIndex % colors.length]; result.moveBelow(text); // If the result is a group, color each of its children differently: From 2c8634793cb51ea264988c0ad0ead760ff6035a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sat, 19 Sep 2015 13:21:29 +0200 Subject: [PATCH 110/280] First attempt at implementing handling of multiple intersections in the same location. Relates to #787, works pretty well already for many situations. --- src/path/PathItem.Boolean.js | 58 ++++++++++++++++++++++++++++++------ 1 file changed, 49 insertions(+), 9 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 6e517e65..c66d181f 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -71,7 +71,7 @@ PathItem.inject(new function() { return result; } - var scaleFactor = 0.5; // 1 / 3000; + var scaleFactor = 1.0; // 1 / 3000; var textAngle = 33; var fontSize = 5; @@ -87,8 +87,6 @@ PathItem.inject(new function() { function computeBoolean(path1, path2, operation) { segmentOffset = {}; pathIndices = {}; - pathIndex = 0; - pathCount = 1; // We do not modify the operands themselves, but create copies instead, // fas produced by the calls to preparePath(). @@ -220,6 +218,12 @@ PathItem.inject(new function() { if (segment._intersection) { console.log('Segment already has an intersection: ' + segment._intersection + ', ' + loc._intersection); + // Create a chain of possible intersections linked through + // _next: + var inter = segment._intersection; + while (inter._next) + inter = inter._next; + inter._next = loc; } else { segment._intersection = loc._intersection; } @@ -451,6 +455,8 @@ PathItem.inject(new function() { * @return {Path[]} the contours traced */ function tracePaths(segments, operation) { + pathIndex = 0; + pathCount = 1; function labelSegment(seg, text, color) { var point = seg.point; @@ -492,6 +498,7 @@ PathItem.inject(new function() { + ' op: ' + (operator && operator(seg._winding)) + ' ov: ' + (inter && inter._overlap || 0) + ' wi: ' + seg._winding + + ' mu: ' + !!(inter && inter._next) , color); } @@ -516,16 +523,23 @@ PathItem.inject(new function() { var paths = [], operator = operators[operation]; for (var i = 0, l = segments.length; i < l; i++) { - var seg = segments[i]; - if (seg._visited || operator && !operator(seg._winding)) + var seg = segments[i], + inter = seg._intersection; + // Do not start a chain with already visited segments, intersections + // with multiple possibilities (TODO: Is that really a problem?), + // and segments that are not going to be part of the resulting + // operation. + if (seg._visited /* || inter && inter._next*/ + || operator && !operator(seg._winding)) continue; var path = new Path(Item.NO_INSERT), start = seg, - inter = seg._intersection, - other = inter && inter._segment, - otherStart = other, + otherStart = null, added = false; // Whether a first segment as added already do { + var other = inter && inter._segment; + if (!otherStart) + otherStart = other; var handleIn = added && seg._handleIn; if (!added || !other || other === start) { // TODO: Is (other === start) check really required? @@ -574,7 +588,8 @@ PathItem.inject(new function() { console.error('Unable to switch to intersecting segment, ' + 'aborting #' + pathCount + '.' + (path ? path._segments.length + 1 : 1) - + ' id: ' + seg._path._id + '.' + seg._index); + + ', id: ' + seg._path._id + '.' + seg._index + + ', multiple: ' + (!!inter._next)); break; } // Add the current segment to the path, and mark the added @@ -583,6 +598,31 @@ PathItem.inject(new function() { seg._visited = added = true; seg = seg.getNext(); inter = seg && seg._intersection; + // If there are multiple possible intersections, find the one + // that's either connecting back to start or is not visited yet, + // and will be part of the boolean result: + function check(inter, ignoreOther) { + if (!inter) + return null; + var seg = inter._segment, + next = seg.getNext(); + console.log('Multiple' + + ', seg: ' + seg._path._id + '.' + seg._index + + ', next: ' + next._path._id + '.' + next._index + + ', visited:' + !!next._visited + + ', operator:' + (operator && operator(next._winding)) + + ', start: ' + (next === start) + + ', next: ' + (!!inter._next)); + return next === start || !next._visited + && (!operator || operator(next._winding)) + ? inter + // If no match, check the other intersection first, + // then carry on with the next linked intersection. + : !ignoreOther && check(inter._intersection, true) + || check(inter._next); + } + inter = check(inter) || inter; + other = inter && inter._segment; if (seg === start || seg === otherStart) { drawSegment(seg, 'done', i, 'red'); From 1d6f552212ed7cedeec1c5a42f944f4f5bb4b3d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sat, 19 Sep 2015 13:42:48 +0200 Subject: [PATCH 111/280] Prevent endless loop through circular references. --- src/path/PathItem.Boolean.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index c66d181f..d6bcbbe8 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -601,7 +601,7 @@ PathItem.inject(new function() { // If there are multiple possible intersections, find the one // that's either connecting back to start or is not visited yet, // and will be part of the boolean result: - function check(inter, ignoreOther) { + function check(inter, prev, ignoreOther) { if (!inter) return null; var seg = inter._segment, @@ -618,8 +618,8 @@ PathItem.inject(new function() { ? inter // If no match, check the other intersection first, // then carry on with the next linked intersection. - : !ignoreOther && check(inter._intersection, true) - || check(inter._next); + : !ignoreOther && check(inter._intersection, null, true) + || inter._next !== prev && check(inter._next, inter); } inter = check(inter) || inter; From 4df65c1809c9630b161de9a467ffd9ca3d6f6460 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sat, 19 Sep 2015 19:07:44 +0200 Subject: [PATCH 112/280] Various improvements to code that handles multiple intersections in same location. Relates to #787 --- src/path/PathItem.Boolean.js | 146 +++++++++++++++++++---------------- 1 file changed, 78 insertions(+), 68 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index d6bcbbe8..fdb662a6 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -215,12 +215,10 @@ PathItem.inject(new function() { t = 0; // Since it's split (might be 1 also?) } // Link the new segment with the intersection on the other curve - if (segment._intersection) { - console.log('Segment already has an intersection: ' - + segment._intersection + ', ' + loc._intersection); - // Create a chain of possible intersections linked through - // _next: - var inter = segment._intersection; + var inter = segment._intersection; + if (inter) { + // Create a chain of possible intersections linked through _next + // First find the last intersection in the chain, then link it. while (inter._next) inter = inter._next; inter._next = loc; @@ -521,25 +519,82 @@ PathItem.inject(new function() { } var paths = [], - operator = operators[operation]; + operator = operators[operation], + start, + otherStart; + + // If there are multiple possible intersections, find the one + // that's either connecting back to start or is not visited yet, + // and will be part of the boolean result: + function getIntersection(inter, prev, ignoreOther) { + if (!inter) + return null; + var next = inter._segment.getNext(); + if (window.reportSegments) { + var seg = inter._segment; + console.log('Multiple' + + ', seg: ' + seg._path._id + '.' +seg._index + + ', next: ' + next._path._id + '.' + next._index + + ', visited:' + !!next._visited + + ', operator:' + (operator && operator(next._winding)) + + ', start: ' + (next === start) + + ', next: ' + (!!inter._next)); + } + return next === start || !next._visited + && (!operator || operator(next._winding)) + ? inter + // If it's no match, check the other intersection first, + // then carry on with the next linked intersection. + : !ignoreOther + && getIntersection(inter._intersection, null, true) + || inter._next != prev // Prevent circular loops + && getIntersection(inter._next, inter, false); + } + for (var i = 0, l = segments.length; i < l; i++) { var seg = segments[i], - inter = seg._intersection; - // Do not start a chain with already visited segments, intersections - // with multiple possibilities (TODO: Is that really a problem?), - // and segments that are not going to be part of the resulting - // operation. - if (seg._visited /* || inter && inter._next*/ - || operator && !operator(seg._winding)) - continue; - var path = new Path(Item.NO_INSERT), - start = seg, - otherStart = null, + path = null, added = false; // Whether a first segment as added already - do { + // Do not start a chain with already visited segments, and segments + // that are not going to be part of the resulting operation. + if (seg._visited || operator && !operator(seg._winding)) + continue; + start = otherStart = null; + while (true) { + var inter = seg._intersection; + // Once we started a chain, see if there are multiple + // intersections, and if so, pick the best one: + inter = added && getIntersection(inter) || inter; + // A switched intersection means we may have changed the segment + if (inter) + seg = inter._intersection._segment; + // Point to the other segment in the selected intersection. var other = inter && inter._segment; - if (!otherStart) + if (added && (seg === start || seg === otherStart)) { + // We've come back to the start, bail out as we're done. + drawSegment(seg, 'done', i, 'red'); + break; + } else if (seg._visited && (!other || other._visited)) { + // TODO: Do we still need to check other too? + drawSegment(seg, 'visited', i, 'red'); + break; + } else if (!inter && operator && !operator(seg._winding)) { + // Intersections are always part of the resulting path, for + // all other segments check the winding contribution to see + // if they are to be kept. If not, the chain has to end here + // TODO: We really should find a way to go backwards perhaps + // and try another path when this happens? + drawSegment(seg, 'discard', i, 'red'); + console.error('Excluded segment encountered, aborting #' + + pathCount + '.' + + (path ? path._segments.length + 1 : 1)); + break; + } + if (!added) { + path = new Path(Item.NO_INSERT); + start = seg; otherStart = other; + } var handleIn = added && seg._handleIn; if (!added || !other || other === start) { // TODO: Is (other === start) check really required? @@ -597,54 +652,9 @@ PathItem.inject(new function() { path.add(new Segment(seg._point, handleIn, seg._handleOut)); seg._visited = added = true; seg = seg.getNext(); - inter = seg && seg._intersection; - // If there are multiple possible intersections, find the one - // that's either connecting back to start or is not visited yet, - // and will be part of the boolean result: - function check(inter, prev, ignoreOther) { - if (!inter) - return null; - var seg = inter._segment, - next = seg.getNext(); - console.log('Multiple' - + ', seg: ' + seg._path._id + '.' + seg._index - + ', next: ' + next._path._id + '.' + next._index - + ', visited:' + !!next._visited - + ', operator:' + (operator && operator(next._winding)) - + ', start: ' + (next === start) - + ', next: ' + (!!inter._next)); - return next === start || !next._visited - && (!operator || operator(next._winding)) - ? inter - // If no match, check the other intersection first, - // then carry on with the next linked intersection. - : !ignoreOther && check(inter._intersection, null, true) - || inter._next !== prev && check(inter._next, inter); - } - inter = check(inter) || inter; - - other = inter && inter._segment; - if (seg === start || seg === otherStart) { - drawSegment(seg, 'done', i, 'red'); - } else if (seg._visited && (!other || other._visited)) { - drawSegment(seg, 'visited', i, 'red'); - } - if (!inter && operator && !operator(seg._winding)) { - // TODO: We really should find a way to go backwards perhaps - // and try another path when this happens? - drawSegment(seg, 'discard', i, 'red'); - console.error('Excluded segment encountered, aborting #' - + pathCount + '.' + - (path ? path._segments.length + 1 : 1)); - } - } while (seg && seg !== start && seg !== otherStart - // If we're about to switch, try to see if we can carry on - // if the other segment wasn't visited yet. - && (!seg._visited || other && !other._visited) - // Intersections are always part of the resulting path, for - // all other segments check the winding contribution to see - // if they are to be kept. If not, the chain has to end here - && (inter || !operator || operator(seg._winding))); + } + if (!path || !added) + continue; // Finish with closing the paths if necessary, correctly linking up // curves etc. if (seg === start || seg === otherStart) { From 53862233e5bc9fc800e09dcb3af0cb7173889bfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sat, 19 Sep 2015 22:47:57 +0200 Subject: [PATCH 113/280] Improve debug logging of new multiple intersections code. --- src/path/PathItem.Boolean.js | 37 +++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index fdb662a6..fe2f4599 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -217,6 +217,10 @@ PathItem.inject(new function() { // Link the new segment with the intersection on the other curve var inter = segment._intersection; if (inter) { + var other = inter._curve; + console.log('Link' + + ', seg: ' + segment._path._id + '.' + segment._index + + ', other: ' + (other && other._path._id)); // Create a chain of possible intersections linked through _next // First find the last intersection in the chain, then link it. while (inter._next) @@ -500,16 +504,20 @@ PathItem.inject(new function() { , color); } - for (var i = 0; i < (window.reportWindings ? segments.length : 0); i++) { + for (var i = 0, j = 0; + i < (window.reportWindings ? segments.length : 0); + i++, j++) { var seg = segments[i]; path = seg._path, id = path._id, point = seg.point, inter = seg._intersection; - if (!(id in pathIndices)) + if (!(id in pathIndices)) { pathIndices[id] = ++pathIndex; + j = 0; + } - labelSegment(seg, '#' + pathIndex + '.' + (i + 1) + labelSegment(seg, '#' + pathIndex + '.' + (j + 1) + ' i: ' + !!inter + ' id: ' + seg._path._id + '.' + seg._index + ' pt: ' + seg._point @@ -529,10 +537,10 @@ PathItem.inject(new function() { function getIntersection(inter, prev, ignoreOther) { if (!inter) return null; - var next = inter._segment.getNext(); + var seg = inter._segment, + next = inter._segment.getNext(); if (window.reportSegments) { - var seg = inter._segment; - console.log('Multiple' + console.log('getIntersection()' + ', seg: ' + seg._path._id + '.' +seg._index + ', next: ' + next._path._id + '.' + next._index + ', visited:' + !!next._visited @@ -540,8 +548,10 @@ PathItem.inject(new function() { + ', start: ' + (next === start) + ', next: ' + (!!inter._next)); } - return next === start || !next._visited - && (!operator || operator(next._winding)) + // If this intersections brings us back to the beginning it's + return next === start || next == otherStart + || !seg._visited && !next._visited && (!operator + || operator(seg._winding) && operator(next._winding)) ? inter // If it's no match, check the other intersection first, // then carry on with the next linked intersection. @@ -564,10 +574,19 @@ PathItem.inject(new function() { var inter = seg._intersection; // Once we started a chain, see if there are multiple // intersections, and if so, pick the best one: + if (window.reportSegments && added && inter) { + console.log('Before getIntersection(), seg: ' + + seg._path._id + '.' +seg._index); + } inter = added && getIntersection(inter) || inter; // A switched intersection means we may have changed the segment - if (inter) + if (inter) { seg = inter._intersection._segment; + if (window.reportSegments && added) { + console.log('After getIntersection(), seg: ' + + seg._path._id + '.' +seg._index); + } + } // Point to the other segment in the selected intersection. var other = inter && inter._segment; if (added && (seg === start || seg === otherStart)) { From 5db9703aff6080b7863f4c18813c6fd8ca4b66d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 20 Sep 2015 14:16:47 +0200 Subject: [PATCH 114/280] No more need to check for crossings since every intersection is now either an overlap or a crossing. --- src/path/PathItem.Boolean.js | 52 +++++++++++++++++------------------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index fe2f4599..c8d4f4f9 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -99,12 +99,12 @@ PathItem.inject(new function() { if (_path2 && /^(subtract|exclude)$/.test(operation) ^ (_path2.isClockwise() !== _path1.isClockwise())) _path2.reverse(); - // Split curves at intersections on both paths. Note that for self + // Split curves at crossings on both paths. Note that for self // intersection, _path2 will be null and getIntersections() handles it. // console.time('intersection'); - var locations = CurveLocation.expand(_path1.getCrossings(_path2)); + var crossings = CurveLocation.expand(_path1.getCrossings(_path2)); // console.timeEnd('intersection'); - splitPath(locations); + splitPath(crossings); var segments = [], // Aggregate of all curves in both operands, monotonic in y @@ -123,11 +123,11 @@ PathItem.inject(new function() { if (_path2) collect(_path2._children || [_path2]); // Propagate the winding contribution. Winding contribution of curves - // does not change between two intersections. + // does not change between two crossings. // First, propagate winding contributions for curve chains starting in - // all intersections: - for (var i = 0, l = locations.length; i < l; i++) { - propagateWinding(locations[i]._segment, _path1, _path2, monoCurves, + // all crossings: + for (var i = 0, l = crossings.length; i < l; i++) { + propagateWinding(crossings[i]._segment, _path1, _path2, monoCurves, operation); } // Now process the segments that are not part of any intersecting chains @@ -143,16 +143,14 @@ PathItem.inject(new function() { } /** - * Private method for splitting a PathItem at the given intersections. - * The routine works for both self intersections and intersections - * between PathItems. + * Private method for splitting a PathItem at the given locations. * - * @param {CurveLocation[]} intersections Array of CurveLocation objects + * @param {CurveLocation[]} locations Array of CurveLocation objects */ - function splitPath(intersections) { + function splitPath(locations) { if (window.reportIntersections) { - console.log('Intersections', intersections.length / 2); - intersections.forEach(function(inter) { + console.log('Crossings', locations.length / 2); + locations.forEach(function(inter) { if (inter._other) return; var other = inter._intersection; @@ -165,7 +163,7 @@ PathItem.inject(new function() { new Path.Circle({ center: inter.point, radius: 2 * scaleFactor, - fillColor: inter.isCrossing() ? 'red' : 'green', + fillColor: 'red', strokeScaling: false }); console.log(log.map(function(v) { @@ -183,8 +181,8 @@ PathItem.inject(new function() { prev, prevT; - for (var i = intersections.length - 1; i >= 0; i--) { - var loc = intersections[i], + for (var i = locations.length - 1; i >= 0; i--) { + var loc = locations[i], t = loc._parameter, locT = t; // Check if we are splitting same curve multiple times, but avoid @@ -560,7 +558,6 @@ PathItem.inject(new function() { || inter._next != prev // Prevent circular loops && getIntersection(inter._next, inter, false); } - for (var i = 0, l = segments.length; i < l; i++) { var seg = segments[i], path = null, @@ -638,17 +635,13 @@ PathItem.inject(new function() { } else if (operation === 'exclude') { // We need to handle exclusion separately, as we want to // switch at each crossing. - if (inter.isCrossing()) { - drawSegment(seg, 'exclude-cross', i, 'green'); - seg = other; - } else { - drawSegment(seg, 'exclude-stay', i, 'blue'); - } + drawSegment(seg, 'exclude-cross', i, 'green'); + seg = other; } else if (operator(seg._winding)) { // Do not switch to the intersecting segment as this segment // is part of the the boolean result. drawSegment(seg, 'keep', i, 'black'); - } else if (operator(other._winding) && inter.isCrossing()) { + } else if (operator(other._winding)) { // The other segment is part of the boolean result, and we // are at crossing, switch over. drawSegment(seg, 'cross', i, 'green'); @@ -786,14 +779,16 @@ PathItem.inject(new function() { }, resolveCrossings: function() { - var locations = this.getCrossings(); - if (!locations.length) + var crossings = this.getCrossings(); + if (!crossings.length) return this.reorient(); var reportSegments = window.reportSegments; + var reportWindings = window.reportWindings; var reportIntersections = window.reportIntersections; window.reportSegments = false; + window.reportWindings = false; window.reportIntersections = false; - splitPath(CurveLocation.expand(locations)); + splitPath(CurveLocation.expand(crossings)); var paths = this._children || [this], segments = []; for (var i = 0, l = paths.length; i < l; i++) { @@ -802,6 +797,7 @@ PathItem.inject(new function() { var res = finishBoolean(tracePaths(segments), this, null, false) .reorient(); window.reportSegments = reportSegments; + window.reportWindings = reportWindings; window.reportIntersections = reportIntersections; return res; } From 20ed1e007ccbfa9073fcd29da8ac915315f1ab23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 20 Sep 2015 14:17:23 +0200 Subject: [PATCH 115/280] More fixes in handling of multiple intersection locations. --- src/path/PathItem.Boolean.js | 45 ++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index c8d4f4f9..e8c27653 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -218,12 +218,12 @@ PathItem.inject(new function() { var other = inter._curve; console.log('Link' + ', seg: ' + segment._path._id + '.' + segment._index - + ', other: ' + (other && other._path._id)); + + ', other: ' + other._path._id); // Create a chain of possible intersections linked through _next // First find the last intersection in the chain, then link it. while (inter._next) inter = inter._next; - inter._next = loc; + inter._next = loc._intersection; } else { segment._intersection = loc._intersection; } @@ -426,6 +426,7 @@ PathItem.inject(new function() { // differently based on the type of operation, and adjust the // winding number accordingly: if (inter && inter._overlap) { + // Preserve original winding contribution for the overlap switch (operation) { case 'unite': if (wind === 1) @@ -496,8 +497,9 @@ PathItem.inject(new function() { + ' v: ' + (seg._visited ? 1 : 0) + ' p: ' + seg._point + ' op: ' + (operator && operator(seg._winding)) - + ' ov: ' + (inter && inter._overlap || 0) + + ' ov: ' + !!(inter && inter._overlap) + ' wi: ' + seg._winding + + ' ow: ' + seg._originalWinding + ' mu: ' + !!(inter && inter._next) , color); } @@ -515,13 +517,17 @@ PathItem.inject(new function() { j = 0; } + var ix = inter && inter._segment; + var nx = inter && inter._next && inter._next._segment; labelSegment(seg, '#' + pathIndex + '.' + (j + 1) - + ' i: ' + !!inter + ' id: ' + seg._path._id + '.' + seg._index + + ' ix: ' + (ix && ix._path._id + '.' + ix._index || '--') + + ' nx: ' + (nx && nx._path._id + '.' + nx._index || '--') + ' pt: ' + seg._point - + ' ov: ' + (inter && inter._overlap || 0) + + ' ov: ' + !!(inter && inter._overlap) + ' wi: ' + seg._winding - , 'green'); + + ' ow: ' + seg._originalWinding + , path.strokeColor || path.fillColor || 'black'); } var paths = [], @@ -536,25 +542,34 @@ PathItem.inject(new function() { if (!inter) return null; var seg = inter._segment, - next = inter._segment.getNext(); + next = seg.getNext(); if (window.reportSegments) { console.log('getIntersection()' + ', seg: ' + seg._path._id + '.' +seg._index + ', next: ' + next._path._id + '.' + next._index - + ', visited:' + !!next._visited - + ', operator:' + (operator && operator(next._winding)) - + ', start: ' + (next === start) + + ', seg vis:' + !!seg._visited + + ', next vis:' + !!next._visited + + ', next start:' + (next === start + || next === otherStart) + + ', seg op:' + (operator && operator(seg._originalWinding)) + + ', next op:' + (operator && operator(next._winding)) + ', next: ' + (!!inter._next)); } - // If this intersections brings us back to the beginning it's - return next === start || next == otherStart - || !seg._visited && !next._visited && (!operator - || operator(seg._winding) && operator(next._winding)) + // See if this segment and next are both not visited yet, or are + // bringing us back to the beginning, and are both part of the + // boolean result. + return !seg._visited && (!next._visited + || next === start || next === otherStart) + && (!operator + || operator(seg._originalWinding) && operator(next._winding)) ? inter // If it's no match, check the other intersection first, // then carry on with the next linked intersection. : !ignoreOther - && getIntersection(inter._intersection, null, true) + // We need to get the intersection on the segment, + // not on inter, since they're only linked up + // through _next there! + && getIntersection(seg._intersection, null, true) || inter._next != prev // Prevent circular loops && getIntersection(inter._next, inter, false); } From 946157f1b1fc796c7dc2fedda38e592e76e424be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 20 Sep 2015 14:19:14 +0200 Subject: [PATCH 116/280] Add note about usage of _originalWinding. --- src/path/PathItem.Boolean.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index e8c27653..0f222978 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -551,7 +551,8 @@ PathItem.inject(new function() { + ', next vis:' + !!next._visited + ', next start:' + (next === start || next === otherStart) - + ', seg op:' + (operator && operator(seg._originalWinding)) + + ', seg op:' + (operator + && operator(seg._originalWinding)) + ', next op:' + (operator && operator(next._winding)) + ', next: ' + (!!inter._next)); } @@ -561,7 +562,11 @@ PathItem.inject(new function() { return !seg._visited && (!next._visited || next === start || next === otherStart) && (!operator - || operator(seg._originalWinding) && operator(next._winding)) + // NOTE: We need to use _originalWinding here since an + // overlap crossing might have brought us here, in which + // case operator(seg._winding) might be false. + || operator(seg._originalWinding) + && operator(next._winding)) ? inter // If it's no match, check the other intersection first, // then carry on with the next linked intersection. From 738cc4c214d8c565c94371e69707283bac4d14b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 20 Sep 2015 15:29:54 +0200 Subject: [PATCH 117/280] Yet another improvement in multiple interseections boolean code. This appears to be the one. The only remaining failing cases seem to be linked to getting the same intersection twice now! Relates to #787 --- src/path/PathItem.Boolean.js | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 0f222978..367340f8 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -573,9 +573,13 @@ PathItem.inject(new function() { : !ignoreOther // We need to get the intersection on the segment, // not on inter, since they're only linked up - // through _next there! - && getIntersection(seg._intersection, null, true) - || inter._next != prev // Prevent circular loops + // through _next there. But do not check that + // intersection in the first call to + // getIntersection() (prev == null), since we'd go + // back to the originating segment. + && (prev || seg._intersection !== inter._intersection) + && getIntersection(seg._intersection, inter, true) + || inter._next !== prev // Prevent circular loops && getIntersection(inter._next, inter, false); } for (var i = 0, l = segments.length; i < l; i++) { @@ -591,20 +595,19 @@ PathItem.inject(new function() { var inter = seg._intersection; // Once we started a chain, see if there are multiple // intersections, and if so, pick the best one: - if (window.reportSegments && added && inter) { + if (inter && added && window.reportSegments) { console.log('Before getIntersection(), seg: ' - + seg._path._id + '.' +seg._index); + + inter._segment._path._id + '.' + + inter._segment._index); } inter = added && getIntersection(inter) || inter; // A switched intersection means we may have changed the segment - if (inter) { - seg = inter._intersection._segment; - if (window.reportSegments && added) { - console.log('After getIntersection(), seg: ' - + seg._path._id + '.' +seg._index); - } - } // Point to the other segment in the selected intersection. + if (inter && added && window.reportSegments) { + console.log('After getIntersection(), seg: ' + + inter._segment._path._id + '.' + + inter._segment._index); + } var other = inter && inter._segment; if (added && (seg === start || seg === otherStart)) { // We've come back to the start, bail out as we're done. From 530b8b7bc8d1a10710143ce4ce3d20f9133f54dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 20 Sep 2015 15:50:26 +0200 Subject: [PATCH 118/280] Handle adjusted overlap winding contribution and operator calls through new isValid() function. --- src/path/PathItem.Boolean.js | 105 ++++++++++++++++------------------- 1 file changed, 47 insertions(+), 58 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 367340f8..cdb31f1d 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -418,29 +418,8 @@ PathItem.inject(new function() { } // Assign the average winding to the entire curve chain. var winding = Math.round(windingSum / 3); - for (var j = chain.length - 1; j >= 0; j--) { - var seg = chain[j].segment, - inter = seg._intersection, - wind = winding; - // We need to handle the edge cases of overlapping curves - // differently based on the type of operation, and adjust the - // winding number accordingly: - if (inter && inter._overlap) { - // Preserve original winding contribution for the overlap - switch (operation) { - case 'unite': - if (wind === 1) - wind = 2; - break; - case 'intersect': - if (wind === 2) - wind = 1; - break; - } - } - seg._originalWinding = winding; - seg._winding = wind; - } + for (var j = chain.length - 1; j >= 0; j--) + chain[j].segment._winding = winding; } /** @@ -496,10 +475,9 @@ PathItem.inject(new function() { + ' id: ' + seg._path._id + '.' + seg._index + ' v: ' + (seg._visited ? 1 : 0) + ' p: ' + seg._point - + ' op: ' + (operator && operator(seg._winding)) + + ' op: ' + isValid(seg) + ' ov: ' + !!(inter && inter._overlap) + ' wi: ' + seg._winding - + ' ow: ' + seg._originalWinding + ' mu: ' + !!(inter && inter._next) , color); } @@ -526,14 +504,28 @@ PathItem.inject(new function() { + ' pt: ' + seg._point + ' ov: ' + !!(inter && inter._overlap) + ' wi: ' + seg._winding - + ' ow: ' + seg._originalWinding , path.strokeColor || path.fillColor || 'black'); } var paths = [], - operator = operators[operation], start, - otherStart; + otherStart, + operator = operators[operation], + // Adjust winding contributions for specific operations on overlaps: + overlapWinding = { + unite: { 1: 2 }, + intersect: { 2: 1 } + }[operation]; + + function isValid(seg, unadjusted) { + if (!operator) // For self-intersection, we're always valid! + return true; + var winding = seg._winding, + inter = seg._intersection; + if (inter && !unadjusted && overlapWinding && inter._overlap) + winding = overlapWinding[winding] || winding; + return operator(winding); + } // If there are multiple possible intersections, find the one // that's either connecting back to start or is not visited yet, @@ -551,36 +543,33 @@ PathItem.inject(new function() { + ', next vis:' + !!next._visited + ', next start:' + (next === start || next === otherStart) - + ', seg op:' + (operator - && operator(seg._originalWinding)) - + ', next op:' + (operator && operator(next._winding)) + + ', seg op:' + isValid(seg, true) + + ', next op:' + isValid(next) + ', next: ' + (!!inter._next)); } // See if this segment and next are both not visited yet, or are // bringing us back to the beginning, and are both part of the // boolean result. return !seg._visited && (!next._visited - || next === start || next === otherStart) - && (!operator - // NOTE: We need to use _originalWinding here since an - // overlap crossing might have brought us here, in which - // case operator(seg._winding) might be false. - || operator(seg._originalWinding) - && operator(next._winding)) - ? inter - // If it's no match, check the other intersection first, - // then carry on with the next linked intersection. - : !ignoreOther - // We need to get the intersection on the segment, - // not on inter, since they're only linked up - // through _next there. But do not check that - // intersection in the first call to - // getIntersection() (prev == null), since we'd go - // back to the originating segment. - && (prev || seg._intersection !== inter._intersection) - && getIntersection(seg._intersection, inter, true) - || inter._next !== prev // Prevent circular loops - && getIntersection(inter._next, inter, false); + || next === start || next === otherStart) + && (!operator // Self-intersection doesn't need isValid() calls + // NOTE: We need to use the unadjusted winding here since an + // overlap crossing might have brought us here, in which + // case isValid(seg, false) might be false. + || isValid(seg, true) && isValid(next)) + ? inter + // If it's no match, check the other intersection first, then + // carry on with the next linked intersection. + : !ignoreOther + // We need to get the intersection on the segment, not + // on inter, since they're only linked up through _next + // there. But do not check that intersection in the + // first call to getIntersection() (prev == null), since + // we'd go back to the originating segment. + && (prev || seg._intersection !== inter._intersection) + && getIntersection(seg._intersection, inter, true) + || inter._next !== prev // Prevent circular loops + && getIntersection(inter._next, inter, false); } for (var i = 0, l = segments.length; i < l; i++) { var seg = segments[i], @@ -588,7 +577,7 @@ PathItem.inject(new function() { added = false; // Whether a first segment as added already // Do not start a chain with already visited segments, and segments // that are not going to be part of the resulting operation. - if (seg._visited || operator && !operator(seg._winding)) + if (seg._visited || !isValid(seg)) continue; start = otherStart = null; while (true) { @@ -617,7 +606,7 @@ PathItem.inject(new function() { // TODO: Do we still need to check other too? drawSegment(seg, 'visited', i, 'red'); break; - } else if (!inter && operator && !operator(seg._winding)) { + } else if (!inter && !isValid(seg)) { // Intersections are always part of the resulting path, for // all other segments check the winding contribution to see // if they are to be kept. If not, the chain has to end here @@ -648,8 +637,8 @@ PathItem.inject(new function() { seg = other; } else if (inter._overlap && operation !== 'intersect') { // Switch to the overlapping intersecting segment if it is - // part of the boolean result. - if (operator(other._originalWinding)) { + // part of the boolean result. Do not adjust for overlap! + if (isValid(other, true)) { drawSegment(seg, 'overlap-cross', i, 'orange'); seg = other; } else { @@ -660,11 +649,11 @@ PathItem.inject(new function() { // switch at each crossing. drawSegment(seg, 'exclude-cross', i, 'green'); seg = other; - } else if (operator(seg._winding)) { + } else if (isValid(seg)) { // Do not switch to the intersecting segment as this segment // is part of the the boolean result. drawSegment(seg, 'keep', i, 'black'); - } else if (operator(other._winding)) { + } else if (isValid(other)) { // The other segment is part of the boolean result, and we // are at crossing, switch over. drawSegment(seg, 'cross', i, 'green'); From b68be09c879d1eb6ec8559198cc6f9825dd2422c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 20 Sep 2015 22:39:28 +0200 Subject: [PATCH 119/280] Fix all accidental non-breaking spaces. --- src/path/PathItem.Boolean.js | 10 +++++----- src/path/PathItem.js | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index cdb31f1d..d1fc352b 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -504,7 +504,7 @@ PathItem.inject(new function() { + ' pt: ' + seg._point + ' ov: ' + !!(inter && inter._overlap) + ' wi: ' + seg._winding - , path.strokeColor || path.fillColor || 'black'); + , path.strokeColor || path.fillColor || 'black'); } var paths = [], @@ -542,7 +542,7 @@ PathItem.inject(new function() { + ', seg vis:' + !!seg._visited + ', next vis:' + !!next._visited + ', next start:' + (next === start - || next === otherStart) + || next === otherStart) + ', seg op:' + isValid(seg, true) + ', next op:' + isValid(next) + ', next: ' + (!!inter._next)); @@ -551,7 +551,7 @@ PathItem.inject(new function() { // bringing us back to the beginning, and are both part of the // boolean result. return !seg._visited && (!next._visited - || next === start || next === otherStart) + || next === start || next === otherStart) && (!operator // Self-intersection doesn't need isValid() calls // NOTE: We need to use the unadjusted winding here since an // overlap crossing might have brought us here, in which @@ -677,7 +677,7 @@ PathItem.inject(new function() { seg._visited = added = true; seg = seg.getNext(); } - if (!path || !added) + if (!path || !added) continue; // Finish with closing the paths if necessary, correctly linking up // curves etc. @@ -801,7 +801,7 @@ PathItem.inject(new function() { window.reportWindings = false; window.reportIntersections = false; splitPath(CurveLocation.expand(crossings)); - var paths = this._children || [this], + var paths = this._children || [this], segments = []; for (var i = 0, l = paths.length; i < l; i++) { segments.push.apply(segments, paths[i]._segments); diff --git a/src/path/PathItem.js b/src/path/PathItem.js index 0e4fa9ae..82ad0c7d 100644 --- a/src/path/PathItem.js +++ b/src/path/PathItem.js @@ -145,7 +145,7 @@ var PathItem = Item.extend(/** @lends PathItem# */{ var locations = this.getIntersections(path); for (var i = locations.length - 1; i >= 0; i--) { // TODO: An overlap could be either a crossing or a tangent! - if (!locations[i].isCrossing() && !locations[i]._overlap) + if (!locations[i].isCrossing() && !locations[i]._overlap) locations.splice(i, 1); } return locations; From 1302df0cb8d4459102e899fc60c032506acb9f95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 20 Sep 2015 23:22:31 +0200 Subject: [PATCH 120/280] Fix variable leackage. --- src/path/CurveLocation.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index f9d75563..cdbf1b1b 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -415,8 +415,8 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ var m = (l + r) >>> 1, loc2 = locations[m], curve2 = loc2._curve, - path2 = curve2._path; - diff = path1 === path2 + path2 = curve2._path, + diff = path1 === path2 ? curve1.getIndex() + loc._parameter - curve2.getIndex() - loc2._parameter // Sort by path id to group all locs on same path. From f47af12b0dbf154609939658dee524921477e837 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 20 Sep 2015 23:22:41 +0200 Subject: [PATCH 121/280] Shorten code statement. --- src/basic/Line.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/basic/Line.js b/src/basic/Line.js index a31b2f28..1677779a 100644 --- a/src/basic/Line.js +++ b/src/basic/Line.js @@ -155,9 +155,7 @@ var Line = Base.extend(/** @lends Line# */{ ccw = v2x * vx + v2y * vy; // ccw = v2.dot(v1); if (ccw > 0) { // ccw = v2.subtract(v1).dot(v1); - v2x -= vx; - v2y -= vy; - ccw = v2x * vx + v2y * vy; + ccw = (v2x - vx) * vx + (v2y - vy) * vy; if (ccw < 0) ccw = 0; } From 1ad95c9020e2381dc2df831621c12faf8abaae8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Mon, 21 Sep 2015 07:06:41 -0400 Subject: [PATCH 122/280] Improve CurveLocation.add() to always merge duplicates. We nee to check neighbors of the found location too. --- src/path/CurveLocation.js | 62 +++++++++++++++++++++++++++------------ 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index cdbf1b1b..34b877ec 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -407,31 +407,56 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ // NOTE: We don't call getCurve() / getParameter() here, since this // code is used internally in boolean operations where all this // information remains valid during processing. - var l = 0, - r = locations.length - 1, - curve1 = loc._curve, - path1 = curve1._path; + var length = locations.length, + l = 0, + r = length - 1, + epsilon = /*#=*/Numerical.CURVETIME_EPSILON, + abs = Math.abs; + + function compare(loc1, loc2) { + var curve1 = loc1._curve, + curve2 = loc2._curve, + path1 = curve1._path, + path2 = curve2._path; + return path1 === path2 + ? curve1.getIndex() + loc1._parameter + - curve2.getIndex() - loc2._parameter + // Sort by path id to group all locs on same path. + : path1._id - path2._id; + } + + function search(start, dir) { + for (var i = start + dir; i >= 0 && i < length; i += dir) { + var loc2 = locations[i]; + if (abs(compare(loc, loc2)) >= epsilon) + return null; + if (loc.equals(loc2)) + return loc2; + } + } + while (l <= r) { var m = (l + r) >>> 1, loc2 = locations[m], - curve2 = loc2._curve, - path2 = curve2._path, - diff = path1 === path2 - ? curve1.getIndex() + loc._parameter - - curve2.getIndex() - loc2._parameter - // Sort by path id to group all locs on same path. - : path1._id - path2._id; + diff = compare(loc, loc2); // Only compare location with equals() if diff is small enough // NOTE: equals() takes the intersection location into account, // while the above calculation of diff doesn't! - if (merge && Math.abs(diff) < /*#=*/Numerical.CURVETIME_EPSILON - && loc.equals(loc2)) { - // Carry over overlap setting! - if (loc._overlap) { - loc2._overlap = loc2._intersection._overlap = true; + if (merge && abs(diff) < epsilon) { + // See if the two locations are actually the same, and merge + // if they are. If they aren't, we're not done yet since + // all neighbors with a diff < epsilon are potential merge + // candidates, so check them too. + if (loc2 = loc.equals(loc2) ? loc2 + : search(m, -1) || search(m, 1)) { + // Carry over overlap setting! + if (loc._overlap) { + loc2._overlap = loc2._intersection._overlap = true; + } + // We're done, don't insert, merge with the found + // location instead: + return loc2; } - // We're done, don't insert, merge with loc2 instead - return loc2; } if (diff < 0) { r = m - 1; @@ -439,6 +464,7 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ l = m + 1; } } + // We didn't merge with a preexisting location, insert it now. locations.splice(l, 0, loc); return loc; }, From 40570b3e59851640fe1ad74dc11d58cb0e452c4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Mon, 21 Sep 2015 09:42:47 -0400 Subject: [PATCH 123/280] Furher improve boolean debug logging and drawing. --- src/path/PathItem.Boolean.js | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index d1fc352b..8ac382f9 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -71,7 +71,7 @@ PathItem.inject(new function() { return result; } - var scaleFactor = 1.0; // 1 / 3000; + var scaleFactor = 0.25; // 1 / 2000; var textAngle = 33; var fontSize = 5; @@ -440,7 +440,8 @@ PathItem.inject(new function() { function labelSegment(seg, text, color) { var point = seg.point; - var key = Math.round(point.x / scaleFactor) + ',' + Math.round(point.y / scaleFactor); + var key = Math.round(point.x / (4 * scaleFactor)) + + ',' + Math.round(point.y / (4 * scaleFactor)); var offset = segmentOffset[key] || 0; segmentOffset[key] = offset + 1; var size = fontSize * scaleFactor; @@ -459,7 +460,7 @@ PathItem.inject(new function() { text.rotate(textAngle); } - function drawSegment(seg, text, index, color) { + function drawSegment(seg, other, text, index, color) { if (!window.reportSegments) return; new Path.Circle({ @@ -473,6 +474,7 @@ PathItem.inject(new function() { + (path ? path._segments.length + 1 : 1) + ' (' + (index + 1) + '): ' + text + ' id: ' + seg._path._id + '.' + seg._index + + (other ? ' -> ' + other._path._id + '.' + other._index : '') + ' v: ' + (seg._visited ? 1 : 0) + ' p: ' + seg._point + ' op: ' + isValid(seg) @@ -590,6 +592,7 @@ PathItem.inject(new function() { + inter._segment._index); } inter = added && getIntersection(inter) || inter; + var other = inter && inter._segment; // A switched intersection means we may have changed the segment // Point to the other segment in the selected intersection. if (inter && added && window.reportSegments) { @@ -597,14 +600,13 @@ PathItem.inject(new function() { + inter._segment._path._id + '.' + inter._segment._index); } - var other = inter && inter._segment; if (added && (seg === start || seg === otherStart)) { // We've come back to the start, bail out as we're done. - drawSegment(seg, 'done', i, 'red'); + drawSegment(seg, null, 'done', i, 'red'); break; } else if (seg._visited && (!other || other._visited)) { // TODO: Do we still need to check other too? - drawSegment(seg, 'visited', i, 'red'); + drawSegment(seg, null, 'visited', i, 'red'); break; } else if (!inter && !isValid(seg)) { // Intersections are always part of the resulting path, for @@ -612,7 +614,7 @@ PathItem.inject(new function() { // if they are to be kept. If not, the chain has to end here // TODO: We really should find a way to go backwards perhaps // and try another path when this happens? - drawSegment(seg, 'discard', i, 'red'); + drawSegment(seg, null, 'discard', i, 'red'); console.error('Excluded segment encountered, aborting #' + pathCount + '.' + (path ? path._segments.length + 1 : 1)); @@ -629,9 +631,9 @@ PathItem.inject(new function() { // Does that ever occur? // Just add the first segment and all segments that have no // intersection. - drawSegment(seg, 'add', i, 'black'); + drawSegment(seg, null, 'add', i, 'black'); } else if (!operator) { // Resolve self-intersections - drawSegment(seg, 'self-int', i, 'purple'); + drawSegment(seg, other, 'self-int', i, 'purple'); // Switch to the intersecting segment, as we need to // resolving self-Intersections. seg = other; @@ -639,28 +641,28 @@ PathItem.inject(new function() { // Switch to the overlapping intersecting segment if it is // part of the boolean result. Do not adjust for overlap! if (isValid(other, true)) { - drawSegment(seg, 'overlap-cross', i, 'orange'); + drawSegment(seg, other, 'overlap-cross', i, 'orange'); seg = other; } else { - drawSegment(seg, 'overlap-stay', i, 'orange'); + drawSegment(seg, null, 'overlap-stay', i, 'orange'); } } else if (operation === 'exclude') { // We need to handle exclusion separately, as we want to // switch at each crossing. - drawSegment(seg, 'exclude-cross', i, 'green'); + drawSegment(seg, other, 'exclude-cross', i, 'green'); seg = other; } else if (isValid(seg)) { // Do not switch to the intersecting segment as this segment // is part of the the boolean result. - drawSegment(seg, 'keep', i, 'black'); + drawSegment(seg, null, 'keep', i, 'black'); } else if (isValid(other)) { // The other segment is part of the boolean result, and we // are at crossing, switch over. - drawSegment(seg, 'cross', i, 'green'); + drawSegment(seg, other, 'cross', i, 'green'); seg = other; } else { // Keep on truckin' - drawSegment(seg, 'stay', i, 'blue'); + drawSegment(seg, null, 'stay', i, 'blue'); } if (seg._visited) { // We didn't manage to switch, so stop right here. From 19a17b29187c11950e63711deabff34284100b6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Mon, 21 Sep 2015 09:43:19 -0400 Subject: [PATCH 124/280] Prevent infinite loops through circular references of multiple intersections. --- src/path/PathItem.Boolean.js | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 8ac382f9..2fcc995a 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -215,15 +215,21 @@ PathItem.inject(new function() { // Link the new segment with the intersection on the other curve var inter = segment._intersection; if (inter) { - var other = inter._curve; - console.log('Link' - + ', seg: ' + segment._path._id + '.' + segment._index - + ', other: ' + other._path._id); - // Create a chain of possible intersections linked through _next - // First find the last intersection in the chain, then link it. - while (inter._next) - inter = inter._next; - inter._next = loc._intersection; + var other = inter._intersection, + next = loc._next; + while (next && next !== other) { + next = next._next; + } + if (!next) { + console.log('Link' + + ', seg: ' + segment._path._id + '.' + segment._index + + ', other: ' + inter._curve._path._id); + // Create a chain of possible intersections linked through _next + // First find the last intersection in the chain, then link it. + while (inter._next) + inter = inter._next; + inter._next = loc._intersection; + } } else { segment._intersection = loc._intersection; } From f6f42fe09b7995450b1694494cf65b5a79203467 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Mon, 21 Sep 2015 09:44:17 -0400 Subject: [PATCH 125/280] Improve handling of overlap ambiguity in getIntersection() Two passes are needed, first strict, then non-strict, to prevent 'better' next candiates over 'worse' ones. --- src/path/PathItem.Boolean.js | 58 +++++++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 17 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 2fcc995a..8bb3beb4 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -538,46 +538,63 @@ PathItem.inject(new function() { // If there are multiple possible intersections, find the one // that's either connecting back to start or is not visited yet, // and will be part of the boolean result: - function getIntersection(inter, prev, ignoreOther) { + function getIntersection(strict, inter, prev, ignoreOther) { if (!inter) return null; var seg = inter._segment, next = seg.getNext(); if (window.reportSegments) { - console.log('getIntersection()' + console.log('getIntersection(' + strict + ')' + ', seg: ' + seg._path._id + '.' +seg._index + ', next: ' + next._path._id + '.' + next._index + ', seg vis:' + !!seg._visited + ', next vis:' + !!next._visited + ', next start:' + (next === start || next === otherStart) + + ', seg wi:' + seg._winding + + ', next wi:' + next._winding + ', seg op:' + isValid(seg, true) + ', next op:' + isValid(next) - + ', next: ' + (!!inter._next)); + + ', seg ov: ' + (seg._intersection + && seg._intersection._overlap) + + ', next ov: ' + (next._intersection + && next._intersection._overlap) + + ', more: ' + (!!inter._next)); } // See if this segment and next are both not visited yet, or are // bringing us back to the beginning, and are both part of the // boolean result. + // Handling overlaps correctly here is a bit tricky business, and + // requires two passes, first with `strict = true`, then `false`: + // In strict mode, the current segment and the next segment are both + // checked for validity, and only the current one is allowed to be + // an overlap (passing true for `unadjusted` in isValid()). If this + // pass does not yield a result, the non-strict mode is used, in + // which invalid current segments are tolerated, and overlaps for + // the next segment are allowed as long as they are valid when not + // adjusted. return !seg._visited && (!next._visited || next === start || next === otherStart) && (!operator // Self-intersection doesn't need isValid() calls // NOTE: We need to use the unadjusted winding here since an // overlap crossing might have brought us here, in which // case isValid(seg, false) might be false. - || isValid(seg, true) && isValid(next)) + || (!strict || isValid(seg, true)) + && isValid(next, !strict && inter._overlap)) ? inter // If it's no match, check the other intersection first, then // carry on with the next linked intersection. : !ignoreOther // We need to get the intersection on the segment, not - // on inter, since they're only linked up through _next - // there. But do not check that intersection in the - // first call to getIntersection() (prev == null), since - // we'd go back to the originating segment. + // on inter, since multiple solutions are only linked up + // as a chain through _next there. But do not check that + // intersection in the first call to getIntersection() + // (prev == null), since we'd go straight back to the + // originating segment. && (prev || seg._intersection !== inter._intersection) - && getIntersection(seg._intersection, inter, true) + && getIntersection(strict, seg._intersection, inter, true) || inter._next !== prev // Prevent circular loops - && getIntersection(inter._next, inter, false); + && getIntersection(strict, inter._next, inter, false); } for (var i = 0, l = segments.length; i < l; i++) { var seg = segments[i], @@ -593,18 +610,25 @@ PathItem.inject(new function() { // Once we started a chain, see if there are multiple // intersections, and if so, pick the best one: if (inter && added && window.reportSegments) { - console.log('Before getIntersection(), seg: ' - + inter._segment._path._id + '.' - + inter._segment._index); + console.log('-----\n' + +'#' + pathCount + '.' + + (path ? path._segments.length + 1 : 1) + + ', Before getIntersection()' + + ', seg: ' + seg._path._id + '.' + seg._index + + ', other: ' + inter._segment._path._id + '.' + + inter._segment._index); } - inter = added && getIntersection(inter) || inter; + inter = added && (getIntersection(true, inter) + || getIntersection(false, inter)) || inter; var other = inter && inter._segment; // A switched intersection means we may have changed the segment // Point to the other segment in the selected intersection. if (inter && added && window.reportSegments) { - console.log('After getIntersection(), seg: ' - + inter._segment._path._id + '.' - + inter._segment._index); + console.log('After getIntersection()' + + ', seg: ' + + seg._path._id + '.' + seg._index + + ', other: ' + inter._segment._path._id + '.' + + inter._segment._index); } if (added && (seg === start || seg === otherStart)) { // We've come back to the start, bail out as we're done. From e36319b71a15b51826c8e669279b7a56db997672 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Mon, 21 Sep 2015 10:41:59 -0400 Subject: [PATCH 126/280] Give PathItem#getIntersections() a way to filter found intersections right away. And use it in #getCrossings() --- src/item/Item.js | 2 +- src/path/Curve.js | 14 +++++--------- src/path/PathItem.js | 20 ++++++++++++-------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/item/Item.js b/src/item/Item.js index d380ebbf..ab0eb38e 100644 --- a/src/item/Item.js +++ b/src/item/Item.js @@ -1686,7 +1686,7 @@ var Item = Base.extend(Emitter, /** @lends Item# */{ return false; // Tell getIntersections() to return as soon as some intersections are // found, because all we care for here is there are some or none: - return this._asPathItem().getIntersections(item._asPathItem(), + return this._asPathItem().getIntersections(item._asPathItem(), null, _matrix || item._matrix, true).length > 0; }, diff --git a/src/path/Curve.js b/src/path/Curve.js index f9d26c50..0e0e01e1 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1352,17 +1352,13 @@ new function() { // Scope for intersection using bezier fat-line clipping t1 = res[0]; t2 = res[1]; } - /* - var d1 = p1 ? p1.getDistance(Curve.getPoint(v1, t1)) : 0, - d2 = p2 ? p2.getDistance(Curve.getPoint(v2, t2)) : 0; - if (!Numerical.isZero(d1) || !Numerical.isZero(d2)) - debugger; - */ - CurveLocation.add(locations, - new CurveLocation(c1, t1, p1 || Curve.getPoint(v1, t1), + var loc = new CurveLocation(c1, t1, p1 || Curve.getPoint(v1, t1), null, overlap, new CurveLocation(c2, t2, p2 || Curve.getPoint(v2, t2), - null, overlap)), true); + null, overlap)), + include = param.include; + if (!include || include(loc)) + CurveLocation.add(locations, loc, true); } } diff --git a/src/path/PathItem.js b/src/path/PathItem.js index 82ad0c7d..277d38fb 100644 --- a/src/path/PathItem.js +++ b/src/path/PathItem.js @@ -32,6 +32,9 @@ var PathItem = Item.extend(/** @lends PathItem# */{ * supported. * * @param {PathItem} path the other item to find the intersections with + * @param {Function} [include] a callback function that can be used to + * filter out undesired locations right while they are collected. + * When defined, it shall return {@true to include a location}. * @return {CurveLocation[]} the locations of all intersection between the * paths * @example {@paperscript} // Finding the intersections between two paths @@ -57,7 +60,7 @@ var PathItem = Item.extend(/** @lends PathItem# */{ * } * } */ - getIntersections: function(path, _matrix, _returnFirst) { + getIntersections: function(path, include, _matrix, _returnFirst) { // NOTE: For self-intersection, path is null. This means you can also // just call path.getIntersections() without an argument to get self // intersections. @@ -103,6 +106,7 @@ var PathItem = Item.extend(/** @lends PathItem# */{ var parts = Curve.subdivide(values1, 0.5); Curve.getIntersections(parts[0], parts[1], curve1, curve1, locations, { + include: include, // Only possible if there is only one closed curve: startConnected: length1 === 1 && p1.equals(p2), // After splitting, the end is always connected: @@ -129,12 +133,15 @@ var PathItem = Item.extend(/** @lends PathItem# */{ Curve.getIntersections( values1, values2[j], curve1, curve2, locations, self ? { + include: include, // Do not compare indices here to determine connection, // since one array of curves can contain curves from // separate sup-paths of a compound path. startConnected: curve1.getPrevious() === curve2, endConnected: curve1.getNext() === curve2 - } : {} + } : { + include: include + } ); } } @@ -142,13 +149,10 @@ var PathItem = Item.extend(/** @lends PathItem# */{ }, getCrossings: function(path) { - var locations = this.getIntersections(path); - for (var i = locations.length - 1; i >= 0; i--) { + return this.getIntersections(path, function(inter) { // TODO: An overlap could be either a crossing or a tangent! - if (!locations[i].isCrossing() && !locations[i]._overlap) - locations.splice(i, 1); - } - return locations; + return inter.isCrossing() || inter._overlap; + }); }, _asPathItem: function() { From 812ac63e60ae8c19607a0080295896bf44902a1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Mon, 21 Sep 2015 10:53:53 -0400 Subject: [PATCH 127/280] Compare points instead of curve time paramters for better precision and reliability. --- src/path/CurveLocation.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index 34b877ec..b66602b7 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -371,8 +371,8 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ || loc instanceof CurveLocation // Call getCurve() and getParameter() to keep in sync && this.getCurve() === loc.getCurve() - && Math.abs(this.getParameter() - loc.getParameter()) - < /*#=*/Numerical.CURVETIME_EPSILON + && this.getPoint().isClose(loc.getPoint(), + /*#=*/Numerical.GEOMETRIC_EPSILON) && (_ignoreOther || (!this._intersection && !loc._intersection || this._intersection && this._intersection.equals( From ce95043e99c98444c8e7426b04ad035af48fe9fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Mon, 21 Sep 2015 10:54:17 -0400 Subject: [PATCH 128/280] Lower geometric epsilon. Differences slightly above 1e-8 where observed. --- src/util/Numerical.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/Numerical.js b/src/util/Numerical.js index 30f28ec5..d1853cd5 100644 --- a/src/util/Numerical.js +++ b/src/util/Numerical.js @@ -93,7 +93,7 @@ var Numerical = new function() { * collinearity. This value is somewhat arbitrary and was chosen by * trial and error. */ - GEOMETRIC_EPSILON: 1e-8, + GEOMETRIC_EPSILON: 1e-7, /** * MACHINE_EPSILON for a double precision (Javascript Number) is * 2.220446049250313e-16. (try this in the js console) From 0f61ce896a6a52fd106df6a725a4f8afa52ddaa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Mon, 21 Sep 2015 10:54:33 -0400 Subject: [PATCH 129/280] Some code clean-up. --- src/path/PathItem.Boolean.js | 14 +++++++++----- src/project/Symbol.js | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 8bb3beb4..000c959b 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -71,7 +71,7 @@ PathItem.inject(new function() { return result; } - var scaleFactor = 0.25; // 1 / 2000; + var scaleFactor = 1 / 10000; var textAngle = 33; var fontSize = 5; @@ -163,7 +163,7 @@ PathItem.inject(new function() { new Path.Circle({ center: inter.point, radius: 2 * scaleFactor, - fillColor: 'red', + strokeColor: 'red', strokeScaling: false }); console.log(log.map(function(v) { @@ -823,15 +823,19 @@ PathItem.inject(new function() { }, resolveCrossings: function() { - var crossings = this.getCrossings(); - if (!crossings.length) - return this.reorient(); var reportSegments = window.reportSegments; var reportWindings = window.reportWindings; var reportIntersections = window.reportIntersections; window.reportSegments = false; window.reportWindings = false; window.reportIntersections = false; + var crossings = this.getCrossings(); + if (!crossings.length) { + window.reportSegments = reportSegments; + window.reportWindings = reportWindings; + window.reportIntersections = reportIntersections; + return this.reorient(); + } splitPath(CurveLocation.expand(crossings)); var paths = this._children || [this], segments = []; diff --git a/src/project/Symbol.js b/src/project/Symbol.js index 6e8d25f7..736b53df 100644 --- a/src/project/Symbol.js +++ b/src/project/Symbol.js @@ -133,7 +133,7 @@ var Symbol = Base.extend(/** @lends Symbol# */{ /** * Places in instance of the symbol in the project. * - * @param [position] The position of the placed symbol + * @param {Point} [position] the position of the placed symbol * @return {PlacedSymbol} */ place: function(position) { From 84bcc537e1ee8838d3bbda360b43120320890a59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Mon, 21 Sep 2015 12:13:53 -0400 Subject: [PATCH 130/280] Simplify addCurveLineIntersections() and exclude end points. --- src/basic/Point.js | 8 ++++---- src/path/Curve.js | 32 +++++++++++++++----------------- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/src/basic/Point.js b/src/basic/Point.js index e2cd6b43..e2dfa881 100644 --- a/src/basic/Point.js +++ b/src/basic/Point.js @@ -460,11 +460,11 @@ var Point = Base.extend(/** @lends Point# */{ return this.clone(); angle = angle * Math.PI / 180; var point = center ? this.subtract(center) : this, - s = Math.sin(angle), - c = Math.cos(angle); + sin = Math.sin(angle), + cos = Math.cos(angle); point = new Point( - point.x * c - point.y * s, - point.x * s + point.y * c + point.x * cos - point.y * sin, + point.x * sin + point.y * cos ); return center ? point.add(center) : point; }, diff --git a/src/path/Curve.js b/src/path/Curve.js index 0e0e01e1..44e76db6 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1561,8 +1561,7 @@ new function() { // Scope for intersection using bezier fat-line clipping * line is on the X axis, and solve the implicit equations for the X axis * and the curve. */ - function addCurveLineIntersections(v1, v2, c1, c2, locations, - param) { + function addCurveLineIntersections(v1, v2, c1, c2, locations, param) { var flip = Curve.isStraight(v1), vc = flip ? v2 : v1, vl = flip ? v1 : v2, @@ -1576,9 +1575,6 @@ new function() { // Scope for intersection using bezier fat-line clipping sin = Math.sin(angle), cos = Math.cos(angle), // (rlx1, rly1) = (0, 0) - rlx2 = ldx * cos - ldy * sin, - // The curve values for the rotated line. - rvl = [0, 0, 0, 0, rlx2, 0, rlx2, 0], // Calculate the curve values of the rotated curve. rvc = []; for(var i = 0; i < 8; i += 2) { @@ -1586,24 +1582,26 @@ new function() { // Scope for intersection using bezier fat-line clipping y = vc[i + 1] - ly1; rvc.push( x * cos - y * sin, - y * cos + x * sin); + x * sin + y * cos); } + // Solve it for y = 0 var roots = [], - count = Curve.solveCubic(rvc, 1, 0, roots, 0, 1); + tMin = /*#=*/Numerical.CURVETIME_EPSILON, + tMax = 1 - tMin; + count = Curve.solveCubic(rvc, 1, 0, roots, tMin, tMax); // NOTE: count could be -1 for infinite solutions, but that should only // happen with lines, in which case we should not be here. for (var i = 0; i < count; i++) { + // For each found solution on the rotated curve, get the point on + // the real curve and with that the location on the line. var tc = roots[i], - x = Curve.getPoint(rvc, tc).x; - // We do have a point on the infinite line. Check if it falls on - // the line *segment*. - if (x >= 0 && x <= rlx2) { - // Find the parameter of the intersection on the rotated line. - var tl = Curve.getParameterOf(rvl, x, 0), - t1 = flip ? tl : tc, - t2 = flip ? tc : tl; - addLocation(locations, param, v1, c1, t1, null, - v2, c2, t2, null); + pc = Curve.getPoint(vc, tc), + tl = Curve.getParameterOf(vl, pc.x, pc.y); + if (tl !== null) { + var pl = Curve.getPoint(vl, tl); + addLocation(locations, param, + v1, c1, flip ? tl : tc, flip ? pl : pc, + v2, c2, flip ? tc : tl, flip ? pc : pl); } } } From 51c34444db937b90a5dced1f3d8b481afc38e5ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Mon, 21 Sep 2015 12:18:57 -0400 Subject: [PATCH 131/280] Some more debugging code clean-up. --- src/path/PathItem.Boolean.js | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 000c959b..d54c2619 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -71,7 +71,7 @@ PathItem.inject(new function() { return result; } - var scaleFactor = 1 / 10000; + var scaleFactor = 0.25; // 1 / 3000; var textAngle = 33; var fontSize = 5; @@ -215,17 +215,23 @@ PathItem.inject(new function() { // Link the new segment with the intersection on the other curve var inter = segment._intersection; if (inter) { + // Prevent circular references that would cause infinite loops + // in getIntersection(): + // See if the location already links back to this intersection, + // and do not create another connection if it does. var other = inter._intersection, next = loc._next; - while (next && next !== other) { + while (next && next !== other) next = next._next; - } if (!next) { - console.log('Link' - + ', seg: ' + segment._path._id + '.' + segment._index - + ', other: ' + inter._curve._path._id); - // Create a chain of possible intersections linked through _next - // First find the last intersection in the chain, then link it. + if (window.reportSegments) { + console.log('Link: ' + + segment._path._id + '.' + segment._index + + ' -> ' + inter._curve._path._id); + } + // Create a chain of possible intersections linked through + // _next First find the last intersection in the chain, then + // link it. while (inter._next) inter = inter._next; inter._next = loc._intersection; @@ -727,6 +733,9 @@ PathItem.inject(new function() { path._segments.length, 'length = ', path.getLength(), '#' + pathCount + '.' + (path ? path._segments.length + 1 : 1)); + paper.project.activeLayer.addChild(path); + path.strokeColor = 'red'; + path.strokeScaling = false; path = null; } // Add the path to the result, while avoiding stray segments and From 6a29f200e3360ff37d3fcff906c1c218894523ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Mon, 21 Sep 2015 12:44:53 -0400 Subject: [PATCH 132/280] Always use getIntersection(), even on the first segment. Now that it works well. --- src/path/PathItem.Boolean.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index d54c2619..bc2912b4 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -615,7 +615,7 @@ PathItem.inject(new function() { var inter = seg._intersection; // Once we started a chain, see if there are multiple // intersections, and if so, pick the best one: - if (inter && added && window.reportSegments) { + if (inter && window.reportSegments) { console.log('-----\n' +'#' + pathCount + '.' + (path ? path._segments.length + 1 : 1) @@ -624,12 +624,12 @@ PathItem.inject(new function() { + ', other: ' + inter._segment._path._id + '.' + inter._segment._index); } - inter = added && (getIntersection(true, inter) - || getIntersection(false, inter)) || inter; + inter = getIntersection(true, inter) + || getIntersection(false, inter) || inter; var other = inter && inter._segment; // A switched intersection means we may have changed the segment // Point to the other segment in the selected intersection. - if (inter && added && window.reportSegments) { + if (inter && window.reportSegments) { console.log('After getIntersection()' + ', seg: ' + seg._path._id + '.' + seg._index @@ -702,8 +702,8 @@ PathItem.inject(new function() { } if (seg._visited) { // We didn't manage to switch, so stop right here. - console.error('Unable to switch to intersecting segment, ' - + 'aborting #' + pathCount + '.' + console.error('Visited segment encountered, aborting #' + + pathCount + '.' + (path ? path._segments.length + 1 : 1) + ', id: ' + seg._path._id + '.' + seg._index + ', multiple: ' + (!!inter._next)); From db1ecdddd589a46a8a3c58ee1b303ea6bacbfade Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Mon, 21 Sep 2015 16:56:08 -0400 Subject: [PATCH 133/280] Fix filtering of locations at ends of curves in addCurveLineIntersections() Only occured when the line / curve had to be flipped. --- src/path/Curve.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 44e76db6..9923e2df 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1584,11 +1584,10 @@ new function() { // Scope for intersection using bezier fat-line clipping x * cos - y * sin, x * sin + y * cos); } - // Solve it for y = 0 + // Solve it for y = 0. We need to include t = 0, 1 and let addLocation() + // do the filtering, to catch important edge cases. var roots = [], - tMin = /*#=*/Numerical.CURVETIME_EPSILON, - tMax = 1 - tMin; - count = Curve.solveCubic(rvc, 1, 0, roots, tMin, tMax); + count = Curve.solveCubic(rvc, 1, 0, roots, 0, 1); // NOTE: count could be -1 for infinite solutions, but that should only // happen with lines, in which case we should not be here. for (var i = 0; i < count; i++) { @@ -1599,9 +1598,21 @@ new function() { // Scope for intersection using bezier fat-line clipping tl = Curve.getParameterOf(vl, pc.x, pc.y); if (tl !== null) { var pl = Curve.getPoint(vl, tl); + // TODO: Consider passing these as actual arguments so flipping + // is less cumbersome: + var startConnected = param.startConnected, + endConnected = param.endConnected; + if (flip) { + param.endConnected = startConnected; + param.startConnected = endConnected; + } addLocation(locations, param, v1, c1, flip ? tl : tc, flip ? pl : pc, v2, c2, flip ? tc : tl, flip ? pc : pl); + if (flip) { + param.endConnected = endConnected; + param.startConnected = startConnected; + } } } } From 20f950ac65dfb0f28bdc467bf31742e240fefa1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 23 Sep 2015 12:26:44 -0400 Subject: [PATCH 134/280] Implement #isFirst() / #isLast() tests on Segment and Curve. --- src/path/Curve.js | 26 +++++++++++++++++++++++--- src/path/Segment.js | 21 +++++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 9923e2df..1a9f2616 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -298,6 +298,26 @@ var Curve = Base.extend(/** @lends Curve# */{ || this._path._closed && curves[curves.length - 1]) || null; }, + /** + * Checks if the this is the first curve in the {@link Path#curves} array. + * + * @return {Boolean} {@true if this is the first curve} + */ + isFirst: function() { + return this._segment1._index === 0; + }, + + /** + * Checks if the this is the last curve in the {@link Path#curves} array. + * + * @return {Boolean} {@true if this is the last curve} + */ + isLast: function() { + var path = this._path; + return path && this._segment1._index === path._curves.length - 1 + || false; + }, + /** * Specifies whether the points and handles of the curve are selected. * @@ -877,7 +897,7 @@ statics: { * @return {Boolean} {@true if the curve is parametrically linear} */ - /** + /** * Checks if the the two curves describe straight lines that are * collinear, meaning they run in parallel. * @@ -889,7 +909,7 @@ statics: { && this.getVector().isCollinear(curve.getVector()); }, - /** + /** * Checks if the curve is a straight horizontal line. * * @return {Boolean} {@true if the line is horizontal} @@ -899,7 +919,7 @@ statics: { < /*#=*/Numerical.GEOMETRIC_EPSILON; }, - /** + /** * Checks if the curve is a straight vertical line. * * @return {Boolean} {@true if the line is vertical} diff --git a/src/path/Segment.js b/src/path/Segment.js index 61b7d610..c42bbb76 100644 --- a/src/path/Segment.js +++ b/src/path/Segment.js @@ -392,6 +392,27 @@ var Segment = Base.extend(/** @lends Segment# */{ || this._path._closed && segments[segments.length - 1]) || null; }, + /** + * Checks if the this is the first segment in the {@link Path#segments} + * array. + * + * @return {Boolean} {@true if this is the first segment} + */ + isFirst: function() { + return this._index === 0; + }, + + /** + * Checks if the this is the last segment in the {@link Path#segments} + * array. + * + * @return {Boolean} {@true if this is the last segment} + */ + isLast: function() { + var path = this._path; + return path && this._index === path._segments.length - 1 || false; + }, + /** * Reverses the {@link #handleIn} and {@link #handleOut} vectors of this * segment. Note: the actual segment is modified, no copy is created. From cc7e60e51aec72ea79fee8cb3a995d5daa5fdde4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 23 Sep 2015 12:44:00 -0400 Subject: [PATCH 135/280] Revert db1ecdddd589a46a8a3c58ee1b303ea6bacbfade and fix issue properly this time. Hopefully? --- src/path/Curve.js | 71 +++++++++++++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 30 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 1a9f2616..5f1ab1a4 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1359,26 +1359,40 @@ new function() { // Scope for intersection using bezier fat-line clipping overlap) { var loc = null, tMin = /*#=*/Numerical.CURVETIME_EPSILON, - tMax = 1 - tMin; + tMax = 1 - tMin, + startConnected = param.startConnected, + endConnected = param.endConnected; if (t1 == null) t1 = Curve.getParameterOf(v1, p1.x, p1.y); - if (t1 >= (param.startConnected ? tMin : 0) - && t1 <= (param.endConnected ? tMax : 1)) { + // Check t1 and t2 against correct bounds, based on start-/endConnected: + // - startConnected means the start of c1 connects to the end of c2 + // - endConneted means the end of c1 connects to the start of c2 + // - If either c1 or c2 are at the end of the path, exclude their end, + // which connects back to the beginning, but only if it's not part of + // a found overlap. The normal intersection will already be found at + // the beginning, and would be added twice otherwise. + if (t1 >= (startConnected ? tMin : 0) && + t1 <= (endConnected || !overlap && c1.isLast() ? tMax : 1)) { if (t2 == null) t2 = Curve.getParameterOf(v2, p2.x, p2.y); - var renormalize = param.renormalize; - if (renormalize) { - var res = renormalize(t1, t2); - t1 = res[0]; - t2 = res[1]; + if (t2 >= (endConnected ? tMin : 0) && + t2 <= (startConnected || !overlap && c2.isLast() ? tMax : 1)) { + // TODO: Don't we need to check the range of t2 as well? Does it + // also need startConnected / endConnected values? + var renormalize = param.renormalize; + if (renormalize) { + var res = renormalize(t1, t2); + t1 = res[0]; + t2 = res[1]; + } + var include = param.include, + loc = new CurveLocation(c1, t1, + p1 || Curve.getPoint(v1, t1), null, overlap, + new CurveLocation(c2, t2, + p2 || Curve.getPoint(v2, t2), null, overlap)); + if (!include || include(loc)) + CurveLocation.add(locations, loc, true); } - var loc = new CurveLocation(c1, t1, p1 || Curve.getPoint(v1, t1), - null, overlap, - new CurveLocation(c2, t2, p2 || Curve.getPoint(v2, t2), - null, overlap)), - include = param.include; - if (!include || include(loc)) - CurveLocation.add(locations, loc, true); } } @@ -1617,21 +1631,16 @@ new function() { // Scope for intersection using bezier fat-line clipping pc = Curve.getPoint(vc, tc), tl = Curve.getParameterOf(vl, pc.x, pc.y); if (tl !== null) { - var pl = Curve.getPoint(vl, tl); - // TODO: Consider passing these as actual arguments so flipping - // is less cumbersome: - var startConnected = param.startConnected, - endConnected = param.endConnected; - if (flip) { - param.endConnected = startConnected; - param.startConnected = endConnected; - } - addLocation(locations, param, - v1, c1, flip ? tl : tc, flip ? pl : pc, - v2, c2, flip ? tc : tl, flip ? pc : pl); - if (flip) { - param.endConnected = endConnected; - param.startConnected = startConnected; + var pl = Curve.getPoint(vl, tl) + t1 = flip ? tl : tc, + t2 = flip ? tc : tl; + // If the two curves are connected and the 2nd is very short, + // (l < Numerical.GEOMETRIC_EPSILON), we need to filter out an + // invalid intersection at the beginning of this short curve. + if (!param.endConnected || t2 > Numerical.CURVETIME_EPSILON) { + addLocation(locations, param, + v1, c1, t1, flip ? pl : pc, + v2, c2, t2, flip ? pc : pl); } } } @@ -1769,6 +1778,8 @@ new function() { // Scope for intersection using bezier fat-line clipping c1p2 = new Point(c1p2x, c1p2y), c2p1 = new Point(c2p1x, c2p1y), c2p2 = new Point(c2p2x, c2p2y), + // NOTE: Use smaller Numerical.EPSILON to compare beginnings and + // end points to avoid matching them on almost collinear lines. epsilon = /*#=*/Numerical.EPSILON; // Handle the special case where the first curve's stat-point // overlaps with the second curve's start- or end-points. From 515d4ff93d99f5b0e3e0951d2a57968a47987320 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 23 Sep 2015 13:26:29 -0400 Subject: [PATCH 136/280] Make Line.isCollinear() / Point#isCollinear() more reliable. --- src/basic/Line.js | 5 +++-- src/basic/Point.js | 15 +++++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/basic/Line.js b/src/basic/Line.js index 1677779a..9d144fe2 100644 --- a/src/basic/Line.js +++ b/src/basic/Line.js @@ -113,8 +113,9 @@ var Line = Base.extend(/** @lends Line# */{ }, isCollinear: function(line) { - return this._vx * line._vy - this._vy * line._vx - < /*#=*/Numerical.GEOMETRIC_EPSILON; + // TODO: Optimize: + // return Point.isCollinear(this._vx, this._vy, line._vx, line._vy); + return this.getVector().isCollinear(line.getVector()); }, statics: /** @lends Line */{ diff --git a/src/basic/Point.js b/src/basic/Point.js index e2dfa881..d24ca33c 100644 --- a/src/basic/Point.js +++ b/src/basic/Point.js @@ -702,7 +702,13 @@ var Point = Base.extend(/** @lends Point# */{ * @return {Boolean} {@true it is collinear} */ isCollinear: function(point) { - return Math.abs(this.cross(point)) < /*#=*/Numerical.GEOMETRIC_EPSILON; + // NOTE: We use normalized vectors so that the epsilon comparison is + // reliable. We could instead scale the epsilon based on the vector + // length. + // TODO: Optimize by creating a static Point.isCollinear() to be used + // in Line.isCollinear() as well. + return Math.abs(this.normalize().cross(point.normalize())) + < /*#=*/Numerical.GEOMETRIC_EPSILON; }, // TODO: Remove version with typo after a while (deprecated June 2015) @@ -716,7 +722,12 @@ var Point = Base.extend(/** @lends Point# */{ * @return {Boolean} {@true it is orthogonal} */ isOrthogonal: function(point) { - return Math.abs(this.dot(point)) < /*#=*/Numerical.GEOMETRIC_EPSILON; + // NOTE: We use normalized vectors so that the epsilon comparison is + // reliable. We could instead scale the epsilon based on the vector + // length. + // TODO: Optimize + return Math.abs(this.normalize().dot(point.normalize())) + < /*#=*/Numerical.GEOMETRIC_EPSILON; }, /** From cf5bf38c3b471c8e299a22d66b0a52199fa8cebb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 23 Sep 2015 13:33:35 -0400 Subject: [PATCH 137/280] Minor simplification. --- src/path/PathItem.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/path/PathItem.js b/src/path/PathItem.js index 277d38fb..45a38318 100644 --- a/src/path/PathItem.js +++ b/src/path/PathItem.js @@ -132,15 +132,13 @@ var PathItem = Item.extend(/** @lends PathItem# */{ // self intersecting. Curve.getIntersections( values1, values2[j], curve1, curve2, locations, - self ? { + { include: include, // Do not compare indices here to determine connection, // since one array of curves can contain curves from // separate sup-paths of a compound path. - startConnected: curve1.getPrevious() === curve2, - endConnected: curve1.getNext() === curve2 - } : { - include: include + startConnected: self && curve1.getPrevious() === curve2, + endConnected: self && curve1.getNext() === curve2 } ); } From c79166a46fb6926a0d7a3b204afe4b0d6f0b7def Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 23 Sep 2015 14:31:12 -0400 Subject: [PATCH 138/280] Mark last segment as visited when done. --- src/path/PathItem.Boolean.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index bc2912b4..099ca21b 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -639,6 +639,7 @@ PathItem.inject(new function() { if (added && (seg === start || seg === otherStart)) { // We've come back to the start, bail out as we're done. drawSegment(seg, null, 'done', i, 'red'); + seg._visited = true; break; } else if (seg._visited && (!other || other._visited)) { // TODO: Do we still need to check other too? From fc0b5a8858e3e26ca4c6529327fd72a8358ffbf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Thu, 24 Sep 2015 07:47:39 -0400 Subject: [PATCH 139/280] Give the intersection that brings us back to the beginning alwasy the priority. --- src/path/PathItem.Boolean.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 099ca21b..e31cfb66 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -560,7 +560,7 @@ PathItem.inject(new function() { + ', seg wi:' + seg._winding + ', next wi:' + next._winding + ', seg op:' + isValid(seg, true) - + ', next op:' + isValid(next) + + ', next op:' + isValid(next, !strict && inter._overlap) + ', seg ov: ' + (seg._intersection && seg._intersection._overlap) + ', next ov: ' + (next._intersection @@ -579,9 +579,9 @@ PathItem.inject(new function() { // which invalid current segments are tolerated, and overlaps for // the next segment are allowed as long as they are valid when not // adjusted. - return !seg._visited && (!next._visited - || next === start || next === otherStart) - && (!operator // Self-intersection doesn't need isValid() calls + return next === start || next === otherStart + // Self-intersection (!operator) doesn't need isValid() calls + || !seg._visited && !next._visited && (!operator // NOTE: We need to use the unadjusted winding here since an // overlap crossing might have brought us here, in which // case isValid(seg, false) might be false. @@ -681,7 +681,7 @@ PathItem.inject(new function() { drawSegment(seg, other, 'overlap-cross', i, 'orange'); seg = other; } else { - drawSegment(seg, null, 'overlap-stay', i, 'orange'); + drawSegment(seg, other, 'overlap-stay', i, 'orange'); } } else if (operation === 'exclude') { // We need to handle exclusion separately, as we want to From fd927cbe2235d328862a56cec0fdce9b2933786d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Thu, 24 Sep 2015 12:49:39 -0400 Subject: [PATCH 140/280] Properly solve issues with self-intersecting special case. (e.g. shapes resembling the infinity sign) --- src/path/Curve.js | 37 +++++++++++++++++++++++------------- src/path/CurveLocation.js | 12 +++--------- src/path/PathItem.Boolean.js | 1 - 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 5f1ab1a4..32433fd2 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1040,7 +1040,7 @@ statics: { step /= 2; } var pt = Curve.getPoint(values, minT); - return new CurveLocation(this, minT, pt, point.getDistance(pt)); + return new CurveLocation(this, minT, pt, null, point.getDistance(pt)); }, /** @@ -1357,11 +1357,10 @@ new function() { // Scope for intersection using bezier fat-line clipping function addLocation(locations, param, v1, c1, t1, p1, v2, c2, t2, p2, overlap) { - var loc = null, + var startConnected = param.startConnected, + endConnected = param.endConnected, tMin = /*#=*/Numerical.CURVETIME_EPSILON, - tMax = 1 - tMin, - startConnected = param.startConnected, - endConnected = param.endConnected; + tMax = 1 - tMin; if (t1 == null) t1 = Curve.getParameterOf(v1, p1.x, p1.y); // Check t1 and t2 against correct bounds, based on start-/endConnected: @@ -1372,11 +1371,11 @@ new function() { // Scope for intersection using bezier fat-line clipping // a found overlap. The normal intersection will already be found at // the beginning, and would be added twice otherwise. if (t1 >= (startConnected ? tMin : 0) && - t1 <= (endConnected || !overlap && c1.isLast() ? tMax : 1)) { + t1 <= (endConnected ? tMax : 1)) { if (t2 == null) t2 = Curve.getParameterOf(v2, p2.x, p2.y); if (t2 >= (endConnected ? tMin : 0) && - t2 <= (startConnected || !overlap && c2.isLast() ? tMax : 1)) { + t2 <= (startConnected ? tMax : 1)) { // TODO: Don't we need to check the range of t2 as well? Does it // also need startConnected / endConnected values? var renormalize = param.renormalize; @@ -1385,13 +1384,25 @@ new function() { // Scope for intersection using bezier fat-line clipping t1 = res[0]; t2 = res[1]; } - var include = param.include, - loc = new CurveLocation(c1, t1, - p1 || Curve.getPoint(v1, t1), null, overlap, - new CurveLocation(c2, t2, - p2 || Curve.getPoint(v2, t2), null, overlap)); - if (!include || include(loc)) + var loc1 = new CurveLocation(c1, t1, + p1 || Curve.getPoint(v1, t1), overlap), + loc2 = new CurveLocation(c2, t2, + p2 || Curve.getPoint(v2, t2), overlap), + // For self-intersections, detect the case where the second + // curve wrapped around, and flip them so they can get + // matched to a potentially already existing intersection. + flip = loc1.getPath() === loc2.getPath() + && loc1.getIndex() > loc2.getIndex(), + loc = flip ? loc2 : loc1, + include = param.include; + // Link the two locations to each other. + loc1._intersection = loc2; + loc2._intersection = loc1; + // TODO: Remove this once debug logging is removed. + (flip ? loc1 : loc2)._other = true; + if (!include || include(loc)) { CurveLocation.add(locations, loc, true); + } } } } diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index b66602b7..b1111a35 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -42,7 +42,7 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ * @param {Point} [point] */ initialize: function CurveLocation(curve, parameter, point, - _distance, _overlap, _intersection) { + _overlap, _distance) { // Merge intersections very close to the end of a curve to the // beginning of the next curve. if (parameter >= 1 - /*#=*/Numerical.CURVETIME_EPSILON) { @@ -60,15 +60,9 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ this._setCurve(curve); this._parameter = parameter; this._point = point || curve.getPointAt(parameter, true); - this._distance = _distance; this._overlap = _overlap; - this._crossing = null; - this._intersection = _intersection; - if (_intersection) { - _intersection._intersection = this; - // TODO: Remove this once debug logging is removed. - _intersection._other = true; - } + this._distance = _distance; + this._intersection = null; }, _setCurve: function(curve) { diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index e31cfb66..9cd124ed 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -639,7 +639,6 @@ PathItem.inject(new function() { if (added && (seg === start || seg === otherStart)) { // We've come back to the start, bail out as we're done. drawSegment(seg, null, 'done', i, 'red'); - seg._visited = true; break; } else if (seg._visited && (!other || other._visited)) { // TODO: Do we still need to check other too? From 3fa810a5579602a46e6656890aacd8cec7bf8e9c Mon Sep 17 00:00:00 2001 From: Jan Date: Fri, 25 Sep 2015 20:32:54 +0200 Subject: [PATCH 141/280] Bugfix for #791 and performance improvement Prevent infinite loop for degenerate curve. Additionally only calculate sy if necessary and save one call to `Curve.solveCubic` --- src/path/Curve.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 32433fd2..daf7cdc2 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -622,9 +622,12 @@ statics: { var txs = [], tys = [], sx = Curve.solveCubic(v, 0, x, txs, 0, 1), - sy = Curve.solveCubic(v, 1, y, tys, 0, 1), + sy = sx === 0 ? null : Curve.solveCubic(v, 1, y, tys, 0, 1), tx, ty; - // sx, sy === -1 means infinite solutions: + // sx, sy === -1 means infinite solutions. + // sx === -1 && sy === -1 means the curve is a point and there is + // an infinite number of solutions. + if (sx === -1 && sy === -1) return 0; // Loop through all solutions for x and match with solutions for y, // to see if we either have a matching pair, or infinite solutions // for one or the other. From ea3cc63e2e50d57b214d131a0bea08c227356ffb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sat, 26 Sep 2015 07:41:03 -0400 Subject: [PATCH 142/280] Reformat code a bit. --- src/path/Curve.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index daf7cdc2..d7029b90 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -622,12 +622,15 @@ statics: { var txs = [], tys = [], sx = Curve.solveCubic(v, 0, x, txs, 0, 1), - sy = sx === 0 ? null : Curve.solveCubic(v, 1, y, tys, 0, 1), + // Only solve for y if x actually has some solutions + sy = sx !== 0 ? Curve.solveCubic(v, 1, y, tys, 0, 1) : 0, tx, ty; // sx, sy === -1 means infinite solutions. - // sx === -1 && sy === -1 means the curve is a point and there is - // an infinite number of solutions. - if (sx === -1 && sy === -1) return 0; + // sx === -1 && sy === -1 means the curve is a point and there really is + // an infinite number of solutions. Let's just return t = 0, as they are + // all valid and actually end up being the same position. + if (sx === -1 && sy === -1) + return 0; // Loop through all solutions for x and match with solutions for y, // to see if we either have a matching pair, or infinite solutions // for one or the other. From 317b809fee43e780bc1ea45be0c1ddc9c72bb736 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sat, 26 Sep 2015 11:46:54 -0500 Subject: [PATCH 143/280] Only calculate non-parametric bezier curve if values are actually used. --- src/path/Curve.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index d7029b90..69a7460b 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1434,13 +1434,6 @@ new function() { // Scope for intersection using bezier fat-line clipping factor = d1 * d2 > 0 ? 3 / 4 : 4 / 9, dMin = factor * Math.min(0, d1, d2), dMax = factor * Math.max(0, d1, d2), - // Calculate non-parametric bezier curve D(ti, di(t)) - di(t) is the - // distance of P from the baseline l of the fat-line, ti is equally - // spaced in [0, 1] - dp0 = getSignedDistance(q0x, q0y, q3x, q3y, v1[0], v1[1]), - dp1 = getSignedDistance(q0x, q0y, q3x, q3y, v1[2], v1[3]), - dp2 = getSignedDistance(q0x, q0y, q3x, q3y, v1[4], v1[5]), - dp3 = getSignedDistance(q0x, q0y, q3x, q3y, v1[6], v1[7]), tMinNew, tMaxNew, tDiff; if (q0x === q3x && uMax - uMin < epsilon && recursion >= 3) { @@ -1450,8 +1443,15 @@ new function() { // Scope for intersection using bezier fat-line clipping tMaxNew = tMinNew = (tMax + tMin) / 2; tDiff = 0; } else { - // Get the top and bottom parts of the convex-hull - var hull = getConvexHull(dp0, dp1, dp2, dp3), + // Calculate non-parametric bezier curve D(ti, di(t)) - di(t) is the + // distance of P from the baseline l of the fat-line, ti is equally + // spaced in [0, 1] + var dp0 = getSignedDistance(q0x, q0y, q3x, q3y, v1[0], v1[1]), + dp1 = getSignedDistance(q0x, q0y, q3x, q3y, v1[2], v1[3]), + dp2 = getSignedDistance(q0x, q0y, q3x, q3y, v1[4], v1[5]), + dp3 = getSignedDistance(q0x, q0y, q3x, q3y, v1[6], v1[7]), + // Get the top and bottom parts of the convex-hull + hull = getConvexHull(dp0, dp1, dp2, dp3), top = hull[0], bottom = hull[1], tMinClip, tMaxClip; From a869add90d43896b028abe60175492b18be25f28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sat, 26 Sep 2015 12:09:44 -0500 Subject: [PATCH 144/280] Rename variables in Line.intersect() --- src/basic/Line.js | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/basic/Line.js b/src/basic/Line.js index 9d144fe2..f7ab39e6 100644 --- a/src/basic/Line.js +++ b/src/basic/Line.js @@ -119,28 +119,27 @@ var Line = Base.extend(/** @lends Line# */{ }, statics: /** @lends Line */{ - intersect: function(apx, apy, avx, avy, bpx, bpy, bvx, bvy, asVector, + intersect: function(p1x, p1y, v1x, v1y, p2x, p2y, v2x, v2y, asVector, isInfinite) { // Convert 2nd points to vectors if they are not specified as such. if (!asVector) { - avx -= apx; - avy -= apy; - bvx -= bpx; - bvy -= bpy; + v1x -= p1x; + v1y -= p1y; + v2x -= p2x; + v2y -= p2y; } - var cross = avx * bvy - avy * bvx; + var cross = v1x * v2y - v1y * v2x; // Avoid divisions by 0, and errors when getting too close to 0 if (!Numerical.isZero(cross)) { - var dx = apx - bpx, - dy = apy - bpy, - ta = (bvx * dy - bvy * dx) / cross, - tb = (avx * dy - avy * dx) / cross; + var dx = p1x - p2x, + dy = p1y - p2y, + t1 = (v2x * dy - v2y * dx) / cross, + t2 = (v1x * dy - v1y * dx) / cross; // Check the ranges of t parameters if the line is not allowed // to extend beyond the definition points. - if (isInfinite || 0 <= ta && ta <= 1 && 0 <= tb && tb <= 1) + if (isInfinite || 0 <= t1 && t1 <= 1 && 0 <= t2 && t2 <= 1) return new Point( - apx + ta * avx, - apy + ta * avy); + p1x + t1 * v1x, } }, From ec70fa1806c41d19f04db83962aadbe789c7f934 Mon Sep 17 00:00:00 2001 From: Jan Date: Wed, 30 Sep 2015 12:19:09 +0200 Subject: [PATCH 145/280] Fix for #773 Indroduced more reliable method for finding self intersection on curves. --- src/path/Curve.js | 161 ++++++++++++++++++++++++++++--------------- src/path/PathItem.js | 40 +++-------- 2 files changed, 116 insertions(+), 85 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index d7029b90..6b107cbb 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -417,13 +417,16 @@ var Curve = Base.extend(/** @lends Curve# */{ * Returns all intersections between two {@link Curve} objects as an array * of {@link CurveLocation} objects. * + * If the parameter curve is null, the self intersection of the curve is + * returned, if it exists. + * * @param {Curve} curve the other curve to find the intersections with * @return {CurveLocation[]} the locations of all intersection between the * curves */ getIntersections: function(curve) { - return Curve.getIntersections(this.getValues(), curve.getValues(), - this, curve, [], {}); + return Curve.getIntersections(this.getValues(), curve ? curve.getValues() : null, + this, curve ? curve : this, [], {}); }, // TODO: adjustThroughPoint @@ -1757,25 +1760,74 @@ new function() { // Scope for intersection using bezier fat-line clipping // #getIntersections() calls as it is required to create the resulting // CurveLocation objects. getIntersections: function(v1, v2, c1, c2, locations, param) { - // Avoid checking curves if completely out of control bounds. - // As a little optimization, we can scale the handles with 0.75 - // before calculating the control bounds and still be sure that the - // curve is fully contained. - var c1p1x = v1[0], c1p1y = v1[1], - c1p2x = v1[6], c1p2y = v1[7], - c2p1x = v2[0], c2p1y = v2[1], - c2p2x = v2[6], c2p2y = v2[7], - c1h1x = (3 * v1[2] + c1p1x) / 4, - c1h1y = (3 * v1[3] + c1p1y) / 4, - c1h2x = (3 * v1[4] + c1p2x) / 4, - c1h2y = (3 * v1[5] + c1p2y) / 4, - c2h1x = (3 * v2[2] + c2p1x) / 4, - c2h1y = (3 * v2[3] + c2p1y) / 4, - c2h2x = (3 * v2[4] + c2p2x) / 4, - c2h2y = (3 * v2[5] + c2p2y) / 4, - min = Math.min, - max = Math.max; - if (!( + if (!v2) { // if v2 is null or undefined, search for self intersection + // get side of both handles + var h1Side = Line.getSide(v1[0], v1[1], v1[6], v1[7], v1[2], v1[3], false); + var h2Side = Line.getSide(v1[0], v1[1], v1[6], v1[7], v1[4], v1[5], false); + if (h1Side == h2Side) { + var edgeSum = (v1[0] - v1[4]) * (v1[3] - v1[7]) + (v1[2] - v1[6]) * (v1[5] - v1[1]); + // if both handles are on the same side, the curve can only have a self intersection if + // the edge sum and the handles's side have different signs. If the handles are on the + // left side, the edge sum must be negative for a self intersection (and vice versa) + if (Math.sign(edgeSum) == h1Side) return locations; + } + // As a second condition we check if the curve has an inflection point. If an inflection point + // exists, the curve cannot have a self intersection. + var ax = v1[6] - 3 * v1[4] + 3 * v1[2] - v1[0]; + var bx = v1[4] - 2 * v1[2] + v1[0]; + var cx = v1[2] - v1[0]; + var ay = v1[7] - 3 * v1[5] + 3 * v1[3] - v1[1]; + var by = v1[5] - 2 * v1[3] + v1[1]; + var cy = v1[3] - v1[1]; + var hasInflectionPoint = (Math.pow(ay * cx - ax * cy, 2) - 4 * (ay * bx - ax * by) * (by * cx - bx * cy) >= 0); + if (!hasInflectionPoint) { + // the curve may have a self intersection, find parameter to split curve. We search for the + // parameter where the velocity has an extremum by finding the roots of the cross product + // between the bezier curve's first and second derivative + var roots = [], + rootCount = Numerical.solveCubic(ax * ax + ay * ay, 3 * (ax * bx + ay * by), + (2 * (bx * bx + by * by) + ax * cx + ay * cy), (bx * cx + by * cy), roots, 0, 1); + // Select extremum with smallest curvature. This is always on the loop in case of a self intersection + var tSplit, maxCurvature; + for (var i = 0; i < rootCount; i++) { + var curvature = Math.abs(c1.getCurvatureAt(roots[i], true)); + if (!maxCurvature || curvature > maxCurvature) { + maxCurvature = curvature; + tSplit = roots[i]; + } + } + // Divide the curve in two and then apply the normal curve intersection code. + var parts = Curve.subdivide(v1, tSplit); + if (!param) param = {}; + // After splitting, the end is always connected: + param.endConnected = true; + // Since the curve was split above, we need to + // adjust the parameters for both locations. + param.renormalize = function(t1, t2) { + return [t1 * tSplit, t2 * (1 - tSplit) + tSplit]; + }; + Curve.getIntersections(parts[0], parts[1], c1, c1, locations, param); + } + } else { + // Avoid checking curves if completely out of control bounds. + // As a little optimization, we can scale the handles with 0.75 + // before calculating the control bounds and still be sure that the + // curve is fully contained. + var c1p1x = v1[0], c1p1y = v1[1], + c1p2x = v1[6], c1p2y = v1[7], + c2p1x = v2[0], c2p1y = v2[1], + c2p2x = v2[6], c2p2y = v2[7], + c1h1x = (3 * v1[2] + c1p1x) / 4, + c1h1y = (3 * v1[3] + c1p1y) / 4, + c1h2x = (3 * v1[4] + c1p2x) / 4, + c1h2y = (3 * v1[5] + c1p2y) / 4, + c2h1x = (3 * v2[2] + c2p1x) / 4, + c2h1y = (3 * v2[3] + c2p1y) / 4, + c2h2x = (3 * v2[4] + c2p2x) / 4, + c2h2y = (3 * v2[5] + c2p2y) / 4, + min = Math.min, + max = Math.max; + if (!( max(c1p1x, c1h1x, c1h2x, c1p2x) >= min(c2p1x, c2h1x, c2h2x, c2p2x) && min(c1p1x, c1h1x, c1h2x, c1p2x) <= @@ -1784,44 +1836,45 @@ new function() { // Scope for intersection using bezier fat-line clipping min(c2p1y, c2h1y, c2h2y, c2p2y) && min(c1p1y, c1h1y, c1h2y, c1p2y) <= max(c2p1y, c2h1y, c2h2y, c2p2y) - ) - // Also detect and handle overlaps: - || !param.startConnected && !param.endConnected + ) + // Also detect and handle overlaps: + || !param.startConnected && !param.endConnected && addOverlap(v1, v2, c1, c2, locations, param)) - return locations; - var straight1 = Curve.isStraight(v1), - straight2 = Curve.isStraight(v2), - c1p1 = new Point(c1p1x, c1p1y), - c1p2 = new Point(c1p2x, c1p2y), - c2p1 = new Point(c2p1x, c2p1y), - c2p2 = new Point(c2p2x, c2p2y), + return locations; + var straight1 = Curve.isStraight(v1), + straight2 = Curve.isStraight(v2), + c1p1 = new Point(c1p1x, c1p1y), + c1p2 = new Point(c1p2x, c1p2y), + c2p1 = new Point(c2p1x, c2p1y), + c2p2 = new Point(c2p2x, c2p2y), // NOTE: Use smaller Numerical.EPSILON to compare beginnings and // end points to avoid matching them on almost collinear lines. - epsilon = /*#=*/Numerical.EPSILON; - // Handle the special case where the first curve's stat-point - // overlaps with the second curve's start- or end-points. - if (c1p1.isClose(c2p1, epsilon)) - addLocation(locations, param, v1, c1, 0, c1p1, v2, c2, 0, c2p1); - if (!param.startConnected && c1p1.isClose(c2p2, epsilon)) - addLocation(locations, param, v1, c1, 0, c1p1, v2, c2, 1, c2p2); - // Determine the correct intersection method based on whether one or - // curves are straight lines: - (straight1 && straight2 - ? addLineIntersection - : straight1 || straight2 + epsilon = /*#=*/Numerical.EPSILON; + // Handle the special case where the first curve's stat-point + // overlaps with the second curve's start- or end-points. + if (c1p1.isClose(c2p1, epsilon)) + addLocation(locations, param, v1, c1, 0, c1p1, v2, c2, 0, c2p1); + if (!param.startConnected && c1p1.isClose(c2p2, epsilon)) + addLocation(locations, param, v1, c1, 0, c1p1, v2, c2, 1, c2p2); + // Determine the correct intersection method based on whether one or + // curves are straight lines: + (straight1 && straight2 + ? addLineIntersection + : straight1 || straight2 ? addCurveLineIntersections : addCurveIntersections)( - v1, v2, c1, c2, locations, param, - // Define the defaults for these parameters of - // addCurveIntersections(): - // tMin, tMax, uMin, uMax, oldTDiff, reverse, recursion - 0, 1, 0, 1, 0, false, 0); - // Handle the special case where the first curve's end-point - // overlaps with the second curve's start- or end-points. - if (!param.endConnected && c1p2.isClose(c2p1, epsilon)) - addLocation(locations, param, v1, c1, 1, c1p2, v2, c2, 0, c2p1); - if (c1p2.isClose(c2p2, epsilon)) - addLocation(locations, param, v1, c1, 1, c1p2, v2, c2, 1, c2p2); + v1, v2, c1, c2, locations, param, + // Define the defaults for these parameters of + // addCurveIntersections(): + // tMin, tMax, uMin, uMax, oldTDiff, reverse, recursion + 0, 1, 0, 1, 0, false, 0); + // Handle the special case where the first curve's end-point + // overlaps with the second curve's start- or end-points. + if (!param.endConnected && c1p2.isClose(c2p1, epsilon)) + addLocation(locations, param, v1, c1, 1, c1p2, v2, c2, 0, c2p1); + if (c1p2.isClose(c2p2, epsilon)) + addLocation(locations, param, v1, c1, 1, c1p2, v2, c2, 1, c2p2); + } return locations; } }}; diff --git a/src/path/PathItem.js b/src/path/PathItem.js index 45a38318..57e1a35d 100644 --- a/src/path/PathItem.js +++ b/src/path/PathItem.js @@ -88,37 +88,15 @@ var PathItem = Item.extend(/** @lends PathItem# */{ values1 = self ? values2[i] : curve1.getValues(matrix1); if (self) { // First check for self-intersections within the same curve - var seg1 = curve1.getSegment1(), - seg2 = curve1.getSegment2(), - p1 = seg1._point, - p2 = seg2._point, - h1 = seg1._handleOut, - h2 = seg2._handleIn, - l1 = new Line(p1.subtract(h1), p1.add(h1)), - l2 = new Line(p2.subtract(h2), p1.add(h2)); - // Check if extended handles of endpoints of this curve - // intersects each other. We cannot have a self intersection - // within this curve if they don't intersect due to convex-hull - // property. - if (l1.intersect(l2, false)) { - // Self intersecting is found by dividing the curve in two - // and and then applying the normal curve intersection code. - var parts = Curve.subdivide(values1, 0.5); - Curve.getIntersections(parts[0], parts[1], curve1, curve1, - locations, { - include: include, - // Only possible if there is only one closed curve: - startConnected: length1 === 1 && p1.equals(p2), - // After splitting, the end is always connected: - endConnected: true, - renormalize: function(t1, t2) { - // Since the curve was split above, we need to - // adjust the parameters for both locations. - return [t1 / 2, (1 + t2) / 2]; - } - } - ); - } + var p1 = curve1.getSegment1()._point, + p2 = curve1.getSegment2()._point; + Curve.getIntersections(values1, null, curve1, curve1, + locations, { + include: include, + // Only possible if there is only one closed curve: + startConnected: length1 === 1 && p1.equals(p2) + } + ); } // Check for intersections with other curves. For self intersection, // we can start at i + 1 instead of 0 From 4e9bac1ca5de00142edb1193ee45eab8d04edf8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 30 Sep 2015 12:39:59 -0500 Subject: [PATCH 146/280] Fix code brokean in commit a869add90d43896b028abe60175492b18be25f28 --- src/basic/Line.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/basic/Line.js b/src/basic/Line.js index f7ab39e6..b8fae4ec 100644 --- a/src/basic/Line.js +++ b/src/basic/Line.js @@ -139,7 +139,8 @@ var Line = Base.extend(/** @lends Line# */{ // to extend beyond the definition points. if (isInfinite || 0 <= t1 && t1 <= 1 && 0 <= t2 && t2 <= 1) return new Point( - p1x + t1 * v1x, + p1x + t1 * v1x, + p1y + t1 * v1y); } }, From 2a7d1c5728d1c80cb2c1818308e47f9bacaf057b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 30 Sep 2015 12:47:02 -0500 Subject: [PATCH 147/280] Improve CurveLocation#equals() Relates to #784, described in https://github.com/paperjs/paper.js/issues/784#issuecomment-143161586 --- src/path/CurveLocation.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index b1111a35..5bfdc06a 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -365,8 +365,10 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ || loc instanceof CurveLocation // Call getCurve() and getParameter() to keep in sync && this.getCurve() === loc.getCurve() - && this.getPoint().isClose(loc.getPoint(), + && (this.getPoint().isClose(loc.getPoint(), /*#=*/Numerical.GEOMETRIC_EPSILON) + || Math.abs(this.getParameter() - loc.getParameter()) + < /*#=*/Numerical.CURVETIME_EPSILON) && (_ignoreOther || (!this._intersection && !loc._intersection || this._intersection && this._intersection.equals( From 12311535533a700cfd620f0531f7002e183e39a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 30 Sep 2015 13:07:55 -0500 Subject: [PATCH 148/280] Start cleaning up code from #773 - Use Line object isntead of static methods - Do not rely on Math.sign() as it's not supported on all browsers - Wrap lines at 80 char width. --- src/path/Curve.js | 143 ++++++++++++++++++++++++------------------- src/path/PathItem.js | 5 +- 2 files changed, 81 insertions(+), 67 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 377b1a23..53bf50a2 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -417,16 +417,16 @@ var Curve = Base.extend(/** @lends Curve# */{ * Returns all intersections between two {@link Curve} objects as an array * of {@link CurveLocation} objects. * - * If the parameter curve is null, the self intersection of the curve is - * returned, if it exists. - * - * @param {Curve} curve the other curve to find the intersections with - * @return {CurveLocation[]} the locations of all intersection between the + * @param {Curve} curve the other curve to find the intersections with (if + * the curve itself or {@code null} is passed, the self intersection of the + * curve is returned, if it exists) + * @return {CurveLocation[]} the locations of all intersections between the * curves */ getIntersections: function(curve) { - return Curve.getIntersections(this.getValues(), curve ? curve.getValues() : null, - this, curve ? curve : this, [], {}); + return Curve.getIntersections(this.getValues(), + curve && curve !== this ? curve.getValues() : null, + this, curve, [], {}); }, // TODO: adjustThroughPoint @@ -1756,63 +1756,78 @@ new function() { // Scope for intersection using bezier fat-line clipping } return { statics: /** @lends Curve */{ - // We need to provide the original left curve reference to the - // #getIntersections() calls as it is required to create the resulting - // CurveLocation objects. getIntersections: function(v1, v2, c1, c2, locations, param) { - if (!v2) { // if v2 is null or undefined, search for self intersection - // get side of both handles - var h1Side = Line.getSide(v1[0], v1[1], v1[6], v1[7], v1[2], v1[3], false); - var h2Side = Line.getSide(v1[0], v1[1], v1[6], v1[7], v1[4], v1[5], false); - if (h1Side == h2Side) { - var edgeSum = (v1[0] - v1[4]) * (v1[3] - v1[7]) + (v1[2] - v1[6]) * (v1[5] - v1[1]); - // if both handles are on the same side, the curve can only have a self intersection if - // the edge sum and the handles's side have different signs. If the handles are on the - // left side, the edge sum must be negative for a self intersection (and vice versa) - if (Math.sign(edgeSum) == h1Side) return locations; + // If v2 is not provided, search for self intersection on v1. + if (!v2) { + // Get side of both handles + var line = new Line(v1[0], v1[1], v1[6], v1[7], false), + side1 = line.getSide(v1[2], v1[3]), + side2 = Line.getSide(v1[4], v1[5]); + if (side1 === side2) { + var edgeSum = (v1[0] - v1[4]) * (v1[3] - v1[7]) + + (v1[2] - v1[6]) * (v1[5] - v1[1]); + // If both handles are on the same side, the curve can only + // have a self intersection if the edge sum and the + // handles' sides have different signs. If the handles are + // on the left side, the edge sum must be negative for a + // self intersection (and vice-versa). + if (edgeSum * side1 > 0) + return locations; } - // As a second condition we check if the curve has an inflection point. If an inflection point - // exists, the curve cannot have a self intersection. - var ax = v1[6] - 3 * v1[4] + 3 * v1[2] - v1[0]; - var bx = v1[4] - 2 * v1[2] + v1[0]; - var cx = v1[2] - v1[0]; - var ay = v1[7] - 3 * v1[5] + 3 * v1[3] - v1[1]; - var by = v1[5] - 2 * v1[3] + v1[1]; - var cy = v1[3] - v1[1]; - var hasInflectionPoint = (Math.pow(ay * cx - ax * cy, 2) - 4 * (ay * bx - ax * by) * (by * cx - bx * cy) >= 0); - if (!hasInflectionPoint) { - // the curve may have a self intersection, find parameter to split curve. We search for the - // parameter where the velocity has an extremum by finding the roots of the cross product - // between the bezier curve's first and second derivative + // As a second condition we check if the curve has an inflection + // point. If an inflection point exists, the curve cannot have a + // self intersection. + var ax = v1[6] - 3 * v1[4] + 3 * v1[2] - v1[0], + bx = v1[4] - 2 * v1[2] + v1[0], + cx = v1[2] - v1[0], + ay = v1[7] - 3 * v1[5] + 3 * v1[3] - v1[1], + by = v1[5] - 2 * v1[3] + v1[1], + cy = v1[3] - v1[1], + hasInflection = Math.pow(ay * cx - ax * cy, 2) + - 4 * (ay * bx - ax * by) * (by * cx - bx * cy) >= 0; + if (!hasInflection) { + // The curve may have a self intersection, find parameter to + // split curve. We search for the parameter where the + // velocity has an extremum by finding the roots of the + // cross product between the bezier curve's first and second + // derivative. var roots = [], - rootCount = Numerical.solveCubic(ax * ax + ay * ay, 3 * (ax * bx + ay * by), - (2 * (bx * bx + by * by) + ax * cx + ay * cy), (bx * cx + by * cy), roots, 0, 1); - // Select extremum with smallest curvature. This is always on the loop in case of a self intersection - var tSplit, maxCurvature; - for (var i = 0; i < rootCount; i++) { - var curvature = Math.abs(c1.getCurvatureAt(roots[i], true)); - if (!maxCurvature || curvature > maxCurvature) { + rootCount = Numerical.solveCubic( + ax * ax + ay * ay, + 3 * (ax * bx + ay * by), + 2 * (bx * bx + by * by) + ax * cx + ay * cy, + bx * cx + by * cy, + roots, 0, 1); + // Select extremum with smallest curvature. This is always + // on the loop in case of a self intersection. + var tSplit; + for (var i = 0, maxCurvature = 0; i < rootCount; i++) { + var curvature = Math.abs( + c1.getCurvatureAt(roots[i], true)); + if (curvature > maxCurvature) { maxCurvature = curvature; tSplit = roots[i]; } } - // Divide the curve in two and then apply the normal curve intersection code. + // Divide the curve in two and then apply the normal curve + // intersection code. var parts = Curve.subdivide(v1, tSplit); - if (!param) param = {}; - // After splitting, the end is always connected: - param.endConnected = true; - // Since the curve was split above, we need to - // adjust the parameters for both locations. - param.renormalize = function(t1, t2) { - return [t1 * tSplit, t2 * (1 - tSplit) + tSplit]; - }; - Curve.getIntersections(parts[0], parts[1], c1, c1, locations, param); + Curve.getIntersections(parts[0], parts[1], c1, c1, locations, { + startConnected: param.startConnected, + // After splitting, the end is always connected: + endConnected: true, + // Since the curve was split above, we need to + // adjust the parameters for both locations. + renormalize: function(t1, t2) { + return [t1 * tSplit, t2 * (1 - tSplit) + tSplit]; + } + }); } } else { - // Avoid checking curves if completely out of control bounds. - // As a little optimization, we can scale the handles with 0.75 - // before calculating the control bounds and still be sure that the - // curve is fully contained. + // Avoid checking curves if completely out of control bounds. As + // a little optimization, we can scale the handles with 0.75 + // before calculating the control bounds and still be sure that + // the curve is fully contained. var c1p1x = v1[0], c1p1y = v1[1], c1p2x = v1[6], c1p2y = v1[7], c2p1x = v2[0], c2p1y = v2[1], @@ -1828,18 +1843,18 @@ new function() { // Scope for intersection using bezier fat-line clipping min = Math.min, max = Math.max; if (!( - max(c1p1x, c1h1x, c1h2x, c1p2x) >= - min(c2p1x, c2h1x, c2h2x, c2p2x) && - min(c1p1x, c1h1x, c1h2x, c1p2x) <= - max(c2p1x, c2h1x, c2h2x, c2p2x) && - max(c1p1y, c1h1y, c1h2y, c1p2y) >= - min(c2p1y, c2h1y, c2h2y, c2p2y) && - min(c1p1y, c1h1y, c1h2y, c1p2y) <= - max(c2p1y, c2h1y, c2h2y, c2p2y) + max(c1p1x, c1h1x, c1h2x, c1p2x) >= + min(c2p1x, c2h1x, c2h2x, c2p2x) && + min(c1p1x, c1h1x, c1h2x, c1p2x) <= + max(c2p1x, c2h1x, c2h2x, c2p2x) && + max(c1p1y, c1h1y, c1h2y, c1p2y) >= + min(c2p1y, c2h1y, c2h2y, c2p2y) && + min(c1p1y, c1h1y, c1h2y, c1p2y) <= + max(c2p1y, c2h1y, c2h2y, c2p2y) ) - // Also detect and handle overlaps: + // Also detect and handle overlaps: || !param.startConnected && !param.endConnected - && addOverlap(v1, v2, c1, c2, locations, param)) + && addOverlap(v1, v2, c1, c2, locations, param)) return locations; var straight1 = Curve.isStraight(v1), straight2 = Curve.isStraight(v2), diff --git a/src/path/PathItem.js b/src/path/PathItem.js index 57e1a35d..3e05848e 100644 --- a/src/path/PathItem.js +++ b/src/path/PathItem.js @@ -88,13 +88,12 @@ var PathItem = Item.extend(/** @lends PathItem# */{ values1 = self ? values2[i] : curve1.getValues(matrix1); if (self) { // First check for self-intersections within the same curve - var p1 = curve1.getSegment1()._point, - p2 = curve1.getSegment2()._point; Curve.getIntersections(values1, null, curve1, curve1, locations, { include: include, // Only possible if there is only one closed curve: - startConnected: length1 === 1 && p1.equals(p2) + startConnected: length1 === 1 && + curve1.getPoint1().equals(curve1.getPoint2()) } ); } From 45040abc5305c4e917918be5d62215a6cfc9380e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 30 Sep 2015 13:40:01 -0500 Subject: [PATCH 149/280] More clean-ups for #773 - Use local variables instead of array lookups for values used repeatetly, and merge with pre-existing variables. - Add some more comments and reference to long explaining post in issue. --- src/path/Curve.js | 211 ++++++++++++++++++++++++---------------------- 1 file changed, 110 insertions(+), 101 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 53bf50a2..af0d678a 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1757,15 +1757,22 @@ new function() { // Scope for intersection using bezier fat-line clipping return { statics: /** @lends Curve */{ getIntersections: function(v1, v2, c1, c2, locations, param) { + var c1p1x = v1[0], c1p1y = v1[1], + c1h1x = v1[2], c1h1y = v1[3], + c1h2x = v1[4], c1h2y = v1[5], + c1p2x = v1[6], c1p2y = v1[7]; // If v2 is not provided, search for self intersection on v1. if (!v2) { - // Get side of both handles - var line = new Line(v1[0], v1[1], v1[6], v1[7], false), - side1 = line.getSide(v1[2], v1[3]), - side2 = Line.getSide(v1[4], v1[5]); + // Read a detailed description of the approach used to handle + // self-intersection, developed by @iconexperience here: + // https://github.com/paperjs/paper.js/issues/773#issuecomment-144018379 + // Get the side of both control handles + var line = new Line(c1p1x, c1p1y, c1p2x, c1p2y, false), + side1 = line.getSide(c1h1x, c1h1y), + side2 = Line.getSide(c1h2x, c1h2y); if (side1 === side2) { - var edgeSum = (v1[0] - v1[4]) * (v1[3] - v1[7]) - + (v1[2] - v1[6]) * (v1[5] - v1[1]); + var edgeSum = (c1p1x - c1h2x) * (c1h1y - c1p2y) + + (c1h1x - c1p2x) * (c1h2y - c1p1y); // If both handles are on the same side, the curve can only // have a self intersection if the edge sum and the // handles' sides have different signs. If the handles are @@ -1777,31 +1784,34 @@ new function() { // Scope for intersection using bezier fat-line clipping // As a second condition we check if the curve has an inflection // point. If an inflection point exists, the curve cannot have a // self intersection. - var ax = v1[6] - 3 * v1[4] + 3 * v1[2] - v1[0], - bx = v1[4] - 2 * v1[2] + v1[0], - cx = v1[2] - v1[0], - ay = v1[7] - 3 * v1[5] + 3 * v1[3] - v1[1], - by = v1[5] - 2 * v1[3] + v1[1], - cy = v1[3] - v1[1], - hasInflection = Math.pow(ay * cx - ax * cy, 2) - - 4 * (ay * bx - ax * by) * (by * cx - bx * cy) >= 0; - if (!hasInflection) { - // The curve may have a self intersection, find parameter to - // split curve. We search for the parameter where the - // velocity has an extremum by finding the roots of the - // cross product between the bezier curve's first and second - // derivative. + var ax = c1p2x - 3 * c1h2x + 3 * c1h1x - c1p1x, + bx = c1h2x - 2 * c1h1x + c1p1x, + cx = c1h1x - c1p1x, + ay = c1p2y - 3 * c1h2y + 3 * c1h1y - c1p1y, + by = c1h2y - 2 * c1h1y + c1p1y, + cy = c1h1y - c1p1y, + // Condition for 1 or 2 inflection points: + // (ay*cx-ax*cy)^2 - 4*(ay*bx-ax*by)*(by*cx-bx*cy) >= 0 + ac = ay * cx - ax * cy, + ab = ay * bx - ax * by, + bc = by * cx - bx * cy; + if (ac * ac - 4 * ab * bc < 0) { + // The curve has no inflection points, so it may have a self + // intersection. Find the right parameter at which to split + // the curve. We search for the parameter where the velocity + // has an extremum by finding the roots of the cross product + // between the bezier curve's first and second derivative. var roots = [], - rootCount = Numerical.solveCubic( + tSplit, + count = Numerical.solveCubic( ax * ax + ay * ay, 3 * (ax * bx + ay * by), 2 * (bx * bx + by * by) + ax * cx + ay * cy, bx * cx + by * cy, roots, 0, 1); - // Select extremum with smallest curvature. This is always - // on the loop in case of a self intersection. - var tSplit; - for (var i = 0, maxCurvature = 0; i < rootCount; i++) { + // Select extremum with highest curvature. This is always on + // the loop in case of a self intersection. + for (var i = 0, maxCurvature = 0; i < count; i++) { var curvature = Math.abs( c1.getCurvatureAt(roots[i], true)); if (curvature > maxCurvature) { @@ -1812,84 +1822,83 @@ new function() { // Scope for intersection using bezier fat-line clipping // Divide the curve in two and then apply the normal curve // intersection code. var parts = Curve.subdivide(v1, tSplit); - Curve.getIntersections(parts[0], parts[1], c1, c1, locations, { - startConnected: param.startConnected, - // After splitting, the end is always connected: - endConnected: true, - // Since the curve was split above, we need to - // adjust the parameters for both locations. - renormalize: function(t1, t2) { - return [t1 * tSplit, t2 * (1 - tSplit) + tSplit]; - } - }); + // After splitting, the end is always connected: + param.endConnected = true; + // Since the curve was split above, we need to adjust the + // parameters for both locations. + param.renormalize = function(t1, t2) { + return [t1 * tSplit, t2 * (1 - tSplit) + tSplit]; + }; + Curve.getIntersections(parts[0], parts[1], c1, c1, + locations, param); } - } else { - // Avoid checking curves if completely out of control bounds. As - // a little optimization, we can scale the handles with 0.75 - // before calculating the control bounds and still be sure that - // the curve is fully contained. - var c1p1x = v1[0], c1p1y = v1[1], - c1p2x = v1[6], c1p2y = v1[7], - c2p1x = v2[0], c2p1y = v2[1], - c2p2x = v2[6], c2p2y = v2[7], - c1h1x = (3 * v1[2] + c1p1x) / 4, - c1h1y = (3 * v1[3] + c1p1y) / 4, - c1h2x = (3 * v1[4] + c1p2x) / 4, - c1h2y = (3 * v1[5] + c1p2y) / 4, - c2h1x = (3 * v2[2] + c2p1x) / 4, - c2h1y = (3 * v2[3] + c2p1y) / 4, - c2h2x = (3 * v2[4] + c2p2x) / 4, - c2h2y = (3 * v2[5] + c2p2y) / 4, - min = Math.min, - max = Math.max; - if (!( - max(c1p1x, c1h1x, c1h2x, c1p2x) >= - min(c2p1x, c2h1x, c2h2x, c2p2x) && - min(c1p1x, c1h1x, c1h2x, c1p2x) <= - max(c2p1x, c2h1x, c2h2x, c2p2x) && - max(c1p1y, c1h1y, c1h2y, c1p2y) >= - min(c2p1y, c2h1y, c2h2y, c2p2y) && - min(c1p1y, c1h1y, c1h2y, c1p2y) <= - max(c2p1y, c2h1y, c2h2y, c2p2y) - ) - // Also detect and handle overlaps: - || !param.startConnected && !param.endConnected - && addOverlap(v1, v2, c1, c2, locations, param)) - return locations; - var straight1 = Curve.isStraight(v1), - straight2 = Curve.isStraight(v2), - c1p1 = new Point(c1p1x, c1p1y), - c1p2 = new Point(c1p2x, c1p2y), - c2p1 = new Point(c2p1x, c2p1y), - c2p2 = new Point(c2p2x, c2p2y), - // NOTE: Use smaller Numerical.EPSILON to compare beginnings and - // end points to avoid matching them on almost collinear lines. - epsilon = /*#=*/Numerical.EPSILON; - // Handle the special case where the first curve's stat-point - // overlaps with the second curve's start- or end-points. - if (c1p1.isClose(c2p1, epsilon)) - addLocation(locations, param, v1, c1, 0, c1p1, v2, c2, 0, c2p1); - if (!param.startConnected && c1p1.isClose(c2p2, epsilon)) - addLocation(locations, param, v1, c1, 0, c1p1, v2, c2, 1, c2p2); - // Determine the correct intersection method based on whether one or - // curves are straight lines: - (straight1 && straight2 - ? addLineIntersection - : straight1 || straight2 - ? addCurveLineIntersections - : addCurveIntersections)( - v1, v2, c1, c2, locations, param, - // Define the defaults for these parameters of - // addCurveIntersections(): - // tMin, tMax, uMin, uMax, oldTDiff, reverse, recursion - 0, 1, 0, 1, 0, false, 0); - // Handle the special case where the first curve's end-point - // overlaps with the second curve's start- or end-points. - if (!param.endConnected && c1p2.isClose(c2p1, epsilon)) - addLocation(locations, param, v1, c1, 1, c1p2, v2, c2, 0, c2p1); - if (c1p2.isClose(c2p2, epsilon)) - addLocation(locations, param, v1, c1, 1, c1p2, v2, c2, 1, c2p2); + // We're done handling self-intersection, let's jump out. + return locations; } + // Avoid checking curves if completely out of control bounds. As + // a little optimization, we can scale the handles with 0.75 + // before calculating the control bounds and still be sure that + // the curve is fully contained. + var c2p1x = v2[0], c2p1y = v2[1], + c2p2x = v2[6], c2p2y = v2[7], + // 's' stands for scaled handles... + c1s1x = (3 * c1h1x + c1p1x) / 4, + c1s1y = (3 * c1h1y + c1p1y) / 4, + c1s2x = (3 * c1h2x + c1p2x) / 4, + c1s2y = (3 * c1h2y + c1p2y) / 4, + c2s1x = (3 * v2[2] + c2p1x) / 4, + c2s1y = (3 * v2[3] + c2p1y) / 4, + c2s2x = (3 * v2[4] + c2p2x) / 4, + c2s2y = (3 * v2[5] + c2p2y) / 4, + min = Math.min, + max = Math.max; + if (!( + max(c1p1x, c1s1x, c1s2x, c1p2x) >= + min(c2p1x, c2s1x, c2s2x, c2p2x) && + min(c1p1x, c1s1x, c1s2x, c1p2x) <= + max(c2p1x, c2s1x, c2s2x, c2p2x) && + max(c1p1y, c1s1y, c1s2y, c1p2y) >= + min(c2p1y, c2s1y, c2s2y, c2p2y) && + min(c1p1y, c1s1y, c1s2y, c1p2y) <= + max(c2p1y, c2s1y, c2s2y, c2p2y) + ) + // Also detect and handle overlaps: + || !param.startConnected && !param.endConnected + && addOverlap(v1, v2, c1, c2, locations, param)) + return locations; + var straight1 = Curve.isStraight(v1), + straight2 = Curve.isStraight(v2), + c1p1 = new Point(c1p1x, c1p1y), + c1p2 = new Point(c1p2x, c1p2y), + c2p1 = new Point(c2p1x, c2p1y), + c2p2 = new Point(c2p2x, c2p2y), + // NOTE: Use smaller Numerical.EPSILON to compare beginnings and + // end points to avoid matching them on almost collinear lines. + epsilon = /*#=*/Numerical.EPSILON; + // Handle the special case where the first curve's stat-point + // overlaps with the second curve's start- or end-points. + if (c1p1.isClose(c2p1, epsilon)) + addLocation(locations, param, v1, c1, 0, c1p1, v2, c2, 0, c2p1); + if (!param.startConnected && c1p1.isClose(c2p2, epsilon)) + addLocation(locations, param, v1, c1, 0, c1p1, v2, c2, 1, c2p2); + // Determine the correct intersection method based on whether one or + // curves are straight lines: + (straight1 && straight2 + ? addLineIntersection + : straight1 || straight2 + ? addCurveLineIntersections + : addCurveIntersections)( + v1, v2, c1, c2, locations, param, + // Define the defaults for these parameters of + // addCurveIntersections(): + // tMin, tMax, uMin, uMax, oldTDiff, reverse, recursion + 0, 1, 0, 1, 0, false, 0); + // Handle the special case where the first curve's end-point + // overlaps with the second curve's start- or end-points. + if (!param.endConnected && c1p2.isClose(c2p1, epsilon)) + addLocation(locations, param, v1, c1, 1, c1p2, v2, c2, 0, c2p1); + if (c1p2.isClose(c2p2, epsilon)) + addLocation(locations, param, v1, c1, 1, c1p2, v2, c2, 1, c2p2); return locations; } }}; From 9bcf369e6a153dc19aac145a0089c0a4fec3e949 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 30 Sep 2015 13:44:51 -0500 Subject: [PATCH 150/280] Ony split potentially self-intersecting curves if there are actual canditates. --- src/path/Curve.js | 42 ++++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index af0d678a..3dba4213 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1809,28 +1809,30 @@ new function() { // Scope for intersection using bezier fat-line clipping 2 * (bx * bx + by * by) + ax * cx + ay * cy, bx * cx + by * cy, roots, 0, 1); - // Select extremum with highest curvature. This is always on - // the loop in case of a self intersection. - for (var i = 0, maxCurvature = 0; i < count; i++) { - var curvature = Math.abs( - c1.getCurvatureAt(roots[i], true)); - if (curvature > maxCurvature) { - maxCurvature = curvature; - tSplit = roots[i]; + if (count > 0) { + // Select extremum with highest curvature. This is + // always on the loop in case of a self intersection. + for (var i = 0, maxCurvature = 0; i < count; i++) { + var curvature = Math.abs( + c1.getCurvatureAt(roots[i], true)); + if (curvature > maxCurvature) { + maxCurvature = curvature; + tSplit = roots[i]; + } } + // Divide the curve in two and then apply the normal + // curve intersection code. + var parts = Curve.subdivide(v1, tSplit); + // After splitting, the end is always connected: + param.endConnected = true; + // Since the curve was split above, we need to adjust + // the parameters for both locations. + param.renormalize = function(t1, t2) { + return [t1 * tSplit, t2 * (1 - tSplit) + tSplit]; + }; + Curve.getIntersections(parts[0], parts[1], c1, c1, + locations, param); } - // Divide the curve in two and then apply the normal curve - // intersection code. - var parts = Curve.subdivide(v1, tSplit); - // After splitting, the end is always connected: - param.endConnected = true; - // Since the curve was split above, we need to adjust the - // parameters for both locations. - param.renormalize = function(t1, t2) { - return [t1 * tSplit, t2 * (1 - tSplit) + tSplit]; - }; - Curve.getIntersections(parts[0], parts[1], c1, c1, - locations, param); } // We're done handling self-intersection, let's jump out. return locations; From d385d25a51d67821819a043ae97dc37bf3a66abb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 30 Sep 2015 13:48:28 -0500 Subject: [PATCH 151/280] Include comment regarding CurveLocation#equals() modification. --- src/path/CurveLocation.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index 5bfdc06a..0c157389 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -365,6 +365,9 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ || loc instanceof CurveLocation // Call getCurve() and getParameter() to keep in sync && this.getCurve() === loc.getCurve() + // NOTE: We need to compare both by proximity of points + // and by parameters, see: + // https://github.com/paperjs/paper.js/issues/784#issuecomment-143161586 && (this.getPoint().isClose(loc.getPoint(), /*#=*/Numerical.GEOMETRIC_EPSILON) || Math.abs(this.getParameter() - loc.getParameter()) From 5f706a4a5d1e36509a1e80776527e30449584c84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 30 Sep 2015 14:19:40 -0500 Subject: [PATCH 152/280] Use lower tolerance in bezier clipping code. This really should be Numerical.CURVETIME_EPSILON, but I get better results using Numerical.GEOMETRIC_EPSILON. Perhaps Numerical.CURVETIME_EPSILON / 2 is the right value to use though. --- src/path/Curve.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 3dba4213..02bbeffd 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1428,7 +1428,7 @@ new function() { // Scope for intersection using bezier fat-line clipping return; // Let P be the first curve and Q be the second var q0x = v2[0], q0y = v2[1], q3x = v2[6], q3y = v2[7], - epsilon = /*#=*/Numerical.CURVETIME_EPSILON, + epsilon = /*#=*/Numerical.GEOMETRIC_EPSILON, getSignedDistance = Line.getSignedDistance, // Calculate the fat-line L for Q is the baseline l and two // offsets which completely encloses the curve P. From 53ff973f06e593bbdcdc606d256b26b48f57d1ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Thu, 1 Oct 2015 03:38:35 -0500 Subject: [PATCH 153/280] Perform the faster check first. --- src/path/Curve.js | 4 ++-- src/path/CurveLocation.js | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 02bbeffd..f4bc01cb 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1874,8 +1874,8 @@ new function() { // Scope for intersection using bezier fat-line clipping c1p2 = new Point(c1p2x, c1p2y), c2p1 = new Point(c2p1x, c2p1y), c2p2 = new Point(c2p2x, c2p2y), - // NOTE: Use smaller Numerical.EPSILON to compare beginnings and - // end points to avoid matching them on almost collinear lines. + // NOTE: Use smaller Numerical.EPSILON to compare beginnings and + // end points to avoid matching them on almost collinear lines. epsilon = /*#=*/Numerical.EPSILON; // Handle the special case where the first curve's stat-point // overlaps with the second curve's start- or end-points. diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index 0c157389..09655b20 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -368,10 +368,10 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ // NOTE: We need to compare both by proximity of points // and by parameters, see: // https://github.com/paperjs/paper.js/issues/784#issuecomment-143161586 - && (this.getPoint().isClose(loc.getPoint(), - /*#=*/Numerical.GEOMETRIC_EPSILON) - || Math.abs(this.getParameter() - loc.getParameter()) - < /*#=*/Numerical.CURVETIME_EPSILON) + && (Math.abs(this.getParameter() - loc.getParameter()) + < /*#=*/Numerical.CURVETIME_EPSILON + || this.getPoint().isClose(loc.getPoint(), + /*#=*/Numerical.GEOMETRIC_EPSILON)) && (_ignoreOther || (!this._intersection && !loc._intersection || this._intersection && this._intersection.equals( From 75a004187e8f5db43236f08ed63d7b3492b87257 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Thu, 1 Oct 2015 04:38:48 -0500 Subject: [PATCH 154/280] Improve Line.intersect() to more reliably find interesctions at the beginnings / ends. Relates to #784 --- src/basic/Line.js | 21 ++++++++++++++++----- src/path/Curve.js | 4 ++-- src/util/Numerical.js | 2 +- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/basic/Line.js b/src/basic/Line.js index b8fae4ec..2aeb7aa3 100644 --- a/src/basic/Line.js +++ b/src/basic/Line.js @@ -133,14 +133,25 @@ var Line = Base.extend(/** @lends Line# */{ if (!Numerical.isZero(cross)) { var dx = p1x - p2x, dy = p1y - p2y, - t1 = (v2x * dy - v2y * dx) / cross, - t2 = (v1x * dy - v1y * dx) / cross; + u1 = (v2x * dy - v2y * dx) / cross, + u2 = (v1x * dy - v1y * dx) / cross, + // Compare the u values with EPSILON tolerance over the + // [0, 1] bounds. + uMin = -/*#=*/Numerical.EPSILON, + uMax = 1 + uMin; // Check the ranges of t parameters if the line is not allowed // to extend beyond the definition points. - if (isInfinite || 0 <= t1 && t1 <= 1 && 0 <= t2 && t2 <= 1) + if (isInfinite + || uMin < u1 && u1 < uMax && uMin < u2 && u2 < uMax) { + if (!isInfinite) { + // Address the tolerance at the bounds by clipping to + // the actual range. + u1 = u1 < 0 ? 0 : u1 > 1 ? 1 : u1; + } return new Point( - p1x + t1 * v1x, - p1y + t1 * v1y); + p1x + u1 * v1x, + p1y + u1 * v1y); + } } }, diff --git a/src/path/Curve.js b/src/path/Curve.js index f4bc01cb..f33926c0 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1379,11 +1379,11 @@ new function() { // Scope for intersection using bezier fat-line clipping // which connects back to the beginning, but only if it's not part of // a found overlap. The normal intersection will already be found at // the beginning, and would be added twice otherwise. - if (t1 >= (startConnected ? tMin : 0) && + if (t1 !== null && t1 >= (startConnected ? tMin : 0) && t1 <= (endConnected ? tMax : 1)) { if (t2 == null) t2 = Curve.getParameterOf(v2, p2.x, p2.y); - if (t2 >= (endConnected ? tMin : 0) && + if (t2 !== null && t2 >= (endConnected ? tMin : 0) && t2 <= (startConnected ? tMax : 1)) { // TODO: Don't we need to check the range of t2 as well? Does it // also need startConnected / endConnected values? diff --git a/src/util/Numerical.js b/src/util/Numerical.js index d1853cd5..1afbe575 100644 --- a/src/util/Numerical.js +++ b/src/util/Numerical.js @@ -112,7 +112,7 @@ var Numerical = new function() { KAPPA: 4 * (sqrt(2) - 1) / 3, /** - * Check if the value is 0, within a tolerance defined by + * Checks if the value is 0, within a tolerance defined by * Numerical.EPSILON. */ isZero: function(val) { From 0ca5a106de2035938ba4e7fbb4316b900f23718c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Thu, 1 Oct 2015 04:50:41 -0500 Subject: [PATCH 155/280] Improve Line.intersect() comments. --- src/basic/Line.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/basic/Line.js b/src/basic/Line.js index 2aeb7aa3..73fbf1b8 100644 --- a/src/basic/Line.js +++ b/src/basic/Line.js @@ -135,12 +135,11 @@ var Line = Base.extend(/** @lends Line# */{ dy = p1y - p2y, u1 = (v2x * dy - v2y * dx) / cross, u2 = (v1x * dy - v1y * dx) / cross, - // Compare the u values with EPSILON tolerance over the - // [0, 1] bounds. + // Check the ranges of the u parameters if the line is not + // allowed to extend beyond the definition points, but + // compare with EPSILON tolerance over the [0, 1] bounds. uMin = -/*#=*/Numerical.EPSILON, uMax = 1 + uMin; - // Check the ranges of t parameters if the line is not allowed - // to extend beyond the definition points. if (isInfinite || uMin < u1 && u1 < uMax && uMin < u2 && u2 < uMax) { if (!isInfinite) { From 53dd726057f894fc65a9698817b95d14c136e1b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Thu, 1 Oct 2015 05:55:22 -0500 Subject: [PATCH 156/280] Rename ignoreStraight argument to _setHandles --- src/path/Curve.js | 4 ++-- src/path/PathItem.Boolean.js | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index f33926c0..8e5e07a1 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -463,7 +463,7 @@ var Curve = Base.extend(/** @lends Curve# */{ * is within the valid range, {code null} otherwise. */ // TODO: Rename to divideAt()? - divide: function(offset, isParameter, ignoreStraight) { + divide: function(offset, isParameter, _setHandles) { var parameter = this._getParameter(offset, isParameter), tMin = /*#=*/Numerical.CURVETIME_EPSILON, tMax = 1 - tMin, @@ -471,7 +471,7 @@ var Curve = Base.extend(/** @lends Curve# */{ // Only divide if not at the beginning or end. if (parameter >= tMin && parameter <= tMax) { var parts = Curve.subdivide(this.getValues(), parameter), - setHandles = ignoreStraight || this.hasHandles(), + setHandles = _setHandles || this.hasHandles(), left = parts[0], right = parts[1]; diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 9cd124ed..49a55298 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -200,8 +200,9 @@ PathItem.inject(new function() { } else if (t > tMax) { segment = curve._segment2; } else { - // Split the curve at t, passing true for ignoreStraight to not - // force the result of splitting straight curves straight. + // Split the curve at t, passing true for _setHandles to always + // set the handles on the sub-curves even if the original curve + // had no handles. var newCurve = curve.divide(t, true, true); segment = newCurve._segment1; curve = newCurve.getPrevious(); From c77165be3ac5d564506eec66cc67480cb80be1f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Thu, 1 Oct 2015 06:21:17 -0500 Subject: [PATCH 157/280] Fix issue in Curve#divide() that lead to intersection segments being linked up wrongly. Relates to #784 --- src/path/Curve.js | 40 ++++++++++++++++-------------------- src/path/Path.js | 8 ++++---- src/path/PathItem.Boolean.js | 7 +++++++ 3 files changed, 29 insertions(+), 26 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 8e5e07a1..5a05ed46 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -471,36 +471,32 @@ var Curve = Base.extend(/** @lends Curve# */{ // Only divide if not at the beginning or end. if (parameter >= tMin && parameter <= tMax) { var parts = Curve.subdivide(this.getValues(), parameter), - setHandles = _setHandles || this.hasHandles(), left = parts[0], - right = parts[1]; - - // Write back the results: + right = parts[1], + setHandles = _setHandles || this.hasHandles(), + segment1 = this._segment1, + segment2 = this._segment2, + path = this._path; if (setHandles) { - this._segment1._handleOut.set(left[2] - left[0], - left[3] - left[1]); - // segment2 is the end segment. By inserting newSegment - // between segment1 and 2, 2 becomes the end segment. + // Adjust the handles on the existing segments. The new segment + // will be inserted between the existing segment1 and segment2: // Convert absolute -> relative - this._segment2._handleIn.set(right[4] - right[6], + segment1._handleOut.set(left[2] - left[0], + left[3] - left[1]); + segment2._handleIn.set(right[4] - right[6], right[5] - right[7]); } - - // Create the new segment, convert absolute -> relative: + // Create the new segment: var x = left[6], y = left[7], segment = new Segment(new Point(x, y), setHandles && new Point(left[4] - x, left[5] - y), setHandles && new Point(right[2] - x, right[3] - y)); - // Insert it in the segments list, if needed: - if (this._path) { - // Insert at the end if this curve is a closing curve of a - // closed path, since otherwise it would be inserted at 0. - if (this._segment1._index > 0 && this._segment2._index === 0) { - this._path.add(segment); - } else { - this._path.insert(this._segment2._index, segment); - } + if (path) { + // By inserting at segment1.index + 1, we make sure to insert at + // the end if this curve is a closing curve of a closed path, + // as with segment2.index it would be inserted at 0. + path.insert(segment1._index + 1, segment); // The way Path#_add handles curves, this curve will always // become the owner of the newly inserted segment. // TODO: I expect this.getNext() to produce the correct result, @@ -509,8 +505,8 @@ var Curve = Base.extend(/** @lends Curve# */{ res = this; // this.getNext(); } else { // otherwise create it from the result of split - var end = this._segment2; - this._segment2 = segment; + var end = segment2; + segment2 = segment; res = new Curve(segment, end); } } diff --git a/src/path/Path.js b/src/path/Path.js index fdc601a2..b8a07e1c 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -374,10 +374,10 @@ var Path = PathItem.extend(/** @lends Path# */{ }, /** - * Private method that adds a segment to the segment list. It assumes that - * the passed object is a segment already and does not perform any checks. - * If a curves list was requested, it will kept in sync with the segments - * list automatically. + * Private method that adds segments to the segment list. It assumes that + * the passed object is an array of segments already and does not perform + * any checks. If a curves list was requested, it will be kept in sync with + * the segments list automatically. */ _add: function(segs, index) { // Local short-cuts: diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 49a55298..91355696 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -471,6 +471,13 @@ PathItem.inject(new function() { text.pivot = text.globalToLocal(text.point); text.scale(scaleFactor); text.rotate(textAngle); + new Path.Line({ + from: text.point, + to: seg.point, + strokeColor: color, + strokeScaling: false + }); + return text; } function drawSegment(seg, other, text, index, color) { From b8c6eb46ade2560f34b5c44525b8f46861b5b85e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Thu, 1 Oct 2015 06:52:08 -0500 Subject: [PATCH 158/280] Fix weirdness of Curve#divide() modifying the wrong Curve object. --- src/path/Curve.js | 8 ++------ src/path/Path.js | 17 ++++++----------- src/path/PathItem.Boolean.js | 12 ++++-------- 3 files changed, 12 insertions(+), 25 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 5a05ed46..659465b3 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -497,12 +497,8 @@ var Curve = Base.extend(/** @lends Curve# */{ // the end if this curve is a closing curve of a closed path, // as with segment2.index it would be inserted at 0. path.insert(segment1._index + 1, segment); - // The way Path#_add handles curves, this curve will always - // become the owner of the newly inserted segment. - // TODO: I expect this.getNext() to produce the correct result, - // but since we're inserting differently in _add (something - // linked with CurveLocation#divide()), this is not the case... - res = this; // this.getNext(); + // The newly inserted segment is the start of the next curve: + res = this.getNext(); } else { // otherwise create it from the result of split var end = segment2; diff --git a/src/path/Path.js b/src/path/Path.js index b8a07e1c..c948d1c0 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -385,7 +385,7 @@ var Path = PathItem.extend(/** @lends Path# */{ curves = this._curves, amount = segs.length, append = index == null, - index = append ? segments.length : index; + from = append ? segments.length : index; // Scan through segments to add first, convert if necessary and set // _path and _index references on them. for (var i = 0; i < amount; i++) { @@ -395,7 +395,7 @@ var Path = PathItem.extend(/** @lends Path# */{ if (segment._path) segment = segs[i] = segment.clone(); segment._path = this; - segment._index = index + i; + segment._index = from + i; // If parts of this segment are selected, adjust the internal // _selectedSegmentState now if (segment._selectionState) @@ -406,20 +406,15 @@ var Path = PathItem.extend(/** @lends Path# */{ segments.push.apply(segments, segs); } else { // Insert somewhere else - segments.splice.apply(segments, [index, 0].concat(segs)); + segments.splice.apply(segments, [from, 0].concat(segs)); // Adjust the indices of the segments above. - for (var i = index + amount, l = segments.length; i < l; i++) + for (var i = from + amount, l = segments.length; i < l; i++) segments[i]._index = i; } // Keep the curves list in sync all the time in case it was requested // already. - if (curves || segs._curves) { - if (!curves) - curves = this._curves = []; - // We need to step one index down from the inserted segment to - // get its curve, except for the first segment. - var from = index > 0 ? index - 1 : index, - start = from, + if (curves) { + var start = from, to = Math.min(from + amount, this._countCurves()); if (segs._curves) { // Reuse removed curves. diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 91355696..4e18f649 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -203,15 +203,11 @@ PathItem.inject(new function() { // Split the curve at t, passing true for _setHandles to always // set the handles on the sub-curves even if the original curve // had no handles. - var newCurve = curve.divide(t, true, true); - segment = newCurve._segment1; - curve = newCurve.getPrevious(); + segment = curve.divide(t, true, true)._segment1; // Keep track of segments of once straight curves, so they can // be set back straight at the end. if (noHandles) clearSegments.push(segment); - // TODO: Figure out the right value for t - t = 0; // Since it's split (might be 1 also?) } // Link the new segment with the intersection on the other curve var inter = segment._intersection; @@ -240,10 +236,10 @@ PathItem.inject(new function() { } else { segment._intersection = loc._intersection; } - // TODO: Figure out why setCurves doesn't work: - // loc._setCurve(segment.getCurve()); + // TODO: Move setting of these values to CurveLocation loc._segment = segment; - loc._parameter = t; + loc._parameter = segment === curve._segment1 ? 0 : 1; + loc._version = segment._path._version; prev = loc; prevT = locT; } From fee3a9032905022ab76a482177cd2a34231a210c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Thu, 1 Oct 2015 07:05:00 -0500 Subject: [PATCH 159/280] Fixed leaked globals. --- src/path/Curve.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 659465b3..097cd473 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1643,7 +1643,7 @@ new function() { // Scope for intersection using bezier fat-line clipping pc = Curve.getPoint(vc, tc), tl = Curve.getParameterOf(vl, pc.x, pc.y); if (tl !== null) { - var pl = Curve.getPoint(vl, tl) + var pl = Curve.getPoint(vl, tl), t1 = flip ? tl : tc, t2 = flip ? tc : tl; // If the two curves are connected and the 2nd is very short, From 72f97056154733e163b4c8b44db79982fd4d27a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Thu, 1 Oct 2015 08:49:26 -0500 Subject: [PATCH 160/280] Fix overeager refactoring in c77165be3ac5d564506eec66cc67480cb80be1f0 --- src/path/Curve.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 097cd473..984ce440 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -501,9 +501,8 @@ var Curve = Base.extend(/** @lends Curve# */{ res = this.getNext(); } else { // otherwise create it from the result of split - var end = segment2; - segment2 = segment; - res = new Curve(segment, end); + this._segment2 = segment; + res = new Curve(segment, segment2); } } return res; From 8aca088bf6b9e9223fa1e32702bda234b32bcf87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Thu, 1 Oct 2015 09:41:57 -0500 Subject: [PATCH 161/280] Clean-up splitPath() code a bit. --- src/path/PathItem.Boolean.js | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 4e18f649..e6d36b4e 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -177,22 +177,21 @@ PathItem.inject(new function() { tMax = 1 - tMin, noHandles = false, clearSegments = [], - curve, - prev, + prevCurve, prevT; for (var i = locations.length - 1; i >= 0; i--) { var loc = locations[i], + curve = loc._curve, t = loc._parameter, - locT = t; - // Check if we are splitting same curve multiple times, but avoid - // dividing with zero. - if (prev && prev._curve === loc._curve && prevT > 0) { - // Scale parameter after previous split. - t /= prevT; - } else { - curve = loc._curve; + origT = t; + if (curve !== prevCurve) { + // This is a new curve, update noHandles setting. noHandles = !curve.hasHandles(); + } else if (prevT > 0) { + // Scale parameter when we are splitting same curve multiple + // times, but avoid dividing by zero. + t /= prevT; } var segment; if (t < tMin) { @@ -204,8 +203,8 @@ PathItem.inject(new function() { // set the handles on the sub-curves even if the original curve // had no handles. segment = curve.divide(t, true, true)._segment1; - // Keep track of segments of once straight curves, so they can - // be set back straight at the end. + // Keep track of segments of curves without handles, so they can + // be cleared again at the end. if (noHandles) clearSegments.push(segment); } @@ -240,8 +239,8 @@ PathItem.inject(new function() { loc._segment = segment; loc._parameter = segment === curve._segment1 ? 0 : 1; loc._version = segment._path._version; - prev = loc; - prevT = locT; + prevCurve = curve; + prevT = origT; } // Clear segment handles if they were part of a curve with no handles, // once we are done with the entire curve. From f5012a78e9586c3ddcade0f4a2a75f66c4a8f562 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Thu, 1 Oct 2015 20:24:47 -0500 Subject: [PATCH 162/280] Reformat nested ternary operators again. This got messed up in a recent refactoring. --- src/path/Curve.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 984ce440..3aa4ea7f 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1879,13 +1879,13 @@ new function() { // Scope for intersection using bezier fat-line clipping (straight1 && straight2 ? addLineIntersection : straight1 || straight2 - ? addCurveLineIntersections - : addCurveIntersections)( - v1, v2, c1, c2, locations, param, - // Define the defaults for these parameters of - // addCurveIntersections(): - // tMin, tMax, uMin, uMax, oldTDiff, reverse, recursion - 0, 1, 0, 1, 0, false, 0); + ? addCurveLineIntersections + : addCurveIntersections)( + v1, v2, c1, c2, locations, param, + // Define the defaults for these parameters of + // addCurveIntersections(): + // tMin, tMax, uMin, uMax, oldTDiff, reverse, recursion + 0, 1, 0, 1, 0, false, 0); // Handle the special case where the first curve's end-point // overlaps with the second curve's start- or end-points. if (!param.endConnected && c1p2.isClose(c2p1, epsilon)) From 9b883e5fb6e3a39df2dbd1ef097277c6ea089e1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Thu, 1 Oct 2015 20:44:27 -0500 Subject: [PATCH 163/280] Introduce new TRIGONOMETRIC_EPSILON with higher precision than GEOMETRIC_EPSILON. --- src/basic/Point.js | 4 ++-- src/path/Curve.js | 4 ++-- src/util/Numerical.js | 6 ++++++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/basic/Point.js b/src/basic/Point.js index d24ca33c..51819713 100644 --- a/src/basic/Point.js +++ b/src/basic/Point.js @@ -708,7 +708,7 @@ var Point = Base.extend(/** @lends Point# */{ // TODO: Optimize by creating a static Point.isCollinear() to be used // in Line.isCollinear() as well. return Math.abs(this.normalize().cross(point.normalize())) - < /*#=*/Numerical.GEOMETRIC_EPSILON; + < /*#=*/Numerical.TRIGONOMETRIC_EPSILON; }, // TODO: Remove version with typo after a while (deprecated June 2015) @@ -727,7 +727,7 @@ var Point = Base.extend(/** @lends Point# */{ // length. // TODO: Optimize return Math.abs(this.normalize().dot(point.normalize())) - < /*#=*/Numerical.GEOMETRIC_EPSILON; + < /*#=*/Numerical.TRIGONOMETRIC_EPSILON; }, /** diff --git a/src/path/Curve.js b/src/path/Curve.js index 3aa4ea7f..d4d5fc27 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -916,7 +916,7 @@ statics: { */ isHorizontal: function() { return this.isStraight() && Math.abs(this.getTangentAt(0.5, true).y) - < /*#=*/Numerical.GEOMETRIC_EPSILON; + < /*#=*/Numerical.TRIGONOMETRIC_EPSILON; }, /** @@ -926,7 +926,7 @@ statics: { */ isVertical: function() { return this.isStraight() && Math.abs(this.getTangentAt(0.5, true).x) - < /*#=*/Numerical.GEOMETRIC_EPSILON; + < /*#=*/Numerical.TRIGONOMETRIC_EPSILON; } }), /** @lends Curve# */{ // Explicitly deactivate the creation of beans, as we have functions here diff --git a/src/util/Numerical.js b/src/util/Numerical.js index 1afbe575..ca15f5c4 100644 --- a/src/util/Numerical.js +++ b/src/util/Numerical.js @@ -94,6 +94,12 @@ var Numerical = new function() { * trial and error. */ GEOMETRIC_EPSILON: 1e-7, + /** + * The epsilon to be used when performing "trigonometric" checks, such + * as examining cross products to check for collinearity. This value is + * somewhat arbitrary and was chosen by trial and error. + */ + TRIGONOMETRIC_EPSILON: 1e-8, /** * MACHINE_EPSILON for a double precision (Javascript Number) is * 2.220446049250313e-16. (try this in the js console) From 4b4ccbac09707c6b93cbb2c4fecf4a488a3df9bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Thu, 1 Oct 2015 20:45:08 -0500 Subject: [PATCH 164/280] Make GEOMETRIC_EPSILON so that overlap edge-cases are correctly matched. Relates to #784 --- src/util/Numerical.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/Numerical.js b/src/util/Numerical.js index ca15f5c4..b6cdbc8c 100644 --- a/src/util/Numerical.js +++ b/src/util/Numerical.js @@ -93,7 +93,7 @@ var Numerical = new function() { * collinearity. This value is somewhat arbitrary and was chosen by * trial and error. */ - GEOMETRIC_EPSILON: 1e-7, + GEOMETRIC_EPSILON: 1e-6, /** * The epsilon to be used when performing "trigonometric" checks, such * as examining cross products to check for collinearity. This value is From c0bb6890bde71837bdefbdb77d62b7bc5cfdbf01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Thu, 1 Oct 2015 20:47:56 -0500 Subject: [PATCH 165/280] Switch back to CURVETIME_EPSILON now that overlap edge-case appears to be handled. --- src/path/Curve.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index d4d5fc27..d6b37f1a 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1419,7 +1419,7 @@ new function() { // Scope for intersection using bezier fat-line clipping return; // Let P be the first curve and Q be the second var q0x = v2[0], q0y = v2[1], q3x = v2[6], q3y = v2[7], - epsilon = /*#=*/Numerical.GEOMETRIC_EPSILON, + epsilon = /*#=*/Numerical.CURVETIME_EPSILON, getSignedDistance = Line.getSignedDistance, // Calculate the fat-line L for Q is the baseline l and two // offsets which completely encloses the curve P. From de57a7fbc8891561c5435b4433f6fafc8bce1ccf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Thu, 1 Oct 2015 21:09:30 -0500 Subject: [PATCH 166/280] Simplify tracePaths() code. --- src/path/PathItem.Boolean.js | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index e6d36b4e..d8cdc6e9 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -607,8 +607,7 @@ PathItem.inject(new function() { } for (var i = 0, l = segments.length; i < l; i++) { var seg = segments[i], - path = null, - added = false; // Whether a first segment as added already + path = null; // Do not start a chain with already visited segments, and segments // that are not going to be part of the resulting operation. if (seg._visited || !isValid(seg)) @@ -639,7 +638,7 @@ PathItem.inject(new function() { + ', other: ' + inter._segment._path._id + '.' + inter._segment._index); } - if (added && (seg === start || seg === otherStart)) { + if (seg === start || seg === otherStart) { // We've come back to the start, bail out as we're done. drawSegment(seg, null, 'done', i, 'red'); break; @@ -659,13 +658,8 @@ PathItem.inject(new function() { (path ? path._segments.length + 1 : 1)); break; } - if (!added) { - path = new Path(Item.NO_INSERT); - start = seg; - otherStart = other; - } - var handleIn = added && seg._handleIn; - if (!added || !other || other === start) { + var handleIn = path && seg._handleIn; + if (!path || !other || other === start) { // TODO: Is (other === start) check really required? // Does that ever occur? // Just add the first segment and all segments that have no @@ -712,13 +706,18 @@ PathItem.inject(new function() { + ', multiple: ' + (!!inter._next)); break; } + if (!path) { + path = new Path(Item.NO_INSERT); + start = seg; + otherStart = other; + } // Add the current segment to the path, and mark the added // segment as visited. path.add(new Segment(seg._point, handleIn, seg._handleOut)); - seg._visited = added = true; + seg._visited = true; seg = seg.getNext(); } - if (!path || !added) + if (!path) continue; // Finish with closing the paths if necessary, correctly linking up // curves etc. @@ -746,8 +745,10 @@ PathItem.inject(new function() { // As an optimization, only check paths with 4 or less segments // for their area, and assume that they cover an area when more. if (path && (path._segments.length > 4 - || !Numerical.isZero(path.getArea()))) + || !Numerical.isZero(path.getArea()))) { paths.push(path); + path = null; + } pathCount++; } return paths; From a808aaf0fa0b4f7809b33f40544f8c31b1645f34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Thu, 1 Oct 2015 21:11:51 -0500 Subject: [PATCH 167/280] Remove unnecessary check for other === start. --- src/path/PathItem.Boolean.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index d8cdc6e9..9ba56009 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -650,8 +650,6 @@ PathItem.inject(new function() { // Intersections are always part of the resulting path, for // all other segments check the winding contribution to see // if they are to be kept. If not, the chain has to end here - // TODO: We really should find a way to go backwards perhaps - // and try another path when this happens? drawSegment(seg, null, 'discard', i, 'red'); console.error('Excluded segment encountered, aborting #' + pathCount + '.' + @@ -659,9 +657,7 @@ PathItem.inject(new function() { break; } var handleIn = path && seg._handleIn; - if (!path || !other || other === start) { - // TODO: Is (other === start) check really required? - // Does that ever occur? + if (!path || !other) { // Just add the first segment and all segments that have no // intersection. drawSegment(seg, null, 'add', i, 'black'); From 8dfa721e5aed384a532cc7968ba355f345d00feb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Thu, 1 Oct 2015 21:12:15 -0500 Subject: [PATCH 168/280] Adjust debug rendering. --- src/path/PathItem.Boolean.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 9ba56009..c5d9eba7 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -71,8 +71,8 @@ PathItem.inject(new function() { return result; } - var scaleFactor = 0.25; // 1 / 3000; - var textAngle = 33; + var scaleFactor = 0.1; // 1 / 3000; + var textAngle = -30; var fontSize = 5; var segmentOffset; @@ -448,8 +448,8 @@ PathItem.inject(new function() { function labelSegment(seg, text, color) { var point = seg.point; - var key = Math.round(point.x / (4 * scaleFactor)) - + ',' + Math.round(point.y / (4 * scaleFactor)); + var key = Math.round(point.x / (10 * scaleFactor)) + + ',' + Math.round(point.y / (10 * scaleFactor)); var offset = segmentOffset[key] || 0; segmentOffset[key] = offset + 1; var size = fontSize * scaleFactor; From 7f7d35a38afbf20d32333d4b07e891028c04f087 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Fri, 2 Oct 2015 01:05:45 -0500 Subject: [PATCH 169/280] Clean-up epsilon definitions. --- src/util/Numerical.js | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/util/Numerical.js b/src/util/Numerical.js index b6cdbc8c..7f4ab847 100644 --- a/src/util/Numerical.js +++ b/src/util/Numerical.js @@ -60,7 +60,6 @@ var Numerical = new function() { var abs = Math.abs, sqrt = Math.sqrt, pow = Math.pow, - TOLERANCE = 1e-6, EPSILON = 1e-12, MACHINE_EPSILON = 1.12e-16; @@ -69,7 +68,7 @@ var Numerical = new function() { } return /** @lends Numerical */{ - TOLERANCE: TOLERANCE, + TOLERANCE: 1e-6, // Precision when comparing against 0 // References: // http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html @@ -81,6 +80,20 @@ var Numerical = new function() { * range (see MACHINE_EPSILON). */ EPSILON: EPSILON, + /** + * The machine epsilon for a double precision (Javascript Number) is + * 2.220446049250313e-16. (try this in the js console: + * (function(){for(var e=1;1<1+e/2;)e/=2;return e}()) + * + * The constant MACHINE_EPSILON here refers to the constants δ and ε + * such that, the error introduced by addition, multiplication on a + * 64bit float (js Number) will be less than δ and ε. That is to say, + * for all X and Y representable by a js Number object, S and P be their + * 'exact' sum and product respectively, then + * |S - (x+y)| <= δ|S|, and |P - (x*y)| <= ε|P|. + * This amounts to about half of the actual machine epsilon. + */ + MACHINE_EPSILON: MACHINE_EPSILON, /** * The epsilon to be used when handling curve-time parameters. This * cannot be smaller, because errors add up to about 1e-7 in the bezier @@ -100,20 +113,6 @@ var Numerical = new function() { * somewhat arbitrary and was chosen by trial and error. */ TRIGONOMETRIC_EPSILON: 1e-8, - /** - * MACHINE_EPSILON for a double precision (Javascript Number) is - * 2.220446049250313e-16. (try this in the js console) - * (function(){for(var e=1;1<1+e/2;)e/=2;return e}()) - * - * Here the constant MACHINE_EPSILON refers to the constants 'δ' and 'ε' - * such that, the error introduced by addition, multiplication - * on a 64bit float (js Number) will be less than δ and ε. That is to - * say, for all X and Y representable by a js Number object, S and P - * be their 'exact' sum and product respectively, then - * |S - (x+y)| <= δ|S|, and |P - (x*y)| <= ε|P|. - * This amounts to about half of the actual MACHINE_EPSILON - */ - MACHINE_EPSILON: MACHINE_EPSILON, // Kappa, see: http://www.whizkidtech.redprince.net/bezier/circle/kappa/ KAPPA: 4 * (sqrt(2) - 1) / 3, From 11611c8fe2aa2001661c4ed43105d94c15905c7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Fri, 2 Oct 2015 01:06:36 -0500 Subject: [PATCH 170/280] Remove isValid() check for current segment before attempting the switch. This properly fixes example 14 in #784. --- src/path/PathItem.Boolean.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index c5d9eba7..67df1943 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -680,10 +680,6 @@ PathItem.inject(new function() { // switch at each crossing. drawSegment(seg, other, 'exclude-cross', i, 'green'); seg = other; - } else if (isValid(seg)) { - // Do not switch to the intersecting segment as this segment - // is part of the the boolean result. - drawSegment(seg, null, 'keep', i, 'black'); } else if (isValid(other)) { // The other segment is part of the boolean result, and we // are at crossing, switch over. From c6de2f7f231ef5b9fbada4c372586e2fa6b99ce0 Mon Sep 17 00:00:00 2001 From: sapics Date: Fri, 2 Oct 2015 17:03:36 +0900 Subject: [PATCH 171/280] Fix to minimize floating point noise --- src/util/Numerical.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/util/Numerical.js b/src/util/Numerical.js index 7f4ab847..6593dc74 100644 --- a/src/util/Numerical.js +++ b/src/util/Numerical.js @@ -231,10 +231,9 @@ var Numerical = new function() { } else { // No real roots if D < 0 if (D >= -MACHINE_EPSILON) { - D = D < 0 ? 0 : D; - var R = sqrt(D); - // Try to minimise floating point noise. - if (b >= MACHINE_EPSILON && b <= MACHINE_EPSILON) { + var R = sqrt(D < 0 ? 0 : D); + // Try to minimize floating point noise. + if (b >= -MACHINE_EPSILON && b <= MACHINE_EPSILON) { x1 = abs(a) >= abs(c) ? R / a : -c / R; x2 = -x1; } else { From f6f9d963ebb7d746a979f08819d7ca7ab137963f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Fri, 2 Oct 2015 15:46:15 -0500 Subject: [PATCH 172/280] Shorten Numerical.solveQuadratic() a bit. --- src/util/Numerical.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/util/Numerical.js b/src/util/Numerical.js index 6593dc74..617c3415 100644 --- a/src/util/Numerical.js +++ b/src/util/Numerical.js @@ -231,18 +231,16 @@ var Numerical = new function() { } else { // No real roots if D < 0 if (D >= -MACHINE_EPSILON) { - var R = sqrt(D < 0 ? 0 : D); + var R = D < 0 ? 0 : sqrt(D); // Try to minimize floating point noise. - if (b >= -MACHINE_EPSILON && b <= MACHINE_EPSILON) { + if (abs(b) < MACHINE_EPSILON) { x1 = abs(a) >= abs(c) ? R / a : -c / R; x2 = -x1; } else { - var q = -(b + (b < 0 ? -1 : 1) * R); + var q = (b < 0 ? R : -R) - b; x1 = q / a; x2 = c / q; } - // Do we actually have two real roots? - // count = D > MACHINE_EPSILON ? 2 : 1; } } // We need to include EPSILON in the comparisons with min / max, From 00f1d5089fe31b95748efefa3a6f0c3b300e4327 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Fri, 2 Oct 2015 18:56:41 -0500 Subject: [PATCH 173/280] Clean up Numerical code a bit. --- src/util/Numerical.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/util/Numerical.js b/src/util/Numerical.js index 617c3415..5f5a2d5e 100644 --- a/src/util/Numerical.js +++ b/src/util/Numerical.js @@ -121,7 +121,7 @@ var Numerical = new function() { * Numerical.EPSILON. */ isZero: function(val) { - return abs(val) <= EPSILON; + return val >= -EPSILON && val <= EPSILON; }, /** @@ -180,7 +180,7 @@ var Numerical = new function() { * * References: * Kahan W. - "To Solve a Real Cubic Equation" - * http://www.cs.berkeley.edu/~wkahan/Math128/Cubic.pdf + * http://www.cs.berkeley.edu/~wkahan/Math128/Cubic.pdf * Blinn J. - "How to solve a Quadratic Equation" * * @param {Number} a the quadratic term @@ -212,8 +212,8 @@ var Numerical = new function() { // We multiply with a factor to normalize the coefficients. // The factor is just the nearest exponent of 10, big enough // to raise all the coefficients to nearly [-1, +1] range. - var mult = pow(10, abs( - Math.floor(Math.log(gmC) * Math.LOG10E))); + var mult = pow(10, + abs(Math.floor(Math.log(gmC) * Math.LOG10E))); if (!isFinite(mult)) mult = 0; a *= mult; From 632eb25f19fbbe5b1aeb607b1badab181a5bdbb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Fri, 2 Oct 2015 18:57:45 -0500 Subject: [PATCH 174/280] Bring back code removed in 11611c8fe2aa2001661c4ed43105d94c15905c7d again. But add a _visited check, to get best of both approaches. --- src/path/PathItem.Boolean.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 67df1943..b64c24a2 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -680,7 +680,11 @@ PathItem.inject(new function() { // switch at each crossing. drawSegment(seg, other, 'exclude-cross', i, 'green'); seg = other; - } else if (isValid(other)) { + } else if (!seg._visited && isValid(seg)) { + // Do not switch to the intersecting segment as this segment + // is part of the the boolean result. + drawSegment(seg, null, 'keep', i, 'black'); + } else if (!other._visited && isValid(other)) { // The other segment is part of the boolean result, and we // are at crossing, switch over. drawSegment(seg, other, 'cross', i, 'green'); From 7496a7c9e25788fe0da91e9ab171f86ef74ae6e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Fri, 2 Oct 2015 19:00:32 -0500 Subject: [PATCH 175/280] Try linked up intersections first before switching to the other intersecetion. --- src/path/PathItem.Boolean.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index b64c24a2..91ec39d5 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -591,19 +591,19 @@ PathItem.inject(new function() { || (!strict || isValid(seg, true)) && isValid(next, !strict && inter._overlap)) ? inter - // If it's no match, check the other intersection first, then - // carry on with the next linked intersection. - : !ignoreOther - // We need to get the intersection on the segment, not - // on inter, since multiple solutions are only linked up - // as a chain through _next there. But do not check that - // intersection in the first call to getIntersection() - // (prev == null), since we'd go straight back to the - // originating segment. - && (prev || seg._intersection !== inter._intersection) - && getIntersection(strict, seg._intersection, inter, true) - || inter._next !== prev // Prevent circular loops - && getIntersection(strict, inter._next, inter, false); + // If it's no match, check the next linked intersection first, + // otherwise carry on with the 'other' intersection location. + : inter._next !== prev // Prevent circular loops + && getIntersection(strict, inter._next, inter, false) + // We need to get the intersection on the segment, not + // on inter, since multiple solutions are only linked up + // as a chain through _next there. But do not check that + // intersection in the first call to getIntersection() + // (prev == null), since we'd go straight back to the + // originating segment. + || !ignoreOther + && (prev || seg._intersection !== inter._intersection) + && getIntersection(strict, seg._intersection, inter, true); } for (var i = 0, l = segments.length; i < l; i++) { var seg = segments[i], From 2167e458ae646f1131bc419e1f8f008dfad5faa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sat, 3 Oct 2015 10:38:45 -0500 Subject: [PATCH 176/280] Since we're using sorting now, we can add all start- / end-point intersections before finding the ones within the curves. --- src/path/Curve.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index d6b37f1a..4dbc0f91 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1868,12 +1868,16 @@ new function() { // Scope for intersection using bezier fat-line clipping // NOTE: Use smaller Numerical.EPSILON to compare beginnings and // end points to avoid matching them on almost collinear lines. epsilon = /*#=*/Numerical.EPSILON; - // Handle the special case where the first curve's stat-point - // overlaps with the second curve's start- or end-points. + // Handle the special case where the first curve's start- or end- + // point overlap with the second curve's start- or end-point. if (c1p1.isClose(c2p1, epsilon)) addLocation(locations, param, v1, c1, 0, c1p1, v2, c2, 0, c2p1); if (!param.startConnected && c1p1.isClose(c2p2, epsilon)) addLocation(locations, param, v1, c1, 0, c1p1, v2, c2, 1, c2p2); + if (!param.endConnected && c1p2.isClose(c2p1, epsilon)) + addLocation(locations, param, v1, c1, 1, c1p2, v2, c2, 0, c2p1); + if (c1p2.isClose(c2p2, epsilon)) + addLocation(locations, param, v1, c1, 1, c1p2, v2, c2, 1, c2p2); // Determine the correct intersection method based on whether one or // curves are straight lines: (straight1 && straight2 @@ -1886,12 +1890,6 @@ new function() { // Scope for intersection using bezier fat-line clipping // addCurveIntersections(): // tMin, tMax, uMin, uMax, oldTDiff, reverse, recursion 0, 1, 0, 1, 0, false, 0); - // Handle the special case where the first curve's end-point - // overlaps with the second curve's start- or end-points. - if (!param.endConnected && c1p2.isClose(c2p1, epsilon)) - addLocation(locations, param, v1, c1, 1, c1p2, v2, c2, 0, c2p1); - if (c1p2.isClose(c2p2, epsilon)) - addLocation(locations, param, v1, c1, 1, c1p2, v2, c2, 1, c2p2); return locations; } }}; From 50c74733379d67e52b404aabcee11862ef2eea30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sat, 3 Oct 2015 10:40:13 -0500 Subject: [PATCH 177/280] Improve CurveLocation#add() and #equals() to better merge locations. Before, very close locations over curve boundaries where not merged. --- src/path/CurveLocation.js | 62 ++++++++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 24 deletions(-) diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index 09655b20..86444939 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -155,8 +155,8 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ }, /** - * The index of the curve within the {@link Path#curves} list, if the - * curve is part of a {@link Path} item. + * The index of the {@link #curve} within the {@link Path#curves} list, if + * it is part of a {@link Path} item. * * @type Index * @bean @@ -167,9 +167,9 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ }, /** - * The curve parameter, as used by various bezier curve calculations. It is - * value between {@code 0} (beginning of the curve) and {@code 1} (end of - * the curve). + * The curve-time parameter, as used by various bezier curve calculations. + * It is value between {@code 0} (beginning of the curve) and {@code 1} + * (end of the curve). * * @type Number * @bean @@ -182,6 +182,19 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ : parameter; }, + + /** + * The {@link #curve}'s {@link #index} and {@link #parameter} added to one + * value that can conveniently be used for sorting and comparing of + * locations. + * + * @type Number + * @bean + */ + getIndexParameter: function() { + return this.getIndex() + this.getParameter(); + }, + /** * The point which is defined by the {@link #curve} and * {@link #parameter}. @@ -361,16 +374,19 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ * @return {Boolean} {@true if the locations are equal} */ equals: function(loc, _ignoreOther) { + // NOTE: We need to compare both by getIndexParameter() and by proximity + // of points, see: + // https://github.com/paperjs/paper.js/issues/784#issuecomment-143161586 + // Use a relaxed threshold of < 1 for getIndexParameter() difference + // when deciding if two locations should be checked for point proximity. + // This is necessary to catch equal locations on very small curves. + var diff; return this === loc || loc instanceof CurveLocation - // Call getCurve() and getParameter() to keep in sync - && this.getCurve() === loc.getCurve() - // NOTE: We need to compare both by proximity of points - // and by parameters, see: - // https://github.com/paperjs/paper.js/issues/784#issuecomment-143161586 - && (Math.abs(this.getParameter() - loc.getParameter()) + && ((diff = Math.abs( + this.getIndexParameter() - loc.getIndexParameter())) < /*#=*/Numerical.CURVETIME_EPSILON - || this.getPoint().isClose(loc.getPoint(), + || diff < 1 && this.getPoint().isClose(loc.getPoint(), /*#=*/Numerical.GEOMETRIC_EPSILON)) && (_ignoreOther || (!this._intersection && !loc._intersection @@ -409,17 +425,13 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ var length = locations.length, l = 0, r = length - 1, - epsilon = /*#=*/Numerical.CURVETIME_EPSILON, abs = Math.abs; function compare(loc1, loc2) { - var curve1 = loc1._curve, - curve2 = loc2._curve, - path1 = curve1._path, - path2 = curve2._path; + var path1 = loc1.getPath(), + path2 = loc2.getPath(); return path1 === path2 - ? curve1.getIndex() + loc1._parameter - - curve2.getIndex() - loc2._parameter + ? loc1.getIndexParameter() - loc2.getIndexParameter() // Sort by path id to group all locs on same path. : path1._id - path2._id; } @@ -427,7 +439,8 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ function search(start, dir) { for (var i = start + dir; i >= 0 && i < length; i += dir) { var loc2 = locations[i]; - if (abs(compare(loc, loc2)) >= epsilon) + // See #equals() for details of why `>= 1` is used here. + if (abs(compare(loc, loc2)) >= 1) return null; if (loc.equals(loc2)) return loc2; @@ -438,14 +451,15 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ var m = (l + r) >>> 1, loc2 = locations[m], diff = compare(loc, loc2); - // Only compare location with equals() if diff is small enough + // Only compare location with equals() if diff is < 1. + // See #equals() for details of why `< 1` is used here. // NOTE: equals() takes the intersection location into account, // while the above calculation of diff doesn't! - if (merge && abs(diff) < epsilon) { + if (merge && abs(diff) < 1) { // See if the two locations are actually the same, and merge // if they are. If they aren't, we're not done yet since - // all neighbors with a diff < epsilon are potential merge - // candidates, so check them too. + // all neighbors with a diff < 1 are potential merge + // candidates, so check them too (see #search() for details) if (loc2 = loc.equals(loc2) ? loc2 : search(m, -1) || search(m, 1)) { // Carry over overlap setting! From 61fc75ace367f624ebea9c9e6a6fc4738c57aeb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sat, 3 Oct 2015 10:40:33 -0500 Subject: [PATCH 178/280] Some code clean-up. --- src/path/PathItem.Boolean.js | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 91ec39d5..196d8d27 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -142,6 +142,19 @@ PathItem.inject(new function() { true); } + function logIntersection(title, inter) { + var other = inter._intersection; + var log = [title, inter._id, 'id', inter.getPath()._id, + 'i', inter.getIndex(), 't', inter._parameter, + 'o', !!inter._overlap, 'p', inter.getPoint(), + 'Other', other._id, 'id', other.getPath()._id, + 'i', other.getIndex(), 't', other._parameter, + 'o', !!other._overlap, 'p', other.getPoint()]; + console.log(log.map(function(v) { + return v == null ? '-' : v + }).join(' ')); + } + /** * Private method for splitting a PathItem at the given locations. * @@ -153,22 +166,13 @@ PathItem.inject(new function() { locations.forEach(function(inter) { if (inter._other) return; - var other = inter._intersection; - var log = ['CurveLocation', inter._id, 'id', inter.getPath()._id, - 'i', inter.getIndex(), 't', inter._parameter, - 'o', !!inter._overlap, 'p', inter.getPoint(), - 'Other', other._id, 'id', other.getPath()._id, - 'i', other.getIndex(), 't', other._parameter, - 'o', !!other._overlap, 'p', other.getPoint()]; + logIntersection('Intersection', inter); new Path.Circle({ center: inter.point, radius: 2 * scaleFactor, strokeColor: 'red', strokeScaling: false }); - console.log(log.map(function(v) { - return v == null ? '-' : v - }).join(' ')); }); } @@ -208,6 +212,10 @@ PathItem.inject(new function() { if (noHandles) clearSegments.push(segment); } + // TODO: Move setting of these values to CurveLocation + loc._segment = segment; + loc._parameter = segment === curve._segment1 ? 0 : 1; + loc._version = segment._path._version; // Link the new segment with the intersection on the other curve var inter = segment._intersection; if (inter) { @@ -235,10 +243,6 @@ PathItem.inject(new function() { } else { segment._intersection = loc._intersection; } - // TODO: Move setting of these values to CurveLocation - loc._segment = segment; - loc._parameter = segment === curve._segment1 ? 0 : 1; - loc._version = segment._path._version; prevCurve = curve; prevT = origT; } From 5d7a5960266797dc3fa6e1a393aa2555e17c6dcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sat, 3 Oct 2015 11:44:43 -0500 Subject: [PATCH 179/280] Fix wrong upper bounds check in Line.intersect() 1 as a solution was accidentally excluded. --- src/basic/Line.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/basic/Line.js b/src/basic/Line.js index 73fbf1b8..1ea3108d 100644 --- a/src/basic/Line.js +++ b/src/basic/Line.js @@ -138,14 +138,15 @@ var Line = Base.extend(/** @lends Line# */{ // Check the ranges of the u parameters if the line is not // allowed to extend beyond the definition points, but // compare with EPSILON tolerance over the [0, 1] bounds. - uMin = -/*#=*/Numerical.EPSILON, - uMax = 1 + uMin; + epsilon = /*#=*/Numerical.EPSILON, + uMin = -epsilon, + uMax = 1 + epsilon; if (isInfinite || uMin < u1 && u1 < uMax && uMin < u2 && u2 < uMax) { if (!isInfinite) { // Address the tolerance at the bounds by clipping to // the actual range. - u1 = u1 < 0 ? 0 : u1 > 1 ? 1 : u1; + u1 = u1 <= 0 ? 0 : u1 >= 1 ? 1 : u1; } return new Point( p1x + u1 * v1x, From 6fb4b7e3c40c05186592af4b37b25d019842fbe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sat, 3 Oct 2015 11:46:50 -0500 Subject: [PATCH 180/280] Change the way overlaps are detected in lines. We don't really care weather they are actually fully collinear, we only really care about the distances from the beginnings- and end-points of one line from the other, since that proximity will affect results elsewhere. --- src/path/Curve.js | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 4dbc0f91..a9602ad1 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1677,14 +1677,24 @@ new function() { // Scope for intersection using bezier fat-line clipping straight1 = Curve.isStraight(v1), straight2 = Curve.isStraight(v2), straight = straight1 && straight2; + + function getLineLengthSquared(v) { + var x = v[6] - v[0], + y = v[7] - v[1]; + return x * x + y * y; + } + if (straight) { - // Linear curves can only overlap if they are collinear, which means - // they must be are collinear and any point of curve 1 must be on - // curve 2 - var line1 = new Line(v1[0], v1[1], v1[6], v1[7]), - line2 = new Line(v2[0], v2[1], v2[6], v2[7]); - if (!line1.isCollinear(line2) || line1.getDistance(line2.getPoint()) - > geomEpsilon) + // Linear curves can only overlap if they are collinear. + // Instead of using the #isCollinear() check, we pick the longer of + // the two lines and see how far the starting and end points of the + // other line are from this line (assumed as an infinite line). + var flip = getLineLengthSquared(v1) < getLineLengthSquared(v2), + l1 = flip ? v2 : v1, + l2 = flip ? v1 : v2, + line = new Line(l1[0], l1[1], l1[6], l1[7]); + if (line.getDistance(new Point(l2[0], l2[1])) > geomEpsilon || + line.getDistance(new Point(l2[6], l2[7])) > geomEpsilon) return false; } else if (straight1 ^ straight2) { // If one curve is straight, the other curve must be straight, too, From 2bed61164819aca70fd0458cc31c48f638d40da0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sat, 3 Oct 2015 12:55:05 -0500 Subject: [PATCH 181/280] Improve Curve#getPart() to directly handle reversed curves and write docs for it. --- src/path/Curve.js | 22 +++++++++++++++++++++- src/path/Path.js | 3 ++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index a9602ad1..0caca696 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -404,6 +404,17 @@ var Curve = Base.extend(/** @lends Curve# */{ return this._segment2._point.subtract(this._segment1._point); }, + /** + * Creates a new curve as a sub-curve from this curve, its range defined by + * the given parameters. If {@code from} is larger than {@code to}, then + * the resulting curve will have its direction reversed. + * + * @param {Number} from the curve-time parameter at which the sub-curve + * starts + * @param {Number} to the curve-time parameter at which the sub-curve + * ends + * @return {Curve} the newly create sub-curve + */ getPart: function(from, to) { return new Curve(Curve.getPart(this.getValues(), from, to)); }, @@ -655,12 +666,21 @@ statics: { // TODO: Find better name getPart: function(v, from, to) { + var flip = from > to; + if (flip) { + var tmp = from; + from = to; + to = tmp; + } if (from > 0) v = Curve.subdivide(v, from)[1]; // [1] right // Interpolate the parameter at 'to' in the new curve and cut there. if (to < 1) v = Curve.subdivide(v, (to - from) / (1 - from))[0]; // [0] left - return v; + // Return reversed curve if from / to were flipped: + return flip + ? [v[6], v[7], v[4], v[5], v[2], v[3], v[0], v[1]] + : v; }, hasHandles: function(v) { diff --git a/src/path/Path.js b/src/path/Path.js index c948d1c0..0dbc3874 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -1189,7 +1189,8 @@ var Path = PathItem.extend(/** @lends Path# */{ * * @param {Number} index the index of the curve in the {@link Path#curves} * array at which to split - * @param {Number} parameter the parameter at which the curve will be split + * @param {Number} parameter the curve-time parameter at which the curve + * will be split * @return {Path} the newly created path after splitting, if any */ split: function(index, parameter) { From 86b1b74869e61ce8484e6a7b844ea62406b623e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sat, 3 Oct 2015 12:55:32 -0500 Subject: [PATCH 182/280] Prevent detection of tiny overlaps and streamline addOverlap() code. --- src/path/Curve.js | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 0caca696..7b5ca983 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1733,12 +1733,11 @@ new function() { // Scope for intersection using bezier fat-line clipping v[i][t1 === 0 ? 1 : 7]); if (t2 != null) { // If point is on curve var pair = i === 0 ? [t1, t2] : [t2, t1]; - if (pairs.length === 1 && pair[0] < pairs[0][0]) { - pairs.unshift(pair); - } else if (pairs.length === 0 - // TODO: Compare distance of the actual points instead! + // Filter out tiny overlaps + // TODO: Compare distance of points instead of curve time? + if (pairs.length === 0 || abs(pair[0] - pairs[0][0]) > timeEpsilon - || abs(pair[1] - pairs[0][1]) > timeEpsilon) { + && abs(pair[1] - pairs[0][1]) > timeEpsilon) { pairs.push(pair); } } @@ -1751,12 +1750,7 @@ new function() { // Scope for intersection using bezier fat-line clipping if (pairs.length === 2) { // create values for overlapping part of each curve var p1 = Curve.getPart(v[0], pairs[0][0], pairs[1][0]), - p2 = Curve.getPart(v[1], Math.min(pairs[0][1], pairs[1][1]), - Math.max(pairs[0][1], pairs[1][1])); - // Reverse values of second curve if necessary - if (pairs[0][1] > pairs[1][1]) { - p2 = [p2[6], p2[7], p2[4], p2[5], p2[2], p2[3], p2[0], p2[1]]; - } + p2 = Curve.getPart(v[1], pairs[0][1], pairs[1][1]); // Check if handles of overlapping paths are similar enough. // We could do another check for curve identity here if we find a // better criteria. From 5af8515d1a57e26dd8ff539184718d9acc7ecc3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sat, 3 Oct 2015 16:18:00 -0400 Subject: [PATCH 183/280] Commit some useful debug code, deactivated for now. --- src/path/PathItem.Boolean.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 196d8d27..55b2aa19 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -71,8 +71,8 @@ PathItem.inject(new function() { return result; } - var scaleFactor = 0.1; // 1 / 3000; - var textAngle = -30; + var scaleFactor = 0.001; + var textAngle = -40; var fontSize = 5; var segmentOffset; @@ -551,7 +551,14 @@ PathItem.inject(new function() { // If there are multiple possible intersections, find the one // that's either connecting back to start or is not visited yet, // and will be part of the boolean result: + var count = 0; function getIntersection(strict, inter, prev, ignoreOther) { + /* + if (!prev) + count = 0; + if (count++ >= 16) + return null; + */ if (!inter) return null; var seg = inter._segment, @@ -735,9 +742,9 @@ PathItem.inject(new function() { path._segments.length, 'length = ', path.getLength(), '#' + pathCount + '.' + (path ? path._segments.length + 1 : 1)); - paper.project.activeLayer.addChild(path); - path.strokeColor = 'red'; - path.strokeScaling = false; + // paper.project.activeLayer.addChild(path); + // path.strokeColor = 'red'; + // path.strokeScaling = false; path = null; } // Add the path to the result, while avoiding stray segments and From 79cb21668435908579c24bb41cffa9fc5a4a04fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sat, 3 Oct 2015 17:15:45 -0400 Subject: [PATCH 184/280] Define CurveLocation#isOverlap() and improve documentation of various related methods. --- src/path/CurveLocation.js | 147 +++++++++++++++++++++++--------------- src/path/PathItem.js | 14 +++- 2 files changed, 103 insertions(+), 58 deletions(-) diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index 86444939..ddeae1f1 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -13,16 +13,16 @@ /** * @name CurveLocation * - * @class CurveLocation objects describe a location on {@link Curve} - * objects, as defined by the curve {@link #parameter}, a value between - * {@code 0} (beginning of the curve) and {@code 1} (end of the curve). If - * the curve is part of a {@link Path} item, its {@link #index} inside the + * @class CurveLocation objects describe a location on {@link Curve} objects, + * as defined by the curve-time {@link #parameter}, a value between {@code 0} + * (beginning of the curve) and {@code 1} (end of the curve). If the curve is + * part of a {@link Path} item, its {@link #index} inside the * {@link Path#curves} array is also provided. * * The class is in use in many places, such as - * {@link Path#getLocationAt(offset, isParameter)}, + * {@link Path#getLocationAt(offset)}, * {@link Path#getLocationOf(point)}, - * {@link Path#getNearestLocation(point), + * {@link Path#getNearestLocation(point)}, * {@link PathItem#getIntersections(path)}, * etc. */ @@ -190,6 +190,7 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ * * @type Number * @bean + * @private */ getIndexParameter: function() { return this.getIndex() + this.getParameter(); @@ -247,22 +248,25 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ /** * The tangential vector to the {@link #curve} at the given location. * - * @name Item#tangent + * @name CurveLocation#getTangent * @type Point + * @bean */ /** * The normal vector to the {@link #curve} at the given location. * - * @name Item#normal + * @name CurveLocation#getNormal * @type Point + * @bean */ /** * The curvature of the {@link #curve} at the given location. * - * @name Item#curvature + * @name CurveLocation#getCurvature * @type Number + * @bean */ /** @@ -289,13 +293,81 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ return curve && curve.split(this.getParameter(), true); }, - isTangent: function() { + /** + * Checks whether tow CurveLocation objects are describing the same location + * on a path, by applying the same tolerances as elsewhere when dealing with + * curve time parameters. + * + * @param {CurveLocation} location + * @return {Boolean} {@true if the locations are equal} + */ + equals: function(loc, _ignoreOther) { + // NOTE: We need to compare both by getIndexParameter() and by proximity + // of points, see: + // https://github.com/paperjs/paper.js/issues/784#issuecomment-143161586 + // Use a relaxed threshold of < 1 for getIndexParameter() difference + // when deciding if two locations should be checked for point proximity. + // This is necessary to catch equal locations on very small curves. + var diff; + return this === loc + || loc instanceof CurveLocation + && ((diff = Math.abs( + this.getIndexParameter() - loc.getIndexParameter())) + < /*#=*/Numerical.CURVETIME_EPSILON + || diff < 1 && this.getPoint().isClose(loc.getPoint(), + /*#=*/Numerical.GEOMETRIC_EPSILON)) + && (_ignoreOther + || (!this._intersection && !loc._intersection + || this._intersection && this._intersection.equals( + loc._intersection, true))) + || false; + }, + + /** + * @return {String} a string representation of the curve location + */ + toString: function() { + var parts = [], + point = this.getPoint(), + f = Formatter.instance; + if (point) + parts.push('point: ' + point); + var index = this.getIndex(); + if (index != null) + parts.push('index: ' + index); + var parameter = this.getParameter(); + if (parameter != null) + parts.push('parameter: ' + f.number(parameter)); + if (this._distance != null) + parts.push('distance: ' + f.number(this._distance)); + return '{ ' + parts.join(', ') + ' }'; + }, + + + /** + * {@grouptitle Tests} + * Checks if the location is an intersection with another curve and is + * merely touching the other curve, as opposed to crossing it. + * + * @return {Boolean} {@true if the location is an intersection that is + * merely touching another curve} + * @see #isCrossing() + */ + isTouching: function() { var t1 = this.getTangent(), inter = this._intersection, t2 = inter && inter.getTangent(); return t1 && t2 ? t1.isCollinear(t2) : false; }, + /** + * Checks if the location is an intersection with another curve and is + * crossing the other curve, as opposed to just touching it. + * + * @return {Boolean} {@true if the location is an intersection that is + * crossing another curve} + * @see #isTouching() + */ isCrossing: function(_report) { // Implementation based on work by Andy Finnell: // http://losingfight.com/blog/2011/07/09/how-to-implement-boolean-operations-on-bezier-paths-part-3/ @@ -313,7 +385,7 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ // tangent or a crossing, no need for the detailed corner check below. // But we do need a check for the edge case of tangents? if (t1 >= tMin && t1 <= tMax || t2 >= tMin && t2 <= tMax) - return !this.isTangent(); + return !this.isTouching(); // Values for getTangentAt() that are almost 0 and 1. // NOTE: Even though getTangentAt() has code to support 0 and 1 instead // of tMin and tMax, we still need to use this instead, as other issues @@ -366,53 +438,16 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ }, /** - * Checks whether tow CurveLocation objects are describing the same location - * on a path, by applying the same tolerances as elsewhere when dealing with - * curve time parameters. + * Checks if the location is an intersection with another curve and is + * part of an overlap between the two involved paths. * - * @param {CurveLocation} location - * @return {Boolean} {@true if the locations are equal} + * @return {Boolean} {@true if the location is an intersection that is + * part of an overlap between the two involved paths} + * @see #isCrossing() + * @see #isTouching() */ - equals: function(loc, _ignoreOther) { - // NOTE: We need to compare both by getIndexParameter() and by proximity - // of points, see: - // https://github.com/paperjs/paper.js/issues/784#issuecomment-143161586 - // Use a relaxed threshold of < 1 for getIndexParameter() difference - // when deciding if two locations should be checked for point proximity. - // This is necessary to catch equal locations on very small curves. - var diff; - return this === loc - || loc instanceof CurveLocation - && ((diff = Math.abs( - this.getIndexParameter() - loc.getIndexParameter())) - < /*#=*/Numerical.CURVETIME_EPSILON - || diff < 1 && this.getPoint().isClose(loc.getPoint(), - /*#=*/Numerical.GEOMETRIC_EPSILON)) - && (_ignoreOther - || (!this._intersection && !loc._intersection - || this._intersection && this._intersection.equals( - loc._intersection, true))) - || false; - }, - - /** - * @return {String} a string representation of the curve location - */ - toString: function() { - var parts = [], - point = this.getPoint(), - f = Formatter.instance; - if (point) - parts.push('point: ' + point); - var index = this.getIndex(); - if (index != null) - parts.push('index: ' + index); - var parameter = this.getParameter(); - if (parameter != null) - parts.push('parameter: ' + f.number(parameter)); - if (this._distance != null) - parts.push('distance: ' + f.number(this._distance)); - return '{ ' + parts.join(', ') + ' }'; + isOverlap: function() { + return this._overlap; }, statics: { diff --git a/src/path/PathItem.js b/src/path/PathItem.js index 3e05848e..2eb9429a 100644 --- a/src/path/PathItem.js +++ b/src/path/PathItem.js @@ -37,6 +37,7 @@ var PathItem = Item.extend(/** @lends PathItem# */{ * When defined, it shall return {@true to include a location}. * @return {CurveLocation[]} the locations of all intersection between the * paths + * @see #getCrossings(path) * @example {@paperscript} // Finding the intersections between two paths * var path = new Path.Rectangle(new Point(30, 25), new Size(50, 50)); * path.strokeColor = 'black'; @@ -123,10 +124,19 @@ var PathItem = Item.extend(/** @lends PathItem# */{ return locations; }, + /** + * Returns all crossings between two {@link PathItem} items as an array + * of {@link CurveLocation} objects. {@link CompoundPath} items are also + * supported. + * Crossings are intersections where the paths actually are crossing each + * other, as opposed to simply touching. + * + * @param {PathItem} path the other item to find the crossings with + * @see #getIntersections(path) + */ getCrossings: function(path) { return this.getIntersections(path, function(inter) { - // TODO: An overlap could be either a crossing or a tangent! - return inter.isCrossing() || inter._overlap; + return inter.isCrossing(); }); }, From 80731830107929d441768673ec5693a1a8414c6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sat, 3 Oct 2015 17:17:12 -0400 Subject: [PATCH 185/280] Use getIntersections() with right filter instead of getCrossings() for boolean operations. --- src/path/PathItem.Boolean.js | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 55b2aa19..e749a052 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -99,12 +99,17 @@ PathItem.inject(new function() { if (_path2 && /^(subtract|exclude)$/.test(operation) ^ (_path2.isClockwise() !== _path1.isClockwise())) _path2.reverse(); - // Split curves at crossings on both paths. Note that for self - // intersection, _path2 will be null and getIntersections() handles it. + // Split curves at crossings and overlaps on both paths. Note that for + // self-intersection, path2 is null and getIntersections() handles it. // console.time('intersection'); - var crossings = CurveLocation.expand(_path1.getCrossings(_path2)); + var intersections = CurveLocation.expand( + _path1.getIntersections(_path2, function(inter) { + // Only handle overlaps when not self-intersecting + return inter.isCrossing() || _path2 && inter.isOverlap(); + }) + ); // console.timeEnd('intersection'); - splitPath(crossings); + splitPath(intersections); var segments = [], // Aggregate of all curves in both operands, monotonic in y @@ -123,12 +128,12 @@ PathItem.inject(new function() { if (_path2) collect(_path2._children || [_path2]); // Propagate the winding contribution. Winding contribution of curves - // does not change between two crossings. + // does not change between two intersections. // First, propagate winding contributions for curve chains starting in - // all crossings: - for (var i = 0, l = crossings.length; i < l; i++) { - propagateWinding(crossings[i]._segment, _path1, _path2, monoCurves, - operation); + // all intersections: + for (var i = 0, l = intersections.length; i < l; i++) { + propagateWinding(intersections[i]._segment, _path1, _path2, + monoCurves, operation); } // Now process the segments that are not part of any intersecting chains for (var i = 0, l = segments.length; i < l; i++) { From be2f98d91aa19eebdd467d75937de400bfb575c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sat, 3 Oct 2015 17:42:52 -0400 Subject: [PATCH 186/280] Optimize various isCollinear() and isOrthogonal() methods. --- src/basic/Line.js | 8 +++++--- src/basic/Point.js | 42 ++++++++++++++++++++++++++---------------- 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/basic/Line.js b/src/basic/Line.js index 1ea3108d..1743a85d 100644 --- a/src/basic/Line.js +++ b/src/basic/Line.js @@ -113,9 +113,11 @@ var Line = Base.extend(/** @lends Line# */{ }, isCollinear: function(line) { - // TODO: Optimize: - // return Point.isCollinear(this._vx, this._vy, line._vx, line._vy); - return this.getVector().isCollinear(line.getVector()); + return Point.isCollinear(this._vx, this._vy, line._vx, line._yy); + }, + + isOrthogonal: function(line) { + return Point.isOrthogonal(this._vx, this._vy, line._vx, line._yy); }, statics: /** @lends Line */{ diff --git a/src/basic/Point.js b/src/basic/Point.js index 51819713..61304f51 100644 --- a/src/basic/Point.js +++ b/src/basic/Point.js @@ -690,7 +690,9 @@ var Point = Base.extend(/** @lends Point# */{ * @param {Number} tolerance the maximum distance allowed * @return {Boolean} {@true if it is within the given distance} */ - isClose: function(point, tolerance) { + isClose: function(/* point, tolerance */) { + var point = Point.read(arguments), + tolerance = Base.read(arguments); return this.getDistance(point) < tolerance; }, @@ -701,14 +703,9 @@ var Point = Base.extend(/** @lends Point# */{ * @param {Point} point the vector to check against * @return {Boolean} {@true it is collinear} */ - isCollinear: function(point) { - // NOTE: We use normalized vectors so that the epsilon comparison is - // reliable. We could instead scale the epsilon based on the vector - // length. - // TODO: Optimize by creating a static Point.isCollinear() to be used - // in Line.isCollinear() as well. - return Math.abs(this.normalize().cross(point.normalize())) - < /*#=*/Numerical.TRIGONOMETRIC_EPSILON; + isCollinear: function(/* point */) { + var point = Point.read(arguments); + return Point.isCollinear(this.x, this.y, point.x, point.y); }, // TODO: Remove version with typo after a while (deprecated June 2015) @@ -721,13 +718,9 @@ var Point = Base.extend(/** @lends Point# */{ * @param {Point} point the vector to check against * @return {Boolean} {@true it is orthogonal} */ - isOrthogonal: function(point) { - // NOTE: We use normalized vectors so that the epsilon comparison is - // reliable. We could instead scale the epsilon based on the vector - // length. - // TODO: Optimize - return Math.abs(this.normalize().dot(point.normalize())) - < /*#=*/Numerical.TRIGONOMETRIC_EPSILON; + isOrthogonal: function(/* point */) { + var point = Point.read(arguments); + return Point.isOrthogonal(this.x, this.y, point.x, point.y); }, /** @@ -922,6 +915,23 @@ var Point = Base.extend(/** @lends Point# */{ */ random: function() { return new Point(Math.random(), Math.random()); + }, + + isCollinear: function(x1, y1, x2, y2) { + // NOTE: We use normalized vectors so that the epsilon comparison is + // reliable. We could instead scale the epsilon based on the vector + // length. But instead of normalizing the vectors before calculating + // the cross product, we can scale the epsilon accordingly. + return Math.abs(x1 * y2 - y1 * x2) + <= Math.sqrt((x1 * x1 + y1 * y1) * (x2 * x2 + y2 * y2)) + * /*#=*/Numerical.TRIGONOMETRIC_EPSILON; + }, + + isOrthogonal: function(x1, y1, x2, y2) { + // See Point.isCollinear() + return Math.abs(x1 * x2 + y1 * y2) + <= Math.sqrt((x1 * x1 + y1 * y1) * (x2 * x2 + y2 * y2)) + * /*#=*/Numerical.TRIGONOMETRIC_EPSILON; } } }, Base.each(['round', 'ceil', 'floor', 'abs'], function(name) { From ebc956353f74b4144a3c5448f07ffa8c62ed69cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 4 Oct 2015 02:25:33 +0200 Subject: [PATCH 187/280] Move code to adjust segments after split to CurveLocation. --- src/path/CurveLocation.js | 6 ++++++ src/path/PathItem.Boolean.js | 5 +---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index ddeae1f1..fcb1180f 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -77,6 +77,12 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ this._segment2 = curve._segment2; }, + _setSegment: function(segment) { + this._setCurve(segment.getCurve()); + this._segment = segment; + this._parameter = segment === this._segment1 ? 0 : 1; + }, + /** * The segment of the curve which is closer to the described location. * diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index e749a052..8e44d2a1 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -217,10 +217,7 @@ PathItem.inject(new function() { if (noHandles) clearSegments.push(segment); } - // TODO: Move setting of these values to CurveLocation - loc._segment = segment; - loc._parameter = segment === curve._segment1 ? 0 : 1; - loc._version = segment._path._version; + loc._setSegment(segment); // Link the new segment with the intersection on the other curve var inter = segment._intersection; if (inter) { From 035a3a1b8c71fc0cabdd8a47d152a3e19675ac0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 4 Oct 2015 02:27:56 +0200 Subject: [PATCH 188/280] Remove unnecessary _visited check. --- src/path/PathItem.Boolean.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 8e44d2a1..8df4d2f5 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -655,10 +655,6 @@ PathItem.inject(new function() { // We've come back to the start, bail out as we're done. drawSegment(seg, null, 'done', i, 'red'); break; - } else if (seg._visited && (!other || other._visited)) { - // TODO: Do we still need to check other too? - drawSegment(seg, null, 'visited', i, 'red'); - break; } else if (!inter && !isValid(seg)) { // Intersections are always part of the resulting path, for // all other segments check the winding contribution to see From 5601e219969f5b085442a9ee57b2d85624cb13e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 4 Oct 2015 10:14:04 +0200 Subject: [PATCH 189/280] Make sure the two locations are actually part of the same path before comparing index / parameter values. --- src/path/CurveLocation.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index fcb1180f..2b5ff2d2 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -317,6 +317,7 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ var diff; return this === loc || loc instanceof CurveLocation + && this.getPath() === loc.getPath() && ((diff = Math.abs( this.getIndexParameter() - loc.getIndexParameter())) < /*#=*/Numerical.CURVETIME_EPSILON From d2c762997f34507314d9df5ad46534652de7f483 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 4 Oct 2015 18:36:18 +0200 Subject: [PATCH 190/280] Address improvements mentioned by @hkrish in #794 --- src/util/Numerical.js | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/util/Numerical.js b/src/util/Numerical.js index 5f5a2d5e..fe04af0e 100644 --- a/src/util/Numerical.js +++ b/src/util/Numerical.js @@ -201,7 +201,9 @@ var Numerical = new function() { x1, x2 = Infinity, B = b, D; - b /= 2; + // a, b, c are expected to be the coefficients of the equation: + // Ax² - 2Bx + C == 0, so we take b = -B/2: + b /= -2; D = b * b - a * c; // Discriminant // If the discriminant is very small, we can try to pre-condition // the coefficients, so that we may get better accuracy @@ -228,19 +230,16 @@ var Numerical = new function() { if (abs(B) < EPSILON) return abs(c) < EPSILON ? -1 : 0; x1 = -c / B; - } else { - // No real roots if D < 0 - if (D >= -MACHINE_EPSILON) { - var R = D < 0 ? 0 : sqrt(D); - // Try to minimize floating point noise. - if (abs(b) < MACHINE_EPSILON) { - x1 = abs(a) >= abs(c) ? R / a : -c / R; - x2 = -x1; - } else { - var q = (b < 0 ? R : -R) - b; - x1 = q / a; - x2 = c / q; - } + } else if (D >= -MACHINE_EPSILON) { // No real roots if D < 0 + var Q = D < 0 ? 0 : sqrt(D), + R = b + (b < 0 ? -Q : Q); + // Try to minimize floating point noise. + if (R === 0) { + x1 = c / a; + x2 = -x1; + } else { + x1 = R / a; + x2 = c / R; } } // We need to include EPSILON in the comparisons with min / max, From ba76ed8671c3ca0ab8d27f74c1494fa445105479 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Mon, 5 Oct 2015 04:24:04 +0200 Subject: [PATCH 191/280] Consider the winding contribution at the intersection of the next segment as well. Fixes both example 20 and example 21 in #784 --- src/path/PathItem.Boolean.js | 48 ++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 8df4d2f5..199d6b3e 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -564,26 +564,30 @@ PathItem.inject(new function() { if (!inter) return null; var seg = inter._segment, - next = seg.getNext(); + nextSeg = seg.getNext(), + nextInter = nextSeg._intersection; if (window.reportSegments) { console.log('getIntersection(' + strict + ')' + ', seg: ' + seg._path._id + '.' +seg._index - + ', next: ' + next._path._id + '.' + next._index + + ', next: ' + nextSeg._path._id + '.' + nextSeg._index + ', seg vis:' + !!seg._visited - + ', next vis:' + !!next._visited - + ', next start:' + (next === start - || next === otherStart) + + ', next vis:' + !!nextSeg._visited + + ', next start:' + (nextSeg === start + || nextSeg === otherStart) + ', seg wi:' + seg._winding - + ', next wi:' + next._winding + + ', next wi:' + nextSeg._winding + ', seg op:' + isValid(seg, true) - + ', next op:' + isValid(next, !strict && inter._overlap) + + ', next op:' + (isValid(nextSeg, + !strict && inter._overlap) + || nextInter && isValid(nextInter._segment, + !strict && nextInter._overlap)) + ', seg ov: ' + (seg._intersection && seg._intersection._overlap) - + ', next ov: ' + (next._intersection - && next._intersection._overlap) + + ', next ov: ' + (nextSeg._intersection + && nextSeg._intersection._overlap) + ', more: ' + (!!inter._next)); } - // See if this segment and next are both not visited yet, or are + // See if this segment and the next are both not visited yet, or are // bringing us back to the beginning, and are both part of the // boolean result. // Handling overlaps correctly here is a bit tricky business, and @@ -595,14 +599,20 @@ PathItem.inject(new function() { // which invalid current segments are tolerated, and overlaps for // the next segment are allowed as long as they are valid when not // adjusted. - return next === start || next === otherStart - // Self-intersection (!operator) doesn't need isValid() calls - || !seg._visited && !next._visited && (!operator - // NOTE: We need to use the unadjusted winding here since an - // overlap crossing might have brought us here, in which - // case isValid(seg, false) might be false. - || (!strict || isValid(seg, true)) - && isValid(next, !strict && inter._overlap)) + return nextSeg === start || nextSeg === otherStart + || !seg._visited && !nextSeg._visited + // Self-intersections (!operator) don't need isValid() calls + && (!operator + // NOTE: We need to use the unadjusted winding here + // since an overlap crossing might have brought us here, + // in which case isValid(seg, false) might be false. + || (!strict || isValid(seg, true)) + // Even if next segment is not valid, its to which + // we may switch might be, so count that too! + && (isValid(nextSeg, !strict && inter._overlap) + || nextInter && isValid(nextInter._segment, + !strict && nextInter._overlap)) + ) ? inter // If it's no match, check the next linked intersection first, // otherwise carry on with the 'other' intersection location. @@ -708,7 +718,7 @@ PathItem.inject(new function() { + pathCount + '.' + (path ? path._segments.length + 1 : 1) + ', id: ' + seg._path._id + '.' + seg._index - + ', multiple: ' + (!!inter._next)); + + ', multiple: ' + !!(inter && inter._next)); break; } if (!path) { From bd4874d73e57376507e6968c245c9931984d70bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Mon, 5 Oct 2015 04:57:12 +0200 Subject: [PATCH 192/280] Improve debug logging of windings. --- src/path/PathItem.Boolean.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 199d6b3e..bfed336d 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -512,14 +512,14 @@ PathItem.inject(new function() { path = seg._path, id = path._id, point = seg.point, - inter = seg._intersection; + inter = seg._intersection, + ix = inter && inter._segment, + nx = inter && inter._next && inter._next._segment, + style = path._parent instanceof CompoundPath ? path._parent : path; if (!(id in pathIndices)) { pathIndices[id] = ++pathIndex; j = 0; } - - var ix = inter && inter._segment; - var nx = inter && inter._next && inter._next._segment; labelSegment(seg, '#' + pathIndex + '.' + (j + 1) + ' id: ' + seg._path._id + '.' + seg._index + ' ix: ' + (ix && ix._path._id + '.' + ix._index || '--') @@ -527,7 +527,7 @@ PathItem.inject(new function() { + ' pt: ' + seg._point + ' ov: ' + !!(inter && inter._overlap) + ' wi: ' + seg._winding - , path.strokeColor || path.fillColor || 'black'); + , style.strokeColor || style.fillColor || 'black'); } var paths = [], From 05bc6afdbb99855ae9e00a67e36dd11a2f3c23b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Mon, 5 Oct 2015 05:34:22 +0200 Subject: [PATCH 193/280] Always give intersection segment priority over current segment if valid. Fixes example 22 in #784, doesn't seem to introduce new issues, unlike last time I tried this approach. --- src/path/CurveLocation.js | 3 ++- src/path/PathItem.Boolean.js | 9 +++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index 2b5ff2d2..3621673c 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -483,10 +483,11 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ var loc2 = locations[i]; // See #equals() for details of why `>= 1` is used here. if (abs(compare(loc, loc2)) >= 1) - return null; + break; if (loc.equals(loc2)) return loc2; } + return null; } while (l <= r) { diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index bfed336d..fd20bffe 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -649,8 +649,8 @@ PathItem.inject(new function() { + ', other: ' + inter._segment._path._id + '.' + inter._segment._index); } - inter = getIntersection(true, inter) - || getIntersection(false, inter) || inter; + inter = inter && (getIntersection(true, inter) + || getIntersection(false, inter)) || inter; var other = inter && inter._segment; // A switched intersection means we may have changed the segment // Point to the other segment in the selected intersection. @@ -699,10 +699,6 @@ PathItem.inject(new function() { // switch at each crossing. drawSegment(seg, other, 'exclude-cross', i, 'green'); seg = other; - } else if (!seg._visited && isValid(seg)) { - // Do not switch to the intersecting segment as this segment - // is part of the the boolean result. - drawSegment(seg, null, 'keep', i, 'black'); } else if (!other._visited && isValid(other)) { // The other segment is part of the boolean result, and we // are at crossing, switch over. @@ -713,6 +709,7 @@ PathItem.inject(new function() { drawSegment(seg, null, 'stay', i, 'blue'); } if (seg._visited) { + // TODO: || !isValid(seg) ? // We didn't manage to switch, so stop right here. console.error('Visited segment encountered, aborting #' + pathCount + '.' From 90b4cf72924a01d384d8c4a03e82efc4b5dca40c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Mon, 5 Oct 2015 05:52:35 +0200 Subject: [PATCH 194/280] No need to check for overlap when determining value for `unadjusted` Since setting only changes behavior if there actually is an overlap. --- src/path/PathItem.Boolean.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index fd20bffe..1963079d 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -577,10 +577,8 @@ PathItem.inject(new function() { + ', seg wi:' + seg._winding + ', next wi:' + nextSeg._winding + ', seg op:' + isValid(seg, true) - + ', next op:' + (isValid(nextSeg, - !strict && inter._overlap) - || nextInter && isValid(nextInter._segment, - !strict && nextInter._overlap)) + + ', next op:' + (isValid(nextSeg, !strict) || nextInter + && isValid(nextInter._segment, !strict)) + ', seg ov: ' + (seg._intersection && seg._intersection._overlap) + ', next ov: ' + (nextSeg._intersection @@ -609,9 +607,8 @@ PathItem.inject(new function() { || (!strict || isValid(seg, true)) // Even if next segment is not valid, its to which // we may switch might be, so count that too! - && (isValid(nextSeg, !strict && inter._overlap) - || nextInter && isValid(nextInter._segment, - !strict && nextInter._overlap)) + && (isValid(nextSeg, !strict) || nextInter + && isValid(nextInter._segment, !strict)) ) ? inter // If it's no match, check the next linked intersection first, From 7494f880f8aebf385aae01428801a93846f7c970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Mon, 5 Oct 2015 10:44:34 +0200 Subject: [PATCH 195/280] Revert previous commit, due to wrong assumption. --- src/path/PathItem.Boolean.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 1963079d..fd20bffe 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -577,8 +577,10 @@ PathItem.inject(new function() { + ', seg wi:' + seg._winding + ', next wi:' + nextSeg._winding + ', seg op:' + isValid(seg, true) - + ', next op:' + (isValid(nextSeg, !strict) || nextInter - && isValid(nextInter._segment, !strict)) + + ', next op:' + (isValid(nextSeg, + !strict && inter._overlap) + || nextInter && isValid(nextInter._segment, + !strict && nextInter._overlap)) + ', seg ov: ' + (seg._intersection && seg._intersection._overlap) + ', next ov: ' + (nextSeg._intersection @@ -607,8 +609,9 @@ PathItem.inject(new function() { || (!strict || isValid(seg, true)) // Even if next segment is not valid, its to which // we may switch might be, so count that too! - && (isValid(nextSeg, !strict) || nextInter - && isValid(nextInter._segment, !strict)) + && (isValid(nextSeg, !strict && inter._overlap) + || nextInter && isValid(nextInter._segment, + !strict && nextInter._overlap)) ) ? inter // If it's no match, check the next linked intersection first, From 93cacffd0638e771fa1bbe16123f4c6b66a753fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Mon, 5 Oct 2015 10:56:29 +0200 Subject: [PATCH 196/280] Improve comments describing isValid() calls. --- src/path/PathItem.Boolean.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index fd20bffe..5e99300d 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -607,11 +607,11 @@ PathItem.inject(new function() { // since an overlap crossing might have brought us here, // in which case isValid(seg, false) might be false. || (!strict || isValid(seg, true)) - // Even if next segment is not valid, its to which - // we may switch might be, so count that too! - && (isValid(nextSeg, !strict && inter._overlap) - || nextInter && isValid(nextInter._segment, - !strict && nextInter._overlap)) + // Even if next segment is not valid, its intersection + // to which we may switch might be, so count that too! + && (isValid(nextSeg, !strict && inter._overlap) + || nextInter && isValid(nextInter._segment, + !strict && nextInter._overlap)) ) ? inter // If it's no match, check the next linked intersection first, From 1f03b00f99316d9b6570641d85c8e70983811515 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Mon, 5 Oct 2015 17:20:56 +0200 Subject: [PATCH 197/280] Remove handling of converged fat-line, as it causes issues. Example 23 in #784 was caused by this, and the code's removal has not produced any new issues, while it solved 6 issues in @iconexperience's test suite. Closes #795 --- src/path/Curve.js | 58 ++++++++++++++++++++--------------------------- 1 file changed, 25 insertions(+), 33 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 7b5ca983..0e7974e4 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1450,39 +1450,31 @@ new function() { // Scope for intersection using bezier fat-line clipping dMax = factor * Math.max(0, d1, d2), tMinNew, tMaxNew, tDiff; - if (q0x === q3x && uMax - uMin < epsilon && recursion >= 3) { - // The fat-line of Q has converged to a point, the clipping is not - // reliable. Return the value we have even though we will miss the - // precision. - tMaxNew = tMinNew = (tMax + tMin) / 2; - tDiff = 0; - } else { - // Calculate non-parametric bezier curve D(ti, di(t)) - di(t) is the - // distance of P from the baseline l of the fat-line, ti is equally - // spaced in [0, 1] - var dp0 = getSignedDistance(q0x, q0y, q3x, q3y, v1[0], v1[1]), - dp1 = getSignedDistance(q0x, q0y, q3x, q3y, v1[2], v1[3]), - dp2 = getSignedDistance(q0x, q0y, q3x, q3y, v1[4], v1[5]), - dp3 = getSignedDistance(q0x, q0y, q3x, q3y, v1[6], v1[7]), - // Get the top and bottom parts of the convex-hull - hull = getConvexHull(dp0, dp1, dp2, dp3), - top = hull[0], - bottom = hull[1], - tMinClip, tMaxClip; - // Clip the convex-hull with dMin and dMax, taking into account that - // there will be no intersections if one of the tvalues are null. - if ((tMinClip = clipConvexHull(top, bottom, dMin, dMax)) == null || - (tMaxClip = clipConvexHull(top.reverse(), bottom.reverse(), - dMin, dMax)) == null) - return; - // Clip P with the fat-line for Q - v1 = Curve.getPart(v1, tMinClip, tMaxClip); - tDiff = tMaxClip - tMinClip; - // tMin and tMax are within the range (0, 1). We need to project it - // to the original parameter range for v2. - tMinNew = tMax * tMinClip + tMin * (1 - tMinClip); - tMaxNew = tMax * tMaxClip + tMin * (1 - tMaxClip); - } + // Calculate non-parametric bezier curve D(ti, di(t)) - di(t) is the + // distance of P from the baseline l of the fat-line, ti is equally + // spaced in [0, 1] + var dp0 = getSignedDistance(q0x, q0y, q3x, q3y, v1[0], v1[1]), + dp1 = getSignedDistance(q0x, q0y, q3x, q3y, v1[2], v1[3]), + dp2 = getSignedDistance(q0x, q0y, q3x, q3y, v1[4], v1[5]), + dp3 = getSignedDistance(q0x, q0y, q3x, q3y, v1[6], v1[7]), + // Get the top and bottom parts of the convex-hull + hull = getConvexHull(dp0, dp1, dp2, dp3), + top = hull[0], + bottom = hull[1], + tMinClip, tMaxClip; + // Clip the convex-hull with dMin and dMax, taking into account that + // there will be no intersections if one of the tvalues are null. + if ((tMinClip = clipConvexHull(top, bottom, dMin, dMax)) == null || + (tMaxClip = clipConvexHull(top.reverse(), bottom.reverse(), + dMin, dMax)) == null) + return; + // Clip P with the fat-line for Q + v1 = Curve.getPart(v1, tMinClip, tMaxClip); + tDiff = tMaxClip - tMinClip; + // tMin and tMax are within the range (0, 1). We need to project it to + // the original parameter range for v2. + tMinNew = tMax * tMinClip + tMin * (1 - tMinClip); + tMaxNew = tMax * tMaxClip + tMin * (1 - tMaxClip); // Check if we need to subdivide the curves if (oldTDiff > 0.5 && tDiff > 0.5) { // Subdivide the curve which has converged the least. From aa3f527ca6d015bd498547652e948fecee3aab53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Mon, 5 Oct 2015 17:35:54 +0200 Subject: [PATCH 198/280] Minor code clean-up. --- examples/Scripts/BooleanOperations.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/Scripts/BooleanOperations.html b/examples/Scripts/BooleanOperations.html index f9017c69..9ed464f0 100644 --- a/examples/Scripts/BooleanOperations.html +++ b/examples/Scripts/BooleanOperations.html @@ -365,8 +365,7 @@ for (var i = 0, len = children && children.length; i < len; i++) { var cCenter = children[i].bounds.center; var vec = cCenter.subtract(center); - vec = (vec.isClose([0,0], 0.5))? vec : vec.normalize(distance); - children[i].translate(vec); + children[i].translate(vec.length < 0.5 ? vec : vec.normalize(distance)); } } From fcdf91686372692442a01542f3831470c9dbe994 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Mon, 5 Oct 2015 17:42:01 +0200 Subject: [PATCH 199/280] Some tweaks to debugging code. Leave open path artifacts clearly visible for now, so they can be more easily detected. --- src/path/PathItem.Boolean.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 5e99300d..7cd21208 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -71,8 +71,8 @@ PathItem.inject(new function() { return result; } - var scaleFactor = 0.001; - var textAngle = -40; + var scaleFactor = 0.1; + var textAngle = 0; var fontSize = 5; var segmentOffset; @@ -230,7 +230,7 @@ PathItem.inject(new function() { while (next && next !== other) next = next._next; if (!next) { - if (window.reportSegments) { + if (window.reportIntersections) { console.log('Link: ' + segment._path._id + '.' + segment._index + ' -> ' + inter._curve._path._id); @@ -747,9 +747,10 @@ PathItem.inject(new function() { path._segments.length, 'length = ', path.getLength(), '#' + pathCount + '.' + (path ? path._segments.length + 1 : 1)); - // paper.project.activeLayer.addChild(path); - // path.strokeColor = 'red'; - // path.strokeScaling = false; + paper.project.activeLayer.addChild(path); + path.strokeColor = 'cyan'; + path.strokeWidth = 2; + path.strokeScaling = false; path = null; } // Add the path to the result, while avoiding stray segments and From c8132584b966fdc95c1773561de4cb7c9c8cc718 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Mon, 5 Oct 2015 19:31:38 +0200 Subject: [PATCH 200/280] Pass on original curves to addLocation(), to correctly determine p1 and p2. This fixes one glitch in @iconexperience's test suite. --- src/path/Curve.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/path/Curve.js b/src/path/Curve.js index 0e7974e4..07022c17 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1501,6 +1501,13 @@ new function() { // Scope for intersection using bezier fat-line clipping // We have isolated the intersection with sufficient precision var t1 = tMinNew + (tMaxNew - tMinNew) / 2, t2 = uMin + (uMax - uMin) / 2; + // Since we've been chopping up v1 and v2, we need to pass on the + // original full curves here again to match the parameter space of + // t1 and t2. + // TODO: Add two more arguments to addCurveIntersections after param + // to pass on the sub-curves. + v1 = c1.getValues(); + v2 = c2.getValues(); addLocation(locations, param, reverse ? v2 : v1, reverse ? c2 : c1, reverse ? t2 : t1, null, reverse ? v1 : v2, reverse ? c1 : c2, reverse ? t1 : t2, null); From b3d45b662476bd84e106277b19946c6164a45244 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Mon, 5 Oct 2015 19:32:13 +0200 Subject: [PATCH 201/280] Some code clean-up in addCurveIntersections() --- src/path/Curve.js | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 07022c17..00d8dcc1 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1448,12 +1448,10 @@ new function() { // Scope for intersection using bezier fat-line clipping factor = d1 * d2 > 0 ? 3 / 4 : 4 / 9, dMin = factor * Math.min(0, d1, d2), dMax = factor * Math.max(0, d1, d2), - tMinNew, tMaxNew, - tDiff; - // Calculate non-parametric bezier curve D(ti, di(t)) - di(t) is the - // distance of P from the baseline l of the fat-line, ti is equally - // spaced in [0, 1] - var dp0 = getSignedDistance(q0x, q0y, q3x, q3y, v1[0], v1[1]), + // Calculate non-parametric bezier curve D(ti, di(t)) - di(t) is the + // distance of P from the baseline l of the fat-line, ti is equally + // spaced in [0, 1] + dp0 = getSignedDistance(q0x, q0y, q3x, q3y, v1[0], v1[1]), dp1 = getSignedDistance(q0x, q0y, q3x, q3y, v1[2], v1[3]), dp2 = getSignedDistance(q0x, q0y, q3x, q3y, v1[4], v1[5]), dp3 = getSignedDistance(q0x, q0y, q3x, q3y, v1[6], v1[7]), @@ -1461,7 +1459,8 @@ new function() { // Scope for intersection using bezier fat-line clipping hull = getConvexHull(dp0, dp1, dp2, dp3), top = hull[0], bottom = hull[1], - tMinClip, tMaxClip; + tMinClip, + tMaxClip; // Clip the convex-hull with dMin and dMax, taking into account that // there will be no intersections if one of the tvalues are null. if ((tMinClip = clipConvexHull(top, bottom, dMin, dMax)) == null || @@ -1470,11 +1469,11 @@ new function() { // Scope for intersection using bezier fat-line clipping return; // Clip P with the fat-line for Q v1 = Curve.getPart(v1, tMinClip, tMaxClip); - tDiff = tMaxClip - tMinClip; - // tMin and tMax are within the range (0, 1). We need to project it to - // the original parameter range for v2. - tMinNew = tMax * tMinClip + tMin * (1 - tMinClip); - tMaxNew = tMax * tMaxClip + tMin * (1 - tMaxClip); + var tDiff = tMaxClip - tMinClip, + // tMin and tMax are within the range (0, 1). We need to project it + // to the original parameter range for v2. + tMinNew = tMax * tMinClip + tMin * (1 - tMinClip), + tMaxNew = tMax * tMaxClip + tMin * (1 - tMaxClip); // Check if we need to subdivide the curves if (oldTDiff > 0.5 && tDiff > 0.5) { // Subdivide the curve which has converged the least. From ea035bd9e44e08b726f06d162ac6fa8eaf5b4048 Mon Sep 17 00:00:00 2001 From: Jan Date: Tue, 6 Oct 2015 15:19:33 +0200 Subject: [PATCH 202/280] New getConvexHull() --- src/path/Curve.js | 53 +++++++++++++++++------------------------------ 1 file changed, 19 insertions(+), 34 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 00d8dcc1..de00c8c6 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1535,11 +1535,9 @@ new function() { // Scope for intersection using bezier fat-line clipping p1 = [ 1 / 3, dq1 ], p2 = [ 2 / 3, dq2 ], p3 = [ 1, dq3 ], - // Find signed distance of p1 and p2 from line [ p0, p3 ] - getSignedDistance = Line.getSignedDistance, - dist1 = getSignedDistance(0, dq0, 1, dq3, 1 / 3, dq1), - dist2 = getSignedDistance(0, dq0, 1, dq3, 2 / 3, dq2), - flip = false, + // Find vertical signed distance of p1 and p2 from line [ p0, p3 ] + dist1 = dq1 - (dq0 + (dq3 - dq0) / 3), + dist2 = dq2 - (dq0 + 2 * (dq3 - dq0) / 3), hull; // Check if p1 and p2 are on the same side of the line [ p0, p3 ] if (dist1 * dist2 < 0) { @@ -1549,40 +1547,27 @@ new function() { // Scope for intersection using bezier fat-line clipping // The top part includes p1, // we will reverse it later if that is not the case hull = [[p0, p1, p3], [p0, p2, p3]]; - flip = dist1 < 0; } else { // p1 and p2 lie on the same sides of [ p0, p3 ]. The hull can be // a triangle or a quadrilateral and line [ p0, p3 ] is part of the - // hull. Check if the hull is a triangle or a quadrilateral. - // Also, if at least one of the distances for p1 or p2, from line - // [p0, p3] is zero then hull must at most have 3 vertices. - var pmax, cross = 0, - distZero = dist1 === 0 || dist2 === 0; - if (Math.abs(dist1) > Math.abs(dist2)) { - pmax = p1; - // apex is dq3 and the other apex point is dq0 vector dqapex -> - // dqapex2 or base vector which is already part of the hull. - cross = (dq3 - dq2 - (dq3 - dq0) / 3) - * (2 * (dq3 - dq2) - dq3 + dq1) / 3; - } else { - pmax = p2; - // apex is dq0 in this case, and the other apex point is dq3 - // vector dqapex -> dqapex2 or base vector which is already part - // of the hull. - cross = (dq1 - dq0 + (dq0 - dq3) / 3) - * (-2 * (dq0 - dq1) + dq0 - dq2) / 3; + // hull. Check if the hull is a triangle or a quadrilateral. We have a + // triangle if the vertical distance of one of the middle points (p1, p2) + // is equal or less than half the vertical distance of the other middle point. + var distRatio = dist1 / dist2; + hull = [ + // p2 is inside, the hull is a triangle. + distRatio >= 2 ? [p0, p1, p3] + // p1 is inside, the hull is a triangle. + : distRatio <= .5 ? [p0, p2, p3] + // Hull is a quadrilateral, we need all lines in correct order. + : [p0, p1, p2, p3], + // Line [p0, p3] is part of the hull. + [p0, p3] + ]; } - // Compare cross products of these vectors to determine if the point - // is in the triangle [ p3, pmax, p0 ], or if it is a quadrilateral. - hull = cross < 0 || distZero - // p2 is inside the triangle, hull is a triangle. - ? [[p0, pmax, p3], [p0, p3]] - // Convex hull is a quadrilateral and we need all lines in - // correct order where line [ p0, p3 ] is part of the hull. - : [[p0, p1, p2, p3], [p0, p3]]; - flip = dist1 ? dist1 < 0 : dist2 < 0; } - return flip ? hull.reverse() : hull; + // flip hull if dist1 is negative or if it is zero and dist2 is negative + return (dist1 || dist2) < 0 ? hull.reverse() : hull; } /** From 04cab797dbbe219a40a3fd7180fc278696c5b404 Mon Sep 17 00:00:00 2001 From: Jan Date: Tue, 6 Oct 2015 15:25:40 +0200 Subject: [PATCH 203/280] Improve clipConvexHullPart() Separately handle special cases --- src/path/Curve.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 00d8dcc1..53358912 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1609,7 +1609,9 @@ new function() { // Scope for intersection using bezier fat-line clipping for (var i = 1, l = part.length; i < l; i++) { var qx = part[i][0], qy = part[i][1]; - if (top ? qy >= threshold : qy <= threshold) + if (qy == threshold) { + return qx; + } else if (top ? qy > threshold : qy < threshold) return px + (threshold - py) * (qx - px) / (qy - py); px = qx; py = qy; From 3a65c87843ed44dbbe432ee1642d5c5a485a134b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 6 Oct 2015 16:09:35 +0200 Subject: [PATCH 204/280] Some code formatting and a fix for a typo in new getConvexHull() --- src/path/Curve.js | 47 +++++++++++++++++++++++------------------------ 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index de00c8c6..5cf8880c 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1535,38 +1535,37 @@ new function() { // Scope for intersection using bezier fat-line clipping p1 = [ 1 / 3, dq1 ], p2 = [ 2 / 3, dq2 ], p3 = [ 1, dq3 ], - // Find vertical signed distance of p1 and p2 from line [ p0, p3 ] + // Find vertical signed distance of p1 and p2 from line [p0, p3] dist1 = dq1 - (dq0 + (dq3 - dq0) / 3), dist2 = dq2 - (dq0 + 2 * (dq3 - dq0) / 3), hull; - // Check if p1 and p2 are on the same side of the line [ p0, p3 ] + // Check if p1 and p2 are on the opposite side of the line [p0, p3] if (dist1 * dist2 < 0) { - // p1 and p2 lie on different sides of [ p0, p3 ]. The hull is a - // quadrilateral and line [ p0, p3 ] is NOT part of the hull so we - // are pretty much done here. - // The top part includes p1, - // we will reverse it later if that is not the case + // p1 and p2 lie on different sides of [p0, p3]. The hull is a + // quadrilateral and line [p0, p3] is NOT part of the hull so we are + // pretty much done here. The top part includes p1, we will reverse + // it later if that is not the case. hull = [[p0, p1, p3], [p0, p2, p3]]; } else { - // p1 and p2 lie on the same sides of [ p0, p3 ]. The hull can be - // a triangle or a quadrilateral and line [ p0, p3 ] is part of the - // hull. Check if the hull is a triangle or a quadrilateral. We have a - // triangle if the vertical distance of one of the middle points (p1, p2) - // is equal or less than half the vertical distance of the other middle point. + // p1 and p2 lie on the same sides of [p0, p3]. The hull can be a + // triangle or a quadrilateral and line [p0, p3] is part of the + // hull. Check if the hull is a triangle or a quadrilateral. We have + // a triangle if the vertical distance of one of the middle points + // (p1, p2) is equal or less than half the vertical distance of the + // other middle point. var distRatio = dist1 / dist2; - hull = [ - // p2 is inside, the hull is a triangle. - distRatio >= 2 ? [p0, p1, p3] - // p1 is inside, the hull is a triangle. - : distRatio <= .5 ? [p0, p2, p3] - // Hull is a quadrilateral, we need all lines in correct order. - : [p0, p1, p2, p3], - // Line [p0, p3] is part of the hull. - [p0, p3] - ]; - } + hull = [ + // p2 is inside, the hull is a triangle. + distRatio >= 2 ? [p0, p1, p3] + // p1 is inside, the hull is a triangle. + : distRatio <= .5 ? [p0, p2, p3] + // Hull is a quadrilateral, we need all lines in correct order. + : [p0, p1, p2, p3], + // Line [p0, p3] is part of the hull. + [p0, p3] + ]; } - // flip hull if dist1 is negative or if it is zero and dist2 is negative + // Flip hull if dist1 is negative or if it is zero and dist2 is negative return (dist1 || dist2) < 0 ? hull.reverse() : hull; } From 1b343d53478c7582075552fe803507aec9dafaa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 6 Oct 2015 16:31:30 +0200 Subject: [PATCH 205/280] Rewrite 04cab797dbbe219a40a3fd7180fc278696c5b404 to only use one return statement. --- src/path/Curve.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index b6050361..19d66f6c 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1593,10 +1593,10 @@ new function() { // Scope for intersection using bezier fat-line clipping for (var i = 1, l = part.length; i < l; i++) { var qx = part[i][0], qy = part[i][1]; - if (qy == threshold) { - return qx; - } else if (top ? qy > threshold : qy < threshold) - return px + (threshold - py) * (qx - px) / (qy - py); + if (top ? qy >= threshold : qy <= threshold) { + return qy === threshold ? qx + : px + (threshold - py) * (qx - px) / (qy - py); + } px = qx; py = qy; } From adabe9126af90f1c8af6a267d64e9de25135d778 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 6 Oct 2015 20:15:15 +0200 Subject: [PATCH 206/280] Simplify calculation of tMinNew and tMaxNew. As suggested by @iconexperience in https://github.com/paperjs/paper.js/issues/795#issuecomment-145918347 --- src/path/Curve.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 19d66f6c..73d40939 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1472,8 +1472,8 @@ new function() { // Scope for intersection using bezier fat-line clipping var tDiff = tMaxClip - tMinClip, // tMin and tMax are within the range (0, 1). We need to project it // to the original parameter range for v2. - tMinNew = tMax * tMinClip + tMin * (1 - tMinClip), - tMaxNew = tMax * tMaxClip + tMin * (1 - tMaxClip); + tMinNew = tMin + (tMax - tMin) * tMinClip, + tMaxNew = tMin + (tMax - tMin) * tMaxClip; // Check if we need to subdivide the curves if (oldTDiff > 0.5 && tDiff > 0.5) { // Subdivide the curve which has converged the least. From 3ac3df8d32a9de42704d2b2b858d8dd7d2c2bc67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 6 Oct 2015 21:09:35 +0200 Subject: [PATCH 207/280] Rewrite method for linking and choosing multiple intersections in the same location. The special handling of overlaps reduces the amount of remaining glitches substantially. Relates to #784. --- src/path/Curve.js | 16 ++-- src/path/CurveLocation.js | 6 +- src/path/PathItem.Boolean.js | 160 ++++++++++++++++++++--------------- 3 files changed, 105 insertions(+), 77 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 73d40939..0fbfd1c2 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1733,21 +1733,21 @@ new function() { // Scope for intersection using bezier fat-line clipping // We only have to check if the handles are the same, too. if (pairs.length === 2) { // create values for overlapping part of each curve - var p1 = Curve.getPart(v[0], pairs[0][0], pairs[1][0]), - p2 = Curve.getPart(v[1], pairs[0][1], pairs[1][1]); + var o1 = Curve.getPart(v[0], pairs[0][0], pairs[1][0]), + o2 = Curve.getPart(v[1], pairs[0][1], pairs[1][1]); // Check if handles of overlapping paths are similar enough. // We could do another check for curve identity here if we find a // better criteria. if (straight || - abs(p2[2] - p1[2]) < geomEpsilon && - abs(p2[3] - p1[3]) < geomEpsilon && - abs(p2[4] - p1[4]) < geomEpsilon && - abs(p2[5] - p1[5]) < geomEpsilon) { + abs(o2[2] - o1[2]) < geomEpsilon && + abs(o2[3] - o1[3]) < geomEpsilon && + abs(o2[4] - o1[4]) < geomEpsilon && + abs(o2[5] - o1[5]) < geomEpsilon) { // Overlapping parts are identical addLocation(locations, param, v1, c1, pairs[0][0], null, - v2, c2, pairs[0][1], null, true), + v2, c2, pairs[0][1], null, o1), addLocation(locations, param, v1, c1, pairs[1][0], null, - v2, c2, pairs[1][1], null, true); + v2, c2, pairs[1][1], null, o2); return true; } } diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index 3621673c..b102ce52 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -454,7 +454,7 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ * @see #isTouching() */ isOverlap: function() { - return this._overlap; + return !!this._overlap; }, statics: { @@ -505,9 +505,9 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ // candidates, so check them too (see #search() for details) if (loc2 = loc.equals(loc2) ? loc2 : search(m, -1) || search(m, 1)) { - // Carry over overlap setting! + // Carry over overlap! if (loc._overlap) { - loc2._overlap = loc2._intersection._overlap = true; + loc2._overlap = loc2._intersection._overlap = loc._overlap; } // We're done, don't insert, merge with the found // location instead: diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 7cd21208..dc98df92 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -160,10 +160,32 @@ PathItem.inject(new function() { }).join(' ')); } - /** - * Private method for splitting a PathItem at the given locations. + /* + * Creates linked lists between intersections through their _next property. * - * @param {CurveLocation[]} locations Array of CurveLocation objects + * @private + */ + function linkIntersections(from, to) { + // Only create links if they are not the same, to avoid endless + // recursions. + if (from !== to) { + // Loop through the existing linked list until we find an + // empty spot, but stop if we find `to`, to avoid adding it + // again. + while (from._next && from._next !== to) + from = from._next; + // If we're reached the end of the list, we can add it. + if (!from._next) + from._next = to; + } + } + + /** + * Splits a path-item at the given locations. + * + * @param {CurveLocation[]} locations an array of the locations to split the + * path-item at. + * @private */ function splitPath(locations) { if (window.reportIntersections) { @@ -218,32 +240,24 @@ PathItem.inject(new function() { clearSegments.push(segment); } loc._setSegment(segment); - // Link the new segment with the intersection on the other curve - var inter = segment._intersection; + + // Create links from the new segment to the intersection on the + // other curve, as well as from there back. If there are multiple + // intersections on the same segment, we create linked lists between + // the intersections through linkIntersections(), linking both ways. + var inter = segment._intersection, + dest = loc._intersection; if (inter) { - // Prevent circular references that would cause infinite loops - // in getIntersection(): - // See if the location already links back to this intersection, - // and do not create another connection if it does. - var other = inter._intersection, - next = loc._next; - while (next && next !== other) - next = next._next; - if (!next) { - if (window.reportIntersections) { - console.log('Link: ' - + segment._path._id + '.' + segment._index - + ' -> ' + inter._curve._path._id); - } - // Create a chain of possible intersections linked through - // _next First find the last intersection in the chain, then - // link it. - while (inter._next) - inter = inter._next; - inter._next = loc._intersection; + linkIntersections(inter, dest); + // Each time we add a new link to the linked list, we need to + // add links from all the other entries to the new entry. + var other = inter; + while (other) { + linkIntersections(other._intersection, inter); + other = other._next; } } else { - segment._intersection = loc._intersection; + segment._intersection = dest; } prevCurve = curve; prevT = origT; @@ -513,21 +527,33 @@ PathItem.inject(new function() { id = path._id, point = seg.point, inter = seg._intersection, - ix = inter && inter._segment, - nx = inter && inter._next && inter._next._segment, - style = path._parent instanceof CompoundPath ? path._parent : path; + ix = inter, + ixs = ix && ix._segment, + n1x = inter && inter._next, + n1xs = n1x && n1x._segment, + n2x = n1x && n1x._next, + n2xs = n2x && n2x._segment, + n3x = n2x && n2x._next, + n3xs = n3x && n3x._segment, + item = path instanceof Path ? path : path._parent; if (!(id in pathIndices)) { pathIndices[id] = ++pathIndex; j = 0; } labelSegment(seg, '#' + pathIndex + '.' + (j + 1) + ' id: ' + seg._path._id + '.' + seg._index - + ' ix: ' + (ix && ix._path._id + '.' + ix._index || '--') - + ' nx: ' + (nx && nx._path._id + '.' + nx._index || '--') + + ' ix: ' + (ixs && ixs._path._id + '.' + ixs._index + + '(' + ix._id + ')' || '--') + + ' n1x: ' + (n1xs && n1xs._path._id + '.' + n1xs._index + + '(' + n1x._id + ')' || '--') + + ' n2x: ' + (n2xs && n2xs._path._id + '.' + n2xs._index + + '(' + n2x._id + ')' || '--') + + ' n3x: ' + (n3xs && n3xs._path._id + '.' + n3xs._index + + '(' + n3x._id + ')' || '--') + ' pt: ' + seg._point + ' ov: ' + !!(inter && inter._overlap) + ' wi: ' + seg._winding - , style.strokeColor || style.fillColor || 'black'); + , item.strokeColor || item.fillColor || 'black'); } var paths = [], @@ -550,17 +576,26 @@ PathItem.inject(new function() { return operator(winding); } + /** + * Checks if the curve from seg1 to seg2 is part of an overlap, by + * getting a curve-point somewhere along the curve (t = 0.5), and + * checking if it is part of the overlap curve. + */ + function isOverlap(seg1, seg2) { + var inter = seg2._intersection, + overlap = inter && inter._overlap; + if (overlap) { + var pt = Curve.getPoint(Curve.getValues(seg1, seg2), 0.5); + if (Curve.getParameterOf(overlap, pt.x, pt.y) !== null) + return true; + } + return false; + } + // If there are multiple possible intersections, find the one // that's either connecting back to start or is not visited yet, // and will be part of the boolean result: - var count = 0; - function getIntersection(strict, inter, prev, ignoreOther) { - /* - if (!prev) - count = 0; - if (count++ >= 16) - return null; - */ + function getIntersection(inter, strict) { if (!inter) return null; var seg = inter._segment, @@ -577,13 +612,13 @@ PathItem.inject(new function() { + ', seg wi:' + seg._winding + ', next wi:' + nextSeg._winding + ', seg op:' + isValid(seg, true) - + ', next op:' + (isValid(nextSeg, - !strict && inter._overlap) - || nextInter && isValid(nextInter._segment, - !strict && nextInter._overlap)) - + ', seg ov: ' + (seg._intersection + + ', next op:' + ((!strict || !isOverlap(seg, nextSeg)) + && isValid(nextSeg, true) + || !strict && nextInter + && isValid(nextInter._segment, true)) + + ', seg ov: ' + !!(seg._intersection && seg._intersection._overlap) - + ', next ov: ' + (nextSeg._intersection + + ', next ov: ' + !!(nextSeg._intersection && nextSeg._intersection._overlap) + ', more: ' + (!!inter._next)); } @@ -607,26 +642,19 @@ PathItem.inject(new function() { // since an overlap crossing might have brought us here, // in which case isValid(seg, false) might be false. || (!strict || isValid(seg, true)) - // Even if next segment is not valid, its intersection - // to which we may switch might be, so count that too! - && (isValid(nextSeg, !strict && inter._overlap) - || nextInter && isValid(nextInter._segment, - !strict && nextInter._overlap)) + // Do not consider the nextSeg in strict mode if it is + // part of an overlap, in order to give non-overlapping + // options that might follow the priority over overlaps. + && (!(strict && isOverlap(seg, nextSeg)) + && isValid(nextSeg, true) + // If next segment is not valid, its intersection to + // which we may switch might be, so allow that too! + || !strict && nextInter + && isValid(nextInter._segment, true)) ) ? inter - // If it's no match, check the next linked intersection first, - // otherwise carry on with the 'other' intersection location. - : inter._next !== prev // Prevent circular loops - && getIntersection(strict, inter._next, inter, false) - // We need to get the intersection on the segment, not - // on inter, since multiple solutions are only linked up - // as a chain through _next there. But do not check that - // intersection in the first call to getIntersection() - // (prev == null), since we'd go straight back to the - // originating segment. - || !ignoreOther - && (prev || seg._intersection !== inter._intersection) - && getIntersection(strict, seg._intersection, inter, true); + // If it's no match, continue with the next linked intersection. + : getIntersection(inter._next, strict) } for (var i = 0, l = segments.length; i < l; i++) { var seg = segments[i], @@ -649,8 +677,8 @@ PathItem.inject(new function() { + ', other: ' + inter._segment._path._id + '.' + inter._segment._index); } - inter = inter && (getIntersection(true, inter) - || getIntersection(false, inter)) || inter; + inter = inter && (getIntersection(inter, true) + || getIntersection(inter, false)) || inter; var other = inter && inter._segment; // A switched intersection means we may have changed the segment // Point to the other segment in the selected intersection. From 7f4d8d54f083b2a0a4458356212c7a1591a933cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 6 Oct 2015 21:14:04 +0200 Subject: [PATCH 208/280] Reduce epsilon in addCurveIntersections() 1/10 of CURVETIME_EPSILON appears to produce good results. It's probably wise to keep it linked. --- src/path/Curve.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 0fbfd1c2..c3067270 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1439,7 +1439,7 @@ new function() { // Scope for intersection using bezier fat-line clipping return; // Let P be the first curve and Q be the second var q0x = v2[0], q0y = v2[1], q3x = v2[6], q3y = v2[7], - epsilon = /*#=*/Numerical.CURVETIME_EPSILON, + epsilon = /*#=*/Numerical.CURVETIME_EPSILON / 10, getSignedDistance = Line.getSignedDistance, // Calculate the fat-line L for Q is the baseline l and two // offsets which completely encloses the curve P. From bc736f439f5dca48f898bcb2b8a28f9c73b26681 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 6 Oct 2015 21:14:43 +0200 Subject: [PATCH 209/280] Have Numerical.CURVETIME_EPSILON / 10 evaluated at preprocess time. --- src/path/Curve.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index c3067270..bf02d548 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1439,7 +1439,7 @@ new function() { // Scope for intersection using bezier fat-line clipping return; // Let P be the first curve and Q be the second var q0x = v2[0], q0y = v2[1], q3x = v2[6], q3y = v2[7], - epsilon = /*#=*/Numerical.CURVETIME_EPSILON / 10, + epsilon = /*#=*/(Numerical.CURVETIME_EPSILON / 10), getSignedDistance = Line.getSignedDistance, // Calculate the fat-line L for Q is the baseline l and two // offsets which completely encloses the curve P. From 525e35518d24afe03a7ee2713df39a8c7422bf2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 6 Oct 2015 21:16:49 +0200 Subject: [PATCH 210/280] No need to default to 0 anymore. See https://github.com/paperjs/paper.js/commit/1b343d53478c7582075552fe803507aec9dafaa6#commitcomment-13622714 --- src/path/Curve.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index bf02d548..5b00f3e1 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1443,8 +1443,8 @@ new function() { // Scope for intersection using bezier fat-line clipping getSignedDistance = Line.getSignedDistance, // Calculate the fat-line L for Q is the baseline l and two // offsets which completely encloses the curve P. - d1 = getSignedDistance(q0x, q0y, q3x, q3y, v2[2], v2[3]) || 0, - d2 = getSignedDistance(q0x, q0y, q3x, q3y, v2[4], v2[5]) || 0, + d1 = getSignedDistance(q0x, q0y, q3x, q3y, v2[2], v2[3]), + d2 = getSignedDistance(q0x, q0y, q3x, q3y, v2[4], v2[5]), factor = d1 * d2 > 0 ? 3 / 4 : 4 / 9, dMin = factor * Math.min(0, d1, d2), dMax = factor * Math.max(0, d1, d2), From 8e4bef217a0d569e8a5921a122a0117181b3f98d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 6 Oct 2015 21:24:58 +0200 Subject: [PATCH 211/280] Change Curve.getParameterOf() to accept a point instead of x, y arguments. --- src/path/Curve.js | 19 ++++++++++--------- src/path/PathItem.Boolean.js | 10 ++++------ 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 5b00f3e1..72df803c 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -615,10 +615,12 @@ statics: { return Numerical.solveCubic(a, b, c, p1 - val, roots, min, max); }, - getParameterOf: function(v, x, y) { + getParameterOf: function(v, point) { // Handle beginnings and end separately, as they are not detected // sometimes. - var epsilon = /*#=*/Numerical.GEOMETRIC_EPSILON, + var x = point.x, + y = point.y, + epsilon = /*#=*/Numerical.GEOMETRIC_EPSILON, abs = Math.abs; if (abs(v[0] - x) < epsilon && abs(v[1] - y) < epsilon) return 0; @@ -979,8 +981,7 @@ statics: { * @return {Number} the curve time parameter of the specified point */ getParameterOf: function(/* point */) { - var point = Point.read(arguments); - return Curve.getParameterOf(this.getValues(), point.x, point.y); + return Curve.getParameterOf(this.getValues(), Point.read(arguments)); }, /** @@ -1382,7 +1383,7 @@ new function() { // Scope for intersection using bezier fat-line clipping tMin = /*#=*/Numerical.CURVETIME_EPSILON, tMax = 1 - tMin; if (t1 == null) - t1 = Curve.getParameterOf(v1, p1.x, p1.y); + t1 = Curve.getParameterOf(v1, p1); // Check t1 and t2 against correct bounds, based on start-/endConnected: // - startConnected means the start of c1 connects to the end of c2 // - endConneted means the end of c1 connects to the start of c2 @@ -1393,7 +1394,7 @@ new function() { // Scope for intersection using bezier fat-line clipping if (t1 !== null && t1 >= (startConnected ? tMin : 0) && t1 <= (endConnected ? tMax : 1)) { if (t2 == null) - t2 = Curve.getParameterOf(v2, p2.x, p2.y); + t2 = Curve.getParameterOf(v2, p2); if (t2 !== null && t2 >= (endConnected ? tMin : 0) && t2 <= (startConnected ? tMax : 1)) { // TODO: Don't we need to check the range of t2 as well? Does it @@ -1644,7 +1645,7 @@ new function() { // Scope for intersection using bezier fat-line clipping // the real curve and with that the location on the line. var tc = roots[i], pc = Curve.getPoint(vc, tc), - tl = Curve.getParameterOf(vl, pc.x, pc.y); + tl = Curve.getParameterOf(vl, pc); if (tl !== null) { var pl = Curve.getPoint(vl, tl), t1 = flip ? tl : tc, @@ -1712,9 +1713,9 @@ new function() { // Scope for intersection using bezier fat-line clipping for (var i = 0, t1 = 0; i < 2 && pairs.length < 2; i += t1 === 0 ? 0 : 1, t1 = t1 ^ 1) { - var t2 = Curve.getParameterOf(v[i ^ 1], + var t2 = Curve.getParameterOf(v[i ^ 1], new Point( v[i][t1 === 0 ? 0 : 6], - v[i][t1 === 0 ? 1 : 7]); + v[i][t1 === 0 ? 1 : 7])); if (t2 != null) { // If point is on curve var pair = i === 0 ? [t1, t2] : [t2, t1]; // Filter out tiny overlaps diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index dc98df92..d073d293 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -584,12 +584,10 @@ PathItem.inject(new function() { function isOverlap(seg1, seg2) { var inter = seg2._intersection, overlap = inter && inter._overlap; - if (overlap) { - var pt = Curve.getPoint(Curve.getValues(seg1, seg2), 0.5); - if (Curve.getParameterOf(overlap, pt.x, pt.y) !== null) - return true; - } - return false; + return overlap + ? Curve.getParameterOf(overlap, Curve.getPoint( + Curve.getValues(seg1, seg2), 0.5)) !== null + : false; } // If there are multiple possible intersections, find the one From 896b068266fb168c28cfa9ecbab99eb3281e4763 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 6 Oct 2015 21:30:51 +0200 Subject: [PATCH 212/280] Switch from recursion to a simple loop in getIntersection() --- src/path/PathItem.Boolean.js | 119 ++++++++++++++++++----------------- 1 file changed, 61 insertions(+), 58 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index d073d293..a4daff1b 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -594,65 +594,68 @@ PathItem.inject(new function() { // that's either connecting back to start or is not visited yet, // and will be part of the boolean result: function getIntersection(inter, strict) { - if (!inter) - return null; - var seg = inter._segment, - nextSeg = seg.getNext(), - nextInter = nextSeg._intersection; - if (window.reportSegments) { - console.log('getIntersection(' + strict + ')' - + ', seg: ' + seg._path._id + '.' +seg._index - + ', next: ' + nextSeg._path._id + '.' + nextSeg._index - + ', seg vis:' + !!seg._visited - + ', next vis:' + !!nextSeg._visited - + ', next start:' + (nextSeg === start - || nextSeg === otherStart) - + ', seg wi:' + seg._winding - + ', next wi:' + nextSeg._winding - + ', seg op:' + isValid(seg, true) - + ', next op:' + ((!strict || !isOverlap(seg, nextSeg)) - && isValid(nextSeg, true) - || !strict && nextInter - && isValid(nextInter._segment, true)) - + ', seg ov: ' + !!(seg._intersection - && seg._intersection._overlap) - + ', next ov: ' + !!(nextSeg._intersection - && nextSeg._intersection._overlap) - + ', more: ' + (!!inter._next)); - } - // See if this segment and the next are both not visited yet, or are - // bringing us back to the beginning, and are both part of the - // boolean result. - // Handling overlaps correctly here is a bit tricky business, and - // requires two passes, first with `strict = true`, then `false`: - // In strict mode, the current segment and the next segment are both - // checked for validity, and only the current one is allowed to be - // an overlap (passing true for `unadjusted` in isValid()). If this - // pass does not yield a result, the non-strict mode is used, in - // which invalid current segments are tolerated, and overlaps for - // the next segment are allowed as long as they are valid when not - // adjusted. - return nextSeg === start || nextSeg === otherStart - || !seg._visited && !nextSeg._visited - // Self-intersections (!operator) don't need isValid() calls - && (!operator - // NOTE: We need to use the unadjusted winding here - // since an overlap crossing might have brought us here, - // in which case isValid(seg, false) might be false. - || (!strict || isValid(seg, true)) - // Do not consider the nextSeg in strict mode if it is - // part of an overlap, in order to give non-overlapping - // options that might follow the priority over overlaps. - && (!(strict && isOverlap(seg, nextSeg)) - && isValid(nextSeg, true) - // If next segment is not valid, its intersection to - // which we may switch might be, so allow that too! - || !strict && nextInter - && isValid(nextInter._segment, true)) - ) - ? inter + while (inter) { + var seg = inter._segment, + nextSeg = seg.getNext(), + nextInter = nextSeg._intersection; + if (window.reportSegments) { + console.log('getIntersection(' + strict + ')' + + ', seg: ' + seg._path._id + '.' + seg._index + + ', next: ' + nextSeg._path._id + '.' + + nextSeg._index + + ', seg vis:' + !!seg._visited + + ', next vis:' + !!nextSeg._visited + + ', next start:' + (nextSeg === start + || nextSeg === otherStart) + + ', seg wi:' + seg._winding + + ', next wi:' + nextSeg._winding + + ', seg op:' + isValid(seg, true) + + ', next op:' + + (!(strict && isOverlap(seg, nextSeg)) + && isValid(nextSeg, true) + || !strict && nextInter + && isValid(nextInter._segment, true)) + + ', seg ov: ' + !!(seg._intersection + && seg._intersection._overlap) + + ', next ov: ' + !!(nextSeg._intersection + && nextSeg._intersection._overlap) + + ', more: ' + (!!inter._next)); + } + // See if this segment and the next are both not visited yet, or + // are bringing us back to the beginning, and are both part of + // the boolean result. + // Handling overlaps correctly here is a bit tricky business, + // and requires two passes, first with `strict = true`, then + // `false`: In strict mode, the current segment and the next + // segment are both checked for validity, and only the current + // one is allowed to be an overlap (passing true for + // `unadjusted` in isValid()). If this pass does not yield a + // result, the non-strict mode is used, in which invalid current + // segments are tolerated, and overlaps for the next segment are + // allowed as long as they are valid when not adjusted. + if (nextSeg === start || nextSeg === otherStart + || !seg._visited && !nextSeg._visited + // Self-intersections (!operator) don't need isValid() + && (!operator + // We need to use the unadjusted winding here since + // an overlap crossing might have brought us here, + // in which case isValid(seg, false) might be false. + || (!strict || isValid(seg, true)) + // Do not consider nextSeg in strict mode if it is + // part of an overlap, in order to prioritize non- + // overlapping options that might follow. + && (!(strict && isOverlap(seg, nextSeg)) + && isValid(nextSeg, true) + // If next segment isn't valid, its intersection + // to which we may switch might be: + || !strict && nextInter + && isValid(nextInter._segment, true)) + )) + return inter; // If it's no match, continue with the next linked intersection. - : getIntersection(inter._next, strict) + inter = inter._next; + } + return null; } for (var i = 0, l = segments.length; i < l; i++) { var seg = segments[i], From 8c56a1a110d374ffcf1b96bcdff86ed5644524e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 6 Oct 2015 22:23:43 +0200 Subject: [PATCH 213/280] Include _visited checks in isValid() calls. This magically reduces the remaining glitches in @iconexperience's test-suite to half. Relates to #784. --- src/path/PathItem.Boolean.js | 39 +++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index a4daff1b..7cad46c6 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -567,6 +567,8 @@ PathItem.inject(new function() { }[operation]; function isValid(seg, unadjusted) { + if (seg._visited) + return false; if (!operator) // For self-intersection, we're always valid! return true; var winding = seg._winding, @@ -635,34 +637,35 @@ PathItem.inject(new function() { // allowed as long as they are valid when not adjusted. if (nextSeg === start || nextSeg === otherStart || !seg._visited && !nextSeg._visited - // Self-intersections (!operator) don't need isValid() - && (!operator - // We need to use the unadjusted winding here since - // an overlap crossing might have brought us here, - // in which case isValid(seg, false) might be false. - || (!strict || isValid(seg, true)) - // Do not consider nextSeg in strict mode if it is - // part of an overlap, in order to prioritize non- - // overlapping options that might follow. - && (!(strict && isOverlap(seg, nextSeg)) - && isValid(nextSeg, true) - // If next segment isn't valid, its intersection - // to which we may switch might be: - || !strict && nextInter - && isValid(nextInter._segment, true)) - )) + // Self-intersections (!operator) don't need isValid() calls + && (!operator + // We need to use the unadjusted winding here since an + // overlap crossing might have brought us here, in which + // case isValid(seg, false) might be false. + || (!strict || isValid(seg, true)) + // Do not consider nextSeg in strict mode if it is part + // of an overlap, in order to give non-overlapping + // options that might follow the priority over overlaps. + && (!(strict && isOverlap(seg, nextSeg)) + && isValid(nextSeg, true) + // If the next segment isn't valid, its intersection + // to which we may switch might be, so check that. + || !strict && nextInter + && isValid(nextInter._segment, true)) + )) return inter; // If it's no match, continue with the next linked intersection. inter = inter._next; } return null; } + for (var i = 0, l = segments.length; i < l; i++) { var seg = segments[i], path = null; // Do not start a chain with already visited segments, and segments // that are not going to be part of the resulting operation. - if (seg._visited || !isValid(seg)) + if (!isValid(seg)) continue; start = otherStart = null; while (true) { @@ -728,7 +731,7 @@ PathItem.inject(new function() { // switch at each crossing. drawSegment(seg, other, 'exclude-cross', i, 'green'); seg = other; - } else if (!other._visited && isValid(other)) { + } else if (isValid(other)) { // The other segment is part of the boolean result, and we // are at crossing, switch over. drawSegment(seg, other, 'cross', i, 'green'); From 1103c7036ff125d23ae177a87a908acc0cbe6b69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 7 Oct 2015 02:02:27 +0200 Subject: [PATCH 214/280] Remove unnecessary isValid() check on segments without intersections. It was only causing issues without solving anything. --- src/path/PathItem.Boolean.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 7cad46c6..6dfac4e9 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -697,15 +697,6 @@ PathItem.inject(new function() { // We've come back to the start, bail out as we're done. drawSegment(seg, null, 'done', i, 'red'); break; - } else if (!inter && !isValid(seg)) { - // Intersections are always part of the resulting path, for - // all other segments check the winding contribution to see - // if they are to be kept. If not, the chain has to end here - drawSegment(seg, null, 'discard', i, 'red'); - console.error('Excluded segment encountered, aborting #' - + pathCount + '.' + - (path ? path._segments.length + 1 : 1)); - break; } var handleIn = path && seg._handleIn; if (!path || !other) { From bfa0459c525d50d3ccaa53ea4429bf770b7d32dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 7 Oct 2015 10:57:09 +0200 Subject: [PATCH 215/280] Go back to using Group for divide() results. As they may contain multiple CompoundPaths. --- src/path/PathItem.Boolean.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 6dfac4e9..110a0f67 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -53,10 +53,10 @@ PathItem.inject(new function() { .transform(null, true, true); } - function finishBoolean(paths, path1, path2, reduce) { - var result = new CompoundPath(Item.NO_INSERT); + function finishBoolean(ctor, paths, path1, path2, reduce) { + var result = new ctor(Item.NO_INSERT); result.addChildren(paths, true); - // See if the CompoundPath can be reduced to just a simple Path. + // See if the item can be reduced to just a simple Path. if (reduce) result = result.reduce(); // Insert the resulting path above whichever of the two paths appear @@ -143,8 +143,8 @@ PathItem.inject(new function() { operation); } } - return finishBoolean(tracePaths(segments, operation), path1, path2, - true); + return finishBoolean(CompoundPath, tracePaths(segments, operation), + path1, path2, true); } function logIntersection(title, inter) { @@ -855,7 +855,8 @@ PathItem.inject(new function() { */ exclude: function(path) { return computeBoolean(this, path, 'exclude'); - // return finishBoolean([this.subtract(path), path.subtract(this)], + // return finishBoolean(CompoundPath, + // [this.subtract(path), path.subtract(this)], // this, path, true); }, @@ -867,7 +868,8 @@ PathItem.inject(new function() { * @return {Group} the resulting group item */ divide: function(path) { - return finishBoolean([this.subtract(path), this.intersect(path)], + return finishBoolean(Group, + [this.subtract(path), this.intersect(path)], this, path, true); }, @@ -891,8 +893,8 @@ PathItem.inject(new function() { for (var i = 0, l = paths.length; i < l; i++) { segments.push.apply(segments, paths[i]._segments); } - var res = finishBoolean(tracePaths(segments), this, null, false) - .reorient(); + var res = finishBoolean(CompoundPath, tracePaths(segments), + this, null, false).reorient(); window.reportSegments = reportSegments; window.reportWindings = reportWindings; window.reportIntersections = reportIntersections; From 4fac3ee6fc9475ac6cec1924f8f6219d9bd1fc68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 7 Oct 2015 10:57:43 +0200 Subject: [PATCH 216/280] Rename getIntersection() -> getBestIntersection() --- src/path/PathItem.Boolean.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 110a0f67..392c0552 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -595,7 +595,8 @@ PathItem.inject(new function() { // If there are multiple possible intersections, find the one // that's either connecting back to start or is not visited yet, // and will be part of the boolean result: - function getIntersection(inter, strict) { + function getBestIntersection(inter, strict) { + var begin = inter; while (inter) { var seg = inter._segment, nextSeg = seg.getNext(), @@ -681,8 +682,8 @@ PathItem.inject(new function() { + ', other: ' + inter._segment._path._id + '.' + inter._segment._index); } - inter = inter && (getIntersection(inter, true) - || getIntersection(inter, false)) || inter; + inter = inter && (getBestIntersection(inter, true) + || getBestIntersection(inter, false)) || inter; var other = inter && inter._segment; // A switched intersection means we may have changed the segment // Point to the other segment in the selected intersection. From 3e9d7593cf1260867030a9509adac6a415252cca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 7 Oct 2015 10:58:29 +0200 Subject: [PATCH 217/280] Some smaller tweaks in tracePaths() --- src/path/PathItem.Boolean.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 392c0552..cc83daee 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -669,13 +669,14 @@ PathItem.inject(new function() { if (!isValid(seg)) continue; start = otherStart = null; - while (true) { + // Loop until we're back at the start. + while (seg !== start && seg !== otherStart) { var inter = seg._intersection; // Once we started a chain, see if there are multiple // intersections, and if so, pick the best one: if (inter && window.reportSegments) { console.log('-----\n' - +'#' + pathCount + '.' + + '#' + pathCount + '.' + (path ? path._segments.length + 1 : 1) + ', Before getIntersection()' + ', seg: ' + seg._path._id + '.' + seg._index @@ -694,11 +695,6 @@ PathItem.inject(new function() { + ', other: ' + inter._segment._path._id + '.' + inter._segment._index); } - if (seg === start || seg === otherStart) { - // We've come back to the start, bail out as we're done. - drawSegment(seg, null, 'done', i, 'red'); - break; - } var handleIn = path && seg._handleIn; if (!path || !other) { // Just add the first segment and all segments that have no @@ -752,6 +748,9 @@ PathItem.inject(new function() { path.add(new Segment(seg._point, handleIn, seg._handleOut)); seg._visited = true; seg = seg.getNext(); + if (seg === start || seg === otherStart) { + drawSegment(seg, null, 'done', i, 'red'); + } } if (!path) continue; @@ -766,7 +765,6 @@ PathItem.inject(new function() { (path ? path._segments.length + 1 : 1)); } } else { - // path.lastSegment._handleOut.set(0, 0); console.error('Boolean operation results in open path, segs =', path._segments.length, 'length = ', path.getLength(), '#' + pathCount + '.' + From 15d797ac5561aa000581ab6eae31c9120a160367 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 7 Oct 2015 17:20:08 +0200 Subject: [PATCH 218/280] Improve linkIntersections() to prevent endless recursions in linked intersections. --- src/path/PathItem.Boolean.js | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index cc83daee..33930fd2 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -166,17 +166,26 @@ PathItem.inject(new function() { * @private */ function linkIntersections(from, to) { - // Only create links if they are not the same, to avoid endless - // recursions. - if (from !== to) { - // Loop through the existing linked list until we find an - // empty spot, but stop if we find `to`, to avoid adding it - // again. - while (from._next && from._next !== to) - from = from._next; - // If we're reached the end of the list, we can add it. - if (!from._next) - from._next = to; + // Only create the link if it's not already in the existing chain, to + // avoid endless recursions. + var prev = from; + while (prev) { + if (prev === to) + return; + prev = prev._prev; + } + // Loop through the existing linked list until we find an + // empty spot, but stop if we find `to`, to avoid adding it + // again. + while (from._next && from._next !== to) + from = from._next; + // If we're reached the end of the list, we can add it. + if (!from._next) { + // Go back to beginning of the other chain, and link the two up. + while (to._prev) + to = to._prev; + from._next = to; + to._prev = from; } } From 5c70f47b6ffa1f523e587dba79d324c44a5457f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 7 Oct 2015 23:37:09 +0200 Subject: [PATCH 219/280] Fix colors in reportWindings code. --- src/path/PathItem.Boolean.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 33930fd2..5007b59c 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -513,7 +513,6 @@ PathItem.inject(new function() { strokeColor: color, strokeScaling: false }); - var inter = seg._intersection; labelSegment(seg, '#' + pathCount + '.' + (path ? path._segments.length + 1 : 1) + ' (' + (index + 1) + '): ' + text @@ -544,7 +543,7 @@ PathItem.inject(new function() { n2xs = n2x && n2x._segment, n3x = n2x && n2x._next, n3xs = n3x && n3x._segment, - item = path instanceof Path ? path : path._parent; + item = path._parent instanceof CompoundPath ? path._parent : path; if (!(id in pathIndices)) { pathIndices[id] = ++pathIndex; j = 0; From e92a71e8c7c8814c0e1db99fa46e96d7854a3be3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Thu, 8 Oct 2015 22:56:05 +0200 Subject: [PATCH 220/280] Switch to improved version of Line. getSignedDistance() Based on the error analysis by @iconexperience outlined in #799 --- src/basic/Line.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/basic/Line.js b/src/basic/Line.js index 1743a85d..5458471c 100644 --- a/src/basic/Line.js +++ b/src/basic/Line.js @@ -182,11 +182,13 @@ var Line = Base.extend(/** @lends Line# */{ vx -= px; vy -= py; } - return Numerical.isZero(vx) - ? vy >= 0 ? px - x : x - px - : Numerical.isZero(vy) - ? vx >= 0 ? y - py : py - y - : (vx * (y - py) - vy * (x - px)) / Math.sqrt(vx * vx + vy * vy); + // Based on the error analysis by @iconexperience outlined in + // https://github.com/paperjs/paper.js/issues/799 + return vx == 0 + ? vy >= 0 ? px - x : x - px + : vy == 0 + ? vx >= 0 ? y - py : py - y + : (vx * (y - py) - vy * (x - px)) / Math.sqrt(vx * vx + vy * vy); } } }); From c9eba83cc72425e52549ddb4ab7b774f3982b525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Thu, 8 Oct 2015 23:13:37 +0200 Subject: [PATCH 221/280] Improve detection of start segments. Segments with multiple intersections need to be checked thoroughly to avoid errors. --- src/path/PathItem.Boolean.js | 66 +++++++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 19 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 5007b59c..5002026f 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -586,6 +586,10 @@ PathItem.inject(new function() { return operator(winding); } + function isStart(seg) { + return seg === start || seg === otherStart; + } + /** * Checks if the curve from seg1 to seg2 is part of an overlap, by * getting a curve-point somewhere along the curve (t = 0.5), and @@ -603,8 +607,9 @@ PathItem.inject(new function() { // If there are multiple possible intersections, find the one // that's either connecting back to start or is not visited yet, // and will be part of the boolean result: - function getBestIntersection(inter, strict) { - var begin = inter; + function findBestIntersection(inter, strict) { + if (!inter._next) + return inter; while (inter) { var seg = inter._segment, nextSeg = seg.getNext(), @@ -616,8 +621,7 @@ PathItem.inject(new function() { + nextSeg._index + ', seg vis:' + !!seg._visited + ', next vis:' + !!nextSeg._visited - + ', next start:' + (nextSeg === start - || nextSeg === otherStart) + + ', next start:' + isStart(nextSeg) + ', seg wi:' + seg._winding + ', next wi:' + nextSeg._winding + ', seg op:' + isValid(seg, true) @@ -644,7 +648,7 @@ PathItem.inject(new function() { // result, the non-strict mode is used, in which invalid current // segments are tolerated, and overlaps for the next segment are // allowed as long as they are valid when not adjusted. - if (nextSeg === start || nextSeg === otherStart + if (isStart(nextSeg) || !seg._visited && !nextSeg._visited // Self-intersections (!operator) don't need isValid() calls && (!operator @@ -669,16 +673,25 @@ PathItem.inject(new function() { return null; } + function findStartSegment(inter, next) { + while (inter) { + var seg = inter._segment; + if (isStart(seg)) + return seg; + inter = inter[next ? '_next' : '_prev']; + } + } + for (var i = 0, l = segments.length; i < l; i++) { var seg = segments[i], - path = null; + path = null, + finished = false; // Do not start a chain with already visited segments, and segments // that are not going to be part of the resulting operation. if (!isValid(seg)) continue; start = otherStart = null; - // Loop until we're back at the start. - while (seg !== start && seg !== otherStart) { + while (!finished) { var inter = seg._intersection; // Once we started a chain, see if there are multiple // intersections, and if so, pick the best one: @@ -691,8 +704,8 @@ PathItem.inject(new function() { + ', other: ' + inter._segment._path._id + '.' + inter._segment._index); } - inter = inter && (getBestIntersection(inter, true) - || getBestIntersection(inter, false)) || inter; + inter = inter && (findBestIntersection(inter, true) + || findBestIntersection(inter, false)) || inter; var other = inter && inter._segment; // A switched intersection means we may have changed the segment // Point to the other segment in the selected intersection. @@ -737,13 +750,27 @@ PathItem.inject(new function() { drawSegment(seg, null, 'stay', i, 'blue'); } if (seg._visited) { - // TODO: || !isValid(seg) ? - // We didn't manage to switch, so stop right here. - console.error('Visited segment encountered, aborting #' - + pathCount + '.' - + (path ? path._segments.length + 1 : 1) - + ', id: ' + seg._path._id + '.' + seg._index - + ', multiple: ' + !!(inter && inter._next)); + if (isStart(seg)) { + drawSegment(seg, null, 'done', i, 'red'); + finished = true; + } else if (inter) { + var found = findStartSegment(inter, true) + || findStartSegment(inter, false); + if (found) { + seg = found; + drawSegment(seg, null, 'done multiple', i, 'red'); + finished = true; + break; + } + } + if (!finished) { + // We didn't manage to switch, so stop right here. + console.error('Visited segment encountered, aborting #' + + pathCount + '.' + + (path ? path._segments.length + 1 : 1) + + ', id: ' + seg._path._id + '.' + seg._index + + ', multiple: ' + !!(inter && inter._next)); + } break; } if (!path) { @@ -756,15 +783,16 @@ PathItem.inject(new function() { path.add(new Segment(seg._point, handleIn, seg._handleOut)); seg._visited = true; seg = seg.getNext(); - if (seg === start || seg === otherStart) { + if (isStart(seg)) { drawSegment(seg, null, 'done', i, 'red'); + finished = true; } } if (!path) continue; // Finish with closing the paths if necessary, correctly linking up // curves etc. - if (seg === start || seg === otherStart) { + if (finished) { path.firstSegment.setHandleIn(seg._handleIn); path.setClosed(true); if (window.reportSegments) { From 5129fb0050b7d92c92cebb74dc7b8908ddbd835f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Thu, 8 Oct 2015 23:38:41 +0200 Subject: [PATCH 222/280] Do not start with segments with multiple intersections. Simplifies the required checks at the end, and generally reduces number edge cases. --- src/path/PathItem.Boolean.js | 27 ++++----------------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 5002026f..22b4383b 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -673,22 +673,13 @@ PathItem.inject(new function() { return null; } - function findStartSegment(inter, next) { - while (inter) { - var seg = inter._segment; - if (isStart(seg)) - return seg; - inter = inter[next ? '_next' : '_prev']; - } - } - for (var i = 0, l = segments.length; i < l; i++) { var seg = segments[i], path = null, finished = false; - // Do not start a chain with already visited segments, and segments - // that are not going to be part of the resulting operation. - if (!isValid(seg)) + // Do not start a chain with segments that have multiple + // intersections or invalid segments. + if (seg._intersection && seg._intersection._next || !isValid(seg)) continue; start = otherStart = null; while (!finished) { @@ -753,17 +744,7 @@ PathItem.inject(new function() { if (isStart(seg)) { drawSegment(seg, null, 'done', i, 'red'); finished = true; - } else if (inter) { - var found = findStartSegment(inter, true) - || findStartSegment(inter, false); - if (found) { - seg = found; - drawSegment(seg, null, 'done multiple', i, 'red'); - finished = true; - break; - } - } - if (!finished) { + } else { // We didn't manage to switch, so stop right here. console.error('Visited segment encountered, aborting #' + pathCount + '.' From 939a9fe0344335d5472261ba18575e054b50fcb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Thu, 8 Oct 2015 23:54:00 +0200 Subject: [PATCH 223/280] Improve overlap handling by actually storing overlap curves on intersections objects. And properly comparing against them in tracePaths(). --- src/path/Curve.js | 190 ++++++++++++++++++----------------- src/path/CurveLocation.js | 26 +++-- src/path/PathItem.Boolean.js | 40 ++++---- 3 files changed, 137 insertions(+), 119 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 72df803c..33bc7a68 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1671,90 +1671,6 @@ new function() { // Scope for intersection using bezier fat-line clipping } } - /** - * Code to detect overlaps of intersecting curves by @iconexperience: - * https://github.com/paperjs/paper.js/issues/648 - */ - function addOverlap(v1, v2, c1, c2, locations, param) { - var abs = Math.abs, - timeEpsilon = /*#=*/Numerical.CURVETIME_EPSILON, - geomEpsilon = /*#=*/Numerical.GEOMETRIC_EPSILON, - straight1 = Curve.isStraight(v1), - straight2 = Curve.isStraight(v2), - straight = straight1 && straight2; - - function getLineLengthSquared(v) { - var x = v[6] - v[0], - y = v[7] - v[1]; - return x * x + y * y; - } - - if (straight) { - // Linear curves can only overlap if they are collinear. - // Instead of using the #isCollinear() check, we pick the longer of - // the two lines and see how far the starting and end points of the - // other line are from this line (assumed as an infinite line). - var flip = getLineLengthSquared(v1) < getLineLengthSquared(v2), - l1 = flip ? v2 : v1, - l2 = flip ? v1 : v2, - line = new Line(l1[0], l1[1], l1[6], l1[7]); - if (line.getDistance(new Point(l2[0], l2[1])) > geomEpsilon || - line.getDistance(new Point(l2[6], l2[7])) > geomEpsilon) - return false; - } else if (straight1 ^ straight2) { - // If one curve is straight, the other curve must be straight, too, - // otherwise they cannot overlap. - return false; - } - var v = [v1, v2], - pairs = []; - // Iterate through all end points: First p1 and p2 of curve 1, - // then p1 and p2 of curve 2 - for (var i = 0, t1 = 0; - i < 2 && pairs.length < 2; - i += t1 === 0 ? 0 : 1, t1 = t1 ^ 1) { - var t2 = Curve.getParameterOf(v[i ^ 1], new Point( - v[i][t1 === 0 ? 0 : 6], - v[i][t1 === 0 ? 1 : 7])); - if (t2 != null) { // If point is on curve - var pair = i === 0 ? [t1, t2] : [t2, t1]; - // Filter out tiny overlaps - // TODO: Compare distance of points instead of curve time? - if (pairs.length === 0 - || abs(pair[0] - pairs[0][0]) > timeEpsilon - && abs(pair[1] - pairs[0][1]) > timeEpsilon) { - pairs.push(pair); - } - } - // If we checked 3 points but found no match, curves cannot overlap - if (i === 1 && pairs.length === 0) - return false; - } - // If we found 2 pairs, the end points of v1 & v2 should be the same. - // We only have to check if the handles are the same, too. - if (pairs.length === 2) { - // create values for overlapping part of each curve - var o1 = Curve.getPart(v[0], pairs[0][0], pairs[1][0]), - o2 = Curve.getPart(v[1], pairs[0][1], pairs[1][1]); - // Check if handles of overlapping paths are similar enough. - // We could do another check for curve identity here if we find a - // better criteria. - if (straight || - abs(o2[2] - o1[2]) < geomEpsilon && - abs(o2[3] - o1[3]) < geomEpsilon && - abs(o2[4] - o1[4]) < geomEpsilon && - abs(o2[5] - o1[5]) < geomEpsilon) { - // Overlapping parts are identical - addLocation(locations, param, v1, c1, pairs[0][0], null, - v2, c2, pairs[0][1], null, o1), - addLocation(locations, param, v1, c1, pairs[1][0], null, - v2, c2, pairs[1][1], null, o2); - return true; - } - } - return false; - } - return { statics: /** @lends Curve */{ getIntersections: function(v1, v2, c1, c2, locations, param) { var c1p1x = v1[0], c1p1y = v1[1], @@ -1854,20 +1770,28 @@ new function() { // Scope for intersection using bezier fat-line clipping c2s2y = (3 * v2[5] + c2p2y) / 4, min = Math.min, max = Math.max; - if (!( - max(c1p1x, c1s1x, c1s2x, c1p2x) >= + if (!( max(c1p1x, c1s1x, c1s2x, c1p2x) >= min(c2p1x, c2s1x, c2s2x, c2p2x) && min(c1p1x, c1s1x, c1s2x, c1p2x) <= max(c2p1x, c2s1x, c2s2x, c2p2x) && max(c1p1y, c1s1y, c1s2y, c1p2y) >= min(c2p1y, c2s1y, c2s2y, c2p2y) && min(c1p1y, c1s1y, c1s2y, c1p2y) <= - max(c2p1y, c2s1y, c2s2y, c2p2y) - ) - // Also detect and handle overlaps: - || !param.startConnected && !param.endConnected - && addOverlap(v1, v2, c1, c2, locations, param)) + max(c2p1y, c2s1y, c2s2y, c2p2y))) return locations; + // Now detect and handle overlaps: + if (!param.startConnected && !param.endConnected) { + var overlaps = Curve.getOverlaps(v1, v2); + if (overlaps) { + for (var i = 0; i < 2; i++) { + var overlap = overlaps[i]; + addLocation(locations, param, v1, c1, overlap[0], null, + v2, c2, overlap[1], null, overlap[2]); + } + return locations; + } + } + var straight1 = Curve.isStraight(v1), straight2 = Curve.isStraight(v2), c1p1 = new Point(c1p1x, c1p1y), @@ -1900,6 +1824,90 @@ new function() { // Scope for intersection using bezier fat-line clipping // tMin, tMax, uMin, uMax, oldTDiff, reverse, recursion 0, 1, 0, 1, 0, false, 0); return locations; + }, + + /** + * Code to detect overlaps of intersecting curves by @iconexperience: + * https://github.com/paperjs/paper.js/issues/648 + */ + getOverlaps: function(v1, v2) { + var abs = Math.abs, + timeEpsilon = /*#=*/Numerical.CURVETIME_EPSILON, + geomEpsilon = /*#=*/Numerical.GEOMETRIC_EPSILON, + straight1 = Curve.isStraight(v1), + straight2 = Curve.isStraight(v2), + straight = straight1 && straight2; + + function getLineLengthSquared(v) { + var x = v[6] - v[0], + y = v[7] - v[1]; + return x * x + y * y; + } + + if (straight) { + // Linear curves can only overlap if they are collinear. Instead + // of using the #isCollinear() check, we pick the longer of the + // two lines and see how far the starting and end points of the + // other line are from this line (assumed as an infinite line). + var flip = getLineLengthSquared(v1) < getLineLengthSquared(v2), + l1 = flip ? v2 : v1, + l2 = flip ? v1 : v2, + line = new Line(l1[0], l1[1], l1[6], l1[7]); + if (line.getDistance(new Point(l2[0], l2[1])) > geomEpsilon || + line.getDistance(new Point(l2[6], l2[7])) > geomEpsilon) + return null; + } else if (straight1 ^ straight2) { + // If one curve is straight, the other curve must be straight, + // too, otherwise they cannot overlap. + return null; + } + var v = [v1, v2], + pairs = []; + // Iterate through all end points: First p1 and p2 of curve 1, + // then p1 and p2 of curve 2 + for (var i = 0, t1 = 0; + i < 2 && pairs.length < 2; + i += t1 === 0 ? 0 : 1, t1 = t1 ^ 1) { + // TODO: Try with getNearestLocation() instead + var t2 = Curve.getParameterOf(v[i ^ 1], new Point( + v[i][t1 === 0 ? 0 : 6], + v[i][t1 === 0 ? 1 : 7])); + if (t2 != null) { // If point is on curve + var pair = i === 0 ? [t1, t2] : [t2, t1]; + // Filter out tiny overlaps + // TODO: Compare distance of points instead of curve time? + if (pairs.length === 0 + || abs(pair[0] - pairs[0][0]) > timeEpsilon + && abs(pair[1] - pairs[0][1]) > timeEpsilon) { + pairs.push(pair); + } + } + // If we checked 3 points but found no match, curves cannot + // overlap + if (i === 1 && pairs.length === 0) + return null; + } + // If we found 2 pairs, the end points of v1 & v2 should be the same. + // We only have to check if the handles are the same, too. + if (pairs.length === 2) { + // create values for overlapping part of each curve + var o1 = Curve.getPart(v1, pairs[0][0], pairs[1][0]), + o2 = Curve.getPart(v2, pairs[0][1], pairs[1][1]); + // Check if handles of overlapping paths are similar enough. We + // could do another check for curve identity here if we find a + // better criteria. + if (straight || + abs(o2[2] - o1[2]) < geomEpsilon && + abs(o2[3] - o1[3]) < geomEpsilon && + abs(o2[4] - o1[4]) < geomEpsilon && + abs(o2[5] - o1[5]) < geomEpsilon) { + // The overlapping parts are identical + pairs[0][2] = o1; + pairs[1][2] = o2; + return pairs; + } + } + return null; } }}; }); diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index b102ce52..f4d2f087 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -60,9 +60,9 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ this._setCurve(curve); this._parameter = parameter; this._point = point || curve.getPointAt(parameter, true); - this._overlap = _overlap; + this._overlaps = _overlap ? [_overlap] : null; this._distance = _distance; - this._intersection = null; + this._intersection = this._next = this._prev = null; }, _setCurve: function(curve) { @@ -454,7 +454,7 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ * @see #isTouching() */ isOverlap: function() { - return !!this._overlap; + return !!this._overlaps; }, statics: { @@ -490,6 +490,16 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ return null; } + function addOverlaps(loc1, loc2) { + var overlaps1 = loc1._overlaps, + overlaps2 = loc2._overlaps; + if (overlaps1) { + overlaps1.push.apply(overlaps1, overlaps2); + } else { + loc1._overlaps = overlaps2.slice(); + } + } + while (l <= r) { var m = (l + r) >>> 1, loc2 = locations[m], @@ -505,12 +515,12 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ // candidates, so check them too (see #search() for details) if (loc2 = loc.equals(loc2) ? loc2 : search(m, -1) || search(m, 1)) { - // Carry over overlap! - if (loc._overlap) { - loc2._overlap = loc2._intersection._overlap = loc._overlap; - } // We're done, don't insert, merge with the found - // location instead: + // location instead, and carry over overlaps: + if (loc._overlaps) { + addOverlaps(loc2, loc); + addOverlaps(loc2._intersection, loc._intersection); + } return loc2; } } diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 22b4383b..365d55a8 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -151,10 +151,10 @@ PathItem.inject(new function() { var other = inter._intersection; var log = [title, inter._id, 'id', inter.getPath()._id, 'i', inter.getIndex(), 't', inter._parameter, - 'o', !!inter._overlap, 'p', inter.getPoint(), + 'o', !!inter._overlaps, 'p', inter.getPoint(), 'Other', other._id, 'id', other.getPath()._id, 'i', other.getIndex(), 't', other._parameter, - 'o', !!other._overlap, 'p', other.getPoint()]; + 'o', !!other._overlaps, 'p', other.getPoint()]; console.log(log.map(function(v) { return v == null ? '-' : v }).join(' ')); @@ -167,16 +167,16 @@ PathItem.inject(new function() { */ function linkIntersections(from, to) { // Only create the link if it's not already in the existing chain, to - // avoid endless recursions. + // avoid endless recursions. First walk to the beginning of the chain, + // and abort if we find `to`. var prev = from; while (prev) { if (prev === to) return; prev = prev._prev; } - // Loop through the existing linked list until we find an - // empty spot, but stop if we find `to`, to avoid adding it - // again. + // Now walk to the end of the existing chain to find an empty spot, but + // stop if we find `to`, to avoid adding it again. while (from._next && from._next !== to) from = from._next; // If we're reached the end of the list, we can add it. @@ -521,7 +521,7 @@ PathItem.inject(new function() { + ' v: ' + (seg._visited ? 1 : 0) + ' p: ' + seg._point + ' op: ' + isValid(seg) - + ' ov: ' + !!(inter && inter._overlap) + + ' ov: ' + !!(inter && inter._overlaps) + ' wi: ' + seg._winding + ' mu: ' + !!(inter && inter._next) , color); @@ -559,7 +559,7 @@ PathItem.inject(new function() { + ' n3x: ' + (n3xs && n3xs._path._id + '.' + n3xs._index + '(' + n3x._id + ')' || '--') + ' pt: ' + seg._point - + ' ov: ' + !!(inter && inter._overlap) + + ' ov: ' + !!(inter && inter._overlaps) + ' wi: ' + seg._winding , item.strokeColor || item.fillColor || 'black'); } @@ -581,7 +581,7 @@ PathItem.inject(new function() { return true; var winding = seg._winding, inter = seg._intersection; - if (inter && !unadjusted && overlapWinding && inter._overlap) + if (inter && !unadjusted && overlapWinding && inter._overlaps) winding = overlapWinding[winding] || winding; return operator(winding); } @@ -591,17 +591,17 @@ PathItem.inject(new function() { } /** - * Checks if the curve from seg1 to seg2 is part of an overlap, by - * getting a curve-point somewhere along the curve (t = 0.5), and - * checking if it is part of the overlap curve. + * Checks if the curve from seg1 to seg2 is part of an overlap. */ function isOverlap(seg1, seg2) { var inter = seg2._intersection, - overlap = inter && inter._overlap; - return overlap - ? Curve.getParameterOf(overlap, Curve.getPoint( - Curve.getValues(seg1, seg2), 0.5)) !== null - : false; + overlaps = inter && inter._overlaps, + values = Curve.getValues(seg1, seg2); + for (var i = 0, l = overlaps && overlaps.length; i < l; i++) { + if (Curve.getOverlaps(values, overlaps[i])) + return true; + } + return false; } // If there are multiple possible intersections, find the one @@ -631,9 +631,9 @@ PathItem.inject(new function() { || !strict && nextInter && isValid(nextInter._segment, true)) + ', seg ov: ' + !!(seg._intersection - && seg._intersection._overlap) + && seg._intersection._overlaps) + ', next ov: ' + !!(nextSeg._intersection - && nextSeg._intersection._overlap) + && nextSeg._intersection._overlaps) + ', more: ' + (!!inter._next)); } // See if this segment and the next are both not visited yet, or @@ -717,7 +717,7 @@ PathItem.inject(new function() { // Switch to the intersecting segment, as we need to // resolving self-Intersections. seg = other; - } else if (inter._overlap && operation !== 'intersect') { + } else if (inter._overlaps && operation !== 'intersect') { // Switch to the overlapping intersecting segment if it is // part of the boolean result. Do not adjust for overlap! if (isValid(other, true)) { From 9590578339bf7807351d859b2637ccae004e95ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Thu, 8 Oct 2015 23:56:18 +0200 Subject: [PATCH 224/280] Avoid all intersections as starting points for boolean paths. --- src/path/PathItem.Boolean.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 365d55a8..c86a030a 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -677,9 +677,9 @@ PathItem.inject(new function() { var seg = segments[i], path = null, finished = false; - // Do not start a chain with segments that have multiple - // intersections or invalid segments. - if (seg._intersection && seg._intersection._next || !isValid(seg)) + // Do not start a chain with segments that have intersections, + // segments that are already visited, or that are invalid. + if (seg._intersection || !isValid(seg)) continue; start = otherStart = null; while (!finished) { From 892154e8f8bd4ee273e3c5e5b92dbced1bf939a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Fri, 9 Oct 2015 00:03:34 +0200 Subject: [PATCH 225/280] Never starting in intersections allows for further code simplifications. --- src/path/PathItem.Boolean.js | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index c86a030a..244ee203 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -566,7 +566,6 @@ PathItem.inject(new function() { var paths = [], start, - otherStart, operator = operators[operation], // Adjust winding contributions for specific operations on overlaps: overlapWinding = { @@ -586,10 +585,6 @@ PathItem.inject(new function() { return operator(winding); } - function isStart(seg) { - return seg === start || seg === otherStart; - } - /** * Checks if the curve from seg1 to seg2 is part of an overlap. */ @@ -621,7 +616,7 @@ PathItem.inject(new function() { + nextSeg._index + ', seg vis:' + !!seg._visited + ', next vis:' + !!nextSeg._visited - + ', next start:' + isStart(nextSeg) + + ', next start:' + (nextSeg === start) + ', seg wi:' + seg._winding + ', next wi:' + nextSeg._winding + ', seg op:' + isValid(seg, true) @@ -648,7 +643,7 @@ PathItem.inject(new function() { // result, the non-strict mode is used, in which invalid current // segments are tolerated, and overlaps for the next segment are // allowed as long as they are valid when not adjusted. - if (isStart(nextSeg) + if (nextSeg === start || !seg._visited && !nextSeg._visited // Self-intersections (!operator) don't need isValid() calls && (!operator @@ -675,14 +670,13 @@ PathItem.inject(new function() { for (var i = 0, l = segments.length; i < l; i++) { var seg = segments[i], - path = null, - finished = false; + path = null; // Do not start a chain with segments that have intersections, // segments that are already visited, or that are invalid. if (seg._intersection || !isValid(seg)) continue; - start = otherStart = null; - while (!finished) { + start = null; + while (seg !== start) { var inter = seg._intersection; // Once we started a chain, see if there are multiple // intersections, and if so, pick the best one: @@ -741,9 +735,8 @@ PathItem.inject(new function() { drawSegment(seg, null, 'stay', i, 'blue'); } if (seg._visited) { - if (isStart(seg)) { + if (seg === start) { drawSegment(seg, null, 'done', i, 'red'); - finished = true; } else { // We didn't manage to switch, so stop right here. console.error('Visited segment encountered, aborting #' @@ -757,23 +750,21 @@ PathItem.inject(new function() { if (!path) { path = new Path(Item.NO_INSERT); start = seg; - otherStart = other; } // Add the current segment to the path, and mark the added // segment as visited. path.add(new Segment(seg._point, handleIn, seg._handleOut)); seg._visited = true; seg = seg.getNext(); - if (isStart(seg)) { + if (seg === start) { drawSegment(seg, null, 'done', i, 'red'); - finished = true; } } if (!path) continue; // Finish with closing the paths if necessary, correctly linking up // curves etc. - if (finished) { + if (seg === start) { path.firstSegment.setHandleIn(seg._handleIn); path.setClosed(true); if (window.reportSegments) { From c45ae4b51a123c2c16769556f8c2ee0045fbc13b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Fri, 9 Oct 2015 10:18:45 +0200 Subject: [PATCH 226/280] Revert "Never starting in intersections allows for further code simplifications." This reverts commit 892154e8f8bd4ee273e3c5e5b92dbced1bf939a1. --- src/path/PathItem.Boolean.js | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 244ee203..c86a030a 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -566,6 +566,7 @@ PathItem.inject(new function() { var paths = [], start, + otherStart, operator = operators[operation], // Adjust winding contributions for specific operations on overlaps: overlapWinding = { @@ -585,6 +586,10 @@ PathItem.inject(new function() { return operator(winding); } + function isStart(seg) { + return seg === start || seg === otherStart; + } + /** * Checks if the curve from seg1 to seg2 is part of an overlap. */ @@ -616,7 +621,7 @@ PathItem.inject(new function() { + nextSeg._index + ', seg vis:' + !!seg._visited + ', next vis:' + !!nextSeg._visited - + ', next start:' + (nextSeg === start) + + ', next start:' + isStart(nextSeg) + ', seg wi:' + seg._winding + ', next wi:' + nextSeg._winding + ', seg op:' + isValid(seg, true) @@ -643,7 +648,7 @@ PathItem.inject(new function() { // result, the non-strict mode is used, in which invalid current // segments are tolerated, and overlaps for the next segment are // allowed as long as they are valid when not adjusted. - if (nextSeg === start + if (isStart(nextSeg) || !seg._visited && !nextSeg._visited // Self-intersections (!operator) don't need isValid() calls && (!operator @@ -670,13 +675,14 @@ PathItem.inject(new function() { for (var i = 0, l = segments.length; i < l; i++) { var seg = segments[i], - path = null; + path = null, + finished = false; // Do not start a chain with segments that have intersections, // segments that are already visited, or that are invalid. if (seg._intersection || !isValid(seg)) continue; - start = null; - while (seg !== start) { + start = otherStart = null; + while (!finished) { var inter = seg._intersection; // Once we started a chain, see if there are multiple // intersections, and if so, pick the best one: @@ -735,8 +741,9 @@ PathItem.inject(new function() { drawSegment(seg, null, 'stay', i, 'blue'); } if (seg._visited) { - if (seg === start) { + if (isStart(seg)) { drawSegment(seg, null, 'done', i, 'red'); + finished = true; } else { // We didn't manage to switch, so stop right here. console.error('Visited segment encountered, aborting #' @@ -750,21 +757,23 @@ PathItem.inject(new function() { if (!path) { path = new Path(Item.NO_INSERT); start = seg; + otherStart = other; } // Add the current segment to the path, and mark the added // segment as visited. path.add(new Segment(seg._point, handleIn, seg._handleOut)); seg._visited = true; seg = seg.getNext(); - if (seg === start) { + if (isStart(seg)) { drawSegment(seg, null, 'done', i, 'red'); + finished = true; } } if (!path) continue; // Finish with closing the paths if necessary, correctly linking up // curves etc. - if (seg === start) { + if (finished) { path.firstSegment.setHandleIn(seg._handleIn); path.setClosed(true); if (window.reportSegments) { From 514e6651e6bed772f78a0c5d2a4c7bfac3a7fe32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Fri, 9 Oct 2015 10:19:05 +0200 Subject: [PATCH 227/280] Revert "Avoid all intersections as starting points for boolean paths." This reverts commit 9590578339bf7807351d859b2637ccae004e95ec. --- src/path/PathItem.Boolean.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index c86a030a..365d55a8 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -677,9 +677,9 @@ PathItem.inject(new function() { var seg = segments[i], path = null, finished = false; - // Do not start a chain with segments that have intersections, - // segments that are already visited, or that are invalid. - if (seg._intersection || !isValid(seg)) + // Do not start a chain with segments that have multiple + // intersections or invalid segments. + if (seg._intersection && seg._intersection._next || !isValid(seg)) continue; start = otherStart = null; while (!finished) { From b2127a83ad39fc17653f0a79a1d20eb403ed043c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Fri, 9 Oct 2015 10:22:54 +0200 Subject: [PATCH 228/280] Revert "Do not start with segments with multiple intersections." This reverts commit 5129fb0050b7d92c92cebb74dc7b8908ddbd835f. --- src/path/PathItem.Boolean.js | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 365d55a8..4b8aff64 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -673,13 +673,22 @@ PathItem.inject(new function() { return null; } + function findStartSegment(inter, next) { + while (inter) { + var seg = inter._segment; + if (isStart(seg)) + return seg; + inter = inter[next ? '_next' : '_prev']; + } + } + for (var i = 0, l = segments.length; i < l; i++) { var seg = segments[i], path = null, finished = false; - // Do not start a chain with segments that have multiple - // intersections or invalid segments. - if (seg._intersection && seg._intersection._next || !isValid(seg)) + // Do not start a chain with already visited segments, and segments + // that are not going to be part of the resulting operation. + if (!isValid(seg)) continue; start = otherStart = null; while (!finished) { @@ -744,7 +753,17 @@ PathItem.inject(new function() { if (isStart(seg)) { drawSegment(seg, null, 'done', i, 'red'); finished = true; - } else { + } else if (inter) { + var found = findStartSegment(inter, true) + || findStartSegment(inter, false); + if (found) { + seg = found; + drawSegment(seg, null, 'done multiple', i, 'red'); + finished = true; + break; + } + } + if (!finished) { // We didn't manage to switch, so stop right here. console.error('Visited segment encountered, aborting #' + pathCount + '.' From 0839dbe1f5d35f6e674edd9fe63c809e094bb398 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Fri, 9 Oct 2015 10:27:25 +0200 Subject: [PATCH 229/280] New overlap handling is producing new glitches. Turn it off for now, until further analysis has been done. --- src/path/PathItem.Boolean.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 4b8aff64..b31d7678 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -597,6 +597,7 @@ PathItem.inject(new function() { var inter = seg2._intersection, overlaps = inter && inter._overlaps, values = Curve.getValues(seg1, seg2); + return !!overlaps; for (var i = 0, l = overlaps && overlaps.length; i < l; i++) { if (Curve.getOverlaps(values, overlaps[i])) return true; From 48bb0a1be4f4cf68557d9da2d5021cf2ecd39089 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Fri, 9 Oct 2015 10:33:43 +0200 Subject: [PATCH 230/280] Address old TODO in getWinding() --- src/path/PathItem.Boolean.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index b31d7678..482cc2e2 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -319,11 +319,12 @@ PathItem.inject(new function() { // half of closest top and bottom intercepts. yTop = (yTop + py) / 2; yBottom = (yBottom + py) / 2; - // TODO: Don't we need to pass on testContains here? if (yTop > -Infinity) - windLeft = getWinding(new Point(px, yTop), curves); + windLeft = getWinding(new Point(px, yTop), curves, false, + testContains); if (yBottom < Infinity) - windRight = getWinding(new Point(px, yBottom), curves); + windRight = getWinding(new Point(px, yBottom), curves, false, + testContains); } else { var xBefore = px - epsilon, xAfter = px + epsilon; From 688f580b951a4f601d40f79e4c37416eed14545b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Fri, 9 Oct 2015 10:34:46 +0200 Subject: [PATCH 231/280] Switch to new Curve.getParameterOf() Simpler code, but improved precision means more glitches to analyze. --- src/path/Curve.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 33bc7a68..026e6ce0 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -615,7 +615,7 @@ statics: { return Numerical.solveCubic(a, b, c, p1 - val, roots, min, max); }, - getParameterOf: function(v, point) { + getParameterOf_: function(v, point) { // Handle beginnings and end separately, as they are not detected // sometimes. var x = point.x, @@ -666,6 +666,21 @@ statics: { return null; }, + getParameterOf: function(v, point) { + var coords = [point.x, point.y], + roots = []; + for (var c = 0; c < 2; c++) { + var count = Curve.solveCubic(v, c, coords[c], roots, 0, 1); + for (var i = 0; i < count; i++) { + var t = roots[i], + pt = Curve.getPoint(v, t); + if (point.isClose(pt, /*#=*/Numerical.GEOMETRIC_EPSILON)) + return t; + } + } + return null; + }, + // TODO: Find better name getPart: function(v, from, to) { var flip = from > to; From 99909953a871516051565ab7044c9b32c862f0fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Fri, 9 Oct 2015 11:07:43 +0200 Subject: [PATCH 232/280] Use colors to distinguish faulty boolean paths. --- src/path/PathItem.Boolean.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 482cc2e2..7dff8f5a 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -803,13 +803,18 @@ PathItem.inject(new function() { (path ? path._segments.length + 1 : 1)); } } else { - console.error('Boolean operation results in open path, segs =', + var colors = ['cyan', 'green', 'orange', 'yellow']; + var color = new Color(colors[pathCount % (colors.length - 1)]); + console.error('%cBoolean operation results in open path', + 'background: ' + color.toCSS() + '; color: #fff;', + 'segs =', path._segments.length, 'length = ', path.getLength(), '#' + pathCount + '.' + (path ? path._segments.length + 1 : 1)); paper.project.activeLayer.addChild(path); - path.strokeColor = 'cyan'; - path.strokeWidth = 2; + color.alpha = 0.5; + path.strokeColor = color; + path.strokeWidth = 3; path.strokeScaling = false; path = null; } From 7aed2218011531fe665fb97d906d0287b34e8488 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 11 Oct 2015 09:18:50 +0200 Subject: [PATCH 233/280] Some refactoring in static methods of CurveLocation. --- src/path/Curve.js | 2 +- src/path/CurveLocation.js | 184 +++++++++++++++++++------------------- 2 files changed, 95 insertions(+), 91 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 026e6ce0..a18f38af 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1437,7 +1437,7 @@ new function() { // Scope for intersection using bezier fat-line clipping // TODO: Remove this once debug logging is removed. (flip ? loc1 : loc2)._other = true; if (!include || include(loc)) { - CurveLocation.add(locations, loc, true); + CurveLocation.insert(locations, loc, true); } } } diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index f4d2f087..1333f84a 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -455,95 +455,6 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ */ isOverlap: function() { return !!this._overlaps; - }, - - statics: { - add: function(locations, loc, merge) { - // Insert-sort by path-id, curve, parameter so we can easily merge - // duplicates with calls to equals() after. - // NOTE: We don't call getCurve() / getParameter() here, since this - // code is used internally in boolean operations where all this - // information remains valid during processing. - var length = locations.length, - l = 0, - r = length - 1, - abs = Math.abs; - - function compare(loc1, loc2) { - var path1 = loc1.getPath(), - path2 = loc2.getPath(); - return path1 === path2 - ? loc1.getIndexParameter() - loc2.getIndexParameter() - // Sort by path id to group all locs on same path. - : path1._id - path2._id; - } - - function search(start, dir) { - for (var i = start + dir; i >= 0 && i < length; i += dir) { - var loc2 = locations[i]; - // See #equals() for details of why `>= 1` is used here. - if (abs(compare(loc, loc2)) >= 1) - break; - if (loc.equals(loc2)) - return loc2; - } - return null; - } - - function addOverlaps(loc1, loc2) { - var overlaps1 = loc1._overlaps, - overlaps2 = loc2._overlaps; - if (overlaps1) { - overlaps1.push.apply(overlaps1, overlaps2); - } else { - loc1._overlaps = overlaps2.slice(); - } - } - - while (l <= r) { - var m = (l + r) >>> 1, - loc2 = locations[m], - diff = compare(loc, loc2); - // Only compare location with equals() if diff is < 1. - // See #equals() for details of why `< 1` is used here. - // NOTE: equals() takes the intersection location into account, - // while the above calculation of diff doesn't! - if (merge && abs(diff) < 1) { - // See if the two locations are actually the same, and merge - // if they are. If they aren't, we're not done yet since - // all neighbors with a diff < 1 are potential merge - // candidates, so check them too (see #search() for details) - if (loc2 = loc.equals(loc2) ? loc2 - : search(m, -1) || search(m, 1)) { - // We're done, don't insert, merge with the found - // location instead, and carry over overlaps: - if (loc._overlaps) { - addOverlaps(loc2, loc); - addOverlaps(loc2._intersection, loc._intersection); - } - return loc2; - } - } - if (diff < 0) { - r = m - 1; - } else { - l = m + 1; - } - } - // We didn't merge with a preexisting location, insert it now. - locations.splice(l, 0, loc); - return loc; - }, - - expand: function(locations) { - // Create a copy since add() keeps modifying the array and inserting - // at sorted indices. - var expanded = locations.slice(); - for (var i = 0, l = locations.length; i < l; i++) { - this.add(expanded, locations[i]._intersection, false); - } - return expanded; - } } }, Base.each(Curve.evaluateMethods, function(name) { // Produce getters for #getTangent() / #getNormal() / #getCurvature() @@ -555,4 +466,97 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ return parameter != null && curve && curve[get](parameter, true); }; } -}, {})); +}, {}), +new function() { // Scope for statics + + function compare(loc1, loc2) { + var path1 = loc1.getPath(), + path2 = loc2.getPath(); + return path1 === path2 + ? loc1.getIndexParameter() - loc2.getIndexParameter() + // Sort by path id to group all locs on same path. + : path1._id - path2._id; + } + + function addOverlaps(loc1, loc2) { + var overlaps1 = loc1._overlaps, + overlaps2 = loc2._overlaps; + if (overlaps1) { + overlaps1.push.apply(overlaps1, overlaps2); + } else { + loc1._overlaps = overlaps2.slice(); + } + } + + function insert(locations, loc, merge) { + // Insert-sort by path-id, curve, parameter so we can easily merge + // duplicates with calls to equals() after. + // NOTE: We don't call getCurve() / getParameter() here, since this code + // is used internally in boolean operations where all this information + // remains valid during processing. + var length = locations.length, + l = 0, + r = length - 1, + abs = Math.abs; + + function search(index, dir) { + for (var i = index + dir; i >= 0 && i < length; i += dir) { + var loc2 = locations[i]; + // See #equals() for details of why `>= 1` is used here. + if (abs(compare(loc, loc2)) >= 1) + break; + if (loc.equals(loc2)) + return loc2; + } + return null; + } + + while (l <= r) { + var m = (l + r) >>> 1, + loc2 = locations[m], + diff = compare(loc, loc2); + // Only compare location with equals() if diff is < 1. + // See #equals() for details of why `< 1` is used here. + // NOTE: equals() takes the intersection location into account, + // while the above calculation of diff doesn't! + if (merge && abs(diff) < 1) { + // See if the two locations are actually the same, and merge if + // they are. If they aren't, we're not done yet since all + // neighbors with a diff < 1 are potential merge candidates, so + // check them too (see #search() for details) + if (loc2 = loc.equals(loc2) ? loc2 + : search(m, -1) || search(m, 1)) { + // We're done, don't insert, merge with the found location + // instead, and carry over overlaps: + if (loc._overlaps) { + addOverlaps(loc2, loc); + addOverlaps(loc2._intersection, loc._intersection); + } + return loc2; + } + } + if (diff < 0) { + r = m - 1; + } else { + l = m + 1; + } + } + // We didn't merge with a preexisting location, insert it now. + locations.splice(l, 0, loc); + return loc; + } + + return { statics: { + insert: insert, + + expand: function(locations) { + // Create a copy since insert() keeps modifying the array and + // inserting at sorted indices. + var expanded = locations.slice(); + for (var i = 0, l = locations.length; i < l; i++) { + insert(expanded, locations[i]._intersection, false); + } + return expanded; + } + }}; +}); From 8a122e19d8be1d890832e83394ebed6d0e5e37e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 11 Oct 2015 09:26:04 +0200 Subject: [PATCH 234/280] Split self-intersection handling into separate method. Increasing readability of both methods. --- src/path/Curve.js | 171 ++++++++++++++++++++++++---------------------- 1 file changed, 88 insertions(+), 83 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index a18f38af..6fab2bf9 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1688,97 +1688,23 @@ new function() { // Scope for intersection using bezier fat-line clipping return { statics: /** @lends Curve */{ getIntersections: function(v1, v2, c1, c2, locations, param) { - var c1p1x = v1[0], c1p1y = v1[1], - c1h1x = v1[2], c1h1y = v1[3], - c1h2x = v1[4], c1h2y = v1[5], - c1p2x = v1[6], c1p2y = v1[7]; - // If v2 is not provided, search for self intersection on v1. if (!v2) { - // Read a detailed description of the approach used to handle - // self-intersection, developed by @iconexperience here: - // https://github.com/paperjs/paper.js/issues/773#issuecomment-144018379 - // Get the side of both control handles - var line = new Line(c1p1x, c1p1y, c1p2x, c1p2y, false), - side1 = line.getSide(c1h1x, c1h1y), - side2 = Line.getSide(c1h2x, c1h2y); - if (side1 === side2) { - var edgeSum = (c1p1x - c1h2x) * (c1h1y - c1p2y) - + (c1h1x - c1p2x) * (c1h2y - c1p1y); - // If both handles are on the same side, the curve can only - // have a self intersection if the edge sum and the - // handles' sides have different signs. If the handles are - // on the left side, the edge sum must be negative for a - // self intersection (and vice-versa). - if (edgeSum * side1 > 0) - return locations; - } - // As a second condition we check if the curve has an inflection - // point. If an inflection point exists, the curve cannot have a - // self intersection. - var ax = c1p2x - 3 * c1h2x + 3 * c1h1x - c1p1x, - bx = c1h2x - 2 * c1h1x + c1p1x, - cx = c1h1x - c1p1x, - ay = c1p2y - 3 * c1h2y + 3 * c1h1y - c1p1y, - by = c1h2y - 2 * c1h1y + c1p1y, - cy = c1h1y - c1p1y, - // Condition for 1 or 2 inflection points: - // (ay*cx-ax*cy)^2 - 4*(ay*bx-ax*by)*(by*cx-bx*cy) >= 0 - ac = ay * cx - ax * cy, - ab = ay * bx - ax * by, - bc = by * cx - bx * cy; - if (ac * ac - 4 * ab * bc < 0) { - // The curve has no inflection points, so it may have a self - // intersection. Find the right parameter at which to split - // the curve. We search for the parameter where the velocity - // has an extremum by finding the roots of the cross product - // between the bezier curve's first and second derivative. - var roots = [], - tSplit, - count = Numerical.solveCubic( - ax * ax + ay * ay, - 3 * (ax * bx + ay * by), - 2 * (bx * bx + by * by) + ax * cx + ay * cy, - bx * cx + by * cy, - roots, 0, 1); - if (count > 0) { - // Select extremum with highest curvature. This is - // always on the loop in case of a self intersection. - for (var i = 0, maxCurvature = 0; i < count; i++) { - var curvature = Math.abs( - c1.getCurvatureAt(roots[i], true)); - if (curvature > maxCurvature) { - maxCurvature = curvature; - tSplit = roots[i]; - } - } - // Divide the curve in two and then apply the normal - // curve intersection code. - var parts = Curve.subdivide(v1, tSplit); - // After splitting, the end is always connected: - param.endConnected = true; - // Since the curve was split above, we need to adjust - // the parameters for both locations. - param.renormalize = function(t1, t2) { - return [t1 * tSplit, t2 * (1 - tSplit) + tSplit]; - }; - Curve.getIntersections(parts[0], parts[1], c1, c1, - locations, param); - } - } - // We're done handling self-intersection, let's jump out. - return locations; + // If v2 is not provided, search for self intersection on v1. + return this.getSelfIntersections(v1, c1, locations, param); } // Avoid checking curves if completely out of control bounds. As // a little optimization, we can scale the handles with 0.75 // before calculating the control bounds and still be sure that // the curve is fully contained. - var c2p1x = v2[0], c2p1y = v2[1], + var c1p1x = v1[0], c1p1y = v1[1], + c1p2x = v1[6], c1p2y = v1[7], + c2p1x = v2[0], c2p1y = v2[1], c2p2x = v2[6], c2p2y = v2[7], // 's' stands for scaled handles... - c1s1x = (3 * c1h1x + c1p1x) / 4, - c1s1y = (3 * c1h1y + c1p1y) / 4, - c1s2x = (3 * c1h2x + c1p2x) / 4, - c1s2y = (3 * c1h2y + c1p2y) / 4, + c1s1x = (3 * v1[2] + c1p1x) / 4, + c1s1y = (3 * v1[3] + c1p1y) / 4, + c1s2x = (3 * v1[4] + c1p2x) / 4, + c1s2y = (3 * v1[5] + c1p2y) / 4, c2s1x = (3 * v2[2] + c2p1x) / 4, c2s1y = (3 * v2[3] + c2p1y) / 4, c2s2x = (3 * v2[4] + c2p2x) / 4, @@ -1841,6 +1767,85 @@ new function() { // Scope for intersection using bezier fat-line clipping return locations; }, + getSelfIntersections: function(v1, c1, locations, param) { + // Read a detailed description of the approach used to handle self- + // intersection, developed by @iconexperience here: + // https://github.com/paperjs/paper.js/issues/773#issuecomment-144018379 + var p1x = v1[0], p1y = v1[1], + h1x = v1[2], h1y = v1[3], + h2x = v1[4], h2y = v1[5], + p2x = v1[6], p2y = v1[7]; + // Get the side of both control handles + var line = new Line(p1x, p1y, p2x, p2y, false), + side1 = line.getSide(h1x, h1y), + side2 = Line.getSide(h2x, h2y); + if (side1 === side2) { + var edgeSum = (p1x - h2x) * (h1y - p2y) + + (h1x - p2x) * (h2y - p1y); + // If both handles are on the same side, the curve can only have + // a self intersection if the edge sum and the handles' sides + // have different signs. If the handles are on the left side, + // the edge sum must be negative for a self intersection (and + // vice-versa). + if (edgeSum * side1 > 0) + return locations; + } + // As a second condition we check if the curve has an inflection + // point. If an inflection point exists, the curve cannot have a + // self intersection. + var ax = p2x - 3 * h2x + 3 * h1x - p1x, + bx = h2x - 2 * h1x + p1x, + cx = h1x - p1x, + ay = p2y - 3 * h2y + 3 * h1y - p1y, + by = h2y - 2 * h1y + p1y, + cy = h1y - p1y, + // Condition for 1 or 2 inflection points: + // (ay*cx-ax*cy)^2 - 4*(ay*bx-ax*by)*(by*cx-bx*cy) >= 0 + ac = ay * cx - ax * cy, + ab = ay * bx - ax * by, + bc = by * cx - bx * cy; + if (ac * ac - 4 * ab * bc < 0) { + // The curve has no inflection points, so it may have a self + // intersection. Find the right parameter at which to split the + // curve. We search for the parameter where the velocity has an + // extremum by finding the roots of the cross product between + // the bezier curve's first and second derivative. + var roots = [], + tSplit, + count = Numerical.solveCubic( + ax * ax + ay * ay, + 3 * (ax * bx + ay * by), + 2 * (bx * bx + by * by) + ax * cx + ay * cy, + bx * cx + by * cy, + roots, 0, 1); + if (count > 0) { + // Select extremum with highest curvature. This is always on + // the loop in case of a self intersection. + for (var i = 0, maxCurvature = 0; i < count; i++) { + var curvature = Math.abs( + c1.getCurvatureAt(roots[i], true)); + if (curvature > maxCurvature) { + maxCurvature = curvature; + tSplit = roots[i]; + } + } + // Divide the curve in two and then apply the normal curve + // intersection code. + var parts = Curve.subdivide(v1, tSplit); + // After splitting, the end is always connected: + param.endConnected = true; + // Since the curve was split above, we need to adjust the + // parameters for both locations. + param.renormalize = function(t1, t2) { + return [t1 * tSplit, t2 * (1 - tSplit) + tSplit]; + }; + Curve.getIntersections(parts[0], parts[1], c1, c1, + locations, param); + } + } + return locations; + }, + /** * Code to detect overlaps of intersecting curves by @iconexperience: * https://github.com/paperjs/paper.js/issues/648 From d20cdf5b73474d5247346d4cc18f212521b0655f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 11 Oct 2015 09:48:55 +0200 Subject: [PATCH 235/280] There can only be one self-intersection per curve. --- src/path/Curve.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 6fab2bf9..bbeeabd2 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1690,7 +1690,7 @@ new function() { // Scope for intersection using bezier fat-line clipping getIntersections: function(v1, v2, c1, c2, locations, param) { if (!v2) { // If v2 is not provided, search for self intersection on v1. - return this.getSelfIntersections(v1, c1, locations, param); + return Curve.getSelfIntersection(v1, c1, locations, param); } // Avoid checking curves if completely out of control bounds. As // a little optimization, we can scale the handles with 0.75 @@ -1767,7 +1767,7 @@ new function() { // Scope for intersection using bezier fat-line clipping return locations; }, - getSelfIntersections: function(v1, c1, locations, param) { + getSelfIntersection: function(v1, c1, locations, param) { // Read a detailed description of the approach used to handle self- // intersection, developed by @iconexperience here: // https://github.com/paperjs/paper.js/issues/773#issuecomment-144018379 From 841381f5208117a5772c16b09e3c860246e8467e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 11 Oct 2015 10:17:03 +0200 Subject: [PATCH 236/280] Reactivate new overlap handling. It appears to work better with the new Curve.getParameterOf() --- 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 7dff8f5a..1ab769fa 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -598,7 +598,6 @@ PathItem.inject(new function() { var inter = seg2._intersection, overlaps = inter && inter._overlaps, values = Curve.getValues(seg1, seg2); - return !!overlaps; for (var i = 0, l = overlaps && overlaps.length; i < l; i++) { if (Curve.getOverlaps(values, overlaps[i])) return true; From 247e80f569ce10360c2331464a6516e01f517cdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 11 Oct 2015 15:50:25 +0200 Subject: [PATCH 237/280] Update to latest prepro.js with proper support for strings and scientific numbers. --- package.json | 2 +- src/core/PaperScope.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 522ee81a..f630afb5 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "devDependencies": { "gulp": "^3.9.0", "gulp-qunit": "^1.2.1", - "prepro": "~0.8.3", + "prepro": "~0.9.0", "qunitjs": "~1.15.0", "uglify-js": "~2.4.23" }, diff --git a/src/core/PaperScope.js b/src/core/PaperScope.js index 4e154fa4..7b6a3b4f 100644 --- a/src/core/PaperScope.js +++ b/src/core/PaperScope.js @@ -120,7 +120,7 @@ var PaperScope = Base.extend(/** @lends PaperScope# */{ * * @type String */ - version: '/*#=*/__options.version', + version: /*#=*/__options.version, // DOCS: PaperScope#settings /** From 6cdead0e8c755c7ba9c75238be9806f482034b86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 11 Oct 2015 16:56:41 +0200 Subject: [PATCH 238/280] Add fallback strategy when ending up in a dead-end in tracePaths(). This simple fix appears to be able to catch quite a few glitches with very small curves. --- src/path/PathItem.Boolean.js | 46 ++++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 1ab769fa..b116df36 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -751,36 +751,62 @@ PathItem.inject(new function() { drawSegment(seg, null, 'stay', i, 'blue'); } if (seg._visited) { - if (isStart(seg)) { + finished = isStart(seg); + if (finished) { drawSegment(seg, null, 'done', i, 'red'); - finished = true; - } else if (inter) { + } + if (!finished && inter) { var found = findStartSegment(inter, true) || findStartSegment(inter, false); + // This should not happen but due to numerical + // imprecisions we sometimes end up in a dead-end. See + // if we can find a way out by checking all valid + // segments to find one that's close enough. + for (var j = 0; !found && j < l; j++) { + var seg2 = segments[j]; + // Do not start a chain with already visited + // segments, and segments that are not going to + // be part of the resulting operation. + if (seg !== seg2 + && seg._point.isClose(seg2._point, + /*#=*/Numerical.GEOMETRIC_EPSILON) + && (isStart(seg2) || isValid(seg2))) { + found = seg2; + } + } if (found) { seg = found; - drawSegment(seg, null, 'done multiple', i, 'red'); - finished = true; - break; + finished = isStart(seg); + if (window.reportSegments) { + console.log('Switching to: ', + seg._path._id + '.' + seg._index); + } + if (finished) { + drawSegment(seg, null, 'done inter', i, 'red'); + } } } - if (!finished) { + if (finished) + break; + if (!isValid(seg)) { // We didn't manage to switch, so stop right here. console.error('Visited segment encountered, aborting #' + pathCount + '.' + (path ? path._segments.length + 1 : 1) + ', id: ' + seg._path._id + '.' + seg._index + ', multiple: ' + !!(inter && inter._next)); + break; } - break; } if (!path) { path = new Path(Item.NO_INSERT); start = seg; otherStart = other; } - // Add the current segment to the path, and mark the added - // segment as visited. + if (window.reportSegments) { + console.log('Adding', seg._path._id + '.' + seg._index); + } + // Add the segment to the path, and mark it as visited. path.add(new Segment(seg._point, handleIn, seg._handleOut)); seg._visited = true; seg = seg.getNext(); From 61db3d9d01774dccef65d21c4a03f0da64a53a4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 11 Oct 2015 16:57:43 +0200 Subject: [PATCH 239/280] Improve handling of boolean debug options. --- src/path/PathItem.Boolean.js | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index b116df36..4dab1f07 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -71,7 +71,7 @@ PathItem.inject(new function() { return result; } - var scaleFactor = 0.1; + var scaleFactor = 1; var textAngle = 0; var fontSize = 5; @@ -85,15 +85,29 @@ PathItem.inject(new function() { // for each curve in the graph after curves in the operands are // split at intersections. function computeBoolean(path1, path2, operation) { + scaleFactor = Base.pick(window.scaleFactor, scaleFactor); + textAngle = Base.pick(window.textAngle, 0); + segmentOffset = {}; pathIndices = {}; + var reportSegments = window.reportSegments; + var reportWindings = window.reportWindings; + var reportIntersections = window.reportIntersections; + if (path2) { + window.reportSegments = false; + window.reportWindings = false; + window.reportIntersections = false; + } // We do not modify the operands themselves, but create copies instead, // fas produced by the calls to preparePath(). // Note that the result paths might not belong to the same type // i.e. subtraction(A:Path, B:Path):CompoundPath etc. var _path1 = preparePath(path1), _path2 = path2 && path1 !== path2 && preparePath(path2); + window.reportSegments = reportSegments; + window.reportWindings = reportWindings; + window.reportIntersections = reportIntersections; // Give both paths the same orientation except for subtraction // and exclusion, where we need them at opposite orientation. if (_path2 && /^(subtract|exclude)$/.test(operation) @@ -941,31 +955,17 @@ PathItem.inject(new function() { }, resolveCrossings: function() { - var reportSegments = window.reportSegments; - var reportWindings = window.reportWindings; - var reportIntersections = window.reportIntersections; - window.reportSegments = false; - window.reportWindings = false; - window.reportIntersections = false; var crossings = this.getCrossings(); - if (!crossings.length) { - window.reportSegments = reportSegments; - window.reportWindings = reportWindings; - window.reportIntersections = reportIntersections; + if (!crossings.length) return this.reorient(); - } splitPath(CurveLocation.expand(crossings)); var paths = this._children || [this], segments = []; for (var i = 0, l = paths.length; i < l; i++) { segments.push.apply(segments, paths[i]._segments); } - var res = finishBoolean(CompoundPath, tracePaths(segments), + return finishBoolean(CompoundPath, tracePaths(segments), this, null, false).reorient(); - window.reportSegments = reportSegments; - window.reportWindings = reportWindings; - window.reportIntersections = reportIntersections; - return res; } }; }); From c1d0bd21b8437de0b0f055733c62a03c761e22ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 11 Oct 2015 16:59:03 +0200 Subject: [PATCH 240/280] Improve Curve#getParameterOf() to better handle very small curves. See #799 --- src/path/Curve.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index bbeeabd2..5c579ce2 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -668,17 +668,19 @@ statics: { getParameterOf: function(v, point) { var coords = [point.x, point.y], - roots = []; + roots = [], + epsilon = /*#=*/Numerical.GEOMETRIC_EPSILON; for (var c = 0; c < 2; c++) { var count = Curve.solveCubic(v, c, coords[c], roots, 0, 1); for (var i = 0; i < count; i++) { - var t = roots[i], - pt = Curve.getPoint(v, t); - if (point.isClose(pt, /*#=*/Numerical.GEOMETRIC_EPSILON)) + var t = roots[i]; + if (point.isClose(Curve.getPoint(v, t), epsilon)) return t; } } - return null; + return point.isClose(new Point(v[0], v[1]), epsilon) ? 0 + : point.isClose(new Point(v[6], v[7]), epsilon) ? 1 + : null; }, // TODO: Find better name From c9f5c02ee46a5758b1cf22330af77b8f9324e855 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 11 Oct 2015 17:00:01 +0200 Subject: [PATCH 241/280] Decrease GEOMETRIC_EPSILON This appears to be better aligned with the new Curve#getParameterOf() behavior. --- src/util/Numerical.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/Numerical.js b/src/util/Numerical.js index fe04af0e..f3501c1c 100644 --- a/src/util/Numerical.js +++ b/src/util/Numerical.js @@ -106,7 +106,7 @@ var Numerical = new function() { * collinearity. This value is somewhat arbitrary and was chosen by * trial and error. */ - GEOMETRIC_EPSILON: 1e-6, + GEOMETRIC_EPSILON: 1e-7, /** * The epsilon to be used when performing "trigonometric" checks, such * as examining cross products to check for collinearity. This value is From 4500e520ea12f6e150ae9b7a62610bccc88981fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 11 Oct 2015 17:00:23 +0200 Subject: [PATCH 242/280] Minor code clean-up. --- src/path/CurveLocation.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index 1333f84a..a5117d4e 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -43,9 +43,9 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ */ initialize: function CurveLocation(curve, parameter, point, _overlap, _distance) { - // Merge intersections very close to the end of a curve to the + // Merge intersections very close to the end of a curve with the // beginning of the next curve. - if (parameter >= 1 - /*#=*/Numerical.CURVETIME_EPSILON) { + if (parameter > /*#=*/(1 - Numerical.CURVETIME_EPSILON)) { var next = curve.getNext(); if (next) { parameter = 0; From bbc0029252cc609fa8ba62699f2d18b1f8171361 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 11 Oct 2015 17:05:23 +0200 Subject: [PATCH 243/280] Go back to simple overlap handling. It appears to produce less glitches. --- src/path/CurveLocation.js | 21 +++++---------------- src/path/PathItem.Boolean.js | 34 ++++++++++------------------------ 2 files changed, 15 insertions(+), 40 deletions(-) diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index a5117d4e..462592e7 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -60,7 +60,7 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ this._setCurve(curve); this._parameter = parameter; this._point = point || curve.getPointAt(parameter, true); - this._overlaps = _overlap ? [_overlap] : null; + this._overlap = _overlap; this._distance = _distance; this._intersection = this._next = this._prev = null; }, @@ -454,7 +454,7 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ * @see #isTouching() */ isOverlap: function() { - return !!this._overlaps; + return !!this._overlap; } }, Base.each(Curve.evaluateMethods, function(name) { // Produce getters for #getTangent() / #getNormal() / #getCurvature() @@ -478,16 +478,6 @@ new function() { // Scope for statics : path1._id - path2._id; } - function addOverlaps(loc1, loc2) { - var overlaps1 = loc1._overlaps, - overlaps2 = loc2._overlaps; - if (overlaps1) { - overlaps1.push.apply(overlaps1, overlaps2); - } else { - loc1._overlaps = overlaps2.slice(); - } - } - function insert(locations, loc, merge) { // Insert-sort by path-id, curve, parameter so we can easily merge // duplicates with calls to equals() after. @@ -527,10 +517,9 @@ new function() { // Scope for statics if (loc2 = loc.equals(loc2) ? loc2 : search(m, -1) || search(m, 1)) { // We're done, don't insert, merge with the found location - // instead, and carry over overlaps: - if (loc._overlaps) { - addOverlaps(loc2, loc); - addOverlaps(loc2._intersection, loc._intersection); + // instead, and carry over overlap: + if (loc._overlap) { + loc2._overlap = loc2._intersection._overlap = true; } return loc2; } diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 4dab1f07..90f855d0 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -165,10 +165,10 @@ PathItem.inject(new function() { var other = inter._intersection; var log = [title, inter._id, 'id', inter.getPath()._id, 'i', inter.getIndex(), 't', inter._parameter, - 'o', !!inter._overlaps, 'p', inter.getPoint(), + 'o', !!inter._overlap, 'p', inter.getPoint(), 'Other', other._id, 'id', other.getPath()._id, 'i', other.getIndex(), 't', other._parameter, - 'o', !!other._overlaps, 'p', other.getPoint()]; + 'o', !!other._overlap, 'p', other.getPoint()]; console.log(log.map(function(v) { return v == null ? '-' : v }).join(' ')); @@ -536,7 +536,7 @@ PathItem.inject(new function() { + ' v: ' + (seg._visited ? 1 : 0) + ' p: ' + seg._point + ' op: ' + isValid(seg) - + ' ov: ' + !!(inter && inter._overlaps) + + ' ov: ' + !!(inter && inter._overlap) + ' wi: ' + seg._winding + ' mu: ' + !!(inter && inter._next) , color); @@ -574,7 +574,7 @@ PathItem.inject(new function() { + ' n3x: ' + (n3xs && n3xs._path._id + '.' + n3xs._index + '(' + n3x._id + ')' || '--') + ' pt: ' + seg._point - + ' ov: ' + !!(inter && inter._overlaps) + + ' ov: ' + !!(inter && inter._overlap) + ' wi: ' + seg._winding , item.strokeColor || item.fillColor || 'black'); } @@ -596,7 +596,7 @@ PathItem.inject(new function() { return true; var winding = seg._winding, inter = seg._intersection; - if (inter && !unadjusted && overlapWinding && inter._overlaps) + if (inter && !unadjusted && overlapWinding && inter._overlap) winding = overlapWinding[winding] || winding; return operator(winding); } @@ -605,20 +605,6 @@ PathItem.inject(new function() { return seg === start || seg === otherStart; } - /** - * Checks if the curve from seg1 to seg2 is part of an overlap. - */ - function isOverlap(seg1, seg2) { - var inter = seg2._intersection, - overlaps = inter && inter._overlaps, - values = Curve.getValues(seg1, seg2); - for (var i = 0, l = overlaps && overlaps.length; i < l; i++) { - if (Curve.getOverlaps(values, overlaps[i])) - return true; - } - return false; - } - // If there are multiple possible intersections, find the one // that's either connecting back to start or is not visited yet, // and will be part of the boolean result: @@ -641,14 +627,14 @@ PathItem.inject(new function() { + ', next wi:' + nextSeg._winding + ', seg op:' + isValid(seg, true) + ', next op:' - + (!(strict && isOverlap(seg, nextSeg)) + + (!(strict && nextInter && nextInter._overlap) && isValid(nextSeg, true) || !strict && nextInter && isValid(nextInter._segment, true)) + ', seg ov: ' + !!(seg._intersection - && seg._intersection._overlaps) + && seg._intersection._overlap) + ', next ov: ' + !!(nextSeg._intersection - && nextSeg._intersection._overlaps) + && nextSeg._intersection._overlap) + ', more: ' + (!!inter._next)); } // See if this segment and the next are both not visited yet, or @@ -674,7 +660,7 @@ PathItem.inject(new function() { // Do not consider nextSeg in strict mode if it is part // of an overlap, in order to give non-overlapping // options that might follow the priority over overlaps. - && (!(strict && isOverlap(seg, nextSeg)) + && (!(strict && nextInter && nextInter._overlap) && isValid(nextSeg, true) // If the next segment isn't valid, its intersection // to which we may switch might be, so check that. @@ -741,7 +727,7 @@ PathItem.inject(new function() { // Switch to the intersecting segment, as we need to // resolving self-Intersections. seg = other; - } else if (inter._overlaps && operation !== 'intersect') { + } else if (inter._overlap && operation !== 'intersect') { // Switch to the overlapping intersecting segment if it is // part of the boolean result. Do not adjust for overlap! if (isValid(other, true)) { From 588ddbe011c0ba202318dd684d18852d57b236e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 11 Oct 2015 17:09:04 +0200 Subject: [PATCH 244/280] Add comments to Curve#getParameterOf() --- src/path/Curve.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/path/Curve.js b/src/path/Curve.js index 5c579ce2..d2823b69 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -678,6 +678,9 @@ statics: { return t; } } + // For very short curves (length ~ 1e-13), the above code will not + // necessarily produce any valid roots. As a fall-back, just check the + // beginnings and ends at the end so we can still return a valid result. return point.isClose(new Point(v[0], v[1]), epsilon) ? 0 : point.isClose(new Point(v[6], v[7]), epsilon) ? 1 : null; From f8edf5d8a7858fe00e271ba16923baaa3d5cf27b Mon Sep 17 00:00:00 2001 From: iconexperience Date: Mon, 12 Oct 2015 08:42:36 +0200 Subject: [PATCH 245/280] Small refactoring in getConvexHull() Make calculations of distances more concise. --- src/path/Curve.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index d2823b69..6811f153 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1557,8 +1557,8 @@ new function() { // Scope for intersection using bezier fat-line clipping p2 = [ 2 / 3, dq2 ], p3 = [ 1, dq3 ], // Find vertical signed distance of p1 and p2 from line [p0, p3] - dist1 = dq1 - (dq0 + (dq3 - dq0) / 3), - dist2 = dq2 - (dq0 + 2 * (dq3 - dq0) / 3), + dist1 = dq1 - (2 * dq0 + dq3) / 3, + dist2 = dq2 - (dq0 + 2 * dq3) / 3, hull; // Check if p1 and p2 are on the opposite side of the line [p0, p3] if (dist1 * dist2 < 0) { From f77579079e59c0dae9977b80bc55c57eb6267538 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 13 Oct 2015 00:06:34 +0200 Subject: [PATCH 246/280] Improve CurveTime#equals() to handle locations that wrap around beginnings / ends of paths. See https://github.com/paperjs/paper.js/issues/805#issuecomment-147470240 for details. --- src/path/CurveLocation.js | 59 +++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 34 deletions(-) diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index 462592e7..ec5a0e9f 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -188,20 +188,6 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ : parameter; }, - - /** - * The {@link #curve}'s {@link #index} and {@link #parameter} added to one - * value that can conveniently be used for sorting and comparing of - * locations. - * - * @type Number - * @bean - * @private - */ - getIndexParameter: function() { - return this.getIndex() + this.getParameter(); - }, - /** * The point which is defined by the {@link #curve} and * {@link #parameter}. @@ -308,26 +294,31 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ * @return {Boolean} {@true if the locations are equal} */ equals: function(loc, _ignoreOther) { - // NOTE: We need to compare both by getIndexParameter() and by proximity + if (this === loc) + return true; + // NOTE: We need to compare both by (index + parameter) and by proximity // of points, see: // https://github.com/paperjs/paper.js/issues/784#issuecomment-143161586 - // Use a relaxed threshold of < 1 for getIndexParameter() difference - // when deciding if two locations should be checked for point proximity. - // This is necessary to catch equal locations on very small curves. - var diff; - return this === loc - || loc instanceof CurveLocation - && this.getPath() === loc.getPath() - && ((diff = Math.abs( - this.getIndexParameter() - loc.getIndexParameter())) - < /*#=*/Numerical.CURVETIME_EPSILON + if (loc instanceof CurveLocation && this.getPath() === loc.getPath()) { + // We need to wrap the diff value around the path beginning / end. + var c1 = this.getCurve(), + c2 = loc.getCurve(); + diff = ((c1.isLast() && c2.isFirst() ? -1 : c1.getIndex()) + + this.getParameter()) + - ((c2.isLast() && c1.isFirst() ? -1 : c2.getIndex()) + + loc.getParameter()); + // Use a relaxed threshold of < 1 for difference when deciding if + // two locations should be checked for point proximity. This is + // necessary to catch equal locations on very small curves. + return (Math.abs(diff) < /*#=*/Numerical.CURVETIME_EPSILON || diff < 1 && this.getPoint().isClose(loc.getPoint(), /*#=*/Numerical.GEOMETRIC_EPSILON)) - && (_ignoreOther - || (!this._intersection && !loc._intersection - || this._intersection && this._intersection.equals( - loc._intersection, true))) - || false; + && (_ignoreOther + || (!this._intersection && !loc._intersection + || this._intersection && this._intersection.equals( + loc._intersection, true))) + } + return false; }, /** @@ -473,7 +464,10 @@ new function() { // Scope for statics var path1 = loc1.getPath(), path2 = loc2.getPath(); return path1 === path2 - ? loc1.getIndexParameter() - loc2.getIndexParameter() + //Sort by both index and parameter. The two values added + // together provides a convenient sorting index. + ? (loc1.getIndex() + loc1.getParameter()) + - (loc2.getIndex() + loc2.getParameter()) // Sort by path id to group all locs on same path. : path1._id - path2._id; } @@ -481,9 +475,6 @@ new function() { // Scope for statics function insert(locations, loc, merge) { // Insert-sort by path-id, curve, parameter so we can easily merge // duplicates with calls to equals() after. - // NOTE: We don't call getCurve() / getParameter() here, since this code - // is used internally in boolean operations where all this information - // remains valid during processing. var length = locations.length, l = 0, r = length - 1, From 3d33bbdfa3df50d9fc8e6ab1f72ecf15923b783d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 13 Oct 2015 00:10:21 +0200 Subject: [PATCH 247/280] Clean-up CurveLocation#equals() --- src/path/CurveLocation.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index ec5a0e9f..9d4a2b0b 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -294,12 +294,12 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ * @return {Boolean} {@true if the locations are equal} */ equals: function(loc, _ignoreOther) { - if (this === loc) - return true; - // NOTE: We need to compare both by (index + parameter) and by proximity - // of points, see: - // https://github.com/paperjs/paper.js/issues/784#issuecomment-143161586 - if (loc instanceof CurveLocation && this.getPath() === loc.getPath()) { + var res = this === loc; + if (!res && loc instanceof CurveLocation + && this.getPath() === loc.getPath()) { + // NOTE: We need to compare both by (index + parameter) and by + // proximity of points, see: + // https://github.com/paperjs/paper.js/issues/784#issuecomment-143161586 // We need to wrap the diff value around the path beginning / end. var c1 = this.getCurve(), c2 = loc.getCurve(); @@ -310,7 +310,7 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ // Use a relaxed threshold of < 1 for difference when deciding if // two locations should be checked for point proximity. This is // necessary to catch equal locations on very small curves. - return (Math.abs(diff) < /*#=*/Numerical.CURVETIME_EPSILON + res = (Math.abs(diff) < /*#=*/Numerical.CURVETIME_EPSILON || diff < 1 && this.getPoint().isClose(loc.getPoint(), /*#=*/Numerical.GEOMETRIC_EPSILON)) && (_ignoreOther @@ -318,7 +318,7 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ || this._intersection && this._intersection.equals( loc._intersection, true))) } - return false; + return res; }, /** From 0553201de85d2d2c2e5b4b53e513ab25dde286ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 13 Oct 2015 00:11:24 +0200 Subject: [PATCH 248/280] Add forgotten semi-colon. --- src/path/CurveLocation.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index 9d4a2b0b..6326d802 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -316,7 +316,7 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ && (_ignoreOther || (!this._intersection && !loc._intersection || this._intersection && this._intersection.equals( - loc._intersection, true))) + loc._intersection, true))); } return res; }, From 2e552853fd39c7b28226e0c9d321712d6f3e1a52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 13 Oct 2015 07:23:15 +0200 Subject: [PATCH 249/280] Handle paths as circular lists in CurveLocation.insert() as well. Relates to #805 --- src/path/CurveLocation.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index 6326d802..5ba720d5 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -300,7 +300,7 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ // NOTE: We need to compare both by (index + parameter) and by // proximity of points, see: // https://github.com/paperjs/paper.js/issues/784#issuecomment-143161586 - // We need to wrap the diff value around the path beginning / end. + // We need to wrap the diff value around the path's beginning / end. var c1 = this.getCurve(), c2 = loc.getCurve(); diff = ((c1.isLast() && c2.isFirst() ? -1 : c1.getIndex()) @@ -488,6 +488,13 @@ new function() { // Scope for statics break; if (loc.equals(loc2)) return loc2; + // If we reach the beginning/end of the list, also compare with + // the location at the other end, as paths are circular lists. + if (i === 0 || i === length - 1) { + loc2 = locations[i === 0 ? length - 1 : 0]; + if (loc.equals(loc2)) + return loc2; + } } return null; } From f6f6a58fe697a302cca41995c4ddde35f289c442 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 13 Oct 2015 07:27:25 +0200 Subject: [PATCH 250/280] Improve handling of paths as circular lists. --- src/path/CurveLocation.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index 5ba720d5..5f55be54 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -482,11 +482,10 @@ new function() { // Scope for statics function search(index, dir) { for (var i = index + dir; i >= 0 && i < length; i += dir) { - var loc2 = locations[i]; - // See #equals() for details of why `>= 1` is used here. - if (abs(compare(loc, loc2)) >= 1) - break; - if (loc.equals(loc2)) + var loc2 = locations[i], + diff = abs(compare(loc, loc2)); + // See #equals() for details of why `diff < 1` is used here. + if (diff < 1 && loc.equals(loc2)) return loc2; // If we reach the beginning/end of the list, also compare with // the location at the other end, as paths are circular lists. @@ -495,6 +494,9 @@ new function() { // Scope for statics if (loc.equals(loc2)) return loc2; } + // Once we're outside of the range, we can stop searching. + if (diff >= 1) + break; } return null; } From 9de6aa97f227811599c28a670adca7472f44609f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 13 Oct 2015 07:59:19 +0200 Subject: [PATCH 251/280] Third attempt at correctly handling paths as circular lists. Relates to #805. --- src/path/CurveLocation.js | 43 +++++++++++++++++---------------------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index 5f55be54..06d8b8a4 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -303,14 +303,15 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ // We need to wrap the diff value around the path's beginning / end. var c1 = this.getCurve(), c2 = loc.getCurve(); - diff = ((c1.isLast() && c2.isFirst() ? -1 : c1.getIndex()) - + this.getParameter()) - - ((c2.isLast() && c1.isFirst() ? -1 : c2.getIndex()) - + loc.getParameter()); + diff = Math.abs( + ((c1.isLast() && c2.isFirst() ? -1 : c1.getIndex()) + + this.getParameter()) - + ((c2.isLast() && c1.isFirst() ? -1 : c2.getIndex()) + + loc.getParameter())); // Use a relaxed threshold of < 1 for difference when deciding if // two locations should be checked for point proximity. This is // necessary to catch equal locations on very small curves. - res = (Math.abs(diff) < /*#=*/Numerical.CURVETIME_EPSILON + res = (diff < /*#=*/Numerical.CURVETIME_EPSILON || diff < 1 && this.getPoint().isClose(loc.getPoint(), /*#=*/Numerical.GEOMETRIC_EPSILON)) && (_ignoreOther @@ -463,6 +464,8 @@ new function() { // Scope for statics function compare(loc1, loc2) { var path1 = loc1.getPath(), path2 = loc2.getPath(); + // NOTE: equals() takes the intersection location into account, + // while this calculation of diff doesn't! return path1 === path2 //Sort by both index and parameter. The two values added // together provides a convenient sorting index. @@ -504,27 +507,19 @@ new function() { // Scope for statics while (l <= r) { var m = (l + r) >>> 1, loc2 = locations[m], - diff = compare(loc, loc2); - // Only compare location with equals() if diff is < 1. - // See #equals() for details of why `< 1` is used here. - // NOTE: equals() takes the intersection location into account, - // while the above calculation of diff doesn't! - if (merge && abs(diff) < 1) { - // See if the two locations are actually the same, and merge if - // they are. If they aren't, we're not done yet since all - // neighbors with a diff < 1 are potential merge candidates, so - // check them too (see #search() for details) - if (loc2 = loc.equals(loc2) ? loc2 - : search(m, -1) || search(m, 1)) { - // We're done, don't insert, merge with the found location - // instead, and carry over overlap: - if (loc._overlap) { - loc2._overlap = loc2._intersection._overlap = true; - } - return loc2; + found; + // See if the two locations are actually the same, and merge if + // they are. If they aren't check the other neighbors with search() + if (merge && (found = loc.equals(loc2) ? loc2 + : (search(m, -1) || search(m, 1)))) { + // We're done, don't insert, merge with the found location + // instead, and carry over overlap: + if (loc._overlap) { + found._overlap = found._intersection._overlap = true; } + return found; } - if (diff < 0) { + if (compare(loc, loc2) < 0) { r = m - 1; } else { l = m + 1; From 2bb3df3314047e0f40d57767c40c668131dfb9cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 13 Oct 2015 08:32:05 +0200 Subject: [PATCH 252/280] Simplify circular neighbor checks. Relates to #805. --- src/path/CurveLocation.js | 33 ++++++++++----------------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index 06d8b8a4..e6320ff6 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -480,28 +480,15 @@ new function() { // Scope for statics // duplicates with calls to equals() after. var length = locations.length, l = 0, - r = length - 1, - abs = Math.abs; + r = length - 1; - function search(index, dir) { - for (var i = index + dir; i >= 0 && i < length; i += dir) { - var loc2 = locations[i], - diff = abs(compare(loc, loc2)); - // See #equals() for details of why `diff < 1` is used here. - if (diff < 1 && loc.equals(loc2)) - return loc2; - // If we reach the beginning/end of the list, also compare with - // the location at the other end, as paths are circular lists. - if (i === 0 || i === length - 1) { - loc2 = locations[i === 0 ? length - 1 : 0]; - if (loc.equals(loc2)) - return loc2; - } - // Once we're outside of the range, we can stop searching. - if (diff >= 1) - break; - } - return null; + function check(index, dir) { + // If we reach the beginning/end of the list, compare with the + // location at the other end, as paths are circular lists. + var i = index + dir, + loc2 = locations[i >= 0 && i < length + ? i : (i < 0 ? length - 1 : 0)]; + return loc.equals(loc2) ? loc2 : null; } while (l <= r) { @@ -509,9 +496,9 @@ new function() { // Scope for statics loc2 = locations[m], found; // See if the two locations are actually the same, and merge if - // they are. If they aren't check the other neighbors with search() + // they are. If they aren't, check the neighbors through check() if (merge && (found = loc.equals(loc2) ? loc2 - : (search(m, -1) || search(m, 1)))) { + : (check(m, -1) || check(m, 1)))) { // We're done, don't insert, merge with the found location // instead, and carry over overlap: if (loc._overlap) { From 0ce825f8c90f24825bffeb7d065eefa891076f03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 13 Oct 2015 08:34:48 +0200 Subject: [PATCH 253/280] One more simplification. --- src/path/CurveLocation.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index e6320ff6..ed56b8e8 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -482,12 +482,11 @@ new function() { // Scope for statics l = 0, r = length - 1; - function check(index, dir) { + function check(index) { // If we reach the beginning/end of the list, compare with the // location at the other end, as paths are circular lists. - var i = index + dir, - loc2 = locations[i >= 0 && i < length - ? i : (i < 0 ? length - 1 : 0)]; + var loc2 = locations[index >= 0 && index < length + ? index : (index < 0 ? length - 1 : 0)]; return loc.equals(loc2) ? loc2 : null; } @@ -498,7 +497,7 @@ new function() { // Scope for statics // See if the two locations are actually the same, and merge if // they are. If they aren't, check the neighbors through check() if (merge && (found = loc.equals(loc2) ? loc2 - : (check(m, -1) || check(m, 1)))) { + : (check(m - 1) || check(m + 1)))) { // We're done, don't insert, merge with the found location // instead, and carry over overlap: if (loc._overlap) { From b5c59c881c3daf3ae5d54ed326e2a06bf109db28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 13 Oct 2015 09:35:08 +0200 Subject: [PATCH 254/280] Revert "One more simplification." This reverts commit 0ce825f8c90f24825bffeb7d065eefa891076f03. --- src/path/CurveLocation.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index ed56b8e8..e6320ff6 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -482,11 +482,12 @@ new function() { // Scope for statics l = 0, r = length - 1; - function check(index) { + function check(index, dir) { // If we reach the beginning/end of the list, compare with the // location at the other end, as paths are circular lists. - var loc2 = locations[index >= 0 && index < length - ? index : (index < 0 ? length - 1 : 0)]; + var i = index + dir, + loc2 = locations[i >= 0 && i < length + ? i : (i < 0 ? length - 1 : 0)]; return loc.equals(loc2) ? loc2 : null; } @@ -497,7 +498,7 @@ new function() { // Scope for statics // See if the two locations are actually the same, and merge if // they are. If they aren't, check the neighbors through check() if (merge && (found = loc.equals(loc2) ? loc2 - : (check(m - 1) || check(m + 1)))) { + : (check(m, -1) || check(m, 1)))) { // We're done, don't insert, merge with the found location // instead, and carry over overlap: if (loc._overlap) { From 9762d2c9e65396ed41ecd73e43ac3e2f0a4fd0bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 13 Oct 2015 09:35:13 +0200 Subject: [PATCH 255/280] Revert "Simplify circular neighbor checks." This reverts commit 2bb3df3314047e0f40d57767c40c668131dfb9cb. --- src/path/CurveLocation.js | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index e6320ff6..06d8b8a4 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -480,15 +480,28 @@ new function() { // Scope for statics // duplicates with calls to equals() after. var length = locations.length, l = 0, - r = length - 1; + r = length - 1, + abs = Math.abs; - function check(index, dir) { - // If we reach the beginning/end of the list, compare with the - // location at the other end, as paths are circular lists. - var i = index + dir, - loc2 = locations[i >= 0 && i < length - ? i : (i < 0 ? length - 1 : 0)]; - return loc.equals(loc2) ? loc2 : null; + function search(index, dir) { + for (var i = index + dir; i >= 0 && i < length; i += dir) { + var loc2 = locations[i], + diff = abs(compare(loc, loc2)); + // See #equals() for details of why `diff < 1` is used here. + if (diff < 1 && loc.equals(loc2)) + return loc2; + // If we reach the beginning/end of the list, also compare with + // the location at the other end, as paths are circular lists. + if (i === 0 || i === length - 1) { + loc2 = locations[i === 0 ? length - 1 : 0]; + if (loc.equals(loc2)) + return loc2; + } + // Once we're outside of the range, we can stop searching. + if (diff >= 1) + break; + } + return null; } while (l <= r) { @@ -496,9 +509,9 @@ new function() { // Scope for statics loc2 = locations[m], found; // See if the two locations are actually the same, and merge if - // they are. If they aren't, check the neighbors through check() + // they are. If they aren't check the other neighbors with search() if (merge && (found = loc.equals(loc2) ? loc2 - : (check(m, -1) || check(m, 1)))) { + : (search(m, -1) || search(m, 1)))) { // We're done, don't insert, merge with the found location // instead, and carry over overlap: if (loc._overlap) { From 3314668a0c8e873dd68ec9928f663110648f2d1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 14 Oct 2015 16:25:36 +0200 Subject: [PATCH 256/280] Streamline mouse event handling between View and Item. Consolidating code and making View#onMouseDown/Up/Move/... events work. --- src/core/Emitter.js | 2 +- src/item/Item.js | 92 ++++++++++++--------------------------------- src/view/View.js | 86 ++++++++++++++++++++++++++++++++---------- 3 files changed, 91 insertions(+), 89 deletions(-) diff --git a/src/core/Emitter.js b/src/core/Emitter.js index 3fc80423..43911bca 100644 --- a/src/core/Emitter.js +++ b/src/core/Emitter.js @@ -31,7 +31,7 @@ var Emitter = { handlers.push(func); // See if this is the first handler that we're attaching, // and call install if defined. - if (entry && entry.install && handlers.length == 1) + if (entry && entry.install && handlers.length === 1) entry.install.call(this, type); } } diff --git a/src/item/Item.js b/src/item/Item.js index ab0eb38e..2f9d2dde 100644 --- a/src/item/Item.js +++ b/src/item/Item.js @@ -125,79 +125,33 @@ var Item = Base.extend(Emitter, /** @lends Item# */{ return hasProps; }, - _events: new function() { - - // Flags defining which native events are required by which Paper events - // as required for counting amount of necessary natives events. - // The mapping is native -> virtual - var mouseFlags = { - mousedown: { - mousedown: 1, - mousedrag: 1, - click: 1, - doubleclick: 1 - }, - mouseup: { - mouseup: 1, - mousedrag: 1, - click: 1, - doubleclick: 1 - }, - mousemove: { - mousedrag: 1, - mousemove: 1, - mouseenter: 1, - mouseleave: 1 - } - }; - - // Entry for all mouse events in the _events list - var mouseEvent = { - install: function(type) { - // If the view requires counting of installed mouse events, - // increase the counters now according to mouseFlags - var counters = this.getView()._eventCounters; - if (counters) { - for (var key in mouseFlags) { - counters[key] = (counters[key] || 0) - + (mouseFlags[key][type] || 0); - } - } - }, - uninstall: function(type) { - // If the view requires counting of installed mouse events, - // decrease the counters now according to mouseFlags - var counters = this.getView()._eventCounters; - if (counters) { - for (var key in mouseFlags) - counters[key] -= mouseFlags[key][type] || 0; - } - } - }; - - return Base.each(['onMouseDown', 'onMouseUp', 'onMouseDrag', 'onClick', + _events: Base.each(['onMouseDown', 'onMouseUp', 'onMouseDrag', 'onClick', 'onDoubleClick', 'onMouseMove', 'onMouseEnter', 'onMouseLeave'], - function(name) { - this[name] = mouseEvent; - }, { - onFrame: { - install: function() { - this._animateItem(true); - }, - uninstall: function() { - this._animateItem(false); - } + function(name) { + this[name] = { + install: function(type) { + this.getView()._installEvent(type); }, - // Only for external sources, e.g. Raster - onLoad: {} - } - ); - }, + uninstall: function(type) { + this.getView()._uninstallEvent(type); + } + }; + }, { + onFrame: { + install: function() { + this.getView()._animateItem(this, true); + }, - _animateItem: function(animate) { - this.getView()._animateItem(this, animate); - }, + uninstall: function() { + this.getView()._animateItem(this, false); + } + }, + + // Only for external sources, e.g. Raster + onLoad: {} + } + ), _serialize: function(options, dictionary) { var props = {}, diff --git a/src/view/View.js b/src/view/View.js index 75a16d22..c40d8769 100644 --- a/src/view/View.js +++ b/src/view/View.js @@ -149,27 +149,29 @@ var View = Base.extend(Emitter, /** @lends View# */{ return true; }, - /** - * @namespace - * @ignore - */ - _events: { - /** - * @namespace - * @ignore - */ - onFrame: { - install: function() { - this.play(); - }, + _events: Base.each(['onResize', 'onMouseDown', 'onMouseUp', 'onMouseMove'], + function(name) { + this[name] = { + install: function(type) { + this._installEvent(type); + }, - uninstall: function() { - this.pause(); + uninstall: function(type) { + this._uninstallEvent(type); + } + }; + }, { + onFrame: { + install: function() { + this.play(); + }, + + uninstall: function() { + this.pause(); + } } - }, - - onResize: {} - }, + } + ), // These are default values for event related properties on the prototype. // Writing item._count++ does not change the defaults, it creates / updates @@ -828,12 +830,58 @@ new function() { // Injection scope for mouse events on the browser load: updateFocus }); + // Flags defining which native events are required by which Paper events + // as required for counting amount of necessary natives events. + // The mapping is native -> virtual + var mouseFlags = { + mousedown: { + mousedown: 1, + mousedrag: 1, + click: 1, + doubleclick: 1 + }, + mouseup: { + mouseup: 1, + mousedrag: 1, + click: 1, + doubleclick: 1 + }, + mousemove: { + mousedrag: 1, + mousemove: 1, + mouseenter: 1, + mouseleave: 1 + } + }; + return { _viewEvents: viewEvents, // To be defined in subclasses _handleEvent: function(/* type, point, event */) {}, + _installEvent: function(type) { + // If the view requires counting of installed mouse events, + // increase the counters now according to mouseFlags + var counters = this._eventCounters; + if (counters) { + for (var key in mouseFlags) { + counters[key] = (counters[key] || 0) + + (mouseFlags[key][type] || 0); + } + } + }, + + _uninstallEvent: function(type) { + // If the view requires counting of installed mouse events, + // decrease the counters now according to mouseFlags + var counters = this._eventCounters; + if (counters) { + for (var key in mouseFlags) + counters[key] -= mouseFlags[key][type] || 0; + } + }, + statics: { /** * Loops through all views and sets the focus on the first From f1debf401b0c414f59d31d669ef423e808a51eff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 20 Oct 2015 10:02:00 +0200 Subject: [PATCH 257/280] Streamline overlap handling code. --- src/path/Curve.js | 46 +++++++++++++++++++++------------------------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 6811f153..dd9fb346 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1731,8 +1731,9 @@ new function() { // Scope for intersection using bezier fat-line clipping if (overlaps) { for (var i = 0; i < 2; i++) { var overlap = overlaps[i]; - addLocation(locations, param, v1, c1, overlap[0], null, - v2, c2, overlap[1], null, overlap[2]); + addLocation(locations, param, + v1, c1, overlap[0], null, + v2, c2, overlap[1], null, true); } return locations; } @@ -1886,6 +1887,7 @@ new function() { // Scope for intersection using bezier fat-line clipping // too, otherwise they cannot overlap. return null; } + var v = [v1, v2], pairs = []; // Iterate through all end points: First p1 and p2 of curve 1, @@ -1901,38 +1903,32 @@ new function() { // Scope for intersection using bezier fat-line clipping var pair = i === 0 ? [t1, t2] : [t2, t1]; // Filter out tiny overlaps // TODO: Compare distance of points instead of curve time? - if (pairs.length === 0 - || abs(pair[0] - pairs[0][0]) > timeEpsilon - && abs(pair[1] - pairs[0][1]) > timeEpsilon) { + if (pairs.length === 0 || + abs(pair[0] - pairs[0][0]) > timeEpsilon && + abs(pair[1] - pairs[0][1]) > timeEpsilon) pairs.push(pair); - } } // If we checked 3 points but found no match, curves cannot // overlap if (i === 1 && pairs.length === 0) - return null; + break; } - // If we found 2 pairs, the end points of v1 & v2 should be the same. - // We only have to check if the handles are the same, too. - if (pairs.length === 2) { - // create values for overlapping part of each curve + if (pairs.length !== 2) { + pairs = null; + } else if (!straight) { + // Straight pairs don't need further checks. If we found 2 pairs + // the end points on v1 & v2 should be the same. We only have to + // check if the handles are the same, too. var o1 = Curve.getPart(v1, pairs[0][0], pairs[1][0]), o2 = Curve.getPart(v2, pairs[0][1], pairs[1][1]); - // Check if handles of overlapping paths are similar enough. We - // could do another check for curve identity here if we find a - // better criteria. - if (straight || - abs(o2[2] - o1[2]) < geomEpsilon && - abs(o2[3] - o1[3]) < geomEpsilon && - abs(o2[4] - o1[4]) < geomEpsilon && - abs(o2[5] - o1[5]) < geomEpsilon) { - // The overlapping parts are identical - pairs[0][2] = o1; - pairs[1][2] = o2; - return pairs; - } + // Check that handles of overlapping paths are similar enough. + if (abs(o2[2] - o1[2]) > geomEpsilon || + abs(o2[3] - o1[3]) > geomEpsilon || + abs(o2[4] - o1[4]) > geomEpsilon || + abs(o2[5] - o1[5]) > geomEpsilon) + pairs = null; } - return null; + return pairs; } }}; }); From e0c31e4a503ec615e3a84fdee5d6871f3d2c44f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 20 Oct 2015 10:02:33 +0200 Subject: [PATCH 258/280] Make static getIntersections() methods 'private'. --- src/path/Curve.js | 10 +++++----- src/path/PathItem.js | 18 ++++++++---------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index dd9fb346..7f98d9d1 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -435,7 +435,7 @@ var Curve = Base.extend(/** @lends Curve# */{ * curves */ getIntersections: function(curve) { - return Curve.getIntersections(this.getValues(), + return Curve._getIntersections(this.getValues(), curve && curve !== this ? curve.getValues() : null, this, curve, [], {}); }, @@ -1692,10 +1692,10 @@ new function() { // Scope for intersection using bezier fat-line clipping } return { statics: /** @lends Curve */{ - getIntersections: function(v1, v2, c1, c2, locations, param) { + _getIntersections: function(v1, v2, c1, c2, locations, param) { if (!v2) { // If v2 is not provided, search for self intersection on v1. - return Curve.getSelfIntersection(v1, c1, locations, param); + return Curve._getSelfIntersection(v1, c1, locations, param); } // Avoid checking curves if completely out of control bounds. As // a little optimization, we can scale the handles with 0.75 @@ -1773,7 +1773,7 @@ new function() { // Scope for intersection using bezier fat-line clipping return locations; }, - getSelfIntersection: function(v1, c1, locations, param) { + _getSelfIntersection: function(v1, c1, locations, param) { // Read a detailed description of the approach used to handle self- // intersection, developed by @iconexperience here: // https://github.com/paperjs/paper.js/issues/773#issuecomment-144018379 @@ -1845,7 +1845,7 @@ new function() { // Scope for intersection using bezier fat-line clipping param.renormalize = function(t1, t2) { return [t1 * tSplit, t2 * (1 - tSplit) + tSplit]; }; - Curve.getIntersections(parts[0], parts[1], c1, c1, + Curve._getIntersections(parts[0], parts[1], c1, c1, locations, param); } } diff --git a/src/path/PathItem.js b/src/path/PathItem.js index 2eb9429a..97c7e45b 100644 --- a/src/path/PathItem.js +++ b/src/path/PathItem.js @@ -88,15 +88,13 @@ var PathItem = Item.extend(/** @lends PathItem# */{ var curve1 = curves1[i], values1 = self ? values2[i] : curve1.getValues(matrix1); if (self) { - // First check for self-intersections within the same curve - Curve.getIntersections(values1, null, curve1, curve1, - locations, { - include: include, - // Only possible if there is only one closed curve: - startConnected: length1 === 1 && - curve1.getPoint1().equals(curve1.getPoint2()) - } - ); + // First check for self-intersections within the same curve. + Curve._getSelfIntersection(values1, curve1, locations, { + include: include, + // Only possible if there is only one closed curve: + startConnected: length1 === 1 && + curve1.getPoint1().equals(curve1.getPoint2()) + }); } // Check for intersections with other curves. For self intersection, // we can start at i + 1 instead of 0 @@ -108,7 +106,7 @@ var PathItem = Item.extend(/** @lends PathItem# */{ var curve2 = curves2[j]; // Avoid end point intersections on consecutive curves when // self intersecting. - Curve.getIntersections( + Curve._getIntersections( values1, values2[j], curve1, curve2, locations, { include: include, From 63303a59f49ef036671128451cab0afd5c65f1dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 20 Oct 2015 15:18:09 +0200 Subject: [PATCH 259/280] Change PathItem#getIntersections() so that the simply circularity checks in addLocations() work. This should address the concerns outlined in https://github.com/paperjs/paper.js/issues/805#issuecomment-147850806 --- src/path/Curve.js | 5 ++--- src/path/CurveLocation.js | 3 +++ src/path/PathItem.js | 37 +++++++++++++++++++++++++++---------- 3 files changed, 32 insertions(+), 13 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 7f98d9d1..db562138 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1917,11 +1917,10 @@ new function() { // Scope for intersection using bezier fat-line clipping pairs = null; } else if (!straight) { // Straight pairs don't need further checks. If we found 2 pairs - // the end points on v1 & v2 should be the same. We only have to - // check if the handles are the same, too. + // the end points on v1 & v2 should be the same. var o1 = Curve.getPart(v1, pairs[0][0], pairs[1][0]), o2 = Curve.getPart(v2, pairs[0][1], pairs[1][1]); - // Check that handles of overlapping paths are similar enough. + // Check if handles of the overlapping curves are the same too. if (abs(o2[2] - o1[2]) > geomEpsilon || abs(o2[3] - o1[3]) > geomEpsilon || abs(o2[4] - o1[4]) > geomEpsilon || diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index 06d8b8a4..43cabf71 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -492,6 +492,9 @@ new function() { // Scope for statics return loc2; // If we reach the beginning/end of the list, also compare with // the location at the other end, as paths are circular lists. + // NOTE: When merging, the locations array will only contain + // locations on the same path, so it is fine that check for the + // end to address circularity. See PathItem#getIntersections() if (i === 0 || i === length - 1) { loc2 = locations[i === 0 ? length - 1 : 0]; if (loc.equals(loc2)) diff --git a/src/path/PathItem.js b/src/path/PathItem.js index 97c7e45b..b8e6e13a 100644 --- a/src/path/PathItem.js +++ b/src/path/PathItem.js @@ -68,25 +68,37 @@ var PathItem = Item.extend(/** @lends PathItem# */{ // NOTE: The hidden argument _matrix is used internally to override the // passed path's transformation matrix. var self = this === path || !path, // self-intersections? - curves1 = this.getCurves(), - curves2 = self ? curves1 : path.getCurves(), matrix1 = this._matrix.orNullIfIdentity(), matrix2 = self ? matrix1 - : (_matrix || path._matrix).orNullIfIdentity(), - length1 = curves1.length, - length2 = self ? length1 : curves2.length, - locations = [], - values2 = []; + : (_matrix || path._matrix).orNullIfIdentity(); // First check the bounds of the two paths. If they don't intersect, // we don't need to iterate through their curves. if (!self && !this.getBounds(matrix1).touches(path.getBounds(matrix2))) - return locations; + return []; + var curves1 = this.getCurves(), + curves2 = self ? curves1 : path.getCurves(), + length1 = curves1.length, + length2 = self ? length1 : curves2.length, + values2 = [], + lists = [], + locations, + path; // Cache values for curves2 as we re-iterate them for each in curves1. for (var i = 0; i < length2; i++) values2[i] = curves2[i].getValues(matrix2); for (var i = 0; i < length1; i++) { var curve1 = curves1[i], - values1 = self ? values2[i] : curve1.getValues(matrix1); + values1 = self ? values2[i] : curve1.getValues(matrix1), + path1 = curve1.getPath(); + // NOTE: Due to the nature of Curve._getIntersections(), we need to + // use separate location arrays per path1, to make sure the + // circularity checks are not getting confused by locations on + // separate paths. We are flattening the separate arrays at the end. + if (path1 !== path) { + path = path1; + locations = []; + lists.push(locations); + } if (self) { // First check for self-intersections within the same curve. Curve._getSelfIntersection(values1, curve1, locations, { @@ -102,7 +114,7 @@ var PathItem = Item.extend(/** @lends PathItem# */{ // There might be already one location from the above // self-intersection check: if (_returnFirst && locations.length) - break; + return locations; var curve2 = curves2[j]; // Avoid end point intersections on consecutive curves when // self intersecting. @@ -119,6 +131,11 @@ var PathItem = Item.extend(/** @lends PathItem# */{ ); } } + // Now flatten the list of location arrays to one array and return it. + locations = []; + for (var i = 0, l = lists.length; i < l; i++) { + locations.push.apply(locations, lists[i]); + } return locations; }, From f2cce4c84d8be12781dcf550ac594a8ff2a7f6c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 20 Oct 2015 15:21:03 +0200 Subject: [PATCH 260/280] Rename variable to be less ambiguous. --- src/path/PathItem.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/path/PathItem.js b/src/path/PathItem.js index b8e6e13a..442468f5 100644 --- a/src/path/PathItem.js +++ b/src/path/PathItem.js @@ -80,7 +80,7 @@ var PathItem = Item.extend(/** @lends PathItem# */{ length1 = curves1.length, length2 = self ? length1 : curves2.length, values2 = [], - lists = [], + arrays = [], locations, path; // Cache values for curves2 as we re-iterate them for each in curves1. @@ -97,7 +97,7 @@ var PathItem = Item.extend(/** @lends PathItem# */{ if (path1 !== path) { path = path1; locations = []; - lists.push(locations); + arrays.push(locations); } if (self) { // First check for self-intersections within the same curve. @@ -133,8 +133,8 @@ var PathItem = Item.extend(/** @lends PathItem# */{ } // Now flatten the list of location arrays to one array and return it. locations = []; - for (var i = 0, l = lists.length; i < l; i++) { - locations.push.apply(locations, lists[i]); + for (var i = 0, l = arrays.length; i < l; i++) { + locations.push.apply(locations, arrays[i]); } return locations; }, From 93e9e54ae57760d8ea1b580eae83281d80fec002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 20 Oct 2015 15:34:09 +0200 Subject: [PATCH 261/280] Make sure we cannot find two intersections between two lines. Adresses point 2. in https://github.com/paperjs/paper.js/issues/805#issuecomment-148503018 --- src/path/Curve.js | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index db562138..4f380937 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1741,26 +1741,14 @@ new function() { // Scope for intersection using bezier fat-line clipping var straight1 = Curve.isStraight(v1), straight2 = Curve.isStraight(v2), - c1p1 = new Point(c1p1x, c1p1y), - c1p2 = new Point(c1p2x, c1p2y), - c2p1 = new Point(c2p1x, c2p1y), - c2p2 = new Point(c2p2x, c2p2y), + straight = straight1 && straight2, // NOTE: Use smaller Numerical.EPSILON to compare beginnings and // end points to avoid matching them on almost collinear lines. - epsilon = /*#=*/Numerical.EPSILON; - // Handle the special case where the first curve's start- or end- - // point overlap with the second curve's start- or end-point. - if (c1p1.isClose(c2p1, epsilon)) - addLocation(locations, param, v1, c1, 0, c1p1, v2, c2, 0, c2p1); - if (!param.startConnected && c1p1.isClose(c2p2, epsilon)) - addLocation(locations, param, v1, c1, 0, c1p1, v2, c2, 1, c2p2); - if (!param.endConnected && c1p2.isClose(c2p1, epsilon)) - addLocation(locations, param, v1, c1, 1, c1p2, v2, c2, 0, c2p1); - if (c1p2.isClose(c2p2, epsilon)) - addLocation(locations, param, v1, c1, 1, c1p2, v2, c2, 1, c2p2); + epsilon = /*#=*/Numerical.EPSILON, + before = locations.length; // Determine the correct intersection method based on whether one or // curves are straight lines: - (straight1 && straight2 + (straight ? addLineIntersection : straight1 || straight2 ? addCurveLineIntersections @@ -1770,6 +1758,24 @@ new function() { // Scope for intersection using bezier fat-line clipping // addCurveIntersections(): // tMin, tMax, uMin, uMax, oldTDiff, reverse, recursion 0, 1, 0, 1, 0, false, 0); + // We're done if we handle lines and found one intersection already: + // https://github.com/paperjs/paper.js/issues/805#issuecomment-148503018 + if (straight && locations.length > before) + return locations; + // Handle the special case where the first curve's start- or end- + // point overlaps with the second curve's start or end-point. + var c1p1 = new Point(c1p1x, c1p1y), + c1p2 = new Point(c1p2x, c1p2y), + c2p1 = new Point(c2p1x, c2p1y), + c2p2 = new Point(c2p2x, c2p2y); + if (c1p1.isClose(c2p1, epsilon)) + addLocation(locations, param, v1, c1, 0, c1p1, v2, c2, 0, c2p1); + if (!param.startConnected && c1p1.isClose(c2p2, epsilon)) + addLocation(locations, param, v1, c1, 0, c1p1, v2, c2, 1, c2p2); + if (!param.endConnected && c1p2.isClose(c2p1, epsilon)) + addLocation(locations, param, v1, c1, 1, c1p2, v2, c2, 0, c2p1); + if (c1p2.isClose(c2p2, epsilon)) + addLocation(locations, param, v1, c1, 1, c1p2, v2, c2, 1, c2p2); return locations; }, From da4395382887dbac1b96113434054ee1f2ef638d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 20 Oct 2015 16:34:40 +0200 Subject: [PATCH 262/280] For curves with only one segment, pick the smaller diff between the two locations. Addresses point 1. in https://github.com/paperjs/paper.js/issues/805#issuecomment-147770300 --- src/path/CurveLocation.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index 43cabf71..3a1fe05a 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -308,6 +308,10 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ + this.getParameter()) - ((c2.isLast() && c1.isFirst() ? -1 : c2.getIndex()) + loc.getParameter())); + // For curves with only one segment, pick the smaller diff between + // the two. + if (c1 === c2 && diff > 0.5 && c1.isFirst() && c1.isLast()) + diff = 1 - diff; // Use a relaxed threshold of < 1 for difference when deciding if // two locations should be checked for point proximity. This is // necessary to catch equal locations on very small curves. From 5dac7e9d29db8cd180591ca7dc7a9df6d3a91e14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 20 Oct 2015 16:35:30 +0200 Subject: [PATCH 263/280] Reduce maximum recursion again in addCurveIntersections() 32 has lead to many deadlocks. --- src/path/Curve.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 4f380937..56360668 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1456,7 +1456,7 @@ new function() { // Scope for intersection using bezier fat-line clipping // below when determining which curve converges the least. He also // recommended a threshold of 0.5 instead of the initial 0.8 // See: https://github.com/paperjs/paper.js/issues/565 - if (++recursion >= 32) + if (++recursion >= 24) return; // Let P be the first curve and Q be the second var q0x = v2[0], q0y = v2[1], q3x = v2[6], q3y = v2[7], @@ -1743,7 +1743,8 @@ new function() { // Scope for intersection using bezier fat-line clipping straight2 = Curve.isStraight(v2), straight = straight1 && straight2, // NOTE: Use smaller Numerical.EPSILON to compare beginnings and - // end points to avoid matching them on almost collinear lines. + // end points to avoid matching them on almost collinear lines, + // see: https://github.com/paperjs/paper.js/issues/777 epsilon = /*#=*/Numerical.EPSILON, before = locations.length; // Determine the correct intersection method based on whether one or From 7cea3488c0a2e06b76450008c0ceb560938d773b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 20 Oct 2015 17:17:31 +0200 Subject: [PATCH 264/280] Remove dependency on curve-time parameter when figuring out which locations to merge. --- src/path/CurveLocation.js | 56 +++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 32 deletions(-) diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index 3a1fe05a..adde8115 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -465,20 +465,6 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ }, {}), new function() { // Scope for statics - function compare(loc1, loc2) { - var path1 = loc1.getPath(), - path2 = loc2.getPath(); - // NOTE: equals() takes the intersection location into account, - // while this calculation of diff doesn't! - return path1 === path2 - //Sort by both index and parameter. The two values added - // together provides a convenient sorting index. - ? (loc1.getIndex() + loc1.getParameter()) - - (loc2.getIndex() + loc2.getParameter()) - // Sort by path id to group all locs on same path. - : path1._id - path2._id; - } - function insert(locations, loc, merge) { // Insert-sort by path-id, curve, parameter so we can easily merge // duplicates with calls to equals() after. @@ -488,24 +474,19 @@ new function() { // Scope for statics abs = Math.abs; function search(index, dir) { - for (var i = index + dir; i >= 0 && i < length; i += dir) { - var loc2 = locations[i], - diff = abs(compare(loc, loc2)); - // See #equals() for details of why `diff < 1` is used here. - if (diff < 1 && loc.equals(loc2)) + // If we reach the beginning/end of the list, also compare with the + // location at the other end, as paths are circular lists. + // NOTE: When merging, the locations array will only contain + // locations on the same path, so it is fine that check for the end + // to address circularity. See PathItem#getIntersections() + for (var i = index + dir; i >= -1 && i <= length; i += dir) { + // Wrap around the actual index, to match the other ends: + var loc2 = locations[((i % length) + length) % length]; + if (loc.equals(loc2)) return loc2; - // If we reach the beginning/end of the list, also compare with - // the location at the other end, as paths are circular lists. - // NOTE: When merging, the locations array will only contain - // locations on the same path, so it is fine that check for the - // end to address circularity. See PathItem#getIntersections() - if (i === 0 || i === length - 1) { - loc2 = locations[i === 0 ? length - 1 : 0]; - if (loc.equals(loc2)) - return loc2; - } - // Once we're outside of the range, we can stop searching. - if (diff >= 1) + // Once we're outside of the spot, we can stop searching. + if (!loc.getPoint().isClose(loc2.getPoint(), + Numerical.GEOMETRIC_EPSILON)) break; } return null; @@ -526,7 +507,18 @@ new function() { // Scope for statics } return found; } - if (compare(loc, loc2) < 0) { + var path1 = loc.getPath(), + path2 = loc2.getPath(), + // NOTE: equals() takes the intersection location into account, + // while this calculation of diff doesn't! + diff = path1 === path2 + //Sort by both index and parameter. The two values added + // together provides a convenient sorting index. + ? (loc.getIndex() + loc.getParameter()) + - (loc2.getIndex() + loc2.getParameter()) + // Sort by path id to group all locs on same path. + : path1._id - path2._id; + if (diff < 0) { r = m - 1; } else { l = m + 1; From 3ae0ca6c944afce83ff8946de41900967d44d61f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 20 Oct 2015 17:53:40 +0200 Subject: [PATCH 265/280] Remove dependency on curve-time comparisons when comparing locations. Locations on consecutive short curves (< 1e-7) where unable to merge due to diff > 1. Relates to #805 --- src/path/CurveLocation.js | 54 ++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index adde8115..df4e4c88 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -125,7 +125,7 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ // If the path's segments have changed in the meantime, clear the // internal _parameter value and force refetching of the correct // curve again here. - curve = this._parameter = this._curve = null; + curve = this._parameter = this._curve = this._offset = null; } // If path is out of sync, access current curve objects through segment1 @@ -208,8 +208,13 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ * @bean */ getOffset: function() { - var path = this.getPath(); - return path ? path._getOffset(this) : this.getCurveOffset(); + var offset = this._offset; + if (offset == null) { + var path = this.getPath(); + offset = this._offset = path ? path._getOffset(this) + : this.getCurveOffset(); + } + return offset; }, /** @@ -302,26 +307,27 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ // https://github.com/paperjs/paper.js/issues/784#issuecomment-143161586 // We need to wrap the diff value around the path's beginning / end. var c1 = this.getCurve(), - c2 = loc.getCurve(); - diff = Math.abs( + c2 = loc.getCurve(), + abs = Math.abs, + diff = abs( ((c1.isLast() && c2.isFirst() ? -1 : c1.getIndex()) + this.getParameter()) - ((c2.isLast() && c1.isFirst() ? -1 : c2.getIndex()) - + loc.getParameter())); - // For curves with only one segment, pick the smaller diff between - // the two. - if (c1 === c2 && diff > 0.5 && c1.isFirst() && c1.isLast()) - diff = 1 - diff; - // Use a relaxed threshold of < 1 for difference when deciding if - // two locations should be checked for point proximity. This is - // necessary to catch equal locations on very small curves. + + loc.getParameter())), + eps = /*#=*/Numerical.GEOMETRIC_EPSILON; res = (diff < /*#=*/Numerical.CURVETIME_EPSILON - || diff < 1 && this.getPoint().isClose(loc.getPoint(), - /*#=*/Numerical.GEOMETRIC_EPSILON)) - && (_ignoreOther - || (!this._intersection && !loc._intersection - || this._intersection && this._intersection.equals( - loc._intersection, true))); + // When the location is close enough, compare the offsets of + // both locations to determine if they're in the same spot, + // taking into account the wrapping around path ends too. + || this.getPoint().isClose(loc.getPoint(), eps) + // Use GEOMETRIC_EPSILON * 2 when comparing offsets, to + // slightly increase tolerance when in the same spot. + && ((diff = abs(this.getOffset() - loc.getOffset())) < eps * 2 + || abs(this.getPath().getLength() - diff) < eps * 2)) + && (_ignoreOther + || (!this._intersection && !loc._intersection + || this._intersection && this._intersection.equals( + loc._intersection, true))); } return res; }, @@ -480,14 +486,14 @@ new function() { // Scope for statics // locations on the same path, so it is fine that check for the end // to address circularity. See PathItem#getIntersections() for (var i = index + dir; i >= -1 && i <= length; i += dir) { - // Wrap around the actual index, to match the other ends: + // Wrap the index around, to match the other ends: var loc2 = locations[((i % length) + length) % length]; + // Once we're outside the spot, we can stop searching. + if (!loc.getPoint().isClose(loc2.getPoint(), + /*#=*/Numerical.GEOMETRIC_EPSILON)) + break; if (loc.equals(loc2)) return loc2; - // Once we're outside of the spot, we can stop searching. - if (!loc.getPoint().isClose(loc2.getPoint(), - Numerical.GEOMETRIC_EPSILON)) - break; } return null; } From 60109e897ad7a49c2840fc33ae3bc739f93561eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 20 Oct 2015 19:22:33 +0200 Subject: [PATCH 266/280] Use 'preserve' to protect #getPoint() against overriding. --- src/path/CurveLocation.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index df4e4c88..7135ebf2 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -460,15 +460,16 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ } }, Base.each(Curve.evaluateMethods, function(name) { // Produce getters for #getTangent() / #getNormal() / #getCurvature() - if (name !== 'getPoint') { - var get = name + 'At'; - this[name] = function() { - var parameter = this.getParameter(), - curve = this.getCurve(); - return parameter != null && curve && curve[get](parameter, true); - }; - } -}, {}), + var get = name + 'At'; + this[name] = function() { + var parameter = this.getParameter(), + curve = this.getCurve(); + return parameter != null && curve && curve[get](parameter, true); + }; +}, { + // Do not override the existing #getPoint(): + preserve: true +}), new function() { // Scope for statics function insert(locations, loc, merge) { From 447feea1da58c64291c47c2e58e74c77bb5ebdd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 20 Oct 2015 22:03:45 +0200 Subject: [PATCH 267/280] Improve Curve#getParameterOf() to first check curve points with zero epsilon. --- src/path/Curve.js | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 56360668..a7cad223 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -667,23 +667,35 @@ statics: { }, getParameterOf: function(v, point) { + // Before solving cubics, compare the beginning and end of the curve + // with zero epsilon: + var p1 = new Point(v[0], v[1]), + p2 = new Point(v[6], v[7]), + epsilon = /*#=*/Numerical.EPSILON, + t = point.isClose(p1, epsilon) ? 0 + : point.isClose(p2, epsilon) ? 1 + : null; + if (t !== null) + return t; + // Solve the cubic for both x- and y-coordinates and consider all found + // solutions, testing with the larger / looser geometric epsilon. var coords = [point.x, point.y], roots = [], - epsilon = /*#=*/Numerical.GEOMETRIC_EPSILON; + geomEpsilon = /*#=*/Numerical.GEOMETRIC_EPSILON; for (var c = 0; c < 2; c++) { var count = Curve.solveCubic(v, c, coords[c], roots, 0, 1); for (var i = 0; i < count; i++) { - var t = roots[i]; - if (point.isClose(Curve.getPoint(v, t), epsilon)) + t = roots[i]; + if (point.isClose(Curve.getPoint(v, t), geomEpsilon)) return t; } } // For very short curves (length ~ 1e-13), the above code will not // necessarily produce any valid roots. As a fall-back, just check the // beginnings and ends at the end so we can still return a valid result. - return point.isClose(new Point(v[0], v[1]), epsilon) ? 0 - : point.isClose(new Point(v[6], v[7]), epsilon) ? 1 - : null; + return point.isClose(p1, geomEpsilon) ? 0 + : point.isClose(p2, geomEpsilon) ? 1 + : null; }, // TODO: Find better name From 3aa7507ce1e63b050f52e932ecaa1db69e35e34a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 20 Oct 2015 22:04:04 +0200 Subject: [PATCH 268/280] Avoid issues with imprecision in CurveLocation#getCurve() / trySegment() --- src/path/CurveLocation.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index 7135ebf2..d475cf3d 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -81,6 +81,8 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ this._setCurve(segment.getCurve()); this._segment = segment; this._parameter = segment === this._segment1 ? 0 : 1; + // To avoid issues with imprecision in getCurve() / trySegment() + this._point = segment._point.clone(); }, /** From 7422e0710fb87ca5ecf10e0ddb8938426129689f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 20 Oct 2015 22:04:30 +0200 Subject: [PATCH 269/280] Some changes to boolean debug logging. --- src/path/PathItem.Boolean.js | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 90f855d0..7a15dd17 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -164,11 +164,11 @@ PathItem.inject(new function() { function logIntersection(title, inter) { var other = inter._intersection; var log = [title, inter._id, 'id', inter.getPath()._id, - 'i', inter.getIndex(), 't', inter._parameter, - 'o', !!inter._overlap, 'p', inter.getPoint(), + 'i', inter.getIndex(), 't', inter.getParameter(), + 'o', inter.isOverlap(), 'p', inter.getPoint(), 'Other', other._id, 'id', other.getPath()._id, - 'i', other.getIndex(), 't', other._parameter, - 'o', !!other._overlap, 'p', other.getPoint()]; + 'i', other.getIndex(), 't', other.getParameter(), + 'o', other.isOverlap(), 'p', other.getPoint()]; console.log(log.map(function(v) { return v == null ? '-' : v }).join(' ')); @@ -290,6 +290,21 @@ PathItem.inject(new function() { for (var i = 0, l = clearSegments.length; i < l; i++) { clearSegments[i].clearHandles(); } + + if (window.reportIntersections) { + console.log('After', locations.length / 2); + locations.forEach(function(inter) { + if (inter._other) + return; + logIntersection('Intersection', inter); + new Path.Circle({ + center: inter.point, + radius: 2 * scaleFactor, + strokeColor: 'red', + strokeScaling: false + }); + }); + } } /** @@ -536,7 +551,7 @@ PathItem.inject(new function() { + ' v: ' + (seg._visited ? 1 : 0) + ' p: ' + seg._point + ' op: ' + isValid(seg) - + ' ov: ' + !!(inter && inter._overlap) + + ' ov: ' + !!(inter && inter.isOverlap()) + ' wi: ' + seg._winding + ' mu: ' + !!(inter && inter._next) , color); @@ -574,7 +589,7 @@ PathItem.inject(new function() { + ' n3x: ' + (n3xs && n3xs._path._id + '.' + n3xs._index + '(' + n3x._id + ')' || '--') + ' pt: ' + seg._point - + ' ov: ' + !!(inter && inter._overlap) + + ' ov: ' + !!(inter && inter.isOverlap()) + ' wi: ' + seg._winding , item.strokeColor || item.fillColor || 'black'); } From 6ccd78e8af7cb116da3031dc755a90e5de0064a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 20 Oct 2015 23:02:19 +0200 Subject: [PATCH 270/280] Go back to simpler code to handle visited segments. It appears that the imprecisions addressed in 6cdead0e8c755c7ba9c75238be9806f482034b86 have since disappeared. --- src/path/PathItem.Boolean.js | 41 ++++++++---------------------------- 1 file changed, 9 insertions(+), 32 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 7a15dd17..c3cfde94 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -766,52 +766,29 @@ PathItem.inject(new function() { drawSegment(seg, null, 'stay', i, 'blue'); } if (seg._visited) { - finished = isStart(seg); - if (finished) { + if (isStart(seg)) { + finished = true; drawSegment(seg, null, 'done', i, 'red'); - } - if (!finished && inter) { + } else if (inter) { + // See if any of the intersections is the start segment, + // and if so finish the path. var found = findStartSegment(inter, true) || findStartSegment(inter, false); - // This should not happen but due to numerical - // imprecisions we sometimes end up in a dead-end. See - // if we can find a way out by checking all valid - // segments to find one that's close enough. - for (var j = 0; !found && j < l; j++) { - var seg2 = segments[j]; - // Do not start a chain with already visited - // segments, and segments that are not going to - // be part of the resulting operation. - if (seg !== seg2 - && seg._point.isClose(seg2._point, - /*#=*/Numerical.GEOMETRIC_EPSILON) - && (isStart(seg2) || isValid(seg2))) { - found = seg2; - } - } if (found) { seg = found; - finished = isStart(seg); - if (window.reportSegments) { - console.log('Switching to: ', - seg._path._id + '.' + seg._index); - } - if (finished) { - drawSegment(seg, null, 'done inter', i, 'red'); - } + finished = true; + drawSegment(seg, null, 'done multiple', i, 'red'); } } - if (finished) - break; - if (!isValid(seg)) { + if (!finished) { // We didn't manage to switch, so stop right here. console.error('Visited segment encountered, aborting #' + pathCount + '.' + (path ? path._segments.length + 1 : 1) + ', id: ' + seg._path._id + '.' + seg._index + ', multiple: ' + !!(inter && inter._next)); - break; } + break; } if (!path) { path = new Path(Item.NO_INSERT); From 08122131dcbc43518af34c93f5aa98f6f0953b79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 20 Oct 2015 23:02:50 +0200 Subject: [PATCH 271/280] Use isOverlap() instead of _overlap everywhere. --- src/path/PathItem.Boolean.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index c3cfde94..d8b46873 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -611,7 +611,7 @@ PathItem.inject(new function() { return true; var winding = seg._winding, inter = seg._intersection; - if (inter && !unadjusted && overlapWinding && inter._overlap) + if (inter && !unadjusted && overlapWinding && inter.isOverlap()) winding = overlapWinding[winding] || winding; return operator(winding); } @@ -641,15 +641,15 @@ PathItem.inject(new function() { + ', seg wi:' + seg._winding + ', next wi:' + nextSeg._winding + ', seg op:' + isValid(seg, true) - + ', next op:' - + (!(strict && nextInter && nextInter._overlap) - && isValid(nextSeg, true) + + ', next op:' + (!(strict && nextInter + && nextInter.isOverlap()) + && isValid(nextSeg, true) || !strict && nextInter && isValid(nextInter._segment, true)) + ', seg ov: ' + !!(seg._intersection - && seg._intersection._overlap) + && seg._intersection.isOverlap()) + ', next ov: ' + !!(nextSeg._intersection - && nextSeg._intersection._overlap) + && nextSeg._intersection.isOverlap()) + ', more: ' + (!!inter._next)); } // See if this segment and the next are both not visited yet, or @@ -675,7 +675,7 @@ PathItem.inject(new function() { // Do not consider nextSeg in strict mode if it is part // of an overlap, in order to give non-overlapping // options that might follow the priority over overlaps. - && (!(strict && nextInter && nextInter._overlap) + && (!(strict && nextInter && nextInter.isOverlap()) && isValid(nextSeg, true) // If the next segment isn't valid, its intersection // to which we may switch might be, so check that. @@ -742,7 +742,7 @@ PathItem.inject(new function() { // Switch to the intersecting segment, as we need to // resolving self-Intersections. seg = other; - } else if (inter._overlap && operation !== 'intersect') { + } else if (inter.isOverlap() && operation !== 'intersect') { // Switch to the overlapping intersecting segment if it is // part of the boolean result. Do not adjust for overlap! if (isValid(other, true)) { From bcd6520e66d5b9eff7fd410224a6d17bc6deda57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 20 Oct 2015 23:03:40 +0200 Subject: [PATCH 272/280] Merge handling of self-intersection crossings with normal crossings. Shorter code and no additional glitches. --- src/path/PathItem.Boolean.js | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index d8b46873..80a4b32e 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -737,10 +737,10 @@ PathItem.inject(new function() { // Just add the first segment and all segments that have no // intersection. drawSegment(seg, null, 'add', i, 'black'); - } else if (!operator) { // Resolve self-intersections - drawSegment(seg, other, 'self-int', i, 'purple'); - // Switch to the intersecting segment, as we need to - // resolving self-Intersections. + } else if (isValid(other)) { + // The other segment is part of the boolean result, and we + // are at crossing, switch over. + drawSegment(seg, other, 'cross', i, 'green'); seg = other; } else if (inter.isOverlap() && operation !== 'intersect') { // Switch to the overlapping intersecting segment if it is @@ -756,11 +756,6 @@ PathItem.inject(new function() { // switch at each crossing. drawSegment(seg, other, 'exclude-cross', i, 'green'); seg = other; - } else if (isValid(other)) { - // The other segment is part of the boolean result, and we - // are at crossing, switch over. - drawSegment(seg, other, 'cross', i, 'green'); - seg = other; } else { // Keep on truckin' drawSegment(seg, null, 'stay', i, 'blue'); From 140fba56cccf7d862aa8ba4d5ec2e032d78c009e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 20 Oct 2015 23:37:37 +0200 Subject: [PATCH 273/280] Fix Line#isCollinear() and #isOrthogonal() --- src/basic/Line.js | 4 ++-- src/path/Curve.js | 12 +++++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/basic/Line.js b/src/basic/Line.js index 5458471c..af99855c 100644 --- a/src/basic/Line.js +++ b/src/basic/Line.js @@ -113,11 +113,11 @@ var Line = Base.extend(/** @lends Line# */{ }, isCollinear: function(line) { - return Point.isCollinear(this._vx, this._vy, line._vx, line._yy); + return Point.isCollinear(this._vx, this._vy, line._vx, line._vy); }, isOrthogonal: function(line) { - return Point.isOrthogonal(this._vx, this._vy, line._vx, line._yy); + return Point.isOrthogonal(this._vx, this._vy, line._vx, line._vy); }, statics: /** @lends Line */{ diff --git a/src/path/Curve.js b/src/path/Curve.js index a7cad223..64003210 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -394,14 +394,12 @@ var Curve = Base.extend(/** @lends Curve# */{ }, /** - * The total direction of the curve as a vector pointing from - * {@link #point1} to {@link #point2}. - * - * @type Point + * @type Line * @bean + * @private */ - getVector: function() { - return this._segment2._point.subtract(this._segment1._point); + getLine: function() { + return new Line(this._segment1._point, this._segment2._point); }, /** @@ -960,7 +958,7 @@ statics: { */ isCollinear: function(curve) { return curve && this.isStraight() && curve.isStraight() - && this.getVector().isCollinear(curve.getVector()); + && this.getLine().isCollinear(curve.getLine()); }, /** From d543658c43d3571ca0fc8ad19d69effa24977f48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 21 Oct 2015 00:17:05 +0200 Subject: [PATCH 274/280] Remove old version of Curve#getParameterOf() --- src/path/Curve.js | 51 ----------------------------------------------- 1 file changed, 51 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 64003210..109f3a77 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -613,57 +613,6 @@ statics: { return Numerical.solveCubic(a, b, c, p1 - val, roots, min, max); }, - getParameterOf_: function(v, point) { - // Handle beginnings and end separately, as they are not detected - // sometimes. - var x = point.x, - y = point.y, - epsilon = /*#=*/Numerical.GEOMETRIC_EPSILON, - abs = Math.abs; - if (abs(v[0] - x) < epsilon && abs(v[1] - y) < epsilon) - return 0; - if (abs(v[6] - x) < epsilon && abs(v[7] - y) < epsilon) - return 1; - var txs = [], - tys = [], - sx = Curve.solveCubic(v, 0, x, txs, 0, 1), - // Only solve for y if x actually has some solutions - sy = sx !== 0 ? Curve.solveCubic(v, 1, y, tys, 0, 1) : 0, - tx, ty; - // sx, sy === -1 means infinite solutions. - // sx === -1 && sy === -1 means the curve is a point and there really is - // an infinite number of solutions. Let's just return t = 0, as they are - // all valid and actually end up being the same position. - if (sx === -1 && sy === -1) - return 0; - // Loop through all solutions for x and match with solutions for y, - // to see if we either have a matching pair, or infinite solutions - // for one or the other. - for (var cx = 0; sx === -1 || cx < sx;) { - if (sx === -1 || (tx = txs[cx++]) > 0 && tx < 1) { - for (var cy = 0; sy === -1 || cy < sy;) { - if (sy === -1 || (ty = tys[cy++]) > 0 && ty < 1) { - // Handle infinite solutions by assigning root of - // the other polynomial - if (sx === -1) { - tx = ty; - } else if (sy === -1) { - ty = tx; - } - // Use average if we're within curve-time epsilon - if (abs(tx - ty) < /*#=*/Numerical.CURVETIME_EPSILON) - return (tx + ty) * 0.5; - } - } - // Avoid endless loops here: If sx is infinite and there was - // no fitting ty, there's no solution for this bezier - if (sx === -1) - break; - } - } - return null; - }, - getParameterOf: function(v, point) { // Before solving cubics, compare the beginning and end of the curve // with zero epsilon: From 1073340eeb85313f69f21ecd4349afa61b3b7532 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 21 Oct 2015 01:09:03 +0200 Subject: [PATCH 275/280] Do not use GEOMETRIC_EPSILON in Curve.getParameterAt() This caused issues in some rare edge-cases. --- src/path/Curve.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index 109f3a77..aba2cbda 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1300,7 +1300,7 @@ new function() { // Scope for methods that require private functions // Get length of total range rangeLength = Numerical.integrate(ds, a, b, getIterations(a, b)); - if (abs(offset - rangeLength) < /*#=*/Numerical.GEOMETRIC_EPSILON) { + if (abs(offset - rangeLength) < /*#=*/Numerical.EPSILON) { // Matched the end: return forward ? b : a; } else if (abs(offset) > rangeLength) { From 1f476c2107580ab72759436825200a617ac3f1a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 21 Oct 2015 01:10:24 +0200 Subject: [PATCH 276/280] Improve CurveLocation#isTouching() to better handle straight lines. --- src/basic/Line.js | 4 ++-- src/path/CurveLocation.js | 14 ++++++++++---- src/path/PathItem.Boolean.js | 22 ++++++++-------------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/basic/Line.js b/src/basic/Line.js index af99855c..7ed66852 100644 --- a/src/basic/Line.js +++ b/src/basic/Line.js @@ -184,9 +184,9 @@ var Line = Base.extend(/** @lends Line# */{ } // Based on the error analysis by @iconexperience outlined in // https://github.com/paperjs/paper.js/issues/799 - return vx == 0 + return vx === 0 ? vy >= 0 ? px - x : x - px - : vy == 0 + : vy === 0 ? vx >= 0 ? y - py : py - y : (vx * (y - py) - vy * (x - px)) / Math.sqrt(vx * vx + vy * vy); } diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index d475cf3d..ab80b917 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -365,10 +365,16 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ * @see #isCrossing() */ isTouching: function() { - var t1 = this.getTangent(), - inter = this._intersection, - t2 = inter && inter.getTangent(); - return t1 && t2 ? t1.isCollinear(t2) : false; + var inter = this._intersection; + if (inter && this.getTangent().isCollinear(inter.getTangent())) { + // Only consider two straight curves as touching if their lines + // don't intersect. + var curve1 = this.getCurve(), + curve2 = inter.getCurve(); + return !(curve1.isStraight() && curve2.isStraight() + && curve1.getLine().intersect(curve2.getLine())); + } + return false; }, /** diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 80a4b32e..c038298a 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -119,7 +119,7 @@ PathItem.inject(new function() { var intersections = CurveLocation.expand( _path1.getIntersections(_path2, function(inter) { // Only handle overlaps when not self-intersecting - return inter.isCrossing() || _path2 && inter.isOverlap(); + return _path2 && inter.isOverlap() || inter.isCrossing(); }) ); // console.timeEnd('intersection'); @@ -161,9 +161,9 @@ PathItem.inject(new function() { path1, path2, true); } - function logIntersection(title, inter) { + function logIntersection(inter) { var other = inter._intersection; - var log = [title, inter._id, 'id', inter.getPath()._id, + var log = ['Intersection', inter._id, 'id', inter.getPath()._id, 'i', inter.getIndex(), 't', inter.getParameter(), 'o', inter.isOverlap(), 'p', inter.getPoint(), 'Other', other._id, 'id', other.getPath()._id, @@ -216,7 +216,7 @@ PathItem.inject(new function() { locations.forEach(function(inter) { if (inter._other) return; - logIntersection('Intersection', inter); + logIntersection(inter); new Path.Circle({ center: inter.point, radius: 2 * scaleFactor, @@ -292,17 +292,11 @@ PathItem.inject(new function() { } if (window.reportIntersections) { - console.log('After', locations.length / 2); + console.log('Split Crossings'); locations.forEach(function(inter) { - if (inter._other) - return; - logIntersection('Intersection', inter); - new Path.Circle({ - center: inter.point, - radius: 2 * scaleFactor, - strokeColor: 'red', - strokeScaling: false - }); + if (!inter._other) { + logIntersection(inter); + } }); } } From eb62530958048577c476c8793d01e96b6b0e8479 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 21 Oct 2015 01:15:46 +0200 Subject: [PATCH 277/280] Improve CurveLocation#equals(). --- src/path/CurveLocation.js | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index ab80b917..fccd561d 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -301,31 +301,33 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ * @return {Boolean} {@true if the locations are equal} */ equals: function(loc, _ignoreOther) { - var res = this === loc; + var res = this === loc, + epsilon = /*#=*/Numerical.GEOMETRIC_EPSILON; + // NOTE: We need to compare both by (index + parameter) and by proximity + // of points. See: + // https://github.com/paperjs/paper.js/issues/784#issuecomment-143161586 if (!res && loc instanceof CurveLocation - && this.getPath() === loc.getPath()) { - // NOTE: We need to compare both by (index + parameter) and by - // proximity of points, see: - // https://github.com/paperjs/paper.js/issues/784#issuecomment-143161586 - // We need to wrap the diff value around the path's beginning / end. + && this.getPath() === loc.getPath() + && this.getPoint().isClose(loc.getPoint(), epsilon)) { + // The position is the same, but it could still be in a different + // location on the path. Perform more thorough checks now: var c1 = this.getCurve(), c2 = loc.getCurve(), abs = Math.abs, + // We need to wrap diff around the path's beginning / end: diff = abs( ((c1.isLast() && c2.isFirst() ? -1 : c1.getIndex()) + this.getParameter()) - ((c2.isLast() && c1.isFirst() ? -1 : c2.getIndex()) - + loc.getParameter())), - eps = /*#=*/Numerical.GEOMETRIC_EPSILON; + + loc.getParameter())); res = (diff < /*#=*/Numerical.CURVETIME_EPSILON - // When the location is close enough, compare the offsets of + // If diff isn't close enough, compare the actual offsets of // both locations to determine if they're in the same spot, // taking into account the wrapping around path ends too. - || this.getPoint().isClose(loc.getPoint(), eps) - // Use GEOMETRIC_EPSILON * 2 when comparing offsets, to - // slightly increase tolerance when in the same spot. - && ((diff = abs(this.getOffset() - loc.getOffset())) < eps * 2 - || abs(this.getPath().getLength() - diff) < eps * 2)) + // This is necessary in order to handle very short consecutive + // curves (length ~< 1e-7), which would lead to diff > 1. + || ((diff = abs(this.getOffset() - loc.getOffset())) < epsilon + || abs(this.getPath().getLength() - diff) < epsilon)) && (_ignoreOther || (!this._intersection && !loc._intersection || this._intersection && this._intersection.equals( From 5d6b761d3a7d3277ee3d3bb2bb6f264414d8e8b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 21 Oct 2015 01:16:52 +0200 Subject: [PATCH 278/280] Introduce separate WINDING_EPSILON and improve GEOMETRIC_EPSILON. New values are based on a lot of testing. --- src/path/PathItem.Boolean.js | 2 +- src/util/Numerical.js | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index c038298a..71ad94d7 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -306,7 +306,7 @@ PathItem.inject(new function() { * with respect to a given set of monotone curves. */ function getWinding(point, curves, horizontal, testContains) { - var epsilon = /*#=*/Numerical.GEOMETRIC_EPSILON, + var epsilon = /*#=*/Numerical.WINDING_EPSILON, tMin = /*#=*/Numerical.CURVETIME_EPSILON, tMax = 1 - tMin, px = point.x, diff --git a/src/util/Numerical.js b/src/util/Numerical.js index f3501c1c..febde0c4 100644 --- a/src/util/Numerical.js +++ b/src/util/Numerical.js @@ -103,14 +103,16 @@ var Numerical = new function() { /** * The epsilon to be used when performing "geometric" checks, such as * point distances and examining cross products to check for - * collinearity. This value is somewhat arbitrary and was chosen by - * trial and error. + * collinearity. */ - GEOMETRIC_EPSILON: 1e-7, + GEOMETRIC_EPSILON: 5e-7, // NOTE: 1e-7 doesn't work in some edge-cases! + /** + * The epsilon to be used when performing winding contribution checks. + */ + WINDING_EPSILON: 2e-7, // NOTE: 1e-7 doesn't work in some edge-cases! /** * The epsilon to be used when performing "trigonometric" checks, such - * as examining cross products to check for collinearity. This value is - * somewhat arbitrary and was chosen by trial and error. + * as examining cross products to check for collinearity. */ TRIGONOMETRIC_EPSILON: 1e-8, // Kappa, see: http://www.whizkidtech.redprince.net/bezier/circle/kappa/ From 8c3d9df06c3dfd1a811403f85d13f6d2274f5b48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 21 Oct 2015 01:42:26 +0200 Subject: [PATCH 279/280] Further fine-tune the various EPSILON values based on edge-case tests. --- src/util/Numerical.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/util/Numerical.js b/src/util/Numerical.js index febde0c4..fc0f7159 100644 --- a/src/util/Numerical.js +++ b/src/util/Numerical.js @@ -96,16 +96,16 @@ var Numerical = new function() { MACHINE_EPSILON: MACHINE_EPSILON, /** * The epsilon to be used when handling curve-time parameters. This - * cannot be smaller, because errors add up to about 1e-7 in the bezier + * cannot be smaller, because errors add up to around 8e-7 in the bezier * fat-line clipping code as a result of recursive sub-division. */ - CURVETIME_EPSILON: 1e-6, + CURVETIME_EPSILON: 8e-7, /** * The epsilon to be used when performing "geometric" checks, such as * point distances and examining cross products to check for * collinearity. */ - GEOMETRIC_EPSILON: 5e-7, // NOTE: 1e-7 doesn't work in some edge-cases! + GEOMETRIC_EPSILON: 4e-7, // NOTE: 1e-7 doesn't work in some edge-cases! /** * The epsilon to be used when performing winding contribution checks. */ @@ -114,7 +114,7 @@ var Numerical = new function() { * The epsilon to be used when performing "trigonometric" checks, such * as examining cross products to check for collinearity. */ - TRIGONOMETRIC_EPSILON: 1e-8, + TRIGONOMETRIC_EPSILON: 1e-7, // Kappa, see: http://www.whizkidtech.redprince.net/bezier/circle/kappa/ KAPPA: 4 * (sqrt(2) - 1) / 3, From 43cf20096ae049a8c4b140e261e61b38faebbffc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 21 Oct 2015 01:43:14 +0200 Subject: [PATCH 280/280] Implement Curve.getNearestParameter() --- src/path/Curve.js | 58 +++++++++++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index aba2cbda..6c9d696a 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -645,6 +645,34 @@ statics: { : null; }, + getNearestParameter: function(v, point) { + var count = 100, + minDist = Infinity, + minT = 0; + + function refine(t) { + if (t >= 0 && t <= 1) { + var dist = point.getDistance(Curve.getPoint(v, t), true); + if (dist < minDist) { + minDist = dist; + minT = t; + return true; + } + } + } + + for (var i = 0; i <= count; i++) + refine(i / count); + + // Now iteratively refine solution until we reach desired precision. + var step = 1 / (count * 2); + while (step > /*#=*/Numerical.CURVETIME_EPSILON) { + if (!refine(minT - step) && !refine(minT + step)) + step /= 2; + } + return minT; + }, + // TODO: Find better name getPart: function(v, from, to) { var flip = from > to; @@ -1015,32 +1043,9 @@ statics: { getNearestLocation: function(/* point */) { var point = Point.read(arguments), values = this.getValues(), - count = 100, - minDist = Infinity, - minT = 0; - - function refine(t) { - if (t >= 0 && t <= 1) { - var dist = point.getDistance(Curve.getPoint(values, t), true); - if (dist < minDist) { - minDist = dist; - minT = t; - return true; - } - } - } - - for (var i = 0; i <= count; i++) - refine(i / count); - - // Now iteratively refine solution until we reach desired precision. - var step = 1 / (count * 2); - while (step > /*#=*/Numerical.CURVETIME_EPSILON) { - if (!refine(minT - step) && !refine(minT + step)) - step /= 2; - } - var pt = Curve.getPoint(values, minT); - return new CurveLocation(this, minT, pt, null, point.getDistance(pt)); + t = Curve.getNearestParameter(values, point), + pt = Curve.getPoint(values, t); + return new CurveLocation(this, t, pt, null, point.getDistance(pt)); }, /** @@ -1861,7 +1866,6 @@ new function() { // Scope for intersection using bezier fat-line clipping for (var i = 0, t1 = 0; i < 2 && pairs.length < 2; i += t1 === 0 ? 0 : 1, t1 = t1 ^ 1) { - // TODO: Try with getNearestLocation() instead var t2 = Curve.getParameterOf(v[i ^ 1], new Point( v[i][t1 === 0 ? 0 : 6], v[i][t1 === 0 ? 1 : 7]));