mirror of
https://github.com/scratchfoundation/paper.js.git
synced 2025-01-07 13:22:07 -05:00
Fix for #773
Indroduced more reliable method for finding self intersection on curves.
This commit is contained in:
parent
ea3cc63e2e
commit
ec70fa1806
2 changed files with 116 additions and 85 deletions
|
@ -417,13 +417,16 @@ var Curve = Base.extend(/** @lends Curve# */{
|
||||||
* Returns all intersections between two {@link Curve} objects as an array
|
* Returns all intersections between two {@link Curve} objects as an array
|
||||||
* of {@link CurveLocation} objects.
|
* 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
|
* @param {Curve} curve the other curve to find the intersections with
|
||||||
* @return {CurveLocation[]} the locations of all intersection between the
|
* @return {CurveLocation[]} the locations of all intersection between the
|
||||||
* curves
|
* curves
|
||||||
*/
|
*/
|
||||||
getIntersections: function(curve) {
|
getIntersections: function(curve) {
|
||||||
return Curve.getIntersections(this.getValues(), curve.getValues(),
|
return Curve.getIntersections(this.getValues(), curve ? curve.getValues() : null,
|
||||||
this, curve, [], {});
|
this, curve ? curve : this, [], {});
|
||||||
},
|
},
|
||||||
|
|
||||||
// TODO: adjustThroughPoint
|
// 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
|
// #getIntersections() calls as it is required to create the resulting
|
||||||
// CurveLocation objects.
|
// CurveLocation objects.
|
||||||
getIntersections: function(v1, v2, c1, c2, locations, param) {
|
getIntersections: function(v1, v2, c1, c2, locations, param) {
|
||||||
// Avoid checking curves if completely out of control bounds.
|
if (!v2) { // if v2 is null or undefined, search for self intersection
|
||||||
// As a little optimization, we can scale the handles with 0.75
|
// get side of both handles
|
||||||
// before calculating the control bounds and still be sure that the
|
var h1Side = Line.getSide(v1[0], v1[1], v1[6], v1[7], v1[2], v1[3], false);
|
||||||
// curve is fully contained.
|
var h2Side = Line.getSide(v1[0], v1[1], v1[6], v1[7], v1[4], v1[5], false);
|
||||||
var c1p1x = v1[0], c1p1y = v1[1],
|
if (h1Side == h2Side) {
|
||||||
c1p2x = v1[6], c1p2y = v1[7],
|
var edgeSum = (v1[0] - v1[4]) * (v1[3] - v1[7]) + (v1[2] - v1[6]) * (v1[5] - v1[1]);
|
||||||
c2p1x = v2[0], c2p1y = v2[1],
|
// if both handles are on the same side, the curve can only have a self intersection if
|
||||||
c2p2x = v2[6], c2p2y = v2[7],
|
// the edge sum and the handles's side have different signs. If the handles are on the
|
||||||
c1h1x = (3 * v1[2] + c1p1x) / 4,
|
// left side, the edge sum must be negative for a self intersection (and vice versa)
|
||||||
c1h1y = (3 * v1[3] + c1p1y) / 4,
|
if (Math.sign(edgeSum) == h1Side) return locations;
|
||||||
c1h2x = (3 * v1[4] + c1p2x) / 4,
|
}
|
||||||
c1h2y = (3 * v1[5] + c1p2y) / 4,
|
// As a second condition we check if the curve has an inflection point. If an inflection point
|
||||||
c2h1x = (3 * v2[2] + c2p1x) / 4,
|
// exists, the curve cannot have a self intersection.
|
||||||
c2h1y = (3 * v2[3] + c2p1y) / 4,
|
var ax = v1[6] - 3 * v1[4] + 3 * v1[2] - v1[0];
|
||||||
c2h2x = (3 * v2[4] + c2p2x) / 4,
|
var bx = v1[4] - 2 * v1[2] + v1[0];
|
||||||
c2h2y = (3 * v2[5] + c2p2y) / 4,
|
var cx = v1[2] - v1[0];
|
||||||
min = Math.min,
|
var ay = v1[7] - 3 * v1[5] + 3 * v1[3] - v1[1];
|
||||||
max = Math.max;
|
var by = v1[5] - 2 * v1[3] + v1[1];
|
||||||
if (!(
|
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) >=
|
max(c1p1x, c1h1x, c1h2x, c1p2x) >=
|
||||||
min(c2p1x, c2h1x, c2h2x, c2p2x) &&
|
min(c2p1x, c2h1x, c2h2x, c2p2x) &&
|
||||||
min(c1p1x, c1h1x, c1h2x, c1p2x) <=
|
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(c2p1y, c2h1y, c2h2y, c2p2y) &&
|
||||||
min(c1p1y, c1h1y, c1h2y, c1p2y) <=
|
min(c1p1y, c1h1y, c1h2y, c1p2y) <=
|
||||||
max(c2p1y, c2h1y, c2h2y, c2p2y)
|
max(c2p1y, c2h1y, c2h2y, c2p2y)
|
||||||
)
|
)
|
||||||
// Also detect and handle overlaps:
|
// Also detect and handle overlaps:
|
||||||
|| !param.startConnected && !param.endConnected
|
|| !param.startConnected && !param.endConnected
|
||||||
&& addOverlap(v1, v2, c1, c2, locations, param))
|
&& addOverlap(v1, v2, c1, c2, locations, param))
|
||||||
return locations;
|
return locations;
|
||||||
var straight1 = Curve.isStraight(v1),
|
var straight1 = Curve.isStraight(v1),
|
||||||
straight2 = Curve.isStraight(v2),
|
straight2 = Curve.isStraight(v2),
|
||||||
c1p1 = new Point(c1p1x, c1p1y),
|
c1p1 = new Point(c1p1x, c1p1y),
|
||||||
c1p2 = new Point(c1p2x, c1p2y),
|
c1p2 = new Point(c1p2x, c1p2y),
|
||||||
c2p1 = new Point(c2p1x, c2p1y),
|
c2p1 = new Point(c2p1x, c2p1y),
|
||||||
c2p2 = new Point(c2p2x, c2p2y),
|
c2p2 = new Point(c2p2x, c2p2y),
|
||||||
// NOTE: Use smaller Numerical.EPSILON to compare beginnings and
|
// NOTE: Use smaller Numerical.EPSILON to compare beginnings and
|
||||||
// end points to avoid matching them on almost collinear lines.
|
// end points to avoid matching them on almost collinear lines.
|
||||||
epsilon = /*#=*/Numerical.EPSILON;
|
epsilon = /*#=*/Numerical.EPSILON;
|
||||||
// Handle the special case where the first curve's stat-point
|
// Handle the special case where the first curve's stat-point
|
||||||
// overlaps with the second curve's start- or end-points.
|
// overlaps with the second curve's start- or end-points.
|
||||||
if (c1p1.isClose(c2p1, epsilon))
|
if (c1p1.isClose(c2p1, epsilon))
|
||||||
addLocation(locations, param, v1, c1, 0, c1p1, v2, c2, 0, c2p1);
|
addLocation(locations, param, v1, c1, 0, c1p1, v2, c2, 0, c2p1);
|
||||||
if (!param.startConnected && c1p1.isClose(c2p2, epsilon))
|
if (!param.startConnected && c1p1.isClose(c2p2, epsilon))
|
||||||
addLocation(locations, param, v1, c1, 0, c1p1, v2, c2, 1, c2p2);
|
addLocation(locations, param, v1, c1, 0, c1p1, v2, c2, 1, c2p2);
|
||||||
// Determine the correct intersection method based on whether one or
|
// Determine the correct intersection method based on whether one or
|
||||||
// curves are straight lines:
|
// curves are straight lines:
|
||||||
(straight1 && straight2
|
(straight1 && straight2
|
||||||
? addLineIntersection
|
? addLineIntersection
|
||||||
: straight1 || straight2
|
: straight1 || straight2
|
||||||
? addCurveLineIntersections
|
? addCurveLineIntersections
|
||||||
: addCurveIntersections)(
|
: addCurveIntersections)(
|
||||||
v1, v2, c1, c2, locations, param,
|
v1, v2, c1, c2, locations, param,
|
||||||
// Define the defaults for these parameters of
|
// Define the defaults for these parameters of
|
||||||
// addCurveIntersections():
|
// addCurveIntersections():
|
||||||
// tMin, tMax, uMin, uMax, oldTDiff, reverse, recursion
|
// tMin, tMax, uMin, uMax, oldTDiff, reverse, recursion
|
||||||
0, 1, 0, 1, 0, false, 0);
|
0, 1, 0, 1, 0, false, 0);
|
||||||
// Handle the special case where the first curve's end-point
|
// Handle the special case where the first curve's end-point
|
||||||
// overlaps with the second curve's start- or end-points.
|
// overlaps with the second curve's start- or end-points.
|
||||||
if (!param.endConnected && c1p2.isClose(c2p1, epsilon))
|
if (!param.endConnected && c1p2.isClose(c2p1, epsilon))
|
||||||
addLocation(locations, param, v1, c1, 1, c1p2, v2, c2, 0, c2p1);
|
addLocation(locations, param, v1, c1, 1, c1p2, v2, c2, 0, c2p1);
|
||||||
if (c1p2.isClose(c2p2, epsilon))
|
if (c1p2.isClose(c2p2, epsilon))
|
||||||
addLocation(locations, param, v1, c1, 1, c1p2, v2, c2, 1, c2p2);
|
addLocation(locations, param, v1, c1, 1, c1p2, v2, c2, 1, c2p2);
|
||||||
|
}
|
||||||
return locations;
|
return locations;
|
||||||
}
|
}
|
||||||
}};
|
}};
|
||||||
|
|
|
@ -88,37 +88,15 @@ var PathItem = Item.extend(/** @lends PathItem# */{
|
||||||
values1 = self ? values2[i] : curve1.getValues(matrix1);
|
values1 = self ? values2[i] : curve1.getValues(matrix1);
|
||||||
if (self) {
|
if (self) {
|
||||||
// First check for self-intersections within the same curve
|
// First check for self-intersections within the same curve
|
||||||
var seg1 = curve1.getSegment1(),
|
var p1 = curve1.getSegment1()._point,
|
||||||
seg2 = curve1.getSegment2(),
|
p2 = curve1.getSegment2()._point;
|
||||||
p1 = seg1._point,
|
Curve.getIntersections(values1, null, curve1, curve1,
|
||||||
p2 = seg2._point,
|
locations, {
|
||||||
h1 = seg1._handleOut,
|
include: include,
|
||||||
h2 = seg2._handleIn,
|
// Only possible if there is only one closed curve:
|
||||||
l1 = new Line(p1.subtract(h1), p1.add(h1)),
|
startConnected: length1 === 1 && p1.equals(p2)
|
||||||
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];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Check for intersections with other curves. For self intersection,
|
// Check for intersections with other curves. For self intersection,
|
||||||
// we can start at i + 1 instead of 0
|
// we can start at i + 1 instead of 0
|
||||||
|
|
Loading…
Reference in a new issue