Files
2025-01-04 00:34:03 +01:00

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);
}