fix preview snapping threshold

This commit is contained in:
Maze Winther
2026-02-23 10:15:51 +01:00
parent baa5f33407
commit d352e58570
4 changed files with 58 additions and 11 deletions
+14 -2
View File
@@ -4,9 +4,16 @@ import { useShiftKey } from "@/hooks/use-shift-key";
import type { TextElement, Transform } from "@/types/timeline"; import type { TextElement, Transform } from "@/types/timeline";
import { getVisibleElementsWithBounds } from "@/lib/preview/element-bounds"; import { getVisibleElementsWithBounds } from "@/lib/preview/element-bounds";
import { hitTest } from "@/lib/preview/hit-test"; 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 { 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; const MIN_DRAG_DISTANCE = 0.5;
@@ -230,11 +237,16 @@ export function usePreviewInteraction({
}; };
const shouldSnap = !isShiftHeldRef.current; const shouldSnap = !isShiftHeldRef.current;
const snapThreshold = screenPixelsToLogicalThreshold({
canvas: canvasRef.current,
screenPixels: SNAP_THRESHOLD_SCREEN_PIXELS,
});
const { snappedPosition, activeLines } = shouldSnap const { snappedPosition, activeLines } = shouldSnap
? snapPosition({ ? snapPosition({
proposedPosition, proposedPosition,
canvasSize, canvasSize,
elementSize: dragStateRef.current.bounds, elementSize: dragStateRef.current.bounds,
snapThreshold,
}) })
: { : {
snappedPosition: proposedPosition, snappedPosition: proposedPosition,
+10 -1
View File
@@ -6,9 +6,13 @@ import {
getVisibleElementsWithBounds, getVisibleElementsWithBounds,
type ElementWithBounds, type ElementWithBounds,
} from "@/lib/preview/element-bounds"; } from "@/lib/preview/element-bounds";
import { screenToCanvas } from "@/lib/preview/preview-coords"; import {
screenPixelsToLogicalThreshold,
screenToCanvas,
} from "@/lib/preview/preview-coords";
import { import {
MIN_SCALE, MIN_SCALE,
SNAP_THRESHOLD_SCREEN_PIXELS,
snapRotation, snapRotation,
snapScale, snapScale,
type SnapLine, type SnapLine,
@@ -228,6 +232,10 @@ export function useTransformHandles({
); );
const canvasSize = editor.project.getActive().settings.canvasSize; const canvasSize = editor.project.getActive().settings.canvasSize;
const snapThreshold = screenPixelsToLogicalThreshold({
canvas: canvasRef.current,
screenPixels: SNAP_THRESHOLD_SCREEN_PIXELS,
});
const shouldSnap = !isShiftHeldRef.current; const shouldSnap = !isShiftHeldRef.current;
const { snappedScale, activeLines } = shouldSnap const { snappedScale, activeLines } = shouldSnap
? snapScale({ ? snapScale({
@@ -236,6 +244,7 @@ export function useTransformHandles({
baseWidth, baseWidth,
baseHeight, baseHeight,
canvasSize, canvasSize,
snapThreshold,
}) })
: { snappedScale: proposedScale, activeLines: [] as SnapLine[] }; : { snappedScale: proposedScale, activeLines: [] as SnapLine[] };
@@ -74,3 +74,17 @@ export function getDisplayScale({
y: canvasRect.height / canvasSize.height, 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),
};
}
+20 -8
View File
@@ -3,10 +3,10 @@ export interface SnapLine {
position: number; position: number;
} }
const SNAP_THRESHOLD = 10;
const ROTATION_SNAP_STEP_DEGREES = 90; const ROTATION_SNAP_STEP_DEGREES = 90;
const ROTATION_SNAP_THRESHOLD_DEGREES = 5; const ROTATION_SNAP_THRESHOLD_DEGREES = 5;
export const MIN_SCALE = 0.01; export const MIN_SCALE = 0.01;
export const SNAP_THRESHOLD_SCREEN_PIXELS = 8;
export interface SnapResult { export interface SnapResult {
snappedPosition: { x: number; y: number }; snappedPosition: { x: number; y: number };
@@ -17,10 +17,12 @@ export function snapPosition({
proposedPosition, proposedPosition,
canvasSize, canvasSize,
elementSize, elementSize,
snapThreshold,
}: { }: {
proposedPosition: { x: number; y: number }; proposedPosition: { x: number; y: number };
canvasSize: { width: number; height: number }; canvasSize: { width: number; height: number };
elementSize: { width: number; height: number }; elementSize: { width: number; height: number };
snapThreshold: { x: number; y: number };
}): SnapResult { }): SnapResult {
const centerX = 0; const centerX = 0;
const centerY = 0; const centerY = 0;
@@ -41,11 +43,13 @@ export function snapPosition({
function getClosestAxisSnap({ function getClosestAxisSnap({
candidates, candidates,
threshold,
}: { }: {
candidates: AxisSnapCandidate[]; candidates: AxisSnapCandidate[];
threshold: number;
}): AxisSnapCandidate | null { }): AxisSnapCandidate | null {
const snapCandidatesWithinThreshold = candidates.filter( const snapCandidatesWithinThreshold = candidates.filter(
(candidate) => candidate.distance <= SNAP_THRESHOLD, (candidate) => candidate.distance <= threshold,
); );
if (snapCandidatesWithinThreshold.length === 0) { if (snapCandidatesWithinThreshold.length === 0) {
return null; return null;
@@ -95,8 +99,14 @@ export function snapPosition({
}); });
} }
const closestX = getClosestAxisSnap({ candidates: xCandidates }); const closestX = getClosestAxisSnap({
const closestY = getClosestAxisSnap({ candidates: yCandidates }); candidates: xCandidates,
threshold: snapThreshold.x,
});
const closestY = getClosestAxisSnap({
candidates: yCandidates,
threshold: snapThreshold.y,
});
const x = closestX?.snappedPosition ?? proposedPosition.x; const x = closestX?.snappedPosition ?? proposedPosition.x;
const y = closestY?.snappedPosition ?? proposedPosition.y; const y = closestY?.snappedPosition ?? proposedPosition.y;
@@ -124,12 +134,14 @@ export function snapScale({
baseWidth, baseWidth,
baseHeight, baseHeight,
canvasSize, canvasSize,
snapThreshold,
}: { }: {
proposedScale: number; proposedScale: number;
position: { x: number; y: number }; position: { x: number; y: number };
baseWidth: number; baseWidth: number;
baseHeight: number; baseHeight: number;
canvasSize: { width: number; height: number }; canvasSize: { width: number; height: number };
snapThreshold: { x: number; y: number };
}): ScaleSnapResult { }): ScaleSnapResult {
const centerX = 0; const centerX = 0;
const centerY = 0; const centerY = 0;
@@ -162,7 +174,7 @@ export function snapScale({
for (const target of verticalTargets) { for (const target of verticalTargets) {
const distanceLeft = Math.abs(leftEdge - target.position); const distanceLeft = Math.abs(leftEdge - target.position);
if (distanceLeft <= SNAP_THRESHOLD) { if (distanceLeft <= snapThreshold.x) {
const scale = (2 * (position.x - target.position)) / baseWidth; const scale = (2 * (position.x - target.position)) / baseWidth;
if (scale > MIN_SCALE) { if (scale > MIN_SCALE) {
candidates.push({ candidates.push({
@@ -173,7 +185,7 @@ export function snapScale({
} }
} }
const distanceRight = Math.abs(rightEdge - target.position); const distanceRight = Math.abs(rightEdge - target.position);
if (distanceRight <= SNAP_THRESHOLD) { if (distanceRight <= snapThreshold.x) {
const scale = (2 * (target.position - position.x)) / baseWidth; const scale = (2 * (target.position - position.x)) / baseWidth;
if (scale > MIN_SCALE) { if (scale > MIN_SCALE) {
candidates.push({ candidates.push({
@@ -199,7 +211,7 @@ export function snapScale({
for (const target of horizontalTargets) { for (const target of horizontalTargets) {
const distanceTop = Math.abs(topEdge - target.position); const distanceTop = Math.abs(topEdge - target.position);
if (distanceTop <= SNAP_THRESHOLD) { if (distanceTop <= snapThreshold.y) {
const scale = (2 * (position.y - target.position)) / baseHeight; const scale = (2 * (position.y - target.position)) / baseHeight;
if (scale > MIN_SCALE) { if (scale > MIN_SCALE) {
candidates.push({ candidates.push({
@@ -210,7 +222,7 @@ export function snapScale({
} }
} }
const distanceBottom = Math.abs(bottomEdge - target.position); const distanceBottom = Math.abs(bottomEdge - target.position);
if (distanceBottom <= SNAP_THRESHOLD) { if (distanceBottom <= snapThreshold.y) {
const scale = (2 * (target.position - position.y)) / baseHeight; const scale = (2 * (target.position - position.y)) / baseHeight;
if (scale > MIN_SCALE) { if (scale > MIN_SCALE) {
candidates.push({ candidates.push({