feat(studio): clip drag engine — preview and commit (unwired)#2206
feat(studio): clip drag engine — preview and commit (unwired)#2206ukimsanov wants to merge 1 commit into
Conversation
cccd643 to
7f164b5
Compare
7fccf6c to
b7eee0d
Compare
7f164b5 to
a2f4dd0
Compare
miga-heygen
left a comment
There was a problem hiding this comment.
Reviewed as part of the 25-PR NLE overhaul stack. Full stack review on #2205 (the keystone). No blockers on this PR. — Miga
vanceingalls
left a comment
There was a problem hiding this comment.
R1 Batch-D deep-dive — #2206 clip-drag engine
Reviewed as the NLE drag brain that the coexistence layer (#2205) makes typeable. SHA b7eee0d. Read all three new files at head; cross-checked against #2205's bivariant callback contract and #2212's legacy-export removal.
Consumer-invariant audit
Shape contract with #2205's TimelineGroupMoveEdit.
timelineClipDragCommit.ts:7-10 defines a local TimelineMoveEdit = { element, updates: Pick<TimelineElement, "start" | "track"> }. Structurally identical to #2205's TimelineGroupMoveEdit (timelineCallbacks.ts:80-83). The local declaration is used only inside timelineClipDragCommit.ts and timelineElementsMove.ts (also declared locally there as TimelineElementMoveEdit, line 22-25) — same shape three times. This is defensible under the "reviewable as functions before any component mounts it" framing (the engine files stand alone), but it's a watch-note for the swap PR (#2212): once wiring lands, the callback boundary flows through TimelineGroupMoveEdit from timelineCallbacks.ts — the three local structural clones should be replaced with the single canonical import so tsc catches drift if the shape ever widens (e.g. adding an optional stackingReorder field only to one). Miga's higher-level review noted a similar SSOT gap on overlapsInTime between #2198/#2199; this is the analogous smell in this PR.
Legacy export retention until the swap.
PR body claims: keeps resolveDragPreviewPlacement export until the timeline swap. Verified at timelineClipDragPreview.ts:290. Verified #2212 diff on the same file deletes exactly that export (-export function resolveDragPreviewPlacement(). Contract honored.
Interaction with Batch-A's math modules.
computeDragPreview(line 98) correctly consumes:resolveTimelineMovefrom #2195 collision (line 3 import).snapMoveToTargets+snapTimelineTime+TIMELINE_SNAP_PXfrom #2196 snapping (line 7-12).resolveInsertRow+resolveZoneDropPlacementfrom #2199 zones (line 13).clampGroupMoveDeltafrom #2197 multi-drag preview (line 14) — the rigid formation clamp.
commitDraggedClipMove(line 91) reusesnormalizeToZones(#2199) after applying the drop, andcomputeStackingPatches(#2198) for lane↔z sync. The sync engagement gate is correct —if (!readZIndex || !onStackingPatches) return;(line 171) means outside the NLE (standalone<Timeline>) the z-sync is a no-op. Good coexistence hygiene.
Multi-selection handling.
Group drag: commitDraggedClipMove:98-99 engages the multi-drag ONLY when the dragged clip is in the selection AND selection size > 1 — the standard NLE gesture. Track changes apply to the dragged clip only (line 129 track: targetTrack on drag; line 130 preserves track: e.track on passengers). Time delta clamped at zero via movedStart (line 101-102: Math.max(0, round3(e.start + delta))) — matches the leftmost-at-0 constraint from #2197.
Fine but flag-worthy: edits.filter((e) => e.updates.start !== e.element.start) at line 111 drops passengers whose clamped start didn't change (e.g. group can't slide left of a passenger at 0). Good — no phantom no-op writes. However, the same filter is NOT applied on the topology-change branch (line 141): every clip normalized-to-a-new-track is persisted, including those that keep their old start. That's correct because normalize-to-zones may have relocated their lane, but readers of the persist path should know the two branches have different filter policies. Docstring at line 82-88 covers the intent; enough.
Three-phase persist (Miga's line-noted nit).
persistMoveEdits (line 49-65) is fire-and-forget with rollback on rejection (line 61-64). The catch handler is console.error only — the caller cannot observe the failure. Miga already noted this: worth documenting for downstream callers, but not a blocker since the optimistic UI is rolled back either way. My addition: the rollback restores { start, track } verbatim from prev — if a concurrent store mutation happened between the optimistic apply and the catch (e.g. from an unrelated undo), rollback overwrites that. Extremely narrow race; the store's operations are synchronous and the persist path awaits its promise synchronously enough that the window is tiny. Accepted as-is.
persistTimelineElementsMove atomicity (in timelineElementsMove.ts).
Three phases (patch-in-memory → atomic write → post-save preview sync) — Miga captured this well. My added observation: patchTimelineMoveGroups:113 short-circuits if (patched === original) continue; per-group. Correct — a no-op group is skipped from filesToSave. But a group with a subset of edits that individually no-op would still land in groupResults if ANY edit changed the patched string; the shift-computation phase 3 (line 141-146) then correctly filters shifts to updates.start - element.start !== 0. Correct end-to-end.
Test coverage on timelineElementsMove.test.ts (+157 lines) and timelineClipDragCommit.test.ts (+242 lines) is thorough on the pure-function boundary. Not a substitute for the deleted useTimelineEditing.test.tsx integration suite (Miga's #2213 nit) — that's a stack-level follow-up.
Overall
🟢 with two watch-notes for the swap PR (#2212):
- Consolidate the three structurally-identical local edit shapes (
TimelineMoveEdit,TimelineElementMoveEdit, and the canonicalTimelineGroupMoveEditfrom #2205'stimelineCallbacks.ts) to a single import once the engine is wired — prevents silent drift. - Fire-and-forget error path in
persistMoveEdits: consider a typed error callback onDragCommitDepsso a UI toast can surface the rollback (currently onlyconsole.error).
Neither is blocking at #2206. Solid module boundary, well-tested pure functions, cleanly consumes the coexistence-layer types.
R1 by Via
a2f4dd0 to
7d93612
Compare
0d3d115 to
de842df
Compare
7d93612 to
dbb7d62
Compare
de842df to
4500814
Compare
What: the NLE clip-drag engine core: timelineClipDragPreview (zone-aware preview placement; keeps the legacy resolveDragPreviewPlacement export until the timeline swap), timelineClipDragCommit (drop → move/insert commit, stacking-synced) and timelineElementsMove (batched ripple/insert move with GSAP position shifting), with test suites. Why: the drag-drop brain, reviewable as functions before any component mounts it. How: new files plus one authored intermediate (preview file = final content + legacy export the old engine still imports); consumes the collision/ snapping/zones/stacking modules and the coexistence layer. Test plan: bunx vitest run on both test suites; tsc --noEmit; fallow audit clean.
4500814 to
61bd37e
Compare
0a4df4f to
4675cb9
Compare

What
the NLE clip-drag engine core: timelineClipDragPreview (zone-aware preview placement; keeps the legacy resolveDragPreviewPlacement export until the timeline swap), timelineClipDragCommit (drop → move/insert commit, stacking-synced) and timelineElementsMove (batched ripple/insert move with GSAP position shifting), with test suites.
Why
the drag-drop brain, reviewable as functions before any component mounts it.
How
new files plus one authored intermediate (preview file = final content + legacy export the old engine still imports); consumes the collision/ snapping/zones/stacking modules and the coexistence layer.
Test plan
bunx vitest run on both test suites; tsc --noEmit; fallow audit clean.
Stack position 15/25 — studio NLE overhaul (CapCut-parity timeline + canvas). Graphite manages bases; merge from #2192 upward. Temporary
TEMP(studio-dnd)fallow entries (unwired-component windows) are all removed by #2213 (app-shell swap), whose tree is byte-identical to the fully-verified integration branch.