diff --git a/src/lib/ai-edition/document/insertion.test.ts b/src/lib/ai-edition/document/insertion.test.ts index 2581c7f0a..f024f524f 100644 --- a/src/lib/ai-edition/document/insertion.test.ts +++ b/src/lib/ai-edition/document/insertion.test.ts @@ -125,6 +125,44 @@ describe("insertGeneratedClip", () => { [5, 10], ]); }); + + it("keeps the speed and Full Camera regions of the right half, and both sides of one across the cut (#1037)", () => { + const region = (id: string, startSec: number, endSec: number) => ({ + id, + clipId: "c1", + sourceStartSec: startSec, + sourceEndSec: endSec, + startMs: startSec * 1000, + endMs: endSec * 1000, + }); + const next = withInsertion( + doc({ + legacyEditor: { + speedRegions: [ + { ...region("right", 6, 8), speed: 2 }, + { ...region("across", 3, 5), speed: 2 }, + ], + cameraFullscreenRegions: [region("right", 7, 9), region("across", 3, 5)], + }, + } as Partial), + ); + const rightId = next.timeline.clips[2].id; + const spans = (rows: unknown) => + (rows as { clipId: string; sourceStartSec: number; sourceEndSec: number }[]) + .map((r) => [r.clipId === rightId ? "right" : r.clipId, r.sourceStartSec, r.sourceEndSec]) + .sort((a, b) => Number(a[1]) - Number(b[1])); + const legacy = next.legacyEditor as Record; + expect(spans(legacy.speedRegions)).toEqual([ + ["c1", 3, 4], + ["right", 4, 5], + ["right", 6, 8], + ]); + expect(spans(legacy.cameraFullscreenRegions)).toEqual([ + ["c1", 3, 4], + ["right", 4, 5], + ["right", 7, 9], + ]); + }); }); describe("removeGeneratedClips", () => { diff --git a/src/lib/ai-edition/document/insertion.ts b/src/lib/ai-edition/document/insertion.ts index 14de27197..58300c274 100644 --- a/src/lib/ai-edition/document/insertion.ts +++ b/src/lib/ai-edition/document/insertion.ts @@ -27,7 +27,7 @@ import { isGeneratedAssetId, } from "../timeline/clip-parts"; import { createId } from "./ids"; -import { rederiveRegionMs, removeClip, resequenceClips } from "./timeline"; +import { mapAllRegionCollections, rederiveRegionMs, removeClip, resequenceClips } from "./timeline"; /** Where a new word goes relative to the word the caret was resting on. */ export type InsertSide = "before" | "after"; @@ -133,11 +133,13 @@ function fanOutAnchors(document: AxcutDocument, from: string, to: string): Axcut (rows ?? []).flatMap((row) => row.clipId === from ? [row, { ...row, id: createId("frag"), clipId: to }] : [row], ); + // Every modifier collection, through the one walk that knows them all (#1037: a hand-picked + // list here forgot speed and Full Camera). Audio is kept out of it and fanned out on its own: + // that walk also re-anchors the takes, which is `rederiveRegionMs`'s job once the ms are fresh. + const fanned = mapAllRegionCollections({ ...document, audioTracks: [] }, both); return { - ...document, - timeline: { ...document.timeline, trimRanges: both(document.timeline.trimRanges) }, - zoomRanges: both(document.zoomRanges), - annotations: both(document.annotations), + ...fanned, + timeline: { ...fanned.timeline, trimRanges: both(fanned.timeline.trimRanges) }, audioTracks: both(document.audioTracks), }; } diff --git a/src/lib/ai-edition/document/timeline.ts b/src/lib/ai-edition/document/timeline.ts index 0e3a6046a..ba256fd3b 100644 --- a/src/lib/ai-edition/document/timeline.ts +++ b/src/lib/ai-edition/document/timeline.ts @@ -353,7 +353,7 @@ type StoredRegion = { }; /** Apply `fn` to all four modifier collections (document-level + legacyEditor envelopes). */ -function mapAllRegionCollections( +export function mapAllRegionCollections( document: AxcutDocument, fn: (regions: StoredRegion[], prefix: string) => StoredRegion[], ): AxcutDocument {