From eea6d43c82e1fd09d94d73001090dad205b1f1d2 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Sun, 26 Apr 2026 02:50:04 +0200 Subject: fix: model editor time as wasm MediaTime ticks end-to-end --- .../hooks/element/use-element-interaction.ts | 70 ++++++++++++++-------- .../timeline/hooks/element/use-keyframe-drag.ts | 57 ++++++++++++------ 2 files changed, 84 insertions(+), 43 deletions(-) (limited to 'apps/web/src/timeline/hooks/element') diff --git a/apps/web/src/timeline/hooks/element/use-element-interaction.ts b/apps/web/src/timeline/hooks/element/use-element-interaction.ts index 621259b9..9caca28a 100644 --- a/apps/web/src/timeline/hooks/element/use-element-interaction.ts +++ b/apps/web/src/timeline/hooks/element/use-element-interaction.ts @@ -17,7 +17,14 @@ import { type MoveGroup, } from "@/timeline/group-move"; import { BASE_TIMELINE_PIXELS_PER_SECOND } from "@/timeline/scale"; -import { TICKS_PER_SECOND } from "@/wasm"; +import { + addMediaTime, + type MediaTime, + mediaTime, + subMediaTime, + TICKS_PER_SECOND, + ZERO_MEDIA_TIME, +} from "@/wasm"; import { TIMELINE_DRAG_THRESHOLD_PX } from "@/timeline/components/interaction"; import { roundToFrame } from "opencut-wasm"; import { computeDropTarget } from "@/timeline/components/drop-target"; @@ -54,9 +61,9 @@ const initialDragState: ElementDragState = { trackId: null, startMouseX: 0, startMouseY: 0, - startElementTime: 0, - clickOffsetTime: 0, - currentTime: 0, + startElementTime: ZERO_MEDIA_TIME, + clickOffsetTime: ZERO_MEDIA_TIME, + currentTime: ZERO_MEDIA_TIME, currentMouseY: 0, }; @@ -66,8 +73,8 @@ interface PendingDragState { selectedElements: ElementRef[]; startMouseX: number; startMouseY: number; - startElementTime: number; - clickOffsetTime: number; + startElementTime: MediaTime; + clickOffsetTime: MediaTime; } function getClickOffsetTime({ @@ -78,10 +85,12 @@ function getClickOffsetTime({ clientX: number; elementRect: DOMRect; zoomLevel: number; -}): number { +}): MediaTime { const clickOffsetX = clientX - elementRect.left; const seconds = clickOffsetX / (BASE_TIMELINE_PIXELS_PER_SECOND * zoomLevel); - return Math.round(seconds * TICKS_PER_SECOND); + return mediaTime({ + ticks: Math.round(seconds * TICKS_PER_SECOND), + }); } function getVerticalDragDirection({ @@ -118,7 +127,7 @@ function getDragDropTarget({ tracksScrollRef: RefObject; headerRef?: RefObject; zoomLevel: number; - snappedTime: number; + snappedTime: MediaTime; verticalDragDirection?: "up" | "down" | null; }): DropTarget | null { const containerRect = tracksContainerRef.current?.getBoundingClientRect(); @@ -162,7 +171,7 @@ interface StartDragParams ElementDragState, "isDragging" | "currentTime" | "currentMouseY" > { - initialCurrentTime: number; + initialCurrentTime: MediaTime; initialCurrentMouseY: number; } @@ -249,7 +258,7 @@ export function useElementInteraction({ dropTarget, }: { group: MoveGroup; - snappedTime: number; + snappedTime: MediaTime; dropTarget: DropTarget | null; }): GroupMoveResult | null => { if (!dropTarget) { @@ -317,7 +326,7 @@ export function useElementInteraction({ frameSnappedTime, group, }: { - frameSnappedTime: number; + frameSnappedTime: MediaTime; group: MoveGroup | null; }) => { if (!group || !snappingEnabled || isShiftHeldRef.current) { @@ -366,15 +375,19 @@ export function useElementInteraction({ zoomLevel, scrollLeft, }); - const adjustedTime = Math.max( - 0, - mouseTime - pendingDragRef.current.clickOffsetTime, - ); - const snappedTime = + const adjustedTime = + mouseTime > pendingDragRef.current.clickOffsetTime + ? subMediaTime({ + a: mouseTime, + b: pendingDragRef.current.clickOffsetTime, + }) + : ZERO_MEDIA_TIME; + const snappedTime = ( roundToFrame({ time: adjustedTime, rate: activeProject.settings.fps, - }) ?? adjustedTime; + }) ?? adjustedTime + ) as MediaTime; const moveGroup = buildMoveGroup({ anchorRef: { trackId: pendingDragRef.current.trackId, @@ -389,7 +402,7 @@ export function useElementInteraction({ moveGroupRef.current = moveGroup; newTrackIdsRef.current = moveGroup.members.map(() => generateUUID()); - const dragTimeOffsets: Record = {}; + const dragTimeOffsets: Record = {}; for (const member of moveGroup.members) { dragTimeOffsets[member.elementId] = member.timeOffset; } @@ -478,10 +491,17 @@ export function useElementInteraction({ zoomLevel, scrollLeft, }); - const adjustedTime = Math.max(0, mouseTime - dragState.clickOffsetTime); + const adjustedTime = + mouseTime > dragState.clickOffsetTime + ? subMediaTime({ + a: mouseTime, + b: dragState.clickOffsetTime, + }) + : ZERO_MEDIA_TIME; const fps = activeProject.settings.fps; - const frameSnappedTime = - roundToFrame({ time: adjustedTime, rate: fps }) ?? adjustedTime; + const frameSnappedTime = ( + roundToFrame({ time: adjustedTime, rate: fps }) ?? adjustedTime + ) as MediaTime; const moveGroup = moveGroupRef.current; const { snappedTime, snapPoint } = getDragSnapResult({ @@ -596,8 +616,10 @@ export function useElementInteraction({ const currentMember = moveGroup.members.find( (member) => member.elementId === move.elementId, ); - const originalStartTime = - dragState.startElementTime + (currentMember?.timeOffset ?? 0); + const originalStartTime = addMediaTime({ + a: dragState.startElementTime, + b: currentMember?.timeOffset ?? ZERO_MEDIA_TIME, + }); return ( currentMember?.trackId !== move.targetTrackId || originalStartTime !== move.newStartTime diff --git a/apps/web/src/timeline/hooks/element/use-keyframe-drag.ts b/apps/web/src/timeline/hooks/element/use-keyframe-drag.ts index 92b6f0d8..afead4e9 100644 --- a/apps/web/src/timeline/hooks/element/use-keyframe-drag.ts +++ b/apps/web/src/timeline/hooks/element/use-keyframe-drag.ts @@ -11,7 +11,15 @@ import { useKeyframeSelection } from "./use-keyframe-selection"; import { roundToFrame, snappedSeekTime } from "opencut-wasm"; import { timelineTimeToSnappedPixels } from "@/timeline"; import { BASE_TIMELINE_PIXELS_PER_SECOND } from "@/timeline/scale"; -import { TICKS_PER_SECOND } from "@/wasm"; +import { + addMediaTime, + type MediaTime, + maxMediaTime, + mediaTime, + minMediaTime, + TICKS_PER_SECOND, + ZERO_MEDIA_TIME, +} from "@/wasm"; import { TIMELINE_DRAG_THRESHOLD_PX } from "@/timeline/components/interaction"; import { RetimeKeyframeCommand } from "@/commands/timeline/element/keyframes/retime-keyframe"; import { BatchCommand } from "@/commands"; @@ -22,13 +30,13 @@ import { registerCanceller } from "@/editor/cancel-interaction"; export interface KeyframeDragState { isDragging: boolean; draggingKeyframeIds: Set; - deltaTime: number; + deltaTime: MediaTime; } const initialDragState: KeyframeDragState = { isDragging: false, draggingKeyframeIds: new Set(), - deltaTime: 0, + deltaTime: ZERO_MEDIA_TIME, }; interface PendingKeyframeDrag { @@ -43,7 +51,7 @@ export function useKeyframeDrag({ }: { zoomLevel: number; element: TimelineElement; - displayedStartTime: number; + displayedStartTime: MediaTime; }) { const editor = useEditor(); const { @@ -83,7 +91,7 @@ export function useKeyframeDrag({ deltaTime, }: { keyframeRefs: SelectedKeyframeRef[]; - deltaTime: number; + deltaTime: MediaTime; }) => { const commands: Command[] = keyframeRefs.flatMap((keyframeRef) => { const keyframe = getKeyframeById({ @@ -92,10 +100,13 @@ export function useKeyframeDrag({ keyframeId: keyframeRef.keyframeId, }); if (!keyframe) return []; - const nextTime = Math.max( - 0, - Math.min(element.duration, keyframe.time + deltaTime), - ); + const nextTime = maxMediaTime({ + a: ZERO_MEDIA_TIME, + b: minMediaTime({ + a: element.duration, + b: addMediaTime({ a: keyframe.time, b: deltaTime }), + }), + }); return [ new RetimeKeyframeCommand({ trackId: keyframeRef.trackId, @@ -138,7 +149,7 @@ export function useKeyframeDrag({ draggingKeyframeIds: new Set( pending.keyframeRefs.map((keyframe) => keyframe.keyframeId), ), - deltaTime: 0, + deltaTime: ZERO_MEDIA_TIME, }); return; } @@ -146,11 +157,14 @@ export function useKeyframeDrag({ if (!dragState.isDragging) return; const startX = mouseDownXRef.current ?? clientX; - const rawDelta = Math.round( - ((clientX - startX) / pixelsPerSecond) * TICKS_PER_SECOND, - ); - const snappedDelta = - roundToFrame({ time: rawDelta, rate: fps }) ?? rawDelta; + const rawDelta = mediaTime({ + ticks: Math.round( + ((clientX - startX) / pixelsPerSecond) * TICKS_PER_SECOND, + ), + }); + const snappedDelta = ( + roundToFrame({ time: rawDelta, rate: fps }) ?? rawDelta + ) as MediaTime; setDragState((previous) => ({ ...previous, deltaTime: snappedDelta })); }; @@ -246,7 +260,7 @@ export function useKeyframeDrag({ event: ReactMouseEvent; keyframes: SelectedKeyframeRef[]; orderedKeyframes: SelectedKeyframeRef[]; - indicatorTime: number; + indicatorTime: MediaTime; }) => { event.stopPropagation(); @@ -259,12 +273,17 @@ export function useKeyframeDrag({ if (wasDrag) return; const duration = editor.timeline.getTotalDuration(); - const seekTime = + const absoluteIndicatorTime = addMediaTime({ + a: displayedStartTime, + b: indicatorTime, + }); + const seekTime = ( snappedSeekTime({ - time: displayedStartTime + indicatorTime, + time: absoluteIndicatorTime, duration, rate: fps, - }) ?? displayedStartTime + indicatorTime; + }) ?? absoluteIndicatorTime + ) as MediaTime; editor.playback.seek({ time: seekTime }); if (event.shiftKey) { -- cgit v1.3.1