mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Line and segment intersection
This commit is contained in:
parent
f347281c21
commit
b7f504796b
3 changed files with 157 additions and 29 deletions
51
packages/math/line.test.ts
Normal file
51
packages/math/line.test.ts
Normal 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);
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue