瀏覽代碼

fix: Rounded diamond edge elbow arrow U route (#9349)

Márk Tolmács 4 月之前
父節點
當前提交
e48b63a0ae
共有 1 個文件被更改,包括 164 次插入87 次删除
  1. 164 87
      packages/element/src/heading.ts

+ 164 - 87
packages/element/src/heading.ts

@@ -1,11 +1,15 @@
+import { invariant, isDevEnv, isTestEnv } from "@excalidraw/common";
+
 import {
-  normalizeRadians,
   pointFrom,
+  pointFromVector,
   pointRotateRads,
   pointScaleFromOrigin,
-  radiansToDegrees,
+  pointsEqual,
   triangleIncludesPoint,
+  vectorCross,
   vectorFromPoint,
+  vectorScale,
 } from "@excalidraw/math";
 
 import type {
@@ -13,7 +17,6 @@ import type {
   GlobalPoint,
   Triangle,
   Vector,
-  Radians,
 } from "@excalidraw/math";
 
 import { getCenterForBounds, type Bounds } from "./bounds";
@@ -26,24 +29,6 @@ export const HEADING_LEFT = [-1, 0] as Heading;
 export const HEADING_UP = [0, -1] as Heading;
 export type Heading = [1, 0] | [0, 1] | [-1, 0] | [0, -1];
 
-export const headingForDiamond = <Point extends GlobalPoint | LocalPoint>(
-  a: Point,
-  b: Point,
-) => {
-  const angle = radiansToDegrees(
-    normalizeRadians(Math.atan2(b[1] - a[1], b[0] - a[0]) as Radians),
-  );
-
-  if (angle >= 315 || angle < 45) {
-    return HEADING_UP;
-  } else if (angle >= 45 && angle < 135) {
-    return HEADING_RIGHT;
-  } else if (angle >= 135 && angle < 225) {
-    return HEADING_DOWN;
-  }
-  return HEADING_LEFT;
-};
-
 export const vectorToHeading = (vec: Vector): Heading => {
   const [x, y] = vec;
   const absX = Math.abs(x);
@@ -76,87 +61,179 @@ export const headingIsHorizontal = (a: Heading) =>
 
 export const headingIsVertical = (a: Heading) => !headingIsHorizontal(a);
 
-// Gets the heading for the point by creating a bounding box around the rotated
-// close fitting bounding box, then creating 4 search cones around the center of
-// the external bbox.
-export const headingForPointFromElement = <Point extends GlobalPoint>(
+const headingForPointFromDiamondElement = (
   element: Readonly<ExcalidrawBindableElement>,
   aabb: Readonly<Bounds>,
-  p: Readonly<Point>,
+  point: Readonly<GlobalPoint>,
 ): Heading => {
-  const SEARCH_CONE_MULTIPLIER = 2;
-
   const midPoint = getCenterForBounds(aabb);
 
-  if (element.type === "diamond") {
-    if (p[0] < element.x) {
-      return HEADING_LEFT;
-    } else if (p[1] < element.y) {
-      return HEADING_UP;
-    } else if (p[0] > element.x + element.width) {
-      return HEADING_RIGHT;
-    } else if (p[1] > element.y + element.height) {
-      return HEADING_DOWN;
-    }
-
-    const top = pointRotateRads(
-      pointScaleFromOrigin(
-        pointFrom(element.x + element.width / 2, element.y),
+  if (isDevEnv() || isTestEnv()) {
+    invariant(
+      element.width > 0 && element.height > 0,
+      "Diamond element has no width or height",
+    );
+    invariant(
+      !pointsEqual(midPoint, point),
+      "The point is too close to the element mid point to determine heading",
+    );
+  }
+
+  const SHRINK = 0.95; // Rounded elements tolerance
+  const top = pointFromVector(
+    vectorScale(
+      vectorFromPoint(
+        pointRotateRads(
+          pointFrom<GlobalPoint>(element.x + element.width / 2, element.y),
+          midPoint,
+          element.angle,
+        ),
         midPoint,
-        SEARCH_CONE_MULTIPLIER,
       ),
-      midPoint,
-      element.angle,
-    );
-    const right = pointRotateRads(
-      pointScaleFromOrigin(
-        pointFrom(element.x + element.width, element.y + element.height / 2),
+      SHRINK,
+    ),
+    midPoint,
+  );
+  const right = pointFromVector(
+    vectorScale(
+      vectorFromPoint(
+        pointRotateRads(
+          pointFrom<GlobalPoint>(
+            element.x + element.width,
+            element.y + element.height / 2,
+          ),
+          midPoint,
+          element.angle,
+        ),
         midPoint,
-        SEARCH_CONE_MULTIPLIER,
       ),
-      midPoint,
-      element.angle,
-    );
-    const bottom = pointRotateRads(
-      pointScaleFromOrigin(
-        pointFrom(element.x + element.width / 2, element.y + element.height),
+      SHRINK,
+    ),
+    midPoint,
+  );
+  const bottom = pointFromVector(
+    vectorScale(
+      vectorFromPoint(
+        pointRotateRads(
+          pointFrom<GlobalPoint>(
+            element.x + element.width / 2,
+            element.y + element.height,
+          ),
+          midPoint,
+          element.angle,
+        ),
         midPoint,
-        SEARCH_CONE_MULTIPLIER,
       ),
-      midPoint,
-      element.angle,
-    );
-    const left = pointRotateRads(
-      pointScaleFromOrigin(
-        pointFrom(element.x, element.y + element.height / 2),
+      SHRINK,
+    ),
+    midPoint,
+  );
+  const left = pointFromVector(
+    vectorScale(
+      vectorFromPoint(
+        pointRotateRads(
+          pointFrom<GlobalPoint>(element.x, element.y + element.height / 2),
+          midPoint,
+          element.angle,
+        ),
         midPoint,
-        SEARCH_CONE_MULTIPLIER,
       ),
-      midPoint,
-      element.angle,
-    );
+      SHRINK,
+    ),
+    midPoint,
+  );
 
-    if (
-      triangleIncludesPoint<Point>([top, right, midPoint] as Triangle<Point>, p)
-    ) {
-      return headingForDiamond(top, right);
-    } else if (
-      triangleIncludesPoint<Point>(
-        [right, bottom, midPoint] as Triangle<Point>,
-        p,
-      )
-    ) {
-      return headingForDiamond(right, bottom);
-    } else if (
-      triangleIncludesPoint<Point>(
-        [bottom, left, midPoint] as Triangle<Point>,
-        p,
-      )
-    ) {
-      return headingForDiamond(bottom, left);
-    }
+  // Corners
+  if (
+    vectorCross(vectorFromPoint(point, top), vectorFromPoint(top, right)) <=
+      0 &&
+    vectorCross(vectorFromPoint(point, top), vectorFromPoint(top, left)) > 0
+  ) {
+    return headingForPoint(top, midPoint);
+  } else if (
+    vectorCross(
+      vectorFromPoint(point, right),
+      vectorFromPoint(right, bottom),
+    ) <= 0 &&
+    vectorCross(vectorFromPoint(point, right), vectorFromPoint(right, top)) > 0
+  ) {
+    return headingForPoint(right, midPoint);
+  } else if (
+    vectorCross(
+      vectorFromPoint(point, bottom),
+      vectorFromPoint(bottom, left),
+    ) <= 0 &&
+    vectorCross(
+      vectorFromPoint(point, bottom),
+      vectorFromPoint(bottom, right),
+    ) > 0
+  ) {
+    return headingForPoint(bottom, midPoint);
+  } else if (
+    vectorCross(vectorFromPoint(point, left), vectorFromPoint(left, top)) <=
+      0 &&
+    vectorCross(vectorFromPoint(point, left), vectorFromPoint(left, bottom)) > 0
+  ) {
+    return headingForPoint(left, midPoint);
+  }
+
+  // Sides
+  if (
+    vectorCross(
+      vectorFromPoint(point, midPoint),
+      vectorFromPoint(top, midPoint),
+    ) <= 0 &&
+    vectorCross(
+      vectorFromPoint(point, midPoint),
+      vectorFromPoint(right, midPoint),
+    ) > 0
+  ) {
+    const p = element.width > element.height ? top : right;
+    return headingForPoint(p, midPoint);
+  } else if (
+    vectorCross(
+      vectorFromPoint(point, midPoint),
+      vectorFromPoint(right, midPoint),
+    ) <= 0 &&
+    vectorCross(
+      vectorFromPoint(point, midPoint),
+      vectorFromPoint(bottom, midPoint),
+    ) > 0
+  ) {
+    const p = element.width > element.height ? bottom : right;
+    return headingForPoint(p, midPoint);
+  } else if (
+    vectorCross(
+      vectorFromPoint(point, midPoint),
+      vectorFromPoint(bottom, midPoint),
+    ) <= 0 &&
+    vectorCross(
+      vectorFromPoint(point, midPoint),
+      vectorFromPoint(left, midPoint),
+    ) > 0
+  ) {
+    const p = element.width > element.height ? bottom : left;
+    return headingForPoint(p, midPoint);
+  }
+
+  const p = element.width > element.height ? top : left;
+  return headingForPoint(p, midPoint);
+};
 
-    return headingForDiamond(left, top);
+// Gets the heading for the point by creating a bounding box around the rotated
+// close fitting bounding box, then creating 4 search cones around the center of
+// the external bbox.
+export const headingForPointFromElement = <Point extends GlobalPoint>(
+  element: Readonly<ExcalidrawBindableElement>,
+  aabb: Readonly<Bounds>,
+  p: Readonly<Point>,
+): Heading => {
+  const SEARCH_CONE_MULTIPLIER = 2;
+
+  const midPoint = getCenterForBounds(aabb);
+
+  if (element.type === "diamond") {
+    return headingForPointFromDiamondElement(element, aabb, p);
   }
 
   const topLeft = pointScaleFromOrigin(