Initial
This commit is contained in:
373
resources/app/common/utils/geometry.mjs
Normal file
373
resources/app/common/utils/geometry.mjs
Normal file
@@ -0,0 +1,373 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user