fix: tighten track placement type safety
This commit is contained in:
parent
0f5dca4f12
commit
26594546ea
|
|
@ -1,7 +1,53 @@
|
||||||
import { describe, expect, test } from "bun:test";
|
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";
|
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({
|
function buildElement({
|
||||||
id,
|
id,
|
||||||
type,
|
type,
|
||||||
|
|
@ -9,61 +55,171 @@ function buildElement({
|
||||||
duration,
|
duration,
|
||||||
}: {
|
}: {
|
||||||
id: string;
|
id: string;
|
||||||
type: ElementType;
|
type: TestElement["type"];
|
||||||
startTime: number;
|
startTime: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
}): TimelineElement {
|
}): TestElement {
|
||||||
return {
|
switch (type) {
|
||||||
id,
|
case "audio":
|
||||||
type,
|
return {
|
||||||
name: id,
|
id,
|
||||||
startTime,
|
type: "audio",
|
||||||
duration,
|
name: id,
|
||||||
trimStart: 0,
|
startTime,
|
||||||
trimEnd: 0,
|
duration,
|
||||||
} as TimelineElement;
|
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({
|
type BuildTrackParams =
|
||||||
id,
|
| {
|
||||||
type,
|
id: string;
|
||||||
elements = [],
|
type: "audio";
|
||||||
isMain = false,
|
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;
|
id: string;
|
||||||
type: TrackType;
|
type: "audio";
|
||||||
elements?: TimelineElement[];
|
elements?: AudioTrack["elements"];
|
||||||
isMain?: boolean;
|
isMain?: boolean;
|
||||||
}): TimelineTrack {
|
}): AudioTrack;
|
||||||
if (type === "video") {
|
function buildTrack(params: {
|
||||||
return {
|
id: string;
|
||||||
id,
|
type: "graphic";
|
||||||
type,
|
elements?: GraphicTrack["elements"];
|
||||||
name: id,
|
isMain?: boolean;
|
||||||
elements: elements as TimelineTrack["elements"],
|
}): GraphicTrack;
|
||||||
isMain,
|
function buildTrack(params: {
|
||||||
muted: false,
|
id: string;
|
||||||
hidden: false,
|
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;
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === "audio") {
|
throw new Error(`Unsupported test track type: ${type}`);
|
||||||
return {
|
|
||||||
id,
|
|
||||||
type,
|
|
||||||
name: id,
|
|
||||||
elements: elements as TimelineTrack["elements"],
|
|
||||||
muted: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
type,
|
|
||||||
name: id,
|
|
||||||
elements: elements as TimelineTrack["elements"],
|
|
||||||
hidden: false,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildTimeSpan({
|
function buildTimeSpan({
|
||||||
|
|
@ -124,7 +280,9 @@ describe("resolveTrackPlacement", () => {
|
||||||
buildTrack({
|
buildTrack({
|
||||||
id: "text-1",
|
id: "text-1",
|
||||||
type: "text",
|
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" }),
|
buildTrack({ id: "text-2", type: "text" }),
|
||||||
];
|
];
|
||||||
|
|
@ -256,7 +414,9 @@ describe("resolveTrackPlacement", () => {
|
||||||
buildTrack({
|
buildTrack({
|
||||||
id: "text-middle",
|
id: "text-middle",
|
||||||
type: "text",
|
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" }),
|
buildTrack({ id: "text-source", type: "text" }),
|
||||||
];
|
];
|
||||||
|
|
@ -281,12 +441,16 @@ describe("resolveTrackPlacement", () => {
|
||||||
buildTrack({
|
buildTrack({
|
||||||
id: "text-top",
|
id: "text-top",
|
||||||
type: "text",
|
type: "text",
|
||||||
elements: [buildElement({ id: "a", type: "text", startTime: 0, duration: 5 })],
|
elements: [
|
||||||
|
buildElement({ id: "a", type: "text", startTime: 0, duration: 5 }),
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
buildTrack({
|
buildTrack({
|
||||||
id: "text-source",
|
id: "text-source",
|
||||||
type: "text",
|
type: "text",
|
||||||
elements: [buildElement({ id: "b", type: "text", startTime: 0, duration: 5 })],
|
elements: [
|
||||||
|
buildElement({ id: "b", type: "text", startTime: 0, duration: 5 }),
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,9 +44,7 @@ function buildExistingTrackResult({
|
||||||
trackId: track.id,
|
trackId: track.id,
|
||||||
trackIndex,
|
trackIndex,
|
||||||
trackType: track.type,
|
trackType: track.type,
|
||||||
...(adjustedStartTime !== requestedStartTime
|
...(adjustedStartTime !== requestedStartTime ? { adjustedStartTime } : {}),
|
||||||
? { 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({
|
export function resolveTrackPlacement({
|
||||||
tracks,
|
tracks,
|
||||||
...placement
|
...placement
|
||||||
|
|
@ -127,7 +143,9 @@ export function resolveTrackPlacement({
|
||||||
const { timeSpans, strategy } = placement;
|
const { timeSpans, strategy } = placement;
|
||||||
|
|
||||||
if (strategy.type === "explicit") {
|
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) {
|
if (trackIndex < 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -182,15 +200,16 @@ export function resolveTrackPlacement({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const insertDirection =
|
|
||||||
!isPreferredTrackCompatible && strategy.verticalDragDirection
|
|
||||||
? strategy.verticalDragDirection
|
|
||||||
: strategy.hoverDirection;
|
|
||||||
const { insertIndex, insertPosition } = resolvePreferredNewTrackPlacement({
|
const { insertIndex, insertPosition } = resolvePreferredNewTrackPlacement({
|
||||||
tracks,
|
tracks,
|
||||||
trackType,
|
trackType,
|
||||||
preferredIndex: strategy.trackIndex,
|
preferredIndex: strategy.trackIndex,
|
||||||
direction: insertDirection,
|
direction: getInsertDirection({
|
||||||
|
hoverDirection: strategy.hoverDirection,
|
||||||
|
verticalDragDirection: !isPreferredTrackCompatible
|
||||||
|
? strategy.verticalDragDirection
|
||||||
|
: null,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
return buildNewTrackResult({
|
return buildNewTrackResult({
|
||||||
trackType,
|
trackType,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue