mirror of
https://github.com/scratchfoundation/paper.js.git
synced 2025-01-01 02:38:43 -05:00
Implement Nearest Point-on-Curve Problem.
This commit is contained in:
parent
4630a1bd4a
commit
ccd4113ba3
1 changed files with 181 additions and 0 deletions
|
@ -647,4 +647,185 @@ var Curve = this.Curve = Base.extend(/** @lends Curve# */{
|
|||
a, b, 16, Numerical.TOLERANCE);
|
||||
}
|
||||
};
|
||||
}, new function() { // Scope for nearest point on curve problem
|
||||
|
||||
// Solving the Nearest Point-on-Curve Problem and A Bezier-Based Root-Finder
|
||||
// by Philip J. Schneider from "Graphics Gems", Academic Press, 1990
|
||||
// Optimised for Paper.js
|
||||
|
||||
var maxDepth = 32,
|
||||
epsilon = Math.pow(2, -maxDepth - 1);
|
||||
|
||||
var zCubic = [
|
||||
[1.0, 0.6, 0.3, 0.1],
|
||||
[0.4, 0.6, 0.6, 0.4],
|
||||
[0.1, 0.3, 0.6, 1.0]
|
||||
];
|
||||
|
||||
var xAxis = new Line(new Point(0, 0), new Point(1, 0));
|
||||
|
||||
/**
|
||||
* Given a point and a Bezier curve, generate a 5th-degree Bezier-format
|
||||
* equation whose solution finds the point on the curve nearest the
|
||||
* user-defined point.
|
||||
*/
|
||||
function toBezierForm(v, point) {
|
||||
var n = 3, // degree of B(t)
|
||||
degree = 5, // degree of B(t) . P
|
||||
c = [],
|
||||
d = [],
|
||||
cd = [],
|
||||
w = [];
|
||||
for(var i = 0; i <= n; i++) {
|
||||
// Determine the c's -- these are vectors created by subtracting
|
||||
// point point from each of the control points
|
||||
c[i] = v[i].subtract(point);
|
||||
// Determine the d's -- these are vectors created by subtracting
|
||||
// each control point from the next
|
||||
if (i < n)
|
||||
d[i] = v[i + 1].subtract(v[i]).multiply(n);
|
||||
}
|
||||
|
||||
// Create the c,d table -- this is a table of dot products of the
|
||||
// c's and d's
|
||||
for (var row = 0; row < n; row++) {
|
||||
cd[row] = [];
|
||||
for (var column = 0; column <= n; column++)
|
||||
cd[row][column] = d[row].dot(c[column]);
|
||||
}
|
||||
|
||||
// Now, apply the z's to the dot products, on the skew diagonal
|
||||
// Also, set up the x-values, making these "points"
|
||||
for (var i = 0; i <= degree; i++)
|
||||
w[i] = new Point(i / degree, 0);
|
||||
|
||||
for (k = 0; k <= degree; k++) {
|
||||
var lb = Math.max(0, k - n + 1),
|
||||
ub = Math.min(k, n);
|
||||
for (var i = lb; i <= ub; i++) {
|
||||
var j = k - i;
|
||||
w[k].y += cd[j][i] * zCubic[j][i];
|
||||
}
|
||||
}
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a 5th-degree equation in Bernstein-Bezier form, find all of the
|
||||
* roots in the interval [0, 1]. Return the number of roots found.
|
||||
*/
|
||||
function findRoots(w, depth) {
|
||||
switch (countCrossings(w)) {
|
||||
case 0:
|
||||
// No solutions here
|
||||
return [];
|
||||
case 1:
|
||||
// Unique solution
|
||||
// Stop recursion when the tree is deep enough
|
||||
// if deep enough, return 1 solution at midpoint
|
||||
if (depth >= maxDepth)
|
||||
return [0.5 * (w[0].x + w[5].x)];
|
||||
// Compute intersection of chord from first control point to last
|
||||
// with x-axis.
|
||||
if (isFlatEnough(w))
|
||||
return [xAxis.intersect(new Line(w[0], w[5], true)).x];
|
||||
}
|
||||
|
||||
// Otherwise, solve recursively after
|
||||
// subdividing control polygon
|
||||
var p = [[]],
|
||||
left = [],
|
||||
right = [];
|
||||
for (var j = 0; j <= 5; j++)
|
||||
p[0][j] = new Point(w[j]);
|
||||
|
||||
// Triangle computation
|
||||
for (var i = 1; i <= 5; i++) {
|
||||
p[i] = [];
|
||||
for (var j = 0 ; j <= 5 - i; j++)
|
||||
p[i][j] = p[i - 1][j].add(p[i - 1][j + 1]).multiply(0.5);
|
||||
}
|
||||
for (var j = 0; j <= 5; j++) {
|
||||
left[j] = p[j][0];
|
||||
right[j] = p[5 - j][j];
|
||||
}
|
||||
|
||||
return findRoots(left, depth + 1).concat(findRoots(right, depth + 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of times a Bezier control polygon crosses the x-axis.
|
||||
* This number is >= the number of roots.
|
||||
*/
|
||||
function countCrossings(v) {
|
||||
var crossings = 0,
|
||||
prevSign = null;
|
||||
for (var i = 0, l = v.length; i < l; i++) {
|
||||
var sign = v[i].y < 0 ? -1 : 1;
|
||||
if (prevSign != null && sign != prevSign)
|
||||
crossings++;
|
||||
prevSign = sign;
|
||||
}
|
||||
return crossings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the control polygon of a Bezier curve is flat enough for
|
||||
* recursive subdivision to bottom out.
|
||||
*/
|
||||
function isFlatEnough(v) {
|
||||
// Find the perpendicular distance from each interior control point to
|
||||
// line connecting v[0] and v[degree]
|
||||
|
||||
// Derive the implicit equation for line connecting first
|
||||
// and last control points
|
||||
var n = v.length - 1,
|
||||
a = v[0].y - v[n].y,
|
||||
b = v[n].x - v[0].x,
|
||||
c = v[0].x * v[n].y - v[n].x * v[0].y,
|
||||
abSquared = a * a + b * b,
|
||||
maxAbove = 0,
|
||||
maxBelow = 0;
|
||||
// Find the largest distance
|
||||
for (var i = 1; i < n; i++) {
|
||||
// Compute distance from each of the points to that line
|
||||
var val = a * v[i].x + b * v[i].y + c,
|
||||
dist = val * val / abSquared;
|
||||
if (val < 0 && dist > maxBelow) {
|
||||
maxBelow = dist;
|
||||
} else if (dist > maxAbove) {
|
||||
maxAbove = dist;
|
||||
}
|
||||
}
|
||||
// Compute intercepts of bounding box
|
||||
return Math.abs((maxAbove + maxBelow) / a) * 0.5 < epsilon;
|
||||
}
|
||||
|
||||
return {
|
||||
getNearestParameterFor: function(point) {
|
||||
var p1 = this._segment1._point,
|
||||
h1 = this._segment1._handleOut,
|
||||
h2 = this._segment2._handleIn,
|
||||
p2 = this._segment2._point;
|
||||
|
||||
var w = toBezierForm([p1, p1.add(h1), p2.add(h2), p2], point);
|
||||
// Also look at beginning and end of curve (t = 0 / 1)
|
||||
var roots = findRoots(w, 0).concat([0, 1]);
|
||||
var min = Infinity,
|
||||
best;
|
||||
for (var i = 0; i < roots.length; i++) {
|
||||
var dist = point.getDistance(this.getPoint(roots[i]));
|
||||
if (dist < min) {
|
||||
min = dist;
|
||||
best = roots[i];
|
||||
}
|
||||
}
|
||||
return best;
|
||||
},
|
||||
|
||||
getNearestPointFor: function(point) {
|
||||
return this.getPoint(this.getNearestParameterFor(point));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue