diff --git a/packages/excalidraw/appState.ts b/packages/excalidraw/appState.ts index dcc3fba11b..895a829fa6 100644 --- a/packages/excalidraw/appState.ts +++ b/packages/excalidraw/appState.ts @@ -124,6 +124,11 @@ export const getDefaultAppState = (): Omit< searchMatches: null, lockedMultiSelections: {}, activeLockedId: null, + cropPositionMovement: { + enabled: false, + croppingElementId: undefined, + directionLock: null, + }, }; }; @@ -249,6 +254,7 @@ const APP_STATE_STORAGE_CONF = (< searchMatches: { browser: false, export: false, server: false }, lockedMultiSelections: { browser: true, export: true, server: true }, activeLockedId: { browser: false, export: false, server: false }, + cropPositionMovement: { browser: false, export: false, server: false }, }); const _clearAppStateForStorage = < diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 304b19c57f..350b36d9ac 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -6994,6 +6994,8 @@ class App extends React.Component { }, cropPositionMovement: { enabled: false, + croppingElementId: undefined, + directionLock: null, }, }; } @@ -8134,20 +8136,47 @@ class App extends React.Component { }; // apply shift key constraint for directional movement + const threshold = 20; + let snappingToOrigin = false; if (event.shiftKey) { - const distanceX = Math.abs(totalDragOffset.x); - const distanceY = Math.abs(totalDragOffset.y); - - const lockX = distanceX < distanceY; - const lockY = distanceX > distanceY; - - if (lockX) { - totalDragOffset.x = 0; + if (!pointerDownState.cropPositionMovement.directionLock) { + if ( + Math.abs(totalDragOffset.x) > threshold || + Math.abs(totalDragOffset.y) > threshold + ) { + pointerDownState.cropPositionMovement.directionLock = + Math.abs(totalDragOffset.x) > Math.abs(totalDragOffset.y) + ? "x" + : "y"; + } else { + // if within threshold and not locked, always snap to origin + snappingToOrigin = true; + } + } else { + // if user moves back within threshold, unlock and snap back + if ( + Math.abs(totalDragOffset.x) < threshold && + Math.abs(totalDragOffset.y) < threshold + ) { + pointerDownState.cropPositionMovement.directionLock = null; + snappingToOrigin = true; + } } + } else { + pointerDownState.cropPositionMovement.directionLock = null; + } - if (lockY) { - totalDragOffset.y = 0; - } + if (snappingToOrigin) { + totalDragOffset.x = 0; + totalDragOffset.y = 0; + } + + if (pointerDownState.cropPositionMovement.directionLock === "x") { + totalDragOffset.y = 0; + } else if ( + pointerDownState.cropPositionMovement.directionLock === "y" + ) { + totalDragOffset.x = 0; } // scale the drag offset diff --git a/packages/excalidraw/types.ts b/packages/excalidraw/types.ts index 85294893b4..d6112984dc 100644 --- a/packages/excalidraw/types.ts +++ b/packages/excalidraw/types.ts @@ -444,6 +444,11 @@ export interface AppState { // as elements are unlocked, we remove the groupId from the elements // and also remove groupId from this map lockedMultiSelections: { [groupId: string]: true }; + cropPositionMovement: { + croppingElementId?: string; + enabled: boolean; + directionLock: "x" | "y" | null; + }; } export type SearchMatch = { @@ -800,6 +805,7 @@ export type PointerDownState = Readonly<{ cropPositionMovement: { croppingElementId?: string; enabled: boolean; + directionLock: "x" | "y" | null; }; }>;