From e837e7504379e100ff3b56282577ec67bb88bec9 Mon Sep 17 00:00:00 2001 From: cashmere Date: Tue, 28 Jul 2026 19:41:14 +0200 Subject: feat: ripple-extend resize, timeline filmstrip, export hardening Yesterday's verified web session (gates green: 316 tests, tsc, build): - ripple-extend pushes right-hand chain on static-element resize - per-position video filmstrip thumbnails with zoom-adaptive density - fps-aware export bitrate, 2s keyframes, even-dim export canvas - freeze frame uses fixed 3s duration, prompt removed --- .../src/timeline/group-resize/compute-resize.ts | 73 +++++++++++++++++++--- 1 file changed, 65 insertions(+), 8 deletions(-) (limited to 'apps/web/src/timeline/group-resize/compute-resize.ts') diff --git a/apps/web/src/timeline/group-resize/compute-resize.ts b/apps/web/src/timeline/group-resize/compute-resize.ts index ed335997..2674aa8d 100644 --- a/apps/web/src/timeline/group-resize/compute-resize.ts +++ b/apps/web/src/timeline/group-resize/compute-resize.ts @@ -18,6 +18,7 @@ import { import type { ComputeGroupResizeArgs, GroupResizeMember, + GroupResizePushTarget, GroupResizeResult, GroupResizeUpdate, ResizeSide, @@ -103,16 +104,72 @@ export function computeGroupResize({ return { deltaTime: Object.is(finalDeltaTime, -0) ? ZERO_MEDIA_TIME : finalDeltaTime, - updates: members.map((member) => - buildResizeUpdate({ - member, - side, - deltaTime: finalDeltaTime, - }), - ), + updates: [ + ...members.map((member) => + buildResizeUpdate({ + member, + side, + deltaTime: finalDeltaTime, + }), + ), + ...buildRipplePushUpdates({ members, side, deltaTime: finalDeltaTime }), + ], }; } +function buildRipplePushUpdates({ + members, + side, + deltaTime, +}: { + members: GroupResizeMember[]; + side: ResizeSide; + deltaTime: MediaTime; +}): GroupResizeUpdate[] { + if (side !== "right" || deltaTime <= 0) { + return []; + } + + const overflowByTarget = new Map< + string, + { target: GroupResizePushTarget; overflow: MediaTime } + >(); + for (const member of members) { + if ( + member.sourceDuration != null || + member.rightPushChain === undefined || + member.rightNeighborBound === null + ) { + continue; + } + const neighborCeiling = subMediaTime({ + a: member.rightNeighborBound, + b: addMediaTime({ a: member.startTime, b: member.duration }), + }); + const overflow = subMediaTime({ a: deltaTime, b: neighborCeiling }); + if (overflow <= 0) { + continue; + } + for (const target of member.rightPushChain) { + const existing = overflowByTarget.get(target.elementId); + if (existing === undefined || overflow > existing.overflow) { + overflowByTarget.set(target.elementId, { target, overflow }); + } + } + } + + return [...overflowByTarget.values()].map(({ target, overflow }) => ({ + trackId: target.trackId, + elementId: target.elementId, + patch: { + trimStart: target.trimStart, + trimEnd: target.trimEnd, + startTime: addMediaTime({ a: target.startTime, b: overflow }), + duration: target.duration, + }, + })); +} + function buildResizeUpdate({ member, side, @@ -219,7 +276,7 @@ function getMaximumAllowedDeltaTime({ b: addMediaTime({ a: member.startTime, b: member.duration }), }); if (member.sourceDuration == null) { - return rightNeighborCeiling; + return member.rightPushChain === undefined ? rightNeighborCeiling : null; } const maximumVisibleSourceSpan = subMediaTime({ -- cgit v1.3.1