From 9af3177dbd30a51bae9718f57ff5e4b9a87ddaf7 Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Fri, 16 May 2025 16:46:56 +0200 Subject: [PATCH] Refactor binding suggestions --- packages/element/src/binding.ts | 45 ++++++++++ packages/element/src/linearElementEditor.ts | 35 +++++--- packages/excalidraw/components/App.tsx | 97 ++++++--------------- 3 files changed, 94 insertions(+), 83 deletions(-) diff --git a/packages/element/src/binding.ts b/packages/element/src/binding.ts index 2ea05510b..07b7559a3 100644 --- a/packages/element/src/binding.ts +++ b/packages/element/src/binding.ts @@ -398,6 +398,51 @@ export const getSuggestedBindingsForArrows = ( ); }; +export const maybeSuggestBindingsForLinearElementAtCoords = ( + linearElement: NonDeleted, + /** scene coords */ + pointerCoords: { + x: number; + y: number; + }[], + scene: Scene, + zoom: AppState["zoom"], + // During line creation the start binding hasn't been written yet + // into `linearElement` + oppositeBindingBoundElement?: ExcalidrawBindableElement | null, +): ExcalidrawBindableElement[] => { + if (!pointerCoords.length) { + return []; + } + + const suggestedBindings = pointerCoords.reduce( + (acc: NonDeleted[], coords) => { + const hoveredBindableElement = getHoveredElementForBinding( + coords, + scene.getNonDeletedElements(), + scene.getNonDeletedElementsMap(), + zoom, + isElbowArrow(linearElement), + isElbowArrow(linearElement), + ); + if ( + hoveredBindableElement != null && + !isLinearElementSimpleAndAlreadyBound( + linearElement, + oppositeBindingBoundElement?.id, + hoveredBindableElement, + ) + ) { + acc.push(hoveredBindableElement); + } + return acc; + }, + [], + ); + + return suggestedBindings; +}; + export const maybeBindLinearElement = ( linearElement: NonDeleted, appState: AppState, diff --git a/packages/element/src/linearElementEditor.ts b/packages/element/src/linearElementEditor.ts index 9e34b5c27..f5a2ccde9 100644 --- a/packages/element/src/linearElementEditor.ts +++ b/packages/element/src/linearElementEditor.ts @@ -39,6 +39,7 @@ import { bindOrUnbindLinearElement, getHoveredElementForBinding, isBindingEnabled, + maybeSuggestBindingsForLinearElementAtCoords, } from "./binding"; import { getElementAbsoluteCoords, @@ -245,18 +246,13 @@ export class LinearElementEditor { app: AppClassProperties, scenePointerX: number, scenePointerY: number, - maybeSuggestBinding: ( - element: NonDeleted, - pointSceneCoords: { x: number; y: number }[], - ) => void, linearElementEditor: LinearElementEditor, - scene: Scene, - ): LinearElementEditor | null { + ): Pick | null { if (!linearElementEditor) { return null; } const { elementId } = linearElementEditor; - const elementsMap = scene.getNonDeletedElementsMap(); + const elementsMap = app.scene.getNonDeletedElementsMap(); const element = LinearElementEditor.getElement(elementId, elementsMap); if (!element) { return null; @@ -309,7 +305,7 @@ export class LinearElementEditor { LinearElementEditor.movePoints( element, - scene, + app.scene, new Map([ [ selectedIndex, @@ -337,7 +333,7 @@ export class LinearElementEditor { LinearElementEditor.movePoints( element, - scene, + app.scene, new Map( selectedPointsIndices.map((pointIndex) => { const newPointPosition: LocalPoint = @@ -369,10 +365,11 @@ export class LinearElementEditor { const boundTextElement = getBoundTextElement(element, elementsMap); if (boundTextElement) { - handleBindTextResize(element, scene, false); + handleBindTextResize(element, app.scene, false); } // suggest bindings for first and last point if selected + let suggestedBindings: ExcalidrawBindableElement[] = []; if (isBindingElement(element, false)) { const coords: { x: number; y: number }[] = []; @@ -404,11 +401,16 @@ export class LinearElementEditor { } if (coords.length) { - maybeSuggestBinding(element, coords); + suggestedBindings = maybeSuggestBindingsForLinearElementAtCoords( + element, + coords, + app.scene, + app.state.zoom, + ); } } - return { + const newLinearElementEditor = { ...linearElementEditor, selectedPointsIndices, segmentMidPointHoveredCoords: @@ -427,6 +429,15 @@ export class LinearElementEditor { : -1, isDragging: true, }; + + return { + ...app.state, + editingLinearElement: app.state.editingLinearElement + ? newLinearElementEditor + : null, + selectedLinearElement: newLinearElementEditor, + suggestedBindings, + }; } return null; diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index b0c43359b..b71f0fcad 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -104,7 +104,11 @@ import { Emitter, } from "@excalidraw/common"; -import { getCommonBounds, getElementAbsoluteCoords } from "@excalidraw/element"; +import { + getCommonBounds, + getElementAbsoluteCoords, + maybeSuggestBindingsForLinearElementAtCoords, +} from "@excalidraw/element"; import { bindOrUnbindLinearElement, @@ -112,7 +116,6 @@ import { fixBindingsAfterDeletion, getHoveredElementForBinding, isBindingEnabled, - isLinearElementSimpleAndAlreadyBound, maybeBindLinearElement, shouldEnableBindingForPointerEvent, updateBoundElements, @@ -299,7 +302,6 @@ import type { ElementUpdate } from "@excalidraw/element"; import type { LocalPoint, Radians } from "@excalidraw/math"; import type { - ExcalidrawBindableElement, ExcalidrawElement, ExcalidrawFreeDrawElement, ExcalidrawGenericElement, @@ -5961,11 +5963,15 @@ class App extends React.Component { // and point const { newElement } = this.state; if (isBindingElement(newElement, false)) { - this.maybeSuggestBindingsForLinearElementAtCoords( - newElement, - [scenePointer], - this.state.startBoundElement, - ); + this.setState({ + suggestedBindings: maybeSuggestBindingsForLinearElementAtCoords( + newElement, + [scenePointer], + this.scene, + this.state.zoom, + this.state.startBoundElement, + ), + }); } else { this.maybeSuggestBindingAtCursor(scenePointer, false); } @@ -8251,31 +8257,19 @@ class App extends React.Component { return; } - const newLinearElementEditor = LinearElementEditor.handlePointDragging( + const newState = LinearElementEditor.handlePointDragging( event, this, pointerCoords.x, pointerCoords.y, - (element, pointsSceneCoords) => { - this.maybeSuggestBindingsForLinearElementAtCoords( - element, - pointsSceneCoords, - ); - }, linearElementEditor, - this.scene, ); - if (newLinearElementEditor) { + if (newState) { pointerDownState.lastCoords.x = pointerCoords.x; pointerDownState.lastCoords.y = pointerCoords.y; pointerDownState.drag.hasOccurred = true; - this.setState({ - editingLinearElement: this.state.editingLinearElement - ? newLinearElementEditor - : null, - selectedLinearElement: newLinearElementEditor, - }); + this.setState(newState); return; } @@ -8754,11 +8748,15 @@ class App extends React.Component { if (isBindingElement(newElement, false)) { // When creating a linear element by dragging - this.maybeSuggestBindingsForLinearElementAtCoords( - newElement, - [pointerCoords], - this.state.startBoundElement, - ); + this.setState({ + suggestedBindings: maybeSuggestBindingsForLinearElementAtCoords( + newElement, + [pointerCoords], + this.scene, + this.state.zoom, + this.state.startBoundElement, + ), + }); } } else { pointerDownState.lastCoords.x = pointerCoords.x; @@ -10285,49 +10283,6 @@ class App extends React.Component { }); }; - private maybeSuggestBindingsForLinearElementAtCoords = ( - linearElement: NonDeleted, - /** scene coords */ - pointerCoords: { - x: number; - y: number; - }[], - // During line creation the start binding hasn't been written yet - // into `linearElement` - oppositeBindingBoundElement?: ExcalidrawBindableElement | null, - ): void => { - if (!pointerCoords.length) { - return; - } - - const suggestedBindings = pointerCoords.reduce( - (acc: NonDeleted[], coords) => { - const hoveredBindableElement = getHoveredElementForBinding( - coords, - this.scene.getNonDeletedElements(), - this.scene.getNonDeletedElementsMap(), - this.state.zoom, - isElbowArrow(linearElement), - isElbowArrow(linearElement), - ); - if ( - hoveredBindableElement != null && - !isLinearElementSimpleAndAlreadyBound( - linearElement, - oppositeBindingBoundElement?.id, - hoveredBindableElement, - ) - ) { - acc.push(hoveredBindableElement); - } - return acc; - }, - [], - ); - - this.setState({ suggestedBindings }); - }; - private clearSelection(hitElement: ExcalidrawElement | null): void { this.setState((prevState) => ({ selectedElementIds: makeNextSelectedElementIds({}, prevState),