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/hooks/use-transform-handles.ts | 631 ---------------------------- 1 file changed, 631 deletions(-) delete mode 100644 apps/web/src/hooks/use-transform-handles.ts (limited to 'apps/web/src/hooks/use-transform-handles.ts') diff --git a/apps/web/src/hooks/use-transform-handles.ts b/apps/web/src/hooks/use-transform-handles.ts deleted file mode 100644 index 7495a387..00000000 --- a/apps/web/src/hooks/use-transform-handles.ts +++ /dev/null @@ -1,631 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from "react"; -import { usePreviewViewport } from "@/components/editor/panels/preview/preview-viewport"; -import type { OnSnapLinesChange } from "@/hooks/use-preview-interaction"; -import { useEditor } from "@/hooks/use-editor"; -import { useShiftKey } from "@/hooks/use-shift-key"; -import { - getVisibleElementsWithBounds, - type ElementWithBounds, -} from "@/lib/preview/element-bounds"; -import { - MIN_SCALE, - SNAP_THRESHOLD_SCREEN_PIXELS, - snapRotation, - snapScale, - snapScaleAxes, - type ScaleEdgePreference, - type SnapLine, -} from "@/lib/preview/preview-snap"; -import { isVisualElement } from "@/lib/timeline/element-utils"; -import { - getElementLocalTime, - hasKeyframesForPath, - resolveTransformAtTime, - setChannel, -} from "@/lib/animation"; -import type { Transform } from "@/lib/rendering"; -import type { ElementAnimations } from "@/lib/animation/types"; -import { registerCanceller } from "@/lib/cancel-interaction"; - -type Corner = "top-left" | "top-right" | "bottom-left" | "bottom-right"; -type Edge = "right" | "left" | "bottom"; -type HandleType = Corner | Edge | "rotation"; - -function getPreferredEdge({ - edge, -}: { - edge: Edge; -}): ScaleEdgePreference { - return edge === "right" - ? { right: true } - : edge === "left" - ? { left: true } - : { bottom: true }; -} - -interface ScaleState { - trackId: string; - elementId: string; - initialTransform: Transform; - initialDistance: number; - initialBoundsCx: number; - initialBoundsCy: number; - baseWidth: number; - baseHeight: number; - shouldClearScaleAnimation: boolean; - animationsWithoutScale: ElementAnimations | undefined; -} - -interface RotationState { - trackId: string; - elementId: string; - initialTransform: Transform; - initialAngle: number; - initialBoundsCx: number; - initialBoundsCy: number; -} - -interface EdgeScaleState { - trackId: string; - elementId: string; - initialTransform: Transform; - initialBoundsCx: number; - initialBoundsCy: number; - baseWidth: number; - baseHeight: number; - edge: Edge; - rotationRad: number; - shouldClearScaleAnimation: boolean; - animationsWithoutScale: ElementAnimations | undefined; -} - -function clampScaleNonZero(scale: number): number { - if (Math.abs(scale) < MIN_SCALE) { - return scale < 0 ? -MIN_SCALE : MIN_SCALE; - } - return scale; -} - -function getCornerDistance({ - bounds, - corner, -}: { - bounds: { - cx: number; - cy: number; - width: number; - height: number; - rotation: number; - }; - corner: Corner; -}): number { - const halfWidth = bounds.width / 2; - const halfHeight = bounds.height / 2; - const angleRad = (bounds.rotation * Math.PI) / 180; - const cos = Math.cos(angleRad); - const sin = Math.sin(angleRad); - - const localX = - corner === "top-left" || corner === "bottom-left" ? -halfWidth : halfWidth; - const localY = - corner === "top-left" || corner === "top-right" ? -halfHeight : halfHeight; - - const rotatedX = localX * cos - localY * sin; - const rotatedY = localX * sin + localY * cos; - return Math.sqrt(rotatedX * rotatedX + rotatedY * rotatedY) || 1; -} - -export function useTransformHandles({ - onSnapLinesChange, -}: { - onSnapLinesChange?: OnSnapLinesChange; -}) { - const editor = useEditor(); - const isShiftHeldRef = useShiftKey(); - const viewport = usePreviewViewport(); - const [activeHandle, setActiveHandle] = useState(null); - const scaleStateRef = useRef(null); - const rotationStateRef = useRef(null); - const edgeScaleStateRef = useRef(null); - const captureRef = useRef<{ element: HTMLElement; pointerId: number } | null>( - null, - ); - - const selectedElements = useEditor((e) => e.selection.getSelectedElements()); - const tracks = useEditor( - (e) => e.timeline.getPreviewTracks() ?? e.scenes.getActiveScene().tracks, - ); - const currentTime = useEditor((e) => e.playback.getCurrentTime()); - const currentTimeRef = useRef(currentTime); - currentTimeRef.current = currentTime; - const mediaAssets = useEditor((e) => e.media.getAssets()); - const canvasSize = useEditor( - (e) => e.project.getActive().settings.canvasSize, - ); - - const elementsWithBounds = getVisibleElementsWithBounds({ - tracks, - currentTime, - canvasSize, - mediaAssets, - }); - - const selectedWithBounds: ElementWithBounds | null = - selectedElements.length === 1 - ? (elementsWithBounds.find( - (entry) => - entry.trackId === selectedElements[0].trackId && - entry.elementId === selectedElements[0].elementId, - ) ?? null) - : null; - - const hasVisualSelection = - selectedWithBounds !== null && isVisualElement(selectedWithBounds.element); - - const clearActiveHandleState = useCallback(() => { - scaleStateRef.current = null; - rotationStateRef.current = null; - edgeScaleStateRef.current = null; - setActiveHandle(null); - onSnapLinesChange?.([]); - }, [onSnapLinesChange]); - - const releaseCapturedPointer = useCallback(() => { - const capture = captureRef.current; - if (!capture) return; - - if (capture.element.hasPointerCapture(capture.pointerId)) { - capture.element.releasePointerCapture(capture.pointerId); - } - - captureRef.current = null; - }, []); - - useEffect(() => { - if (!activeHandle) return; - - return registerCanceller({ - fn: () => { - editor.timeline.discardPreview(); - clearActiveHandleState(); - releaseCapturedPointer(); - }, - }); - }, [activeHandle, clearActiveHandleState, editor.timeline, releaseCapturedPointer]); - - const handleCornerPointerDown = useCallback( - ({ event, corner }: { event: React.PointerEvent; corner: Corner }) => { - if (!selectedWithBounds) return; - event.stopPropagation(); - - const { bounds, trackId, elementId, element } = selectedWithBounds; - if (!isVisualElement(element)) return; - - const localTime = getElementLocalTime({ - timelineTime: currentTimeRef.current, - elementStartTime: element.startTime, - elementDuration: element.duration, - }); - const resolvedTransform = resolveTransformAtTime({ - baseTransform: element.transform, - animations: element.animations, - localTime, - }); - - const initialDistance = getCornerDistance({ bounds, corner }); - const baseWidth = bounds.width / resolvedTransform.scaleX; - const baseHeight = bounds.height / resolvedTransform.scaleY; - const shouldClearScaleAnimation = - hasKeyframesForPath({ - animations: element.animations, - propertyPath: "transform.scaleX", - }) || - hasKeyframesForPath({ - animations: element.animations, - propertyPath: "transform.scaleY", - }); - const animationsWithoutScale = shouldClearScaleAnimation - ? setChannel({ - animations: setChannel({ - animations: element.animations, - propertyPath: "transform.scaleX", - channel: undefined, - }), - propertyPath: "transform.scaleY", - channel: undefined, - }) - : element.animations; - - scaleStateRef.current = { - trackId, - elementId, - initialTransform: resolvedTransform, - initialDistance, - initialBoundsCx: bounds.cx, - initialBoundsCy: bounds.cy, - baseWidth, - baseHeight, - shouldClearScaleAnimation, - animationsWithoutScale, - }; - setActiveHandle(corner); - const captureTarget = event.currentTarget as HTMLElement; - captureTarget.setPointerCapture(event.pointerId); - captureRef.current = { - element: captureTarget, - pointerId: event.pointerId, - }; - }, - [selectedWithBounds], - ); - - const handleRotationPointerDown = useCallback( - ({ event }: { event: React.PointerEvent }) => { - if (!selectedWithBounds) return; - event.stopPropagation(); - - const { bounds, trackId, elementId, element } = selectedWithBounds; - if (!isVisualElement(element)) return; - - const localTime = getElementLocalTime({ - timelineTime: currentTimeRef.current, - elementStartTime: element.startTime, - elementDuration: element.duration, - }); - const resolvedTransform = resolveTransformAtTime({ - baseTransform: element.transform, - animations: element.animations, - localTime, - }); - - const position = viewport.screenToCanvas({ - clientX: event.clientX, - clientY: event.clientY, - }); - if (!position) return; - const deltaX = position.x - bounds.cx; - const deltaY = position.y - bounds.cy; - const initialAngle = (Math.atan2(deltaY, deltaX) * 180) / Math.PI; - - rotationStateRef.current = { - trackId, - elementId, - initialTransform: resolvedTransform, - initialAngle, - initialBoundsCx: bounds.cx, - initialBoundsCy: bounds.cy, - }; - setActiveHandle("rotation"); - const captureTarget = event.currentTarget as HTMLElement; - captureTarget.setPointerCapture(event.pointerId); - captureRef.current = { - element: captureTarget, - pointerId: event.pointerId, - }; - }, - [selectedWithBounds, viewport], - ); - - const handleEdgePointerDown = useCallback( - ({ event, edge }: { event: React.PointerEvent; edge: Edge }) => { - if (!selectedWithBounds) return; - event.stopPropagation(); - - const { bounds, trackId, elementId, element } = selectedWithBounds; - if (!isVisualElement(element)) return; - - const localTime = getElementLocalTime({ - timelineTime: currentTimeRef.current, - elementStartTime: element.startTime, - elementDuration: element.duration, - }); - const resolvedTransform = resolveTransformAtTime({ - baseTransform: element.transform, - animations: element.animations, - localTime, - }); - - const baseWidth = bounds.width / resolvedTransform.scaleX; - const baseHeight = bounds.height / resolvedTransform.scaleY; - const rotationRad = (bounds.rotation * Math.PI) / 180; - - const propertyPath = - edge === "right" || edge === "left" - ? "transform.scaleX" - : "transform.scaleY"; - const shouldClearScaleAnimation = - hasKeyframesForPath({ - animations: element.animations, - propertyPath, - }); - const animationsWithoutScale = shouldClearScaleAnimation - ? setChannel({ - animations: element.animations, - propertyPath, - channel: undefined, - }) - : element.animations; - - edgeScaleStateRef.current = { - trackId, - elementId, - initialTransform: resolvedTransform, - initialBoundsCx: bounds.cx, - initialBoundsCy: bounds.cy, - baseWidth, - baseHeight, - edge, - rotationRad, - shouldClearScaleAnimation, - animationsWithoutScale, - }; - setActiveHandle(edge); - const captureTarget = event.currentTarget as HTMLElement; - captureTarget.setPointerCapture(event.pointerId); - captureRef.current = { - element: captureTarget, - pointerId: event.pointerId, - }; - }, - [selectedWithBounds], - ); - - const handlePointerMove = useCallback( - ({ event }: { event: React.PointerEvent }) => { - if ( - !scaleStateRef.current && - !rotationStateRef.current && - !edgeScaleStateRef.current - ) - return; - - const position = viewport.screenToCanvas({ - clientX: event.clientX, - clientY: event.clientY, - }); - if (!position) return; - - if ( - scaleStateRef.current && - activeHandle && - activeHandle !== "rotation" - ) { - const { - trackId, - elementId, - initialTransform, - initialDistance, - initialBoundsCx, - initialBoundsCy, - baseWidth, - baseHeight, - shouldClearScaleAnimation, - animationsWithoutScale, - } = scaleStateRef.current; - - const deltaX = position.x - initialBoundsCx; - const deltaY = position.y - initialBoundsCy; - const currentDistance = - Math.sqrt(deltaX * deltaX + deltaY * deltaY) || 1; - const scaleFactor = currentDistance / initialDistance; - - // Use actual element dimensions (base * current scale) so snap - // computes the correct edges when scaleX ≠ scaleY - const effectiveWidth = baseWidth * initialTransform.scaleX; - const effectiveHeight = baseHeight * initialTransform.scaleY; - - const snapThreshold = viewport.screenPixelsToLogicalThreshold({ - screenPixels: SNAP_THRESHOLD_SCREEN_PIXELS, - }); - const { snappedScale: snappedFactor, activeLines } = - isShiftHeldRef.current - ? { snappedScale: scaleFactor, activeLines: [] as SnapLine[] } - : snapScale({ - proposedScale: scaleFactor, - position: initialTransform.position, - baseWidth: effectiveWidth, - baseHeight: effectiveHeight, - rotation: initialTransform.rotate, - canvasSize, - snapThreshold, - }); - - onSnapLinesChange?.(activeLines); - - editor.timeline.previewElements({ - updates: [ - { - trackId, - elementId, - updates: { - transform: { - ...initialTransform, - scaleX: clampScaleNonZero( - initialTransform.scaleX * snappedFactor, - ), - scaleY: clampScaleNonZero( - initialTransform.scaleY * snappedFactor, - ), - }, - ...(shouldClearScaleAnimation && { - animations: animationsWithoutScale, - }), - }, - }, - ], - }); - return; - } - - if ( - edgeScaleStateRef.current && - (activeHandle === "right" || - activeHandle === "left" || - activeHandle === "bottom") - ) { - const { - trackId, - elementId, - initialTransform, - initialBoundsCx, - initialBoundsCy, - baseWidth, - baseHeight, - edge, - rotationRad, - shouldClearScaleAnimation, - animationsWithoutScale, - } = edgeScaleStateRef.current; - - const deltaX = position.x - initialBoundsCx; - const deltaY = position.y - initialBoundsCy; - const xProjection = - deltaX * Math.cos(rotationRad) + deltaY * Math.sin(rotationRad); - const yProjection = - -deltaX * Math.sin(rotationRad) + deltaY * Math.cos(rotationRad); - const projection = - edge === "right" - ? xProjection - : edge === "left" - ? -xProjection - : yProjection; - - const baseAxisHalf = - edge === "right" || edge === "left" ? baseWidth / 2 : baseHeight / 2; - const proposedScale = clampScaleNonZero(projection / baseAxisHalf); - - const proposedScaleX = - edge === "right" || edge === "left" - ? proposedScale - : initialTransform.scaleX; - const proposedScaleY = - edge === "bottom" ? proposedScale : initialTransform.scaleY; - - const snapThreshold = viewport.screenPixelsToLogicalThreshold({ - screenPixels: SNAP_THRESHOLD_SCREEN_PIXELS, - }); - const { x: xSnap, y: ySnap } = isShiftHeldRef.current - ? { - x: { - snappedScale: proposedScaleX, - snapDistance: Infinity, - activeLines: [] as SnapLine[], - }, - y: { - snappedScale: proposedScaleY, - snapDistance: Infinity, - activeLines: [] as SnapLine[], - }, - } - : snapScaleAxes({ - proposedScaleX, - proposedScaleY, - position: initialTransform.position, - baseWidth, - baseHeight, - rotation: initialTransform.rotate, - canvasSize, - snapThreshold, - preferredEdges: getPreferredEdge({ edge }), - }); - - const relevantSnap = - edge === "right" || edge === "left" ? xSnap : ySnap; - onSnapLinesChange?.(relevantSnap.activeLines); - - editor.timeline.previewElements({ - updates: [ - { - trackId, - elementId, - updates: { - transform: { - ...initialTransform, - scaleX: - edge === "right" || edge === "left" - ? xSnap.snappedScale - : initialTransform.scaleX, - scaleY: - edge === "bottom" - ? ySnap.snappedScale - : initialTransform.scaleY, - }, - ...(shouldClearScaleAnimation && { - animations: animationsWithoutScale, - }), - }, - }, - ], - }); - return; - } - - if (rotationStateRef.current && activeHandle === "rotation") { - const { - trackId, - elementId, - initialTransform, - initialAngle, - initialBoundsCx, - initialBoundsCy, - } = rotationStateRef.current; - - const deltaX = position.x - initialBoundsCx; - const deltaY = position.y - initialBoundsCy; - const currentAngle = (Math.atan2(deltaY, deltaX) * 180) / Math.PI; - let deltaAngle = currentAngle - initialAngle; - if (deltaAngle > 180) deltaAngle -= 360; - if (deltaAngle < -180) deltaAngle += 360; - const newRotate = initialTransform.rotate + deltaAngle; - const { snappedRotation } = isShiftHeldRef.current - ? { snappedRotation: newRotate } - : snapRotation({ proposedRotation: newRotate }); - - editor.timeline.previewElements({ - updates: [ - { - trackId, - elementId, - updates: { - transform: { ...initialTransform, rotate: snappedRotation }, - }, - }, - ], - }); - } - }, - [ - activeHandle, - canvasSize, - editor, - isShiftHeldRef, - onSnapLinesChange, - viewport, - ], - ); - - const handlePointerUp = useCallback(() => { - if ( - scaleStateRef.current || - rotationStateRef.current || - edgeScaleStateRef.current - ) { - editor.timeline.commitPreview(); - clearActiveHandleState(); - } - releaseCapturedPointer(); - }, - [clearActiveHandleState, editor, releaseCapturedPointer], - ); - - return { - selectedWithBounds, - hasVisualSelection, - activeHandle, - handleCornerPointerDown, - handleEdgePointerDown, - handleRotationPointerDown, - handlePointerMove, - handlePointerUp, - }; -} -- cgit v1.3.1