diff --git a/packages/element/src/binding.ts b/packages/element/src/binding.ts
index af66ebd9fe..25c09732dc 100644
--- a/packages/element/src/binding.ts
+++ b/packages/element/src/binding.ts
@@ -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 {
diff --git a/packages/element/src/bounds.ts b/packages/element/src/bounds.ts
index 260a9bd4ed..2c07631a7a 100644
--- a/packages/element/src/bounds.ts
+++ b/packages/element/src/bounds.ts
@@ -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 =
(
): 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,
diff --git a/packages/element/src/collision.ts b/packages/element/src/collision.ts
index 5ae7753e63..cc15947edb 100644
--- a/packages/element/src/collision.ts
+++ b/packages/element/src/collision.ts
@@ -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[],
+ segment: LineSegment,
+ 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[],
+ segment: LineSegment,
+ 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,
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,
+ segment: LineSegment,
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(
- l[0],
+ segment[0],
center,
-element.angle as Radians,
);
const rotatedB = pointRotateRads(
- 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,
- );
- if (intersection) {
- intersections.push(pointRotateRads(intersection, center, element.angle));
+ lineIntersections(
+ sides,
+ rotatedIntersector,
+ intersections,
+ center,
+ element.angle,
+ onlyFirst,
+ );
- if (onlyFirst) {
- return intersections;
- }
- }
+ 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,
- );
- if (intersection) {
- intersections.push(pointRotateRads(intersection, center, element.angle));
+ lineIntersections(
+ sides,
+ rotatedIntersector,
+ intersections,
+ center,
+ element.angle,
+ onlyFirst,
+ );
- if (onlyFirst) {
- return intersections;
- }
- }
+ 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;
};
diff --git a/packages/element/src/utils.ts b/packages/element/src/utils.ts
index ca31639d5c..44b0fe79c6 100644
--- a/packages/element/src/utils.ts
+++ b/packages/element/src/utils.ts
@@ -90,6 +90,12 @@ const setElementShapesCacheEntry = (
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[], Curve[]] {
@@ -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,
diff --git a/packages/excalidraw/lasso/utils.ts b/packages/excalidraw/lasso/utils.ts
index 8256f0069a..2cab64662b 100644
--- a/packages/excalidraw/lasso/utils.ts
+++ b/packages/excalidraw/lasso/utils.ts
@@ -4,12 +4,12 @@ import {
polygonFromPoints,
lineSegment,
polygonIncludesPointNonZero,
- doBoundsIntersect,
} from "@excalidraw/math";
import {
type Bounds,
computeBoundTextPosition,
+ doBoundsIntersect,
getBoundTextElement,
getElementBounds,
intersectElementWithLineSegment,
diff --git a/packages/math/src/curve.ts b/packages/math/src/curve.ts
index 3323456b51..fa11abd460 100644
--- a/packages/math/src/curve.ts
+++ b/packages/math/src/curve.ts
@@ -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 = (
export function curveIntersectLineSegment<
Point extends GlobalPoint | LocalPoint,
>(c: Curve, l: LineSegment): 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(
l[0][0] + s * (l[1][0] - l[0][0]),
@@ -295,15 +279,6 @@ export function curveTangent(
);
}
-function curveBounds(
- c: Curve,
-): 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,
diff --git a/packages/math/src/rectangle.ts b/packages/math/src/rectangle.ts
index 394b5c2f83..865e4afe53 100644
--- a/packages/math/src/rectangle.ts
+++ b/packages/math/src/rectangle.ts
@@ -10,6 +10,12 @@ export function rectangle(
return [topLeft, bottomRight] as Rectangle
;
}
+export function rectangleFromNumberSequence<
+ Point extends LocalPoint | GlobalPoint,
+>(minX: number, minY: number, maxX: number, maxY: number) {
+ return rectangle(pointFrom(minX, minY), pointFrom(maxX, maxY));
+}
+
export function rectangleIntersectLineSegment<
Point extends LocalPoint | GlobalPoint,
>(r: Rectangle, l: LineSegment): 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, rectangle2: Rectangle): boolean {
+ const [[minX1, minY1], [maxX1, maxY1]] = rectangle1;
+ const [[minX2, minY2], [maxX2, maxY2]] = rectangle2;
+
+ return minX1 < maxX2 && maxX1 > minX2 && minY1 < maxY2 && maxY1 > minY2;
+}
diff --git a/packages/math/src/utils.ts b/packages/math/src/utils.ts
index 912ddd1358..8807c275e4 100644
--- a/packages/math/src/utils.ts
+++ b/packages/math/src/utils.ts
@@ -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;
-};