Browse Source

chore: Refactor doBoundsIntersect (#9657)

Márk Tolmács 2 months ago
parent
commit
958597dfaa

+ 5 - 2
packages/element/src/binding.ts

@@ -24,7 +24,6 @@ import {
   pointsEqual,
   pointsEqual,
   lineSegmentIntersectionPoints,
   lineSegmentIntersectionPoints,
   PRECISION,
   PRECISION,
-  doBoundsIntersect,
 } from "@excalidraw/math";
 } from "@excalidraw/math";
 
 
 import type { LocalPoint, Radians } 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 type { MapEntry, Mutable } from "@excalidraw/common/utility-types";
 
 
-import { getCenterForBounds, getElementBounds } from "./bounds";
+import {
+  doBoundsIntersect,
+  getCenterForBounds,
+  getElementBounds,
+} from "./bounds";
 import { intersectElementWithLineSegment } from "./collision";
 import { intersectElementWithLineSegment } from "./collision";
 import { distanceToElement } from "./distance";
 import { distanceToElement } from "./distance";
 import {
 import {

+ 15 - 1
packages/element/src/bounds.ts

@@ -584,7 +584,7 @@ const solveQuadratic = (
   return [s1, s2];
   return [s1, s2];
 };
 };
 
 
-const getCubicBezierCurveBound = (
+export const getCubicBezierCurveBound = (
   p0: GlobalPoint,
   p0: GlobalPoint,
   p1: GlobalPoint,
   p1: GlobalPoint,
   p2: GlobalPoint,
   p2: GlobalPoint,
@@ -1230,6 +1230,20 @@ export const pointInsideBounds = <P extends GlobalPoint | LocalPoint>(
 ): boolean =>
 ): boolean =>
   p[0] > bounds[0] && p[0] < bounds[2] && p[1] > bounds[1] && p[1] < bounds[3];
   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 = (
 export const elementCenterPoint = (
   element: ExcalidrawElement,
   element: ExcalidrawElement,
   elementsMap: ElementsMap,
   elementsMap: ElementsMap,

+ 125 - 55
packages/element/src/collision.ts

@@ -11,7 +11,6 @@ import {
   vectorFromPoint,
   vectorFromPoint,
   vectorNormalize,
   vectorNormalize,
   vectorScale,
   vectorScale,
-  doBoundsIntersect,
 } from "@excalidraw/math";
 } from "@excalidraw/math";
 
 
 import {
 import {
@@ -19,15 +18,22 @@ import {
   ellipseSegmentInterceptPoints,
   ellipseSegmentInterceptPoints,
 } from "@excalidraw/math/ellipse";
 } 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 type { FrameNameBounds } from "@excalidraw/excalidraw/types";
 
 
 import { isPathALoop } from "./utils";
 import { isPathALoop } from "./utils";
 import {
 import {
   type Bounds,
   type Bounds,
+  doBoundsIntersect,
   elementCenterPoint,
   elementCenterPoint,
   getCenterForBounds,
   getCenterForBounds,
+  getCubicBezierCurveBound,
   getElementBounds,
   getElementBounds,
 } from "./bounds";
 } from "./bounds";
 import {
 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 = (
 const intersectLinearOrFreeDrawWithLineSegment = (
   element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement,
   element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement,
   segment: LineSegment<GlobalPoint>,
   segment: LineSegment<GlobalPoint>,
   onlyFirst = false,
   onlyFirst = false,
 ): GlobalPoint[] => {
 ): 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 [lines, curves] = deconstructLinearOrFreeDrawElement(element);
-  const intersections = [];
+  const intersections: GlobalPoint[] = [];
 
 
   for (const l of lines) {
   for (const l of lines) {
     const intersection = lineSegmentIntersectionPoints(l, segment);
     const intersection = lineSegmentIntersectionPoints(l, segment);
@@ -275,6 +343,19 @@ const intersectLinearOrFreeDrawWithLineSegment = (
   }
   }
 
 
   for (const c of curves) {
   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);
     const hits = curveIntersectLineSegment(c, segment);
 
 
     if (hits.length > 0) {
     if (hits.length > 0) {
@@ -292,7 +373,7 @@ const intersectLinearOrFreeDrawWithLineSegment = (
 const intersectRectanguloidWithLineSegment = (
 const intersectRectanguloidWithLineSegment = (
   element: ExcalidrawRectanguloidElement,
   element: ExcalidrawRectanguloidElement,
   elementsMap: ElementsMap,
   elementsMap: ElementsMap,
-  l: LineSegment<GlobalPoint>,
+  segment: LineSegment<GlobalPoint>,
   offset: number = 0,
   offset: number = 0,
   onlyFirst = false,
   onlyFirst = false,
 ): GlobalPoint[] => {
 ): GlobalPoint[] => {
@@ -300,48 +381,43 @@ const intersectRectanguloidWithLineSegment = (
   // To emulate a rotated rectangle we rotate the point in the inverse angle
   // To emulate a rotated rectangle we rotate the point in the inverse angle
   // instead. It's all the same distance-wise.
   // instead. It's all the same distance-wise.
   const rotatedA = pointRotateRads<GlobalPoint>(
   const rotatedA = pointRotateRads<GlobalPoint>(
-    l[0],
+    segment[0],
     center,
     center,
     -element.angle as Radians,
     -element.angle as Radians,
   );
   );
   const rotatedB = pointRotateRads<GlobalPoint>(
   const rotatedB = pointRotateRads<GlobalPoint>(
-    l[1],
+    segment[1],
     center,
     center,
     -element.angle as Radians,
     -element.angle as Radians,
   );
   );
+  const rotatedIntersector = lineSegment(rotatedA, rotatedB);
 
 
   // Get the element's building components we can test against
   // Get the element's building components we can test against
   const [sides, corners] = deconstructRectanguloidElement(element, offset);
   const [sides, corners] = deconstructRectanguloidElement(element, offset);
 
 
   const intersections: GlobalPoint[] = [];
   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;
   return intersections;
 };
 };
@@ -366,38 +442,32 @@ const intersectDiamondWithLineSegment = (
   // points. It's all the same distance-wise.
   // points. It's all the same distance-wise.
   const rotatedA = pointRotateRads(l[0], center, -element.angle as Radians);
   const rotatedA = pointRotateRads(l[0], center, -element.angle as Radians);
   const rotatedB = pointRotateRads(l[1], 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 [sides, corners] = deconstructDiamondElement(element, offset);
-
   const intersections: GlobalPoint[] = [];
   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;
   return intersections;
 };
 };

+ 11 - 5
packages/element/src/utils.ts

@@ -90,6 +90,12 @@ const setElementShapesCacheEntry = <T extends ExcalidrawElement>(
   shapes.set(offset, shape);
   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(
 export function deconstructLinearOrFreeDrawElement(
   element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement,
   element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement,
 ): [LineSegment<GlobalPoint>[], Curve<GlobalPoint>[]] {
 ): [LineSegment<GlobalPoint>[], Curve<GlobalPoint>[]] {
@@ -171,11 +177,11 @@ export function deconstructLinearOrFreeDrawElement(
 
 
 /**
 /**
  * Get the building components of a rectanguloid element in the form of
  * 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 element Target rectanguloid element
  * @param offset Optional offset to expand the rectanguloid shape
  * @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(
 export function deconstructRectanguloidElement(
   element: ExcalidrawRectanguloidElement,
   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 element The element to deconstruct
  * @param offset An optional offset
  * @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(
 export function deconstructDiamondElement(
   element: ExcalidrawDiamondElement,
   element: ExcalidrawDiamondElement,

+ 1 - 1
packages/excalidraw/lasso/utils.ts

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

+ 0 - 25
packages/math/src/curve.ts

@@ -1,9 +1,6 @@
-import { type Bounds } from "@excalidraw/element";
-
 import { isPoint, pointDistance, pointFrom, pointFromVector } from "./point";
 import { isPoint, pointDistance, pointFrom, pointFromVector } from "./point";
 import { vector, vectorNormal, vectorNormalize, vectorScale } from "./vector";
 import { vector, vectorNormal, vectorNormalize, vectorScale } from "./vector";
 import { LegendreGaussN24CValues, LegendreGaussN24TValues } from "./constants";
 import { LegendreGaussN24CValues, LegendreGaussN24TValues } from "./constants";
-import { doBoundsIntersect } from "./utils";
 
 
 import type { Curve, GlobalPoint, LineSegment, LocalPoint } from "./types";
 import type { Curve, GlobalPoint, LineSegment, LocalPoint } from "./types";
 
 
@@ -105,19 +102,6 @@ export const bezierEquation = <Point extends GlobalPoint | LocalPoint>(
 export function curveIntersectLineSegment<
 export function curveIntersectLineSegment<
   Point extends GlobalPoint | LocalPoint,
   Point extends GlobalPoint | LocalPoint,
 >(c: Curve<Point>, l: LineSegment<Point>): Point[] {
 >(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) =>
   const line = (s: number) =>
     pointFrom<Point>(
     pointFrom<Point>(
       l[0][0] + s * (l[1][0] - l[0][0]),
       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(
 export function curveCatmullRomQuadraticApproxPoints(
   points: GlobalPoint[],
   points: GlobalPoint[],
   tension = 0.5,
   tension = 0.5,

+ 15 - 0
packages/math/src/rectangle.ts

@@ -10,6 +10,12 @@ export function rectangle<P extends GlobalPoint | LocalPoint>(
   return [topLeft, bottomRight] as Rectangle<P>;
   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<
 export function rectangleIntersectLineSegment<
   Point extends LocalPoint | GlobalPoint,
   Point extends LocalPoint | GlobalPoint,
 >(r: Rectangle<Point>, l: LineSegment<Point>): Point[] {
 >(r: Rectangle<Point>, l: LineSegment<Point>): Point[] {
@@ -22,3 +28,12 @@ export function rectangleIntersectLineSegment<
     .map((s) => lineSegmentIntersectionPoints(l, s))
     .map((s) => lineSegmentIntersectionPoints(l, s))
     .filter((i): i is Point => !!i);
     .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;
+}

+ 0 - 16
packages/math/src/utils.ts

@@ -1,5 +1,3 @@
-import { type Bounds } from "@excalidraw/element";
-
 export const PRECISION = 10e-5;
 export const PRECISION = 10e-5;
 
 
 export const clamp = (value: number, min: number, max: number) => {
 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) =>
 export const isCloseTo = (a: number, b: number, precision = PRECISION) =>
   Math.abs(a - b) < 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;
-};