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 --- .../panels/preview/bookmark-note-overlay.tsx | 59 --- .../editor/panels/preview/context-menu.tsx | 73 --- .../components/editor/panels/preview/cursors.ts | 21 - .../editor/panels/preview/guide-overlay.tsx | 32 -- .../editor/panels/preview/guide-popover.tsx | 100 ---- .../editor/panels/preview/handle-primitives.tsx | 427 --------------- .../src/components/editor/panels/preview/index.tsx | 294 ----------- .../editor/panels/preview/mask-handles.tsx | 308 ----------- .../panels/preview/preview-interaction-overlay.tsx | 103 ---- .../editor/panels/preview/preview-viewport.tsx | 576 --------------------- .../editor/panels/preview/snap-guides.tsx | 47 -- .../editor/panels/preview/text-edit-overlay.tsx | 158 ------ .../components/editor/panels/preview/toolbar.tsx | 148 ------ .../editor/panels/preview/transform-handles.tsx | 145 ------ 14 files changed, 2491 deletions(-) delete mode 100644 apps/web/src/components/editor/panels/preview/bookmark-note-overlay.tsx delete mode 100644 apps/web/src/components/editor/panels/preview/context-menu.tsx delete mode 100644 apps/web/src/components/editor/panels/preview/cursors.ts delete mode 100644 apps/web/src/components/editor/panels/preview/guide-overlay.tsx delete mode 100644 apps/web/src/components/editor/panels/preview/guide-popover.tsx delete mode 100644 apps/web/src/components/editor/panels/preview/handle-primitives.tsx delete mode 100644 apps/web/src/components/editor/panels/preview/index.tsx delete mode 100644 apps/web/src/components/editor/panels/preview/mask-handles.tsx delete mode 100644 apps/web/src/components/editor/panels/preview/preview-interaction-overlay.tsx delete mode 100644 apps/web/src/components/editor/panels/preview/preview-viewport.tsx delete mode 100644 apps/web/src/components/editor/panels/preview/snap-guides.tsx delete mode 100644 apps/web/src/components/editor/panels/preview/text-edit-overlay.tsx delete mode 100644 apps/web/src/components/editor/panels/preview/toolbar.tsx delete mode 100644 apps/web/src/components/editor/panels/preview/transform-handles.tsx (limited to 'apps/web/src/components/editor/panels/preview') diff --git a/apps/web/src/components/editor/panels/preview/bookmark-note-overlay.tsx b/apps/web/src/components/editor/panels/preview/bookmark-note-overlay.tsx deleted file mode 100644 index 3d96f762..00000000 --- a/apps/web/src/components/editor/panels/preview/bookmark-note-overlay.tsx +++ /dev/null @@ -1,59 +0,0 @@ -"use client"; - -import { useEditor } from "@/hooks/use-editor"; -import { findCurrentScene } from "@/lib/scenes"; -import { getBookmarksActiveAtTime } from "@/lib/timeline/bookmarks"; - -export function BookmarkNoteOverlay() { - const editor = useEditor(); - const currentTime = editor.playback.getCurrentTime(); - const activeProject = editor.project.getActive(); - - if (!activeProject) { - return null; - } - - const activeScene = findCurrentScene({ - scenes: activeProject.scenes, - currentSceneId: activeProject.currentSceneId, - }); - - if (!activeScene) { - return null; - } - - const bookmarks = activeScene.bookmarks; - const activeBookmarks = getBookmarksActiveAtTime({ - bookmarks, - time: currentTime, - }); - const bookmarksWithNotes = activeBookmarks.filter( - (bookmark) => bookmark.note != null && bookmark.note.trim() !== "", - ); - - if (bookmarksWithNotes.length === 0) { - return null; - } - - return ( -
- {bookmarksWithNotes.map((bookmark) => ( -
- {bookmark.note} -
- ))} -
- ); -} diff --git a/apps/web/src/components/editor/panels/preview/context-menu.tsx b/apps/web/src/components/editor/panels/preview/context-menu.tsx deleted file mode 100644 index 677f35db..00000000 --- a/apps/web/src/components/editor/panels/preview/context-menu.tsx +++ /dev/null @@ -1,73 +0,0 @@ -"use client"; - -import { - ContextMenuCheckboxItem, - ContextMenuContent, - ContextMenuItem, - ContextMenuSeparator, -} from "@/components/ui/context-menu"; -import { usePreviewViewport } from "@/components/editor/panels/preview/preview-viewport"; -import { useEditor } from "@/hooks/use-editor"; -import { usePreviewStore } from "@/stores/preview-store"; -import { toast } from "sonner"; - -export function PreviewContextMenu({ - onToggleFullscreen, - containerRef, -}: { - onToggleFullscreen: () => void; - containerRef: React.RefObject; -}) { - const editor = useEditor(); - const viewport = usePreviewViewport(); - const { overlays, setOverlayVisibility } = usePreviewStore(); - - const handleCopySnapshot = async () => { - const result = await editor.renderer.copySnapshot(); - - if (!result.success) { - toast.error("Failed to copy snapshot", { - description: result.error ?? "Please try again", - }); - return; - } - }; - - const handleSaveSnapshot = async () => { - const result = await editor.renderer.saveSnapshot(); - - if (!result.success) { - toast.error("Failed to save snapshot", { - description: result.error ?? "Please try again", - }); - return; - } - }; - - return ( - - - Fit to screen - - - - Full screen - - - Save snapshot - - - Copy snapshot - - - - setOverlayVisibility({ overlay: "bookmarks", isVisible: !!checked }) - } - > - Show bookmark notes - - - ); -} diff --git a/apps/web/src/components/editor/panels/preview/cursors.ts b/apps/web/src/components/editor/panels/preview/cursors.ts deleted file mode 100644 index f04dbef0..00000000 --- a/apps/web/src/components/editor/panels/preview/cursors.ts +++ /dev/null @@ -1,21 +0,0 @@ -function svgCursor({ - svg, - hotspotX, - hotspotY, -}: { - svg: string; - hotspotX: number; - hotspotY: number; -}): string { - return `url("data:image/svg+xml,${encodeURIComponent(svg)}") ${hotspotX} ${hotspotY}, crosshair`; -} - -/** Hotspot is at the nib tip, which is where anchor points land. */ -export const PEN_CURSOR = svgCursor({ - svg: ` - - -`, - hotspotX: 1, - hotspotY: 1, -}); diff --git a/apps/web/src/components/editor/panels/preview/guide-overlay.tsx b/apps/web/src/components/editor/panels/preview/guide-overlay.tsx deleted file mode 100644 index 4270fcb3..00000000 --- a/apps/web/src/components/editor/panels/preview/guide-overlay.tsx +++ /dev/null @@ -1,32 +0,0 @@ -"use client"; - -import { getGuideById } from "@/lib/guides"; -import { usePreviewStore } from "@/stores/preview-store"; -import { usePreviewViewport } from "./preview-viewport"; - -export function GuideOverlay() { - const activeGuide = usePreviewStore((state) => state.activeGuide); - const viewport = usePreviewViewport(); - const guide = getGuideById(activeGuide); - - if (!guide) { - return null; - } - - return ( -
- {guide.renderOverlay({ - width: viewport.sceneWidth, - height: viewport.sceneHeight, - })} -
- ); -} diff --git a/apps/web/src/components/editor/panels/preview/guide-popover.tsx b/apps/web/src/components/editor/panels/preview/guide-popover.tsx deleted file mode 100644 index bcac8075..00000000 --- a/apps/web/src/components/editor/panels/preview/guide-popover.tsx +++ /dev/null @@ -1,100 +0,0 @@ -"use client"; - -import { AnimatePresence, motion } from "motion/react"; -import { GUIDE_REGISTRY, getGuideById } from "@/lib/guides"; -import { usePreviewStore } from "@/stores/preview-store"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; -import { Label } from "@/components/ui/label"; -import { Separator } from "@/components/ui/separator"; -import { cn } from "@/utils/ui"; - -export function GridPopover({ children }: { children: React.ReactNode }) { - const activeGuide = usePreviewStore((state) => state.activeGuide); - const toggleGuide = usePreviewStore((state) => state.toggleGuide); - const activeGuideDef = getGuideById(activeGuide); - const options = activeGuideDef?.renderOptions?.(); - - return ( - - {children} - -
- -
- {GUIDE_REGISTRY.map((guide) => ( - toggleGuide(guide.id)} - /> - ))} -
-
- - {options && ( - - -
{options}
-
- )} -
-
-
- ); -} - -function GridItem({ - label, - preview, - isSelected, - onClick, -}: { - label: string; - preview: React.ReactNode; - isSelected?: boolean; - onClick: () => void; -}) { - return ( - - ); -} diff --git a/apps/web/src/components/editor/panels/preview/handle-primitives.tsx b/apps/web/src/components/editor/panels/preview/handle-primitives.tsx deleted file mode 100644 index 33baf9fc..00000000 --- a/apps/web/src/components/editor/panels/preview/handle-primitives.tsx +++ /dev/null @@ -1,427 +0,0 @@ -"use client"; - -import { cn } from "@/utils/ui"; -import { HugeiconsIcon } from "@hugeicons/react"; -import type { IconSvgElement } from "@hugeicons/react"; - -export const HANDLE_SIZE = 10; -export const HANDLE_HIT_AREA_SIZE = 18; -export const ICON_HANDLE_RADIUS = 10; -export const EDGE_HANDLE_THIN_SIZE = 6; -export const EDGE_HANDLE_THICK_SIZE = 14; -export const LINE_HIT_AREA_SIZE = 48; - -export function getResizeCursor({ angleDeg }: { angleDeg: number }): string { - const normalized = ((angleDeg % 180) + 180) % 180; - if (normalized < 22.5 || normalized >= 157.5) return "ew-resize"; - if (normalized < 67.5) return "nwse-resize"; - if (normalized < 112.5) return "ns-resize"; - return "nesw-resize"; -} - -export function HandleButton({ - screen, - cursor, - hitAreaSize, - className, - onPointerDown, - onPointerMove, - onPointerUp, - children, -}: { - screen: { x: number; y: number }; - cursor?: string; - hitAreaSize: number; - className?: string; - onPointerDown: (event: React.PointerEvent) => void; - onPointerMove: (event: React.PointerEvent) => void; - onPointerUp: (event: React.PointerEvent) => void; - children: React.ReactNode; -}) { - return ( - - ); -} - -export function CornerHandle({ - cursor, - screen, - onPointerDown, - onPointerMove, - onPointerUp, -}: { - cursor?: string; - screen: { x: number; y: number }; - onPointerDown: (event: React.PointerEvent) => void; - onPointerMove: (event: React.PointerEvent) => void; - onPointerUp: (event: React.PointerEvent) => void; -}) { - return ( - -
- - ); -} - -export function CircleHandle({ - cursor, - screen, - size = HANDLE_SIZE, - isSelected = false, - onPointerDown, - onPointerMove, - onPointerUp, -}: { - cursor?: string; - screen: { x: number; y: number }; - size?: number; - isSelected?: boolean; - onPointerDown: (event: React.PointerEvent) => void; - onPointerMove: (event: React.PointerEvent) => void; - onPointerUp: (event: React.PointerEvent) => void; -}) { - return ( - -
- - ); -} - -export function EdgeHandle({ - edge, - screen, - rotation, - onPointerDown, - onPointerMove, - onPointerUp, -}: { - edge: "right" | "left" | "bottom"; - screen: { x: number; y: number }; - rotation: number; - onPointerDown: (event: React.PointerEvent) => void; - onPointerMove: (event: React.PointerEvent) => void; - onPointerUp: (event: React.PointerEvent) => void; -}) { - const isHorizontalEdge = edge === "right" || edge === "left"; - const width = isHorizontalEdge - ? EDGE_HANDLE_THIN_SIZE - : EDGE_HANDLE_THICK_SIZE; - const height = isHorizontalEdge - ? EDGE_HANDLE_THICK_SIZE - : EDGE_HANDLE_THIN_SIZE; - const cursor = getResizeCursor({ - angleDeg: isHorizontalEdge ? rotation : rotation + 90, - }); - - return ( - -
- - ); -} - -export function IconHandle({ - icon, - screen, - onPointerDown, - onPointerMove, - onPointerUp, -}: { - icon: IconSvgElement; - screen: { x: number; y: number }; - onPointerDown: (event: React.PointerEvent) => void; - onPointerMove: (event: React.PointerEvent) => void; - onPointerUp: (event: React.PointerEvent) => void; -}) { - return ( - - - - ); -} - -export function BoundingBoxOutline({ - center, - outlineWidth, - outlineHeight, - rotation, - cursor, - dashed = false, - onPointerDown, - onPointerMove, - onPointerUp, -}: { - center: { x: number; y: number }; - outlineWidth: number; - outlineHeight: number; - rotation: number; - cursor?: string; - dashed?: boolean; - onPointerDown?: (event: React.PointerEvent) => void; - onPointerMove?: (event: React.PointerEvent) => void; - onPointerUp?: (event: React.PointerEvent) => void; -}) { - return ( - - ); -} - -export function ShapeOutline({ - center, - outlineWidth, - outlineHeight, - rotation, - pathData, - cursor, - onPointerDown, - onPointerMove, - onPointerUp, -}: { - center: { x: number; y: number }; - outlineWidth: number; - outlineHeight: number; - rotation: number; - pathData: string; - cursor?: string; - onPointerDown?: (event: React.PointerEvent) => void; - onPointerMove?: (event: React.PointerEvent) => void; - onPointerUp?: (event: React.PointerEvent) => void; -}) { - return ( - - ); -} - -export function CanvasPathOutline({ - pathData, - translateX = 0, - translateY = 0, - scaleX = 1, - scaleY = 1, - cursor, - strokeWidth = 1, - strokeOpacity = 0.75, - onPointerDown, - onPointerMove, - onPointerUp, -}: { - pathData: string; - translateX?: number; - translateY?: number; - scaleX?: number; - scaleY?: number; - cursor?: string; - strokeWidth?: number; - strokeOpacity?: number; - onPointerDown?: (event: React.PointerEvent) => void; - onPointerMove?: (event: React.PointerEvent) => void; - onPointerUp?: (event: React.PointerEvent) => void; -}) { - return ( - - ); -} - -export function LineOverlay({ - start, - end, - cursor, - onPointerDown, - onPointerMove, - onPointerUp, -}: { - start: { x: number; y: number }; - end: { x: number; y: number }; - cursor?: string; - onPointerDown?: (event: React.PointerEvent) => void; - onPointerMove?: (event: React.PointerEvent) => void; - onPointerUp?: (event: React.PointerEvent) => void; -}) { - const dx = end.x - start.x; - const dy = end.y - start.y; - const length = Math.sqrt(dx * dx + dy * dy); - const angleDeg = (Math.atan2(dy, dx) * 180) / Math.PI; - const cx = (start.x + end.x) / 2; - const cy = (start.y + end.y) / 2; - - const sharedStyle = { - left: cx - length / 2, - width: length, - transform: `rotate(${angleDeg}deg)`, - transformOrigin: "center center", - }; - - return ( - <> - {onPointerDown && ( -
- )} -
- - ); -} diff --git a/apps/web/src/components/editor/panels/preview/index.tsx b/apps/web/src/components/editor/panels/preview/index.tsx deleted file mode 100644 index 369d854c..00000000 --- a/apps/web/src/components/editor/panels/preview/index.tsx +++ /dev/null @@ -1,294 +0,0 @@ -"use client"; - -import { useCallback, useEffect, useMemo, useRef } from "react"; -import useDeepCompareEffect from "use-deep-compare-effect"; -import { useEditor } from "@/hooks/use-editor"; -import { useRafLoop } from "@/hooks/use-raf-loop"; -import { useContainerSize } from "@/hooks/use-container-size"; -import { useFullscreen } from "@/hooks/use-fullscreen"; -import { CanvasRenderer } from "@/services/renderer/canvas-renderer"; -import { TICKS_PER_SECOND } from "@/lib/wasm"; -import type { RootNode } from "@/services/renderer/nodes/root-node"; -import { buildScene } from "@/services/renderer/scene-builder"; -import { PreviewInteractionOverlay } from "./preview-interaction-overlay"; -import { BookmarkNoteOverlay } from "./bookmark-note-overlay"; -import { GuideOverlay } from "./guide-overlay"; -import { ContextMenu, ContextMenuTrigger } from "@/components/ui/context-menu"; -import { usePreviewStore } from "@/stores/preview-store"; -import { PreviewContextMenu } from "./context-menu"; -import { PreviewToolbar } from "./toolbar"; -import { - PreviewViewportProvider, - usePreviewViewportState, -} from "./preview-viewport"; - -function usePreviewSize() { - const canvasSize = useEditor( - (e) => e.project.getActive()?.settings.canvasSize, - ); - - return { - width: canvasSize?.width, - height: canvasSize?.height, - }; -} - -function normalizeWheelDelta({ - delta, - deltaMode, - pageSize, -}: { - delta: number; - deltaMode: number; - pageSize: number; -}): number { - if (deltaMode === WheelEvent.DOM_DELTA_LINE) { - return delta * 16; - } - - if (deltaMode === WheelEvent.DOM_DELTA_PAGE) { - return delta * pageSize; - } - - return delta; -} - -export function PreviewPanel() { - const containerRef = useRef(null); - const { toggleFullscreen } = useFullscreen({ containerRef }); - - return ( -
- - -
- ); -} - -function RenderTreeController() { - const editor = useEditor(); - const tracks = useEditor( - (e) => e.timeline.getPreviewTracks() ?? e.scenes.getActiveScene().tracks, - ); - const mediaAssets = useEditor((e) => e.media.getAssets()); - const activeProject = useEditor((e) => e.project.getActive()); - - const { width, height } = usePreviewSize(); - - useDeepCompareEffect(() => { - if (!activeProject) return; - - const duration = editor.timeline.getTotalDuration(); - const renderTree = buildScene({ - tracks, - mediaAssets, - duration, - canvasSize: { width, height }, - background: activeProject.settings.background, - isPreview: true, - }); - - editor.renderer.setRenderTree({ renderTree }); - }, [tracks, mediaAssets, activeProject?.settings.background, width, height]); - - return null; -} - -function PreviewCanvas({ - containerRef, - onToggleFullscreen, -}: { - containerRef: React.RefObject; - onToggleFullscreen: () => void; -}) { - const canvasRef = useRef(null); - const viewportRef = useRef(null); - const lastFrameRef = useRef(-1); - const lastSceneRef = useRef(null); - const renderingRef = useRef(false); - const { width: nativeWidth, height: nativeHeight } = usePreviewSize(); - const viewportSize = useContainerSize({ containerRef: viewportRef }); - const editor = useEditor(); - const activeProject = useEditor((e) => e.project.getActive()); - const renderTree = useEditor((e) => e.renderer.getRenderTree()); - const { overlays } = usePreviewStore(); - const viewport = usePreviewViewportState({ - canvasHeight: nativeHeight, - canvasWidth: nativeWidth, - viewportHeight: viewportSize.height, - viewportRef, - viewportWidth: viewportSize.width, - }); - - const renderer = useMemo(() => { - return new CanvasRenderer({ - width: nativeWidth, - height: nativeHeight, - fps: activeProject.settings.fps, - }); - }, [nativeWidth, nativeHeight, activeProject.settings.fps.numerator, activeProject.settings.fps.denominator]); - - const render = useCallback(() => { - if (canvasRef.current && renderTree && !renderingRef.current) { - const renderTime = Math.min( - editor.playback.getCurrentTime(), - editor.timeline.getLastFrameTime(), - ); - const ticksPerFrame = Math.round(TICKS_PER_SECOND * renderer.fps.denominator / renderer.fps.numerator); - const frame = Math.floor(renderTime / ticksPerFrame); - - if ( - frame !== lastFrameRef.current || - renderTree !== lastSceneRef.current - ) { - renderingRef.current = true; - lastSceneRef.current = renderTree; - lastFrameRef.current = frame; - renderer - .renderToCanvas({ - node: renderTree, - time: renderTime, - targetCanvas: canvasRef.current, - }) - .then(() => { - renderingRef.current = false; - }); - } - } - }, [renderer, renderTree, editor.playback]); - - useRafLoop(render); - - useEffect(() => { - const container = viewportRef.current; - if (!container) return; - - let pendingZoomDelta = 0; - let pendingPanDeltaX = 0; - let pendingPanDeltaY = 0; - let zoomRafId: ReturnType | null = null; - let panRafId: ReturnType | null = null; - - const onWheel = (event: WheelEvent) => { - const normalizedDeltaX = normalizeWheelDelta({ - delta: event.deltaX, - deltaMode: event.deltaMode, - pageSize: container.clientWidth, - }); - const normalizedDeltaY = normalizeWheelDelta({ - delta: event.deltaY, - deltaMode: event.deltaMode, - pageSize: container.clientHeight, - }); - const isZoomGesture = event.ctrlKey || event.metaKey; - if (isZoomGesture) { - event.preventDefault(); - pendingZoomDelta += normalizedDeltaY; - - if (zoomRafId === null) { - zoomRafId = requestAnimationFrame(() => { - const cappedDelta = - Math.sign(pendingZoomDelta) * - Math.min(Math.abs(pendingZoomDelta), 30); - const zoomFactor = Math.exp(-cappedDelta / 300); - - viewport.scaleZoom({ factor: zoomFactor }); - pendingZoomDelta = 0; - zoomRafId = null; - }); - } - - return; - } - - if (!viewport.canPan) { - return; - } - - if (normalizedDeltaX === 0 && normalizedDeltaY === 0) { - return; - } - - event.preventDefault(); - pendingPanDeltaX += normalizedDeltaX; - pendingPanDeltaY += normalizedDeltaY; - - if (panRafId === null) { - panRafId = requestAnimationFrame(() => { - viewport.panByScreenDelta({ - deltaX: pendingPanDeltaX, - deltaY: pendingPanDeltaY, - }); - pendingPanDeltaX = 0; - pendingPanDeltaY = 0; - panRafId = null; - }); - } - }; - - container.addEventListener("wheel", onWheel, { - capture: true, - passive: false, - }); - - return () => { - container.removeEventListener("wheel", onWheel, { - capture: true, - }); - if (zoomRafId !== null) { - cancelAnimationFrame(zoomRafId); - } - if (panRafId !== null) { - cancelAnimationFrame(panRafId); - } - }; - }, [viewport.canPan, viewport.panByScreenDelta, viewport.scaleZoom]); - - return ( - -
-
- - -
- - - - {overlays.bookmarks && } -
-
- -
-
- -
-
- ); -} diff --git a/apps/web/src/components/editor/panels/preview/mask-handles.tsx b/apps/web/src/components/editor/panels/preview/mask-handles.tsx deleted file mode 100644 index d32b6ff9..00000000 --- a/apps/web/src/components/editor/panels/preview/mask-handles.tsx +++ /dev/null @@ -1,308 +0,0 @@ -"use client"; - -import { PEN_CURSOR } from "@/components/editor/panels/preview/cursors"; -import { usePreviewViewport } from "@/components/editor/panels/preview/preview-viewport"; -import { useMaskHandles } from "@/hooks/use-mask-handles"; -import type { SnapLine } from "@/lib/preview/preview-snap"; -import { - CornerHandle, - CircleHandle, - CanvasPathOutline, - EdgeHandle, - IconHandle, - LineOverlay, - BoundingBoxOutline, - ShapeOutline, -} from "./handle-primitives"; - -const CUSTOM_MASK_ANCHOR_SIZE = 7; -const CUSTOM_MASK_TANGENT_SIZE = 6; -import { Rotate01Icon, FeatherIcon } from "@hugeicons/core-free-icons"; - -export function MaskHandles({ - onSnapLinesChange, -}: { - onSnapLinesChange?: (lines: SnapLine[]) => void; -}) { - const viewport = usePreviewViewport(); - const { - selectedWithMask, - handlePositions, - overlays, - isCreatingCustomMask, - handleCanvasPointerDown, - handlePointerDown, - handlePointerMove, - handlePointerUp, - } = useMaskHandles({ onSnapLinesChange }); - - if (!selectedWithMask) return null; - - const toOverlay = ({ - canvasX, - canvasY, - }: { - canvasX: number; - canvasY: number; - }) => - viewport.canvasToOverlay({ - canvasX, - canvasY, - }); - - const { x: scaleX, y: scaleY } = viewport.getDisplayScale(); - const canvasOrigin = toOverlay({ canvasX: 0, canvasY: 0 }); - - const onPointerMove = (event: React.PointerEvent) => { - if (viewport.handlePanPointerMove({ event })) { - return; - } - - handlePointerMove({ event }); - }; - const onPointerUp = (event: React.PointerEvent) => { - if (viewport.handlePanPointerUp({ event })) { - return; - } - - handlePointerUp(); - }; - const handleMaskPointerDown = ({ - event, - handleId, - }: { - event: React.PointerEvent; - handleId: string; - }) => { - if (viewport.handlePanPointerDown({ event })) { - return; - } - - handlePointerDown({ event, handleId }); - }; - const handleCanvasOverlayPointerDown = (event: React.PointerEvent) => { - if (viewport.handlePanPointerDown({ event })) { - return; - } - - handleCanvasPointerDown({ event }); - }; - - return ( -
- {isCreatingCustomMask ? ( -
- ) : null} - {overlays.map((overlay) => { - const overlayHandleId = overlay.handleId; - const pointerHandlers = overlayHandleId - ? { - onPointerDown: (event: React.PointerEvent) => - handleMaskPointerDown({ event, handleId: overlayHandleId }), - onPointerMove, - onPointerUp, - } - : {}; - - if (overlay.type === "line") { - return ( - - ); - } - - if (overlay.type === "rect") { - return ( - - ); - } - - if (overlay.type === "shape") { - return ( - - ); - } - - if (overlay.type === "canvas-path") { - return ( - - ); - } - - return null; - })} - {handlePositions.map((handle) => { - const screen = toOverlay({ canvasX: handle.x, canvasY: handle.y }); - - if (handle.kind === "icon" && handle.icon === "rotate") { - return ( - - handleMaskPointerDown({ event, handleId: handle.id }) - } - onPointerMove={onPointerMove} - onPointerUp={onPointerUp} - /> - ); - } - - if (handle.kind === "icon" && handle.icon === "feather") { - return ( - - handleMaskPointerDown({ event, handleId: handle.id }) - } - onPointerMove={onPointerMove} - onPointerUp={onPointerUp} - /> - ); - } - - if (handle.kind === "edge" && handle.edgeAxis === "horizontal") { - return ( - - handleMaskPointerDown({ event, handleId: handle.id }) - } - onPointerMove={onPointerMove} - onPointerUp={onPointerUp} - /> - ); - } - - if (handle.kind === "edge" && handle.edgeAxis === "vertical") { - return ( - - handleMaskPointerDown({ event, handleId: handle.id }) - } - onPointerMove={onPointerMove} - onPointerUp={onPointerUp} - /> - ); - } - - if (handle.kind === "point" || handle.kind === "tangent") { - return ( - - handleMaskPointerDown({ event, handleId: handle.id }) - } - onPointerMove={onPointerMove} - onPointerUp={onPointerUp} - /> - ); - } - - if (handle.kind === "corner") { - return ( - - handleMaskPointerDown({ event, handleId: handle.id }) - } - onPointerMove={onPointerMove} - onPointerUp={onPointerUp} - /> - ); - } - - return ( - - handleMaskPointerDown({ event, handleId: handle.id }) - } - onPointerMove={onPointerMove} - onPointerUp={onPointerUp} - /> - ); - })} -
- ); -} diff --git a/apps/web/src/components/editor/panels/preview/preview-interaction-overlay.tsx b/apps/web/src/components/editor/panels/preview/preview-interaction-overlay.tsx deleted file mode 100644 index 6ee4b584..00000000 --- a/apps/web/src/components/editor/panels/preview/preview-interaction-overlay.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { useState } from "react"; -import { usePreviewViewport } from "@/components/editor/panels/preview/preview-viewport"; -import { usePreviewInteraction } from "@/hooks/use-preview-interaction"; -import type { SnapLine } from "@/lib/preview/preview-snap"; -import { TransformHandles } from "./transform-handles"; -import { MaskHandles } from "./mask-handles"; -import { SnapGuides } from "./snap-guides"; -import { TextEditOverlay } from "./text-edit-overlay"; -import { usePropertiesStore } from "../properties/stores/properties-store"; -import { useEditor } from "@/hooks/use-editor"; - -export function PreviewInteractionOverlay() { - const [snapLines, setSnapLines] = useState([]); - const editor = useEditor(); - const viewport = usePreviewViewport(); - const selectedElements = useEditor((e) => e.selection.getSelectedElements()); - const activeTabPerType = usePropertiesStore((s) => s.activeTabPerType); - - const selectedRef = - selectedElements.length === 1 ? selectedElements[0] : null; - const activeTrack = selectedRef - ? editor.timeline.getTrackById({ trackId: selectedRef.trackId }) - : null; - const activeElement = - activeTrack?.elements.find( - (element) => element.id === selectedRef?.elementId, - ) ?? null; - const isMaskMode = activeElement - ? activeTabPerType[activeElement.type] === "masks" - : false; - - const { - onPointerDown, - onPointerMove, - onPointerUp, - onDoubleClick, - editingText, - commitTextEdit, - } = usePreviewInteraction({ - onSnapLinesChange: setSnapLines, - isMaskMode, - }); - - const handlePointerDown = (event: React.PointerEvent) => { - if (viewport.handlePanPointerDown({ event })) { - return; - } - - onPointerDown(event); - }; - - const handlePointerMove = (event: React.PointerEvent) => { - if (viewport.handlePanPointerMove({ event })) { - return; - } - - onPointerMove(event); - }; - - const handlePointerUp = (event: React.PointerEvent) => { - if (viewport.handlePanPointerUp({ event })) { - return; - } - - onPointerUp(event); - }; - - return ( -
-
e.preventDefault()} - /> - {editingText ? ( - - ) : isMaskMode ? ( - - ) : ( - - )} - -
- ); -} diff --git a/apps/web/src/components/editor/panels/preview/preview-viewport.tsx b/apps/web/src/components/editor/panels/preview/preview-viewport.tsx deleted file mode 100644 index 2ac6bd92..00000000 --- a/apps/web/src/components/editor/panels/preview/preview-viewport.tsx +++ /dev/null @@ -1,576 +0,0 @@ -"use client"; - -import { - createContext, - useCallback, - useContext, - useEffect, - useMemo, - useRef, - useState, -} from "react"; -import { - canvasToOverlay, - getDisplayScale, - positionToOverlay, - screenPixelsToLogicalThreshold, - screenToCanvas, -} from "@/lib/preview/preview-coords"; -import { clamp, isNearlyEqual } from "@/utils/math"; -import { PREVIEW_ZOOM } from "@/lib/preview/zoom"; - -const MIDDLE_MOUSE_BUTTON = 1; -const IS_AT_FIT_EPSILON = 0.001; -const IS_AT_ACTUAL_SIZE_EPSILON = 0.01; - -interface PreviewViewportContextValue { - canPan: boolean; - isAtFit: boolean; - isAtActualSize: boolean; - isPanning: boolean; - sceneHeight: number; - sceneLeft: number; - sceneTop: number; - sceneWidth: number; - zoomPercent: number; - canvasToOverlay: ({ - canvasX, - canvasY, - }: { - canvasX: number; - canvasY: number; - }) => { x: number; y: number }; - fitToScreen: () => void; - getDisplayScale: () => { x: number; y: number }; - handlePanPointerDown: ({ event }: { event: React.PointerEvent }) => boolean; - handlePanPointerMove: ({ event }: { event: React.PointerEvent }) => boolean; - handlePanPointerUp: ({ event }: { event: React.PointerEvent }) => boolean; - positionToOverlay: ({ - positionX, - positionY, - }: { - positionX: number; - positionY: number; - }) => { x: number; y: number }; - panByScreenDelta: ({ - deltaX, - deltaY, - }: { - deltaX: number; - deltaY: number; - }) => void; - resetPan: () => void; - scaleZoom: ({ factor }: { factor: number }) => void; - screenPixelsToLogicalThreshold: ({ - screenPixels, - }: { - screenPixels: number; - }) => { x: number; y: number }; - screenToCanvas: ({ - clientX, - clientY, - }: { - clientX: number; - clientY: number; - }) => { x: number; y: number } | null; - setActualSize: () => void; - setViewportPercent: ({ percent }: { percent: number }) => void; - zoomIn: () => void; - zoomOut: () => void; -} - -const PreviewViewportContext = - createContext(null); - -interface PreviewViewportProviderProps { - children: React.ReactNode; - value: PreviewViewportContextValue; -} - -interface PreviewViewportStateOptions { - canvasHeight: number; - canvasWidth: number; - viewportHeight: number; - viewportRef: React.RefObject; - viewportWidth: number; -} - -interface PanSession { - centerX: number; - centerY: number; - clientX: number; - clientY: number; - pointerId: number; -} - -function getClampedCenterAxis({ - axisSize, - center, - scale, - viewportSize, -}: { - axisSize: number; - center: number; - scale: number; - viewportSize: number; -}): number { - if (axisSize <= 0 || scale <= 0 || viewportSize <= 0) { - return axisSize / 2; - } - - const visibleHalfSpan = viewportSize / (2 * scale); - if (visibleHalfSpan >= axisSize / 2) { - return axisSize / 2; - } - - return clamp({ - value: center, - min: visibleHalfSpan, - max: axisSize - visibleHalfSpan, - }); -} - -function clampViewportCenter({ - canvasHeight, - canvasWidth, - centerX, - centerY, - scale, - viewportHeight, - viewportWidth, -}: { - canvasHeight: number; - canvasWidth: number; - centerX: number; - centerY: number; - scale: number; - viewportHeight: number; - viewportWidth: number; -}): { x: number; y: number } { - return { - x: getClampedCenterAxis({ - center: centerX, - axisSize: canvasWidth, - scale, - viewportSize: viewportWidth, - }), - y: getClampedCenterAxis({ - center: centerY, - axisSize: canvasHeight, - scale, - viewportSize: viewportHeight, - }), - }; -} - -function getFitScale({ - canvasHeight, - canvasWidth, - viewportHeight, - viewportWidth, -}: { - canvasHeight: number; - canvasWidth: number; - viewportHeight: number; - viewportWidth: number; -}): number { - if ( - canvasHeight <= 0 || - canvasWidth <= 0 || - viewportHeight <= 0 || - viewportWidth <= 0 - ) { - return 1; - } - - return Math.min(viewportWidth / canvasWidth, viewportHeight / canvasHeight); -} - -function getClampedZoom({ zoom }: { zoom: number }): number { - return clamp({ - value: zoom, - min: PREVIEW_ZOOM.min, - max: PREVIEW_ZOOM.max, - }); -} - -export function PreviewViewportProvider({ - children, - value, -}: PreviewViewportProviderProps) { - return ( - - {children} - - ); -} - -export function usePreviewViewportState({ - canvasHeight, - canvasWidth, - viewportHeight, - viewportRef, - viewportWidth, -}: PreviewViewportStateOptions): PreviewViewportContextValue { - const [zoom, setZoomState] = useState(1); - const [center, setCenter] = useState(() => ({ - x: canvasWidth / 2, - y: canvasHeight / 2, - })); - const [isPanning, setIsPanning] = useState(false); - const panSessionRef = useRef(null); - const centerRef = useRef(center); - centerRef.current = center; - - const fitScale = useMemo( - () => - getFitScale({ - canvasHeight, - canvasWidth, - viewportHeight, - viewportWidth, - }), - [canvasHeight, canvasWidth, viewportHeight, viewportWidth], - ); - const viewportScale = fitScale * zoom; - const geometry = useMemo( - () => ({ - canvasHeight, - canvasWidth, - centerX: center.x, - centerY: center.y, - scale: viewportScale, - viewportHeight, - viewportWidth, - }), - [ - canvasHeight, - canvasWidth, - center.x, - center.y, - viewportHeight, - viewportScale, - viewportWidth, - ], - ); - - const scaleZoom = useCallback(({ factor }: { factor: number }) => { - setZoomState((previousZoom) => - getClampedZoom({ - zoom: previousZoom * factor, - }), - ); - }, []); - - const panByScreenDelta = useCallback( - ({ deltaX, deltaY }: { deltaX: number; deltaY: number }) => { - if (zoom <= 1 || (deltaX === 0 && deltaY === 0)) { - return; - } - - setCenter((previousCenter) => - clampViewportCenter({ - canvasHeight, - canvasWidth, - centerX: previousCenter.x + deltaX / viewportScale, - centerY: previousCenter.y + deltaY / viewportScale, - scale: viewportScale, - viewportHeight, - viewportWidth, - }), - ); - }, - [ - canvasHeight, - canvasWidth, - viewportHeight, - viewportScale, - viewportWidth, - zoom, - ], - ); - - const resetPan = useCallback(() => { - setCenter({ - x: canvasWidth / 2, - y: canvasHeight / 2, - }); - }, [canvasHeight, canvasWidth]); - - const fitToScreen = useCallback(() => { - setZoomState(1); - setCenter({ - x: canvasWidth / 2, - y: canvasHeight / 2, - }); - }, [canvasHeight, canvasWidth]); - - const zoomIn = useCallback(() => { - setZoomState((previousZoom) => - getClampedZoom({ - zoom: previousZoom * PREVIEW_ZOOM.step, - }), - ); - }, []); - - const zoomOut = useCallback(() => { - setZoomState((previousZoom) => - getClampedZoom({ - zoom: previousZoom / PREVIEW_ZOOM.step, - }), - ); - }, []); - - const setActualSize = useCallback(() => { - const actualSizeZoom = fitScale > 0 ? 1 / fitScale : 1; - setZoomState( - getClampedZoom({ - zoom: actualSizeZoom, - }), - ); - }, [fitScale]); - - const setViewportPercent = useCallback( - ({ percent }: { percent: number }) => { - const targetZoom = fitScale > 0 ? percent / 100 / fitScale : 1; - setZoomState(getClampedZoom({ zoom: targetZoom })); - }, - [fitScale], - ); - - const getViewportRect = useCallback( - () => viewportRef.current?.getBoundingClientRect() ?? null, - [viewportRef], - ); - - const screenToCanvasWithViewport = useCallback( - ({ clientX, clientY }: { clientX: number; clientY: number }) => { - const viewportRect = getViewportRect(); - if (!viewportRect) return null; - - return screenToCanvas({ - clientX, - clientY, - geometry, - viewportRect, - }); - }, - [getViewportRect, geometry], - ); - - const canvasToOverlayWithViewport = useCallback( - ({ canvasX, canvasY }: { canvasX: number; canvasY: number }) => - canvasToOverlay({ - canvasX, - canvasY, - geometry, - }), - [geometry], - ); - - const positionToOverlayWithViewport = useCallback( - ({ positionX, positionY }: { positionX: number; positionY: number }) => - positionToOverlay({ - geometry, - positionX, - positionY, - }), - [geometry], - ); - - const getDisplayScaleWithViewport = useCallback( - () => getDisplayScale({ geometry }), - [geometry], - ); - - const getLogicalThresholdForScreenPixels = useCallback( - ({ screenPixels }: { screenPixels: number }) => - screenPixelsToLogicalThreshold({ - geometry, - screenPixels, - }), - [geometry], - ); - - const handlePanPointerDown = useCallback( - ({ event }: { event: React.PointerEvent }) => { - if (event.button !== MIDDLE_MOUSE_BUTTON || zoom <= 1) { - return false; - } - - event.preventDefault(); - event.stopPropagation(); - - panSessionRef.current = { - centerX: centerRef.current.x, - centerY: centerRef.current.y, - clientX: event.clientX, - clientY: event.clientY, - pointerId: event.pointerId, - }; - setIsPanning(true); - (event.currentTarget as HTMLElement).setPointerCapture(event.pointerId); - return true; - }, - [zoom], - ); - - const handlePanPointerMove = useCallback( - ({ event }: { event: React.PointerEvent }) => { - const panSession = panSessionRef.current; - if (!panSession) { - return false; - } - - event.preventDefault(); - event.stopPropagation(); - - const deltaX = event.clientX - panSession.clientX; - const deltaY = event.clientY - panSession.clientY; - const nextCenter = clampViewportCenter({ - canvasHeight, - canvasWidth, - centerX: panSession.centerX - deltaX / viewportScale, - centerY: panSession.centerY - deltaY / viewportScale, - scale: viewportScale, - viewportHeight, - viewportWidth, - }); - - setCenter(nextCenter); - return true; - }, - [canvasHeight, canvasWidth, viewportHeight, viewportScale, viewportWidth], - ); - - const handlePanPointerUp = useCallback( - ({ event }: { event: React.PointerEvent }) => { - const panSession = panSessionRef.current; - if (!panSession) { - return false; - } - - if ( - (event.currentTarget as HTMLElement).hasPointerCapture( - panSession.pointerId, - ) - ) { - (event.currentTarget as HTMLElement).releasePointerCapture( - panSession.pointerId, - ); - } - - panSessionRef.current = null; - setIsPanning(false); - return true; - }, - [], - ); - - useEffect(() => { - setZoomState(1); - setCenter({ - x: canvasWidth / 2, - y: canvasHeight / 2, - }); - panSessionRef.current = null; - setIsPanning(false); - }, [canvasHeight, canvasWidth]); - - useEffect(() => { - setCenter((previousCenter) => - clampViewportCenter({ - canvasHeight, - canvasWidth, - centerX: previousCenter.x, - centerY: previousCenter.y, - scale: viewportScale, - viewportHeight, - viewportWidth, - }), - ); - }, [canvasHeight, canvasWidth, viewportHeight, viewportScale, viewportWidth]); - - const sceneWidth = canvasWidth * viewportScale; - const sceneHeight = canvasHeight * viewportScale; - const sceneLeft = viewportWidth / 2 - center.x * viewportScale; - const sceneTop = viewportHeight / 2 - center.y * viewportScale; - const canPan = zoom > 1; - const zoomPercent = Math.round(viewportScale * 100); - - return useMemo( - () => ({ - canPan, - isAtActualSize: isNearlyEqual({ - leftValue: viewportScale, - rightValue: 1, - epsilon: IS_AT_ACTUAL_SIZE_EPSILON, - }), - isAtFit: isNearlyEqual({ - leftValue: zoom, - rightValue: 1, - epsilon: IS_AT_FIT_EPSILON, - }), - isPanning, - sceneHeight, - sceneLeft, - sceneTop, - sceneWidth, - zoomPercent, - canvasToOverlay: canvasToOverlayWithViewport, - fitToScreen, - getDisplayScale: getDisplayScaleWithViewport, - handlePanPointerDown, - handlePanPointerMove, - handlePanPointerUp, - panByScreenDelta, - positionToOverlay: positionToOverlayWithViewport, - resetPan, - scaleZoom, - screenPixelsToLogicalThreshold: getLogicalThresholdForScreenPixels, - screenToCanvas: screenToCanvasWithViewport, - setActualSize, - setViewportPercent, - zoomIn, - zoomOut, - }), - [ - canPan, - viewportScale, - zoom, - isPanning, - sceneHeight, - sceneLeft, - sceneTop, - sceneWidth, - zoomPercent, - canvasToOverlayWithViewport, - fitToScreen, - getDisplayScaleWithViewport, - handlePanPointerDown, - handlePanPointerMove, - handlePanPointerUp, - panByScreenDelta, - positionToOverlayWithViewport, - resetPan, - scaleZoom, - getLogicalThresholdForScreenPixels, - screenToCanvasWithViewport, - setActualSize, - setViewportPercent, - zoomIn, - zoomOut, - ], - ); -} - -export function usePreviewViewport() { - const context = useContext(PreviewViewportContext); - if (!context) { - throw new Error( - "usePreviewViewport must be used within PreviewViewportProvider", - ); - } - - return context; -} diff --git a/apps/web/src/components/editor/panels/preview/snap-guides.tsx b/apps/web/src/components/editor/panels/preview/snap-guides.tsx deleted file mode 100644 index ced5e921..00000000 --- a/apps/web/src/components/editor/panels/preview/snap-guides.tsx +++ /dev/null @@ -1,47 +0,0 @@ -"use client"; - -import { usePreviewViewport } from "@/components/editor/panels/preview/preview-viewport"; -import type { SnapLine } from "@/lib/preview/preview-snap"; - -export function SnapGuides({ lines }: { lines: SnapLine[] }) { - const viewport = usePreviewViewport(); - - if (lines.length === 0) { - return null; - } - - const toOverlayX = (logicalX: number) => - viewport.positionToOverlay({ - positionX: logicalX, - positionY: 0, - }).x; - - const toOverlayY = (logicalY: number) => - viewport.positionToOverlay({ - positionX: 0, - positionY: logicalY, - }).y; - - return ( -
- {lines.map((line) => { - if (line.type === "vertical") { - return ( -
- ); - } - return ( -
- ); - })} -
- ); -} diff --git a/apps/web/src/components/editor/panels/preview/text-edit-overlay.tsx b/apps/web/src/components/editor/panels/preview/text-edit-overlay.tsx deleted file mode 100644 index eabbde8a..00000000 --- a/apps/web/src/components/editor/panels/preview/text-edit-overlay.tsx +++ /dev/null @@ -1,158 +0,0 @@ -"use client"; - -import { useCallback, useEffect, useRef } from "react"; -import { usePreviewViewport } from "@/components/editor/panels/preview/preview-viewport"; -import { useEditor } from "@/hooks/use-editor"; -import type { TextElement } from "@/lib/timeline"; -import { DEFAULTS } from "@/lib/timeline/defaults"; -import { - getElementLocalTime, - resolveTransformAtTime, -} from "@/lib/animation"; -import { resolveTextLayout } from "@/lib/text/primitives"; - -export function TextEditOverlay({ - trackId, - elementId, - element, - onCommit, -}: { - trackId: string; - elementId: string; - element: TextElement; - onCommit: () => void; -}) { - const editor = useEditor(); - const viewport = usePreviewViewport(); - const divRef = useRef(null); - - useEffect(() => { - const div = divRef.current; - if (!div) return; - div.focus(); - const range = document.createRange(); - range.selectNodeContents(div); - const selection = window.getSelection(); - selection?.removeAllRanges(); - selection?.addRange(range); - }, []); - - const handleInput = useCallback(() => { - const div = divRef.current; - if (!div) return; - const text = div.innerText; - editor.timeline.previewElements({ - updates: [{ trackId, elementId, updates: { content: text } }], - }); - }, [editor.timeline, trackId, elementId]); - - const handleKeyDown = useCallback( - ({ event }: { event: React.KeyboardEvent }) => { - const { key } = event; - if (key === "Escape") { - event.preventDefault(); - onCommit(); - return; - } - }, - [onCommit], - ); - - const canvasSize = editor.project.getActive().settings.canvasSize; - - if (!canvasSize) return null; - - const currentTime = editor.playback.getCurrentTime(); - const localTime = getElementLocalTime({ - timelineTime: currentTime, - elementStartTime: element.startTime, - elementDuration: element.duration, - }); - const transform = resolveTransformAtTime({ - baseTransform: element.transform, - animations: element.animations, - localTime, - }); - - const { x: posX, y: posY } = viewport.positionToOverlay({ - positionX: transform.position.x, - positionY: transform.position.y, - }); - - const { x: displayScaleX } = viewport.getDisplayScale(); - const resolvedTextLayout = resolveTextLayout({ - text: { - content: element.content, - fontSize: element.fontSize, - fontFamily: element.fontFamily, - fontWeight: element.fontWeight, - fontStyle: element.fontStyle, - textAlign: element.textAlign, - textDecoration: element.textDecoration, - letterSpacing: element.letterSpacing, - lineHeight: element.lineHeight, - }, - canvasHeight: canvasSize.height, - }); - - const lineHeight = element.lineHeight ?? DEFAULTS.text.lineHeight; - const canvasLetterSpacing = element.letterSpacing ?? 0; - const lineHeightPx = resolvedTextLayout.lineHeightPx; - - const bg = element.background; - const shouldShowBackground = - bg.enabled && bg.color && bg.color !== "transparent"; - const fontSizeRatio = resolvedTextLayout.fontSizeRatio; - const canvasPaddingX = shouldShowBackground - ? (bg.paddingX ?? DEFAULTS.text.background.paddingX) * fontSizeRatio - : 0; - const canvasPaddingY = shouldShowBackground - ? (bg.paddingY ?? DEFAULTS.text.background.paddingY) * fontSizeRatio - : 0; - - return ( -
- {/* biome-ignore lint/a11y/useSemanticElements: contenteditable required for multiline, IME, paste */} -
handleKeyDown({ event })} - > - {element.content || ""} -
-
- ); -} diff --git a/apps/web/src/components/editor/panels/preview/toolbar.tsx b/apps/web/src/components/editor/panels/preview/toolbar.tsx deleted file mode 100644 index 9ee6c822..00000000 --- a/apps/web/src/components/editor/panels/preview/toolbar.tsx +++ /dev/null @@ -1,148 +0,0 @@ -"use client"; - -import { useState, useEffect } from "react"; -import { useEditor } from "@/hooks/use-editor"; -import { formatTimecode } from "opencut-wasm"; -import { invokeAction } from "@/lib/actions"; -import { EditableTimecode } from "@/components/editable-timecode"; -import { Button } from "@/components/ui/button"; -import { - FullScreenIcon, - GridTableIcon, - PauseIcon, - PlayIcon, -} from "@hugeicons/core-free-icons"; -import { HugeiconsIcon } from "@hugeicons/react"; -import { getGuideById } from "@/lib/guides"; -import { Separator } from "@/components/ui/separator"; -import { - Select, - SelectTrigger, - SelectContent, - SelectItem, - SelectSeparator, -} from "@/components/ui/select"; -import { PREVIEW_ZOOM_PRESETS } from "@/lib/preview/zoom"; -import { usePreviewViewport } from "./preview-viewport"; -import { GridPopover } from "./guide-popover"; -import { usePreviewStore } from "@/stores/preview-store"; - -export function PreviewToolbar({ - onToggleFullscreen, -}: { - onToggleFullscreen: () => void; -}) { - const activeGuide = usePreviewStore((state) => state.activeGuide); - const activeGuideDefinition = getGuideById(activeGuide); - - return ( -
- - -
- - - {/* v0.4.0 */} - {/* - - */} - -
-
- ); -} - -function TimecodeDisplay() { - const editor = useEditor(); - const totalDuration = useEditor((e) => e.timeline.getTotalDuration()); - const fps = useEditor((e) => e.project.getActive().settings.fps); - const [currentTime, setCurrentTime] = useState(() => - editor.playback.getCurrentTime(), - ); - - useEffect(() => { - const handler = (e: Event) => - setCurrentTime((e as CustomEvent<{ time: number }>).detail.time); - window.addEventListener("playback-update", handler); - window.addEventListener("playback-seek", handler); - return () => { - window.removeEventListener("playback-update", handler); - window.removeEventListener("playback-seek", handler); - }; - }, []); - - return ( -
- editor.playback.seek({ time })} - className="text-center" - /> - / - - {formatTimecode({ time: totalDuration, format: "HH:MM:SS:FF", rate: fps })} - -
- ); -} - -function ZoomSelect() { - const { isAtFit, zoomPercent, fitToScreen, setViewportPercent } = - usePreviewViewport(); - - const displayLabel = isAtFit ? "Fit" : `${zoomPercent}%`; - - const onValueChange = (value: string) => { - if (value === "fit") { - fitToScreen(); - } else { - setViewportPercent({ percent: Number(value) }); - } - }; - - return ( - - ); -} - -function PlayPauseButton() { - const isPlaying = useEditor((e) => e.playback.getIsPlaying()); - - return ( - - ); -} diff --git a/apps/web/src/components/editor/panels/preview/transform-handles.tsx b/apps/web/src/components/editor/panels/preview/transform-handles.tsx deleted file mode 100644 index 2b81b557..00000000 --- a/apps/web/src/components/editor/panels/preview/transform-handles.tsx +++ /dev/null @@ -1,145 +0,0 @@ -"use client"; - -import { usePreviewViewport } from "@/components/editor/panels/preview/preview-viewport"; -import { useTransformHandles } from "@/hooks/use-transform-handles"; -import { isVisualElement } from "@/lib/timeline/element-utils"; -import { - getCornerPosition, - getEdgeHandlePosition, - ROTATION_HANDLE_OFFSET, - type Corner, - type Edge, -} from "@/lib/preview/element-bounds"; -import type { OnSnapLinesChange } from "@/hooks/use-preview-interaction"; -import { - BoundingBoxOutline, - CornerHandle, - EdgeHandle, - IconHandle, - getResizeCursor, -} from "./handle-primitives"; -import { Rotate01Icon } from "@hugeicons/core-free-icons"; - -const CORNERS: Corner[] = [ - "top-left", - "top-right", - "bottom-left", - "bottom-right", -]; -const EDGES: Edge[] = ["right", "left", "bottom"]; - -export function TransformHandles({ - onSnapLinesChange, -}: { - onSnapLinesChange?: OnSnapLinesChange; -}) { - const viewport = usePreviewViewport(); - const { - selectedWithBounds, - hasVisualSelection, - handleCornerPointerDown, - handleEdgePointerDown, - handleRotationPointerDown, - handlePointerMove, - handlePointerUp, - } = useTransformHandles({ onSnapLinesChange }); - - if (!hasVisualSelection || !selectedWithBounds) return null; - - const { bounds, element } = selectedWithBounds; - if (!isVisualElement(element)) return null; - - const displayScale = viewport.getDisplayScale(); - - const toOverlay = ({ - canvasX, - canvasY, - }: { - canvasX: number; - canvasY: number; - }) => - viewport.canvasToOverlay({ - canvasX, - canvasY, - }); - - const center = toOverlay({ canvasX: bounds.cx, canvasY: bounds.cy }); - const outlineWidth = Math.abs(bounds.width) * displayScale.x; - const outlineHeight = Math.abs(bounds.height) * displayScale.y; - - const rotationAngleRad = (bounds.rotation * Math.PI) / 180; - const topCenterLocalY = -bounds.height / 2; - const topCenterScreen = toOverlay({ - canvasX: bounds.cx - topCenterLocalY * Math.sin(rotationAngleRad), - canvasY: bounds.cy + topCenterLocalY * Math.cos(rotationAngleRad), - }); - const rotationHandleScreen = { - x: topCenterScreen.x + Math.sin(rotationAngleRad) * ROTATION_HANDLE_OFFSET, - y: topCenterScreen.y - Math.cos(rotationAngleRad) * ROTATION_HANDLE_OFFSET, - }; - - const onPointerMove = (event: React.PointerEvent) => - handlePointerMove({ event }); - const onPointerUp = () => handlePointerUp(); - - return ( -
- - {CORNERS.map((corner) => { - const cornerPosition = getCornerPosition({ bounds, corner }); - const screen = toOverlay({ - canvasX: cornerPosition.x, - canvasY: cornerPosition.y, - }); - const angleDeg = - Math.atan2(screen.y - center.y, screen.x - center.x) * - (180 / Math.PI); - return ( - - handleCornerPointerDown({ event, corner }) - } - onPointerMove={onPointerMove} - onPointerUp={onPointerUp} - /> - ); - })} - {EDGES.map((edge) => { - const edgePosition = getEdgeHandlePosition({ bounds, edge }); - const screen = toOverlay({ - canvasX: edgePosition.x, - canvasY: edgePosition.y, - }); - return ( - handleEdgePointerDown({ event, edge })} - onPointerMove={onPointerMove} - onPointerUp={onPointerUp} - /> - ); - })} - handleRotationPointerDown({ event })} - onPointerMove={onPointerMove} - onPointerUp={onPointerUp} - /> -
- ); -} -- cgit v1.3.1