summaryrefslogtreecommitdiff
path: root/apps/web/src/timeline/group-move
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-26 02:50:04 +0200
committerMaze Winther <mazewinther@gmail.com>2026-04-26 02:50:04 +0200
commiteea6d43c82e1fd09d94d73001090dad205b1f1d2 (patch)
tree384c92e467effb708bcc96e25117c064b18464b8 /apps/web/src/timeline/group-move
parentf1b45b4328cff6a016720ce7b38b87ac2992dc2d (diff)
downloadopencut-eea6d43c82e1fd09d94d73001090dad205b1f1d2.tar.gz
fix: model editor time as wasm MediaTime ticks end-to-end
Diffstat (limited to 'apps/web/src/timeline/group-move')
-rw-r--r--apps/web/src/timeline/group-move/build-group.ts6
-rw-r--r--apps/web/src/timeline/group-move/resolve-move.ts71
-rw-r--r--apps/web/src/timeline/group-move/snap.ts31
-rw-r--r--apps/web/src/timeline/group-move/types.ts7
4 files changed, 81 insertions, 34 deletions
diff --git a/apps/web/src/timeline/group-move/build-group.ts b/apps/web/src/timeline/group-move/build-group.ts
index 00f39d55..3e19e3e3 100644
--- a/apps/web/src/timeline/group-move/build-group.ts
+++ b/apps/web/src/timeline/group-move/build-group.ts
@@ -2,6 +2,7 @@ import type { ElementRef, SceneTracks } from "@/timeline";
import { findTrackInSceneTracks } from "@/timeline/track-element-update";
import type { GroupMember, MoveGroup } from "./types";
import { getTrackPlacementById } from "./track-placement";
+import { subMediaTime } from "@/wasm";
export function buildMoveGroup({
anchorRef,
@@ -59,7 +60,10 @@ export function buildMoveGroup({
elementId: element.id,
elementType: element.type,
duration: element.duration,
- timeOffset: element.startTime - anchorElement.startTime,
+ timeOffset: subMediaTime({
+ a: element.startTime,
+ b: anchorElement.startTime,
+ }),
trackSection: placement.section,
sectionIndex: placement.sectionIndex,
displayIndex: placement.displayIndex,
diff --git a/apps/web/src/timeline/group-move/resolve-move.ts b/apps/web/src/timeline/group-move/resolve-move.ts
index 60940315..8307aad0 100644
--- a/apps/web/src/timeline/group-move/resolve-move.ts
+++ b/apps/web/src/timeline/group-move/resolve-move.ts
@@ -12,6 +12,13 @@ import {
getTrackPlacementByDisplayIndex,
getTrackPlacementById,
} from "./track-placement";
+import {
+ addMediaTime,
+ maxMediaTime,
+ type MediaTime,
+ subMediaTime,
+ ZERO_MEDIA_TIME,
+} from "@/wasm";
type GroupMoveTarget =
| {
@@ -32,7 +39,7 @@ export function resolveGroupMove({
}: {
group: MoveGroup;
tracks: SceneTracks;
- anchorStartTime: number;
+ anchorStartTime: MediaTime;
target: GroupMoveTarget;
}): GroupMoveResult | null {
if (target.kind === "newTracks") {
@@ -61,7 +68,7 @@ function resolveExistingTrackMove({
}: {
group: MoveGroup;
tracks: SceneTracks;
- anchorStartTime: number;
+ anchorStartTime: MediaTime;
anchorTargetTrackId: string;
}): GroupMoveResult | null {
const anchorTargetPlacement = getTrackPlacementById({
@@ -93,7 +100,10 @@ function resolveExistingTrackMove({
targetTrackId:
targetTrackIdsByElementId.get(member.elementId) ?? member.trackId,
elementId: member.elementId,
- newStartTime: clampedAnchorStartTime + member.timeOffset,
+ newStartTime: addMediaTime({
+ a: clampedAnchorStartTime,
+ b: member.timeOffset,
+ }),
}));
if (!canApplyMovesToExistingTracks({ tracks, moves })) {
@@ -119,7 +129,7 @@ function resolveNewTrackMove({
}: {
group: MoveGroup;
tracks: SceneTracks;
- anchorStartTime: number;
+ anchorStartTime: MediaTime;
anchorInsertIndex: number;
newTrackIds: string[];
}): GroupMoveResult | null {
@@ -173,7 +183,10 @@ function resolveNewTrackMove({
sourceTrackId: member.trackId,
targetTrackId: newTrackIds[memberIndex],
elementId: member.elementId,
- newStartTime: clampedAnchorStartTime + member.timeOffset,
+ newStartTime: addMediaTime({
+ a: clampedAnchorStartTime,
+ b: member.timeOffset,
+ }),
}));
return {
@@ -335,17 +348,26 @@ function clampAnchorStartTime({
}: {
group: MoveGroup;
tracks: SceneTracks;
- anchorStartTime: number;
+ anchorStartTime: MediaTime;
targetTrackIdsByElementId: Map<string, string>;
-}): number {
- const minimumAnchorStartTime = Math.max(
- 0,
- ...group.members.map((member) => -member.timeOffset),
- );
- let clampedAnchorStartTime = Math.max(
- minimumAnchorStartTime,
- anchorStartTime,
+}): MediaTime {
+ const minimumAnchorStartTime = group.members.reduce(
+ (minimumStartTime, member) =>
+ member.timeOffset < ZERO_MEDIA_TIME
+ ? maxMediaTime({
+ a: minimumStartTime,
+ b: subMediaTime({
+ a: ZERO_MEDIA_TIME,
+ b: member.timeOffset,
+ }),
+ })
+ : minimumStartTime,
+ ZERO_MEDIA_TIME,
);
+ let clampedAnchorStartTime =
+ anchorStartTime < minimumAnchorStartTime
+ ? minimumAnchorStartTime
+ : anchorStartTime;
const memberOnMainTrack = group.members.find(
(member) =>
@@ -358,11 +380,13 @@ function clampAnchorStartTime({
const movingElementIds = new Set(
group.members.map((member) => member.elementId),
);
- const requestedMainStartTime =
- clampedAnchorStartTime + memberOnMainTrack.timeOffset;
+ const requestedMainStartTime = addMediaTime({
+ a: clampedAnchorStartTime,
+ b: memberOnMainTrack.timeOffset,
+ });
const earliestStationaryMainStartTime = tracks.main.elements
.filter((element) => !movingElementIds.has(element.id))
- .reduce<number | null>((earliestStartTime, element) => {
+ .reduce<MediaTime | null>((earliestStartTime, element) => {
if (earliestStartTime == null || element.startTime < earliestStartTime) {
return element.startTime;
}
@@ -373,10 +397,13 @@ function clampAnchorStartTime({
earliestStationaryMainStartTime == null ||
requestedMainStartTime <= earliestStationaryMainStartTime
) {
- clampedAnchorStartTime = Math.max(
- minimumAnchorStartTime,
- -memberOnMainTrack.timeOffset,
- );
+ clampedAnchorStartTime = maxMediaTime({
+ a: minimumAnchorStartTime,
+ b: subMediaTime({
+ a: ZERO_MEDIA_TIME,
+ b: memberOnMainTrack.timeOffset,
+ }),
+ });
}
return clampedAnchorStartTime;
@@ -422,7 +449,7 @@ function canApplyMovesToExistingTracks({
const sourceElement = sourceElements.get(move.elementId);
return {
startTime: move.newStartTime,
- duration: sourceElement?.duration ?? 0,
+ duration: sourceElement?.duration ?? ZERO_MEDIA_TIME,
};
});
if (hasOverlappingTimeSpans({ timeSpans })) {
diff --git a/apps/web/src/timeline/group-move/snap.ts b/apps/web/src/timeline/group-move/snap.ts
index 32c0a2f1..aa06a35b 100644
--- a/apps/web/src/timeline/group-move/snap.ts
+++ b/apps/web/src/timeline/group-move/snap.ts
@@ -9,6 +9,7 @@ import { getElementEdgeSnapPoints } from "@/timeline/element-snap-source";
import { getPlayheadSnapPoints } from "@/timeline/playhead-snap-source";
import { getAnimationKeyframeSnapPointsForTimeline } from "@/animation/timeline-snap-points";
import type { MoveGroup } from "./types";
+import { addMediaTime, type MediaTime, subMediaTime } from "@/wasm";
export function snapGroupEdges({
group,
@@ -18,12 +19,12 @@ export function snapGroupEdges({
zoomLevel,
}: {
group: MoveGroup;
- anchorStartTime: number;
+ anchorStartTime: MediaTime;
tracks: SceneTracks;
- playheadTime: number;
+ playheadTime: MediaTime;
zoomLevel: number;
}): {
- snappedAnchorStartTime: number;
+ snappedAnchorStartTime: MediaTime;
snapPoint: SnapPoint | null;
} {
const excludeElementIds = new Set(
@@ -47,7 +48,10 @@ export function snapGroupEdges({
let snapPoint: SnapPoint | null = null;
for (const member of group.members) {
- const memberStartTime = anchorStartTime + member.timeOffset;
+ const memberStartTime = addMediaTime({
+ a: anchorStartTime,
+ b: member.timeOffset,
+ });
const memberStartSnap = resolveTimelineSnap({
targetTime: memberStartTime,
snapPoints,
@@ -58,12 +62,18 @@ export function snapGroupEdges({
memberStartSnap.snapDistance < closestSnapDistance
) {
closestSnapDistance = memberStartSnap.snapDistance;
- snappedAnchorStartTime = memberStartSnap.snappedTime - member.timeOffset;
+ snappedAnchorStartTime = subMediaTime({
+ a: memberStartSnap.snappedTime as MediaTime,
+ b: member.timeOffset,
+ });
snapPoint = memberStartSnap.snapPoint;
}
const memberEndSnap = resolveTimelineSnap({
- targetTime: memberStartTime + member.duration,
+ targetTime: addMediaTime({
+ a: memberStartTime,
+ b: member.duration,
+ }),
snapPoints,
maxSnapDistance,
});
@@ -72,8 +82,13 @@ export function snapGroupEdges({
memberEndSnap.snapDistance < closestSnapDistance
) {
closestSnapDistance = memberEndSnap.snapDistance;
- snappedAnchorStartTime =
- memberEndSnap.snappedTime - member.duration - member.timeOffset;
+ snappedAnchorStartTime = subMediaTime({
+ a: subMediaTime({
+ a: memberEndSnap.snappedTime as MediaTime,
+ b: member.duration,
+ }),
+ b: member.timeOffset,
+ });
snapPoint = memberEndSnap.snapPoint;
}
}
diff --git a/apps/web/src/timeline/group-move/types.ts b/apps/web/src/timeline/group-move/types.ts
index daf9c6e9..f97b698c 100644
--- a/apps/web/src/timeline/group-move/types.ts
+++ b/apps/web/src/timeline/group-move/types.ts
@@ -1,11 +1,12 @@
import type { ElementRef, ElementType, TrackType } from "@/timeline";
+import type { MediaTime } from "@/wasm";
export type GroupTrackSection = "overlay" | "main" | "audio";
export interface GroupMember extends ElementRef {
elementType: ElementType;
- duration: number;
- timeOffset: number;
+ duration: MediaTime;
+ timeOffset: MediaTime;
trackSection: GroupTrackSection;
sectionIndex: number;
displayIndex: number;
@@ -26,7 +27,7 @@ export interface PlannedElementMove {
sourceTrackId: string;
targetTrackId: string;
elementId: string;
- newStartTime: number;
+ newStartTime: MediaTime;
}
export interface GroupMoveResult {