From 001df5badeafef158b45e2b114713aedf3007ac0 Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Sat, 24 May 2025 18:14:13 +0200 Subject: [PATCH] Function implemented --- packages/element/src/linearElementEditor.ts | 14 +++++++ packages/element/src/sizeHelpers.ts | 41 +++++++++++++++++-- .../regressionTests.test.tsx.snap | 4 ++ packages/math/src/angle.ts | 32 +++++++++++++++ 4 files changed, 88 insertions(+), 3 deletions(-) diff --git a/packages/element/src/linearElementEditor.ts b/packages/element/src/linearElementEditor.ts index eec3fc7a0..80541b2ce 100644 --- a/packages/element/src/linearElementEditor.ts +++ b/packages/element/src/linearElementEditor.ts @@ -117,6 +117,7 @@ export class LinearElementEditor { public readonly hoverPointIndex: number; public readonly segmentMidPointHoveredCoords: GlobalPoint | null; public readonly elbowed: boolean; + public readonly customLineAngle: number | null; constructor( element: NonDeleted, @@ -150,6 +151,7 @@ export class LinearElementEditor { this.hoverPointIndex = -1; this.segmentMidPointHoveredCoords = null; this.elbowed = isElbowArrow(element) && element.elbowed; + this.customLineAngle = null; } // --------------------------------------------------------------------------- @@ -253,6 +255,7 @@ export class LinearElementEditor { const { elementId } = linearElementEditor; const elementsMap = scene.getNonDeletedElementsMap(); const element = LinearElementEditor.getElement(elementId, elementsMap); + let customLineAngle = linearElementEditor.customLineAngle; if (!element) { return null; } @@ -293,6 +296,12 @@ export class LinearElementEditor { const selectedIndex = selectedPointsIndices[0]; const referencePoint = element.points[selectedIndex === 0 ? 1 : selectedIndex - 1]; + customLineAngle = + linearElementEditor.customLineAngle ?? + Math.atan( + (element.points[selectedIndex][1] - referencePoint[1]) / + (element.points[selectedIndex][0] - referencePoint[0]), + ); const [width, height] = LinearElementEditor._getShiftLockedDelta( element, @@ -300,6 +309,7 @@ export class LinearElementEditor { referencePoint, pointFrom(scenePointerX, scenePointerY), event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(), + customLineAngle, ); LinearElementEditor.movePoints( @@ -421,6 +431,7 @@ export class LinearElementEditor { ? lastClickedPoint : -1, isDragging: true, + customLineAngle, }; } @@ -524,6 +535,7 @@ export class LinearElementEditor { : selectedPointsIndices, isDragging: false, pointerOffset: { x: 0, y: 0 }, + customLineAngle: null, }; } @@ -1531,6 +1543,7 @@ export class LinearElementEditor { referencePoint: LocalPoint, scenePointer: GlobalPoint, gridSize: NullableGridSize, + customLineAngle?: number, ) { const referencePointCoords = LinearElementEditor.getPointGlobalCoordinates( element, @@ -1556,6 +1569,7 @@ export class LinearElementEditor { referencePointCoords[1], gridX, gridY, + customLineAngle, ); return pointRotateRads( diff --git a/packages/element/src/sizeHelpers.ts b/packages/element/src/sizeHelpers.ts index bd3d3fb0c..0bb7d0456 100644 --- a/packages/element/src/sizeHelpers.ts +++ b/packages/element/src/sizeHelpers.ts @@ -2,6 +2,12 @@ import { SHIFT_LOCKING_ANGLE, viewportCoordsToSceneCoords, } from "@excalidraw/common"; +import { + normalizeRadians, + radiansBetweenAngles, + radiansDifference, + type Radians, +} from "@excalidraw/math"; import type { AppState, Offsets, Zoom } from "@excalidraw/excalidraw/types"; @@ -134,13 +140,42 @@ export const getLockedLinearCursorAlignSize = ( originY: number, x: number, y: number, + customAngle?: number, ) => { let width = x - originX; let height = y - originY; - const lockedAngle = - Math.round(Math.atan(height / width) / SHIFT_LOCKING_ANGLE) * - SHIFT_LOCKING_ANGLE; + const angle = Math.atan(height / width) as Radians; + let lockedAngle = (Math.round(angle / SHIFT_LOCKING_ANGLE) * + SHIFT_LOCKING_ANGLE) as Radians; + + if (customAngle) { + // If custom angle is provided, we check if the angle is close to the + // custom angle, snap to that if close engough, otherwise snap to the + // higher or lower angle depending on the current angle vs custom angle. + const lower = (Math.floor(customAngle / SHIFT_LOCKING_ANGLE) * + SHIFT_LOCKING_ANGLE) as Radians; + if ( + radiansBetweenAngles( + angle, + lower, + (lower + SHIFT_LOCKING_ANGLE) as Radians, + ) + ) { + if ( + radiansDifference(angle, customAngle as Radians) < + SHIFT_LOCKING_ANGLE / 6 + ) { + lockedAngle = customAngle as Radians; + } else if ( + normalizeRadians(angle) > normalizeRadians(customAngle as Radians) + ) { + lockedAngle = (lower + SHIFT_LOCKING_ANGLE) as Radians; + } else { + lockedAngle = lower; + } + } + } if (lockedAngle === 0) { height = 0; diff --git a/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap index 11447c731..c5e2b309b 100644 --- a/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap @@ -8628,6 +8628,7 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1` "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, "selectedLinearElement": LinearElementEditor { + "customLineAngle": null, "elbowed": false, "elementId": "id0", "endBindingElement": "keep", @@ -8851,6 +8852,7 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`] "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, "selectedLinearElement": LinearElementEditor { + "customLineAngle": null, "elbowed": false, "elementId": "id0", "endBindingElement": "keep", @@ -9267,6 +9269,7 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1` "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, "selectedLinearElement": LinearElementEditor { + "customLineAngle": null, "elbowed": false, "elementId": "id0", "endBindingElement": "keep", @@ -9670,6 +9673,7 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`] "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, "selectedLinearElement": LinearElementEditor { + "customLineAngle": null, "elbowed": false, "elementId": "id0", "endBindingElement": "keep", diff --git a/packages/math/src/angle.ts b/packages/math/src/angle.ts index 353dc5dad..e500b7515 100644 --- a/packages/math/src/angle.ts +++ b/packages/math/src/angle.ts @@ -49,3 +49,35 @@ export function radiansToDegrees(degrees: Radians): Degrees { export function isRightAngleRads(rads: Radians): boolean { return Math.abs(Math.sin(2 * rads)) < PRECISION; } + +export function radiansBetweenAngles( + a: Radians, + min: Radians, + max: Radians, +): boolean { + a = normalizeRadians(a); + min = normalizeRadians(min); + max = normalizeRadians(max); + + if (min < max) { + return a >= min && a <= max; + } + + // The range wraps around the 0 angle + return a >= min || a <= max; +} + +export function radiansDifference(a: Radians, b: Radians): Radians { + a = normalizeRadians(a); + b = normalizeRadians(b); + + let diff = a - b; + + if (diff < -Math.PI) { + diff = (diff + 2 * Math.PI) as Radians; + } else if (diff > Math.PI) { + diff = (diff - 2 * Math.PI) as Radians; + } + + return Math.abs(diff) as Radians; +}