diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-04-15 05:43:29 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-04-15 05:43:29 +0200 |
| commit | ac43a5a73fa905cdac905bc956763b6093d6738e (patch) | |
| tree | 8d9ff8c3a9ae55bac8860215bec69cc5b6094a1c /apps/web/src/lib/timeline/group-move/__tests__/group-move.test.ts | |
| parent | 6312fc5b078689bc82c52fda9b7f92ee0a21d311 (diff) | |
| download | opencut-ac43a5a73fa905cdac905bc956763b6093d6738e.tar.gz | |
feat: move/resize multiple elements
Diffstat (limited to 'apps/web/src/lib/timeline/group-move/__tests__/group-move.test.ts')
| -rw-r--r-- | apps/web/src/lib/timeline/group-move/__tests__/group-move.test.ts | 287 |
1 files changed, 287 insertions, 0 deletions
diff --git a/apps/web/src/lib/timeline/group-move/__tests__/group-move.test.ts b/apps/web/src/lib/timeline/group-move/__tests__/group-move.test.ts new file mode 100644 index 00000000..e6c86e41 --- /dev/null +++ b/apps/web/src/lib/timeline/group-move/__tests__/group-move.test.ts @@ -0,0 +1,287 @@ +import { describe, expect, test } from "bun:test"; +import type { + AudioTrack, + OverlayTrack, + SceneTracks, + TextElement, + TextTrack, + VideoElement, + VideoTrack, +} from "@/lib/timeline/types"; +import type { Transform } from "@/lib/rendering"; +import { buildMoveGroup } from "@/lib/timeline/group-move/build-group"; +import { resolveGroupMove } from "@/lib/timeline/group-move/resolve-move"; + +function buildTransform(): Transform { + return { + scaleX: 1, + scaleY: 1, + position: { x: 0, y: 0 }, + rotate: 0, + }; +} + +function buildVideoElement({ + id, + startTime, + duration, +}: { + id: string; + startTime: number; + duration: number; +}): VideoElement { + return { + id, + type: "video", + name: id, + startTime, + duration, + trimStart: 0, + trimEnd: 0, + mediaId: `${id}-media`, + transform: buildTransform(), + opacity: 1, + }; +} + +function buildTextElement({ + id, + startTime, + duration, +}: { + id: string; + startTime: number; + duration: number; +}): TextElement { + return { + id, + type: "text", + name: id, + startTime, + duration, + trimStart: 0, + trimEnd: 0, + content: id, + fontSize: 32, + fontFamily: "Inter", + color: "#ffffff", + background: { + enabled: false, + color: "#000000", + }, + textAlign: "left", + fontWeight: "normal", + fontStyle: "normal", + textDecoration: "none", + transform: buildTransform(), + opacity: 1, + }; +} + +function buildVideoTrack({ + id, + elements = [], +}: { + id: string; + elements?: VideoTrack["elements"]; +}): VideoTrack { + return { + id, + type: "video", + name: id, + elements, + muted: false, + hidden: false, + }; +} + +function buildTextTrack({ + id, + elements = [], +}: { + id: string; + elements?: TextTrack["elements"]; +}): TextTrack { + return { + id, + type: "text", + name: id, + elements, + hidden: false, + }; +} + +function buildTracks({ + overlay, + main, + audio = [], +}: { + overlay: OverlayTrack[]; + main: VideoTrack; + audio?: AudioTrack[]; +}): SceneTracks { + return { + overlay, + main, + audio, + }; +} + +describe("group move", () => { + test("buildMoveGroup keeps rigid time offsets", () => { + const tracks = buildTracks({ + overlay: [ + buildVideoTrack({ + id: "overlay-video", + elements: [ + buildVideoElement({ id: "video-1", startTime: 5, duration: 4 }), + ], + }), + buildTextTrack({ + id: "overlay-text", + elements: [ + buildTextElement({ id: "text-1", startTime: 8, duration: 3 }), + ], + }), + ], + main: buildVideoTrack({ id: "main" }), + }); + + const group = buildMoveGroup({ + anchorRef: { trackId: "overlay-video", elementId: "video-1" }, + selectedElements: [ + { trackId: "overlay-video", elementId: "video-1" }, + { trackId: "overlay-text", elementId: "text-1" }, + ], + tracks, + }); + + expect(group).not.toBeNull(); + expect(group?.members.map((member) => member.timeOffset)).toEqual([0, 3]); + }); + + test("resolveGroupMove preserves order and clamps rigidly at time zero", () => { + const tracks = buildTracks({ + overlay: [ + buildTextTrack({ + id: "overlay-text", + elements: [ + buildTextElement({ id: "text-1", startTime: 0, duration: 3 }), + ], + }), + buildVideoTrack({ + id: "overlay-video", + elements: [ + buildVideoElement({ id: "video-1", startTime: 5, duration: 4 }), + ], + }), + ], + main: buildVideoTrack({ id: "main" }), + }); + const group = buildMoveGroup({ + anchorRef: { trackId: "overlay-video", elementId: "video-1" }, + selectedElements: [ + { trackId: "overlay-video", elementId: "video-1" }, + { trackId: "overlay-text", elementId: "text-1" }, + ], + tracks, + }); + if (!group) { + throw new Error("Expected group"); + } + + const result = resolveGroupMove({ + group, + tracks, + anchorStartTime: 7, + target: { + kind: "existingTrack", + anchorTargetTrackId: "main", + }, + }); + + expect(result?.moves).toEqual([ + { + sourceTrackId: "overlay-video", + elementId: "video-1", + targetTrackId: "main", + newStartTime: 5, + }, + { + sourceTrackId: "overlay-text", + elementId: "text-1", + targetTrackId: "overlay-text", + newStartTime: 0, + }, + ]); + }); + + test("resolveGroupMove falls back to new overlay tracks when the group cannot cross main", () => { + const tracks = buildTracks({ + overlay: [ + buildVideoTrack({ + id: "overlay-video", + elements: [ + buildVideoElement({ id: "video-1", startTime: 5, duration: 4 }), + ], + }), + buildTextTrack({ + id: "overlay-text", + elements: [ + buildTextElement({ id: "text-1", startTime: 5, duration: 3 }), + ], + }), + ], + main: buildVideoTrack({ id: "main" }), + }); + const group = buildMoveGroup({ + anchorRef: { trackId: "overlay-video", elementId: "video-1" }, + selectedElements: [ + { trackId: "overlay-video", elementId: "video-1" }, + { trackId: "overlay-text", elementId: "text-1" }, + ], + tracks, + }); + if (!group) { + throw new Error("Expected group"); + } + + const result = resolveGroupMove({ + group, + tracks, + anchorStartTime: 6, + target: { + kind: "newTracks", + anchorInsertIndex: 2, + newTrackIds: ["new-video-track", "new-text-track"], + }, + }); + + expect(result?.createTracks).toEqual([ + { + id: "new-video-track", + type: "video", + index: 2, + }, + { + id: "new-text-track", + type: "text", + index: 3, + }, + ]); + expect(result?.moves).toEqual([ + { + sourceTrackId: "overlay-video", + elementId: "video-1", + targetTrackId: "new-video-track", + newStartTime: 6, + }, + { + sourceTrackId: "overlay-text", + elementId: "text-1", + targetTrackId: "new-text-track", + newStartTime: 6, + }, + ]); + }); +}); |
