From 9ba0f5dbc9d551aad9277341a82449e04043c5ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rk=20Tolm=C3=A1cs?= Date: Tue, 20 Jan 2026 12:38:40 +0100 Subject: [PATCH] fix: Arrow drag start in bindable area jumps across bindable (#10676) Signed-off-by: Mark Tolmacs --- packages/element/src/binding.ts | 8 +- .../tests/__snapshots__/history.test.tsx.snap | 220 +++++++++--------- .../tests/__snapshots__/move.test.tsx.snap | 20 +- packages/excalidraw/tests/history.test.tsx | 24 +- packages/excalidraw/tests/move.test.tsx | 11 +- 5 files changed, 137 insertions(+), 146 deletions(-) diff --git a/packages/element/src/binding.ts b/packages/element/src/binding.ts index 1a519b1c98..c29c870260 100644 --- a/packages/element/src/binding.ts +++ b/packages/element/src/binding.ts @@ -15,6 +15,7 @@ import { pointFrom, pointFromVector, pointRotateRads, + pointsEqual, vectorFromPoint, vectorNormalize, vectorScale, @@ -1602,7 +1603,12 @@ export const updateBoundPoint = ( if ( binding == null || // We only need to update the other end if this is a 2 point line element - (binding.elementId !== bindableElement.id && arrow.points.length > 2) + (binding.elementId !== bindableElement.id && arrow.points.length > 2) || + // Initial arrow created on pointer down needs to not update the points + pointsEqual( + arrow.points[arrow.points.length - 1], + pointFrom(0, 0), + ) ) { return null; } diff --git a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap index 7c0c8b500c..d87b67f8d7 100644 --- a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap @@ -2398,7 +2398,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "361.17383", + "height": "353.93938", "id": "id4", "index": "a2", "isDeleted": false, @@ -2412,8 +2412,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl 0, ], [ - "478.06757", - "-361.17383", + "478.03877", + "-353.93938", ], ], "roughness": 1, @@ -2424,8 +2424,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "startBinding": { "elementId": "id0", "fixedPoint": [ - "0.54844", - "0.54844", + "0.50010", + "0.50010", ], "mode": "orbit", }, @@ -2434,10 +2434,10 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 14, - "width": "478.06757", + "version": 12, + "width": "478.03877", "x": 11, - "y": "-37.61782", + "y": "-45.14705", } `; @@ -2566,7 +2566,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "361.17383", + "height": "353.93938", "index": "a2", "isDeleted": false, "link": null, @@ -2578,8 +2578,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl 0, ], [ - "478.06757", - "-361.17383", + "478.03877", + "-353.93938", ], ], "roughness": 1, @@ -2590,8 +2590,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "startBinding": { "elementId": "id0", "fixedPoint": [ - "0.54844", - "0.54844", + "0.50010", + "0.50010", ], "mode": "orbit", }, @@ -2599,14 +2599,14 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "strokeStyle": "solid", "strokeWidth": 2, "type": "arrow", - "version": 14, - "width": "478.06757", + "version": 12, + "width": "478.03877", "x": 11, - "y": "-37.61782", + "y": "-45.14705", }, "inserted": { "isDeleted": true, - "version": 11, + "version": 9, }, }, }, @@ -16383,7 +16383,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "1.22525", + "height": "0.00108", "id": "id13", "index": "a3", "isDeleted": false, @@ -16397,8 +16397,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding 0, ], [ - 78, - "-1.22525", + "78.00000", + "0.00108", ], ], "roughness": 1, @@ -16409,8 +16409,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "startBinding": { "elementId": "id0", "fixedPoint": [ - "0.53796", - "0.53796", + "0.50010", + "0.50010", ], "mode": "orbit", }, @@ -16419,10 +16419,10 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 14, - "width": 78, + "version": 12, + "width": "78.00000", "x": 11, - "y": "2.19362", + "y": "0.00807", } `; @@ -16467,12 +16467,12 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "startBinding": { "elementId": "id0", "fixedPoint": [ - "0.53796", - "0.53796", + "0.50010", + "0.50010", ], "mode": "orbit", }, - "version": 14, + "version": 12, }, "inserted": { "endBinding": { @@ -16487,12 +16487,12 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "startBinding": { "elementId": "id0", "fixedPoint": [ - "0.53796", - "0.53796", + "0.50010", + "0.50010", ], "mode": "orbit", }, - "version": 11, + "version": 9, }, }, }, @@ -16801,7 +16801,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "0.00096", + "height": "0.00561", "index": "a3", "isDeleted": false, "link": null, @@ -16814,7 +16814,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding ], [ "78.00000", - "0.00096", + "0.00561", ], ], "roughness": 1, @@ -16825,8 +16825,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "startBinding": { "elementId": "id0", "fixedPoint": [ - "0.53796", - "0.53796", + "0.50010", + "0.50010", ], "mode": "orbit", }, @@ -16834,14 +16834,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "strokeStyle": "solid", "strokeWidth": 2, "type": "arrow", - "version": 10, + "version": 8, "width": "78.00000", "x": 11, - "y": "0.00830", + "y": 0, }, "inserted": { "isDeleted": true, - "version": 9, + "version": 7, }, }, }, @@ -17134,7 +17134,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "1.22525", + "height": "0.00108", "id": "id13", "index": "a3", "isDeleted": false, @@ -17148,8 +17148,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding 0, ], [ - 78, - "-1.22525", + "78.00000", + "0.00108", ], ], "roughness": 1, @@ -17160,8 +17160,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "startBinding": { "elementId": "id0", "fixedPoint": [ - "0.53796", - "0.53796", + "0.50010", + "0.50010", ], "mode": "orbit", }, @@ -17170,10 +17170,10 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 14, - "width": 78, + "version": 12, + "width": "78.00000", "x": 11, - "y": "2.19362", + "y": "0.00807", } `; @@ -17442,7 +17442,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "1.22525", + "height": "0.00108", "index": "a3", "isDeleted": false, "link": null, @@ -17454,8 +17454,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding 0, ], [ - 78, - "-1.22525", + "78.00000", + "0.00108", ], ], "roughness": 1, @@ -17466,8 +17466,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "startBinding": { "elementId": "id0", "fixedPoint": [ - "0.53796", - "0.53796", + "0.50010", + "0.50010", ], "mode": "orbit", }, @@ -17475,14 +17475,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "strokeStyle": "solid", "strokeWidth": 2, "type": "arrow", - "version": 14, - "width": 78, + "version": 12, + "width": "78.00000", "x": 11, - "y": "2.19362", + "y": "0.00807", }, "inserted": { "isDeleted": true, - "version": 11, + "version": 9, }, }, }, @@ -17783,7 +17783,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "1.22525", + "height": "0.00108", "id": "id13", "index": "a3", "isDeleted": false, @@ -17797,8 +17797,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding 0, ], [ - 78, - "-1.22525", + "78.00000", + "0.00108", ], ], "roughness": 1, @@ -17809,8 +17809,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "startBinding": { "elementId": "id0", "fixedPoint": [ - "0.53796", - "0.53796", + "0.50010", + "0.50010", ], "mode": "orbit", }, @@ -17819,10 +17819,10 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 14, - "width": 78, + "version": 12, + "width": "78.00000", "x": 11, - "y": "2.19362", + "y": "0.00807", } `; @@ -18091,7 +18091,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "1.22525", + "height": "0.00108", "index": "a3", "isDeleted": false, "link": null, @@ -18103,8 +18103,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding 0, ], [ - 78, - "-1.22525", + "78.00000", + "0.00108", ], ], "roughness": 1, @@ -18115,8 +18115,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "startBinding": { "elementId": "id0", "fixedPoint": [ - "0.53796", - "0.53796", + "0.50010", + "0.50010", ], "mode": "orbit", }, @@ -18124,14 +18124,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "strokeStyle": "solid", "strokeWidth": 2, "type": "arrow", - "version": 14, - "width": 78, + "version": 12, + "width": "78.00000", "x": 11, - "y": "2.19362", + "y": "0.00807", }, "inserted": { "isDeleted": true, - "version": 11, + "version": 9, }, }, }, @@ -18430,7 +18430,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "1.22525", + "height": "0.00108", "id": "id13", "index": "a3", "isDeleted": false, @@ -18444,8 +18444,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding 0, ], [ - 78, - "-1.22525", + "78.00000", + "0.00108", ], ], "roughness": 1, @@ -18456,8 +18456,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "startBinding": { "elementId": "id0", "fixedPoint": [ - "0.53796", - "0.53796", + "0.50010", + "0.50010", ], "mode": "orbit", }, @@ -18466,10 +18466,10 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 14, - "width": 78, + "version": 12, + "width": "78.00000", "x": 11, - "y": "2.19362", + "y": "0.00807", } `; @@ -18530,12 +18530,12 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "startBinding": { "elementId": "id0", "fixedPoint": [ - "0.53796", - "0.53796", + "0.50010", + "0.50010", ], "mode": "orbit", }, - "version": 14, + "version": 12, }, "inserted": { "endBinding": { @@ -18547,7 +18547,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "mode": "orbit", }, "startBinding": null, - "version": 11, + "version": 9, }, }, "id2": { @@ -18824,7 +18824,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "0.00096", + "height": "0.00561", "index": "a3", "isDeleted": false, "link": null, @@ -18837,7 +18837,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding ], [ "78.00000", - "0.00096", + "0.00561", ], ], "roughness": 1, @@ -18848,8 +18848,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "startBinding": { "elementId": "id0", "fixedPoint": [ - "0.53796", - "0.53796", + "0.50010", + "0.50010", ], "mode": "orbit", }, @@ -18857,14 +18857,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "strokeStyle": "solid", "strokeWidth": 2, "type": "arrow", - "version": 10, + "version": 8, "width": "78.00000", "x": 11, - "y": "0.00830", + "y": 0, }, "inserted": { "isDeleted": true, - "version": 9, + "version": 7, }, }, }, @@ -19185,7 +19185,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "1.22525", + "height": "0.00108", "id": "id13", "index": "a3", "isDeleted": false, @@ -19199,8 +19199,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding 0, ], [ - 78, - "-1.22525", + "78.00000", + "0.00108", ], ], "roughness": 1, @@ -19211,8 +19211,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "startBinding": { "elementId": "id0", "fixedPoint": [ - "0.53796", - "0.53796", + "0.50010", + "0.50010", ], "mode": "orbit", }, @@ -19221,10 +19221,10 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 15, - "width": 78, + "version": 13, + "width": "78.00000", "x": 11, - "y": "2.19362", + "y": "0.00807", } `; @@ -19296,17 +19296,17 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "startBinding": { "elementId": "id0", "fixedPoint": [ - "0.53796", - "0.53796", + "0.50010", + "0.50010", ], "mode": "orbit", }, - "version": 15, + "version": 13, }, "inserted": { "endBinding": null, "startBinding": null, - "version": 12, + "version": 10, }, }, }, @@ -19575,7 +19575,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "0.00096", + "height": "0.00561", "index": "a3", "isDeleted": false, "link": null, @@ -19588,7 +19588,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding ], [ "78.00000", - "0.00096", + "0.00561", ], ], "roughness": 1, @@ -19599,8 +19599,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "startBinding": { "elementId": "id0", "fixedPoint": [ - "0.53796", - "0.53796", + "0.50010", + "0.50010", ], "mode": "orbit", }, @@ -19608,14 +19608,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "strokeStyle": "solid", "strokeWidth": 2, "type": "arrow", - "version": 10, + "version": 8, "width": "78.00000", "x": 11, - "y": "0.00830", + "y": 0, }, "inserted": { "isDeleted": true, - "version": 9, + "version": 7, }, }, }, diff --git a/packages/excalidraw/tests/__snapshots__/move.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/move.test.tsx.snap index fceb5f14a7..6678adef33 100644 --- a/packages/excalidraw/tests/__snapshots__/move.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/move.test.tsx.snap @@ -163,7 +163,7 @@ exports[`move element > rectangles with binding arrow 6`] = ` "type": "rectangle", "updated": 1, "version": 7, - "versionNonce": 888958951, + "versionNonce": 735304455, "width": 300, "x": 201, "y": 2, @@ -182,14 +182,14 @@ exports[`move element > rectangles with binding arrow 7`] = ` "elementId": "id3", "fixedPoint": [ "-0.03667", - "0.42773", + "0.43000", ], "mode": "orbit", }, "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "124.16785", + "height": "80.00000", "id": "id6", "index": "a2", "isDeleted": false, @@ -204,7 +204,7 @@ exports[`move element > rectangles with binding arrow 7`] = ` ], [ "79.00000", - "124.16785", + "80.00000", ], ], "roughness": 1, @@ -216,8 +216,8 @@ exports[`move element > rectangles with binding arrow 7`] = ` "startBinding": { "elementId": "id0", "fixedPoint": [ - "1.10000", - "-0.04577", + "1.11000", + "0.51000", ], "mode": "orbit", }, @@ -226,10 +226,10 @@ exports[`move element > rectangles with binding arrow 7`] = ` "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 12, - "versionNonce": 2066753033, + "version": 13, + "versionNonce": 271613161, "width": "79.00000", - "x": 111, - "y": "6.14995", + "x": "111.00000", + "y": "51.00000", } `; diff --git a/packages/excalidraw/tests/history.test.tsx b/packages/excalidraw/tests/history.test.tsx index d3c6e2d470..d3b0cf48f3 100644 --- a/packages/excalidraw/tests/history.test.tsx +++ b/packages/excalidraw/tests/history.test.tsx @@ -1590,9 +1590,7 @@ describe("history", () => { expect(API.getUndoStack().length).toBe(5); expect(arrow.startBinding).toEqual({ elementId: rect1.id, - fixedPoint: expect.arrayContaining([ - 0.5379561888991137, 0.5379561888991137, - ]), + fixedPoint: expect.arrayContaining([0.5001, 0.5001]), mode: "orbit", }); expect(arrow.endBinding).toEqual({ @@ -1615,9 +1613,7 @@ describe("history", () => { expect(API.getRedoStack().length).toBe(1); expect(arrow.startBinding).toEqual({ elementId: rect1.id, - fixedPoint: expect.arrayContaining([ - 0.5379561888991137, 0.5379561888991137, - ]), + fixedPoint: expect.arrayContaining([0.5001, 0.5001]), mode: "orbit", }); expect(arrow.endBinding).toEqual({ @@ -1640,9 +1636,7 @@ describe("history", () => { expect(API.getRedoStack().length).toBe(0); expect(arrow.startBinding).toEqual({ elementId: rect1.id, - fixedPoint: expect.arrayContaining([ - 0.5379561888991137, 0.5379561888991137, - ]), + fixedPoint: expect.arrayContaining([0.5001, 0.5001]), mode: "orbit", }); expect(arrow.endBinding).toEqual({ @@ -1673,9 +1667,7 @@ describe("history", () => { expect(API.getRedoStack().length).toBe(0); expect(arrow.startBinding).toEqual({ elementId: rect1.id, - fixedPoint: expect.arrayContaining([ - 0.5379561888991137, 0.5379561888991137, - ]), + fixedPoint: expect.arrayContaining([0.5001, 0.5001]), mode: "orbit", }); expect(arrow.endBinding).toEqual({ @@ -1698,9 +1690,7 @@ describe("history", () => { expect(API.getRedoStack().length).toBe(1); expect(arrow.startBinding).toEqual({ elementId: rect1.id, - fixedPoint: expect.arrayContaining([ - 0.5379561888991137, 0.5379561888991137, - ]), + fixedPoint: expect.arrayContaining([0.5001, 0.5001]), mode: "orbit", }); expect(arrow.endBinding).toEqual({ @@ -5061,9 +5051,7 @@ describe("history", () => { id: arrowId, startBinding: expect.objectContaining({ elementId: rect1.id, - fixedPoint: expect.arrayContaining([ - 0.548442798411514, 0.548442798411514, - ]), + fixedPoint: expect.arrayContaining([0.5001, 0.5001]), }), endBinding: expect.objectContaining({ elementId: rect2.id, diff --git a/packages/excalidraw/tests/move.test.tsx b/packages/excalidraw/tests/move.test.tsx index a881bb83e6..659917da8c 100644 --- a/packages/excalidraw/tests/move.test.tsx +++ b/packages/excalidraw/tests/move.test.tsx @@ -110,11 +110,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([200, 0]); - expect([[arrow.x, arrow.y]]).toCloselyEqualPoints([[110, -4.576537]], 0); - expect([[arrow.width, arrow.height]]).toCloselyEqualPoints( - [[79, 132.89433]], - 0, - ); + expect([[arrow.x, arrow.y]]).toCloselyEqualPoints([[111, 51]], 0); + expect([[arrow.width, arrow.height]]).toCloselyEqualPoints([[78, 78]], 0); renderInteractiveScene.mockClear(); renderStaticScene.mockClear(); @@ -132,10 +129,10 @@ 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([[111, 6.1499]], 0); + expect([[arrow.x, arrow.y]]).toCloselyEqualPoints([[111, 51]], 0); expect([[arrow.width, arrow.height]]).toCloselyEqualPoints( [[79, 124.1678]], - 0, + 2, ); h.elements.forEach((element) => expect(element).toMatchSnapshot());