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:53:23 +0200
committerMaze Winther <mazewinther@gmail.com>2026-03-31 15:53:23 +0200
commitd23be079cce6108482fc3e85401cccf949087174 (patch)
tree7d5acd324f9274b3b823bc67154349b8693e3412 /apps/web/src/lib/timeline/placement/main-track.ts
parentdcf54c1250431f5a4a2848f7c07781ef5eebacaf (diff)
parent70a6d58b8432e455ab1443d45bc23dd70b363bea (diff)
downloadopencut-d23be079cce6108482fc3e85401cccf949087174.tar.gz
Merge branch 'feature/track-placement'
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;
+}