From 4aaf4d925d35ccd1f64c9f9953b0e4428a9505b3 Mon Sep 17 00:00:00 2001 From: Arham Wani Date: Tue, 4 Aug 2026 05:47:32 +0530 Subject: [PATCH] fix(editor): cascade clip modifier deletion --- electron/ai-edition/document-service.test.ts | 29 ++++++- electron/ai-edition/document-service.ts | 13 ++-- src/lib/ai-edition/document/timeline.test.ts | 82 ++++++++++++++++++++ src/lib/ai-edition/document/timeline.ts | 7 +- 4 files changed, 124 insertions(+), 7 deletions(-) diff --git a/electron/ai-edition/document-service.test.ts b/electron/ai-edition/document-service.test.ts index ecc988a07..39ed1f2e0 100644 --- a/electron/ai-edition/document-service.test.ts +++ b/electron/ai-edition/document-service.test.ts @@ -140,7 +140,7 @@ describe("DocumentService", () => { }); describe("removeAsset", () => { - it("removes the asset and cascades clips + trimRanges", async () => { + it("removes the asset and cascades clips, trims, and clip modifiers", async () => { const doc = await service.createProject("P"); const withAsset = await service.addAsset(doc.project.id, { path: "/tmp/a.mp4" }); const assetId = withAsset.assets[0]?.id ?? ""; @@ -174,12 +174,39 @@ describe("DocumentService", () => { }, ], }, + zoomRanges: [ + { + id: "zoom_1", + clipId: "clip_1", + sourceStartSec: 0, + sourceEndSec: 1, + startMs: 0, + endMs: 1000, + depth: 3, + focus: { cx: 0.5, cy: 0.5 }, + }, + ], + legacyEditor: { + speedRegions: [ + { + id: "speed_1", + clipId: "clip_1", + sourceStartSec: 0, + sourceEndSec: 1, + startMs: 0, + endMs: 1000, + speed: 2, + }, + ], + }, }); const after = await service.removeAsset(docWithTimeline.project.id, assetId); expect(after.assets).toHaveLength(0); expect(after.timeline.clips).toHaveLength(0); expect(after.timeline.trimRanges).toHaveLength(0); + expect(after.zoomRanges).toHaveLength(0); + expect((after.legacyEditor as { speedRegions: unknown[] }).speedRegions).toHaveLength(0); expect(after.project.primaryAssetId).toBeUndefined(); }); diff --git a/electron/ai-edition/document-service.ts b/electron/ai-edition/document-service.ts index 08201eed2..3a1663cfd 100644 --- a/electron/ai-edition/document-service.ts +++ b/electron/ai-edition/document-service.ts @@ -13,6 +13,7 @@ import fs, { type FileHandle } from "node:fs/promises"; import path from "node:path"; import { createId } from "../../src/lib/ai-edition/document/ids"; +import { removeClip } from "../../src/lib/ai-edition/document/timeline"; import { type AxcutAsset, type AxcutDocument, @@ -309,16 +310,18 @@ export class DocumentService { doc.project.primaryAssetId === assetId ? (assets[0]?.id ?? undefined) : doc.project.primaryAssetId; + const withoutAssetClips = doc.timeline.clips + .filter((clip) => clip.assetId === assetId) + .reduce((current, clip) => removeClip(current, clip.id), doc); const next: AxcutDocument = { - ...doc, + ...withoutAssetClips, assets, timeline: { - ...doc.timeline, - clips: doc.timeline.clips.filter((c) => c.assetId !== assetId), - trimRanges: doc.timeline.trimRanges.filter((r) => r.assetId !== assetId), + ...withoutAssetClips.timeline, + trimRanges: withoutAssetClips.timeline.trimRanges.filter((r) => r.assetId !== assetId), }, project: { - ...doc.project, + ...withoutAssetClips.project, primaryAssetId, updatedAt: new Date().toISOString(), }, diff --git a/src/lib/ai-edition/document/timeline.test.ts b/src/lib/ai-edition/document/timeline.test.ts index 19f90a8ca..527cbf836 100644 --- a/src/lib/ai-edition/document/timeline.test.ts +++ b/src/lib/ai-edition/document/timeline.test.ts @@ -1370,6 +1370,88 @@ describe("removeClip — delete a clip, close the gap, drop its pills", () => { expect(next.zoomRanges[0]).toMatchObject({ startMs: 2000, endMs: 4000 }); }); + it("drops every modifier anchored to the last remaining clip", () => { + const before = makeDoc({ + timeline: { + ...makeDoc().timeline, + clips: [ + makeClip({ id: "clip_a", sourceStartSec: 0, sourceEndSec: 10, timelineEndSec: 10 }), + ], + }, + zoomRanges: [ + makeZoom({ id: "anchored_zoom", clipId: "clip_a", sourceEndSec: 1, endMs: 1000 }), + makeZoom({ + id: "legacy_zoom", + clipId: undefined, + sourceStartSec: undefined, + sourceEndSec: undefined, + }), + ], + annotations: [ + { + id: "anchored_annotation", + clipId: "clip_a", + sourceStartSec: 0, + sourceEndSec: 1, + startMs: 0, + endMs: 1000, + type: "text", + content: "remove me", + position: { x: 50, y: 50 }, + size: { width: 30, height: 20 }, + style: { + color: "#fff", + backgroundColor: "transparent", + fontSize: 32, + fontFamily: "Inter", + fontWeight: "bold", + fontStyle: "normal", + textDecoration: "none", + textAlign: "center", + textAnimation: "none", + }, + zIndex: 1, + }, + ] as unknown as AxcutDocument["annotations"], + legacyEditor: { + speedRegions: [ + { + id: "anchored_speed", + clipId: "clip_a", + sourceStartSec: 0, + sourceEndSec: 1, + startMs: 0, + endMs: 1000, + speed: 2, + }, + { id: "legacy_speed", startMs: 0, endMs: 1000, speed: 1.5 }, + ], + cameraFullscreenRegions: [ + { + id: "anchored_camera", + clipId: "clip_a", + sourceStartSec: 0, + sourceEndSec: 1, + startMs: 0, + endMs: 1000, + }, + ], + }, + }); + + const next = removeClip(before, "clip_a"); + + expect(next.timeline.clips).toEqual([]); + expect(next.zoomRanges.map((region) => region.id)).toEqual(["legacy_zoom"]); + expect(next.annotations).toEqual([]); + expect((next.legacyEditor as { speedRegions: Array<{ id: string }> }).speedRegions).toEqual([ + expect.objectContaining({ id: "legacy_speed" }), + ]); + expect( + (next.legacyEditor as { cameraFullscreenRegions: unknown[] }).cameraFullscreenRegions, + ).toEqual([]); + }); + it("is a no-op for an unknown clip", () => { const before = doc(); const next = removeClip(before, "clip_missing"); diff --git a/src/lib/ai-edition/document/timeline.ts b/src/lib/ai-edition/document/timeline.ts index 96f517e6b..7458cdc61 100644 --- a/src/lib/ai-edition/document/timeline.ts +++ b/src/lib/ai-edition/document/timeline.ts @@ -935,7 +935,12 @@ export function removeClip(document: AxcutDocument, clipId: string): AxcutDocume trimRanges: document.timeline.trimRanges.filter((t) => t.clipId !== clipId), }, }; - return oldClips.length > 0 && newClips.length > 0 ? rederiveRegionMs(next, newClips) : next; + const withoutRemovedRegions = mapAllRegionCollections(next, (regions) => + regions.filter((region) => region.clipId !== clipId), + ); + return newClips.length > 0 + ? rederiveRegionMs(withoutRemovedRegions, newClips) + : withoutRemovedRegions; } export function restoreFullTimeline(document: AxcutDocument): AxcutDocument {