374 lines
13 KiB
JavaScript
374 lines
13 KiB
JavaScript
|
|
/**
|
||
|
|
* Determine the relative orientation of three points in two-dimensional space.
|
||
|
|
* The result is also an approximation of twice the signed area of the triangle defined by the three points.
|
||
|
|
* This method is fast - but not robust against issues of floating point precision. Best used with integer coordinates.
|
||
|
|
* Adapted from https://github.com/mourner/robust-predicates.
|
||
|
|
* @param {Point} a An endpoint of segment AB, relative to which point C is tested
|
||
|
|
* @param {Point} b An endpoint of segment AB, relative to which point C is tested
|
||
|
|
* @param {Point} c A point that is tested relative to segment AB
|
||
|
|
* @returns {number} The relative orientation of points A, B, and C
|
||
|
|
* A positive value if the points are in counter-clockwise order (C lies to the left of AB)
|
||
|
|
* A negative value if the points are in clockwise order (C lies to the right of AB)
|
||
|
|
* Zero if the points A, B, and C are collinear.
|
||
|
|
*/
|
||
|
|
export function orient2dFast(a, b, c) {
|
||
|
|
return (a.y - c.y) * (b.x - c.x) - (a.x - c.x) * (b.y - c.y);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Quickly test whether the line segment AB intersects with the line segment CD.
|
||
|
|
* This method does not determine the point of intersection, for that use lineLineIntersection.
|
||
|
|
* @param {Point} a The first endpoint of segment AB
|
||
|
|
* @param {Point} b The second endpoint of segment AB
|
||
|
|
* @param {Point} c The first endpoint of segment CD
|
||
|
|
* @param {Point} d The second endpoint of segment CD
|
||
|
|
* @returns {boolean} Do the line segments intersect?
|
||
|
|
*/
|
||
|
|
export function lineSegmentIntersects(a, b, c, d) {
|
||
|
|
|
||
|
|
// First test the orientation of A and B with respect to CD to reject collinear cases
|
||
|
|
const xa = foundry.utils.orient2dFast(a, b, c);
|
||
|
|
const xb = foundry.utils.orient2dFast(a, b, d);
|
||
|
|
if ( !xa && !xb ) return false;
|
||
|
|
const xab = (xa * xb) <= 0;
|
||
|
|
|
||
|
|
// Also require an intersection of CD with respect to AB
|
||
|
|
const xcd = (foundry.utils.orient2dFast(c, d, a) * foundry.utils.orient2dFast(c, d, b)) <= 0;
|
||
|
|
return xab && xcd;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @typedef {Object} LineIntersection
|
||
|
|
* @property {number} x The x-coordinate of intersection
|
||
|
|
* @property {number} y The y-coordinate of intersection
|
||
|
|
* @property {number} t0 The vector distance from A to B on segment AB
|
||
|
|
* @property {number} [t1] The vector distance from C to D on segment CD
|
||
|
|
*/
|
||
|
|
|
||
|
|
/**
|
||
|
|
* An internal helper method for computing the intersection between two infinite-length lines.
|
||
|
|
* Adapted from http://paulbourke.net/geometry/pointlineplane/.
|
||
|
|
* @param {Point} a The first endpoint of segment AB
|
||
|
|
* @param {Point} b The second endpoint of segment AB
|
||
|
|
* @param {Point} c The first endpoint of segment CD
|
||
|
|
* @param {Point} d The second endpoint of segment CD
|
||
|
|
* @param {object} [options] Options which affect the intersection test
|
||
|
|
* @param {boolean} [options.t1=false] Return the optional vector distance from C to D on CD
|
||
|
|
* @returns {LineIntersection|null} An intersection point, or null if no intersection occurred
|
||
|
|
*/
|
||
|
|
export function lineLineIntersection(a, b, c, d, {t1=false}={}) {
|
||
|
|
|
||
|
|
// If either line is length 0, they cannot intersect
|
||
|
|
if (((a.x === b.x) && (a.y === b.y)) || ((c.x === d.x) && (c.y === d.y))) return null;
|
||
|
|
|
||
|
|
// Check denominator - avoid parallel lines where d = 0
|
||
|
|
const dnm = ((d.y - c.y) * (b.x - a.x) - (d.x - c.x) * (b.y - a.y));
|
||
|
|
if (dnm === 0) return null;
|
||
|
|
|
||
|
|
// Vector distances
|
||
|
|
const t0 = ((d.x - c.x) * (a.y - c.y) - (d.y - c.y) * (a.x - c.x)) / dnm;
|
||
|
|
t1 = t1 ? ((b.x - a.x) * (a.y - c.y) - (b.y - a.y) * (a.x - c.x)) / dnm : undefined;
|
||
|
|
|
||
|
|
// Return the point of intersection
|
||
|
|
return {
|
||
|
|
x: a.x + t0 * (b.x - a.x),
|
||
|
|
y: a.y + t0 * (b.y - a.y),
|
||
|
|
t0: t0,
|
||
|
|
t1: t1
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* An internal helper method for computing the intersection between two finite line segments.
|
||
|
|
* Adapted from http://paulbourke.net/geometry/pointlineplane/
|
||
|
|
* @param {Point} a The first endpoint of segment AB
|
||
|
|
* @param {Point} b The second endpoint of segment AB
|
||
|
|
* @param {Point} c The first endpoint of segment CD
|
||
|
|
* @param {Point} d The second endpoint of segment CD
|
||
|
|
* @param {number} [epsilon] A small epsilon which defines a tolerance for near-equality
|
||
|
|
* @returns {LineIntersection|null} An intersection point, or null if no intersection occurred
|
||
|
|
*/
|
||
|
|
export function lineSegmentIntersection(a, b, c, d, epsilon=1e-8) {
|
||
|
|
|
||
|
|
// If either line is length 0, they cannot intersect
|
||
|
|
if (((a.x === b.x) && (a.y === b.y)) || ((c.x === d.x) && (c.y === d.y))) return null;
|
||
|
|
|
||
|
|
// Check denominator - avoid parallel lines where d = 0
|
||
|
|
const dnm = ((d.y - c.y) * (b.x - a.x) - (d.x - c.x) * (b.y - a.y));
|
||
|
|
if (dnm === 0) return null;
|
||
|
|
|
||
|
|
// Vector distance from a
|
||
|
|
const t0 = ((d.x - c.x) * (a.y - c.y) - (d.y - c.y) * (a.x - c.x)) / dnm;
|
||
|
|
if ( !Number.between(t0, 0-epsilon, 1+epsilon) ) return null;
|
||
|
|
|
||
|
|
// Vector distance from c
|
||
|
|
const t1 = ((b.x - a.x) * (a.y - c.y) - (b.y - a.y) * (a.x - c.x)) / dnm;
|
||
|
|
if ( !Number.between(t1, 0-epsilon, 1+epsilon) ) return null;
|
||
|
|
|
||
|
|
// Return the point of intersection and the vector distance from both line origins
|
||
|
|
return {
|
||
|
|
x: a.x + t0 * (b.x - a.x),
|
||
|
|
y: a.y + t0 * (b.y - a.y),
|
||
|
|
t0: Math.clamp(t0, 0, 1),
|
||
|
|
t1: Math.clamp(t1, 0, 1)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @typedef {Object} LineCircleIntersection
|
||
|
|
* @property {boolean} aInside Is point A inside the circle?
|
||
|
|
* @property {boolean} bInside Is point B inside the circle?
|
||
|
|
* @property {boolean} contained Is the segment AB contained within the circle?
|
||
|
|
* @property {boolean} outside Is the segment AB fully outside the circle?
|
||
|
|
* @property {boolean} tangent Is the segment AB tangent to the circle?
|
||
|
|
* @property {Point[]} intersections Intersection points: zero, one, or two
|
||
|
|
*/
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Determine the intersection between a line segment and a circle.
|
||
|
|
* @param {Point} a The first vertex of the segment
|
||
|
|
* @param {Point} b The second vertex of the segment
|
||
|
|
* @param {Point} center The center of the circle
|
||
|
|
* @param {number} radius The radius of the circle
|
||
|
|
* @param {number} epsilon A small tolerance for floating point precision
|
||
|
|
* @returns {LineCircleIntersection} The intersection of the segment AB with the circle
|
||
|
|
*/
|
||
|
|
export function lineCircleIntersection(a, b, center, radius, epsilon=1e-8) {
|
||
|
|
const r2 = Math.pow(radius, 2);
|
||
|
|
let intersections = [];
|
||
|
|
|
||
|
|
// Test whether endpoint A is contained
|
||
|
|
const ar2 = Math.pow(a.x - center.x, 2) + Math.pow(a.y - center.y, 2);
|
||
|
|
const aInside = ar2 < r2 - epsilon;
|
||
|
|
|
||
|
|
// Test whether endpoint B is contained
|
||
|
|
const br2 = Math.pow(b.x - center.x, 2) + Math.pow(b.y - center.y, 2);
|
||
|
|
const bInside = br2 < r2 - epsilon;
|
||
|
|
|
||
|
|
// Find quadratic intersection points
|
||
|
|
const contained = aInside && bInside;
|
||
|
|
if ( !contained ) intersections = quadraticIntersection(a, b, center, radius, epsilon);
|
||
|
|
|
||
|
|
// Return the intersection data
|
||
|
|
return {
|
||
|
|
aInside,
|
||
|
|
bInside,
|
||
|
|
contained,
|
||
|
|
outside: !contained && !intersections.length,
|
||
|
|
tangent: !aInside && !bInside && intersections.length === 1,
|
||
|
|
intersections
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Identify the point closest to C on segment AB
|
||
|
|
* @param {Point} c The reference point C
|
||
|
|
* @param {Point} a Point A on segment AB
|
||
|
|
* @param {Point} b Point B on segment AB
|
||
|
|
* @returns {Point} The closest point to C on segment AB
|
||
|
|
*/
|
||
|
|
export function closestPointToSegment(c, a, b) {
|
||
|
|
const dx = b.x - a.x;
|
||
|
|
const dy = b.y - a.y;
|
||
|
|
if (( dx === 0 ) && ( dy === 0 )) {
|
||
|
|
throw new Error("Zero-length segment AB not supported");
|
||
|
|
}
|
||
|
|
const u = (((c.x - a.x) * dx) + ((c.y - a.y) * dy)) / (dx * dx + dy * dy);
|
||
|
|
if ( u < 0 ) return a;
|
||
|
|
if ( u > 1 ) return b;
|
||
|
|
else return {
|
||
|
|
x: a.x + (u * dx),
|
||
|
|
y: a.y + (u * dy)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Determine the points of intersection between a line segment (p0,p1) and a circle.
|
||
|
|
* There will be zero, one, or two intersections
|
||
|
|
* See https://math.stackexchange.com/a/311956.
|
||
|
|
* @param {Point} p0 The initial point of the line segment
|
||
|
|
* @param {Point} p1 The terminal point of the line segment
|
||
|
|
* @param {Point} center The center of the circle
|
||
|
|
* @param {number} radius The radius of the circle
|
||
|
|
* @param {number} [epsilon=0] A small tolerance for floating point precision
|
||
|
|
*/
|
||
|
|
export function quadraticIntersection(p0, p1, center, radius, epsilon=0) {
|
||
|
|
const dx = p1.x - p0.x;
|
||
|
|
const dy = p1.y - p0.y;
|
||
|
|
|
||
|
|
// Quadratic terms where at^2 + bt + c = 0
|
||
|
|
const a = Math.pow(dx, 2) + Math.pow(dy, 2);
|
||
|
|
const b = (2 * dx * (p0.x - center.x)) + (2 * dy * (p0.y - center.y));
|
||
|
|
const c = Math.pow(p0.x - center.x, 2) + Math.pow(p0.y - center.y, 2) - Math.pow(radius, 2);
|
||
|
|
|
||
|
|
// Discriminant
|
||
|
|
let disc2 = Math.pow(b, 2) - (4 * a * c);
|
||
|
|
if ( disc2.almostEqual(0) ) disc2 = 0; // segment endpoint touches the circle; 1 intersection
|
||
|
|
else if ( disc2 < 0 ) return []; // no intersections
|
||
|
|
|
||
|
|
// Roots
|
||
|
|
const disc = Math.sqrt(disc2);
|
||
|
|
const t1 = (-b - disc) / (2 * a);
|
||
|
|
|
||
|
|
// If t1 hits (between 0 and 1) it indicates an "entry"
|
||
|
|
const intersections = [];
|
||
|
|
if ( t1.between(0-epsilon, 1+epsilon) ) {
|
||
|
|
intersections.push({
|
||
|
|
x: p0.x + (dx * t1),
|
||
|
|
y: p0.y + (dy * t1)
|
||
|
|
});
|
||
|
|
}
|
||
|
|
if ( !disc2 ) return intersections; // 1 intersection
|
||
|
|
|
||
|
|
// If t2 hits (between 0 and 1) it indicates an "exit"
|
||
|
|
const t2 = (-b + disc) / (2 * a);
|
||
|
|
if ( t2.between(0-epsilon, 1+epsilon) ) {
|
||
|
|
intersections.push({
|
||
|
|
x: p0.x + (dx * t2),
|
||
|
|
y: p0.y + (dy * t2)
|
||
|
|
});
|
||
|
|
}
|
||
|
|
return intersections;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Calculate the centroid non-self-intersecting closed polygon.
|
||
|
|
* See https://en.wikipedia.org/wiki/Centroid#Of_a_polygon.
|
||
|
|
* @param {Point[]|number[]} points The points of the polygon
|
||
|
|
* @returns {Point} The centroid of the polygon
|
||
|
|
*/
|
||
|
|
export function polygonCentroid(points) {
|
||
|
|
const n = points.length;
|
||
|
|
if ( n === 0 ) return {x: 0, y: 0};
|
||
|
|
let x = 0;
|
||
|
|
let y = 0;
|
||
|
|
let a = 0;
|
||
|
|
if ( typeof points[0] === "number" ) {
|
||
|
|
let x0 = points[n - 2];
|
||
|
|
let y0 = points[n - 1];
|
||
|
|
for ( let i = 0; i < n; i += 2 ) {
|
||
|
|
const x1 = points[i];
|
||
|
|
const y1 = points[i + 1];
|
||
|
|
const z = (x0 * y1) - (x1 * y0);
|
||
|
|
x += (x0 + x1) * z;
|
||
|
|
y += (y0 + y1) * z;
|
||
|
|
x0 = x1;
|
||
|
|
y0 = y1;
|
||
|
|
a += z;
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
let {x: x0, y: y0} = points[n - 1];
|
||
|
|
for ( let i = 0; i < n; i++ ) {
|
||
|
|
const {x: x1, y: y1} = points[i];
|
||
|
|
const z = (x0 * y1) - (x1 * y0);
|
||
|
|
x += (x0 + x1) * z;
|
||
|
|
y += (y0 + y1) * z;
|
||
|
|
x0 = x1;
|
||
|
|
y0 = y1;
|
||
|
|
a += z;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
a *= 3;
|
||
|
|
x /= a;
|
||
|
|
y /= a;
|
||
|
|
return {x, y};
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Test whether the circle given by the center and radius intersects the path (open or closed).
|
||
|
|
* @param {Point[]|number[]} points The points of the path
|
||
|
|
* @param {boolean} close If true, the edge from the last to the first point is tested
|
||
|
|
* @param {Point} center The center of the circle
|
||
|
|
* @param {number} radius The radius of the circle
|
||
|
|
* @returns {boolean} Does the circle intersect the path?
|
||
|
|
*/
|
||
|
|
export function pathCircleIntersects(points, close, center, radius) {
|
||
|
|
const n = points.length;
|
||
|
|
if ( n === 0 ) return false;
|
||
|
|
const {x: cx, y: cy} = center;
|
||
|
|
const rr = radius * radius;
|
||
|
|
let i;
|
||
|
|
let x0;
|
||
|
|
let y0;
|
||
|
|
if ( typeof points[0] === "number" ) {
|
||
|
|
if ( close ) {
|
||
|
|
i = 0;
|
||
|
|
x0 = points[n - 2];
|
||
|
|
y0 = points[n - 1];
|
||
|
|
} else {
|
||
|
|
i = 2;
|
||
|
|
x0 = points[0];
|
||
|
|
y0 = points[1];
|
||
|
|
}
|
||
|
|
for ( ; i < n; i += 2 ) {
|
||
|
|
const x1 = points[i];
|
||
|
|
const y1 = points[i + 1];
|
||
|
|
let dx = cx - x0;
|
||
|
|
let dy = cy - y0;
|
||
|
|
const nx = x1 - x0;
|
||
|
|
const ny = y1 - y0;
|
||
|
|
const t = Math.clamp(((dx * nx) + (dy * ny)) / ((nx * nx) + (ny * ny)), 0, 1);
|
||
|
|
dx = (t * nx) - dx;
|
||
|
|
dy = (t * ny) - dy;
|
||
|
|
if ( (dx * dx) + (dy * dy) <= rr ) return true;
|
||
|
|
x0 = x1;
|
||
|
|
y0 = y1;
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
if ( close ) {
|
||
|
|
i = 0;
|
||
|
|
({x: x0, y: y0} = points[n - 1]);
|
||
|
|
} else {
|
||
|
|
i = 1;
|
||
|
|
({x: x0, y: y0} = points[0]);
|
||
|
|
}
|
||
|
|
for ( ; i < n; i++ ) {
|
||
|
|
const {x: x2, y: y2} = points[i];
|
||
|
|
let dx = cx - x0;
|
||
|
|
let dy = cy - y0;
|
||
|
|
const nx = x1 - x0;
|
||
|
|
const ny = y1 - y0;
|
||
|
|
const t = Math.clamp(((dx * nx) + (dy * ny)) / ((nx * nx) + (ny * ny)), 0, 1);
|
||
|
|
dx = (t * nx) - dx;
|
||
|
|
dy = (t * ny) - dy;
|
||
|
|
if ( (dx * dx) + (dy * dy) <= rr ) return true;
|
||
|
|
x0 = x2;
|
||
|
|
y0 = y2;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Test whether two circles (with position and radius) intersect.
|
||
|
|
* @param {number} x0 x center coordinate of circle A.
|
||
|
|
* @param {number} y0 y center coordinate of circle A.
|
||
|
|
* @param {number} r0 radius of circle A.
|
||
|
|
* @param {number} x1 x center coordinate of circle B.
|
||
|
|
* @param {number} y1 y center coordinate of circle B.
|
||
|
|
* @param {number} r1 radius of circle B.
|
||
|
|
* @returns {boolean} True if the two circles intersect, false otherwise.
|
||
|
|
*/
|
||
|
|
export function circleCircleIntersects(x0, y0, r0, x1, y1, r1) {
|
||
|
|
return Math.hypot(x0 - x1, y0 - y1) <= (r0 + r1);
|
||
|
|
}
|
||
|
|
|