summaryrefslogtreecommitdiff
path: root/apps/web/src
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-03-31 22:38:56 +0200
committerMaze Winther <mazewinther@gmail.com>2026-03-31 22:38:56 +0200
commit0488a6be455c225535414fd651c838c355a9d3bd (patch)
tree54bb89b6167b5205b810e15a2b34d88f2644eb69 /apps/web/src
parent0f5dca4f12d926ec0015116e85ec1129a6891f40 (diff)
downloadopencut-0488a6be455c225535414fd651c838c355a9d3bd.tar.gz
fix: normalize vertical drag direction in preferIndex placement
Diffstat (limited to 'apps/web/src')
-rw-r--r--apps/web/src/lib/timeline/placement/__tests__/resolve.test.ts266
-rw-r--r--apps/web/src/lib/timeline/placement/resolve.ts37
2 files changed, 243 insertions, 60 deletions
diff --git a/apps/web/src/lib/timeline/placement/__tests__/resolve.test.ts b/apps/web/src/lib/timeline/placement/__tests__/resolve.test.ts
index 95424160..e067fb16 100644
--- a/apps/web/src/lib/timeline/placement/__tests__/resolve.test.ts
+++ b/apps/web/src/lib/timeline/placement/__tests__/resolve.test.ts
@@ -1,7 +1,53 @@
import { describe, expect, test } from "bun:test";
-import type { ElementType, TimelineElement, TimelineTrack, TrackType } from "@/lib/timeline";
+import type {
+ AudioElement,
+ AudioTrack,
+ GraphicElement,
+ GraphicTrack,
+ TextElement,
+ TextTrack,
+ TimelineTrack,
+ VideoElement,
+ VideoTrack,
+} from "@/lib/timeline";
+import type { Transform } from "@/lib/rendering";
import { resolveTrackPlacement } from "@/lib/timeline/placement";
+function buildTransform(): Transform {
+ return {
+ scaleX: 1,
+ scaleY: 1,
+ position: { x: 0, y: 0 },
+ rotate: 0,
+ };
+}
+
+type TestElement = AudioElement | GraphicElement | TextElement | VideoElement;
+
+function buildElement(params: {
+ id: string;
+ type: "audio";
+ startTime: number;
+ duration: number;
+}): AudioElement;
+function buildElement(params: {
+ id: string;
+ type: "graphic";
+ startTime: number;
+ duration: number;
+}): GraphicElement;
+function buildElement(params: {
+ id: string;
+ type: "text";
+ startTime: number;
+ duration: number;
+}): TextElement;
+function buildElement(params: {
+ id: string;
+ type: "video";
+ startTime: number;
+ duration: number;
+}): VideoElement;
function buildElement({
id,
type,
@@ -9,61 +55,171 @@ function buildElement({
duration,
}: {
id: string;
- type: ElementType;
+ type: TestElement["type"];
startTime: number;
duration: number;
-}): TimelineElement {
- return {
- id,
- type,
- name: id,
- startTime,
- duration,
- trimStart: 0,
- trimEnd: 0,
- } as TimelineElement;
+}): TestElement {
+ switch (type) {
+ case "audio":
+ return {
+ id,
+ type: "audio",
+ name: id,
+ startTime,
+ duration,
+ trimStart: 0,
+ trimEnd: 0,
+ volume: 1,
+ sourceType: "upload",
+ mediaId: `media-${id}`,
+ } satisfies AudioElement;
+ case "graphic":
+ return {
+ id,
+ type: "graphic",
+ name: id,
+ startTime,
+ duration,
+ trimStart: 0,
+ trimEnd: 0,
+ definitionId: `graphic-${id}`,
+ params: {},
+ transform: buildTransform(),
+ opacity: 1,
+ } satisfies GraphicElement;
+ case "text":
+ return {
+ id,
+ type: "text",
+ name: id,
+ startTime,
+ duration,
+ trimStart: 0,
+ trimEnd: 0,
+ content: id,
+ fontSize: 32,
+ fontFamily: "sans-serif",
+ color: "#ffffff",
+ background: {
+ enabled: false,
+ color: "#000000",
+ },
+ textAlign: "left",
+ fontWeight: "normal",
+ fontStyle: "normal",
+ textDecoration: "none",
+ transform: buildTransform(),
+ opacity: 1,
+ } satisfies TextElement;
+ case "video":
+ return {
+ id,
+ type: "video",
+ name: id,
+ startTime,
+ duration,
+ trimStart: 0,
+ trimEnd: 0,
+ mediaId: `media-${id}`,
+ transform: buildTransform(),
+ opacity: 1,
+ } satisfies VideoElement;
+ }
+
+ throw new Error(`Unsupported test element type: ${type}`);
}
-function buildTrack({
- id,
- type,
- elements = [],
- isMain = false,
-}: {
+type BuildTrackParams =
+ | {
+ id: string;
+ type: "audio";
+ elements?: AudioTrack["elements"];
+ isMain?: boolean;
+ }
+ | {
+ id: string;
+ type: "graphic";
+ elements?: GraphicTrack["elements"];
+ isMain?: boolean;
+ }
+ | {
+ id: string;
+ type: "text";
+ elements?: TextTrack["elements"];
+ isMain?: boolean;
+ }
+ | {
+ id: string;
+ type: "video";
+ elements?: VideoTrack["elements"];
+ isMain?: boolean;
+ };
+
+function buildTrack(params: {
id: string;
- type: TrackType;
- elements?: TimelineElement[];
+ type: "audio";
+ elements?: AudioTrack["elements"];
isMain?: boolean;
-}): TimelineTrack {
- if (type === "video") {
- return {
- id,
- type,
- name: id,
- elements: elements as TimelineTrack["elements"],
- isMain,
- muted: false,
- hidden: false,
- };
- }
+}): AudioTrack;
+function buildTrack(params: {
+ id: string;
+ type: "graphic";
+ elements?: GraphicTrack["elements"];
+ isMain?: boolean;
+}): GraphicTrack;
+function buildTrack(params: {
+ id: string;
+ type: "text";
+ elements?: TextTrack["elements"];
+ isMain?: boolean;
+}): TextTrack;
+function buildTrack(params: {
+ id: string;
+ type: "video";
+ elements?: VideoTrack["elements"];
+ isMain?: boolean;
+}): VideoTrack;
+function buildTrack(params: BuildTrackParams): TimelineTrack {
+ const { id, type, isMain = false } = params;
- if (type === "audio") {
- return {
- id,
- type,
- name: id,
- elements: elements as TimelineTrack["elements"],
- muted: false,
- };
+ switch (type) {
+ case "audio":
+ return {
+ id,
+ type: "audio",
+ name: id,
+ elements: params.elements ?? [],
+ muted: false,
+ };
+ case "graphic":
+ return {
+ id,
+ type: "graphic",
+ name: id,
+ elements: params.elements ?? [],
+ hidden: false,
+ };
+ case "text":
+ return {
+ id,
+ type: "text",
+ name: id,
+ elements: params.elements ?? [],
+ hidden: false,
+ };
+ case "video":
+ return {
+ id,
+ type: "video",
+ name: id,
+ elements: params.elements ?? [],
+ isMain,
+ muted: false,
+ hidden: false,
+ };
}
- return {
- id,
- type,
- name: id,
- elements: elements as TimelineTrack["elements"],
- hidden: false,
- };
+ throw new Error(`Unsupported test track type: ${type}`);
}
function buildTimeSpan({
@@ -124,7 +280,9 @@ describe("resolveTrackPlacement", () => {
buildTrack({
id: "text-1",
type: "text",
- elements: [buildElement({ id: "a", type: "text", startTime: 0, duration: 5 })],
+ elements: [
+ buildElement({ id: "a", type: "text", startTime: 0, duration: 5 }),
+ ],
}),
buildTrack({ id: "text-2", type: "text" }),
];
@@ -256,7 +414,9 @@ describe("resolveTrackPlacement", () => {
buildTrack({
id: "text-middle",
type: "text",
- elements: [buildElement({ id: "a", type: "text", startTime: 0, duration: 5 })],
+ elements: [
+ buildElement({ id: "a", type: "text", startTime: 0, duration: 5 }),
+ ],
}),
buildTrack({ id: "text-source", type: "text" }),
];
@@ -281,12 +441,16 @@ describe("resolveTrackPlacement", () => {
buildTrack({
id: "text-top",
type: "text",
- elements: [buildElement({ id: "a", type: "text", startTime: 0, duration: 5 })],
+ elements: [
+ buildElement({ id: "a", type: "text", startTime: 0, duration: 5 }),
+ ],
}),
buildTrack({
id: "text-source",
type: "text",
- elements: [buildElement({ id: "b", type: "text", startTime: 0, duration: 5 })],
+ elements: [
+ buildElement({ id: "b", type: "text", startTime: 0, duration: 5 }),
+ ],
}),
];
diff --git a/apps/web/src/lib/timeline/placement/resolve.ts b/apps/web/src/lib/timeline/placement/resolve.ts
index 235c789a..35718250 100644
--- a/apps/web/src/lib/timeline/placement/resolve.ts
+++ b/apps/web/src/lib/timeline/placement/resolve.ts
@@ -44,9 +44,7 @@ function buildExistingTrackResult({
trackId: track.id,
trackIndex,
trackType: track.type,
- ...(adjustedStartTime !== requestedStartTime
- ? { adjustedStartTime }
- : {}),
+ ...(adjustedStartTime !== requestedStartTime ? { adjustedStartTime } : {}),
};
}
@@ -114,6 +112,24 @@ function resolveAlwaysNewTrack({
});
}
+function getInsertDirection({
+ hoverDirection,
+ verticalDragDirection,
+}: {
+ hoverDirection: "above" | "below";
+ verticalDragDirection?: "up" | "down" | null;
+}): "above" | "below" {
+ if (verticalDragDirection === "up") {
+ return "above";
+ }
+
+ if (verticalDragDirection === "down") {
+ return "below";
+ }
+
+ return hoverDirection;
+}
+
export function resolveTrackPlacement({
tracks,
...placement
@@ -127,7 +143,9 @@ export function resolveTrackPlacement({
const { timeSpans, strategy } = placement;
if (strategy.type === "explicit") {
- const trackIndex = tracks.findIndex((track) => track.id === strategy.trackId);
+ const trackIndex = tracks.findIndex(
+ (track) => track.id === strategy.trackId,
+ );
if (trackIndex < 0) {
return null;
}
@@ -182,15 +200,16 @@ export function resolveTrackPlacement({
});
}
- const insertDirection =
- !isPreferredTrackCompatible && strategy.verticalDragDirection
- ? strategy.verticalDragDirection
- : strategy.hoverDirection;
const { insertIndex, insertPosition } = resolvePreferredNewTrackPlacement({
tracks,
trackType,
preferredIndex: strategy.trackIndex,
- direction: insertDirection,
+ direction: getInsertDirection({
+ hoverDirection: strategy.hoverDirection,
+ verticalDragDirection: !isPreferredTrackCompatible
+ ? strategy.verticalDragDirection
+ : null,
+ }),
});
return buildNewTrackResult({
trackType,