From 7d4e166678c35d48b22b8d1d05639f981a956a8b Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Mon, 13 Apr 2026 04:41:33 +0200 Subject: fix: text measurement, rotation handle, video cache races, playback sync Made-with: Cursor --- .../editor/panels/preview/transform-handles.tsx | 285 +++++++++++---------- 1 file changed, 145 insertions(+), 140 deletions(-) (limited to 'apps/web/src/components/editor/panels/preview/transform-handles.tsx') diff --git a/apps/web/src/components/editor/panels/preview/transform-handles.tsx b/apps/web/src/components/editor/panels/preview/transform-handles.tsx index 141359c8..2b81b557 100644 --- a/apps/web/src/components/editor/panels/preview/transform-handles.tsx +++ b/apps/web/src/components/editor/panels/preview/transform-handles.tsx @@ -1,140 +1,145 @@ -"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, - getRotationHandlePosition, - 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 rotationHandleCanvas = getRotationHandlePosition({ bounds }); - const rotationHandleScreen = toOverlay({ - canvasX: rotationHandleCanvas.x, - canvasY: rotationHandleCanvas.y, - }); - - 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} - /> -
- ); -} +"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