From 3e89d2998588f8e35c524e523ba2ee853f0f6475 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Mon, 20 Apr 2026 11:31:17 +0200 Subject: refactor: restructure files to their domains, new preview overlay system, and dep graph --- apps/web/src/lib/preview/preview-snap.ts | 539 ------------------------------- 1 file changed, 539 deletions(-) delete mode 100644 apps/web/src/lib/preview/preview-snap.ts (limited to 'apps/web/src/lib/preview/preview-snap.ts') diff --git a/apps/web/src/lib/preview/preview-snap.ts b/apps/web/src/lib/preview/preview-snap.ts deleted file mode 100644 index 86d35390..00000000 --- a/apps/web/src/lib/preview/preview-snap.ts +++ /dev/null @@ -1,539 +0,0 @@ -export interface SnapLine { - type: "horizontal" | "vertical"; - position: number; -} - -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 }; - activeLines: SnapLine[]; -} - -type ScaleEdge = "left" | "right" | "top" | "bottom"; - -export interface ScaleEdgePreference { - left?: boolean; - right?: boolean; - top?: boolean; - bottom?: boolean; -} - -function hasPreferredEdge({ - preferredEdges, - edge, -}: { - preferredEdges?: ScaleEdgePreference; - edge: ScaleEdge; -}): boolean { - return preferredEdges?.[edge] === true; -} - -function pickClosestScaleCandidate({ - candidates, - preferredEdges, -}: { - candidates: T[]; - preferredEdges?: ScaleEdgePreference; -}): T | null { - if (candidates.length === 0) { - return null; - } - - return candidates.reduce((bestCandidate, candidate) => { - if (candidate.distance < bestCandidate.distance) { - return candidate; - } - if (candidate.distance > bestCandidate.distance) { - return bestCandidate; - } - - const shouldPreferCandidate = hasPreferredEdge({ - preferredEdges, - edge: candidate.edge, - }); - const shouldPreferBestCandidate = hasPreferredEdge({ - preferredEdges, - edge: bestCandidate.edge, - }); - - return shouldPreferCandidate && !shouldPreferBestCandidate - ? candidate - : bestCandidate; - }); -} - -export function snapPosition({ - proposedPosition, - canvasSize, - elementSize, - rotation = 0, - snapThreshold, -}: { - proposedPosition: { x: number; y: number }; - canvasSize: { width: number; height: number }; - elementSize: { width: number; height: number }; - rotation?: number; - snapThreshold: { x: number; y: number }; -}): SnapResult { - const centerX = 0; - const centerY = 0; - const left = -canvasSize.width / 2; - const right = canvasSize.width / 2; - const top = -canvasSize.height / 2; - const bottom = canvasSize.height / 2; - - const rotRad = (rotation * Math.PI) / 180; - const cosR = Math.abs(Math.cos(rotRad)); - const sinR = Math.abs(Math.sin(rotRad)); - const halfWidth = (elementSize.width * cosR + elementSize.height * sinR) / 2; - const halfHeight = (elementSize.width * sinR + elementSize.height * cosR) / 2; - const activeLines: SnapLine[] = []; - - type AxisSnapCandidate = { - snappedPosition: number; - line: SnapLine; - distance: number; - }; - - function getClosestAxisSnap({ - candidates, - threshold, - }: { - candidates: AxisSnapCandidate[]; - threshold: number; - }): AxisSnapCandidate | null { - const snapCandidatesWithinThreshold = candidates.filter( - (candidate) => candidate.distance <= threshold, - ); - if (snapCandidatesWithinThreshold.length === 0) { - return null; - } - return snapCandidatesWithinThreshold.reduce((closest, current) => - current.distance < closest.distance ? current : closest, - ); - } - - const verticalTargets = [centerX, left, right]; - const horizontalTargets = [centerY, top, bottom]; - - const xCandidates: AxisSnapCandidate[] = []; - for (const targetX of verticalTargets) { - xCandidates.push({ - snappedPosition: targetX, - line: { type: "vertical", position: targetX }, - distance: Math.abs(proposedPosition.x - targetX), - }); - xCandidates.push({ - snappedPosition: targetX + halfWidth, - line: { type: "vertical", position: targetX }, - distance: Math.abs(proposedPosition.x - halfWidth - targetX), - }); - xCandidates.push({ - snappedPosition: targetX - halfWidth, - line: { type: "vertical", position: targetX }, - distance: Math.abs(proposedPosition.x + halfWidth - targetX), - }); - } - const yCandidates: AxisSnapCandidate[] = []; - for (const targetY of horizontalTargets) { - yCandidates.push({ - snappedPosition: targetY, - line: { type: "horizontal", position: targetY }, - distance: Math.abs(proposedPosition.y - targetY), - }); - yCandidates.push({ - snappedPosition: targetY + halfHeight, - line: { type: "horizontal", position: targetY }, - distance: Math.abs(proposedPosition.y - halfHeight - targetY), - }); - yCandidates.push({ - snappedPosition: targetY - halfHeight, - line: { type: "horizontal", position: targetY }, - distance: Math.abs(proposedPosition.y + halfHeight - targetY), - }); - } - - 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; - if (closestX) { - activeLines.push(closestX.line); - } - if (closestY) { - activeLines.push(closestY.line); - } - - return { - snappedPosition: { x, y }, - activeLines, - }; -} - -export interface ScaleSnapResult { - snappedScale: number; - activeLines: SnapLine[]; -} - -export function snapScale({ - proposedScale, - position, - baseWidth, - baseHeight, - rotation = 0, - canvasSize, - snapThreshold, - preferredEdges, -}: { - proposedScale: number; - position: { x: number; y: number }; - baseWidth: number; - baseHeight: number; - rotation?: number; - canvasSize: { width: number; height: number }; - snapThreshold: { x: number; y: number }; - preferredEdges?: ScaleEdgePreference; -}): ScaleSnapResult { - const centerX = 0; - const centerY = 0; - const left = -canvasSize.width / 2; - const right = canvasSize.width / 2; - const top = -canvasSize.height / 2; - const bottom = canvasSize.height / 2; - - const rotRad = (rotation * Math.PI) / 180; - const cosR = Math.abs(Math.cos(rotRad)); - const sinR = Math.abs(Math.sin(rotRad)); - const aabbBaseHalfW = (baseWidth * cosR + baseHeight * sinR) / 2; - const aabbBaseHalfH = (baseWidth * sinR + baseHeight * cosR) / 2; - - const leftEdge = position.x - aabbBaseHalfW * proposedScale; - const rightEdge = position.x + aabbBaseHalfW * proposedScale; - const topEdge = position.y - aabbBaseHalfH * proposedScale; - const bottomEdge = position.y + aabbBaseHalfH * proposedScale; - - interface SnapCandidate { - scale: number; - distance: number; - lines: SnapLine[]; - edge: ScaleEdge; - } - - const candidates: SnapCandidate[] = []; - - const verticalTargets = [ - { position: left, line: { type: "vertical" as const, position: left } }, - { - position: centerX, - line: { type: "vertical" as const, position: centerX }, - }, - { position: right, line: { type: "vertical" as const, position: right } }, - ]; - - for (const target of verticalTargets) { - const distanceLeft = Math.abs(leftEdge - target.position); - if (distanceLeft <= snapThreshold.x) { - const scale = (position.x - target.position) / aabbBaseHalfW; - if (Math.abs(scale) > MIN_SCALE) { - candidates.push({ - scale, - distance: distanceLeft, - lines: [target.line], - edge: "left", - }); - } - } - const distanceRight = Math.abs(rightEdge - target.position); - if (distanceRight <= snapThreshold.x) { - const scale = (target.position - position.x) / aabbBaseHalfW; - if (Math.abs(scale) > MIN_SCALE) { - candidates.push({ - scale, - distance: distanceRight, - lines: [target.line], - edge: "right", - }); - } - } - } - - const horizontalTargets = [ - { position: top, line: { type: "horizontal" as const, position: top } }, - { - position: centerY, - line: { type: "horizontal" as const, position: centerY }, - }, - { - position: bottom, - line: { type: "horizontal" as const, position: bottom }, - }, - ]; - - for (const target of horizontalTargets) { - const distanceTop = Math.abs(topEdge - target.position); - if (distanceTop <= snapThreshold.y) { - const scale = (position.y - target.position) / aabbBaseHalfH; - if (Math.abs(scale) > MIN_SCALE) { - candidates.push({ - scale, - distance: distanceTop, - lines: [target.line], - edge: "top", - }); - } - } - const distanceBottom = Math.abs(bottomEdge - target.position); - if (distanceBottom <= snapThreshold.y) { - const scale = (target.position - position.y) / aabbBaseHalfH; - if (Math.abs(scale) > MIN_SCALE) { - candidates.push({ - scale, - distance: distanceBottom, - lines: [target.line], - edge: "bottom", - }); - } - } - } - - const best = pickClosestScaleCandidate({ - candidates, - preferredEdges, - }); - if (!best) { - return { snappedScale: proposedScale, activeLines: [] }; - } - - const snappedLeft = position.x - aabbBaseHalfW * best.scale; - const snappedRight = position.x + aabbBaseHalfW * best.scale; - const snappedTop = position.y - aabbBaseHalfH * best.scale; - const snappedBottom = position.y + aabbBaseHalfH * best.scale; - - const activeLines: SnapLine[] = []; - const seenKeys = new Set(); - - function addLine({ line }: { line: SnapLine }) { - const key = `${line.type}-${line.position}`; - if (!seenKeys.has(key)) { - seenKeys.add(key); - activeLines.push(line); - } - } - - for (const target of verticalTargets) { - if ( - (hasPreferredEdge({ preferredEdges, edge: "left" }) && - Math.abs(snappedLeft - target.position) <= 1) || - (hasPreferredEdge({ preferredEdges, edge: "right" }) && - Math.abs(snappedRight - target.position) <= 1) || - (!preferredEdges && - (Math.abs(snappedLeft - target.position) <= 1 || - Math.abs(snappedRight - target.position) <= 1)) - ) { - addLine({ line: target.line }); - } - } - for (const target of horizontalTargets) { - if ( - (hasPreferredEdge({ preferredEdges, edge: "top" }) && - Math.abs(snappedTop - target.position) <= 1) || - (hasPreferredEdge({ preferredEdges, edge: "bottom" }) && - Math.abs(snappedBottom - target.position) <= 1) || - (!preferredEdges && - (Math.abs(snappedTop - target.position) <= 1 || - Math.abs(snappedBottom - target.position) <= 1)) - ) { - addLine({ line: target.line }); - } - } - - return { - snappedScale: best.scale, - activeLines, - }; -} - -export interface AxisSnapResult { - snappedScale: number; - /** Infinity when no snap candidate was within threshold */ - snapDistance: number; - activeLines: SnapLine[]; -} - -export function snapScaleAxes({ - proposedScaleX, - proposedScaleY, - position, - baseWidth, - baseHeight, - rotation = 0, - canvasSize, - snapThreshold, - preferredEdges, -}: { - proposedScaleX: number; - proposedScaleY: number; - position: { x: number; y: number }; - baseWidth: number; - baseHeight: number; - rotation?: number; - canvasSize: { width: number; height: number }; - snapThreshold: { x: number; y: number }; - preferredEdges?: ScaleEdgePreference; -}): { x: AxisSnapResult; y: AxisSnapResult } { - const canvasLeft = -canvasSize.width / 2; - const canvasRight = canvasSize.width / 2; - const canvasTop = -canvasSize.height / 2; - const canvasBottom = canvasSize.height / 2; - - const rotRad = (rotation * Math.PI) / 180; - const cosR = Math.abs(Math.cos(rotRad)); - const sinR = Math.abs(Math.sin(rotRad)); - const EPSILON = 1e-6; - - // Current AABB edges at proposed scales - const currentAabbHalfW = (baseWidth * proposedScaleX * cosR + baseHeight * proposedScaleY * sinR) / 2; - const currentAabbHalfH = (baseWidth * proposedScaleX * sinR + baseHeight * proposedScaleY * cosR) / 2; - const currentLeftEdge = position.x - currentAabbHalfW; - const currentRightEdge = position.x + currentAabbHalfW; - const currentTopEdge = position.y - currentAabbHalfH; - const currentBottomEdge = position.y + currentAabbHalfH; - - interface Candidate { - scale: number; - distance: number; - line: SnapLine; - edge: ScaleEdge; - } - - function bestCandidate({ - candidates, - proposedScale, - }: { - candidates: Candidate[]; - proposedScale: number; - }): AxisSnapResult { - const best = pickClosestScaleCandidate({ - candidates, - preferredEdges, - }); - if (!best) { - return { snappedScale: proposedScale, snapDistance: Infinity, activeLines: [] }; - } - return { snappedScale: best.scale, snapDistance: best.distance, activeLines: [best.line] }; - } - - // sX candidates: snap via vertical targets (left/right AABB edges) — only valid when cosR ≠ 0 - // snap via horizontal targets (top/bottom AABB edges) — only valid when sinR ≠ 0 - const xCandidates: Candidate[] = []; - const yContribW = baseHeight * proposedScaleY * sinR; - const yContribH = baseHeight * proposedScaleY * cosR; - - if (cosR > EPSILON) { - for (const T of [canvasLeft, 0, canvasRight]) { - const line: SnapLine = { type: "vertical", position: T }; - const distLeft = Math.abs(currentLeftEdge - T); - if (distLeft <= snapThreshold.x) { - const scale = (2 * (position.x - T) - yContribW) / (baseWidth * cosR); - if (Math.abs(scale) > MIN_SCALE) xCandidates.push({ scale, distance: distLeft, line, edge: "left" }); - } - const distRight = Math.abs(currentRightEdge - T); - if (distRight <= snapThreshold.x) { - const scale = (2 * (T - position.x) - yContribW) / (baseWidth * cosR); - if (Math.abs(scale) > MIN_SCALE) xCandidates.push({ scale, distance: distRight, line, edge: "right" }); - } - } - } - - if (sinR > EPSILON) { - for (const T of [canvasTop, 0, canvasBottom]) { - const line: SnapLine = { type: "horizontal", position: T }; - const distTop = Math.abs(currentTopEdge - T); - if (distTop <= snapThreshold.y) { - const scale = (2 * (position.y - T) - yContribH) / (baseWidth * sinR); - if (Math.abs(scale) > MIN_SCALE) xCandidates.push({ scale, distance: distTop, line, edge: "top" }); - } - const distBottom = Math.abs(currentBottomEdge - T); - if (distBottom <= snapThreshold.y) { - const scale = (2 * (T - position.y) - yContribH) / (baseWidth * sinR); - if (Math.abs(scale) > MIN_SCALE) xCandidates.push({ scale, distance: distBottom, line, edge: "bottom" }); - } - } - } - - // sY candidates: snap via vertical targets — only valid when sinR ≠ 0 - // snap via horizontal targets — only valid when cosR ≠ 0 - const yCandidates: Candidate[] = []; - const xContribW = baseWidth * proposedScaleX * cosR; - const xContribH = baseWidth * proposedScaleX * sinR; - - if (sinR > EPSILON) { - for (const T of [canvasLeft, 0, canvasRight]) { - const line: SnapLine = { type: "vertical", position: T }; - const distLeft = Math.abs(currentLeftEdge - T); - if (distLeft <= snapThreshold.x) { - const scale = (2 * (position.x - T) - xContribW) / (baseHeight * sinR); - if (Math.abs(scale) > MIN_SCALE) yCandidates.push({ scale, distance: distLeft, line, edge: "left" }); - } - const distRight = Math.abs(currentRightEdge - T); - if (distRight <= snapThreshold.x) { - const scale = (2 * (T - position.x) - xContribW) / (baseHeight * sinR); - if (Math.abs(scale) > MIN_SCALE) yCandidates.push({ scale, distance: distRight, line, edge: "right" }); - } - } - } - - if (cosR > EPSILON) { - for (const T of [canvasTop, 0, canvasBottom]) { - const line: SnapLine = { type: "horizontal", position: T }; - const distTop = Math.abs(currentTopEdge - T); - if (distTop <= snapThreshold.y) { - const scale = (2 * (position.y - T) - xContribH) / (baseHeight * cosR); - if (Math.abs(scale) > MIN_SCALE) yCandidates.push({ scale, distance: distTop, line, edge: "top" }); - } - const distBottom = Math.abs(currentBottomEdge - T); - if (distBottom <= snapThreshold.y) { - const scale = (2 * (T - position.y) - xContribH) / (baseHeight * cosR); - if (Math.abs(scale) > MIN_SCALE) yCandidates.push({ scale, distance: distBottom, line, edge: "bottom" }); - } - } - } - - return { - x: bestCandidate({ candidates: xCandidates, proposedScale: proposedScaleX }), - y: bestCandidate({ candidates: yCandidates, proposedScale: proposedScaleY }), - }; -} - -export interface RotationSnapResult { - snappedRotation: number; - isSnapped: boolean; -} - -export function snapRotation({ - proposedRotation, -}: { - proposedRotation: number; -}): RotationSnapResult { - const nearestRotationSnap = - Math.round(proposedRotation / ROTATION_SNAP_STEP_DEGREES) * - ROTATION_SNAP_STEP_DEGREES; - const distanceToNearestSnap = Math.abs( - proposedRotation - nearestRotationSnap, - ); - if (distanceToNearestSnap <= ROTATION_SNAP_THRESHOLD_DEGREES) { - return { snappedRotation: nearestRotationSnap, isSnapped: true }; - } - return { snappedRotation: proposedRotation, isSnapped: false }; -} -- cgit v1.3.1