From 3068787ac4a183728e461161eb6d19589a6d1130 Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Tue, 8 Apr 2025 13:44:54 +0200 Subject: [PATCH] Move linear element handling out of App.tsx Signed-off-by: Mark Tolmacs --- packages/common/src/utils.ts | 12 +- packages/element/src/binding.ts | 19 +- packages/excalidraw/components/App.tsx | 443 +++-------------------- packages/excalidraw/linear.ts | 478 +++++++++++++++++++++++++ 4 files changed, 532 insertions(+), 420 deletions(-) create mode 100644 packages/excalidraw/linear.ts diff --git a/packages/common/src/utils.ts b/packages/common/src/utils.ts index 8b2e28f7f5..7fa98eb2da 100644 --- a/packages/common/src/utils.ts +++ b/packages/common/src/utils.ts @@ -1,14 +1,7 @@ -import { - average, - type GlobalPoint, - type LocalPoint, - pointTranslate, - vector, -} from "@excalidraw/math"; +import { average } from "@excalidraw/math"; import type { ExcalidrawBindableElement, - ExcalidrawElement, FontFamilyValues, FontString, } from "@excalidraw/element/types"; @@ -1208,6 +1201,3 @@ export const escapeDoubleQuotes = (str: string) => { export const castArray = (value: T | T[]): T[] => Array.isArray(value) ? value : [value]; - -export const toLocalPoint = (p: GlobalPoint, element: ExcalidrawElement) => - pointTranslate(p, vector(-element.x, -element.y)); diff --git a/packages/element/src/binding.ts b/packages/element/src/binding.ts index fde832f767..7ab6a69279 100644 --- a/packages/element/src/binding.ts +++ b/packages/element/src/binding.ts @@ -6,7 +6,6 @@ import { invariant, isDevEnv, isTestEnv, - toLocalPoint, } from "@excalidraw/common"; import { @@ -527,14 +526,18 @@ export const bindLinearElement = ( const points = Array.from(linearElement.points); if (isArrowElement(linearElement)) { - points[edgePointIndex] = toLocalPoint( - bindPointToSnapToElementOutline( - linearElement, - hoveredElement, - startOrEnd, - elementsMap, - ), + const [x, y] = bindPointToSnapToElementOutline( linearElement, + hoveredElement, + startOrEnd, + elementsMap, + ); + points[edgePointIndex] = LinearElementEditor.createPointAt( + linearElement, + elementsMap, + x, + y, + null, ); } diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 01726b158e..1b6dd6100c 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -100,7 +100,6 @@ import { arrayToMap, type EXPORT_IMAGE_TYPES, randomInteger, - toLocalPoint, } from "@excalidraw/common"; import { @@ -114,7 +113,6 @@ import { fixBindingsAfterDeletion, getHoveredElementForBinding, isBindingEnabled, - isLinearElementSimpleAndAlreadyBound, maybeBindLinearElement, shouldEnableBindingForPointerEvent, updateBoundElements, @@ -172,7 +170,6 @@ import { } from "@excalidraw/element/typeChecks"; import { - getLockedLinearCursorAlignSize, getNormalizedDimensions, isElementCompletelyInViewport, isElementInViewport, @@ -307,7 +304,6 @@ import { isNonDeletedElement } from "@excalidraw/element"; import type { GlobalPoint, LocalPoint, Radians } from "@excalidraw/math"; import type { - ExcalidrawBindableElement, ExcalidrawElement, ExcalidrawFreeDrawElement, ExcalidrawGenericElement, @@ -466,6 +462,14 @@ import { isMaybeMermaidDefinition } from "../mermaid"; import { LassoTrail } from "../lasso"; +import { + handleCanvasPointerMoveForLinearElement, + handleDoubleClickForLinearElement, + maybeSuggestBindingsForLinearElementAtCoords, + onPointerMoveFromPointerDownOnLinearElement, + onPointerUpFromPointerDownOnLinearElementHandler, +} from "../linear"; + import { activeConfirmDialogAtom } from "./ActiveConfirmDialog"; import BraveMeasureTextError from "./BraveMeasureTextError"; import { ContextMenu, CONTEXT_MENU_SEPARATOR } from "./ContextMenu"; @@ -5447,75 +5451,16 @@ class App extends React.Component { if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) { if ( - event[KEYS.CTRL_OR_CMD] && - (!this.state.editingLinearElement || - this.state.editingLinearElement.elementId !== - selectedElements[0].id) && - !isElbowArrow(selectedElements[0]) + handleDoubleClickForLinearElement( + this, + this.store, + selectedElements[0], + event, + sceneX, + sceneY, + ) ) { - this.store.shouldCaptureIncrement(); - this.setState({ - editingLinearElement: new LinearElementEditor(selectedElements[0]), - }); return; - } else if ( - this.state.selectedLinearElement && - isElbowArrow(selectedElements[0]) - ) { - const hitCoords = LinearElementEditor.getSegmentMidpointHitCoords( - this.state.selectedLinearElement, - { x: sceneX, y: sceneY }, - this.state, - this.scene.getNonDeletedElementsMap(), - ); - const midPoint = hitCoords - ? LinearElementEditor.getSegmentMidPointIndex( - this.state.selectedLinearElement, - this.state, - hitCoords, - this.scene.getNonDeletedElementsMap(), - ) - : -1; - - if (midPoint && midPoint > -1) { - this.store.shouldCaptureIncrement(); - LinearElementEditor.deleteFixedSegment(selectedElements[0], midPoint); - - const nextCoords = LinearElementEditor.getSegmentMidpointHitCoords( - { - ...this.state.selectedLinearElement, - segmentMidPointHoveredCoords: null, - }, - { x: sceneX, y: sceneY }, - this.state, - this.scene.getNonDeletedElementsMap(), - ); - const nextIndex = nextCoords - ? LinearElementEditor.getSegmentMidPointIndex( - this.state.selectedLinearElement, - this.state, - nextCoords, - this.scene.getNonDeletedElementsMap(), - ) - : null; - - this.setState({ - selectedLinearElement: { - ...this.state.selectedLinearElement, - pointerDownState: { - ...this.state.selectedLinearElement.pointerDownState, - segmentMidpoint: { - index: nextIndex, - value: hitCoords, - added: false, - }, - }, - segmentMidPointHoveredCoords: nextCoords, - }, - }); - - return; - } } } @@ -5896,9 +5841,10 @@ class App extends React.Component { // and point const { newElement } = this.state; if (isBindingElement(newElement, false)) { - this.maybeSuggestBindingsForLinearElementAtCoords( + maybeSuggestBindingsForLinearElementAtCoords( newElement, [scenePointer], + this, this.state.startBoundElement, ); } else { @@ -5908,121 +5854,14 @@ class App extends React.Component { if (this.state.multiElement) { const { multiElement } = this.state; - const { x: rx, y: ry } = multiElement; - - const { points, lastCommittedPoint } = multiElement; - const lastPoint = points[points.length - 1]; - - setCursorForShape(this.interactiveCanvas, this.state); - - if (lastPoint === lastCommittedPoint) { - // if we haven't yet created a temp point and we're beyond commit-zone - // threshold, add a point - if ( - pointDistance( - pointFrom(scenePointerX - rx, scenePointerY - ry), - lastPoint, - ) >= LINE_CONFIRM_THRESHOLD - ) { - mutateElement( - multiElement, - { - points: [ - ...points, - pointFrom(scenePointerX - rx, scenePointerY - ry), - ], - }, - false, - ); - } else { - setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER); - // in this branch, we're inside the commit zone, and no uncommitted - // point exists. Thus do nothing (don't add/remove points). - } - } else if ( - points.length > 2 && - lastCommittedPoint && - pointDistance( - pointFrom(scenePointerX - rx, scenePointerY - ry), - lastCommittedPoint, - ) < LINE_CONFIRM_THRESHOLD - ) { - setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER); - mutateElement( - multiElement, - { - points: points.slice(0, -1), - }, - false, - ); - } else { - const [gridX, gridY] = getGridPoint( - scenePointerX, - scenePointerY, - event[KEYS.CTRL_OR_CMD] || isElbowArrow(multiElement) - ? null - : this.getEffectiveGridSize(), - ); - - const [lastCommittedX, lastCommittedY] = - multiElement?.lastCommittedPoint ?? [0, 0]; - - let dxFromLastCommitted = gridX - rx - lastCommittedX; - let dyFromLastCommitted = gridY - ry - lastCommittedY; - - if (shouldRotateWithDiscreteAngle(event)) { - ({ width: dxFromLastCommitted, height: dyFromLastCommitted } = - getLockedLinearCursorAlignSize( - // actual coordinate of the last committed point - lastCommittedX + rx, - lastCommittedY + ry, - // cursor-grid coordinate - gridX, - gridY, - )); - } - - if (isPathALoop(points, this.state.zoom.value)) { - setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER); - } - // update last uncommitted point - mutateElement( - multiElement, - { - points: [ - ...points.slice(0, -1), - isArrowElement(multiElement) - ? toLocalPoint( - getOutlineAvoidingPoint( - multiElement, - pointFrom(scenePointerX, scenePointerY), - multiElement.points.length - 1, - this.scene, - this.state.zoom, - pointFrom( - multiElement.x + lastCommittedX + dxFromLastCommitted, - multiElement.y + lastCommittedY + dyFromLastCommitted, - ), - ), - multiElement, - ) - : pointFrom( - lastCommittedX + dxFromLastCommitted, - lastCommittedY + dyFromLastCommitted, - ), - ], - }, - false, - { - isDragging: true, - }, - ); - - // in this path, we're mutating multiElement to reflect - // how it will be after adding pointer position as the next point - // trigger update here so that new element canvas renders again to reflect this - this.triggerRender(false); - } + handleCanvasPointerMoveForLinearElement( + multiElement, + this, + scenePointerX, + scenePointerY, + event, + this.triggerRender, + ); return; } @@ -8301,9 +8140,10 @@ class App extends React.Component { pointerCoords.x, pointerCoords.y, (element, pointsSceneCoords) => { - this.maybeSuggestBindingsForLinearElementAtCoords( + maybeSuggestBindingsForLinearElementAtCoords( element, pointsSceneCoords, + this, ); }, linearElementEditor, @@ -8691,120 +8531,14 @@ class App extends React.Component { }); } } else if (isLinearElement(newElement)) { - pointerDownState.drag.hasOccurred = true; - const points = newElement.points; - const [gridX, gridY] = getGridPoint( - pointerCoords.x, - pointerCoords.y, - event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(), - ); - let dx = gridX - newElement.x; - let dy = gridY - newElement.y; - - if (shouldRotateWithDiscreteAngle(event) && points.length === 2) { - ({ width: dx, height: dy } = getLockedLinearCursorAlignSize( - newElement.x, - newElement.y, - pointerCoords.x, - pointerCoords.y, - )); - } - - if (points.length === 1) { - mutateElement( - newElement, - { - points: [ - ...points, - isArrowElement(newElement) - ? toLocalPoint( - getOutlineAvoidingPoint( - newElement, - pointFrom( - pointerCoords.x, - pointerCoords.y, - ), - newElement.points.length - 1, - this.scene, - this.state.zoom, - pointFrom( - newElement.x + dx, - newElement.y + dy, - ), - ), - newElement, - ) - : pointFrom(dx, dy), - ], - }, - false, - ); - } else if ( - points.length === 2 || - (points.length > 1 && isElbowArrow(newElement)) - ) { - mutateElement( - newElement, - { - points: [ - ...points.slice(0, -1), - isArrowElement(newElement) - ? toLocalPoint( - getOutlineAvoidingPoint( - newElement, - pointFrom( - pointerCoords.x, - pointerCoords.y, - ), - newElement.points.length - 1, - this.scene, - this.state.zoom, - pointFrom( - newElement.x + dx, - newElement.y + dy, - ), - ), - newElement, - ) - : pointFrom(dx, dy), - ], - }, - false, - { isDragging: true }, - ); - LinearElementEditor.movePoints(newElement, [ - { - index: 0, - isDragging: false, - point: toLocalPoint( - getOutlineAvoidingPoint( - newElement, - pointFrom( - pointerDownState.origin.x, - pointerDownState.origin.y, - ), - 0, - this.scene, - this.state.zoom, - ), - newElement, - ), - }, - ]); - } - - this.setState({ + onPointerMoveFromPointerDownOnLinearElement( newElement, - }); - - if (isBindingElement(newElement, false)) { - // When creating a linear element by dragging - this.maybeSuggestBindingsForLinearElementAtCoords( - newElement, - [pointerCoords], - this.state.startBoundElement, - ); - } + this, + pointerDownState, + pointerCoords, + event, + elementsMap, + ); } else { pointerDownState.lastCoords.x = pointerCoords.x; pointerDownState.lastCoords.y = pointerCoords.y; @@ -9170,65 +8904,15 @@ class App extends React.Component { } if (isLinearElement(newElement)) { - if (newElement!.points.length > 1) { - this.store.shouldCaptureIncrement(); - } - const pointerCoords = viewportCoordsToSceneCoords( + onPointerUpFromPointerDownOnLinearElementHandler( + newElement, + multiElement, + this, + this.store, + pointerDownState, childEvent, - this.state, + activeTool, ); - - if (!pointerDownState.drag.hasOccurred && newElement && !multiElement) { - mutateElement(newElement, { - points: [ - ...newElement.points, - pointFrom( - pointerCoords.x - newElement.x, - pointerCoords.y - newElement.y, - ), - ], - }); - this.setState({ - multiElement: newElement, - newElement, - }); - } else if (pointerDownState.drag.hasOccurred && !multiElement) { - if ( - isBindingEnabled(this.state) && - isBindingElement(newElement, false) - ) { - maybeBindLinearElement( - newElement, - this.state, - this.scene.getNonDeletedElementsMap(), - this.scene.getNonDeletedElements(), - ); - } - this.setState({ suggestedBindings: [], startBoundElement: null }); - if (!activeTool.locked) { - resetCursor(this.interactiveCanvas); - this.setState((prevState) => ({ - newElement: null, - activeTool: updateActiveTool(this.state, { - type: "selection", - }), - selectedElementIds: makeNextSelectedElementIds( - { - ...prevState.selectedElementIds, - [newElement.id]: true, - }, - prevState, - ), - selectedLinearElement: new LinearElementEditor(newElement), - })); - } else { - this.setState((prevState) => ({ - newElement: null, - })); - } - // so that the scene gets rendered again to display the newly drawn linear as well - this.scene.triggerUpdate(); - } return; } @@ -10294,49 +9978,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), diff --git a/packages/excalidraw/linear.ts b/packages/excalidraw/linear.ts new file mode 100644 index 0000000000..fbbe31a066 --- /dev/null +++ b/packages/excalidraw/linear.ts @@ -0,0 +1,478 @@ +import { + CURSOR_TYPE, + getGridPoint, + KEYS, + LINE_CONFIRM_THRESHOLD, + shouldRotateWithDiscreteAngle, + updateActiveTool, + viewportCoordsToSceneCoords, +} from "@excalidraw/common"; + +import { getLockedLinearCursorAlignSize } from "@excalidraw/element/sizeHelpers"; + +import { + isArrowElement, + isBindingElement, + isElbowArrow, +} from "@excalidraw/element/typeChecks"; +import { + getHoveredElementForBinding, + getOutlineAvoidingPoint, + isBindingEnabled, + isLinearElementSimpleAndAlreadyBound, + maybeBindLinearElement, +} from "@excalidraw/element/binding"; + +import { pointDistance, pointFrom } from "@excalidraw/math"; +import { mutateElement } from "@excalidraw/element/mutateElement"; + +import { LinearElementEditor } from "@excalidraw/element/linearElementEditor"; + +import { isPathALoop } from "@excalidraw/element/shapes"; + +import { makeNextSelectedElementIds } from "@excalidraw/element/selection"; + +import type { GlobalPoint, LocalPoint } from "@excalidraw/math"; + +import type { + ExcalidrawBindableElement, + ExcalidrawLinearElement, + NonDeleted, + NonDeletedSceneElementsMap, +} from "@excalidraw/element/types"; + +import { resetCursor, setCursor, setCursorForShape } from "./cursor"; + +import type App from "./components/App"; + +import type { ActiveTool, PointerDownState } from "./types"; + +/** + * This function is called when the user drags the pointer to create a new linear element. + */ +export function onPointerMoveFromPointerDownOnLinearElement( + newElement: ExcalidrawLinearElement, + app: App, + pointerDownState: PointerDownState, + pointerCoords: { x: number; y: number }, + event: PointerEvent, + elementsMap: NonDeletedSceneElementsMap, +) { + pointerDownState.drag.hasOccurred = true; + const points = newElement.points; + const [gridX, gridY] = getGridPoint( + pointerCoords.x, + pointerCoords.y, + event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(), + ); + let dx = gridX - newElement.x; + let dy = gridY - newElement.y; + + if (shouldRotateWithDiscreteAngle(event) && points.length === 2) { + ({ width: dx, height: dy } = getLockedLinearCursorAlignSize( + newElement.x, + newElement.y, + pointerCoords.x, + pointerCoords.y, + )); + } + + if (points.length === 1) { + let x = newElement.x + dx; + let y = newElement.y + dy; + if (isArrowElement(newElement)) { + [x, y] = getOutlineAvoidingPoint( + newElement, + pointFrom(pointerCoords.x, pointerCoords.y), + newElement.points.length - 1, + app.scene, + app.state.zoom, + pointFrom(newElement.x + dx, newElement.y + dy), + ); + } + + mutateElement( + newElement, + { + points: [ + ...points, + LinearElementEditor.createPointAt( + newElement, + elementsMap, + x, + y, + app.getEffectiveGridSize(), + ), + ], + }, + false, + ); + } else if ( + points.length === 2 || + (points.length > 1 && isElbowArrow(newElement)) + ) { + const targets = [ + { + index: points.length - 1, + isDragging: true, + point: pointFrom(dx, dy), + }, + ]; + + if (isArrowElement(newElement)) { + const [x, y] = getOutlineAvoidingPoint( + newElement, + pointFrom( + pointerDownState.origin.x, + pointerDownState.origin.y, + ), + 0, + app.scene, + app.state.zoom, + ); + targets.unshift({ + index: 0, + isDragging: false, + point: LinearElementEditor.createPointAt( + newElement, + elementsMap, + x, + y, + app.getEffectiveGridSize(), + ), + }); + } + + LinearElementEditor.movePoints(newElement, targets); + } + + app.setState({ + newElement, + }); + + if (isBindingElement(newElement, false)) { + // When creating a linear element by dragging + maybeSuggestBindingsForLinearElementAtCoords( + newElement, + [pointerCoords], + app, + app.state.startBoundElement, + ); + } +} + +/** + * + */ +export function handleCanvasPointerMoveForLinearElement( + multiElement: NonDeleted, + app: App, + scenePointerX: number, + scenePointerY: number, + event: React.PointerEvent, + triggerRender: (forceUpdate?: boolean) => void, +) { + const { x: rx, y: ry } = multiElement; + + const { points, lastCommittedPoint } = multiElement; + const lastPoint = points[points.length - 1]; + + setCursorForShape(app.interactiveCanvas, app.state); + + if (lastPoint === lastCommittedPoint) { + // if we haven't yet created a temp point and we're beyond commit-zone + // threshold, add a point + if ( + pointDistance( + pointFrom(scenePointerX - rx, scenePointerY - ry), + lastPoint, + ) >= LINE_CONFIRM_THRESHOLD + ) { + mutateElement( + multiElement, + { + points: [ + ...points, + pointFrom(scenePointerX - rx, scenePointerY - ry), + ], + }, + false, + ); + } else { + setCursor(app.interactiveCanvas, CURSOR_TYPE.POINTER); + // in this branch, we're inside the commit zone, and no uncommitted + // point exists. Thus do nothing (don't add/remove points). + } + } else if ( + points.length > 2 && + lastCommittedPoint && + pointDistance( + pointFrom(scenePointerX - rx, scenePointerY - ry), + lastCommittedPoint, + ) < LINE_CONFIRM_THRESHOLD + ) { + setCursor(app.interactiveCanvas, CURSOR_TYPE.POINTER); + mutateElement( + multiElement, + { + points: points.slice(0, -1), + }, + false, + ); + } else { + const [gridX, gridY] = getGridPoint( + scenePointerX, + scenePointerY, + event[KEYS.CTRL_OR_CMD] || isElbowArrow(multiElement) + ? null + : app.getEffectiveGridSize(), + ); + console.log(points); + const [lastCommittedX, lastCommittedY] = + multiElement?.lastCommittedPoint ?? [0, 0]; + + let dxFromLastCommitted = gridX - rx - lastCommittedX; + let dyFromLastCommitted = gridY - ry - lastCommittedY; + + if (shouldRotateWithDiscreteAngle(event)) { + ({ width: dxFromLastCommitted, height: dyFromLastCommitted } = + getLockedLinearCursorAlignSize( + // actual coordinate of the last committed point + lastCommittedX + rx, + lastCommittedY + ry, + // cursor-grid coordinate + gridX, + gridY, + )); + } + + if (isPathALoop(points, app.state.zoom.value)) { + setCursor(app.interactiveCanvas, CURSOR_TYPE.POINTER); + } + + let x = multiElement.x + lastCommittedX + dxFromLastCommitted; + let y = multiElement.y + lastCommittedY + dyFromLastCommitted; + + if (isArrowElement(multiElement)) { + [x, y] = getOutlineAvoidingPoint( + multiElement, + pointFrom(scenePointerX, scenePointerY), + multiElement.points.length - 1, + app.scene, + app.state.zoom, + pointFrom(x, y), + ); + } + + // update last uncommitted point + LinearElementEditor.movePoints(multiElement, [ + { + index: points.length - 1, + point: LinearElementEditor.createPointAt( + multiElement, + app.scene.getNonDeletedElementsMap(), + x, + y, + app.getEffectiveGridSize(), + ), + isDragging: true, + }, + ]); + + // in this path, we're mutating multiElement to reflect + // how it will be after adding pointer position as the next point + // trigger update here so that new element canvas renders again to reflect this + triggerRender(false); + } +} + +export function onPointerUpFromPointerDownOnLinearElementHandler( + newElement: ExcalidrawLinearElement, + multiElement: NonDeleted | null, + app: App, + store: App["store"], + pointerDownState: PointerDownState, + childEvent: PointerEvent, + activeTool: { + lastActiveTool: ActiveTool | null; + locked: boolean; + fromSelection: boolean; + } & ActiveTool, +) { + if (newElement!.points.length > 1) { + store.shouldCaptureIncrement(); + } + const pointerCoords = viewportCoordsToSceneCoords(childEvent, app.state); + + if (!pointerDownState.drag.hasOccurred && newElement && !multiElement) { + mutateElement(newElement, { + points: [ + ...newElement.points, + pointFrom( + pointerCoords.x - newElement.x, + pointerCoords.y - newElement.y, + ), + ], + }); + app.setState({ + multiElement: newElement, + newElement, + }); + } else if (pointerDownState.drag.hasOccurred && !multiElement) { + if (isBindingEnabled(app.state) && isBindingElement(newElement, false)) { + maybeBindLinearElement( + newElement, + app.state, + app.scene.getNonDeletedElementsMap(), + app.scene.getNonDeletedElements(), + ); + } + app.setState({ suggestedBindings: [], startBoundElement: null }); + if (!activeTool.locked) { + resetCursor(app.interactiveCanvas); + app.setState((prevState) => ({ + newElement: null, + activeTool: updateActiveTool(app.state, { + type: "selection", + }), + selectedElementIds: makeNextSelectedElementIds( + { + ...prevState.selectedElementIds, + [newElement.id]: true, + }, + prevState, + ), + selectedLinearElement: new LinearElementEditor(newElement), + })); + } else { + app.setState((prevState) => ({ + newElement: null, + })); + } + // so that the scene gets rendered again to display the newly drawn linear as well + app.scene.triggerUpdate(); + } +} + +/** + * Handles double click on a linear element to edit it or delete a segment + */ +export function handleDoubleClickForLinearElement( + app: App, + store: App["store"], + selectedElement: NonDeleted, + event: React.MouseEvent, + sceneX: number, + sceneY: number, +) { + if ( + event[KEYS.CTRL_OR_CMD] && + (!app.state.editingLinearElement || + app.state.editingLinearElement.elementId !== selectedElement.id) && + !isElbowArrow(selectedElement) + ) { + store.shouldCaptureIncrement(); + app.setState({ + editingLinearElement: new LinearElementEditor(selectedElement), + }); + } else if (app.state.selectedLinearElement && isElbowArrow(selectedElement)) { + const hitCoords = LinearElementEditor.getSegmentMidpointHitCoords( + app.state.selectedLinearElement, + { x: sceneX, y: sceneY }, + app.state, + app.scene.getNonDeletedElementsMap(), + ); + const midPoint = hitCoords + ? LinearElementEditor.getSegmentMidPointIndex( + app.state.selectedLinearElement, + app.state, + hitCoords, + app.scene.getNonDeletedElementsMap(), + ) + : -1; + + if (midPoint && midPoint > -1) { + store.shouldCaptureIncrement(); + LinearElementEditor.deleteFixedSegment(selectedElement, midPoint); + + const nextCoords = LinearElementEditor.getSegmentMidpointHitCoords( + { + ...app.state.selectedLinearElement, + segmentMidPointHoveredCoords: null, + }, + { x: sceneX, y: sceneY }, + app.state, + app.scene.getNonDeletedElementsMap(), + ); + const nextIndex = nextCoords + ? LinearElementEditor.getSegmentMidPointIndex( + app.state.selectedLinearElement, + app.state, + nextCoords, + app.scene.getNonDeletedElementsMap(), + ) + : null; + + app.setState({ + selectedLinearElement: { + ...app.state.selectedLinearElement, + pointerDownState: { + ...app.state.selectedLinearElement.pointerDownState, + segmentMidpoint: { + index: nextIndex, + value: hitCoords, + added: false, + }, + }, + segmentMidPointHoveredCoords: nextCoords, + }, + }); + + return true; + } + } +} + +export function maybeSuggestBindingsForLinearElementAtCoords( + linearElement: NonDeleted, + /** scene coords */ + pointerCoords: { + x: number; + y: number; + }[], + app: App, + // During line creation the start binding hasn't been written yet + // into `linearElement` + oppositeBindingBoundElement?: ExcalidrawBindableElement | null, +) { + if (!pointerCoords.length) { + return; + } + + const suggestedBindings = pointerCoords.reduce( + (acc: NonDeleted[], coords) => { + const hoveredBindableElement = getHoveredElementForBinding( + coords, + app.scene.getNonDeletedElements(), + app.scene.getNonDeletedElementsMap(), + app.state.zoom, + isElbowArrow(linearElement), + isElbowArrow(linearElement), + ); + if ( + hoveredBindableElement != null && + !isLinearElementSimpleAndAlreadyBound( + linearElement, + oppositeBindingBoundElement?.id, + hoveredBindableElement, + ) + ) { + acc.push(hoveredBindableElement); + } + return acc; + }, + [], + ); + + app.setState({ suggestedBindings }); +}