Line and segment intersection

This commit is contained in:
Mark Tolmacs 2024-09-30 14:00:52 +02:00
parent f347281c21
commit b7f504796b
No known key found for this signature in database
3 changed files with 157 additions and 29 deletions

View file

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

View file

@ -1,5 +1,6 @@
import { pointCenter, pointRotateRads } from "./point"; import { point, pointCenter, pointRotateRads } from "./point";
import type { GenericPoint, Line, Radians } from "./types"; import { segmentIncludesPoint } from "./segment";
import type { GenericPoint, Line, Radians, Segment } from "./types";
/** /**
* Create a line from two points. * Create a line from two points.
@ -40,13 +41,55 @@ export function lineFromPointArray<P extends GenericPoint>(
// return the coordinates resulting from rotating the given line about an origin by an angle in degrees // 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 // note that when the origin is not given, the midpoint of the given line is used as the origin
export const lineRotate = <Point extends GenericPoint>( export function lineRotate<Point extends GenericPoint>(
l: Line<Point>, l: Line<Point>,
angle: Radians, angle: Radians,
origin?: Point, origin?: Point,
): Line<Point> => { ): Line<Point> {
return line( return line(
pointRotateRads(l[0], origin || pointCenter(l[0], l[1]), angle), pointRotateRads(l[0], origin || pointCenter(l[0], l[1]), angle),
pointRotateRads(l[1], 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<Point extends GenericPoint>(
[[x1, y1], [x2, y2]]: Line<Point>,
[[x3, y3], [x4, y4]]: Line<Point>,
): 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<Point>(xnum / den, ynum / den);
}
/**
* Returns the intersection point of a segment and a line
*
* @param l
* @param s
* @returns
*/
export function lineIntersectsSegment<Point extends GenericPoint>(
l: Line<Point>,
s: Segment<Point>,
): Point | null {
const candidate = lineIntersectsLine(l, line(s[0], s[1]));
if (!candidate || !segmentIncludesPoint(candidate, s)) {
return null;
}
return candidate;
}

View file

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