Browse Source

fix: Reimplement rectangle intersection (#8367)

Márk Tolmács 1 year ago
parent
commit
8420e1aa13

+ 11 - 44
packages/excalidraw/element/binding.ts

@@ -72,6 +72,7 @@ import {
   vectorToHeading,
   vectorToHeading,
   type Heading,
   type Heading,
 } from "./heading";
 } from "./heading";
+import { segmentIntersectRectangleElement } from "../../utils/geometry/geometry";
 
 
 export type SuggestedBinding =
 export type SuggestedBinding =
   | NonDeleted<ExcalidrawBindableElement>
   | NonDeleted<ExcalidrawBindableElement>
@@ -753,6 +754,7 @@ export const bindPointToSnapToElementOutline = (
 
 
   if (bindableElement && aabb) {
   if (bindableElement && aabb) {
     // TODO: Dirty hacks until tangents are properly calculated
     // TODO: Dirty hacks until tangents are properly calculated
+    const heading = headingForPointFromElement(bindableElement, aabb, point);
     const intersections = [
     const intersections = [
       ...intersectElementWithLine(
       ...intersectElementWithLine(
         bindableElement,
         bindableElement,
@@ -760,61 +762,22 @@ export const bindPointToSnapToElementOutline = (
         [point[0], point[1] + 2 * bindableElement.height],
         [point[0], point[1] + 2 * bindableElement.height],
         FIXED_BINDING_DISTANCE,
         FIXED_BINDING_DISTANCE,
         elementsMap,
         elementsMap,
-      ).map((i) => {
-        if (!isRectangularElement(bindableElement)) {
-          return i;
-        }
-
-        const d = distanceToBindableElement(
-          {
-            ...bindableElement,
-            x: Math.round(bindableElement.x),
-            y: Math.round(bindableElement.y),
-            width: Math.round(bindableElement.width),
-            height: Math.round(bindableElement.height),
-          },
-          [Math.round(i[0]), Math.round(i[1])],
-          new Map(),
-        );
-
-        return d >= bindableElement.height / 2 || d < FIXED_BINDING_DISTANCE
-          ? ([point[0], -1 * i[1]] as Point)
-          : ([point[0], i[1]] as Point);
-      }),
+      ),
       ...intersectElementWithLine(
       ...intersectElementWithLine(
         bindableElement,
         bindableElement,
         [point[0] - 2 * bindableElement.width, point[1]],
         [point[0] - 2 * bindableElement.width, point[1]],
         [point[0] + 2 * bindableElement.width, point[1]],
         [point[0] + 2 * bindableElement.width, point[1]],
         FIXED_BINDING_DISTANCE,
         FIXED_BINDING_DISTANCE,
         elementsMap,
         elementsMap,
-      ).map((i) => {
-        if (!isRectangularElement(bindableElement)) {
-          return i;
-        }
-
-        const d = distanceToBindableElement(
-          {
-            ...bindableElement,
-            x: Math.round(bindableElement.x),
-            y: Math.round(bindableElement.y),
-            width: Math.round(bindableElement.width),
-            height: Math.round(bindableElement.height),
-          },
-          [Math.round(i[0]), Math.round(i[1])],
-          new Map(),
-        );
-
-        return d >= bindableElement.width / 2 || d < FIXED_BINDING_DISTANCE
-          ? ([-1 * i[0], point[1]] as Point)
-          : ([i[0], point[1]] as Point);
-      }),
+      ),
     ];
     ];
 
 
-    const heading = headingForPointFromElement(bindableElement, aabb, point);
     const isVertical =
     const isVertical =
       compareHeading(heading, HEADING_LEFT) ||
       compareHeading(heading, HEADING_LEFT) ||
       compareHeading(heading, HEADING_RIGHT);
       compareHeading(heading, HEADING_RIGHT);
-    const dist = distanceToBindableElement(bindableElement, point, elementsMap);
+    const dist = Math.abs(
+      distanceToBindableElement(bindableElement, point, elementsMap),
+    );
     const isInner = isVertical
     const isInner = isVertical
       ? dist < bindableElement.width * -0.1
       ? dist < bindableElement.width * -0.1
       : dist < bindableElement.height * -0.1;
       : dist < bindableElement.height * -0.1;
@@ -1641,6 +1604,10 @@ const intersectElementWithLine = (
   gap: number = 0,
   gap: number = 0,
   elementsMap: ElementsMap,
   elementsMap: ElementsMap,
 ): Point[] => {
 ): Point[] => {
+  if (isRectangularElement(element)) {
+    return segmentIntersectRectangleElement(element, [a, b], gap);
+  }
+
   const relateToCenter = relativizationToElementCenter(element, elementsMap);
   const relateToCenter = relativizationToElementCenter(element, elementsMap);
   const aRel = GATransform.apply(relateToCenter, GAPoint.from(a));
   const aRel = GATransform.apply(relateToCenter, GAPoint.from(a));
   const bRel = GATransform.apply(relateToCenter, GAPoint.from(b));
   const bRel = GATransform.apply(relateToCenter, GAPoint.from(b));

+ 27 - 27
packages/excalidraw/tests/__snapshots__/history.test.tsx.snap

@@ -191,7 +191,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
   "fillStyle": "solid",
   "fillStyle": "solid",
   "frameId": null,
   "frameId": null,
   "groupIds": [],
   "groupIds": [],
-  "height": "99.19726",
+  "height": 99,
   "id": "id166",
   "id": "id166",
   "index": "a2",
   "index": "a2",
   "isDeleted": false,
   "isDeleted": false,
@@ -205,8 +205,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
       0,
       0,
     ],
     ],
     [
     [
-      "98.40368",
-      "99.19726",
+      "98.20800",
+      99,
     ],
     ],
   ],
   ],
   "roughness": 1,
   "roughness": 1,
@@ -221,7 +221,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
   "type": "arrow",
   "type": "arrow",
   "updated": 1,
   "updated": 1,
   "version": 40,
   "version": 40,
-  "width": "98.40368",
+  "width": "98.20800",
   "x": 1,
   "x": 1,
   "y": 0,
   "y": 0,
 }
 }
@@ -387,15 +387,15 @@ History {
                 "focus": 0,
                 "focus": 0,
                 "gap": 1,
                 "gap": 1,
               },
               },
-              "height": "99.19726",
+              "height": 99,
               "points": [
               "points": [
                 [
                 [
                   0,
                   0,
                   0,
                   0,
                 ],
                 ],
                 [
                 [
-                  "98.40368",
-                  "99.19726",
+                  "98.20800",
+                  99,
                 ],
                 ],
               ],
               ],
               "startBinding": null,
               "startBinding": null,
@@ -813,7 +813,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
   "updated": 1,
   "updated": 1,
   "version": 30,
   "version": 30,
   "width": 0,
   "width": 0,
-  "x": 251,
+  "x": 200,
   "y": 0,
   "y": 0,
 }
 }
 `;
 `;
@@ -1242,7 +1242,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
       0,
       0,
     ],
     ],
     [
     [
-      98,
+      "98.00000",
       "-2.61991",
       "-2.61991",
     ],
     ],
   ],
   ],
@@ -1266,8 +1266,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
   "type": "arrow",
   "type": "arrow",
   "updated": 1,
   "updated": 1,
   "version": 11,
   "version": 11,
-  "width": 98,
-  "x": 1,
+  "width": "98.00000",
+  "x": "1.00000",
   "y": "3.98333",
   "y": "3.98333",
 }
 }
 `;
 `;
@@ -1607,7 +1607,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
       0,
       0,
     ],
     ],
     [
     [
-      98,
+      "98.00000",
       "-2.61991",
       "-2.61991",
     ],
     ],
   ],
   ],
@@ -1631,8 +1631,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
   "type": "arrow",
   "type": "arrow",
   "updated": 1,
   "updated": 1,
   "version": 11,
   "version": 11,
-  "width": 98,
-  "x": 1,
+  "width": "98.00000",
+  "x": "1.00000",
   "y": "3.98333",
   "y": "3.98333",
 }
 }
 `;
 `;
@@ -1764,7 +1764,7 @@ History {
                   0,
                   0,
                 ],
                 ],
                 [
                 [
-                  98,
+                  "98.00000",
                   "-22.36242",
                   "-22.36242",
                 ],
                 ],
               ],
               ],
@@ -1786,9 +1786,9 @@ History {
               "strokeStyle": "solid",
               "strokeStyle": "solid",
               "strokeWidth": 2,
               "strokeWidth": 2,
               "type": "arrow",
               "type": "arrow",
-              "width": 98,
+              "width": "98.00000",
               "x": 1,
               "x": 1,
-              "y": "34.00000",
+              "y": 34,
             },
             },
             "inserted": {
             "inserted": {
               "isDeleted": true,
               "isDeleted": true,
@@ -14847,7 +14847,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
       0,
       0,
     ],
     ],
     [
     [
-      98,
+      "98.00000",
       0,
       0,
     ],
     ],
   ],
   ],
@@ -14868,7 +14868,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
   "type": "arrow",
   "type": "arrow",
   "updated": 1,
   "updated": 1,
   "version": 10,
   "version": 10,
-  "width": 98,
+  "width": "98.00000",
   "x": 1,
   "x": 1,
   "y": 0,
   "y": 0,
 }
 }
@@ -15540,7 +15540,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
       0,
       0,
     ],
     ],
     [
     [
-      98,
+      "98.00000",
       0,
       0,
     ],
     ],
   ],
   ],
@@ -15561,7 +15561,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
   "type": "arrow",
   "type": "arrow",
   "updated": 1,
   "updated": 1,
   "version": 10,
   "version": 10,
-  "width": 98,
+  "width": "98.00000",
   "x": 1,
   "x": 1,
   "y": 0,
   "y": 0,
 }
 }
@@ -16157,7 +16157,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
       0,
       0,
     ],
     ],
     [
     [
-      98,
+      "98.00000",
       0,
       0,
     ],
     ],
   ],
   ],
@@ -16178,7 +16178,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
   "type": "arrow",
   "type": "arrow",
   "updated": 1,
   "updated": 1,
   "version": 10,
   "version": 10,
-  "width": 98,
+  "width": "98.00000",
   "x": 1,
   "x": 1,
   "y": 0,
   "y": 0,
 }
 }
@@ -16772,7 +16772,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
       0,
       0,
     ],
     ],
     [
     [
-      98,
+      "98.00000",
       0,
       0,
     ],
     ],
   ],
   ],
@@ -16793,7 +16793,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
   "type": "arrow",
   "type": "arrow",
   "updated": 1,
   "updated": 1,
   "version": 10,
   "version": 10,
-  "width": 98,
+  "width": "98.00000",
   "x": 1,
   "x": 1,
   "y": 0,
   "y": 0,
 }
 }
@@ -17484,7 +17484,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
       0,
       0,
     ],
     ],
     [
     [
-      98,
+      "98.00000",
       0,
       0,
     ],
     ],
   ],
   ],
@@ -17505,7 +17505,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
   "type": "arrow",
   "type": "arrow",
   "updated": 1,
   "updated": 1,
   "version": 11,
   "version": 11,
-  "width": 98,
+  "width": "98.00000",
   "x": 1,
   "x": 1,
   "y": 0,
   "y": 0,
 }
 }

+ 2 - 2
packages/excalidraw/tests/rotate.test.tsx

@@ -77,6 +77,6 @@ test("unselected bound arrows update when rotating their target elements", async
   expect(textArrow.x).toEqual(360);
   expect(textArrow.x).toEqual(360);
   expect(textArrow.y).toEqual(300);
   expect(textArrow.y).toEqual(300);
   expect(textArrow.points[0]).toEqual([0, 0]);
   expect(textArrow.points[0]).toEqual([0, 0]);
-  expect(textArrow.points[1][0]).toBeCloseTo(-94, 1);
-  expect(textArrow.points[1][1]).toBeCloseTo(-116.1, 1);
+  expect(textArrow.points[1][0]).toBeCloseTo(-94, 0);
+  expect(textArrow.points[1][1]).toBeCloseTo(-116.1, 0);
 });
 });

+ 91 - 1
packages/utils/geometry/geometry.ts

@@ -1,4 +1,13 @@
-import { distance2d } from "../../excalidraw/math";
+import type { ExcalidrawBindableElement } from "../../excalidraw/element/types";
+import {
+  addVectors,
+  distance2d,
+  rotatePoint,
+  scaleVector,
+  subtractVectors,
+} from "../../excalidraw/math";
+import type { LineSegment } from "../bbox";
+import { crossProduct } from "../bbox";
 import type {
 import type {
   Point,
   Point,
   Line,
   Line,
@@ -968,3 +977,84 @@ export const pointInEllipse = (point: Point, ellipse: Ellipse) => {
     1
     1
   );
   );
 };
 };
+
+/**
+ * Calculates the point two line segments with a definite start and end point
+ * intersect at.
+ */
+export const segmentsIntersectAt = (
+  a: Readonly<LineSegment>,
+  b: Readonly<LineSegment>,
+): Point | null => {
+  const r = subtractVectors(a[1], a[0]);
+  const s = subtractVectors(b[1], b[0]);
+  const denominator = crossProduct(r, s);
+
+  if (denominator === 0) {
+    return null;
+  }
+
+  const i = subtractVectors(b[0], a[0]);
+  const u = crossProduct(i, r) / denominator;
+  const t = crossProduct(i, s) / denominator;
+
+  if (u === 0) {
+    return null;
+  }
+
+  const p = addVectors(a[0], scaleVector(r, t));
+
+  if (t >= 0 && t < 1 && u >= 0 && u < 1) {
+    return p;
+  }
+
+  return null;
+};
+
+/**
+ * Determine intersection of a rectangular shaped element and a
+ * line segment.
+ *
+ * @param element The rectangular element to test against
+ * @param segment The segment intersecting the element
+ * @param gap Optional value to inflate the shape before testing
+ * @returns An array of intersections
+ */
+// TODO: Replace with final rounded rectangle code
+export const segmentIntersectRectangleElement = (
+  element: ExcalidrawBindableElement,
+  segment: LineSegment,
+  gap: number = 0,
+): Point[] => {
+  const bounds = [
+    element.x - gap,
+    element.y - gap,
+    element.x + element.width + gap,
+    element.y + element.height + gap,
+  ];
+  const center = [
+    (bounds[0] + bounds[2]) / 2,
+    (bounds[1] + bounds[3]) / 2,
+  ] as Point;
+
+  return [
+    [
+      rotatePoint([bounds[0], bounds[1]], center, element.angle),
+      rotatePoint([bounds[2], bounds[1]], center, element.angle),
+    ] as LineSegment,
+    [
+      rotatePoint([bounds[2], bounds[1]], center, element.angle),
+      rotatePoint([bounds[2], bounds[3]], center, element.angle),
+    ] as LineSegment,
+    [
+      rotatePoint([bounds[2], bounds[3]], center, element.angle),
+      rotatePoint([bounds[0], bounds[3]], center, element.angle),
+    ] as LineSegment,
+    [
+      rotatePoint([bounds[0], bounds[3]], center, element.angle),
+      rotatePoint([bounds[0], bounds[1]], center, element.angle),
+    ] as LineSegment,
+  ]
+    .map((s) => segmentsIntersectAt(segment, s))
+    .filter((i): i is Point => !!i);
+};