From 50749b8119e6ab677626c1bdec3e42f8a87e8136 Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Wed, 25 Mar 2026 18:59:45 +0000 Subject: [PATCH] fix: Jump inside Signed-off-by: Mark Tolmacs --- packages/element/src/binding.ts | 80 ++++++++++++------- packages/element/src/linearElementEditor.ts | 46 ++++++----- .../regressionTests.test.tsx.snap | 4 + 3 files changed, 80 insertions(+), 50 deletions(-) diff --git a/packages/element/src/binding.ts b/packages/element/src/binding.ts index 3cbaea93e5..480a0f1102 100644 --- a/packages/element/src/binding.ts +++ b/packages/element/src/binding.ts @@ -694,6 +694,7 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = ( localPoint, elementsMap, ); + const hit = getHoveredElementForBinding( globalPoint, elements, @@ -749,7 +750,8 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = ( : // NOTE: Can only affect the start point because new arrows always drag the end point opts?.newArrow ? appState.selectedLinearElement!.initialState.origin! - : LinearElementEditor.getPointAtIndexGlobalCoordinates( + : appState.selectedLinearElement?.initialState.altFocusPoint || + LinearElementEditor.getPointAtIndexGlobalCoordinates( arrow, 0, elementsMap, @@ -760,7 +762,8 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = ( element: hit, focusPoint: endDragged ? globalPoint - : LinearElementEditor.getPointAtIndexGlobalCoordinates( + : appState.selectedLinearElement?.initialState.altFocusPoint || + LinearElementEditor.getPointAtIndexGlobalCoordinates( arrow, -1, elementsMap, @@ -833,41 +836,56 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = ( overrideShouldTestInside: true, }); const otherPointWasInsideAtStart = - !appState.selectedLinearElement?.initialState.altFocusPoint || - pointsEqual( - appState.selectedLinearElement.initialState.altFocusPoint, - otherEndpoint, - ); + appState.selectedLinearElement?.initialState + .arrowOtherEndpointInitialBinding?.mode === "inside"; const otherNeverOverride = opts?.newArrow ? appState.selectedLinearElement?.initialState.arrowStartIsInside : otherBinding?.mode === "inside" && otherPointWasInsideAtStart; - const other: BindingStrategy = !otherNeverOverride - ? otherBindableElement && + + let other: BindingStrategy = { mode: undefined }; + if (!otherNeverOverride) { + if ( + otherBinding?.mode === "inside" && + !otherPointWasInsideAtStart && + otherBindableElement + ) { + other = { + mode: "orbit", + element: otherBindableElement, + focusPoint: getGlobalFixedPointForBindableElement( + otherBinding.fixedPoint, + otherBindableElement, + elementsMap, + ), + }; + } else if ( + otherBindableElement && !otherFocusPointIsInElement && !pointIsCloseToOtherElement && appState.selectedLinearElement?.initialState.altFocusPoint - ? { - mode: "orbit", - element: otherBindableElement, - focusPoint: appState.selectedLinearElement.initialState.altFocusPoint, - } - : opts?.angleLocked && otherBindableElement - ? { - mode: "orbit", - element: otherBindableElement, - focusPoint: - projectFixedPointOntoDiagonal( - arrow, - otherEndpoint, - otherBindableElement, - startDragged ? "end" : "start", - elementsMap, - appState.zoom, - appState.isMidpointSnappingEnabled, - ) || otherEndpoint, - } - : { mode: undefined } - : { mode: undefined }; + ) { + other = { + mode: "orbit", + element: otherBindableElement, + focusPoint: appState.selectedLinearElement.initialState.altFocusPoint, + }; + } else if (opts?.angleLocked && otherBindableElement) { + other = { + mode: "orbit", + element: otherBindableElement, + focusPoint: + projectFixedPointOntoDiagonal( + arrow, + otherEndpoint, + otherBindableElement, + startDragged ? "end" : "start", + elementsMap, + appState.zoom, + appState.isMidpointSnappingEnabled, + ) || otherEndpoint, + }; + } + } return { start: startDragged ? current : other, diff --git a/packages/element/src/linearElementEditor.ts b/packages/element/src/linearElementEditor.ts index 8af8ecd858..781825bb52 100644 --- a/packages/element/src/linearElementEditor.ts +++ b/packages/element/src/linearElementEditor.ts @@ -141,6 +141,7 @@ export class LinearElementEditor { }; arrowStartIsInside: boolean; altFocusPoint: Readonly | null; + arrowOtherEndpointInitialBinding: FixedPointBinding | null; }>; /** whether you're dragging a point */ @@ -193,6 +194,7 @@ export class LinearElementEditor { added: false, }, arrowStartIsInside: false, + arrowOtherEndpointInitialBinding: null, altFocusPoint: null, }; this.hoverPointIndex = -1; @@ -764,6 +766,7 @@ export class LinearElementEditor { ...editingLinearElement.initialState, origin: null, arrowStartIsInside: false, + arrowOtherEndpointInitialBinding: null, }, }; } @@ -1085,6 +1088,10 @@ export class LinearElementEditor { !!app.state.newElement && (app.state.bindMode === "inside" || app.state.bindMode === "skip"), altFocusPoint: null, + arrowOtherEndpointInitialBinding: + linearElementEditor.selectedPointsIndices?.includes(0) + ? element.startBinding + : element.endBinding, }, selectedPointsIndices: [element.points.length - 1], lastUncommittedPoint: null, @@ -1147,6 +1154,10 @@ export class LinearElementEditor { !!app.state.newElement && (app.state.bindMode === "inside" || app.state.bindMode === "skip"), altFocusPoint: null, + arrowOtherEndpointInitialBinding: + linearElementEditor.selectedPointsIndices?.includes(0) + ? element.startBinding + : element.endBinding, }, selectedPointsIndices: nextSelectedPointsIndices, pointerOffset: targetPoint @@ -2409,22 +2420,21 @@ const pointDraggingUpdates = ( )! as ExcalidrawBindableElement) : null; - const endLocalPoint = startIsDraggingOverEndElement - ? nextArrow.points[nextArrow.points.length - 1] - : endIsDraggingOverStartElement && - app.state.bindMode !== "inside" && - getFeatureFlag("COMPLEX_BINDINGS") - ? nextArrow.points[0] - : endBindable - ? updateBoundPoint( - nextArrow, - "endBinding", - nextArrow.endBinding, - endBindable, - elementsMap, - endIsDragged, - ) || nextArrow.points[nextArrow.points.length - 1] - : nextArrow.points[nextArrow.points.length - 1]; + const endLocalPoint = + endIsDraggingOverStartElement && + app.state.bindMode !== "inside" && + getFeatureFlag("COMPLEX_BINDINGS") + ? nextArrow.points[0] + : endBindable + ? updateBoundPoint( + nextArrow, + "endBinding", + nextArrow.endBinding, + endBindable, + elementsMap, + endIsDragged, + ) || nextArrow.points[nextArrow.points.length - 1] + : nextArrow.points[nextArrow.points.length - 1]; // We need to keep the simulated next arrow up-to-date, because // updateBoundPoint looks at the opposite point @@ -2458,13 +2468,11 @@ const pointDraggingUpdates = ( : nextArrow.points[0]; const endChanged = - !startIsDraggingOverEndElement && !( endIsDraggingOverStartElement && app.state.bindMode !== "inside" && getFeatureFlag("COMPLEX_BINDINGS") - ) && - !!endBindable; + ) && !!endBindable; const startChanged = pointDistance(startLocalPoint, nextArrow.points[0]) !== 0; diff --git a/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap index e97991d96f..a2a765b22a 100644 --- a/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap @@ -8663,6 +8663,7 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1` "hoveredFocusPointBinding": null, "initialState": { "altFocusPoint": null, + "arrowOtherEndpointInitialBinding": null, "arrowStartIsInside": false, "lastClickedPoint": -1, "origin": null, @@ -8895,6 +8896,7 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`] "hoveredFocusPointBinding": null, "initialState": { "altFocusPoint": null, + "arrowOtherEndpointInitialBinding": null, "arrowStartIsInside": false, "lastClickedPoint": -1, "origin": null, @@ -9317,6 +9319,7 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1` "hoveredFocusPointBinding": null, "initialState": { "altFocusPoint": null, + "arrowOtherEndpointInitialBinding": null, "arrowStartIsInside": false, "lastClickedPoint": -1, "origin": null, @@ -9729,6 +9732,7 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`] "hoveredFocusPointBinding": null, "initialState": { "altFocusPoint": null, + "arrowOtherEndpointInitialBinding": null, "arrowStartIsInside": false, "lastClickedPoint": -1, "origin": null,