Compare commits

...
5 changed files with 266 additions and 63 deletions
+20
View File
@@ -41,6 +41,26 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Rust wasm target
run: rustup target add wasm32-unknown-unknown
- name: Install wasm-pack
uses: jetli/wasm-pack-action@v0.4.0
with:
version: latest
- name: Cache Rust build artifacts
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('Cargo.lock') }}
- name: Build WASM
run: wasm-pack build rust/wasm --target bundler --out-dir pkg
- name: Install Bun
uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76
with:
@@ -38,7 +38,7 @@ export class PasteCommand extends Command {
...this.clipboardItems.map((item) => item.element.startTime),
);
const updatedTracks = [...this.savedState];
let updatedTracks = [...this.savedState];
const itemsByTrackId = groupClipboardItemsByTrackId({
clipboardItems: this.clipboardItems,
});
@@ -26,7 +26,7 @@ export class DuplicateElementsCommand extends Command {
this.previousSelection = editor.selection.getSelectedElements();
this.duplicatedElements = [];
const updatedTracks = [...this.savedState];
let updatedTracks = [...this.savedState];
for (const track of this.savedState) {
const elementsToDuplicate = this.elements.filter(
@@ -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;
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") {
return {
id,
type,
name: id,
elements: elements as TimelineTrack["elements"],
muted: 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 }),
],
}),
];
+28 -9
View File
@@ -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,