Skip to content

Commit c3d167f

Browse files
committed
fix(timeline): apply a clip's range and crop in one pass
A range applied first could fold the clip into a neighbour its new crop tells apart, and the crop was then lost. setClipSourceRange takes the crop, for the Edit clip dialog and setClipRange alike. Part of #1180.
1 parent c489df4 commit c3d167f

4 files changed

Lines changed: 43 additions & 12 deletions

File tree

‎electron/ai-edition/agent-tools.test.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3656,6 +3656,28 @@ describe("clip structure (#1180)", () => {
36563656
expect(run(twoTakes(), "setClipRange", { clipId: "clip_1" }).ok).toBe(false);
36573657
});
36583658

3659+
it("setClipRange weighs a new range and a new crop together", () => {
3660+
// clip_2 is moved to meet clip_1 in the media AND given its own crop in one call: the
3661+
// crop keeps them apart, so neither edit is lost to a fold.
3662+
const placed = run(twoTakes(), "insertClip", {
3663+
assetId: "asset_1",
3664+
sourceStartSec: 12,
3665+
sourceEndSec: 16,
3666+
});
3667+
const { clipId } = JSON.parse(placed.resultJson);
3668+
const crop = { x: 0, y: 0, width: 0.5, height: 0.5 };
3669+
const result = run(placed.document as AxcutDocument, "setClipRange", {
3670+
clipId,
3671+
sourceStartSec: 10,
3672+
sourceEndSec: 16,
3673+
cropRegion: crop,
3674+
});
3675+
expect((result.document as AxcutDocument).timeline.clips).toMatchObject([
3676+
{ id: "clip_1", sourceEndSec: 10 },
3677+
{ id: clipId, sourceStartSec: 10, sourceEndSec: 16, cropRegion: crop },
3678+
]);
3679+
});
3680+
36593681
it("setClipRange moves one edge and keeps the other", () => {
36603682
const result = run(twoTakes(), "setClipRange", { clipId: "clip_1", sourceEndSec: 6 });
36613683
expect((result.document as AxcutDocument).timeline.clips[0]).toMatchObject({

‎electron/ai-edition/agent-tools.ts‎

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2363,13 +2363,11 @@ export function executeAgentTool(
23632363
cropRegion.y === 0 &&
23642364
cropRegion.width === 1 &&
23652365
cropRegion.height === 1;
2366-
const afterRange = ranged
2367-
? setClipSourceRange(document, clipId, sourceStartSec, sourceEndSec)
2368-
: document;
2369-
const next =
2370-
cropRegion === undefined
2371-
? afterRange
2372-
: setClipCrop(afterRange, clipId, wholeFrame ? null : cropRegion);
2366+
const crop = cropRegion === undefined ? undefined : wholeFrame ? null : cropRegion;
2367+
// One pass for both, as the dialog's Apply: the fold weighs range and crop together.
2368+
const next = ranged
2369+
? setClipSourceRange(document, clipId, sourceStartSec, sourceEndSec, crop)
2370+
: setClipCrop(document, clipId, crop ?? null);
23732371
const dropped = droppedByEdit(document, next);
23742372
const casualties = dropped.droppedModifierIds.length + dropped.droppedTrimIds.length;
23752373
const after = next.timeline.clips.find((c) => c.id === clipId);

‎src/lib/ai-edition/document/timeline.ts‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1083,7 +1083,8 @@ export function insertAssetClip(
10831083
}
10841084

10851085
/**
1086-
* The single mutator for a clip's crop: the clip's Edit modal and the agent's `setClipRange`.
1086+
* The single mutator for a clip's crop alone (the agent's crop-only `setClipRange`); with a range,
1087+
* `setClipSourceRange` takes the crop in the same pass.
10871088
* `null` clears it back to the full frame rather than storing the identity region. Through
10881089
* `withClipsChanged`, since crop is the one property that keeps two meeting clips apart. An
10891090
* unknown `clipId` is a no-op. Pure.
@@ -1113,19 +1114,31 @@ export function setClipCrop(
11131114
* its source length), lay everything back-to-back, then `rederiveRegionMs` clamps every
11141115
* anchored pill to the clip's kept window (dropping what the trim removed) and refreshes the
11151116
* derived ms of the clips that reflowed. An unknown `clipId` is a no-op.
1117+
*
1118+
* `cropRegion` is the Edit modal's crop, set in the same pass: `undefined` leaves it, `null`
1119+
* clears it. One pass, because the fold decides on range AND crop together: a range applied
1120+
* first can fold the clip into a neighbour whose crop the new one would have told apart.
11161121
*/
11171122
export function setClipSourceRange(
11181123
document: AxcutDocument,
11191124
clipId: string,
11201125
sourceStartSec: number,
11211126
sourceEndSec: number,
1127+
cropRegion?: AxcutClipCropRegion | null,
11221128
): AxcutDocument {
11231129
const clamp = (n: number) => (Number.isFinite(n) ? Math.max(0, n) : 0);
11241130
const lo = Math.min(clamp(sourceStartSec), clamp(sourceEndSec));
11251131
const hi = Math.max(clamp(sourceStartSec), clamp(sourceEndSec));
11261132
const arr = document.timeline.clips.map((c) =>
11271133
c.id === clipId
1128-
? { ...c, sourceStartSec: lo, sourceEndSec: hi, timelineStartSec: 0, timelineEndSec: 0 }
1134+
? {
1135+
...c,
1136+
sourceStartSec: lo,
1137+
sourceEndSec: hi,
1138+
timelineStartSec: 0,
1139+
timelineEndSec: 0,
1140+
...(cropRegion === undefined ? {} : { cropRegion: cropRegion ?? undefined }),
1141+
}
11291142
: c,
11301143
);
11311144
return withClipsChanged(document, arr);

‎src/lib/ai-edition/store/useTimeline.ts‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ import {
3535
removeClip as removeClipInDocument,
3636
removeRegion as removeRegionInDocument,
3737
resequenceClips,
38-
setClipCrop,
3938
setClipSourceRange,
4039
} from "../document/timeline";
4140
import type { AxcutAudioTrack, AxcutClipCropRegion, AxcutDocument } from "../schema";
@@ -1221,8 +1220,7 @@ export function useTimeline() {
12211220
) => {
12221221
const doc = useProjectStore.getState().document;
12231222
if (!doc) return;
1224-
const ranged = setClipSourceRange(doc, clipId, sourceStartSec, sourceEndSec);
1225-
const next = cropRegion === undefined ? ranged : setClipCrop(ranged, clipId, cropRegion);
1223+
const next = setClipSourceRange(doc, clipId, sourceStartSec, sourceEndSec, cropRegion);
12261224
await saveDocument(next, { history: true });
12271225
},
12281226
[saveDocument],

0 commit comments

Comments
 (0)