Форк Rambox
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

506 lines
18 KiB

/**
* @private
* Singleton that provides methods used by the Ext.draw.Path
* for hit testing and finding path intersection points.
*/
Ext.define('Ext.draw.PathUtil', function () {
var abs = Math.abs,
pow = Math.pow,
cos = Math.cos,
acos = Math.acos,
sqrt = Math.sqrt,
PI = Math.PI;
// For extra info see: http://pomax.github.io/bezierinfo/
return {
singleton: true,
requires: [
'Ext.draw.overrides.Path',
'Ext.draw.overrides.sprite.Path',
'Ext.draw.overrides.Surface'
],
/**
* @private
* Finds roots of a cubic equation in t, where t lies in the interval of [0,1].
* Based on http://www.particleincell.com/blog/2013/cubic-line-intersection/
* @param P {Number[]} Cubic equation coefficients.
* @return {Number[]} Returns an array of parametric intersection locations along the cubic,
* with -1 indicating an out-of-bounds intersection
* (before or after the end point or in the imaginary plane).
*/
cubicRoots: function (P) {
var a = P[0],
b = P[1],
c = P[2],
d = P[3];
if (a === 0) {
return this.quadraticRoots(b, c, d);
}
var A = b / a,
B = c / a,
C = d / a,
Q = (3*B - pow(A, 2)) / 9,
R = (9*A*B - 27*C - 2*pow(A, 3)) / 54,
D = pow(Q, 3) + pow(R, 2), // Polynomial discriminant.
t = [],
S, T, Im, th, i,
sign = Ext.Number.sign;
if (D >= 0) { // Complex or duplicate roots.
S = sign(R + sqrt(D)) * pow(abs(R + sqrt(D)), 1/3);
T = sign(R - sqrt(D)) * pow(abs(R - sqrt(D)), 1/3);
t[0] = -A/3 + (S + T); // Real root.
t[1] = -A/3 - (S + T)/2; // Real part of complex root.
t[2] = t[1]; // Real part of complex root.
Im = abs(sqrt(3) * (S - T)/2); // Complex part of root pair.
// Discard complex roots.
if (Im !== 0) {
t[1] =- 1;
t[2] =- 1;
}
} else { // Distinct real roots.
th = acos(R / sqrt(-pow(Q, 3)));
t[0] = 2*sqrt(-Q)*cos(th/3) - A/3;
t[1] = 2*sqrt(-Q)*cos((th + 2*PI)/3) - A/3;
t[2] = 2*sqrt(-Q)*cos((th + 4*PI)/3) - A/3;
}
// Discard out of spec roots.
for (i = 0; i < 3; i++) {
if (t[i] < 0 || t[i] > 1) {
t[i] = -1;
}
}
return t;
},
/**
* @private
* Finds roots of a quadratic equation in t, where t lies in the interval of [0,1].
* Takes three quadratic equation coefficients as parameters.
* @param a {Number}
* @param b {Number}
* @param c {Number}
* @return {Array}
*/
quadraticRoots: function (a, b, c) {
var D, rD, t, i;
if (a === 0) {
return this.linearRoot(b, c);
}
D = b*b - 4*a*c;
if (D === 0) { // One real root.
t = [-b/(2*a)];
} else if (D > 0) { // Distinct real roots.
rD = sqrt(D);
t = [(-b - rD) / (2*a), (-b + rD) / (2*a)];
} else { // Complex roots.
return [];
}
for (i = 0; i < t.length; i++) {
if (t[i] < 0 || t[i] > 1) {
t[i] = -1;
}
}
return t;
},
/**
* @private
* Finds roots of a linear equation in t, where t lies in the interval of [0,1].
* Takes two linear equation coefficients as parameters.
* @param a {Number}
* @param b {Number}
* @return {Array}
*/
linearRoot: function (a, b) {
var t = -b/a;
if (a === 0 || t < 0 || t > 1) {
return [];
}
return [t];
},
/**
* @private
* Calculates the coefficients of a cubic function for the given coordinates.
* @param P0 {Number}
* @param P1 {Number}
* @param P2 {Number}
* @param P3 {Number}
* @return {Array}
*/
bezierCoeffs: function (P0, P1, P2, P3) {
var Z = [];
Z[0] = -P0 + 3*P1 - 3*P2 + P3;
Z[1] = 3*P0 - 6*P1 + 3*P2;
Z[2] = -3*P0 + 3*P1;
Z[3] = P0;
return Z;
},
/**
* @private
* Computes intersection points between a cubic spline and a line segment.
* Takes in x/y components of cubic control points and line segment start/end points
* as parameters.
* @param px1 {Number}
* @param px2 {Number}
* @param px3 {Number}
* @param px4 {Number}
* @param py1 {Number}
* @param py2 {Number}
* @param py3 {Number}
* @param py4 {Number}
* @param x1 {Number}
* @param y1 {Number}
* @param x2 {Number}
* @param y2 {Number}
* @return {Array} Array of intersection points, where each intersection point
* is itself a two-item array [x,y].
*/
cubicLineIntersections: function (px1, px2, px3, px4, py1, py2, py3, py4,
x1, y1, x2, y2) {
var P = [],
intersections = [],
// Finding line equation coefficients.
A = y1 - y2,
B = x2 - x1,
C = x1 * (y2 - y1) - y1 * (x2 - x1),
// Finding cubic Bezier curve equation coefficients.
bx = this.bezierCoeffs(px1, px2, px3, px4),
by = this.bezierCoeffs(py1, py2, py3, py4),
i, r, s,
t, tt, ttt,
cx, cy;
P[0] = A*bx[0] + B*by[0]; // t^3
P[1] = A*bx[1] + B*by[1]; // t^2
P[2] = A*bx[2] + B*by[2]; // t
P[3] = A*bx[3] + B*by[3] + C; // 1
r = this.cubicRoots(P);
// Verify the roots are in bounds of the linear segment.
for (i = 0; i < r.length; i++) {
t = r[i];
if (t < 0 || t > 1) {
continue;
}
tt = t*t;
ttt = tt*t;
cx = bx[0]*ttt + bx[1]*tt + bx[2]*t + bx[3];
cy = by[0]*ttt + by[1]*tt + by[2]*t + by[3];
// Above is intersection point assuming infinitely long line segment,
// make sure we are also in bounds of the line.
if ((x2 - x1) !== 0) { // If not vertical line
s = (cx - x1) / (x2 - x1);
} else {
s = (cy - y1) / (y2 - y1);
}
// In bounds?
if (!(s < 0 || s > 1)) {
intersections.push([cx, cy]);
}
}
return intersections;
},
/**
* @private
* Splits cubic Bezier curve into two cubic Bezier curves at point z,
* where z belongs to a range of [0, 1].
* Accepts cubic coefficients and point z as parameters.
* @param P1 {Number}
* @param P2 {Number}
* @param P3 {Number}
* @param P4 {Number}
* @param z Point to split the given curve at.
* @return {Array} Two-item array, where each item is itself an array
* of cubic coefficients.
*/
splitCubic: function (P1, P2, P3, P4, z) {
var zz = z * z,
zzz = z * zz,
iz = z - 1,
izz = iz * iz,
izzz = iz * izz,
// Common point for both curves.
P = zzz * P4 - 3 * zz * iz * P3 + 3 * z * izz * P2 - izzz * P1;
return [
[
P1,
z * P2 - iz * P1,
zz * P3 - 2 * z * iz * P2 + izz * P1,
P
],
[
P,
zz * P4 - 2 * z * iz * P3 + izz * P2,
z * P4 - iz * P3,
P4
]
]
},
/**
* @private
* Returns the dimension of a cubic Bezier curve in a single direction.
* @param a {Number}
* @param b {Number}
* @param c {Number}
* @param d {Number}
* @return {Array} Two-item array representing cubic's range in the given direction.
*/
cubicDimension: function (a, b, c, d) {
var qa = 3 * (-a + 3 * (b - c) + d),
qb = 6 * (a - 2 * b + c),
qc = -3 * (a - b), x, y,
min = Math.min(a, d),
max = Math.max(a, d), delta;
if (qa === 0) {
if (qb === 0) {
return [min, max];
} else {
x = -qc / qb;
if (0 < x && x < 1) {
y = this.interpolateCubic(a, b, c, d, x);
min = Math.min(min, y);
max = Math.max(max, y);
}
}
} else {
delta = qb * qb - 4 * qa * qc;
if (delta >= 0) {
delta = sqrt(delta);
x = (delta - qb) / 2 / qa;
if (0 < x && x < 1) {
y = this.interpolateCubic(a, b, c, d, x);
min = Math.min(min, y);
max = Math.max(max, y);
}
if (delta > 0) {
x -= delta / qa;
if (0 < x && x < 1) {
y = this.interpolateCubic(a, b, c, d, x);
min = Math.min(min, y);
max = Math.max(max, y);
}
}
}
}
return [min, max];
},
/**
* @private
* Calculates a value of a cubic function at the given point t. In other words
* returns a * (1 - t) ^ 3 + 3 * b (1 - t) ^ 2 * t + 3 * c (1 - t) * t ^ 3 + d * t ^ 3
* for given a, b, c, d and t, where t belongs to an interval of [0, 1].
* @param a {Number}
* @param b {Number}
* @param c {Number}
* @param d {Number}
* @param t {Number}
* @return {Number}
*/
interpolateCubic: function (a, b, c, d, t) {
if (t === 0) {
return a;
}
if (t === 1) {
return d;
}
var rate = (1 - t) / t;
return t * t * t * (d + rate * (3 * c + rate * (3 * b + rate * a)));
},
/**
* @private
* Computes intersection points between two cubic Bezier curve segments.
* Takes x/y components of control points for two Bezier curve segments.
* @param ax1 {Number}
* @param ax2 {Number}
* @param ax3 {Number}
* @param ax4 {Number}
* @param ay1 {Number}
* @param ay2 {Number}
* @param ay3 {Number}
* @param ay4 {Number}
* @param bx1 {Number}
* @param bx2 {Number}
* @param bx3 {Number}
* @param bx4 {Number}
* @param by1 {Number}
* @param by2 {Number}
* @param by3 {Number}
* @param by4 {Number}
* @return {Array} Array of intersection points, where each intersection point
* is itself a two-item array [x,y].
*/
cubicsIntersections: function (ax1, ax2, ax3, ax4, ay1, ay2, ay3, ay4,
bx1, bx2, bx3, bx4, by1, by2, by3, by4) {
var me = this,
axDim = me.cubicDimension(ax1, ax2, ax3, ax4),
ayDim = me.cubicDimension(ay1, ay2, ay3, ay4),
bxDim = me.cubicDimension(bx1, bx2, bx3, bx4),
byDim = me.cubicDimension(by1, by2, by3, by4),
splitAx, splitAy, splitBx, splitBy,
points = [];
// Curves' bounding boxes don't intersect.
if (axDim[0] > bxDim[1] || axDim[1] < bxDim[0] || ayDim[0] > byDim[1] || ayDim[1] < byDim[0]) {
return [];
}
// Both curves occupy sub-pixel areas which is effectively their intersection point.
if (abs(ay1 - ay2) < 1 && abs(ay3 - ay4) < 1 && abs(ax1 - ax4) < 1 && abs(ax2 - ax3) < 1 &&
abs(by1 - by2) < 1 && abs(by3 - by4) < 1 && abs(bx1 - bx4) < 1 && abs(bx2 - bx3) < 1) {
return [[ (ax1 + ax4) * 0.5, (ay1 + ay2) * 0.5 ]];
}
splitAx = me.splitCubic(ax1, ax2, ax3, ax4, 0.5);
splitAy = me.splitCubic(ay1, ay2, ay3, ay4, 0.5);
splitBx = me.splitCubic(bx1, bx2, bx3, bx4, 0.5);
splitBy = me.splitCubic(by1, by2, by3, by4, 0.5);
points.push.apply(points, me.cubicsIntersections.apply(me, splitAx[0].concat(splitAy[0], splitBx[0], splitBy[0])));
points.push.apply(points, me.cubicsIntersections.apply(me, splitAx[0].concat(splitAy[0], splitBx[1], splitBy[1])));
points.push.apply(points, me.cubicsIntersections.apply(me, splitAx[1].concat(splitAy[1], splitBx[0], splitBy[0])));
points.push.apply(points, me.cubicsIntersections.apply(me, splitAx[1].concat(splitAy[1], splitBx[1], splitBy[1])));
return points;
},
/**
* @private
* Returns the point [x,y] where two line segments intersect or null.
* Takes x/y components of the start and end point of the segments as parameters.
* Based on Paul Bourke's explanation:
* http://paulbourke.net/geometry/pointlineplane/
* @param x1 {Number}
* @param y1 {Number}
* @param x2 {Number}
* @param y2 {Number}
* @param x3 {Number}
* @param y3 {Number}
* @param x4 {Number}
* @param y4 {Number}
* @return {Number[]|null}
*/
linesIntersection: function (x1, y1, x2, y2, x3, y3, x4, y4) {
var d = (x2 - x1) * (y4 - y3) - (y2 - y1) * (x4 - x3),
ua, ub;
if (d === 0) { // Lines are parallel.
return null;
}
ua = ( (x4 - x3) * (y1 - y3) - (x1 - x3) * (y4 - y3) ) / d;
ub = ( (x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3) ) / d;
if (ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1) {
return [
x1 + ua * (x2 - x1), // x
y1 + ua * (y2 - y1) // y
];
}
return null; // The intersection point is outside one or both segments.
},
/**
* @private
* Checks if a point belongs to a line segment.
* Takes x/y components of the start and end points of the segment and the point's
* coordinates as parameters.
* @param x1 {Number}
* @param y1 {Number}
* @param x2 {Number}
* @param y2 {Number}
* @param x {Number}
* @param y {Number}
* @return {Boolean}
*/
pointOnLine: function (x1, y1, x2, y2, x, y) {
var t, _;
if (abs(x2 - x1) < abs(y2 - y1)) {
_ = x1;
x1 = y1;
y1 = _;
_ = x2;
x2 = y2;
y2 = _;
_ = x;
x = y;
y = _;
}
t = (x - x1) / (x2 - x1);
if (t < 0 || t > 1) {
return false;
}
return abs(y1 + t * (y2 - y1) - y) < 4;
},
/**
* @private
* Checks if a point belongs to a cubic Bezier curve segment.
* Takes x/y components of the control points of the segment and the point's
* coordinates as parameters.
* @param px1 {Number}
* @param px2 {Number}
* @param px3 {Number}
* @param px4 {Number}
* @param py1 {Number}
* @param py2 {Number}
* @param py3 {Number}
* @param py4 {Number}
* @param x {Number}
* @param y {Number}
* @return {Boolean}
*/
pointOnCubic: function (px1, px2, px3, px4, py1, py2, py3, py4, x, y) {
// Finding cubic Bezier curve equation coefficients.
var me = this,
bx = me.bezierCoeffs(px1, px2, px3, px4),
by = me.bezierCoeffs(py1, py2, py3, py4),
i, j, rx, ry, t;
bx[3] -= x;
by[3] -= y;
rx = me.cubicRoots(bx);
ry = me.cubicRoots(by);
for (i = 0; i < rx.length; i++) {
t = rx[i];
for (j = 0; j < ry.length; j++) {
// TODO: for more accurate results tolerance should be dynamic
// TODO: based on the length and shape of the segment.
if (t >= 0 && t <= 1 && abs(t - ry[j]) < 0.05) {
return true;
}
}
}
return false;
}
};
});