diff --git a/packages/element/src/linearElementEditor.ts b/packages/element/src/linearElementEditor.ts index 0acd0056cc..4bb9fc4bca 100644 --- a/packages/element/src/linearElementEditor.ts +++ b/packages/element/src/linearElementEditor.ts @@ -19,6 +19,7 @@ import { shouldRotateWithDiscreteAngle, getGridPoint, invariant, + isShallowEqual, } from "@excalidraw/common"; import { @@ -279,9 +280,105 @@ export class LinearElementEditor { }); } - /** - * @returns whether point was dragged - */ + static handlePointerMove( + event: PointerEvent, + app: AppClassProperties, + scenePointerX: number, + scenePointerY: number, + linearElementEditor: LinearElementEditor, + ): Pick | null { + const elementsMap = app.scene.getNonDeletedElementsMap(); + const elements = app.scene.getNonDeletedElements(); + const { elementId } = linearElementEditor; + + const element = LinearElementEditor.getElement(elementId, elementsMap); + + invariant(element, "Element being dragged must exist in the scene"); + invariant(element.points.length > 1, "Element must have at least 2 points"); + + const idx = element.points.length - 1; + const point = element.points[idx]; + const pivotPoint = element.points[idx - 1]; + const customLineAngle = + linearElementEditor.customLineAngle ?? + determineCustomLinearAngle(pivotPoint, element.points[idx]); + + // Determine if point movement should happen and how much + let deltaX = 0; + let deltaY = 0; + if (shouldRotateWithDiscreteAngle(event)) { + const [width, height] = LinearElementEditor._getShiftLockedDelta( + element, + elementsMap, + pivotPoint, + pointFrom(scenePointerX, scenePointerY), + event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(), + customLineAngle, + ); + const target = pointFrom( + width + pivotPoint[0], + height + pivotPoint[1], + ); + + deltaX = target[0] - point[0]; + deltaY = target[1] - point[1]; + } else { + const newDraggingPointPosition = LinearElementEditor.createPointAt( + element, + elementsMap, + scenePointerX - linearElementEditor.pointerOffset.x, + scenePointerY - linearElementEditor.pointerOffset.y, + event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(), + ); + deltaX = newDraggingPointPosition[0] - point[0]; + deltaY = newDraggingPointPosition[1] - point[1]; + } + + // Apply the point movement if needed + if (deltaX || deltaY) { + const { positions, updates } = pointDraggingUpdates( + [idx], + deltaX, + deltaY, + elementsMap, + element, + elements, + app, + ); + + LinearElementEditor.movePoints(element, app.scene, positions, updates); + } + + // Suggest bindings for first and last point if selected + let suggestedBinding: AppState["suggestedBinding"] = null; + if (isBindingElement(element, false)) { + suggestedBinding = maybeSuggestBindingsForBindingElementAtCoords( + element, + "end", + app.scene, + pointFrom(scenePointerX, scenePointerY), + ); + } + + const newLinearElementEditor = { + ...linearElementEditor, + customLineAngle, + }; + + if ( + app.state.selectedLinearElement?.customLineAngle === customLineAngle && + (!suggestedBinding || + isShallowEqual(app.state.suggestedBinding ?? [], suggestedBinding)) + ) { + return null; + } + + return { + selectedLinearElement: newLinearElementEditor, + suggestedBinding, + }; + } + static handlePointDragging( event: PointerEvent, app: AppClassProperties, @@ -913,7 +1010,7 @@ export class LinearElementEditor { return pointsEqual(point1, point2); } - static handlePointerMove( + static handlePointerMoveInEditMode( event: React.PointerEvent, scenePointerX: number, scenePointerY: number, diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 65bde9b392..f18dc45d8e 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -153,7 +153,6 @@ import { isFlowchartNodeElement, isBindableElement, isTextElement, - getLockedLinearCursorAlignSize, getNormalizedDimensions, isElementCompletelyInViewport, isElementInViewport, @@ -240,8 +239,6 @@ import { type ApplyToOptions, calculateFixedPointForNonElbowArrowBinding, bindOrUnbindBindingElement, - getBindingStrategyForDraggingBindingElementEndpoints, - getStartGlobalEndLocalPointsForSimpleArrowBinding, mutateElement, } from "@excalidraw/element"; @@ -912,18 +909,23 @@ class App extends React.Component { "Expected lastPointerMoveCoords to be set", ); - if (!this.state.selectedLinearElement?.selectedPointsIndices?.length) { - return; - } - - const startDragged = - this.state.selectedLinearElement.selectedPointsIndices.includes(0); - const endDragged = - this.state.selectedLinearElement.selectedPointsIndices.includes( - arrow.points.length - 1, + if (!this.state.multiElement) { + invariant( + this.state.selectedLinearElement?.selectedPointsIndices?.length, + "There has to be at least one selected point to trigger bind mode change at arrow drag creation", ); - if ((!startDragged && !endDragged) || (startDragged && endDragged)) { - return; + + const startDragged = + this.state.selectedLinearElement.selectedPointsIndices.includes(0); + const endDragged = + this.state.selectedLinearElement.selectedPointsIndices.includes( + arrow.points.length - 1, + ); + + // Check if the whole arrow is dragged by selecting all endpoints + if ((!startDragged && !endDragged) || (startDragged && endDragged)) { + return; + } } const { x, y } = this.lastPointerMoveCoords; @@ -967,13 +969,21 @@ class App extends React.Component { 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.state.selectedLinearElement, - ); + const newState = this.state.multiElement + ? LinearElementEditor.handlePointerMove( + event, + this, + x - this.state.selectedLinearElement.pointerOffset.x, + y - this.state.selectedLinearElement.pointerOffset.y, + this.state.selectedLinearElement, + ) + : LinearElementEditor.handlePointDragging( + event, + this, + x - this.state.selectedLinearElement.pointerOffset.x, + y - this.state.selectedLinearElement.pointerOffset.y, + this.state.selectedLinearElement, + ); this.setState(newState); } }; @@ -6173,7 +6183,7 @@ class App extends React.Component { ) { const editingLinearElement = this.state.newElement ? null - : LinearElementEditor.handlePointerMove( + : LinearElementEditor.handlePointerMoveInEditMode( event, scenePointerX, scenePointerY, @@ -6261,51 +6271,14 @@ class App extends React.Component { { informMutation: false, isDragging: false }, ); } else { - const [lastCommittedX, lastCommittedY] = - multiElement?.lastCommittedPoint ?? [0, 0]; - - // Handle grid snapping - const [gridX, gridY] = getGridPoint( - scenePointerX, - scenePointerY, - event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(), - ); - 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 arrow points const elementsMap = this.scene.getNonDeletedElementsMap(); - let startGlobalPoint = - this.state.selectedLinearElement?.pointerDownState - ?.arrowOriginalStartPoint ?? - LinearElementEditor.getPointAtIndexGlobalCoordinates( - multiElement, - 0, - elementsMap, - ); - let endLocalPoint = pointFrom( - lastCommittedX + dxFromLastCommitted, - lastCommittedY + dyFromLastCommitted, - ); - let startBinding = multiElement.startBinding; - if (isBindingElement(multiElement) && !isElbowArrow(multiElement)) { + if (isSimpleArrow(multiElement)) { const hoveredElement = getHoveredElementForBinding( pointFrom(scenePointerX, scenePointerY), this.scene.getNonDeletedElements(), @@ -6313,66 +6286,23 @@ class App extends React.Component { ); this.handleDelayedBindModeChange(multiElement, hoveredElement); - - const point = pointFrom( - scenePointerX - rx, - scenePointerY - ry, - ); - const { start, end } = - getBindingStrategyForDraggingBindingElementEndpoints( - multiElement, - new Map([ - [multiElement.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( - multiElement, - start.element, - "start", - elementsMap, - ), - }; - } - - [startGlobalPoint, endLocalPoint] = - getStartGlobalEndLocalPointsForSimpleArrowBinding( - multiElement, - start, - end, - startGlobalPoint, - endLocalPoint, - elementsMap, - ); } - // update last uncommitted point - this.scene.mutateElement( - multiElement, - { - x: startGlobalPoint[0], - y: startGlobalPoint[1], - points: [...points.slice(0, -1), endLocalPoint], - startBinding, - }, - { - isDragging: true, - informMutation: false, - }, + invariant( + this.state.selectedLinearElement, + "Expected selectedLinearElement to be set to operate on a linear element", ); - // 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); + const newState = LinearElementEditor.handlePointerMove( + event.nativeEvent, + this, + scenePointerX, + scenePointerY, + this.state.selectedLinearElement, + ); + if (newState) { + this.setState(newState); + } } return;