Parcourir la source

Revert to master

Mark Tolmacs il y a 4 mois
Parent
commit
b33cc74183

+ 0 - 2
excalidraw-app/components/DebugCanvas.tsx

@@ -13,8 +13,6 @@ import { useCallback, useImperativeHandle, useRef } from "react";
 
 import {
   isLineSegment,
-  isCurve,
-  type Curve,
   type GlobalPoint,
   type LineSegment,
 } from "@excalidraw/math";

+ 74 - 59
packages/element/src/binding.ts

@@ -462,10 +462,23 @@ export const maybeBindLinearElement = (
   }
 };
 
-const normalizePointBinding = (binding: { focus: number; gap: number }) => {
+const normalizePointBinding = (
+  binding: { focus: number; gap: number },
+  hoveredElement: ExcalidrawBindableElement,
+) => {
+  let gap = binding.gap;
+  const maxGap = maxBindingGap(
+    hoveredElement,
+    hoveredElement.width,
+    hoveredElement.height,
+  );
+
+  if (gap > maxGap) {
+    gap = BINDING_HIGHLIGHT_THICKNESS + BINDING_HIGHLIGHT_OFFSET;
+  }
   return {
     ...binding,
-    gap: FIXED_BINDING_DISTANCE,
+    gap,
   };
 };
 
@@ -715,7 +728,7 @@ const calculateFocusAndGap = (
 
   return {
     focus: determineFocusDistance(hoveredElement, adjacentPoint, edgePoint),
-    gap: FIXED_BINDING_DISTANCE,
+    gap: Math.max(1, distanceToBindableElement(hoveredElement, edgePoint)),
   };
 };
 
@@ -733,7 +746,7 @@ export const updateBoundElements = (
     changedElements?: Map<string, OrderedExcalidrawElement>;
   },
 ) => {
-  const { simultaneouslyUpdated } = options ?? {};
+  const { newSize, simultaneouslyUpdated } = options ?? {};
   const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds(
     simultaneouslyUpdated,
   );
@@ -766,13 +779,23 @@ export const updateBoundElements = (
       startBounds = getElementBounds(startBindingElement, elementsMap);
       endBounds = getElementBounds(endBindingElement, elementsMap);
     }
+
+    const bindings = {
+      startBinding: maybeCalculateNewGapWhenScaling(
+        changedElement,
+        element.startBinding,
+        newSize,
+      ),
+      endBinding: maybeCalculateNewGapWhenScaling(
+        changedElement,
+        element.endBinding,
+        newSize,
+      ),
+    };
+
     // `linearElement` is being moved/scaled already, just update the binding
     if (simultaneouslyUpdatedElementIds.has(element.id)) {
-      mutateElement(
-        element,
-        { startBinding: element.startBinding, endBinding: element.endBinding },
-        true,
-      );
+      mutateElement(element, bindings, true);
       return;
     }
 
@@ -794,9 +817,7 @@ export const updateBoundElements = (
           const point = updateBoundPoint(
             element,
             bindingProp,
-            bindingProp === "startBinding"
-              ? element.startBinding
-              : element.endBinding,
+            bindings[bindingProp],
             bindableElement,
             elementsMap,
           );
@@ -916,7 +937,6 @@ export const bindPointToSnapToElementOutline = (
   arrow: ExcalidrawElbowArrowElement,
   bindableElement: ExcalidrawBindableElement,
   startOrEnd: "start" | "end",
-  elementsMap: ElementsMap,
 ): GlobalPoint => {
   if (isDevEnv() || isTestEnv()) {
     invariant(arrow.points.length > 1, "Arrow should have at least 2 points");
@@ -924,12 +944,10 @@ export const bindPointToSnapToElementOutline = (
 
   const aabb = aabbForElement(bindableElement);
   const localP =
-    linearElement.points[
-      startOrEnd === "start" ? 0 : linearElement.points.length - 1
-    ];
+    arrow.points[startOrEnd === "start" ? 0 : arrow.points.length - 1];
   const globalP = pointFrom<GlobalPoint>(
-    linearElement.x + localP[0],
-    linearElement.y + localP[1],
+    arrow.x + localP[0],
+    arrow.y + localP[1],
   );
   const edgePoint = isRectanguloidElement(bindableElement)
     ? avoidRectangularCorner(bindableElement, globalP)
@@ -966,11 +984,7 @@ export const bindPointToSnapToElementOutline = (
           ),
           otherPoint,
         ),
-        adjacentPoint,
       ),
-    ).sort(
-      (g, h) =>
-        pointDistanceSq(g, adjacentPoint) - pointDistanceSq(h, adjacentPoint),
     )[0];
   } else {
     intersection = intersectElementWithLineSegment(
@@ -1018,20 +1032,6 @@ export const bindPointToSnapToElementOutline = (
     );
   }
 
-  const currentDistance = pointDistance(edgePoint, center);
-  const fullDistance = Math.max(
-    pointDistance(intersection ?? edgePoint, center),
-    1e-5, // Avoid division by zero
-  );
-
-  if (!isInside) {
-    return intersection;
-  }
-
-  if (elbowed) {
-    return headingToMidBindPoint(edgePoint, bindableElement, aabb);
-  }
-
   return edgePoint;
 };
 
@@ -1039,7 +1039,10 @@ export const avoidRectangularCorner = (
   element: ExcalidrawBindableElement,
   p: GlobalPoint,
 ): GlobalPoint => {
-  const center = elementCenterPoint(element);
+  const center = pointFrom<GlobalPoint>(
+    element.x + element.width / 2,
+    element.y + element.height / 2,
+  );
   const nonRotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
 
   if (nonRotatedPoint[0] < element.x && nonRotatedPoint[1] < element.y) {
@@ -1309,24 +1312,6 @@ const updateBoundPoint = (
       ),
     ];
 
-    // debugClear();
-    // intersections.forEach((intersection) => {
-    //   debugDrawPoint(intersection, { permanent: true, color: "red" });
-    // });
-    // debugDrawLine(
-    //   lineSegment<GlobalPoint>(
-    //     adjacentPoint,
-    //     pointFromVector(
-    //       vectorScale(
-    //         vectorNormalize(vectorFromPoint(focusPointAbsolute, adjacentPoint)),
-    //         interceptorLength,
-    //       ),
-    //       adjacentPoint,
-    //     ),
-    //   ),
-    //   { permanent: true, color: "green" },
-    // );
-
     if (intersections.length > 1) {
       // The adjacent point is outside the shape (+ gap)
       newEdgePoint = intersections[0];
@@ -1362,7 +1347,6 @@ export const calculateFixedPointForElbowArrowBinding = (
     linearElement,
     hoveredElement,
     startOrEnd,
-    elementsMap,
   );
   const globalMidPoint = pointFrom(
     bounds[0] + (bounds[2] - bounds[0]) / 2,
@@ -1384,6 +1368,28 @@ export const calculateFixedPointForElbowArrowBinding = (
   };
 };
 
+const maybeCalculateNewGapWhenScaling = (
+  changedElement: ExcalidrawBindableElement,
+  currentBinding: PointBinding | null | undefined,
+  newSize: { width: number; height: number } | undefined,
+): PointBinding | null | undefined => {
+  if (currentBinding == null || newSize == null) {
+    return currentBinding;
+  }
+  const { width: newWidth, height: newHeight } = newSize;
+  const { width, height } = changedElement;
+  const newGap = Math.max(
+    1,
+    Math.min(
+      maxBindingGap(changedElement, newWidth, newHeight),
+      currentBinding.gap *
+        (newWidth < newHeight ? newWidth / width : newHeight / height),
+    ),
+  );
+
+  return { ...currentBinding, gap: newGap };
+};
+
 const getElligibleElementForBindingElement = (
   linearElement: NonDeleted<ExcalidrawLinearElement>,
   startOrEnd: "start" | "end",
@@ -1574,7 +1580,10 @@ const determineFocusDistance = (
   // Another point on the line, in absolute coordinates (closer to element)
   b: GlobalPoint,
 ): number => {
-  const center = elementCenterPoint(element);
+  const center = pointFrom<GlobalPoint>(
+    element.x + element.width / 2,
+    element.y + element.height / 2,
+  );
 
   if (pointsEqual(a, b)) {
     return 0;
@@ -1704,7 +1713,10 @@ const determineFocusPoint = (
   focus: number,
   adjacentPoint: GlobalPoint,
 ): GlobalPoint => {
-  const center = elementCenterPoint(element);
+  const center = pointFrom<GlobalPoint>(
+    element.x + element.width / 2,
+    element.y + element.height / 2,
+  );
 
   if (focus === 0) {
     return center;
@@ -2135,7 +2147,10 @@ export const getGlobalFixedPointForBindableElement = (
       element.x + element.width * fixedX,
       element.y + element.height * fixedY,
     ),
-    elementCenterPoint(element),
+    pointFrom<GlobalPoint>(
+      element.x + element.width / 2,
+      element.y + element.height / 2,
+    ),
     element.angle,
   );
 };

+ 52 - 46
packages/element/src/collision.ts

@@ -41,7 +41,6 @@ import {
 import {
   deconstructDiamondElement,
   deconstructRectanguloidElement,
-  elementCenterPoint,
 } from "./utils";
 
 import type {
@@ -192,7 +191,10 @@ const intersectRectanguloidWithLineSegment = (
   l: LineSegment<GlobalPoint>,
   offset: number = 0,
 ): GlobalPoint[] => {
-  const center = elementCenterPoint(element);
+  const center = pointFrom<GlobalPoint>(
+    element.x + element.width / 2,
+    element.y + element.height / 2,
+  );
   // 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>(
@@ -210,32 +212,31 @@ const intersectRectanguloidWithLineSegment = (
   const [sides, corners] = deconstructRectanguloidElement(element, offset);
 
   return (
-    // Test intersection against the sides, keep only the valid
-    // intersection points and rotate them back to scene space
-    sides
-      .map((s) =>
-        lineSegmentIntersectionPoints(
-          lineSegment<GlobalPoint>(rotatedA, rotatedB),
-          s,
-        ),
-      )
-      .filter((x) => x != null)
-      .map((j) => pointRotateRads<GlobalPoint>(j!, center, element.angle))
+    [
+      // Test intersection against the sides, keep only the valid
+      // intersection points and rotate them back to scene space
+      ...sides
+        .map((s) =>
+          lineSegmentIntersectionPoints(
+            lineSegment<GlobalPoint>(rotatedA, rotatedB),
+            s,
+          ),
+        )
+        .filter((x) => x != null)
+        .map((j) => pointRotateRads<GlobalPoint>(j!, center, element.angle)),
       // Test intersection against the corners which are cubic bezier curves,
       // keep only the valid intersection points and rotate them back to scene
       // space
-      .concat(
-        corners
-          .flatMap((t) =>
-            curveIntersectLineSegment(t, lineSegment(rotatedA, rotatedB)),
-          )
-          .filter((i) => i != null)
-          .map((j) => pointRotateRads(j, center, element.angle)),
-      )
+      ...corners
+        .flatMap((t) =>
+          curveIntersectLineSegment(t, lineSegment(rotatedA, rotatedB)),
+        )
+        .filter((i) => i != null)
+        .map((j) => pointRotateRads(j, center, element.angle)),
+    ]
       // Remove duplicates
       .filter(
-        (p, idx, points) =>
-          points.findIndex((d) => pointsEqual(p, d, 1e-3)) === idx,
+        (p, idx, points) => points.findIndex((d) => pointsEqual(p, d)) === idx,
       )
   );
 };
@@ -252,7 +253,10 @@ const intersectDiamondWithLineSegment = (
   l: LineSegment<GlobalPoint>,
   offset: number = 0,
 ): GlobalPoint[] => {
-  const center = elementCenterPoint(element);
+  const center = pointFrom<GlobalPoint>(
+    element.x + element.width / 2,
+    element.y + element.height / 2,
+  );
 
   // Rotate the point to the inverse direction to simulate the rotated diamond
   // points. It's all the same distance-wise.
@@ -262,29 +266,28 @@ const intersectDiamondWithLineSegment = (
   const [sides, curves] = deconstructDiamondElement(element, offset);
 
   return (
-    sides
-      .map((s) =>
-        lineSegmentIntersectionPoints(
-          lineSegment<GlobalPoint>(rotatedA, rotatedB),
-          s,
-        ),
-      )
-      .filter((p): p is GlobalPoint => p != null)
-      // Rotate back intersection points
-      .map((p) => pointRotateRads<GlobalPoint>(p!, center, element.angle))
-      .concat(
-        curves
-          .flatMap((p) =>
-            curveIntersectLineSegment(p, lineSegment(rotatedA, rotatedB)),
-          )
-          .filter((p) => p != null)
-          // Rotate back intersection points
-          .map((p) => pointRotateRads(p, center, element.angle)),
-      )
+    [
+      ...sides
+        .map((s) =>
+          lineSegmentIntersectionPoints(
+            lineSegment<GlobalPoint>(rotatedA, rotatedB),
+            s,
+          ),
+        )
+        .filter((p): p is GlobalPoint => p != null)
+        // Rotate back intersection points
+        .map((p) => pointRotateRads<GlobalPoint>(p!, center, element.angle)),
+      ...curves
+        .flatMap((p) =>
+          curveIntersectLineSegment(p, lineSegment(rotatedA, rotatedB)),
+        )
+        .filter((p) => p != null)
+        // Rotate back intersection points
+        .map((p) => pointRotateRads(p, center, element.angle)),
+    ]
       // Remove duplicates
       .filter(
-        (p, idx, points) =>
-          points.findIndex((d) => pointsEqual(p, d, 1e-3)) === idx,
+        (p, idx, points) => points.findIndex((d) => pointsEqual(p, d)) === idx,
       )
   );
 };
@@ -301,7 +304,10 @@ const intersectEllipseWithLineSegment = (
   l: LineSegment<GlobalPoint>,
   offset: number = 0,
 ): GlobalPoint[] => {
-  const center = elementCenterPoint(element);
+  const center = pointFrom<GlobalPoint>(
+    element.x + element.width / 2,
+    element.y + element.height / 2,
+  );
 
   const rotatedA = pointRotateRads(l[0], center, -element.angle as Radians);
   const rotatedB = pointRotateRads(l[1], center, -element.angle as Radians);

+ 1 - 1
packages/element/src/cropElement.ts

@@ -61,7 +61,7 @@ export const cropElement = (
 
   const rotatedPointer = pointRotateRads(
     pointFrom(pointerX, pointerY),
-    elementCenterPoint(element),
+    pointFrom(element.x + element.width / 2, element.y + element.height / 2),
     -element.angle as Radians,
   );
 

+ 13 - 3
packages/element/src/distance.ts

@@ -1,6 +1,7 @@
 import {
   curvePointDistance,
   distanceToLineSegment,
+  pointFrom,
   pointRotateRads,
 } from "@excalidraw/math";
 
@@ -52,7 +53,10 @@ const distanceToRectanguloidElement = (
   element: ExcalidrawRectanguloidElement,
   p: GlobalPoint,
 ) => {
-  const center = elementCenterPoint(element);
+  const center = pointFrom<GlobalPoint>(
+    element.x + element.width / 2,
+    element.y + element.height / 2,
+  );
   // To emulate a rotated rectangle we rotate the point in the inverse angle
   // instead. It's all the same distance-wise.
   const rotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
@@ -80,7 +84,10 @@ const distanceToDiamondElement = (
   element: ExcalidrawDiamondElement,
   p: GlobalPoint,
 ): number => {
-  const center = elementCenterPoint(element);
+  const center = pointFrom<GlobalPoint>(
+    element.x + element.width / 2,
+    element.y + element.height / 2,
+  );
 
   // Rotate the point to the inverse direction to simulate the rotated diamond
   // points. It's all the same distance-wise.
@@ -108,7 +115,10 @@ const distanceToEllipseElement = (
   element: ExcalidrawEllipseElement,
   p: GlobalPoint,
 ): number => {
-  const center = elementCenterPoint(element);
+  const center = pointFrom(
+    element.x + element.width / 2,
+    element.y + element.height / 2,
+  );
   return ellipseDistanceFromPoint(
     // Instead of rotating the ellipse, rotate the point to the inverse angle
     pointRotateRads(p, center, -element.angle as Radians),

+ 0 - 3
packages/element/src/elbowArrow.ts

@@ -1249,7 +1249,6 @@ const getElbowArrowData = (
       ...arrow,
       type: "arrow",
       elbowed: true,
-      type: "arrow",
       points: nextPoints,
     } as ExcalidrawElbowArrowElement,
     "start",
@@ -1263,7 +1262,6 @@ const getElbowArrowData = (
       ...arrow,
       type: "arrow",
       elbowed: true,
-      type: "arrow",
       points: nextPoints,
     } as ExcalidrawElbowArrowElement,
     "end",
@@ -2223,7 +2221,6 @@ const getGlobalPoint = (
         arrow,
         element,
         startOrEnd,
-        elementsMap,
       );
 
       return snapToMid(element, snapPoint);

+ 14 - 77
packages/element/src/linearElementEditor.ts

@@ -238,43 +238,6 @@ export class LinearElementEditor {
     });
   }
 
-  static getOutlineAvoidingPoint(
-    element: NonDeleted<ExcalidrawLinearElement>,
-    coords: GlobalPoint,
-    pointIndex: number,
-    app: AppClassProperties,
-    fallback?: GlobalPoint,
-  ): GlobalPoint {
-    const hoveredElement = getHoveredElementForBinding(
-      { x: coords[0], y: coords[1] },
-      app.scene.getNonDeletedElements(),
-      app.scene.getNonDeletedElementsMap(),
-      app.state.zoom,
-      true,
-      isElbowArrow(element),
-    );
-
-    if (hoveredElement) {
-      const newPoints = Array.from(element.points);
-      newPoints[pointIndex] = pointFrom<LocalPoint>(
-        coords[0] - element.x,
-        coords[1] - element.y,
-      );
-
-      return bindPointToSnapToElementOutline(
-        {
-          ...element,
-          points: newPoints,
-        },
-        hoveredElement,
-        pointIndex === 0 ? "start" : "end",
-        app.scene.getNonDeletedElementsMap(),
-      );
-    }
-
-    return fallback ?? coords;
-  }
-
   /**
    * @returns whether point was dragged
    */
@@ -294,16 +257,14 @@ export class LinearElementEditor {
       return null;
     }
     const { elementId } = linearElementEditor;
-    const elementsMap = app.scene.getNonDeletedElementsMap();
+    const elementsMap = scene.getNonDeletedElementsMap();
     const element = LinearElementEditor.getElement(elementId, elementsMap);
     if (!element) {
       return null;
     }
 
-    const elbowed = isElbowArrow(element);
-
     if (
-      elbowed &&
+      isElbowArrow(element) &&
       !linearElementEditor.pointerDownState.lastClickedIsEndPoint &&
       linearElementEditor.pointerDownState.lastClickedPoint !== 0
     ) {
@@ -320,7 +281,7 @@ export class LinearElementEditor {
             : undefined,
         ].filter((idx): idx is number => idx !== undefined)
       : linearElementEditor.selectedPointsIndices;
-    const lastClickedPoint = elbowed
+    const lastClickedPoint = isElbowArrow(element)
       ? linearElementEditor.pointerDownState.lastClickedPoint > 0
         ? element.points.length - 1
         : 0
@@ -372,43 +333,19 @@ export class LinearElementEditor {
         LinearElementEditor.movePoints(
           element,
           selectedPointsIndices.map((pointIndex) => {
-            let newPointPosition = pointFrom<LocalPoint>(
-              element.points[pointIndex][0] + deltaX,
-              element.points[pointIndex][1] + deltaY,
-            );
-
-            // Check if point dragging is happening
-            if (pointIndex === lastClickedPoint) {
-              let globalNewPointPosition = pointFrom<GlobalPoint>(
-                scenePointerX - linearElementEditor.pointerOffset.x,
-                scenePointerY - linearElementEditor.pointerOffset.y,
-              );
-
-              if (
-                pointIndex === 0 ||
-                pointIndex === element.points.length - 1
-              ) {
-                globalNewPointPosition =
-                  LinearElementEditor.getOutlineAvoidingPoint(
+            const newPointPosition: LocalPoint =
+              pointIndex === lastClickedPoint
+                ? LinearElementEditor.createPointAt(
                     element,
-                    pointFrom<GlobalPoint>(
-                      element.x + element.points[pointIndex][0] + deltaX,
-                      element.y + element.points[pointIndex][1] + deltaY,
-                    ),
-                    pointIndex,
-                    app,
+                    elementsMap,
+                    scenePointerX - linearElementEditor.pointerOffset.x,
+                    scenePointerY - linearElementEditor.pointerOffset.y,
+                    event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
+                  )
+                : pointFrom(
+                    element.points[pointIndex][0] + deltaX,
+                    element.points[pointIndex][1] + deltaY,
                   );
-              }
-
-              newPointPosition = LinearElementEditor.createPointAt(
-                element,
-                elementsMap,
-                globalNewPointPosition[0],
-                globalNewPointPosition[1],
-                event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
-              );
-            }
-
             return {
               index: pointIndex,
               point: newPointPosition,

+ 8 - 10
packages/element/src/utils.ts

@@ -18,7 +18,6 @@ import { getDiamondPoints } from "./bounds";
 
 import type {
   ExcalidrawDiamondElement,
-  ExcalidrawElement,
   ExcalidrawRectanguloidElement,
 } from "./types";
 
@@ -69,7 +68,10 @@ export function deconstructRectanguloidElement(
     return [sides, []];
   }
 
-  const center = elementCenterPoint(element);
+  const center = pointFrom<GlobalPoint>(
+    element.x + element.width / 2,
+    element.y + element.height / 2,
+  );
 
   const r = rectangle(
     pointFrom(element.x, element.y),
@@ -252,7 +254,10 @@ export function deconstructDiamondElement(
     return [[topRight, bottomRight, bottomLeft, topLeft], []];
   }
 
-  const center = elementCenterPoint(element);
+  const center = pointFrom<GlobalPoint>(
+    element.x + element.width / 2,
+    element.y + element.height / 2,
+  );
 
   const [top, right, bottom, left]: GlobalPoint[] = [
     pointFrom(element.x + topX, element.y + topY),
@@ -352,10 +357,3 @@ export function deconstructDiamondElement(
 
   return [sides, corners];
 }
-
-export function elementCenterPoint(element: ExcalidrawElement) {
-  return pointFrom<GlobalPoint>(
-    element.x + element.width / 2,
-    element.y + element.height / 2,
-  );
-}

+ 1 - 12
packages/element/tests/binding.test.tsx

@@ -190,18 +190,7 @@ describe("element binding", () => {
 
     // Sever connection
     expect(API.getSelectedElement().type).toBe("arrow");
-    Keyboard.withModifierKeys({ shift: true }, () => {
-      // We have to move a significant distance to get out of the binding zone
-      Keyboard.keyPress(KEYS.ARROW_LEFT);
-      Keyboard.keyPress(KEYS.ARROW_LEFT);
-      Keyboard.keyPress(KEYS.ARROW_LEFT);
-      Keyboard.keyPress(KEYS.ARROW_LEFT);
-      Keyboard.keyPress(KEYS.ARROW_LEFT);
-      Keyboard.keyPress(KEYS.ARROW_LEFT);
-      Keyboard.keyPress(KEYS.ARROW_LEFT);
-      Keyboard.keyPress(KEYS.ARROW_LEFT);
-      Keyboard.keyPress(KEYS.ARROW_LEFT);
-    });
+    Keyboard.keyPress(KEYS.ARROW_LEFT);
     expect(arrow.endBinding).toBe(null);
     Keyboard.keyPress(KEYS.ARROW_RIGHT);
     expect(arrow.endBinding).toBe(null);

+ 19 - 18
packages/element/tests/elbowArrow.test.tsx

@@ -77,9 +77,9 @@ describe("elbow arrow segment move", () => {
 
     expect(arrow.points).toCloselyEqualPoints([
       [0, 0],
-      [107.93, 0],
-      [107.93, 185.86],
-      [185.86, 185.86],
+      [110, 0],
+      [110, 200],
+      [190, 200],
     ]);
 
     mouse.reset();
@@ -88,9 +88,9 @@ describe("elbow arrow segment move", () => {
 
     expect(arrow.points).toCloselyEqualPoints([
       [0, 0],
-      [107.93, 0],
-      [107.93, 185.86],
-      [185.86, 185.86],
+      [110, 0],
+      [110, 200],
+      [190, 200],
     ]);
   });
 
@@ -198,11 +198,11 @@ describe("elbow arrow routing", () => {
       points: [pointFrom<LocalPoint>(0, 0), pointFrom<LocalPoint>(90, 200)],
     });
 
-    expect(arrow.points).toCloselyEqualPoints([
+    expect(arrow.points).toEqual([
       [0, 0],
-      [42.93, 0],
-      [42.93, 195.7],
-      [85.86, 195.7],
+      [45, 0],
+      [45, 200],
+      [90, 200],
     ]);
   });
 });
@@ -241,9 +241,9 @@ describe("elbow arrow ui", () => {
     expect(h.state.currentItemArrowType).toBe(ARROW_TYPE.elbow);
 
     mouse.reset();
-    mouse.moveTo(-50, -100);
+    mouse.moveTo(-43, -99);
     mouse.click();
-    mouse.moveTo(50, 100);
+    mouse.moveTo(43, 99);
     mouse.click();
 
     const arrow = h.scene.getSelectedElements(
@@ -252,11 +252,11 @@ describe("elbow arrow ui", () => {
 
     expect(arrow.type).toBe("arrow");
     expect(arrow.elbowed).toBe(true);
-    expect(arrow.points).toCloselyEqualPoints([
+    expect(arrow.points).toEqual([
       [0, 0],
-      [42.93, 0],
-      [42.93, 153.48],
-      [85.86, 153.48],
+      [45, 0],
+      [45, 200],
+      [90, 200],
     ]);
   });
 
@@ -296,8 +296,9 @@ describe("elbow arrow ui", () => {
 
     expect(arrow.points.map((point) => point.map(Math.round))).toEqual([
       [0, 0],
-      [129, 0],
-      [129, 131],
+      [35, 0],
+      [35, 165],
+      [103, 165],
     ]);
   });
 

+ 13 - 13
packages/element/tests/resize.test.tsx

@@ -195,7 +195,7 @@ describe("generic element", () => {
     UI.resize(rectangle, "w", [50, 0]);
 
     expect(arrow.endBinding?.elementId).toEqual(rectangle.id);
-    expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(80.62, 0);
+    expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(80, 0);
   });
 
   it("resizes with a label", async () => {
@@ -510,13 +510,13 @@ describe("arrow element", () => {
       h.state,
     )[0] as ExcalidrawElbowArrowElement;
 
-    expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.07);
-    expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.86);
+    expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1);
+    expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
 
     UI.resize(rectangle, "se", [-200, -150]);
 
-    expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.07);
-    expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.86);
+    expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1);
+    expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
   });
 
   it("flips the fixed point binding on negative resize for group selection", () => {
@@ -538,8 +538,8 @@ describe("arrow element", () => {
       h.state,
     )[0] as ExcalidrawElbowArrowElement;
 
-    expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.07);
-    expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.86);
+    expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1);
+    expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
 
     UI.resize([rectangle, arrow], "nw", [300, 350]);
     expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(0);
@@ -809,7 +809,7 @@ describe("image element", () => {
     });
     API.setElements([image]);
     const arrow = UI.createElement("arrow", {
-      x: -29,
+      x: -30,
       y: 50,
       width: 28,
       height: 5,
@@ -819,14 +819,14 @@ describe("image element", () => {
 
     UI.resize(image, "ne", [40, 0]);
 
-    expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(30, 0);
+    expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(31, 0);
 
     const imageWidth = image.width;
     const scale = 20 / image.height;
     UI.resize(image, "nw", [50, 20]);
 
     expect(arrow.endBinding?.elementId).toEqual(image.id);
-    expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(
+    expect(Math.floor(arrow.width + arrow.endBinding!.gap)).toBeCloseTo(
       30 + imageWidth * scale,
       0,
     );
@@ -1033,11 +1033,11 @@ describe("multiple selection", () => {
 
     expect(leftBoundArrow.x).toBeCloseTo(-110);
     expect(leftBoundArrow.y).toBeCloseTo(50);
-    expect(leftBoundArrow.width).toBeCloseTo(146.46, 0);
+    expect(leftBoundArrow.width).toBeCloseTo(143, 0);
     expect(leftBoundArrow.height).toBeCloseTo(7, 0);
     expect(leftBoundArrow.angle).toEqual(0);
     expect(leftBoundArrow.startBinding).toBeNull();
-    expect(leftBoundArrow.endBinding?.gap).toEqual(FIXED_BINDING_DISTANCE);
+    expect(leftBoundArrow.endBinding?.gap).toBeCloseTo(10);
     expect(leftBoundArrow.endBinding?.elementId).toBe(
       leftArrowBinding.elementId,
     );
@@ -1051,7 +1051,7 @@ describe("multiple selection", () => {
     expect(rightBoundArrow.height).toBeCloseTo(0);
     expect(rightBoundArrow.angle).toEqual(0);
     expect(rightBoundArrow.startBinding).toBeNull();
-    expect(rightBoundArrow.endBinding?.gap).toEqual(FIXED_BINDING_DISTANCE);
+    expect(rightBoundArrow.endBinding?.gap).toBeCloseTo(8.0952);
     expect(rightBoundArrow.endBinding?.elementId).toBe(
       rightArrowBinding.elementId,
     );

+ 3 - 19
packages/excalidraw/actions/actionFinalize.tsx

@@ -91,26 +91,10 @@ export const actionFinalize = register({
         multiPointElement.type !== "freedraw" &&
         appState.lastPointerDownWith !== "touch"
       ) {
-        const { x: rx, y: ry, points, lastCommittedPoint } = multiPointElement;
-        const lastGlobalPoint = pointFrom<GlobalPoint>(
-          rx + points[points.length - 1][0],
-          ry + points[points.length - 1][1],
-        );
-        const hoveredElementForBinding = getHoveredElementForBinding(
-          {
-            x: lastGlobalPoint[0],
-            y: lastGlobalPoint[1],
-          },
-          elements,
-          elementsMap,
-          app.state.zoom,
-          true,
-          isElbowArrow(multiPointElement),
-        );
+        const { points, lastCommittedPoint } = multiPointElement;
         if (
-          !hoveredElementForBinding &&
-          (!lastCommittedPoint ||
-            points[points.length - 1] !== lastCommittedPoint)
+          !lastCommittedPoint ||
+          points[points.length - 1] !== lastCommittedPoint
         ) {
           mutateElement(multiPointElement, {
             points: multiPointElement.points.slice(0, -1),

+ 0 - 2
packages/excalidraw/actions/actionProperties.tsx

@@ -1655,7 +1655,6 @@ export const actionChangeArrowType = register({
               newElement,
               startHoveredElement,
               "start",
-              elementsMap,
             )
           : startGlobalPoint;
         const finalEndPoint = endHoveredElement
@@ -1663,7 +1662,6 @@ export const actionChangeArrowType = register({
               newElement,
               endHoveredElement,
               "end",
-              elementsMap,
             )
           : endGlobalPoint;
 

+ 68 - 162
packages/excalidraw/components/App.tsx

@@ -5965,25 +5965,15 @@ class App extends React.Component<AppProps, AppState> {
         if (isPathALoop(points, this.state.zoom.value)) {
           setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
         }
-
         // update last uncommitted point
         mutateElement(
           multiElement,
           {
             points: [
               ...points.slice(0, -1),
-              pointTranslate<GlobalPoint, LocalPoint>(
-                LinearElementEditor.getOutlineAvoidingPoint(
-                  multiElement,
-                  pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
-                  multiElement.points.length - 1,
-                  this,
-                  pointFrom<GlobalPoint>(
-                    multiElement.x + lastCommittedX + dxFromLastCommitted,
-                    multiElement.y + lastCommittedY + dyFromLastCommitted,
-                  ),
-                ),
-                vector(-multiElement.x, -multiElement.y),
+              pointFrom<LocalPoint>(
+                lastCommittedX + dxFromLastCommitted,
+                lastCommittedY + dyFromLastCommitted,
               ),
             ],
           },
@@ -7725,34 +7715,18 @@ class App extends React.Component<AppProps, AppState> {
       }
 
       const { x: rx, y: ry, lastCommittedPoint } = multiElement;
-      const lastGlobalPoint = pointFrom<GlobalPoint>(
-        rx + multiElement.points[multiElement.points.length - 1][0],
-        ry + multiElement.points[multiElement.points.length - 1][1],
-      );
-      const hoveredElementForBinding = getHoveredElementForBinding(
-        {
-          x: lastGlobalPoint[0],
-          y: lastGlobalPoint[1],
-        },
-        this.scene.getNonDeletedElements(),
-        this.scene.getNonDeletedElementsMap(),
-        this.state.zoom,
-        true,
-        isElbowArrow(multiElement),
-      );
 
       // clicking inside commit zone → finalize arrow
       if (
-        !!hoveredElementForBinding ||
-        (multiElement.points.length > 1 &&
-          lastCommittedPoint &&
-          pointDistance(
-            pointFrom(
-              pointerDownState.origin.x - rx,
-              pointerDownState.origin.y - ry,
-            ),
-            lastCommittedPoint,
-          ) < LINE_CONFIRM_THRESHOLD)
+        multiElement.points.length > 1 &&
+        lastCommittedPoint &&
+        pointDistance(
+          pointFrom(
+            pointerDownState.origin.x - rx,
+            pointerDownState.origin.y - ry,
+          ),
+          lastCommittedPoint,
+        ) < LINE_CONFIRM_THRESHOLD
       ) {
         this.actionManager.executeAction(actionFinalize);
         return;
@@ -7796,93 +7770,53 @@ class App extends React.Component<AppProps, AppState> {
           ? [currentItemStartArrowhead, currentItemEndArrowhead]
           : [null, null];
 
-      let element: NonDeleted<ExcalidrawLinearElement>;
-      if (elementType === "arrow") {
-        const arrow: Mutable<NonDeleted<ExcalidrawArrowElement>> =
-          newArrowElement({
-            type: "arrow",
-            x: gridX,
-            y: gridY,
-            strokeColor: this.state.currentItemStrokeColor,
-            backgroundColor: this.state.currentItemBackgroundColor,
-            fillStyle: this.state.currentItemFillStyle,
-            strokeWidth: this.state.currentItemStrokeWidth,
-            strokeStyle: this.state.currentItemStrokeStyle,
-            roughness: this.state.currentItemRoughness,
-            opacity: this.state.currentItemOpacity,
-            roundness:
-              this.state.currentItemArrowType === ARROW_TYPE.round
-                ? { type: ROUNDNESS.PROPORTIONAL_RADIUS }
-                : // note, roundness doesn't have any effect for elbow arrows,
-                  // but it's best to set it to null as well
-                  null,
-            startArrowhead,
-            endArrowhead,
-            locked: false,
-            frameId: topLayerFrame ? topLayerFrame.id : null,
-            elbowed: this.state.currentItemArrowType === ARROW_TYPE.elbow,
-            fixedSegments:
-              this.state.currentItemArrowType === ARROW_TYPE.elbow ? [] : null,
-          });
-
-        const hoveredElement = getHoveredElementForBinding(
-          { x: gridX, y: gridY },
-          this.scene.getNonDeletedElements(),
-          this.scene.getNonDeletedElementsMap(),
-          this.state.zoom,
-          true,
-          this.state.currentItemArrowType === ARROW_TYPE.elbow,
-        );
-
-        if (hoveredElement) {
-          [arrow.x, arrow.y] =
-            intersectElementWithLineSegment(
-              hoveredElement,
-              lineSegment(
-                pointFrom<GlobalPoint>(gridX, gridY),
-                pointFrom<GlobalPoint>(
-                  gridX,
-                  hoveredElement.y + hoveredElement.height / 2,
-                ),
-              ),
-              2 * FIXED_BINDING_DISTANCE,
-            )[0] ??
-            intersectElementWithLineSegment(
-              hoveredElement,
-              lineSegment(
-                pointFrom<GlobalPoint>(gridX, gridY),
-                pointFrom<GlobalPoint>(
-                  hoveredElement.x + hoveredElement.width / 2,
-                  gridY,
-                ),
-              ),
-              2 * FIXED_BINDING_DISTANCE,
-            )[0] ??
-            pointFrom<GlobalPoint>(gridX, gridY);
-        }
-
-        element = arrow;
-      } else {
-        element = newLinearElement({
-          type: elementType,
-          x: gridX,
-          y: gridY,
-          strokeColor: this.state.currentItemStrokeColor,
-          backgroundColor: this.state.currentItemBackgroundColor,
-          fillStyle: this.state.currentItemFillStyle,
-          strokeWidth: this.state.currentItemStrokeWidth,
-          strokeStyle: this.state.currentItemStrokeStyle,
-          roughness: this.state.currentItemRoughness,
-          opacity: this.state.currentItemOpacity,
-          roundness:
-            this.state.currentItemRoundness === "round"
-              ? { type: ROUNDNESS.PROPORTIONAL_RADIUS }
-              : null,
-          locked: false,
-          frameId: topLayerFrame ? topLayerFrame.id : null,
-        });
-      }
-
+      const element =
+        elementType === "arrow"
+          ? newArrowElement({
+              type: elementType,
+              x: gridX,
+              y: gridY,
+              strokeColor: this.state.currentItemStrokeColor,
+              backgroundColor: this.state.currentItemBackgroundColor,
+              fillStyle: this.state.currentItemFillStyle,
+              strokeWidth: this.state.currentItemStrokeWidth,
+              strokeStyle: this.state.currentItemStrokeStyle,
+              roughness: this.state.currentItemRoughness,
+              opacity: this.state.currentItemOpacity,
+              roundness:
+                this.state.currentItemArrowType === ARROW_TYPE.round
+                  ? { type: ROUNDNESS.PROPORTIONAL_RADIUS }
+                  : // note, roundness doesn't have any effect for elbow arrows,
+                    // but it's best to set it to null as well
+                    null,
+              startArrowhead,
+              endArrowhead,
+              locked: false,
+              frameId: topLayerFrame ? topLayerFrame.id : null,
+              elbowed: this.state.currentItemArrowType === ARROW_TYPE.elbow,
+              fixedSegments:
+                this.state.currentItemArrowType === ARROW_TYPE.elbow
+                  ? []
+                  : null,
+            })
+          : newLinearElement({
+              type: elementType,
+              x: gridX,
+              y: gridY,
+              strokeColor: this.state.currentItemStrokeColor,
+              backgroundColor: this.state.currentItemBackgroundColor,
+              fillStyle: this.state.currentItemFillStyle,
+              strokeWidth: this.state.currentItemStrokeWidth,
+              strokeStyle: this.state.currentItemStrokeStyle,
+              roughness: this.state.currentItemRoughness,
+              opacity: this.state.currentItemOpacity,
+              roundness:
+                this.state.currentItemRoundness === "round"
+                  ? { type: ROUNDNESS.PROPORTIONAL_RADIUS }
+                  : null,
+              locked: false,
+              frameId: topLayerFrame ? topLayerFrame.id : null,
+            });
       this.setState((prevState) => {
         const nextSelectedElementIds = {
           ...prevState.selectedElementIds,
@@ -8197,6 +8131,12 @@ class App extends React.Component<AppProps, AppState> {
         this.laserTrails.addPointToPath(pointerCoords.x, pointerCoords.y);
       }
 
+      const [gridX, gridY] = getGridPoint(
+        pointerCoords.x,
+        pointerCoords.y,
+        event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(),
+      );
+
       // for arrows/lines, don't start dragging until a given threshold
       // to ensure we don't create a 2-point arrow by mistake when
       // user clicks mouse in a way that it moves a tiny bit (thus
@@ -8297,6 +8237,7 @@ class App extends React.Component<AppProps, AppState> {
             );
           },
           linearElementEditor,
+          this.scene,
         );
         if (newLinearElementEditor) {
           pointerDownState.lastCoords.x = pointerCoords.x;
@@ -8638,11 +8579,6 @@ class App extends React.Component<AppProps, AppState> {
         } else if (isLinearElement(newElement)) {
           pointerDownState.drag.hasOccurred = true;
           const points = newElement.points;
-          const [gridX, gridY] = getGridPoint(
-            pointerCoords.x,
-            pointerCoords.y,
-            event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(),
-          );
           let dx = gridX - newElement.x;
           let dy = gridY - newElement.y;
 
@@ -8659,22 +8595,7 @@ class App extends React.Component<AppProps, AppState> {
             mutateElement(
               newElement,
               {
-                points: [
-                  ...points,
-                  pointTranslate<GlobalPoint, LocalPoint>(
-                    LinearElementEditor.getOutlineAvoidingPoint(
-                      newElement,
-                      pointFrom<GlobalPoint>(pointerCoords.x, pointerCoords.y),
-                      newElement.points.length - 1,
-                      this,
-                      pointFrom<GlobalPoint>(
-                        newElement.x + dx,
-                        newElement.y + dy,
-                      ),
-                    ),
-                    vector(-newElement.x, -newElement.y),
-                  ),
-                ],
+                points: [...points, pointFrom<LocalPoint>(dx, dy)],
               },
               false,
             );
@@ -8685,22 +8606,7 @@ class App extends React.Component<AppProps, AppState> {
             mutateElement(
               newElement,
               {
-                points: [
-                  ...points.slice(0, -1),
-                  pointTranslate<GlobalPoint, LocalPoint>(
-                    LinearElementEditor.getOutlineAvoidingPoint(
-                      newElement,
-                      pointFrom<GlobalPoint>(pointerCoords.x, pointerCoords.y),
-                      newElement.points.length - 1,
-                      this,
-                      pointFrom<GlobalPoint>(
-                        newElement.x + dx,
-                        newElement.y + dy,
-                      ),
-                    ),
-                    vector(-newElement.x, -newElement.y),
-                  ),
-                ],
+                points: [...points.slice(0, -1), pointFrom<LocalPoint>(dx, dy)],
               },
               false,
               { isDragging: true },

+ 14 - 14
packages/excalidraw/data/__snapshots__/transform.test.ts.snap

@@ -89,7 +89,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
   "endBinding": {
     "elementId": "ellipse-1",
     "focus": -0.007519379844961235,
-    "gap": 5,
+    "gap": 11.562288374879595,
   },
   "fillStyle": "solid",
   "frameId": null,
@@ -119,7 +119,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
   "startBinding": {
     "elementId": "id49",
     "focus": -0.0813953488372095,
-    "gap": 5,
+    "gap": 1,
   },
   "strokeColor": "#1864ab",
   "strokeStyle": "solid",
@@ -145,7 +145,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
   "endBinding": {
     "elementId": "ellipse-1",
     "focus": 0.10666666666666667,
-    "gap": 5,
+    "gap": 3.8343264684446097,
   },
   "fillStyle": "solid",
   "frameId": null,
@@ -175,7 +175,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
   "startBinding": {
     "elementId": "diamond-1",
     "focus": 0,
-    "gap": 5,
+    "gap": 4.545343408287929,
   },
   "strokeColor": "#e67700",
   "strokeStyle": "solid",
@@ -335,7 +335,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
   "endBinding": {
     "elementId": "text-2",
     "focus": 0,
-    "gap": 5,
+    "gap": 14,
   },
   "fillStyle": "solid",
   "frameId": null,
@@ -365,7 +365,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
   "startBinding": {
     "elementId": "text-1",
     "focus": 0,
-    "gap": 5,
+    "gap": 1,
   },
   "strokeColor": "#1e1e1e",
   "strokeStyle": "solid",
@@ -437,7 +437,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
   "endBinding": {
     "elementId": "id42",
     "focus": -0,
-    "gap": 5,
+    "gap": 1,
   },
   "fillStyle": "solid",
   "frameId": null,
@@ -467,7 +467,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
   "startBinding": {
     "elementId": "id41",
     "focus": 0,
-    "gap": 5,
+    "gap": 1,
   },
   "strokeColor": "#1e1e1e",
   "strokeStyle": "solid",
@@ -613,7 +613,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
   "endBinding": {
     "elementId": "id46",
     "focus": -0,
-    "gap": 5,
+    "gap": 1,
   },
   "fillStyle": "solid",
   "frameId": null,
@@ -643,7 +643,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
   "startBinding": {
     "elementId": "id45",
     "focus": 0,
-    "gap": 5,
+    "gap": 1,
   },
   "strokeColor": "#1e1e1e",
   "strokeStyle": "solid",
@@ -1475,7 +1475,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
   "endBinding": {
     "elementId": "Alice",
     "focus": -0,
-    "gap": 5,
+    "gap": 5.299874999999986,
   },
   "fillStyle": "solid",
   "frameId": null,
@@ -1507,7 +1507,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
   "startBinding": {
     "elementId": "Bob",
     "focus": 0,
-    "gap": 5,
+    "gap": 1,
   },
   "strokeColor": "#1e1e1e",
   "strokeStyle": "solid",
@@ -1538,7 +1538,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
   "endBinding": {
     "elementId": "B",
     "focus": 0,
-    "gap": 5,
+    "gap": 14,
   },
   "fillStyle": "solid",
   "frameId": null,
@@ -1566,7 +1566,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
   "startBinding": {
     "elementId": "Bob",
     "focus": 0,
-    "gap": 5,
+    "gap": 1,
   },
   "strokeColor": "#1e1e1e",
   "strokeStyle": "solid",

+ 3 - 3
packages/excalidraw/data/transform.test.ts

@@ -433,7 +433,7 @@ describe("Test Transform", () => {
         startBinding: {
           elementId: rectangle.id,
           focus: 0,
-          gap: FIXED_BINDING_DISTANCE,
+          gap: 1,
         },
         endBinding: {
           elementId: ellipse.id,
@@ -518,7 +518,7 @@ describe("Test Transform", () => {
         startBinding: {
           elementId: text2.id,
           focus: 0,
-          gap: FIXED_BINDING_DISTANCE,
+          gap: 1,
         },
         endBinding: {
           elementId: text3.id,
@@ -781,7 +781,7 @@ describe("Test Transform", () => {
       expect((arrow as ExcalidrawArrowElement).endBinding).toStrictEqual({
         elementId: "rect-1",
         focus: -0,
-        gap: FIXED_BINDING_DISTANCE,
+        gap: 14,
       });
       expect(rect.boundElements).toStrictEqual([
         {

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

@@ -197,7 +197,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
   "fillStyle": "solid",
   "frameId": null,
   "groupIds": [],
-  "height": "99.58947",
+  "height": "102.35417",
   "id": "id172",
   "index": "a2",
   "isDeleted": false,
@@ -211,8 +211,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
       0,
     ],
     [
-      "99.58947",
-      "99.58947",
+      "101.77517",
+      "102.35417",
     ],
   ],
   "roughness": 1,
@@ -227,8 +227,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
   "type": "arrow",
   "updated": 1,
   "version": 40,
-  "width": "99.58947",
-  "x": 0,
+  "width": "101.77517",
+  "x": "0.70711",
   "y": 0,
 }
 `;
@@ -295,49 +295,47 @@ History {
               "endBinding": {
                 "elementId": "id171",
                 "focus": "0.00990",
-                "gap": 5,
+                "gap": 1,
               },
-              "height": "0.92929",
+              "height": "0.98586",
               "points": [
                 [
                   0,
                   0,
                 ],
                 [
-                  "92.92893",
-                  "-0.92929",
+                  "98.58579",
+                  "-0.98586",
                 ],
               ],
               "startBinding": {
                 "elementId": "id170",
                 "focus": "0.02970",
-                "gap": 5,
+                "gap": 1,
               },
-              "width": "92.92893",
             },
             "inserted": {
               "endBinding": {
                 "elementId": "id171",
-                "focus": "-0.02075",
-                "gap": 5,
+                "focus": "-0.02000",
+                "gap": 1,
               },
-              "height": "0.07074",
+              "height": "0.00000",
               "points": [
                 [
                   0,
                   0,
                 ],
                 [
-                  "92.92893",
-                  "0.07074",
+                  "98.58579",
+                  "0.00000",
                 ],
               ],
               "startBinding": {
                 "elementId": "id170",
-                "focus": "0.01770",
-                "gap": 5,
+                "focus": "0.02000",
+                "gap": 1,
               },
-              "width": "92.92893",
             },
           },
         },
@@ -391,47 +389,43 @@ History {
                 "focus": 0,
                 "gap": 1,
               },
-              "height": "99.58947",
+              "height": "102.35417",
               "points": [
                 [
                   0,
                   0,
                 ],
                 [
-                  "99.58947",
-                  "99.58947",
+                  "101.77517",
+                  "102.35417",
                 ],
               ],
               "startBinding": null,
-              "width": "99.58947",
-              "x": 0,
               "y": 0,
             },
             "inserted": {
               "endBinding": {
                 "elementId": "id171",
                 "focus": "0.00990",
-                "gap": 5,
+                "gap": 1,
               },
-              "height": "0.92929",
+              "height": "0.98586",
               "points": [
                 [
                   0,
                   0,
                 ],
                 [
-                  "92.92893",
-                  "-0.92929",
+                  "98.58579",
+                  "-0.98586",
                 ],
               ],
               "startBinding": {
                 "elementId": "id170",
                 "focus": "0.02970",
-                "gap": 5,
+                "gap": 1,
               },
-              "width": "92.92893",
-              "x": "3.53553",
-              "y": "0.96033",
+              "y": "0.99364",
             },
           },
           "id175" => Delta {
@@ -862,7 +856,6 @@ History {
                   0,
                 ],
               ],
-              "width": 0,
             },
             "inserted": {
               "points": [
@@ -871,11 +864,10 @@ History {
                   0,
                 ],
                 [
-                  "85.85786",
+                  100,
                   0,
                 ],
               ],
-              "width": "85.85786",
             },
           },
         },
@@ -932,14 +924,12 @@ History {
                 ],
               ],
               "startBinding": null,
-              "width": 100,
-              "x": 150,
             },
             "inserted": {
               "endBinding": {
                 "elementId": "id166",
                 "focus": -0,
-                "gap": 5,
+                "gap": 1,
               },
               "points": [
                 [
@@ -954,10 +944,8 @@ History {
               "startBinding": {
                 "elementId": "id165",
                 "focus": 0,
-                "gap": 5,
+                "gap": 1,
               },
-              "width": 0,
-              "x": "146.46447",
             },
           },
         },
@@ -1250,7 +1238,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
   "fillStyle": "solid",
   "frameId": null,
   "groupIds": [],
-  "height": "1.71911",
+  "height": "1.30038",
   "id": "id178",
   "index": "Zz",
   "isDeleted": false,
@@ -1264,8 +1252,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
       0,
     ],
     [
-      "92.92893",
-      "1.71911",
+      "98.58579",
+      "1.30038",
     ],
   ],
   "roughness": 1,
@@ -1288,8 +1276,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
   "type": "arrow",
   "updated": 1,
   "version": 11,
-  "width": "92.92893",
-  "x": "3.53553",
+  "width": "98.58579",
+  "x": "0.70711",
   "y": 0,
 }
 `;
@@ -1621,7 +1609,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
   "fillStyle": "solid",
   "frameId": null,
   "groupIds": [],
-  "height": "1.71911",
+  "height": "1.30038",
   "id": "id181",
   "index": "a0",
   "isDeleted": false,
@@ -1635,8 +1623,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
       0,
     ],
     [
-      "92.92893",
-      "1.71911",
+      "98.58579",
+      "1.30038",
     ],
   ],
   "roughness": 1,
@@ -1659,8 +1647,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
   "type": "arrow",
   "updated": 1,
   "version": 11,
-  "width": "92.92893",
-  "x": "3.53553",
+  "width": "98.58579",
+  "x": "0.70711",
   "y": 0,
 }
 `;
@@ -1779,7 +1767,7 @@ History {
               "fillStyle": "solid",
               "frameId": null,
               "groupIds": [],
-              "height": "12.86717",
+              "height": "11.27227",
               "index": "a0",
               "isDeleted": false,
               "lastCommittedPoint": null,
@@ -1792,8 +1780,8 @@ History {
                   0,
                 ],
                 [
-                  "92.92893",
-                  "12.86717",
+                  "98.58579",
+                  "11.27227",
                 ],
               ],
               "roughness": 1,
@@ -1814,8 +1802,8 @@ History {
               "strokeStyle": "solid",
               "strokeWidth": 2,
               "type": "arrow",
-              "width": "92.92893",
-              "x": "3.53553",
+              "width": "98.58579",
+              "x": "0.70711",
               "y": 0,
             },
             "inserted": {
@@ -2327,12 +2315,12 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
   "endBinding": {
     "elementId": "id185",
     "focus": -0,
-    "gap": 5,
+    "gap": 1,
   },
   "fillStyle": "solid",
   "frameId": null,
   "groupIds": [],
-  "height": "369.21589",
+  "height": "374.05754",
   "id": "id186",
   "index": "a2",
   "isDeleted": false,
@@ -2346,8 +2334,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
       0,
     ],
     [
-      "496.84035",
-      "-369.21589",
+      "502.78936",
+      "-374.05754",
     ],
   ],
   "roughness": 1,
@@ -2358,7 +2346,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
   "startBinding": {
     "elementId": "id184",
     "focus": 0,
-    "gap": 5,
+    "gap": 1,
   },
   "strokeColor": "#1e1e1e",
   "strokeStyle": "solid",
@@ -2366,9 +2354,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
   "type": "arrow",
   "updated": 1,
   "version": 10,
-  "width": "496.84035",
-  "x": "2.18463",
-  "y": "-38.80748",
+  "width": "502.78936",
+  "x": "-0.83465",
+  "y": "-36.58211",
 }
 `;
 
@@ -2487,7 +2475,7 @@ History {
               "endBinding": {
                 "elementId": "id185",
                 "focus": -0,
-                "gap": 5,
+                "gap": 1,
               },
               "fillStyle": "solid",
               "frameId": null,
@@ -2517,7 +2505,7 @@ History {
               "startBinding": {
                 "elementId": "id184",
                 "focus": 0,
-                "gap": 5,
+                "gap": 1,
               },
               "strokeColor": "#1e1e1e",
               "strokeStyle": "solid",
@@ -15123,7 +15111,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
   "endBinding": {
     "elementId": "id58",
     "focus": -0,
-    "gap": 5,
+    "gap": 1,
   },
   "fillStyle": "solid",
   "frameId": null,
@@ -15142,7 +15130,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
       0,
     ],
     [
-      "92.92893",
+      "98.58579",
       0,
     ],
   ],
@@ -15154,7 +15142,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
   "startBinding": {
     "elementId": "id56",
     "focus": 0,
-    "gap": 5,
+    "gap": 1,
   },
   "strokeColor": "#1e1e1e",
   "strokeStyle": "solid",
@@ -15162,8 +15150,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
   "type": "arrow",
   "updated": 1,
   "version": 10,
-  "width": "92.92893",
-  "x": "3.53553",
+  "width": "98.58579",
+  "x": "0.70711",
   "y": 0,
 }
 `;
@@ -15494,7 +15482,7 @@ History {
               "endBinding": {
                 "elementId": "id58",
                 "focus": -0,
-                "gap": 5,
+                "gap": 1,
               },
               "fillStyle": "solid",
               "frameId": null,
@@ -15524,7 +15512,7 @@ History {
               "startBinding": {
                 "elementId": "id56",
                 "focus": 0,
-                "gap": 5,
+                "gap": 1,
               },
               "strokeColor": "#1e1e1e",
               "strokeStyle": "solid",
@@ -15820,7 +15808,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
   "endBinding": {
     "elementId": "id52",
     "focus": -0,
-    "gap": 5,
+    "gap": 1,
   },
   "fillStyle": "solid",
   "frameId": null,
@@ -15839,7 +15827,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
       0,
     ],
     [
-      "92.92893",
+      "98.58579",
       0,
     ],
   ],
@@ -15851,7 +15839,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
   "startBinding": {
     "elementId": "id50",
     "focus": 0,
-    "gap": 5,
+    "gap": 1,
   },
   "strokeColor": "#1e1e1e",
   "strokeStyle": "solid",
@@ -15859,8 +15847,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
   "type": "arrow",
   "updated": 1,
   "version": 10,
-  "width": "92.92893",
-  "x": "3.53553",
+  "width": "98.58579",
+  "x": "0.70711",
   "y": 0,
 }
 `;
@@ -16113,7 +16101,7 @@ History {
               "endBinding": {
                 "elementId": "id52",
                 "focus": -0,
-                "gap": 5,
+                "gap": 1,
               },
               "fillStyle": "solid",
               "frameId": null,
@@ -16143,7 +16131,7 @@ History {
               "startBinding": {
                 "elementId": "id50",
                 "focus": 0,
-                "gap": 5,
+                "gap": 1,
               },
               "strokeColor": "#1e1e1e",
               "strokeStyle": "solid",
@@ -16439,7 +16427,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
   "endBinding": {
     "elementId": "id64",
     "focus": -0,
-    "gap": 5,
+    "gap": 1,
   },
   "fillStyle": "solid",
   "frameId": null,
@@ -16458,7 +16446,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
       0,
     ],
     [
-      "92.92893",
+      "98.58579",
       0,
     ],
   ],
@@ -16470,7 +16458,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
   "startBinding": {
     "elementId": "id62",
     "focus": 0,
-    "gap": 5,
+    "gap": 1,
   },
   "strokeColor": "#1e1e1e",
   "strokeStyle": "solid",
@@ -16478,8 +16466,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
   "type": "arrow",
   "updated": 1,
   "version": 10,
-  "width": "92.92893",
-  "x": "3.53553",
+  "width": "98.58579",
+  "x": "0.70711",
   "y": 0,
 }
 `;
@@ -16732,7 +16720,7 @@ History {
               "endBinding": {
                 "elementId": "id64",
                 "focus": -0,
-                "gap": 5,
+                "gap": 1,
               },
               "fillStyle": "solid",
               "frameId": null,
@@ -16762,7 +16750,7 @@ History {
               "startBinding": {
                 "elementId": "id62",
                 "focus": 0,
-                "gap": 5,
+                "gap": 1,
               },
               "strokeColor": "#1e1e1e",
               "strokeStyle": "solid",
@@ -17056,7 +17044,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
   "endBinding": {
     "elementId": "id70",
     "focus": -0,
-    "gap": 5,
+    "gap": 1,
   },
   "fillStyle": "solid",
   "frameId": null,
@@ -17075,7 +17063,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
       0,
     ],
     [
-      "92.92893",
+      "98.58579",
       0,
     ],
   ],
@@ -17087,7 +17075,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
   "startBinding": {
     "elementId": "id68",
     "focus": 0,
-    "gap": 5,
+    "gap": 1,
   },
   "strokeColor": "#1e1e1e",
   "strokeStyle": "solid",
@@ -17095,8 +17083,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
   "type": "arrow",
   "updated": 1,
   "version": 10,
-  "width": "92.92893",
-  "x": "3.53553",
+  "width": "98.58579",
+  "x": "0.70711",
   "y": 0,
 }
 `;
@@ -17159,7 +17147,7 @@ History {
               "startBinding": {
                 "elementId": "id68",
                 "focus": 0,
-                "gap": 5,
+                "gap": 1,
               },
             },
             "inserted": {
@@ -17419,7 +17407,7 @@ History {
               "endBinding": {
                 "elementId": "id70",
                 "focus": -0,
-                "gap": 5,
+                "gap": 1,
               },
               "fillStyle": "solid",
               "frameId": null,
@@ -17449,7 +17437,7 @@ History {
               "startBinding": {
                 "elementId": "id68",
                 "focus": 0,
-                "gap": 5,
+                "gap": 1,
               },
               "strokeColor": "#1e1e1e",
               "strokeStyle": "solid",
@@ -17769,7 +17757,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
   "endBinding": {
     "elementId": "id77",
     "focus": -0,
-    "gap": 5,
+    "gap": 1,
   },
   "fillStyle": "solid",
   "frameId": null,
@@ -17788,7 +17776,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
       0,
     ],
     [
-      "92.92893",
+      "98.58579",
       0,
     ],
   ],
@@ -17800,7 +17788,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
   "startBinding": {
     "elementId": "id75",
     "focus": 0,
-    "gap": 5,
+    "gap": 1,
   },
   "strokeColor": "#1e1e1e",
   "strokeStyle": "solid",
@@ -17808,8 +17796,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
   "type": "arrow",
   "updated": 1,
   "version": 11,
-  "width": "92.92893",
-  "x": "3.53553",
+  "width": "98.58579",
+  "x": "0.70711",
   "y": 0,
 }
 `;
@@ -17871,7 +17859,7 @@ History {
               "endBinding": {
                 "elementId": "id77",
                 "focus": -0,
-                "gap": 5,
+                "gap": 1,
               },
               "points": [
                 [
@@ -17886,7 +17874,7 @@ History {
               "startBinding": {
                 "elementId": "id75",
                 "focus": 0,
-                "gap": 5,
+                "gap": 1,
               },
             },
             "inserted": {
@@ -18147,7 +18135,7 @@ History {
               "endBinding": {
                 "elementId": "id77",
                 "focus": -0,
-                "gap": 5,
+                "gap": 1,
               },
               "fillStyle": "solid",
               "frameId": null,
@@ -18177,7 +18165,7 @@ History {
               "startBinding": {
                 "elementId": "id75",
                 "focus": 0,
-                "gap": 5,
+                "gap": 1,
               },
               "strokeColor": "#1e1e1e",
               "strokeStyle": "solid",

+ 8 - 8
packages/excalidraw/tests/__snapshots__/move.test.tsx.snap

@@ -191,12 +191,12 @@ exports[`move element > rectangles with binding arrow 7`] = `
   "endBinding": {
     "elementId": "id1",
     "focus": "-0.46667",
-    "gap": 5,
+    "gap": 10,
   },
   "fillStyle": "solid",
   "frameId": null,
   "groupIds": [],
-  "height": "94.40997",
+  "height": "87.29887",
   "id": "id2",
   "index": "a2",
   "isDeleted": false,
@@ -210,8 +210,8 @@ exports[`move element > rectangles with binding arrow 7`] = `
       0,
     ],
     [
-      "93.92893",
-      "94.40997",
+      "86.85786",
+      "87.29887",
     ],
   ],
   "roughness": 1,
@@ -223,7 +223,7 @@ exports[`move element > rectangles with binding arrow 7`] = `
   "startBinding": {
     "elementId": "id0",
     "focus": "-0.60000",
-    "gap": 5,
+    "gap": 10,
   },
   "strokeColor": "#1e1e1e",
   "strokeStyle": "solid",
@@ -232,8 +232,8 @@ exports[`move element > rectangles with binding arrow 7`] = `
   "updated": 1,
   "version": 11,
   "versionNonce": 1051383431,
-  "width": "93.92893",
-  "x": "103.53553",
-  "y": "43.53553",
+  "width": "86.85786",
+  "x": "107.07107",
+  "y": "47.07107",
 }
 `;

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

@@ -4779,12 +4779,12 @@ describe("history", () => {
               startBinding: expect.objectContaining({
                 elementId: rect1.id,
                 focus: 0,
-                gap: FIXED_BINDING_DISTANCE,
+                gap: 1,
               }),
               endBinding: expect.objectContaining({
                 elementId: rect2.id,
                 focus: -0,
-                gap: FIXED_BINDING_DISTANCE,
+                gap: 1,
               }),
               isDeleted: true,
             }),

+ 1 - 1
packages/excalidraw/tests/linearElementEditor.test.tsx

@@ -1246,7 +1246,7 @@ describe("Test Linear Elements", () => {
       mouse.downAt(rect.x, rect.y);
       mouse.moveTo(200, 0);
       mouse.upAt(200, 0);
-      expect(arrow.width).toBeCloseTo(206.86, 0);
+      expect(arrow.width).toBeCloseTo(204, 0);
       expect(rect.x).toBe(200);
       expect(rect.y).toBe(0);
       expect(handleBindTextResizeSpy).toHaveBeenCalledWith(

+ 2 - 4
packages/excalidraw/tests/move.test.tsx

@@ -128,10 +128,8 @@ describe("move element", () => {
     expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
     expect([rectA.x, rectA.y]).toEqual([0, 0]);
     expect([rectB.x, rectB.y]).toEqual([201, 2]);
-    expect([[arrow.x, arrow.y]]).toCloselyEqualPoints([[103.54, 43.53]]);
-    expect([[arrow.width, arrow.height]]).toCloselyEqualPoints([
-      [93.93, 94.41],
-    ]);
+    expect([[arrow.x, arrow.y]]).toCloselyEqualPoints([[107.07, 47.07]]);
+    expect([[arrow.width, arrow.height]]).toCloselyEqualPoints([[86.86, 87.3]]);
 
     h.elements.forEach((element) => expect(element).toMatchSnapshot());
   });

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

@@ -35,7 +35,7 @@ test("unselected bound arrow updates when rotating its target element", async ()
   expect(arrow.endBinding?.elementId).toEqual(rectangle.id);
   expect(arrow.x).toBeCloseTo(-80);
   expect(arrow.y).toBeCloseTo(50);
-  expect(arrow.width).toBeCloseTo(119.58, 1);
+  expect(arrow.width).toBeCloseTo(116.7, 1);
   expect(arrow.height).toBeCloseTo(0);
 });
 
@@ -72,13 +72,13 @@ test("unselected bound arrows update when rotating their target elements", async
   expect(ellipseArrow.x).toEqual(0);
   expect(ellipseArrow.y).toEqual(0);
   expect(ellipseArrow.points[0]).toEqual([0, 0]);
-  expect(ellipseArrow.points[1][0]).toBeCloseTo(54.36, 1);
-  expect(ellipseArrow.points[1][1]).toBeCloseTo(139.61, 1);
+  expect(ellipseArrow.points[1][0]).toBeCloseTo(48.98, 1);
+  expect(ellipseArrow.points[1][1]).toBeCloseTo(125.79, 1);
 
   expect(textArrow.endBinding?.elementId).toEqual(text.id);
   expect(textArrow.x).toEqual(360);
   expect(textArrow.y).toEqual(300);
   expect(textArrow.points[0]).toEqual([0, 0]);
-  expect(textArrow.points[1][0]).toBeCloseTo(-100.12, 0);
-  expect(textArrow.points[1][1]).toBeCloseTo(-123.63, 0);
+  expect(textArrow.points[1][0]).toBeCloseTo(-94, 0);
+  expect(textArrow.points[1][1]).toBeCloseTo(-116.1, 0);
 });

+ 15 - 6
packages/math/src/curve.ts

@@ -157,13 +157,22 @@ export function curveIntersectLineSegment<
     return bezierEquation(c, t);
   };
 
-  const solutions = [
-    calculate(initial_guesses[0]),
-    calculate(initial_guesses[1]),
-    calculate(initial_guesses[2]),
-  ].filter((x, i, a): x is Point => x !== null && a.indexOf(x) === i);
+  let solution = calculate(initial_guesses[0]);
+  if (solution) {
+    return [solution];
+  }
+
+  solution = calculate(initial_guesses[1]);
+  if (solution) {
+    return [solution];
+  }
+
+  solution = calculate(initial_guesses[2]);
+  if (solution) {
+    return [solution];
+  }
 
-  return solutions;
+  return [];
 }
 
 /**

+ 1 - 2
packages/math/src/point.ts

@@ -91,10 +91,9 @@ export function isPoint(p: unknown): p is LocalPoint | GlobalPoint {
 export function pointsEqual<Point extends GlobalPoint | LocalPoint>(
   a: Point,
   b: Point,
-  precision = PRECISION,
 ): boolean {
   const abs = Math.abs;
-  return abs(a[0] - b[0]) < precision && abs(a[1] - b[1]) < precision;
+  return abs(a[0] - b[0]) < PRECISION && abs(a[1] - b[1]) < PRECISION;
 }
 
 /**

+ 1 - 1
packages/math/src/utils.ts

@@ -6,7 +6,7 @@ export const clamp = (value: number, min: number, max: number) => {
 
 export const round = (
   value: number,
-  precision: number = (Math.log(1 / PRECISION) * Math.LOG10E + 1) | 0,
+  precision: number,
   func: "round" | "floor" | "ceil" = "round",
 ) => {
   const multiplier = Math.pow(10, precision);