diff --git a/packages/math/line.test.ts b/packages/math/line.test.ts new file mode 100644 index 0000000000..f72617d4bb --- /dev/null +++ b/packages/math/line.test.ts @@ -0,0 +1,51 @@ +import { line, lineIntersectsLine, lineIntersectsSegment } from "./line"; +import { point } from "./point"; +import { segment } from "./segment"; + +describe("line-line intersections", () => { + it("should correctly detect intersection at origin", () => { + expect( + lineIntersectsLine( + line(point(-5, -5), point(5, 5)), + line(point(5, -5), point(-5, 5)), + ), + ).toEqual(point(0, 0)); + }); + + it("should correctly detect intersection at non-origin", () => { + expect( + lineIntersectsLine( + line(point(0, 0), point(10, 10)), + line(point(10, 0), point(0, 10)), + ), + ).toEqual(point(5, 5)); + }); + + it("should correctly detect parallel lines", () => { + expect( + lineIntersectsLine( + line(point(0, 0), point(0, 10)), + line(point(10, 0), point(10, 10)), + ), + ).toBe(null); + }); +}); + +describe("line-segment intersections", () => { + it("should correctly detect intersection", () => { + expect( + lineIntersectsSegment( + line(point(0, 0), point(5, 0)), + segment(point(2, -2), point(3, 2)), + ), + ).toEqual(point(2.5, -0)); + }); + it("should correctly detect non-intersection", () => { + expect( + lineIntersectsSegment( + line(point(0, 0), point(5, 0)), + segment(point(3, 1), point(4, 4)), + ), + ).toEqual(null); + }); +}); diff --git a/packages/math/line.ts b/packages/math/line.ts index 6ff8a66eeb..16a0ec02f2 100644 --- a/packages/math/line.ts +++ b/packages/math/line.ts @@ -1,5 +1,6 @@ -import { pointCenter, pointRotateRads } from "./point"; -import type { GenericPoint, Line, Radians } from "./types"; +import { point, pointCenter, pointRotateRads } from "./point"; +import { segmentIncludesPoint } from "./segment"; +import type { GenericPoint, Line, Radians, Segment } from "./types"; /** * Create a line from two points. @@ -40,13 +41,55 @@ export function lineFromPointArray

( // return the coordinates resulting from rotating the given line about an origin by an angle in degrees // note that when the origin is not given, the midpoint of the given line is used as the origin -export const lineRotate = ( +export function lineRotate( l: Line, angle: Radians, origin?: Point, -): Line => { +): Line { return line( pointRotateRads(l[0], origin || pointCenter(l[0], l[1]), angle), pointRotateRads(l[1], origin || pointCenter(l[0], l[1]), angle), ); -}; +} + +/** + * Returns the intersection point of two infinite lines, if any + * + * @param a One of the line to intersect + * @param b Another line to intersect + * @returns The intersection point + */ +export function lineIntersectsLine( + [[x1, y1], [x2, y2]]: Line, + [[x3, y3], [x4, y4]]: Line, +): Point | null { + const a = x1 * y2 - x2 * y1; + const c = x3 * y4 - x4 * y3; + const den = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4); + if (den === 0) { + return null; + } + const xnum = a * (x3 - x4) - (x1 - x2) * c; + const ynum = a * (y3 - y4) - (y1 - y2) * c; + + return point(xnum / den, ynum / den); +} + +/** + * Returns the intersection point of a segment and a line + * + * @param l + * @param s + * @returns + */ +export function lineIntersectsSegment( + l: Line, + s: Segment, +): Point | null { + const candidate = lineIntersectsLine(l, line(s[0], s[1])); + if (!candidate || !segmentIncludesPoint(candidate, s)) { + return null; + } + + return candidate; +} diff --git a/packages/math/segment.ts b/packages/math/segment.ts index baec9065ab..775402ab4f 100644 --- a/packages/math/segment.ts +++ b/packages/math/segment.ts @@ -1,3 +1,4 @@ +import { lineIntersectsSegment } from "./line"; import { isPoint, pointCenter, @@ -5,7 +6,7 @@ import { pointRotateRads, pointsEqual, } from "./point"; -import type { GenericPoint, Segment, Radians } from "./types"; +import type { GenericPoint, Segment, Radians, Line } from "./types"; import { PRECISION } from "./utils"; import { vectorAdd, @@ -38,17 +39,21 @@ export function segmentFromPointArray

( } /** + * Determines if the provided value is a segment * - * @param segment - * @returns + * @param value The candidate + * @returns Returns TRUE if the provided value is a segment */ -export const isSegment = ( - segment: unknown, -): segment is Segment => - Array.isArray(segment) && - segment.length === 2 && - isPoint(segment[0]) && - isPoint(segment[0]); +export function isSegment( + value: unknown, +): value is Segment { + return ( + Array.isArray(value) && + segment.length === 2 && + isPoint(value[0]) && + isPoint(value[0]) + ); +} /** * Return the coordinates resulting from rotating the given line about an origin by an angle in radians @@ -59,25 +64,25 @@ export const isSegment = ( * @param origin * @returns */ -export const segmentRotate = ( +export function segmentRotate( l: Segment, angle: Radians, origin?: Point, -): Segment => { +): Segment { return segment( pointRotateRads(l[0], origin || pointCenter(l[0], l[1]), angle), pointRotateRads(l[1], origin || pointCenter(l[0], l[1]), angle), ); -}; +} /** * Calculates the point two line segments with a definite start and end point * intersect at. */ -export const segmentsIntersectAt = ( +export function segmentsIntersectAt( a: Readonly>, b: Readonly>, -): Point | null => { +): Point | null { const a0 = vectorFromPoint(a[0]); const a1 = vectorFromPoint(a[1]); const b0 = vectorFromPoint(b[0]); @@ -105,26 +110,41 @@ export const segmentsIntersectAt = ( } return null; -}; +} -export const segmentIncludesPoint = ( +/** + * Determnines if a point lies on a segment + * + * @param point + * @param s + * @param threshold + * @returns + */ +export function segmentIncludesPoint( point: Point, - line: Segment, + s: Segment, threshold = PRECISION, -) => { - const distance = segmentDistanceToPoint(point, line); +) { + const distance = segmentDistanceToPoint(point, s); if (distance === 0) { return true; } return distance < threshold; -}; +} -export const segmentDistanceToPoint = ( +/** + * Returns the shortest distance from a point to a segment. + * + * @param p + * @param s + * @returns + */ +export function segmentDistanceToPoint( p: Point, s: Segment, -) => { +): number { const [x, y] = p; const [[x1, y1], [x2, y2]] = s; @@ -157,4 +177,18 @@ export const segmentDistanceToPoint = ( const dx = x - xx; const dy = y - yy; return Math.sqrt(dx * dx + dy * dy); -}; +} + +/** + * Returns the intersection point between a segment and a line, if any + * + * @param s + * @param l + * @returns + */ +export function segmentIntersectsLine( + s: Segment, + l: Line, +): Point | null { + return lineIntersectsSegment(l, s); +}