From d7e075d3168036a391e584baca7573e46f857e1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Thu, 25 Apr 2013 11:03:49 -0700 Subject: [PATCH] Handle contour edge cases in Path#contains(). Closes #208. --- src/path/Curve.js | 81 ++++++++++++++++++++++++------------- src/path/Path.js | 4 +- test/tests/Item_Contains.js | 17 +++++++- 3 files changed, 71 insertions(+), 31 deletions(-) diff --git a/src/path/Curve.js b/src/path/Curve.js index c3cd9321..a8afc23e 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -295,39 +295,62 @@ var Curve = this.Curve = Base.extend(/** @lends Curve# */{ var vals = this.getValues(), count = Curve.solveCubic(vals, 1, point.y, roots), crossings = 0, - tolerance = /*#=*/ Numerical.TOLERANCE; + tolerance = /*#=*/ Numerical.TOLERANCE, + abs = Math.abs; + + // Checks the y-slope between the current curve and the previous for a + // change of orientation, when a solution is found at t == 0 + function changesOrientation(curve, tangent) { + return Curve.evaluate(curve.getPrevious().getValues(), 1, true, 1).y + * tangent.y > 0; + } + + // TODO: See if this speeds up code, or slows it down: + // var bounds = this.getBounds(); + // if (point.y < bounds.getTop() || point.y > bounds.getBottom() + // || point.x > bounds.getRight()) + // return 0; + + if (count === -1) { + // Infinite solutions, so we have a horizontal curve. + // Find parameter through getParameterOf() + roots[0] = Curve.getParameterOf(vals, point.x, point.y); + count = roots[0] !== null ? 1 : 0; + } for (var i = 0; i < count; i++) { var t = roots[i]; - if (t >= -tolerance && t < 1 - tolerance) { + if (t > -tolerance && t < 1 - tolerance) { var pt = Curve.evaluate(vals, t, true, 0); -/*#*/ if (options.debug) { - console.log(t, point + '', pt + ''); - new Path.Circle({ - center: Curve.evaluate(vals, t, true, 0), - radius: 2, - strokeColor: 'red', - strokeWidth: 0.25 - }); -/*#*/ } - if (pt.x >= point.x - tolerance) { + if (point.x < pt.x + tolerance) { // Passing 1 for Curve.evaluate()'s type calculates tangents. - var tangent = Curve.evaluate(vals, t, true, 1); - if ( - // Skip touching stationary points (tips), but if the - // actual point is on one, do not skip this solution! - Math.abs(pt.x - point.x) > tolerance - && ( - // Check derivate for stationary points - Math.abs(tangent.y) < tolerance - // If root is close to 0 and not changing vertical - // orientation from the previous curve, do not count - // this root, as it's touching a corner. - || t < tolerance - // Check the y-slope for a change of orientation - && tangent.y * Curve.evaluate( - this.getPrevious().getValues(), 1, true, 1).y - < tolerance)) - continue; + var tan = Curve.evaluate(vals, t, true, 1), + diff = abs(pt.x - point.x); + // Wee need to handle all kind of edge cases when points are + // on contours, ore rays are touching countours, do termine + // wether the corssings counts or not. + // Is the actual point is on the countour? + if (diff < tolerance) { + // Do not count the crossing if it is on the left + // hand side of the shape (tangent pointing upwards) + // since the ray will go out the other end and the + // point is on the contour, so inside. + var angle = tan.getAngle(); + if (angle > -180 && angle < 0 + // Handle special case where point is on a corner, + // in which case we only skip this crossing if both + // tangents have the same orientation (see below) + && (t > tolerance || changesOrientation(this, tan))) + continue; + } else { + // Skip touching stationary points + if (abs(tan.y) < tolerance + // Check derivate for stationary points. If root is + // close to 0 and not changing vertical orientation + // from the previous curve, do not count this root, + // as it's touching a corner. + || t < tolerance && !changesOrientation(this, tan)) + continue; + } crossings++; } } diff --git a/src/path/Path.js b/src/path/Path.js index 08eaaf9b..c7ba2f4c 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -1602,10 +1602,12 @@ var Path = this.Path = PathItem.extend(/** @lends Path# */{ roots = new Array(3); for (var i = 0, l = curves.length; i < l; i++) crossings += curves[i].getCrossings(point, roots); + // TODO: Do not close open path for contains(), but create a straight + // closing lines instead, just like how filling open paths works. if (!this._closed && hasFill) crossings += Curve.create(this, segments[segments.length - 1], segments[0]).getCrossings(point, roots); - return (crossings & 1) == 1; + return (crossings & 1) === 1; }, _hitTest: function(point, options) { diff --git a/test/tests/Item_Contains.js b/test/tests/Item_Contains.js index c6cfe20e..bc849156 100644 --- a/test/tests/Item_Contains.js +++ b/test/tests/Item_Contains.js @@ -106,6 +106,21 @@ test('Path#contains() (Rectangle Contours)', function() { var path = new Path.Rectangle(new Point(100, 100), [200, 200]), curves = path.getCurves(); - for (var i = 0; i < curves.length; i++) + for (var i = 0; i < curves.length; i++) { + testPoint(path, curves[i].getPoint(0), true); testPoint(path, curves[i].getPoint(0.5), true); + } +}); + + +test('Path#contains() (Rotated Rectangle Contours)', function() { + var path = new Path.Rectangle(new Point(100, 100), [200, 200]), + curves = path.getCurves(); + + path.rotate(45); + + for (var i = 0; i < curves.length; i++) { + testPoint(path, curves[i].getPoint(0), true); + testPoint(path, curves[i].getPoint(0.5), true); + } });