From cbd1b8212370c67b34a70dccd21bd82de72fead2 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Tue, 7 Apr 2026 04:41:27 +0200 Subject: refactor: structured track layout per scene --- apps/web/src/services/renderer/scene-builder.ts | 22 ++-- apps/web/src/services/storage/migrations/index.ts | 4 +- .../storage/migrations/transformers/v23-to-v24.ts | 113 +++++++++++++++++++++ .../src/services/storage/migrations/v23-to-v24.ts | 16 +++ apps/web/src/services/storage/service.ts | 22 ++-- 5 files changed, 148 insertions(+), 29 deletions(-) create mode 100644 apps/web/src/services/storage/migrations/transformers/v23-to-v24.ts create mode 100644 apps/web/src/services/storage/migrations/v23-to-v24.ts (limited to 'apps/web/src/services') diff --git a/apps/web/src/services/renderer/scene-builder.ts b/apps/web/src/services/renderer/scene-builder.ts index 76b97021..22d941be 100644 --- a/apps/web/src/services/renderer/scene-builder.ts +++ b/apps/web/src/services/renderer/scene-builder.ts @@ -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, diff --git a/apps/web/src/services/storage/migrations/index.ts b/apps/web/src/services/storage/migrations/index.ts index 9f0f3e0d..b7ee37f5 100644 --- a/apps/web/src/services/storage/migrations/index.ts +++ b/apps/web/src/services/storage/migrations/index.ts @@ -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(), ]; diff --git a/apps/web/src/services/storage/migrations/transformers/v23-to-v24.ts b/apps/web/src/services/storage/migrations/transformers/v23-to-v24.ts new file mode 100644 index 00000000..710d4dc1 --- /dev/null +++ b/apps/web/src/services/storage/migrations/transformers/v23-to-v24.ts @@ -0,0 +1,113 @@ +import type { MigrationResult, ProjectRecord } from "./types"; +import { getProjectId, isRecord } from "./utils"; + +export function transformProjectV23ToV24({ + project, +}: { + project: ProjectRecord; +}): MigrationResult { + 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; +} diff --git a/apps/web/src/services/storage/migrations/v23-to-v24.ts b/apps/web/src/services/storage/migrations/v23-to-v24.ts new file mode 100644 index 00000000..fe952bb1 --- /dev/null +++ b/apps/web/src/services/storage/migrations/v23-to-v24.ts @@ -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 }); + } +} diff --git a/apps/web/src/services/storage/service.ts b/apps/web/src/services/storage/service.ts index 3a014894..1aea0297 100644 --- a/apps/web/src/services/storage/service.ts +++ b/apps/web/src/services/storage/service.ts @@ -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 { @@ -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), -- cgit v1.3.1