diff --git a/examples/Scripts/PathTangentsToVector.html b/examples/Scripts/PathTangentsToVector.html new file mode 100644 index 00000000..f9914774 --- /dev/null +++ b/examples/Scripts/PathTangentsToVector.html @@ -0,0 +1,73 @@ + + + + + Path Tangents To Vector + + + + + + + + diff --git a/src/path/Curve.js b/src/path/Curve.js index 188b34c9..b2f58b0b 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1147,6 +1147,23 @@ statics: /** @lends Curve */{ */ getParameterAt: '#getTimeAt', + /** + * Calculates the curve-time parameters where the curve is tangential to + * provided tangent. Note that tangents at the start or end are included. + * + * @param {Point} tangent the tangent to which the curve must be tangential + * @return {Number[]} at most two curve-time parameters, where the curve is + * tangential to the given tangent + */ + getTimesWithTangent: function (/* tangent */) { + var vector = Point.read(arguments); + if (vector.isZero()) { + return []; + } + + return Curve.getTimesWithTangent(this.getValues(), vector); + }, + /** * Calculates the curve offset at the specified curve-time parameter on * the curve. @@ -2230,6 +2247,56 @@ new function() { // Scope for bezier intersection using fat-line clipping return pairs; } + /** + * Internal method to calculates the curve-time parameters where the curve + * is tangential to provided tangent. + * Tangents at the start or end are included. + * + * @param {Number[]} v curve values + * @param {Point} point the tangent to which the curve must be tangential + * @return {Number[]} at most two curve-time parameters, where the curve is + * tangential to the given tangent + */ + function getTimesWithTangent(v, point) { + // Algorithm adapted from: https://stackoverflow.com/a/34837312/7615922 + var x0 = v[0], y0 = v[1], + x1 = v[2], y1 = v[3], + x2 = v[4], y2 = v[5], + x3 = v[6], y3 = v[7], + normalized = point.normalize(), + tx = normalized.x, + ty = normalized.y, + ax = 3 * x3 - 9 * x2 + 9 * x1 - 3 * x0, + ay = 3 * y3 - 9 * y2 + 9 * y1 - 3 * y0, + bx = 6 * x2 - 12 * x1 + 6 * x0, + by = 6 * y2 - 12 * y1 + 6 * y0, + cx = 3 * x1 - 3 * x0, + cy = 3 * y1 - 3 * y0, + den = 2 * ax * ty - 2 * ay * tx, + times = []; + if (Math.abs(den) < Numerical.CURVETIME_EPSILON) { + var num = ax * cy - ay * cx; + var den = ax * by - ay * bx; + if (den != 0) { + var t = -num / den; + if (t >= 0 && t <= 1) times.push(t); + } + } else { + var delta = (bx * bx - 4 * ax * cx) * ty * ty + + (-2 * bx * by + 4 * ay * cx + 4 * ax * cy) * tx * ty + + (by * by - 4 * ay * cy) * tx * tx; + var k = bx * ty - by * tx; + if (delta >= 0 && den != 0) { + var d = Math.sqrt(delta); + var t0 = -(k + d) / den; + var t1 = (-k + d) / den; + if (t0 >= 0 && t0 <= 1) times.push(t0); + if (t1 >= 0 && t1 <= 1) times.push(t1); + } + } + return times; + } + return /** @lends Curve# */{ /** * Returns all intersections between two {@link Curve} objects as an @@ -2252,7 +2319,8 @@ new function() { // Scope for bezier intersection using fat-line clipping getOverlaps: getOverlaps, // Exposed for use in boolean offsetting getIntersections: getIntersections, - getCurveLineIntersections: getCurveLineIntersections + getCurveLineIntersections: getCurveLineIntersections, + getTimesWithTangent: getTimesWithTangent } }; }); diff --git a/src/path/Path.js b/src/path/Path.js index a4d02ca2..200811e2 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -1895,7 +1895,7 @@ var Path = PathItem.extend(/** @lends Path# */{ return offset; } return null; - } + }, /** * Calculates the point on the path at the given offset. @@ -2123,6 +2123,42 @@ var Path = PathItem.extend(/** @lends Path# */{ * the beginning of the path and {@link Path#length} at the end * @return {Number} the normal vector at the given offset */ + + /** + * Calculates path offsets where the path is tangential to provided tangent. + * Note that tangent at start or end are included. + * Tangent at segment point is returned even if only one of its handles is + * collinear with the provided tangent. + * + * @param {Point} tangent the tangent to which the path must be tangential + * @return {Number[]} path offsets where the path is tangential to the + * provided tangent + */ + getOffsetsWithTangent: function(/* tangent */) { + var tangent = Point.read(arguments); + if (tangent.isZero()) { + return []; + } + + var offsets = []; + var offsetBeforeCurve = 0; + var curves = this.getCurves(); + for (var i = 0; i < curves.length; i++) { + var curve = curves[i]; + // Calculate curves times at vector tangent... + var curveTimes = curve.getTimesWithTangent(tangent); + for (var j = 0; j < curveTimes.length; j++) { + // ...and convert them to path offsets... + var offset = offsetBeforeCurve + curve.getOffsetAtTime(curveTimes[j]); + // ...avoiding duplicates. + if (offsets.indexOf(offset) < 0) { + offsets.push(offset); + } + } + offsetBeforeCurve += curve.length; + } + return offsets; + } }), new function() { // Scope for drawing diff --git a/test/tests/Curve.js b/test/tests/Curve.js index 341fd219..6a4b6c4a 100644 --- a/test/tests/Curve.js +++ b/test/tests/Curve.js @@ -347,3 +347,25 @@ test('Curve#divideAt(offset)', function() { return new Curve(point1, point2).divideAtTime(0.5).point1; }, middle); }); + +test('Curve#getTimesWithTangent()', function() { + var curve = new Curve([0, 0], [100, 0], [0, -100], [200, 200]); + equals(curve.getTimesWithTangent(), [], 'should return empty array when called without argument'); + equals(curve.getTimesWithTangent([1, 0]), [0], 'should return tangent at start'); + equals(curve.getTimesWithTangent([-1, 0]), [0], 'should return the same when called with opposite direction vector'); + equals(curve.getTimesWithTangent([0, 1]), [1], 'should return tangent at end'); + equals(curve.getTimesWithTangent([1, 1]), [0.5], 'should return tangent at middle'); + equals(curve.getTimesWithTangent([1, -1]), [], 'should return empty array when there is no tangent'); + + equals( + new Curve([0, 0], [100, 0], [500, -500], [-500, -500]).getTimesWithTangent([1, 0]).length, + 2, + 'should return 2 values for specific self-intersecting path case' + ); + + equals( + new Curve([0, 0], [100, 0], [0, -100], [0, -100]).getTimesWithTangent([1, 0]).length, + 2, + 'should return 2 values for specific parabollic path case' + ); +}); diff --git a/test/tests/Path.js b/test/tests/Path.js index f198d17d..d5b8e812 100644 --- a/test/tests/Path.js +++ b/test/tests/Path.js @@ -611,6 +611,14 @@ test('Path#arcTo(from, through, to); where from, through and to all share the sa equals(error != null, true, 'We expect this arcTo() command to throw an error'); }); +test('Path#getOffsetsWithTangent()', function() { + var path = new Path.Circle(new Point(0, 0), 50); + var length = path.length; + equals(path.getOffsetsWithTangent(), [], 'should return empty array when called without argument'); + equals(path.getOffsetsWithTangent([1, 0]), [0.25 * length, 0.75 * length], 'should not return duplicates when tangent is at segment point'); + equals(path.getOffsetsWithTangent([1, 1]).length, 2, 'should return 2 values when called on a circle with a diagonal vector'); +}); + test('Path#add() with a lot of segments (#1493)', function() { var segments = []; for (var i = 0; i < 100000; i++) {