From 83004e2c012a6c457a9129d760a3efa79e44157d Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Thu, 24 Jul 2025 17:38:57 +0200 Subject: [PATCH] Fix fixed angle orbiting --- packages/element/src/binding.ts | 36 +++++---- packages/element/src/linearElementEditor.ts | 60 ++++++++++---- packages/excalidraw/components/App.tsx | 86 ++++++++++++++------- 3 files changed, 122 insertions(+), 60 deletions(-) diff --git a/packages/element/src/binding.ts b/packages/element/src/binding.ts index c0edc6dae0..18bc166a69 100644 --- a/packages/element/src/binding.ts +++ b/packages/element/src/binding.ts @@ -15,7 +15,7 @@ import { PRECISION, } from "@excalidraw/math"; -import type { LocalPoint, Radians } from "@excalidraw/math"; +import type { LineSegment, LocalPoint, Radians } from "@excalidraw/math"; import type { AppState } from "@excalidraw/excalidraw/types"; @@ -839,6 +839,7 @@ export const bindPointToSnapToElementOutline = ( bindableElement: ExcalidrawBindableElement, startOrEnd: "start" | "end", elementsMap: ElementsMap, + customIntersector?: LineSegment, ): GlobalPoint => { const aabb = aabbForElement(bindableElement, elementsMap); const localP = @@ -881,16 +882,18 @@ export const bindPointToSnapToElementOutline = ( isHorizontal ? center[0] : snapPoint[0], !isHorizontal ? center[1] : snapPoint[1], ); - const intersector = lineSegment( - otherPoint, - pointFromVector( - vectorScale( - vectorNormalize(vectorFromPoint(snapPoint, otherPoint)), - Math.max(bindableElement.width, bindableElement.height) * 2, - ), + const intersector = + customIntersector ?? + lineSegment( otherPoint, - ), - ); + pointFromVector( + vectorScale( + vectorNormalize(vectorFromPoint(snapPoint, otherPoint)), + Math.max(bindableElement.width, bindableElement.height) * 2, + ), + otherPoint, + ), + ); intersection = intersectElementWithLineSegment( bindableElement, elementsMap, @@ -898,9 +901,8 @@ export const bindPointToSnapToElementOutline = ( FIXED_BINDING_DISTANCE, ).sort(pointDistanceSq)[0]; } else { - intersection = intersectElementWithLineSegment( - bindableElement, - elementsMap, + const intersector = + customIntersector ?? lineSegment( adjacentPoint, pointFromVector( @@ -911,7 +913,11 @@ export const bindPointToSnapToElementOutline = ( ), adjacentPoint, ), - ), + ); + intersection = intersectElementWithLineSegment( + bindableElement, + elementsMap, + intersector, FIXED_BINDING_DISTANCE, ).sort( (g, h) => @@ -936,6 +942,7 @@ export const getOutlineAvoidingPoint = ( coords: GlobalPoint, pointIndex: number, elementsMap: ElementsMap, + customIntersector?: LineSegment, ): GlobalPoint => { if (hoveredElement) { const newPoints = Array.from(element.points); @@ -952,6 +959,7 @@ export const getOutlineAvoidingPoint = ( hoveredElement, pointIndex === 0 ? "start" : "end", elementsMap, + customIntersector, ); } diff --git a/packages/element/src/linearElementEditor.ts b/packages/element/src/linearElementEditor.ts index df306e24e0..cc92bd541e 100644 --- a/packages/element/src/linearElementEditor.ts +++ b/packages/element/src/linearElementEditor.ts @@ -9,6 +9,7 @@ import { vectorFromPoint, curveLength, curvePointAtLength, + lineSegment, } from "@excalidraw/math"; import { getCurvePathOps } from "@excalidraw/utils/shape"; @@ -322,6 +323,8 @@ export class LinearElementEditor { const draggingPoint = element.points[lastClickedPoint]; if (selectedPointsIndices && draggingPoint) { + const elements = app.scene.getNonDeletedElements(); + if ( shouldRotateWithDiscreteAngle(event) && selectedPointsIndices.length === 1 && @@ -336,7 +339,6 @@ export class LinearElementEditor { element.points[selectedIndex][1] - referencePoint[1], element.points[selectedIndex][0] - referencePoint[0], ); - const [width, height] = LinearElementEditor._getShiftLockedDelta( element, elementsMap, @@ -345,22 +347,32 @@ export class LinearElementEditor { event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(), customLineAngle, ); - + const [x, y] = LinearElementEditor.getPointGlobalCoordinates( + element, + pointFrom( + width + referencePoint[0], + height + referencePoint[1], + ), + elementsMap, + ); LinearElementEditor.movePoints( element, app.scene, - new Map([ - [ - selectedIndex, - { - point: pointFrom( - width + referencePoint[0], - height + referencePoint[1], - ), - isDragging: selectedIndex === lastClickedPoint, - }, - ], - ]), + pointDraggingUpdates( + selectedPointsIndices, + 0, + 0, + elementsMap, + lastClickedPoint, + element, + x, + y, + linearElementEditor, + event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(), + elements, + app, + true, + ), ); } else { const newDraggingPointPosition = LinearElementEditor.createPointAt( @@ -372,7 +384,6 @@ export class LinearElementEditor { ); const deltaX = newDraggingPointPosition[0] - draggingPoint[0]; const deltaY = newDraggingPointPosition[1] - draggingPoint[1]; - const elements = app.scene.getNonDeletedElements(); LinearElementEditor.movePoints( element, @@ -995,7 +1006,6 @@ export class LinearElementEditor { if (shouldRotateWithDiscreteAngle(event) && points.length >= 2) { const lastCommittedPoint = points[points.length - 2]; - const [width, height] = LinearElementEditor._getShiftLockedDelta( element, elementsMap, @@ -1937,6 +1947,7 @@ const pointDraggingUpdates = ( gridSize: NullableGridSize, elements: readonly Ordered[], app: AppClassProperties, + angleLocked?: boolean, ): PointsPositionUpdates => { const [, , , , cx, cy] = getElementAbsoluteCoords(element, elementsMap, true); const hasMidPoints = @@ -2001,12 +2012,29 @@ const pointDraggingUpdates = ( app.state.bindMode === "orbit" && !otherPointInsideElement ) { + let customIntersector; + if (angleLocked) { + const adjacentPointIndex = + pointIndex === 0 ? 1 : element.points.length - 2; + const globalAdjacentPoint = + LinearElementEditor.getPointAtIndexGlobalCoordinates( + element, + adjacentPointIndex, + elementsMap, + ); + customIntersector = lineSegment( + globalAdjacentPoint, + newGlobalPointPosition, + ); + } + newGlobalPointPosition = getOutlineAvoidingPoint( element, hoveredElement, newGlobalPointPosition, pointIndex, elementsMap, + customIntersector, ); } diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index f9b08de1a2..1c791a9d09 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -17,6 +17,7 @@ import { vectorDot, vectorNormalize, pointsEqual, + lineSegment, } from "@excalidraw/math"; import { @@ -6132,11 +6133,29 @@ class App extends React.Component { { informMutation: false, isDragging: false }, ); } else { - let [gridX, gridY] = getGridPoint( + 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 ( isArrowElement(multiElement) && @@ -6172,38 +6191,31 @@ class App extends React.Component { pointFrom(scenePointerX, scenePointerY), multiElement.points.length - 1, this.scene.getNonDeletedElementsMap(), + shouldRotateWithDiscreteAngle(event) + ? lineSegment( + otherPoint, + pointFrom( + multiElement.x + lastCommittedX + dxFromLastCommitted, + multiElement.y + lastCommittedY + dyFromLastCommitted, + ), + ) + : undefined, ); - gridX = avoidancePoint + const x = avoidancePoint ? avoidancePoint[0] : hoveredElement ? scenePointerX : gridX; - gridY = avoidancePoint + const y = avoidancePoint ? avoidancePoint[1] : hoveredElement ? scenePointerY : gridY; + dxFromLastCommitted = x - rx - lastCommittedX; + dyFromLastCommitted = y - ry - lastCommittedY; } } - 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); } @@ -9089,6 +9101,15 @@ class App extends React.Component { 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 ( !isElbowArrow(newElement) && this.state.editingLinearElement && @@ -9119,6 +9140,20 @@ class App extends React.Component { : pointFrom(gridX, gridY), newElement.points.length - 1, elementsMap, + shouldRotateWithDiscreteAngle(event) && + points.length === 2 + ? lineSegment( + LinearElementEditor.getPointGlobalCoordinates( + newElement, + points[0], + elementsMap, + ), + pointFrom( + newElement.x + dx, + newElement.y + dy, + ), + ) + : undefined, ) : pointFrom(gridX, gridY); @@ -9176,15 +9211,6 @@ class App extends React.Component { } } - if (shouldRotateWithDiscreteAngle(event) && points.length === 2) { - ({ width: dx, height: dy } = getLockedLinearCursorAlignSize( - newElement.x, - newElement.y, - pointerCoords.x, - pointerCoords.y, - )); - } - if (points.length === 1) { this.scene.mutateElement( newElement,