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 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<P extends GenericPoint>(
// 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 = <Point extends GenericPoint>(
export function lineRotate<Point extends GenericPoint>(
l: Line<Point>,
angle: Radians,
origin?: Point,
): Line<Point> => {
): Line<Point> {
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<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 {
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<P extends GenericPoint>(
}
/**
* 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 = <Point extends GenericPoint>(
segment: unknown,
): segment is Segment<Point> =>
Array.isArray(segment) &&
segment.length === 2 &&
isPoint(segment[0]) &&
isPoint(segment[0]);
export function isSegment<Point extends GenericPoint>(
value: unknown,
): value is Segment<Point> {
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 = <Point extends GenericPoint>(
* @param origin
* @returns
*/
export const segmentRotate = <Point extends GenericPoint>(
export function segmentRotate<Point extends GenericPoint>(
l: Segment<Point>,
angle: Radians,
origin?: Point,
): Segment<Point> => {
): Segment<Point> {
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 = <Point extends GenericPoint>(
export function segmentsIntersectAt<Point extends GenericPoint>(
a: Readonly<Segment<Point>>,
b: Readonly<Segment<Point>>,
): 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 = <Point extends GenericPoint>(
}
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,
line: Segment<Point>,
s: Segment<Point>,
threshold = PRECISION,
) => {
const distance = segmentDistanceToPoint(point, line);
) {
const distance = segmentDistanceToPoint(point, s);
if (distance === 0) {
return true;
}
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,
s: Segment<Point>,
) => {
): number {
const [x, y] = p;
const [[x1, y1], [x2, y2]] = s;
@ -157,4 +177,18 @@ export const segmentDistanceToPoint = <Point extends GenericPoint>(
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<Point extends GenericPoint>(
s: Segment<Point>,
l: Line<Point>,
): Point | null {
return lineIntersectsSegment(l, s);
}