From 8c9666b8abcbcb99af7bc700c4d46691923753b9 Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Tue, 4 Mar 2025 20:08:44 +0100 Subject: [PATCH] Multipoint arrows now have single point commit in binding zones --- packages/element/src/linearElementEditor.ts | 31 +++++++-- .../excalidraw/actions/actionFinalize.tsx | 22 ++++++- packages/excalidraw/components/App.tsx | 64 ++++++++++++++----- 3 files changed, 92 insertions(+), 25 deletions(-) diff --git a/packages/element/src/linearElementEditor.ts b/packages/element/src/linearElementEditor.ts index 8c01ebab2f..e2f145427c 100644 --- a/packages/element/src/linearElementEditor.ts +++ b/packages/element/src/linearElementEditor.ts @@ -239,24 +239,23 @@ export class LinearElementEditor { }); } - static getOutlineAvoidingPoint( + static getOutlineAvoidingPointOrNull( element: NonDeleted, coords: { x: number; y: number }, pointIndex: number, app: AppClassProperties, - ): GlobalPoint { - const elbowed = isElbowArrow(element); + ) { const hoveredElement = getHoveredElementForBinding( coords, app.scene.getNonDeletedElements(), app.scene.getNonDeletedElementsMap(), app.state.zoom, true, - elbowed, + isElbowArrow(element), ); - const p = pointFrom(coords.x, coords.y); if (hoveredElement) { + const p = pointFrom(coords.x, coords.y); const newPoints = Array.from(element.points); newPoints[pointIndex] = pointFrom( p[0] - element.x, @@ -274,7 +273,27 @@ export class LinearElementEditor { ); } - return p; + return null; + } + + static getOutlineAvoidingPoint( + element: NonDeleted, + coords: { x: number; y: number }, + pointIndex: number, + app: AppClassProperties, + ): GlobalPoint { + const p = LinearElementEditor.getOutlineAvoidingPointOrNull( + element, + coords, + pointIndex, + app, + ); + + if (p) { + return p; + } + + return pointFrom(coords.x, coords.y); } /** diff --git a/packages/excalidraw/actions/actionFinalize.tsx b/packages/excalidraw/actions/actionFinalize.tsx index 9849616562..0da63a29e6 100644 --- a/packages/excalidraw/actions/actionFinalize.tsx +++ b/packages/excalidraw/actions/actionFinalize.tsx @@ -91,10 +91,26 @@ export const actionFinalize = register({ multiPointElement.type !== "freedraw" && appState.lastPointerDownWith !== "touch" ) { - const { points, lastCommittedPoint } = multiPointElement; + const { x: rx, y: ry, points, lastCommittedPoint } = multiPointElement; + const lastGlobalPoint = pointFrom( + rx + points[points.length - 1][0], + ry + points[points.length - 1][1], + ); + const hoveredElementForBinding = getHoveredElementForBinding( + { + x: lastGlobalPoint[0], + y: lastGlobalPoint[1], + }, + elements, + elementsMap, + app.state.zoom, + true, + isElbowArrow(multiPointElement), + ); if ( - !lastCommittedPoint || - points[points.length - 1] !== lastCommittedPoint + !hoveredElementForBinding && + (!lastCommittedPoint || + points[points.length - 1] !== lastCommittedPoint) ) { mutateElement(multiPointElement, { points: multiPointElement.points.slice(0, -1), diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 3bf3abb76e..c615966c14 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -5991,17 +5991,33 @@ class App extends React.Component { if (isPathALoop(points, this.state.zoom.value)) { setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER); } + + const outlineGlobalPoint = + LinearElementEditor.getOutlineAvoidingPointOrNull( + multiElement, + { + x: scenePointerX, + y: scenePointerY, + }, + multiElement.points.length - 1, + this, + ); + + const nextPoint = outlineGlobalPoint + ? pointFrom( + outlineGlobalPoint[0] - rx, + outlineGlobalPoint[1] - ry, + ) + : pointFrom( + lastCommittedX + dxFromLastCommitted, + lastCommittedY + dyFromLastCommitted, + ); + // update last uncommitted point mutateElement( multiElement, { - points: [ - ...points.slice(0, -1), - pointFrom( - lastCommittedX + dxFromLastCommitted, - lastCommittedY + dyFromLastCommitted, - ), - ], + points: [...points.slice(0, -1), nextPoint], }, false, { @@ -7751,18 +7767,34 @@ class App extends React.Component { } const { x: rx, y: ry, lastCommittedPoint } = multiElement; + const lastGlobalPoint = pointFrom( + rx + multiElement.points[multiElement.points.length - 1][0], + ry + multiElement.points[multiElement.points.length - 1][1], + ); + const hoveredElementForBinding = getHoveredElementForBinding( + { + x: lastGlobalPoint[0], + y: lastGlobalPoint[1], + }, + this.scene.getNonDeletedElements(), + this.scene.getNonDeletedElementsMap(), + this.state.zoom, + true, + isElbowArrow(multiElement), + ); // clicking inside commit zone → finalize arrow if ( - multiElement.points.length > 1 && - lastCommittedPoint && - pointDistance( - pointFrom( - pointerDownState.origin.x - rx, - pointerDownState.origin.y - ry, - ), - lastCommittedPoint, - ) < LINE_CONFIRM_THRESHOLD + !!hoveredElementForBinding || + (multiElement.points.length > 1 && + lastCommittedPoint && + pointDistance( + pointFrom( + pointerDownState.origin.x - rx, + pointerDownState.origin.y - ry, + ), + lastCommittedPoint, + ) < LINE_CONFIRM_THRESHOLD) ) { this.actionManager.executeAction(actionFinalize); return;