chore: Refactor doBoundsIntersect (#9657)

This commit is contained in:
Márk Tolmács 2025-06-16 12:30:42 +02:00 committed by GitHub
parent 058918f8e5
commit 958597dfaa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 172 additions and 105 deletions

View File

@ -24,7 +24,6 @@ import {
pointsEqual,
lineSegmentIntersectionPoints,
PRECISION,
doBoundsIntersect,
} from "@excalidraw/math";
import type { LocalPoint, Radians } from "@excalidraw/math";
@ -33,7 +32,11 @@ import type { AppState } from "@excalidraw/excalidraw/types";
import type { MapEntry, Mutable } from "@excalidraw/common/utility-types";
import { getCenterForBounds, getElementBounds } from "./bounds";
import {
doBoundsIntersect,
getCenterForBounds,
getElementBounds,
} from "./bounds";
import { intersectElementWithLineSegment } from "./collision";
import { distanceToElement } from "./distance";
import {

View File

@ -584,7 +584,7 @@ const solveQuadratic = (
return [s1, s2];
};
const getCubicBezierCurveBound = (
export const getCubicBezierCurveBound = (
p0: GlobalPoint,
p1: GlobalPoint,
p2: GlobalPoint,
@ -1230,6 +1230,20 @@ export const pointInsideBounds = <P extends GlobalPoint | LocalPoint>(
): boolean =>
p[0] > bounds[0] && p[0] < bounds[2] && p[1] > bounds[1] && p[1] < bounds[3];
export const doBoundsIntersect = (
bounds1: Bounds | null,
bounds2: Bounds | null,
): boolean => {
if (bounds1 == null || bounds2 == null) {
return false;
}
const [minX1, minY1, maxX1, maxY1] = bounds1;
const [minX2, minY2, maxX2, maxY2] = bounds2;
return minX1 < maxX2 && maxX1 > minX2 && minY1 < maxY2 && maxY1 > minY2;
};
export const elementCenterPoint = (
element: ExcalidrawElement,
elementsMap: ElementsMap,

View File

@ -11,7 +11,6 @@ import {
vectorFromPoint,
vectorNormalize,
vectorScale,
doBoundsIntersect,
} from "@excalidraw/math";
import {
@ -19,15 +18,22 @@ import {
ellipseSegmentInterceptPoints,
} from "@excalidraw/math/ellipse";
import type { GlobalPoint, LineSegment, Radians } from "@excalidraw/math";
import type {
Curve,
GlobalPoint,
LineSegment,
Radians,
} from "@excalidraw/math";
import type { FrameNameBounds } from "@excalidraw/excalidraw/types";
import { isPathALoop } from "./utils";
import {
type Bounds,
doBoundsIntersect,
elementCenterPoint,
getCenterForBounds,
getCubicBezierCurveBound,
getElementBounds,
} from "./bounds";
import {
@ -255,13 +261,75 @@ export const intersectElementWithLineSegment = (
}
};
const curveIntersections = (
curves: Curve<GlobalPoint>[],
segment: LineSegment<GlobalPoint>,
intersections: GlobalPoint[],
center: GlobalPoint,
angle: Radians,
onlyFirst = false,
) => {
for (const c of curves) {
// Optimize by doing a cheap bounding box check first
const b1 = getCubicBezierCurveBound(c[0], c[1], c[2], c[3]);
const b2 = [
Math.min(segment[0][0], segment[1][0]),
Math.min(segment[0][1], segment[1][1]),
Math.max(segment[0][0], segment[1][0]),
Math.max(segment[0][1], segment[1][1]),
] as Bounds;
if (!doBoundsIntersect(b1, b2)) {
continue;
}
const hits = curveIntersectLineSegment(c, segment);
if (hits.length > 0) {
for (const j of hits) {
intersections.push(pointRotateRads(j, center, angle));
}
if (onlyFirst) {
return intersections;
}
}
}
return intersections;
};
const lineIntersections = (
lines: LineSegment<GlobalPoint>[],
segment: LineSegment<GlobalPoint>,
intersections: GlobalPoint[],
center: GlobalPoint,
angle: Radians,
onlyFirst = false,
) => {
for (const l of lines) {
const intersection = lineSegmentIntersectionPoints(l, segment);
if (intersection) {
intersections.push(pointRotateRads(intersection, center, angle));
if (onlyFirst) {
return intersections;
}
}
}
return intersections;
};
const intersectLinearOrFreeDrawWithLineSegment = (
element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement,
segment: LineSegment<GlobalPoint>,
onlyFirst = false,
): GlobalPoint[] => {
// NOTE: This is the only one which return the decomposed elements
// rotated! This is due to taking advantage of roughjs definitions.
const [lines, curves] = deconstructLinearOrFreeDrawElement(element);
const intersections = [];
const intersections: GlobalPoint[] = [];
for (const l of lines) {
const intersection = lineSegmentIntersectionPoints(l, segment);
@ -275,6 +343,19 @@ const intersectLinearOrFreeDrawWithLineSegment = (
}
for (const c of curves) {
// Optimize by doing a cheap bounding box check first
const b1 = getCubicBezierCurveBound(c[0], c[1], c[2], c[3]);
const b2 = [
Math.min(segment[0][0], segment[1][0]),
Math.min(segment[0][1], segment[1][1]),
Math.max(segment[0][0], segment[1][0]),
Math.max(segment[0][1], segment[1][1]),
] as Bounds;
if (!doBoundsIntersect(b1, b2)) {
continue;
}
const hits = curveIntersectLineSegment(c, segment);
if (hits.length > 0) {
@ -292,7 +373,7 @@ const intersectLinearOrFreeDrawWithLineSegment = (
const intersectRectanguloidWithLineSegment = (
element: ExcalidrawRectanguloidElement,
elementsMap: ElementsMap,
l: LineSegment<GlobalPoint>,
segment: LineSegment<GlobalPoint>,
offset: number = 0,
onlyFirst = false,
): GlobalPoint[] => {
@ -300,48 +381,43 @@ const intersectRectanguloidWithLineSegment = (
// To emulate a rotated rectangle we rotate the point in the inverse angle
// instead. It's all the same distance-wise.
const rotatedA = pointRotateRads<GlobalPoint>(
l[0],
segment[0],
center,
-element.angle as Radians,
);
const rotatedB = pointRotateRads<GlobalPoint>(
l[1],
segment[1],
center,
-element.angle as Radians,
);
const rotatedIntersector = lineSegment(rotatedA, rotatedB);
// Get the element's building components we can test against
const [sides, corners] = deconstructRectanguloidElement(element, offset);
const intersections: GlobalPoint[] = [];
for (const s of sides) {
const intersection = lineSegmentIntersectionPoints(
lineSegment(rotatedA, rotatedB),
s,
lineIntersections(
sides,
rotatedIntersector,
intersections,
center,
element.angle,
onlyFirst,
);
if (intersection) {
intersections.push(pointRotateRads(intersection, center, element.angle));
if (onlyFirst) {
if (onlyFirst && intersections.length > 0) {
return intersections;
}
}
}
for (const t of corners) {
const hits = curveIntersectLineSegment(t, lineSegment(rotatedA, rotatedB));
if (hits.length > 0) {
for (const j of hits) {
intersections.push(pointRotateRads(j, center, element.angle));
}
if (onlyFirst) {
return intersections;
}
}
}
curveIntersections(
corners,
rotatedIntersector,
intersections,
center,
element.angle,
onlyFirst,
);
return intersections;
};
@ -366,38 +442,32 @@ const intersectDiamondWithLineSegment = (
// points. It's all the same distance-wise.
const rotatedA = pointRotateRads(l[0], center, -element.angle as Radians);
const rotatedB = pointRotateRads(l[1], center, -element.angle as Radians);
const rotatedIntersector = lineSegment(rotatedA, rotatedB);
const [sides, corners] = deconstructDiamondElement(element, offset);
const intersections: GlobalPoint[] = [];
for (const s of sides) {
const intersection = lineSegmentIntersectionPoints(
lineSegment(rotatedA, rotatedB),
s,
lineIntersections(
sides,
rotatedIntersector,
intersections,
center,
element.angle,
onlyFirst,
);
if (intersection) {
intersections.push(pointRotateRads(intersection, center, element.angle));
if (onlyFirst) {
if (onlyFirst && intersections.length > 0) {
return intersections;
}
}
}
for (const t of corners) {
const hits = curveIntersectLineSegment(t, lineSegment(rotatedA, rotatedB));
if (hits.length > 0) {
for (const j of hits) {
intersections.push(pointRotateRads(j, center, element.angle));
}
if (onlyFirst) {
return intersections;
}
}
}
curveIntersections(
corners,
rotatedIntersector,
intersections,
center,
element.angle,
onlyFirst,
);
return intersections;
};

View File

@ -90,6 +90,12 @@ const setElementShapesCacheEntry = <T extends ExcalidrawElement>(
shapes.set(offset, shape);
};
/**
* Returns the **rotated** components of freedraw, line or arrow elements.
*
* @param element The linear element to deconstruct
* @returns The rotated in components.
*/
export function deconstructLinearOrFreeDrawElement(
element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement,
): [LineSegment<GlobalPoint>[], Curve<GlobalPoint>[]] {
@ -171,11 +177,11 @@ export function deconstructLinearOrFreeDrawElement(
/**
* Get the building components of a rectanguloid element in the form of
* line segments and curves.
* line segments and curves **unrotated**.
*
* @param element Target rectanguloid element
* @param offset Optional offset to expand the rectanguloid shape
* @returns Tuple of line segments (0) and curves (1)
* @returns Tuple of **unrotated** line segments (0) and curves (1)
*/
export function deconstructRectanguloidElement(
element: ExcalidrawRectanguloidElement,
@ -310,12 +316,12 @@ export function deconstructRectanguloidElement(
}
/**
* Get the building components of a diamond element in the form of
* line segments and curves as a tuple, in this order.
* Get the **unrotated** building components of a diamond element
* in the form of line segments and curves as a tuple, in this order.
*
* @param element The element to deconstruct
* @param offset An optional offset
* @returns Tuple of line segments (0) and curves (1)
* @returns Tuple of line **unrotated** segments (0) and curves (1)
*/
export function deconstructDiamondElement(
element: ExcalidrawDiamondElement,

View File

@ -4,12 +4,12 @@ import {
polygonFromPoints,
lineSegment,
polygonIncludesPointNonZero,
doBoundsIntersect,
} from "@excalidraw/math";
import {
type Bounds,
computeBoundTextPosition,
doBoundsIntersect,
getBoundTextElement,
getElementBounds,
intersectElementWithLineSegment,

View File

@ -1,9 +1,6 @@
import { type Bounds } from "@excalidraw/element";
import { isPoint, pointDistance, pointFrom, pointFromVector } from "./point";
import { vector, vectorNormal, vectorNormalize, vectorScale } from "./vector";
import { LegendreGaussN24CValues, LegendreGaussN24TValues } from "./constants";
import { doBoundsIntersect } from "./utils";
import type { Curve, GlobalPoint, LineSegment, LocalPoint } from "./types";
@ -105,19 +102,6 @@ export const bezierEquation = <Point extends GlobalPoint | LocalPoint>(
export function curveIntersectLineSegment<
Point extends GlobalPoint | LocalPoint,
>(c: Curve<Point>, l: LineSegment<Point>): Point[] {
// Optimize by doing a cheap bounding box check first
const b1 = curveBounds(c);
const b2 = [
Math.min(l[0][0], l[1][0]),
Math.min(l[0][1], l[1][1]),
Math.max(l[0][0], l[1][0]),
Math.max(l[0][1], l[1][1]),
] as Bounds;
if (!doBoundsIntersect(b1, b2)) {
return [];
}
const line = (s: number) =>
pointFrom<Point>(
l[0][0] + s * (l[1][0] - l[0][0]),
@ -295,15 +279,6 @@ export function curveTangent<Point extends GlobalPoint | LocalPoint>(
);
}
function curveBounds<Point extends GlobalPoint | LocalPoint>(
c: Curve<Point>,
): Bounds {
const [P0, P1, P2, P3] = c;
const x = [P0[0], P1[0], P2[0], P3[0]];
const y = [P0[1], P1[1], P2[1], P3[1]];
return [Math.min(...x), Math.min(...y), Math.max(...x), Math.max(...y)];
}
export function curveCatmullRomQuadraticApproxPoints(
points: GlobalPoint[],
tension = 0.5,

View File

@ -10,6 +10,12 @@ export function rectangle<P extends GlobalPoint | LocalPoint>(
return [topLeft, bottomRight] as Rectangle<P>;
}
export function rectangleFromNumberSequence<
Point extends LocalPoint | GlobalPoint,
>(minX: number, minY: number, maxX: number, maxY: number) {
return rectangle(pointFrom<Point>(minX, minY), pointFrom<Point>(maxX, maxY));
}
export function rectangleIntersectLineSegment<
Point extends LocalPoint | GlobalPoint,
>(r: Rectangle<Point>, l: LineSegment<Point>): Point[] {
@ -22,3 +28,12 @@ export function rectangleIntersectLineSegment<
.map((s) => lineSegmentIntersectionPoints(l, s))
.filter((i): i is Point => !!i);
}
export function rectangleIntersectRectangle<
Point extends LocalPoint | GlobalPoint,
>(rectangle1: Rectangle<Point>, rectangle2: Rectangle<Point>): boolean {
const [[minX1, minY1], [maxX1, maxY1]] = rectangle1;
const [[minX2, minY2], [maxX2, maxY2]] = rectangle2;
return minX1 < maxX2 && maxX1 > minX2 && minY1 < maxY2 && maxY1 > minY2;
}

View File

@ -1,5 +1,3 @@
import { type Bounds } from "@excalidraw/element";
export const PRECISION = 10e-5;
export const clamp = (value: number, min: number, max: number) => {
@ -33,17 +31,3 @@ export const isFiniteNumber = (value: any): value is number => {
export const isCloseTo = (a: number, b: number, precision = PRECISION) =>
Math.abs(a - b) < precision;
export const doBoundsIntersect = (
bounds1: Bounds | null,
bounds2: Bounds | null,
): boolean => {
if (bounds1 == null || bounds2 == null) {
return false;
}
const [minX1, minY1, maxX1, maxY1] = bounds1;
const [minX2, minY2, maxX2, maxY2] = bounds2;
return minX1 < maxX2 && maxX1 > minX2 && minY1 < maxY2 && maxY1 > minY2;
};