From 0134596f66635f8fa5e78cd8c76f0960a5aeda55 Mon Sep 17 00:00:00 2001 From: hkrish Date: Wed, 25 Dec 2013 20:38:48 +0100 Subject: [PATCH] _getMonotoneCurves method which returns and caches curves that are monotonic in Y direction --- src/path/CompoundPath.js | 21 ++++- src/path/Path.js | 172 ++++++++++++++++++++++++++++++--------- 2 files changed, 150 insertions(+), 43 deletions(-) diff --git a/src/path/CompoundPath.js b/src/path/CompoundPath.js index e36dc71e..affc0709 100644 --- a/src/path/CompoundPath.js +++ b/src/path/CompoundPath.js @@ -236,14 +236,27 @@ var CompoundPath = PathItem.extend(/** @lends CompoundPath# */{ return paths.join(' '); }, - _getWinding: function(point) { + /** + * Private method that returns all the curves in this CompoundPath, which + * are monotonically decreasing or increasing in the 'y' direction. + * Used by PathItem#_getWinding method. + */ + _getMonotoneCurves: function() { var children = this._children, - winding = 0; + monoCurves = []; for (var i = 0, l = children.length; i < l; i++) - winding += children[i]._getWinding(point); - return winding; + monoCurves.push.apply(monoCurves, children[i]._getMonotoneCurves()); + return monoCurves; }, + // _getWinding: function(point) { + // var children = this._children, + // winding = 0; + // for (var i = 0, l = children.length; i < l; i++) + // winding += children[i]._getWinding(point); + // return winding; + // }, + _getChildHitTestOptions: function(options) { // If we're not specifically asked to returns paths through // options.type == 'path' do not test children for fill, since a diff --git a/src/path/Path.js b/src/path/Path.js index 4eb8bd69..4a321aeb 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -145,6 +145,9 @@ var Path = PathItem.extend(/** @lends Path# */{ for (var i = 0, l = this._curves.length; i < l; i++) this._curves[i]._changed(/*#=*/ Change.GEOMETRY); } + // Clear cached curves used for winding direction and containment + // calculation. + this._monotoneCurves = 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 @@ -1708,49 +1711,140 @@ var Path = PathItem.extend(/** @lends Path# */{ return null; }, - _getWinding: function(point) { - var closed = this._closed; - // If the path is not closed, we should not bail out in case it has a - // fill color! - if (!closed && !this.hasFill() - || !this.getInternalRoughBounds()._containsPoint(point)) - return 0; - // Use the crossing number algorithm, by counting the crossings of the - // beam in right y-direction with the shape, and see if it's an odd - // number, meaning the starting point is inside the shape. - // http://en.wikipedia.org/wiki/Point_in_polygon - var curves = this.getCurves(), - segments = this._segments, - winding = 0, - // Reuse arrays for root-finding, give garbage collector a break - roots1 = [], - roots2 = [], - last = (closed - ? curves[curves.length - 1] - // Create a straight closing line for open paths, just like - // how filling open paths works. - : new Curve(segments[segments.length - 1]._point, - segments[0]._point)).getValues(), - previous = last; - for (var i = 0, l = curves.length; i < l; i++) { - var curve = curves[i].getValues(), - x = curve[0], - y = curve[1]; - // Filter out curves with 0-length (all 4 points in the same place): - if (!(x === curve[2] && y === curve[3] && x === curve[4] - && y === curve[5] && x === curve[6] && y === curve[7])) { - winding += Curve._getWinding(curve, previous, point.x, point.y, - roots1, roots2); - previous = curve; + /** + * Private method that returns and caches all the curves in this Path, which + * are monotonically decreasing or increasing in the 'y' direction. + * Used by PathItem#_getWinding method. + */ + _getMonotoneCurves: function() { + var monoCurves = this._monotoneCurves, + // TODO: replace instances with constants ( /*#=*/ ?) + INCREASING = 1, + DECREASING = -1; + if (!monoCurves) { + // Insert curve values into a cached array + // Always avoid horizontal curves + function insertValues(v, dir) { + var y0 = v[1], y1 = v[7]; + dir = dir || INCREASING; + if (y0 === y1) { + return; + } else if (y0 > y1) { + dir = DECREASING; + } + v.push(dir); + monoCurves.push(v); + } + // Handle bezier curves. We need to chop them into smaller curves + // with defined orientation, by solving the derivative curve for + // Y extrema. + function insertCurves(v, dir) { + var y0 = v[1], y1 = v[3], + y2 = v[5], y3 = v[7], + roots = [], tolerance = /*#=*/ Numerical.TOLERANCE, + i, li; + // Split the curve at y extrema, to get bezier curves with clear + // orientation: Calculate the derivative and find its roots. + var a = 3 * (y1 - y2) - y0 + y3, + b = 2 * (y0 + y2) - 4 * y1, + c = y1 - y0; + // 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) { + insertValues(v, dir); + } else { + roots.sort(); + var parts, t = roots[0]; + parts = Curve.subdivide(v, t); + if (count > 1) { + // Now renormalize t1 to the range of the next part. + t = (roots[1] - t) / (1 - t); + var subparts = Curve.subdivide(parts[1], t); + parts.splice(1, 1, subparts[0], subparts[1]); + } + for (i = 0, li = parts.length; i < li; i++) + insertValues(parts[i]); + } + } + // Insert curves that are monotonic in y direction into a cached array + monoCurves = this._monotoneCurves = []; + var curves = this.getCurves(), + crv, vals, i, li; + // If the path is not closed, we should join the end points + // with a straight line, just like how filling open paths works. + if (!this._closed) { + // if (!this.hasFill() + // || !this.getInternalRoughBounds()._containsPoint(point)) + // return 0; + var segments = this._segments; + curves.push(new Curve(segments[segments.length - 1]._point, + segments[0]._point).getValues()); + } + for (i = 0, li = curves.length; i < li; i++) { + crv = curves[i]; + vals = crv.getValues(); + if (crv.isLinear()) { + insertValues(vals); + } else { + var y0 = vals[1], y1 = vals[7]; + if (y0 > y1) { + insertCurves(vals, DECREASING); + } else if (y0 == y1 && y0 == vals[3] && y0 == vals[5]) { + continue; + } else { + insertCurves(vals, INCREASING); + } + } } } - if (!closed) { - winding += Curve._getWinding(last, previous, point.x, point.y, - roots1, roots2); - } - return winding; + return monoCurves; }, + // _getWinding: function(point) { + // var closed = this._closed; + // // If the path is not closed, we should not bail out in case it has a + // // fill color! + // if (!closed && !this.hasFill() + // || !this.getInternalRoughBounds()._containsPoint(point)) + // return 0; + // // Use the crossing number algorithm, by counting the crossings of the + // // beam in right y-direction with the shape, and see if it's an odd + // // number, meaning the starting point is inside the shape. + // // http://en.wikipedia.org/wiki/Point_in_polygon + // var curves = this.getCurves(), + // segments = this._segments, + // winding = 0, + // // Reuse arrays for root-finding, give garbage collector a break + // roots1 = [], + // roots2 = [], + // last = (closed + // ? curves[curves.length - 1] + // // Create a straight closing line for open paths, just like + // // how filling open paths works. + // : new Curve(segments[segments.length - 1]._point, + // segments[0]._point)).getValues(), + // previous = last; + // for (var i = 0, l = curves.length; i < l; i++) { + // var curve = curves[i].getValues(), + // x = curve[0], + // y = curve[1]; + // // Filter out curves with 0-length (all 4 points in the same place): + // if (!(x === curve[2] && y === curve[3] && x === curve[4] + // && y === curve[5] && x === curve[6] && y === curve[7])) { + // winding += Curve._getWinding(curve, previous, point.x, point.y, + // roots1, roots2); + // previous = curve; + // } + // } + // if (!closed) { + // winding += Curve._getWinding(last, previous, point.x, point.y, + // roots1, roots2); + // } + // return winding; + // }, + _hitTest: function(point, options) { var that = this, style = this.getStyle(),