diff --git a/apps/web/src/hooks/use-preview-interaction.ts b/apps/web/src/hooks/use-preview-interaction.ts index 2bb33f21..d29f2a4b 100644 --- a/apps/web/src/hooks/use-preview-interaction.ts +++ b/apps/web/src/hooks/use-preview-interaction.ts @@ -4,9 +4,16 @@ import { useShiftKey } from "@/hooks/use-shift-key"; import type { TextElement, Transform } from "@/types/timeline"; import { getVisibleElementsWithBounds } from "@/lib/preview/element-bounds"; import { hitTest } from "@/lib/preview/hit-test"; -import { screenToCanvas } from "@/lib/preview/preview-coords"; +import { + screenPixelsToLogicalThreshold, + screenToCanvas, +} from "@/lib/preview/preview-coords"; import { isVisualElement } from "@/lib/timeline/element-utils"; -import { snapPosition, type SnapLine } from "@/lib/preview/preview-snap"; +import { + SNAP_THRESHOLD_SCREEN_PIXELS, + snapPosition, + type SnapLine, +} from "@/lib/preview/preview-snap"; const MIN_DRAG_DISTANCE = 0.5; @@ -230,11 +237,16 @@ export function usePreviewInteraction({ }; const shouldSnap = !isShiftHeldRef.current; + const snapThreshold = screenPixelsToLogicalThreshold({ + canvas: canvasRef.current, + screenPixels: SNAP_THRESHOLD_SCREEN_PIXELS, + }); const { snappedPosition, activeLines } = shouldSnap ? snapPosition({ proposedPosition, canvasSize, elementSize: dragStateRef.current.bounds, + snapThreshold, }) : { snappedPosition: proposedPosition, diff --git a/apps/web/src/hooks/use-transform-handles.ts b/apps/web/src/hooks/use-transform-handles.ts index 37ef6d5f..8be6420c 100644 --- a/apps/web/src/hooks/use-transform-handles.ts +++ b/apps/web/src/hooks/use-transform-handles.ts @@ -6,9 +6,13 @@ import { getVisibleElementsWithBounds, type ElementWithBounds, } from "@/lib/preview/element-bounds"; -import { screenToCanvas } from "@/lib/preview/preview-coords"; +import { + screenPixelsToLogicalThreshold, + screenToCanvas, +} from "@/lib/preview/preview-coords"; import { MIN_SCALE, + SNAP_THRESHOLD_SCREEN_PIXELS, snapRotation, snapScale, type SnapLine, @@ -228,6 +232,10 @@ export function useTransformHandles({ ); const canvasSize = editor.project.getActive().settings.canvasSize; + const snapThreshold = screenPixelsToLogicalThreshold({ + canvas: canvasRef.current, + screenPixels: SNAP_THRESHOLD_SCREEN_PIXELS, + }); const shouldSnap = !isShiftHeldRef.current; const { snappedScale, activeLines } = shouldSnap ? snapScale({ @@ -236,6 +244,7 @@ export function useTransformHandles({ baseWidth, baseHeight, canvasSize, + snapThreshold, }) : { snappedScale: proposedScale, activeLines: [] as SnapLine[] }; diff --git a/apps/web/src/lib/preview/preview-coords.ts b/apps/web/src/lib/preview/preview-coords.ts index 08c16735..dcc87282 100644 --- a/apps/web/src/lib/preview/preview-coords.ts +++ b/apps/web/src/lib/preview/preview-coords.ts @@ -74,3 +74,17 @@ export function getDisplayScale({ y: canvasRect.height / canvasSize.height, }; } + +export function screenPixelsToLogicalThreshold({ + canvas, + screenPixels, +}: { + canvas: HTMLCanvasElement; + screenPixels: number; +}): { x: number; y: number } { + const canvasRect = canvas.getBoundingClientRect(); + return { + x: screenPixels * (canvas.width / canvasRect.width), + y: screenPixels * (canvas.height / canvasRect.height), + }; +} diff --git a/apps/web/src/lib/preview/preview-snap.ts b/apps/web/src/lib/preview/preview-snap.ts index bd53f0ca..36efc3ce 100644 --- a/apps/web/src/lib/preview/preview-snap.ts +++ b/apps/web/src/lib/preview/preview-snap.ts @@ -3,10 +3,10 @@ export interface SnapLine { position: number; } -const SNAP_THRESHOLD = 10; const ROTATION_SNAP_STEP_DEGREES = 90; const ROTATION_SNAP_THRESHOLD_DEGREES = 5; export const MIN_SCALE = 0.01; +export const SNAP_THRESHOLD_SCREEN_PIXELS = 8; export interface SnapResult { snappedPosition: { x: number; y: number }; @@ -17,10 +17,12 @@ export function snapPosition({ proposedPosition, canvasSize, elementSize, + snapThreshold, }: { proposedPosition: { x: number; y: number }; canvasSize: { width: number; height: number }; elementSize: { width: number; height: number }; + snapThreshold: { x: number; y: number }; }): SnapResult { const centerX = 0; const centerY = 0; @@ -41,11 +43,13 @@ export function snapPosition({ function getClosestAxisSnap({ candidates, + threshold, }: { candidates: AxisSnapCandidate[]; + threshold: number; }): AxisSnapCandidate | null { const snapCandidatesWithinThreshold = candidates.filter( - (candidate) => candidate.distance <= SNAP_THRESHOLD, + (candidate) => candidate.distance <= threshold, ); if (snapCandidatesWithinThreshold.length === 0) { return null; @@ -95,8 +99,14 @@ export function snapPosition({ }); } - const closestX = getClosestAxisSnap({ candidates: xCandidates }); - const closestY = getClosestAxisSnap({ candidates: yCandidates }); + const closestX = getClosestAxisSnap({ + candidates: xCandidates, + threshold: snapThreshold.x, + }); + const closestY = getClosestAxisSnap({ + candidates: yCandidates, + threshold: snapThreshold.y, + }); const x = closestX?.snappedPosition ?? proposedPosition.x; const y = closestY?.snappedPosition ?? proposedPosition.y; @@ -124,12 +134,14 @@ export function snapScale({ baseWidth, baseHeight, canvasSize, + snapThreshold, }: { proposedScale: number; position: { x: number; y: number }; baseWidth: number; baseHeight: number; canvasSize: { width: number; height: number }; + snapThreshold: { x: number; y: number }; }): ScaleSnapResult { const centerX = 0; const centerY = 0; @@ -162,7 +174,7 @@ export function snapScale({ for (const target of verticalTargets) { const distanceLeft = Math.abs(leftEdge - target.position); - if (distanceLeft <= SNAP_THRESHOLD) { + if (distanceLeft <= snapThreshold.x) { const scale = (2 * (position.x - target.position)) / baseWidth; if (scale > MIN_SCALE) { candidates.push({ @@ -173,7 +185,7 @@ export function snapScale({ } } const distanceRight = Math.abs(rightEdge - target.position); - if (distanceRight <= SNAP_THRESHOLD) { + if (distanceRight <= snapThreshold.x) { const scale = (2 * (target.position - position.x)) / baseWidth; if (scale > MIN_SCALE) { candidates.push({ @@ -199,7 +211,7 @@ export function snapScale({ for (const target of horizontalTargets) { const distanceTop = Math.abs(topEdge - target.position); - if (distanceTop <= SNAP_THRESHOLD) { + if (distanceTop <= snapThreshold.y) { const scale = (2 * (position.y - target.position)) / baseHeight; if (scale > MIN_SCALE) { candidates.push({ @@ -210,7 +222,7 @@ export function snapScale({ } } const distanceBottom = Math.abs(bottomEdge - target.position); - if (distanceBottom <= SNAP_THRESHOLD) { + if (distanceBottom <= snapThreshold.y) { const scale = (2 * (target.position - position.y)) / baseHeight; if (scale > MIN_SCALE) { candidates.push({