diff --git a/src/path/Curve.js b/src/path/Curve.js index d7029b90..6b107cbb 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -417,13 +417,16 @@ var Curve = Base.extend(/** @lends Curve# */{ * Returns all intersections between two {@link Curve} objects as an array * of {@link CurveLocation} objects. * + * If the parameter curve is null, the self intersection of the curve is + * returned, if it exists. + * * @param {Curve} curve the other curve to find the intersections with * @return {CurveLocation[]} the locations of all intersection between the * curves */ getIntersections: function(curve) { - return Curve.getIntersections(this.getValues(), curve.getValues(), - this, curve, [], {}); + return Curve.getIntersections(this.getValues(), curve ? curve.getValues() : null, + this, curve ? curve : this, [], {}); }, // TODO: adjustThroughPoint @@ -1757,25 +1760,74 @@ new function() { // Scope for intersection using bezier fat-line clipping // #getIntersections() calls as it is required to create the resulting // CurveLocation objects. getIntersections: function(v1, v2, c1, c2, locations, param) { - // Avoid checking curves if completely out of control bounds. - // As a little optimization, we can scale the handles with 0.75 - // before calculating the control bounds and still be sure that the - // curve is fully contained. - var c1p1x = v1[0], c1p1y = v1[1], - c1p2x = v1[6], c1p2y = v1[7], - c2p1x = v2[0], c2p1y = v2[1], - c2p2x = v2[6], c2p2y = v2[7], - c1h1x = (3 * v1[2] + c1p1x) / 4, - c1h1y = (3 * v1[3] + c1p1y) / 4, - c1h2x = (3 * v1[4] + c1p2x) / 4, - c1h2y = (3 * v1[5] + c1p2y) / 4, - c2h1x = (3 * v2[2] + c2p1x) / 4, - c2h1y = (3 * v2[3] + c2p1y) / 4, - c2h2x = (3 * v2[4] + c2p2x) / 4, - c2h2y = (3 * v2[5] + c2p2y) / 4, - min = Math.min, - max = Math.max; - if (!( + if (!v2) { // if v2 is null or undefined, search for self intersection + // get side of both handles + var h1Side = Line.getSide(v1[0], v1[1], v1[6], v1[7], v1[2], v1[3], false); + var h2Side = Line.getSide(v1[0], v1[1], v1[6], v1[7], v1[4], v1[5], false); + if (h1Side == h2Side) { + var edgeSum = (v1[0] - v1[4]) * (v1[3] - v1[7]) + (v1[2] - v1[6]) * (v1[5] - v1[1]); + // if both handles are on the same side, the curve can only have a self intersection if + // the edge sum and the handles's side have different signs. If the handles are on the + // left side, the edge sum must be negative for a self intersection (and vice versa) + if (Math.sign(edgeSum) == h1Side) return locations; + } + // As a second condition we check if the curve has an inflection point. If an inflection point + // exists, the curve cannot have a self intersection. + var ax = v1[6] - 3 * v1[4] + 3 * v1[2] - v1[0]; + var bx = v1[4] - 2 * v1[2] + v1[0]; + var cx = v1[2] - v1[0]; + var ay = v1[7] - 3 * v1[5] + 3 * v1[3] - v1[1]; + var by = v1[5] - 2 * v1[3] + v1[1]; + var cy = v1[3] - v1[1]; + var hasInflectionPoint = (Math.pow(ay * cx - ax * cy, 2) - 4 * (ay * bx - ax * by) * (by * cx - bx * cy) >= 0); + if (!hasInflectionPoint) { + // the curve may have a self intersection, find parameter to split curve. We search for the + // parameter where the velocity has an extremum by finding the roots of the cross product + // between the bezier curve's first and second derivative + var roots = [], + rootCount = Numerical.solveCubic(ax * ax + ay * ay, 3 * (ax * bx + ay * by), + (2 * (bx * bx + by * by) + ax * cx + ay * cy), (bx * cx + by * cy), roots, 0, 1); + // Select extremum with smallest curvature. This is always on the loop in case of a self intersection + var tSplit, maxCurvature; + for (var i = 0; i < rootCount; i++) { + var curvature = Math.abs(c1.getCurvatureAt(roots[i], true)); + if (!maxCurvature || curvature > maxCurvature) { + maxCurvature = curvature; + tSplit = roots[i]; + } + } + // Divide the curve in two and then apply the normal curve intersection code. + var parts = Curve.subdivide(v1, tSplit); + if (!param) param = {}; + // After splitting, the end is always connected: + param.endConnected = true; + // Since the curve was split above, we need to + // adjust the parameters for both locations. + param.renormalize = function(t1, t2) { + return [t1 * tSplit, t2 * (1 - tSplit) + tSplit]; + }; + Curve.getIntersections(parts[0], parts[1], c1, c1, locations, param); + } + } else { + // Avoid checking curves if completely out of control bounds. + // As a little optimization, we can scale the handles with 0.75 + // before calculating the control bounds and still be sure that the + // curve is fully contained. + var c1p1x = v1[0], c1p1y = v1[1], + c1p2x = v1[6], c1p2y = v1[7], + c2p1x = v2[0], c2p1y = v2[1], + c2p2x = v2[6], c2p2y = v2[7], + c1h1x = (3 * v1[2] + c1p1x) / 4, + c1h1y = (3 * v1[3] + c1p1y) / 4, + c1h2x = (3 * v1[4] + c1p2x) / 4, + c1h2y = (3 * v1[5] + c1p2y) / 4, + c2h1x = (3 * v2[2] + c2p1x) / 4, + c2h1y = (3 * v2[3] + c2p1y) / 4, + c2h2x = (3 * v2[4] + c2p2x) / 4, + c2h2y = (3 * v2[5] + c2p2y) / 4, + min = Math.min, + max = Math.max; + if (!( max(c1p1x, c1h1x, c1h2x, c1p2x) >= min(c2p1x, c2h1x, c2h2x, c2p2x) && min(c1p1x, c1h1x, c1h2x, c1p2x) <= @@ -1784,44 +1836,45 @@ new function() { // Scope for intersection using bezier fat-line clipping min(c2p1y, c2h1y, c2h2y, c2p2y) && min(c1p1y, c1h1y, c1h2y, c1p2y) <= max(c2p1y, c2h1y, c2h2y, c2p2y) - ) - // Also detect and handle overlaps: - || !param.startConnected && !param.endConnected + ) + // Also detect and handle overlaps: + || !param.startConnected && !param.endConnected && addOverlap(v1, v2, c1, c2, locations, param)) - return locations; - var straight1 = Curve.isStraight(v1), - straight2 = Curve.isStraight(v2), - c1p1 = new Point(c1p1x, c1p1y), - c1p2 = new Point(c1p2x, c1p2y), - c2p1 = new Point(c2p1x, c2p1y), - c2p2 = new Point(c2p2x, c2p2y), + return locations; + var straight1 = Curve.isStraight(v1), + straight2 = Curve.isStraight(v2), + c1p1 = new Point(c1p1x, c1p1y), + c1p2 = new Point(c1p2x, c1p2y), + c2p1 = new Point(c2p1x, c2p1y), + c2p2 = new Point(c2p2x, c2p2y), // NOTE: Use smaller Numerical.EPSILON to compare beginnings and // end points to avoid matching them on almost collinear lines. - epsilon = /*#=*/Numerical.EPSILON; - // Handle the special case where the first curve's stat-point - // overlaps with the second curve's start- or end-points. - if (c1p1.isClose(c2p1, epsilon)) - addLocation(locations, param, v1, c1, 0, c1p1, v2, c2, 0, c2p1); - if (!param.startConnected && c1p1.isClose(c2p2, epsilon)) - addLocation(locations, param, v1, c1, 0, c1p1, v2, c2, 1, c2p2); - // Determine the correct intersection method based on whether one or - // curves are straight lines: - (straight1 && straight2 - ? addLineIntersection - : straight1 || straight2 + epsilon = /*#=*/Numerical.EPSILON; + // Handle the special case where the first curve's stat-point + // overlaps with the second curve's start- or end-points. + if (c1p1.isClose(c2p1, epsilon)) + addLocation(locations, param, v1, c1, 0, c1p1, v2, c2, 0, c2p1); + if (!param.startConnected && c1p1.isClose(c2p2, epsilon)) + addLocation(locations, param, v1, c1, 0, c1p1, v2, c2, 1, c2p2); + // Determine the correct intersection method based on whether one or + // curves are straight lines: + (straight1 && straight2 + ? addLineIntersection + : straight1 || straight2 ? addCurveLineIntersections : addCurveIntersections)( - v1, v2, c1, c2, locations, param, - // Define the defaults for these parameters of - // addCurveIntersections(): - // tMin, tMax, uMin, uMax, oldTDiff, reverse, recursion - 0, 1, 0, 1, 0, false, 0); - // Handle the special case where the first curve's end-point - // overlaps with the second curve's start- or end-points. - if (!param.endConnected && c1p2.isClose(c2p1, epsilon)) - addLocation(locations, param, v1, c1, 1, c1p2, v2, c2, 0, c2p1); - if (c1p2.isClose(c2p2, epsilon)) - addLocation(locations, param, v1, c1, 1, c1p2, v2, c2, 1, c2p2); + v1, v2, c1, c2, locations, param, + // Define the defaults for these parameters of + // addCurveIntersections(): + // tMin, tMax, uMin, uMax, oldTDiff, reverse, recursion + 0, 1, 0, 1, 0, false, 0); + // Handle the special case where the first curve's end-point + // overlaps with the second curve's start- or end-points. + if (!param.endConnected && c1p2.isClose(c2p1, epsilon)) + addLocation(locations, param, v1, c1, 1, c1p2, v2, c2, 0, c2p1); + if (c1p2.isClose(c2p2, epsilon)) + addLocation(locations, param, v1, c1, 1, c1p2, v2, c2, 1, c2p2); + } return locations; } }}; diff --git a/src/path/PathItem.js b/src/path/PathItem.js index 45a38318..57e1a35d 100644 --- a/src/path/PathItem.js +++ b/src/path/PathItem.js @@ -88,37 +88,15 @@ var PathItem = Item.extend(/** @lends PathItem# */{ values1 = self ? values2[i] : curve1.getValues(matrix1); if (self) { // First check for self-intersections within the same curve - var seg1 = curve1.getSegment1(), - seg2 = curve1.getSegment2(), - p1 = seg1._point, - p2 = seg2._point, - h1 = seg1._handleOut, - h2 = seg2._handleIn, - l1 = new Line(p1.subtract(h1), p1.add(h1)), - l2 = new Line(p2.subtract(h2), p1.add(h2)); - // Check if extended handles of endpoints of this curve - // intersects each other. We cannot have a self intersection - // within this curve if they don't intersect due to convex-hull - // property. - if (l1.intersect(l2, false)) { - // Self intersecting is found by dividing the curve in two - // and and then applying the normal curve intersection code. - var parts = Curve.subdivide(values1, 0.5); - Curve.getIntersections(parts[0], parts[1], curve1, curve1, - locations, { - include: include, - // Only possible if there is only one closed curve: - startConnected: length1 === 1 && p1.equals(p2), - // After splitting, the end is always connected: - endConnected: true, - renormalize: function(t1, t2) { - // Since the curve was split above, we need to - // adjust the parameters for both locations. - return [t1 / 2, (1 + t2) / 2]; - } - } - ); - } + var p1 = curve1.getSegment1()._point, + p2 = curve1.getSegment2()._point; + Curve.getIntersections(values1, null, curve1, curve1, + locations, { + include: include, + // Only possible if there is only one closed curve: + startConnected: length1 === 1 && p1.equals(p2) + } + ); } // Check for intersections with other curves. For self intersection, // we can start at i + 1 instead of 0