Introduce CURVETIME_EPSILON, to be used when handling curve time parameters.

Relates to #777
This commit is contained in:
Jürg Lehni 2015-09-12 22:55:58 +02:00
parent 4f04dae20f
commit d62caf6faa
6 changed files with 55 additions and 51 deletions

View file

@ -442,10 +442,11 @@ var Curve = Base.extend(/** @lends Curve# */{
// TODO: Rename to divideAt()? // TODO: Rename to divideAt()?
divide: function(offset, isParameter, ignoreStraight) { divide: function(offset, isParameter, ignoreStraight) {
var parameter = this._getParameter(offset, isParameter), var parameter = this._getParameter(offset, isParameter),
tolerance = /*#=*/Numerical.TOLERANCE, tMin = /*#=*/Numerical.CURVETIME_EPSILON,
tMax = 1 - tMin,
res = null; res = null;
// Only divide if not at the beginning or end. // 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), var parts = Curve.subdivide(this.getValues(), parameter),
setHandles = ignoreStraight || this.hasHandles(), setHandles = ignoreStraight || this.hasHandles(),
left = parts[0], left = parts[0],
@ -618,8 +619,8 @@ statics: {
} else if (sy === -1) { } else if (sy === -1) {
ty = tx; ty = tx;
} }
// Use average if we're within tolerance // Use average if we're within epsilon
if (abs(tx - ty) < /*#=*/Numerical.TOLERANCE) if (abs(tx - ty) < /*#=*/Numerical.CURVETIME_EPSILON)
return (tx + ty) * 0.5; return (tx + ty) * 0.5;
} }
} }
@ -726,7 +727,7 @@ statics: {
// Add some tolerance for good roots, as t = 0, 1 are added // 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 // separately anyhow, and we don't want joins to be added with radii
// in getStrokeBounds() // in getStrokeBounds()
tMin = /*#=*/Numerical.TOLERANCE, tMin = /*#=*/Numerical.CURVETIME_EPSILON,
tMax = 1 - tMin; tMax = 1 - tMin;
// Only add strokeWidth to bounds for points which lie within 0 < t < 1 // Only add strokeWidth to bounds for points which lie within 0 < t < 1
// The corner cases for cap and join are handled in getStrokeBounds() // 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. // Now iteratively refine solution until we reach desired precision.
var step = 1 / (count * 2); var step = 1 / (count * 2);
while (step > /*#=*/Numerical.TOLERANCE) { while (step > /*#=*/Numerical.CURVETIME_EPSILON) {
if (!refine(minT - step) && !refine(minT + step)) if (!refine(minT - step) && !refine(minT + step))
step /= 2; step /= 2;
} }
@ -1155,12 +1156,13 @@ new function() { // Scope for methods that require private functions
c1x = v[2], c1y = v[3], c1x = v[2], c1y = v[3],
c2x = v[4], c2y = v[5], c2x = v[4], c2y = v[5],
p2x = v[6], p2y = v[7], p2x = v[6], p2y = v[7],
tolerance = /*#=*/Numerical.TOLERANCE, tMin = /*#=*/Numerical.CURVETIME_EPSILON,
tMax = 1 - tMin,
x, y; x, y;
// Handle special case at beginning / end of curve // Handle special case at beginning / end of curve
if (type === 0 && (t < tolerance || t > 1 - tolerance)) { if (type === 0 && (t < tMin || t > tMax)) {
var isZero = t < tolerance; var isZero = t < tMin;
x = isZero ? p1x : p2x; x = isZero ? p1x : p2x;
y = isZero ? p1y : p2y; y = isZero ? p1y : p2y;
} else { } else {
@ -1184,10 +1186,10 @@ new function() { // Scope for methods that require private functions
// the x and y coordinates: // the x and y coordinates:
// Prevent tangents and normals of length 0: // Prevent tangents and normals of length 0:
// http://stackoverflow.com/questions/10506868/ // http://stackoverflow.com/questions/10506868/
if (t < tolerance) { if (t < tMin) {
x = cx; x = cx;
y = cy; y = cy;
} else if (t > 1 - tolerance) { } else if (t > tMax) {
x = 3 * (p2x - c2x); x = 3 * (p2x - c2x);
y = 3 * (p2y - c2y); y = 3 * (p2y - c2y);
} else { } 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 // When the tangent at t is zero and we're at the beginning
// or the end, we can use the vector between the handles, // or the end, we can use the vector between the handles,
// but only when normalizing as its weighted length is 0. // but only when normalizing as its weighted length is 0.
if (x === 0 && y === 0 if (x === 0 && y === 0 && (t < tMin || t > tMax)) {
&& (t < tolerance || t > 1 - tolerance)) {
x = c2x - c1x; x = c2x - c1x;
y = c2y - c1y; y = c2y - c1y;
} }
@ -1250,8 +1251,7 @@ new function() { // Scope for methods that require private functions
return start; return start;
// See if we're going forward or backward, and handle cases // See if we're going forward or backward, and handle cases
// differently // differently
var tolerance = /*#=*/Numerical.TOLERANCE, var abs = Math.abs,
abs = Math.abs,
forward = offset > 0, forward = offset > 0,
a = forward ? start : 0, a = forward ? start : 0,
b = forward ? 1 : start, b = forward ? 1 : start,
@ -1261,7 +1261,7 @@ new function() { // Scope for methods that require private functions
// Get length of total range // Get length of total range
rangeLength = Numerical.integrate(ds, a, b, rangeLength = Numerical.integrate(ds, a, b,
getIterations(a, b)); getIterations(a, b));
if (abs(offset - rangeLength) < tolerance) { if (abs(offset - rangeLength) < /*#=*/Numerical.GEOMETRIC_EPSILON) {
// Matched the end: // Matched the end:
return forward ? b : a; return forward ? b : a;
} else if (abs(offset) > rangeLength) { } 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. // Start with out initial guess for x.
// NOTE: guess is a negative value when not looking forward. // NOTE: guess is a negative value when not looking forward.
return Numerical.findRoot(f, ds, start + guess, a, b, 16, return Numerical.findRoot(f, ds, start + guess, a, b, 16,
tolerance); /*#=*/Numerical.CURVETIME_EPSILON);
}, },
getPoint: function(v, t) { 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, function addLocation(locations, param, v1, c1, t1, p1, v2, c2, t2, p2,
overlap) { overlap) {
var loc = null, var loc = null,
tMin = /*#=*/Numerical.TOLERANCE, tMin = /*#=*/Numerical.CURVETIME_EPSILON,
tMax = 1 - tMin; tMax = 1 - tMin;
if (t1 == null) if (t1 == null)
t1 = Curve.getParameterOf(v1, p1.x, p1.y); t1 = Curve.getParameterOf(v1, p1.x, p1.y);
@ -1353,7 +1353,7 @@ new function() { // Scope for intersection using bezier fat-line clipping
return; return;
// Let P be the first curve and Q be the second // Let P be the first curve and Q be the second
var q0x = v2[0], q0y = v2[1], q3x = v2[6], q3y = v2[7], var q0x = v2[0], q0y = v2[1], q3x = v2[6], q3y = v2[7],
tolerance = /*#=*/Numerical.TOLERANCE, epsilon = /*#=*/Numerical.CURVETIME_EPSILON,
getSignedDistance = Line.getSignedDistance, getSignedDistance = Line.getSignedDistance,
// Calculate the fat-line L for Q is the baseline l and two // Calculate the fat-line L for Q is the baseline l and two
// offsets which completely encloses the curve P. // 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]), dp3 = getSignedDistance(q0x, q0y, q3x, q3y, v1[6], v1[7]),
tMinNew, tMaxNew, tMinNew, tMaxNew,
tDiff; 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 // 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 // reliable. Return the value we have even though we will miss the
// precision. // precision.
@ -1419,7 +1419,7 @@ new function() { // Scope for intersection using bezier fat-line clipping
parts[1], v1, c2, c1, locations, param, parts[1], v1, c2, c1, locations, param,
t, uMax, tMinNew, tMaxNew, tDiff, !reverse, recursion); 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 // We have isolated the intersection with sufficient precision
var t1 = tMinNew + (tMaxNew - tMinNew) / 2, var t1 = tMinNew + (tMaxNew - tMinNew) / 2,
t2 = uMin + (uMax - uMin) / 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) { function addOverlap(v1, v2, c1, c2, locations, param) {
var abs = Math.abs, var abs = Math.abs,
tolerance = /*#=*/Numerical.TOLERANCE, timeEpsilon = /*#=*/Numerical.CURVETIME_EPSILON,
epsilon = /*#=*/Numerical.EPSILON, geomEpsilon = /*#=*/Numerical.GEOMETRIC_EPSILON,
straight1 = Curve.isStraight(v1), straight1 = Curve.isStraight(v1),
straight2 = Curve.isStraight(v2), straight2 = Curve.isStraight(v2),
straight = straight1 && straight2; 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]), var line1 = new Line(v1[0], v1[1], v1[6], v1[7]),
line2 = new Line(v2[0], v2[1], v2[6], v2[7]); line2 = new Line(v2[0], v2[1], v2[6], v2[7]);
if (!line1.isCollinear(line2) || line1.getDistance(line2.getPoint()) if (!line1.isCollinear(line2) || line1.getDistance(line2.getPoint())
> /*#=*/Numerical.GEOMETRIC_EPSILON) > geomEpsilon)
return false; return false;
} else if (straight1 ^ straight2) { } else if (straight1 ^ straight2) {
// If one curve is straight, the other curve must be straight, too, // 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]) { if (pairs.length === 1 && pair[0] < pairs[0][0]) {
pairs.unshift(pair); pairs.unshift(pair);
} else if (pairs.length === 0 } else if (pairs.length === 0
|| abs(pair[0] - pairs[0][0]) > tolerance || abs(pair[0] - pairs[0][0]) > timeEpsilon
|| abs(pair[1] - pairs[0][1]) > tolerance) { || abs(pair[1] - pairs[0][1]) > timeEpsilon) {
pairs.push(pair); 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 // We could do another check for curve identity here if we find a
// better criteria. // better criteria.
if (straight || if (straight ||
abs(p2[2] - p1[2]) < epsilon && abs(p2[2] - p1[2]) < geomEpsilon &&
abs(p2[3] - p1[3]) < epsilon && abs(p2[3] - p1[3]) < geomEpsilon &&
abs(p2[4] - p1[4]) < epsilon && abs(p2[4] - p1[4]) < geomEpsilon &&
abs(p2[5] - p1[5]) < epsilon) { abs(p2[5] - p1[5]) < geomEpsilon) {
// Overlapping parts are identical // Overlapping parts are identical
addLocation(locations, param, v1, c1, pairs[0][0], null, addLocation(locations, param, v1, c1, pairs[0][0], null,
v2, c2, pairs[0][1], null, true), v2, c2, pairs[0][1], null, true),

View file

@ -45,7 +45,7 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{
_distance, _overlap, _intersection) { _distance, _overlap, _intersection) {
// Merge intersections very close to the end of a curve to the // Merge intersections very close to the end of a curve to the
// beginning of the next curve. // beginning of the next curve.
if (parameter >= 1 - /*#=*/Numerical.TOLERANCE) { if (parameter >= 1 - /*#=*/Numerical.CURVETIME_EPSILON) {
var next = curve.getNext(); var next = curve.getNext();
if (next) { if (next) {
parameter = 0; parameter = 0;
@ -286,7 +286,7 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{
// Use the same tolerance for curve time parameter // Use the same tolerance for curve time parameter
// comparisons as in Curve.js // comparisons as in Curve.js
&& Math.abs(this.getParameter() - loc.getParameter()) && Math.abs(this.getParameter() - loc.getParameter())
< /*#=*/Numerical.TOLERANCE < /*#=*/Numerical.CURVETIME_EPSILON
&& (_ignoreIntersection && (_ignoreIntersection
|| (!this._intersection && !loc._intersection || (!this._intersection && !loc._intersection
|| this._intersection && this._intersection.equals( || this._intersection && this._intersection.equals(
@ -316,7 +316,6 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{
statics: { statics: {
sort: function(locations) { sort: function(locations) {
var tolerance = /*#=*/Numerical.TOLERANCE;
locations.sort(function compare(l1, l2) { locations.sort(function compare(l1, l2) {
var curve1 = l1._curve, var curve1 = l1._curve,
curve2 = l2._curve, curve2 = l2._curve,
@ -331,7 +330,7 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{
if (path1 === path2) { if (path1 === path2) {
if (curve1 === curve2) { if (curve1 === curve2) {
var diff = l1._parameter - l2._parameter; var diff = l1._parameter - l2._parameter;
if (Math.abs(diff) < tolerance) { if (Math.abs(diff) < /*#=*/Numerical.CURVETIME_EPSILON){
var i1 = l1._intersection, var i1 = l1._intersection,
i2 = l2._intersection, i2 = l2._intersection,
curve21 = i1 && i1._curve, curve21 = i1 && i1._curve,

View file

@ -1182,7 +1182,7 @@ var Path = PathItem.extend(/** @lends Path# */{
index = arg.index; index = arg.index;
parameter = arg.parameter; parameter = arg.parameter;
} }
var tMin = /*#=*/Numerical.TOLERANCE, var tMin = /*#=*/Numerical.CURVETIME_EPSILON,
tMax = 1 - tMin; tMax = 1 - tMin;
if (parameter >= tMax) { if (parameter >= tMax) {
// t == 1 is the same as t == 0 and index ++ // t == 1 is the same as t == 0 and index ++

View file

@ -100,7 +100,7 @@ PathItem.inject(new function() {
segments = [], segments = [],
// Aggregate of all curves in both operands, monotonic in y // Aggregate of all curves in both operands, monotonic in y
monoCurves = [], monoCurves = [],
tolerance = /*#=*/Numerical.TOLERANCE; epsilon = /*#=*/Numerical.GEOMETRIC_EPSILON;
function collect(paths) { function collect(paths) {
for (var i = 0, l = paths.length; i < l; i++) { for (var i = 0, l = paths.length; i < l; i++) {
@ -154,8 +154,7 @@ PathItem.inject(new function() {
if (length <= curveLength) { if (length <= curveLength) {
// If the selected location on the curve falls onto its // If the selected location on the curve falls onto its
// beginning or end, use the curve's center instead. // beginning or end, use the curve's center instead.
if (length < tolerance if (length < epsilon || curveLength - length < epsilon)
|| curveLength - length < tolerance)
length = curveLength / 2; length = curveLength / 2;
var curve = node.segment.getCurve(), var curve = node.segment.getCurve(),
pt = curve.getPointAt(length), pt = curve.getPointAt(length),
@ -227,15 +226,20 @@ PathItem.inject(new function() {
if (false) { if (false) {
console.log('Intersections', intersections.length); console.log('Intersections', intersections.length);
intersections.forEach(function(inter) { intersections.forEach(function(inter) {
if (inter._other)
return;
var other = inter._intersection;
var log = ['CurveLocation', inter._id, 'p', inter.getPath()._id, var log = ['CurveLocation', inter._id, 'p', inter.getPath()._id,
'i', inter.getIndex(), 't', inter._parameter, 'i', inter.getIndex(), 't', inter._parameter,
'o', !!inter._overlap]; 'o', !!inter._overlap,
if (inter._other) { 'Other', other._id, 'p', other.getPath()._id,
inter = inter._intersection; 'i', other.getIndex(), 't', other._parameter,
log.push('Other', inter._id, 'p', inter.getPath()._id, 'o', !!other._overlap];
'i', inter.getIndex(), 't', inter._parameter, new Path.Circle({
'o', !!inter._overlap); center: inter.point,
} radius: 3,
strokeColor: 'green'
});
console.log(log.map(function(v) { console.log(log.map(function(v) {
return v == null ? '-' : v return v == null ? '-' : v
}).join(' ')); }).join(' '));
@ -243,7 +247,7 @@ PathItem.inject(new function() {
} }
// TODO: Make public in API, since useful! // TODO: Make public in API, since useful!
var tMin = /*#=*/Numerical.TOLERANCE, var tMin = /*#=*/Numerical.CURVETIME_EPSILON,
tMax = 1 - tMin, tMax = 1 - tMin,
noHandles = false, noHandles = false,
clearSegments = []; clearSegments = [];
@ -298,7 +302,7 @@ PathItem.inject(new function() {
// Determine if the curve is a horizontal straight curve by checking the // Determine if the curve is a horizontal straight curve by checking the
// slope of it's tangent. // slope of it's tangent.
return curve.isStraight() && Math.abs(curve.getTangentAt(0.5, true).y) 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) { function getWinding(point, curves, horizontal, testContains) {
var epsilon = /*#=*/Numerical.GEOMETRIC_EPSILON, var epsilon = /*#=*/Numerical.GEOMETRIC_EPSILON,
tMin = /*#=*/Numerical.TOLERANCE, tMin = /*#=*/Numerical.CURVETIME_EPSILON,
tMax = 1 - tMin, tMax = 1 - tMin,
px = point.x, px = point.x,
py = point.y, py = point.y,
@ -514,7 +518,7 @@ PathItem.inject(new function() {
// NOTE: Even though getTangentAt() supports 0 and 1 instead of // NOTE: Even though getTangentAt() supports 0 and 1 instead of
// tMin and tMax, we still need to use this instead, as other issues // tMin and tMax, we still need to use this instead, as other issues
// emerge from switching to 0 and 1 in edge cases. // emerge from switching to 0 and 1 in edge cases.
tMin = /*#=*/Numerical.TOLERANCE, tMin = /*#=*/Numerical.CURVETIME_EPSILON,
tMax = 1 - tMin; tMax = 1 - tMin;
for (var i = 0, seg, startSeg, l = segments.length; i < l; i++) { for (var i = 0, seg, startSeg, l = segments.length; i < l; i++) {
seg = startSeg = segments[i]; seg = startSeg = segments[i];
@ -801,7 +805,7 @@ Path.inject(/** @lends Path# */{
var a = 3 * (y1 - y2) - y0 + y3, var a = 3 * (y1 - y2) - y0 + y3,
b = 2 * (y0 + y2) - 4 * y1, b = 2 * (y0 + y2) - 4 * y1,
c = y1 - y0, c = y1 - y0,
tMin = /*#=*/Numerical.TOLERANCE, tMin = /*#=*/Numerical.CURVETIME_EPSILON,
tMax = 1 - tMin, tMax = 1 - tMin,
roots = [], roots = [],
// Keep then range to 0 .. 1 (excluding) in the search for y // Keep then range to 0 .. 1 (excluding) in the search for y

View file

@ -81,6 +81,7 @@ var Numerical = new function() {
* range (see MACHINE_EPSILON). * range (see MACHINE_EPSILON).
*/ */
EPSILON: EPSILON, EPSILON: EPSILON,
CURVETIME_EPSILON: 1e-6,
GEOMETRIC_EPSILON: 1e-9, GEOMETRIC_EPSILON: 1e-9,
/** /**
* MACHINE_EPSILON for a double precision (Javascript Number) is * MACHINE_EPSILON for a double precision (Javascript Number) is

View file

@ -161,7 +161,7 @@ test('Curve#getParameterAt()', function() {
var t2 = curve.getParameterAt(o2); var t2 = curve.getParameterAt(o2);
equals(t1, t2, 'Curve parameter at offset ' + o1 equals(t1, t2, 'Curve parameter at offset ' + o1
+ ' should be the same value as at offset' + o2, + ' should be the same value as at offset' + o2,
Numerical.TOLERANCE); Numerical.CURVETIME_EPSILON);
} }
equals(curve.getParameterAt(curve.length + 1), null, equals(curve.getParameterAt(curve.length + 1), null,