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 { 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;
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue