From 1739fa92b10bb54b514517d0f9c1f1043b779da9 Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Tue, 15 Jul 2025 19:07:51 +0200 Subject: [PATCH] Turn off inside binding mode after leaving a shape --- packages/element/src/binding.ts | 2 +- packages/element/src/linearElementEditor.ts | 2 +- .../excalidraw/actions/actionProperties.tsx | 4 +- packages/excalidraw/appState.ts | 2 +- packages/excalidraw/components/App.tsx | 139 ++++++++++-------- packages/excalidraw/types.ts | 2 +- 6 files changed, 81 insertions(+), 70 deletions(-) diff --git a/packages/element/src/binding.ts b/packages/element/src/binding.ts index 1506274648..79bdb84299 100644 --- a/packages/element/src/binding.ts +++ b/packages/element/src/binding.ts @@ -247,7 +247,7 @@ const bindingStrategyForEndpointDragging = ( // If the global bind mode is in free binding mode, just bind // where the pointer is and keep the other end intact - if (globalBindMode === "fixed") { + if (globalBindMode === "inside") { current = hovered ? { element: hovered, mode: hit ? "inside" : "outside" } : { mode: undefined }; diff --git a/packages/element/src/linearElementEditor.ts b/packages/element/src/linearElementEditor.ts index 3b5fe147ef..2b3fa0605f 100644 --- a/packages/element/src/linearElementEditor.ts +++ b/packages/element/src/linearElementEditor.ts @@ -1992,7 +1992,7 @@ const pointDraggingUpdates = ( isBindingEnabled(appState) && isArrowElement(element) && hoveredElement && - appState.bindMode === "focus" && + appState.bindMode === "orbit" && !otherPointInsideElement ) { newGlobalPointPosition = getOutlineAvoidingPoint( diff --git a/packages/excalidraw/actions/actionProperties.tsx b/packages/excalidraw/actions/actionProperties.tsx index 7561b65a82..7502404d70 100644 --- a/packages/excalidraw/actions/actionProperties.tsx +++ b/packages/excalidraw/actions/actionProperties.tsx @@ -1720,7 +1720,7 @@ export const actionChangeArrowType = register({ bindBindingElement( newElement, startElement, - appState.bindMode === "fixed" ? "inside" : "orbit", + appState.bindMode === "inside" ? "inside" : "orbit", "start", app.scene, ); @@ -1734,7 +1734,7 @@ export const actionChangeArrowType = register({ bindBindingElement( newElement, endElement, - appState.bindMode === "fixed" ? "inside" : "orbit", + appState.bindMode === "inside" ? "inside" : "orbit", "end", app.scene, ); diff --git a/packages/excalidraw/appState.ts b/packages/excalidraw/appState.ts index 8267564dee..a16c1ff30b 100644 --- a/packages/excalidraw/appState.ts +++ b/packages/excalidraw/appState.ts @@ -124,7 +124,7 @@ export const getDefaultAppState = (): Omit< searchMatches: null, lockedMultiSelections: {}, activeLockedId: null, - bindMode: "focus", + bindMode: "orbit", }; }; diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 4032afb378..d9f0246fd5 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -4326,7 +4326,7 @@ class App extends React.Component { } // Handle Alt key for bind mode - if (event.key === KEYS.ALT && this.state.bindMode === "focus") { + if (event.key === KEYS.ALT && this.state.bindMode === "orbit") { // Cancel any pending bind mode timer if (this.bindModeHandler) { clearTimeout(this.bindModeHandler); @@ -4647,7 +4647,7 @@ class App extends React.Component { ) { // Handle Alt key release for bind mode this.setState({ - bindMode: "focus", + bindMode: "orbit", }); // Restart the timer if we're creating/editing a linear element and hovering over an element @@ -4670,9 +4670,9 @@ class App extends React.Component { if (hoveredElement && !this.bindModeHandler) { this.bindModeHandler = setTimeout(() => { if (hoveredElement) { - // this.setState({ - // bindMode: "fixed", - // }); + this.setState({ + bindMode: "inside", + }); } else { this.bindModeHandler = null; } @@ -5999,10 +5999,7 @@ class App extends React.Component { if (arrowWithoutUncommittedPoint || this.state.newElement) { // Timed bind mode handler for arrow elements - if ( - isArrowElement(this.state.newElement) && - this.state.bindMode === "focus" - ) { + if (isArrowElement(this.state.newElement)) { const hoveredElement = getHoveredElementForBinding( pointFrom(scenePointer.x, scenePointer.y), this.scene.getNonDeletedElements(), @@ -6010,48 +6007,56 @@ class App extends React.Component { this.state.zoom, ); - if (this.bindModeHandler && !hoveredElement) { - clearTimeout(this.bindModeHandler); - this.bindModeHandler = null; - } else if (!this.bindModeHandler && hoveredElement) { - this.bindModeHandler = setTimeout(() => { - if (hoveredElement) { - flushSync(() => { - // this.setState({ - // bindMode: "fixed", - // }); - }); + if (this.state.bindMode === "orbit") { + if (this.bindModeHandler && !hoveredElement) { + clearTimeout(this.bindModeHandler); + this.bindModeHandler = null; + } else if (!this.bindModeHandler && hoveredElement) { + this.bindModeHandler = setTimeout(() => { + if (hoveredElement) { + flushSync(() => { + this.setState({ + bindMode: "inside", + }); + }); - if (isArrowElement(this.state.newElement)) { - const lastSceneCoords = viewportCoordsToSceneCoords( - { - clientX: this.lastPointerMoveEvent?.clientX ?? 0, - clientY: this.lastPointerMoveEvent?.clientY ?? 0, - }, - this.state, - ); - this.scene.mutateElement( - this.state.newElement, - { - points: [ - ...this.state.newElement.points.slice(0, -1), - LinearElementEditor.pointFromAbsoluteCoords( - this.state.newElement, - pointFrom( - lastSceneCoords.x, - lastSceneCoords.y, + if (isArrowElement(this.state.newElement)) { + const lastSceneCoords = viewportCoordsToSceneCoords( + { + clientX: this.lastPointerMoveEvent?.clientX ?? 0, + clientY: this.lastPointerMoveEvent?.clientY ?? 0, + }, + this.state, + ); + this.scene.mutateElement( + this.state.newElement, + { + points: [ + ...this.state.newElement.points.slice(0, -1), + LinearElementEditor.pointFromAbsoluteCoords( + this.state.newElement, + pointFrom( + lastSceneCoords.x, + lastSceneCoords.y, + ), + this.scene.getNonDeletedElementsMap(), ), - this.scene.getNonDeletedElementsMap(), - ), - ], - }, - { informMutation: false, isDragging: false }, - ); + ], + }, + { informMutation: false, isDragging: false }, + ); + } + } else { + this.bindModeHandler = null; } - } else { - this.bindModeHandler = null; - } - }, BIND_MODE_TIMEOUT); + }, BIND_MODE_TIMEOUT); + } + } else if (!hoveredElement) { + flushSync(() => { + this.setState({ + bindMode: "orbit", + }); + }); } } @@ -6135,7 +6140,7 @@ class App extends React.Component { if ( isArrowElement(multiElement) && - this.state.bindMode === "focus" && + this.state.bindMode === "orbit" && isBindingEnabled(this.state) ) { const hoveredElement = getHoveredElementForBinding( @@ -6923,13 +6928,13 @@ class App extends React.Component { this.lastPointerUpEvent = event; // Cancel any pending timeout for bind mode change - if (this.state.bindMode === "fixed" || this.state.bindMode === "skip") { + if (this.state.bindMode === "inside" || this.state.bindMode === "skip") { if (this.bindModeHandler) { clearTimeout(this.bindModeHandler); } this.bindModeHandler = null; this.setState({ - bindMode: "focus", + bindMode: "orbit", }); } @@ -8579,15 +8584,15 @@ class App extends React.Component { return; } - // Timed bind mode handler for arrow elements - if (this.state.bindMode === "focus") { - const hoveredElement = getHoveredElementForBinding( - pointFrom(pointerCoords.x, pointerCoords.y), - this.scene.getNonDeletedElements(), - elementsMap, - this.state.zoom, - ); + const hoveredElement = getHoveredElementForBinding( + pointFrom(pointerCoords.x, pointerCoords.y), + this.scene.getNonDeletedElements(), + elementsMap, + this.state.zoom, + ); + // Timed bind mode handler for arrow elements + if (this.state.bindMode === "orbit") { if (this.bindModeHandler && !hoveredElement) { clearTimeout(this.bindModeHandler); this.bindModeHandler = null; @@ -8595,9 +8600,9 @@ class App extends React.Component { this.bindModeHandler = setTimeout(() => { if (hoveredElement) { flushSync(() => { - // this.setState({ - // bindMode: "fixed", - // }); + this.setState({ + bindMode: "inside", + }); }); const newState = LinearElementEditor.handlePointDragging( event, @@ -8620,6 +8625,12 @@ class App extends React.Component { } }, BIND_MODE_TIMEOUT); } + } else if (!hoveredElement) { + flushSync(() => { + this.setState({ + bindMode: "orbit", + }); + }); } const newState = LinearElementEditor.handlePointDragging( @@ -9104,7 +9115,7 @@ class App extends React.Component { if (!arrowEndpointsAboutToBindToSameElement) { const [targetPointX, targetPointY] = - this.state.bindMode === "focus" && isBindingEnabled(this.state) + this.state.bindMode === "orbit" && isBindingEnabled(this.state) ? getOutlineAvoidingPoint( newElement, hoveredElement, @@ -9462,7 +9473,7 @@ class App extends React.Component { this.setState({ selectedElementsAreBeingDragged: false, - bindMode: "focus", + bindMode: "orbit", }); if ( diff --git a/packages/excalidraw/types.ts b/packages/excalidraw/types.ts index 11f1ce46db..9487cfdbc0 100644 --- a/packages/excalidraw/types.ts +++ b/packages/excalidraw/types.ts @@ -444,7 +444,7 @@ export interface AppState { // as elements are unlocked, we remove the groupId from the elements // and also remove groupId from this map lockedMultiSelections: { [groupId: string]: true }; - bindMode: "focus" | "fixed" | "skip"; + bindMode: "orbit" | "inside" | "skip"; } export type SearchMatch = {