refactor: structured track layout per scene

This commit is contained in:
Maze Winther
2026-04-07 04:41:27 +02:00
parent 4f7d401a97
commit cbd1b82123
91 changed files with 2086 additions and 1378 deletions
@@ -1,4 +1,4 @@
import type { TimelineTrack } from "@/lib/timeline";
import type { SceneTracks, TimelineTrack } from "@/lib/timeline";
import type { MediaAsset } from "@/lib/media/types";
import { RootNode } from "./nodes/root-node";
import { VideoNode } from "./nodes/video-node";
@@ -12,7 +12,6 @@ import { EffectLayerNode } from "./nodes/effect-layer-node";
import type { BaseNode } from "./nodes/base-node";
import type { TBackground, TCanvasSize } from "@/lib/project/types";
import { DEFAULT_BACKGROUND_BLUR_INTENSITY } from "@/lib/background/constants";
import { isMainTrack } from "@/lib/timeline/placement";
const PREVIEW_MAX_IMAGE_SIZE = 2048;
@@ -209,7 +208,7 @@ function buildBlurBackgroundNodes({
export type BuildSceneParams = {
canvasSize: TCanvasSize;
tracks: TimelineTrack[];
tracks: SceneTracks;
mediaAssets: MediaAsset[];
duration: number;
background: TBackground;
@@ -227,19 +226,12 @@ export function buildScene({
const rootNode = new RootNode({ duration });
const mediaMap = new Map(mediaAssets.map((m) => [m.id, m]));
const visibleTracks = tracks.filter(
(track) => !("hidden" in track && track.hidden),
);
const orderedTracksTopToBottom = [
...visibleTracks.filter((track) => !isMainTrack(track)),
...visibleTracks.filter((track) => isMainTrack(track)),
const visibleTracks = [
...tracks.overlay.filter((track) => !("hidden" in track && track.hidden)),
...(!tracks.main.hidden ? [tracks.main] : []),
];
const orderedTracksBottomToTop = orderedTracksTopToBottom.slice().reverse();
const mainTrack = orderedTracksBottomToTop.find((track) =>
isMainTrack(track),
);
const orderedTracksBottomToTop = visibleTracks.slice().reverse();
const mainTrack = tracks.main.hidden ? undefined : tracks.main;
const allNodes = buildTrackNodes({
tracks: orderedTracksBottomToTop,
@@ -22,10 +22,11 @@ import { V19toV20Migration } from "./v19-to-v20";
import { V20toV21Migration } from "./v20-to-v21";
import { V21toV22Migration } from "./v21-to-v22";
import { V22toV23Migration } from "./v22-to-v23";
import { V23toV24Migration } from "./v23-to-v24";
export { runStorageMigrations } from "./runner";
export type { MigrationProgress } from "./runner";
export const CURRENT_PROJECT_VERSION = 23;
export const CURRENT_PROJECT_VERSION = 24;
export const migrations = [
new V0toV1Migration(),
@@ -51,4 +52,5 @@ export const migrations = [
new V20toV21Migration(),
new V21toV22Migration(),
new V22toV23Migration(),
new V23toV24Migration(),
];
@@ -0,0 +1,113 @@
import type { MigrationResult, ProjectRecord } from "./types";
import { getProjectId, isRecord } from "./utils";
export function transformProjectV23ToV24({
project,
}: {
project: ProjectRecord;
}): MigrationResult<ProjectRecord> {
if (!getProjectId({ project })) {
return { project, skipped: true, reason: "no project id" };
}
const version = project.version;
if (typeof version !== "number") {
return { project, skipped: true, reason: "invalid version" };
}
if (version >= 24) {
return { project, skipped: true, reason: "already v24" };
}
if (version !== 23) {
return { project, skipped: true, reason: "not v23" };
}
return {
project: {
...migrateProject({ project }),
version: 24,
},
skipped: false,
};
}
function migrateProject({
project,
}: {
project: ProjectRecord;
}): ProjectRecord {
if (!Array.isArray(project.scenes)) {
return project;
}
return {
...project,
scenes: project.scenes.map((scene) => migrateScene({ scene })),
};
}
function migrateScene({ scene }: { scene: unknown }): unknown {
if (!isRecord(scene) || !Array.isArray(scene.tracks)) {
return scene;
}
const mainTrack = findMainTrack({ tracks: scene.tracks });
if (!mainTrack) {
return scene;
}
return {
...scene,
tracks: {
overlay: scene.tracks
.filter((track) => track !== mainTrack)
.map((track) => migrateTrack({ track }))
.filter(
(track): track is ProjectRecord =>
isRecord(track) && track.type !== "audio",
),
main: migrateTrack({ track: mainTrack }),
audio: scene.tracks
.map((track) => migrateTrack({ track }))
.filter(
(track): track is ProjectRecord =>
isRecord(track) && track.type === "audio",
),
},
};
}
function findMainTrack({
tracks,
}: {
tracks: unknown[];
}): ProjectRecord | null {
for (const track of tracks) {
if (!isRecord(track)) {
continue;
}
if (track.type === "video" && track.isMain === true) {
return track;
}
}
for (const track of tracks) {
if (!isRecord(track)) {
continue;
}
if (track.type === "video") {
return track;
}
}
return null;
}
function migrateTrack({ track }: { track: unknown }): unknown {
if (!isRecord(track)) {
return track;
}
const nextTrack = { ...track };
delete nextTrack.isMain;
return nextTrack;
}
@@ -0,0 +1,16 @@
import { StorageMigration } from "./base";
import type { ProjectRecord } from "./transformers/types";
import { transformProjectV23ToV24 } from "./transformers/v23-to-v24";
export class V23toV24Migration extends StorageMigration {
from = 23;
to = 24;
async transform(project: ProjectRecord): Promise<{
project: ProjectRecord;
skipped: boolean;
reason?: string;
}> {
return transformProjectV23ToV24({ project });
}
}
+9 -13
View File
@@ -21,7 +21,7 @@ import {
migrations,
runStorageMigrations,
} from "@/services/storage/migrations";
import type { Bookmark, TimelineTrack, TScene } from "@/lib/timeline";
import type { Bookmark, SceneTracks, TScene } from "@/lib/timeline";
function normalizeBookmarks({ raw }: { raw: unknown }): Bookmark[] {
if (!Array.isArray(raw)) return [];
@@ -116,18 +116,18 @@ class StorageService {
private stripAudioBuffers({
tracks,
}: {
tracks: TimelineTrack[];
}): TimelineTrack[] {
return tracks.map((track) => {
if (track.type !== "audio") return track;
return {
tracks: SceneTracks;
}): SceneTracks {
return {
...tracks,
audio: tracks.audio.map((track) => ({
...track,
elements: track.elements.map((element) => {
const { buffer: _buffer, ...rest } = element;
return rest;
}),
};
});
})),
};
}
async saveProject({ project }: { project: TProject }): Promise<void> {
@@ -178,11 +178,7 @@ class StorageService {
id: scene.id,
name: scene.name,
isMain: scene.isMain,
tracks: (scene.tracks ?? []).map((track) =>
track.type === "video"
? { ...track, isMain: track.isMain ?? false } // legacy: isMain was optional
: track,
),
tracks: scene.tracks,
bookmarks: normalizeBookmarks({ raw: scene.bookmarks }),
createdAt: new Date(scene.createdAt),
updatedAt: new Date(scene.updatedAt),