summaryrefslogtreecommitdiff
path: root/apps/web/src/lib/timeline/placement/main-track.ts
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-03-31 15:47:30 +0200
committerMaze Winther <mazewinther@gmail.com>2026-03-31 15:47:30 +0200
commit70a6d58b8432e455ab1443d45bc23dd70b363bea (patch)
treeb4bcdb88b87cbd2cd295c54cde013f38481c7f2a /apps/web/src/lib/timeline/placement/main-track.ts
parent5cc6abc3a44c9d046a1cb71a8c916f1d35827e8f (diff)
downloadopencut-70a6d58b8432e455ab1443d45bc23dd70b363bea.tar.gz
refactor: centralize track placement into its own module
Diffstat (limited to 'apps/web/src/lib/timeline/placement/main-track.ts')
-rw-r--r--apps/web/src/lib/timeline/placement/main-track.ts96
1 files changed, 96 insertions, 0 deletions
diff --git a/apps/web/src/lib/timeline/placement/main-track.ts b/apps/web/src/lib/timeline/placement/main-track.ts
new file mode 100644
index 00000000..1783352c
--- /dev/null
+++ b/apps/web/src/lib/timeline/placement/main-track.ts
@@ -0,0 +1,96 @@
+import type { TimelineElement, TimelineTrack, VideoTrack } from "@/lib/timeline";
+import { generateUUID } from "@/utils/id";
+
+const MAIN_TRACK_NAME = "Main Track";
+
+export function isMainTrack(track: TimelineTrack): track is VideoTrack {
+ return track.type === "video" && track.isMain === true;
+}
+
+export function getMainTrack({
+ tracks,
+}: {
+ tracks: TimelineTrack[];
+}): VideoTrack | null {
+ return tracks.find((track) => isMainTrack(track)) ?? null;
+}
+
+export function ensureMainTrack({
+ tracks,
+}: {
+ tracks: TimelineTrack[];
+}): TimelineTrack[] {
+ if (tracks.some((track) => isMainTrack(track))) {
+ return tracks;
+ }
+
+ return [
+ {
+ id: generateUUID(),
+ name: MAIN_TRACK_NAME,
+ type: "video",
+ elements: [],
+ muted: false,
+ isMain: true,
+ hidden: false,
+ },
+ ...tracks,
+ ];
+}
+
+export function getEarliestMainTrackElement({
+ tracks,
+ excludeElementId,
+}: {
+ tracks: TimelineTrack[];
+ excludeElementId?: string;
+}): TimelineElement | null {
+ const mainTrack = getMainTrack({ tracks });
+ if (!mainTrack) {
+ return null;
+ }
+
+ const elements = mainTrack.elements.filter((element) => {
+ return !excludeElementId || element.id !== excludeElementId;
+ });
+ if (elements.length === 0) {
+ return null;
+ }
+
+ return elements.reduce((earliestElement, element) => {
+ return element.startTime < earliestElement.startTime
+ ? element
+ : earliestElement;
+ });
+}
+
+export function enforceMainTrackStart({
+ tracks,
+ targetTrackId,
+ requestedStartTime,
+ excludeElementId,
+}: {
+ tracks: TimelineTrack[];
+ targetTrackId: string;
+ requestedStartTime: number;
+ excludeElementId?: string;
+}): number {
+ const mainTrack = getMainTrack({ tracks });
+ if (!mainTrack || mainTrack.id !== targetTrackId) {
+ return requestedStartTime;
+ }
+
+ const earliestElement = getEarliestMainTrackElement({
+ tracks,
+ excludeElementId,
+ });
+ if (!earliestElement) {
+ return 0;
+ }
+
+ if (requestedStartTime <= earliestElement.startTime) {
+ return 0;
+ }
+
+ return requestedStartTime;
+}