From ab6353b2faa7799928031dc7a5b42684b8aee5ff Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Thu, 28 Aug 2025 11:49:14 +0200 Subject: [PATCH] chore: More point dragging centralization --- packages/element/src/linearElementEditor.ts | 221 +++++++++++--------- packages/excalidraw/components/App.tsx | 159 +++++--------- 2 files changed, 174 insertions(+), 206 deletions(-) diff --git a/packages/element/src/linearElementEditor.ts b/packages/element/src/linearElementEditor.ts index 649104da53..6cad067034 100644 --- a/packages/element/src/linearElementEditor.ts +++ b/packages/element/src/linearElementEditor.ts @@ -43,6 +43,7 @@ import type { } from "@excalidraw/excalidraw/types"; import { + calculateFixedPointForNonElbowArrowBinding, getBindingStrategyForDraggingBindingElementEndpoints, getStartGlobalEndLocalPointsForSimpleArrowBinding, maybeSuggestBindingsForBindingElementAtCoords, @@ -117,6 +118,12 @@ const getNormalizedPoints = ({ }; }; +type PointMoveOtherUpdates = { + startBinding?: FixedPointBinding | null; + endBinding?: FixedPointBinding | null; + moveMidPointsWithElement?: boolean | null; +}; + export class LinearElementEditor { public readonly elementId: ExcalidrawElement["id"] & { _brand: "excalidrawLinearElementId"; @@ -282,7 +289,7 @@ export class LinearElementEditor { scenePointerX: number, scenePointerY: number, linearElementEditor: LinearElementEditor, - ): Pick | null { + ): Pick | null { if (!linearElementEditor) { return null; } @@ -356,94 +363,48 @@ export class LinearElementEditor { const deltaX = reference[0] - draggingPoint[0]; const deltaY = reference[1] - draggingPoint[1]; - LinearElementEditor.movePoints( + const { positions, updates } = pointDraggingUpdates( + selectedPointsIndices, + deltaX, + deltaY, + elementsMap, element, - app.scene, - pointDraggingUpdates( - selectedPointsIndices, - deltaX, - deltaY, - elementsMap, - element, - elements, - app, - ), + elements, + app, ); - } else { - const scenePointer = pointFrom( + + LinearElementEditor.movePoints(element, app.scene, positions, updates); + } else if ( + shouldAllowDraggingPoint( + element, scenePointerX, scenePointerY, + selectedPointsIndices, + elementsMap, + app, + ) + ) { + const newDraggingPointPosition = LinearElementEditor.createPointAt( + element, + elementsMap, + scenePointerX - linearElementEditor.pointerOffset.x, + scenePointerY - linearElementEditor.pointerOffset.y, + event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(), ); - // Do not allow dragging the bound arrow closer to the shape than - // the dragging threshold - let allowDrag = true; - if (isSimpleArrow(element)) { - if (selectedPointsIndices.includes(0) && element.startBinding) { - const boundElement = elementsMap.get( - element.startBinding.elementId, - )!; - const dist = distanceToElement( - boundElement, - elementsMap, - scenePointer, - ); - const inside = isPointInElement( - scenePointer, - boundElement, - elementsMap, - ); - allowDrag = allowDrag && (dist > DRAGGING_THRESHOLD || inside); - if (allowDrag) { - unbindBindingElement(element, "start", app.scene); - } - } - if ( - selectedPointsIndices.includes(element.points.length - 1) && - element.endBinding - ) { - const boundElement = elementsMap.get(element.endBinding.elementId)!; - const dist = distanceToElement( - boundElement, - elementsMap, - scenePointer, - ); - const inside = isPointInElement( - scenePointer, - boundElement, - elementsMap, - ); - allowDrag = allowDrag && (dist > DRAGGING_THRESHOLD || inside); - if (allowDrag) { - unbindBindingElement(element, "end", app.scene); - } - } - } + const deltaX = newDraggingPointPosition[0] - draggingPoint[0]; + const deltaY = newDraggingPointPosition[1] - draggingPoint[1]; - if (allowDrag) { - const newDraggingPointPosition = LinearElementEditor.createPointAt( - element, - elementsMap, - scenePointerX - linearElementEditor.pointerOffset.x, - scenePointerY - linearElementEditor.pointerOffset.y, - event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(), - ); - const deltaX = newDraggingPointPosition[0] - draggingPoint[0]; - const deltaY = newDraggingPointPosition[1] - draggingPoint[1]; + const { positions, updates } = pointDraggingUpdates( + selectedPointsIndices, + deltaX, + deltaY, + elementsMap, + element, + elements, + app, + ); - LinearElementEditor.movePoints( - element, - app.scene, - pointDraggingUpdates( - selectedPointsIndices, - deltaX, - deltaY, - elementsMap, - element, - elements, - app, - ), - ); - } + LinearElementEditor.movePoints(element, app.scene, positions, updates); } const boundTextElement = getBoundTextElement(element, elementsMap); @@ -1987,7 +1948,10 @@ export const pointDraggingUpdates = ( element: NonDeleted, elements: readonly Ordered[], app: AppClassProperties, -): PointsPositionUpdates => { +): { + positions: PointsPositionUpdates; + updates?: PointMoveOtherUpdates; +} => { const naiveDraggingPoints = new Map( selectedPointsIndices.map((pointIndex) => { return [ @@ -2005,7 +1969,9 @@ export const pointDraggingUpdates = ( // Linear elements have no special logic if (!isArrowElement(element) || isElbowArrow(element)) { - return naiveDraggingPoints; + return { + positions: naiveDraggingPoints, + }; } const startIsDragged = selectedPointsIndices.includes(0); @@ -2014,7 +1980,9 @@ export const pointDraggingUpdates = ( ); if (startIsDragged === endIsDragged) { - return naiveDraggingPoints; + return { + positions: naiveDraggingPoints, + }; } const { start, end } = getBindingStrategyForDraggingBindingElementEndpoints( @@ -2090,16 +2058,75 @@ export const pointDraggingUpdates = ( new Set([0, element.points.length - 1, ...selectedPointsIndices]), ); - return new Map( - indices.map((idx) => { - return [ - idx, - idx === 0 - ? { point: startLocalPoint, isDragging: true } - : idx === element.points.length - 1 - ? { point: finalEndLocalPoint, isDragging: true } - : naiveDraggingPoints.get(idx)!, - ]; - }), - ); + return { + updates: start.mode + ? { + startBinding: { + elementId: start.element.id, + mode: start.mode, + ...calculateFixedPointForNonElbowArrowBinding( + element, + start.element, + "start", + elementsMap, + ), + }, + } + : undefined, + positions: new Map( + indices.map((idx) => { + return [ + idx, + idx === 0 + ? { point: startLocalPoint, isDragging: true } + : idx === element.points.length - 1 + ? { point: finalEndLocalPoint, isDragging: true } + : naiveDraggingPoints.get(idx)!, + ]; + }), + ), + }; +}; + +const shouldAllowDraggingPoint = ( + element: ExcalidrawLinearElement, + scenePointerX: number, + scenePointerY: number, + selectedPointsIndices: readonly number[], + elementsMap: Readonly, + app: AppClassProperties, +) => { + if (!isSimpleArrow(element)) { + return true; + } + + const scenePointer = pointFrom(scenePointerX, scenePointerY); + // Do not allow dragging the bound arrow closer to the shape than + // the dragging threshold + let allowDrag = true; + if (isSimpleArrow(element)) { + if (selectedPointsIndices.includes(0) && element.startBinding) { + const boundElement = elementsMap.get(element.startBinding.elementId)!; + const dist = distanceToElement(boundElement, elementsMap, scenePointer); + const inside = isPointInElement(scenePointer, boundElement, elementsMap); + allowDrag = allowDrag && (dist > DRAGGING_THRESHOLD || inside); + if (allowDrag) { + unbindBindingElement(element, "start", app.scene); + } + } + if ( + selectedPointsIndices.includes(element.points.length - 1) && + element.endBinding + ) { + const boundElement = elementsMap.get(element.endBinding.elementId)!; + const dist = distanceToElement(boundElement, elementsMap, scenePointer); + const inside = isPointInElement(scenePointer, boundElement, elementsMap); + allowDrag = allowDrag && (dist > DRAGGING_THRESHOLD || inside); + if (allowDrag) { + unbindBindingElement(element, "end", app.scene); + } + } + } + + return allowDrag; }; diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 4a3252dc3d..5c3b0e8252 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -243,7 +243,6 @@ import { getBindingStrategyForDraggingBindingElementEndpoints, getStartGlobalEndLocalPointsForSimpleArrowBinding, mutateElement, - pointDraggingUpdates, } from "@excalidraw/element"; import type { GlobalPoint, LocalPoint, Radians } from "@excalidraw/math"; @@ -965,34 +964,17 @@ class App extends React.Component { }); }); - const elementsMap = this.scene.getNonDeletedElementsMap(); - const elements = this.scene.getNonDeletedElements(); - const newDraggingPointPosition = LinearElementEditor.createPointAt( - arrow, - elementsMap, + const event = + this.lastPointerMoveEvent ?? this.lastPointerDownEvent?.nativeEvent; + invariant(event, "Last event must exist"); + const newState = LinearElementEditor.handlePointDragging( + event, + this, x - this.state.selectedLinearElement.pointerOffset.x, y - this.state.selectedLinearElement.pointerOffset.y, - this.getEffectiveGridSize(), - ); - const draggingPoint = - arrow.points[ - this.state.selectedLinearElement.pointerDownState.lastClickedPoint - ]; - const deltaX = newDraggingPointPosition[0] - draggingPoint[0]; - const deltaY = newDraggingPointPosition[1] - draggingPoint[1]; - LinearElementEditor.movePoints( - arrow, - this.scene, - pointDraggingUpdates( - startDragged ? [0] : endDragged ? [arrow.points.length - 1] : [], - deltaX, - deltaY, - elementsMap, - arrow, - elements, - this, - ), + this.state.selectedLinearElement, ); + this.setState(newState); } }; @@ -8419,26 +8401,31 @@ class App extends React.Component { this.setState((prevState) => { let linearElementEditor = null; let nextSelectedElementIds = prevState.selectedElementIds; - if (isBindingElement(element)) { - const linearElement = new LinearElementEditor( + if (isLinearElement(element)) { + linearElementEditor = new LinearElementEditor( element, this.scene.getNonDeletedElementsMap(), ); - linearElementEditor = { - ...linearElement, - pointerDownState: { - ...linearElement.pointerDownState, - arrowOriginalStartPoint: pointFrom( - pointerDownState.origin.x, - pointerDownState.origin.y, - ), - }, - selectedPointsIndices: [1], - }; - nextSelectedElementIds = makeNextSelectedElementIds( - { [element.id]: true }, - prevState, - ); + + if (isBindingElement(element)) { + linearElementEditor = { + ...linearElementEditor, + selectedPointsIndices: [1], + pointerDownState: { + ...linearElementEditor.pointerDownState, + lastClickedIsEndPoint: true, + lastClickedPoint: 1, + arrowOriginalStartPoint: pointFrom( + pointerDownState.origin.x, + pointerDownState.origin.y, + ), + }, + }; + nextSelectedElementIds = makeNextSelectedElementIds( + { [element.id]: true }, + prevState, + ); + } } return { @@ -9297,83 +9284,37 @@ class App extends React.Component { pointerDownState.drag.hasOccurred = true; const points = newElement.points; - // Update arrow points - let startBinding = newElement.startBinding; - let startGlobalPoint = - this.state.selectedLinearElement?.pointerDownState - ?.arrowOriginalStartPoint ?? - LinearElementEditor.getPointAtIndexGlobalCoordinates( - newElement, - 0, - elementsMap, - ); - let endLocalPoint = pointFrom( - gridX - newElement.x, - gridY - newElement.y, - ); - - // Simple arrows need both their start and end points adjusted - if (isBindingElement(newElement) && !isElbowArrow(newElement)) { - const point = pointFrom( - pointerCoords.x - newElement.x, - pointerCoords.y - newElement.y, - ); - const { start, end } = - getBindingStrategyForDraggingBindingElementEndpoints( - newElement, - new Map([ - [newElement.points.length - 1, { point, isDragging: true }], - ]), - elementsMap, - this.scene.getNonDeletedElements(), - this.state, - { newArrow: !!this.state.newElement }, - ); - - if (start.mode) { - startBinding = { - elementId: start.element.id, - mode: start.mode, - ...calculateFixedPointForNonElbowArrowBinding( - newElement, - start.element, - "start", - elementsMap, - ), - }; - } - - [startGlobalPoint, endLocalPoint] = - getStartGlobalEndLocalPointsForSimpleArrowBinding( - newElement, - start, - end, - startGlobalPoint, - endLocalPoint, - elementsMap, - ); - } - invariant( points.length > 1, "Do not create linear elements with less than 2 points", ); - if (isElbowArrow(newElement) || points.length === 2) { - this.scene.mutateElement( + let linearElementEditor = this.state.selectedLinearElement; + if (!linearElementEditor) { + linearElementEditor = new LinearElementEditor( newElement, - { - x: startGlobalPoint[0], - y: startGlobalPoint[1], - points: [pointFrom(0, 0), endLocalPoint], - startBinding, - }, - { isDragging: true, informMutation: false }, + this.scene.getNonDeletedElementsMap(), ); + linearElementEditor = { + ...linearElementEditor, + selectedPointsIndices: [1], + pointerDownState: { + ...linearElementEditor.pointerDownState, + lastClickedIsEndPoint: true, + lastClickedPoint: 1, + }, + }; } this.setState({ newElement, + ...LinearElementEditor.handlePointDragging( + event, + this, + gridX, + gridY, + linearElementEditor, + )!, }); if (isBindingElement(newElement, false)) {