summaryrefslogtreecommitdiff
path: root/apps/web/src
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-02-23 10:15:51 +0100
committerMaze Winther <mazewinther@gmail.com>2026-02-23 10:15:51 +0100
commitd352e585709bf4011c78719e020592f345401195 (patch)
treedbc2a43b615bfb1b143c4c57d8805a8fe38d7c56 /apps/web/src
parentbaa5f33407da81463196cde7639592d1fc984494 (diff)
downloadopencut-d352e585709bf4011c78719e020592f345401195.tar.gz
fix preview snapping threshold
Diffstat (limited to 'apps/web/src')
-rw-r--r--apps/web/src/hooks/use-preview-interaction.ts16
-rw-r--r--apps/web/src/hooks/use-transform-handles.ts11
-rw-r--r--apps/web/src/lib/preview/preview-coords.ts14
-rw-r--r--apps/web/src/lib/preview/preview-snap.ts28
4 files changed, 58 insertions, 11 deletions
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({