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] 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,