Skip to content

feat(studio): add ripple, roll, slip and slide trim tools - #3341

Open
miguel-heygen wants to merge 1 commit into
mainfrom
feat/studio-trim-tools
Open

feat(studio): add ripple, roll, slip and slide trim tools#3341
miguel-heygen wants to merge 1 commit into
mainfrom
feat/studio-trim-tools

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

What

Four dedicated trim tools for the Studio timeline, alongside Select and Razor:

The timeline toolbar: selection, razor, then ripple, roll, slip and slide
Tool Key What it does to the track
Ripple trim T Trim a clip edge; every later clip on the track follows, so no gap or overlap appears.
Roll edit ⇧T Move the cut between two butted clips; one grows by exactly what the other gives up, nothing downstream moves.
Slip Y Change which part of the source plays. Position and duration are untouched.
Slide ⇧Y Move a clip along the track; its neighbours absorb the move.

Closes #3285 (except the optional FCP-style precision view — see "Not covered").

Why

Timeline trim only ever changed the grabbed clip's own in/out. On a track with
more than one clip that means every trim opens a gap or an overlap that has to
be closed by hand, and there is no single action for "trim this and shift the
rest to match". Roll, slip and slide had no equivalent at all.

Demo

Recorded against a real Studio on a track of A 0-8, B 8-12, C 12-16 (plus a
sub-composition on a second lane). Every clip below is the actual pointer
gesture, not a mock-up.

Ripple — drag A's out point right; B and C follow, and the second lane is
untouched.

Ripple trim: dragging clip A's out point pushes B and C along the track

Roll — drag the A|B cut; A grows by exactly what B gives up and C never
moves.

Roll edit: the cut between A and B moves, C stays where it is

Slide — drag B along the track; A stretches and C shortens to absorb it, so
the track keeps its total length.

Slide: clip B moves while A and C absorb the move

Slip has no motion to record — that is the whole point of it. On the
sub-composition clip it moved the in point 3s → 4.5s while start and
duration stayed at 4 + 6. On a clip with no source at all it refuses up
front and says why, instead of starting a gesture that does nothing:

Slipping a clip with no source media shows: Only clips with source media can be slipped

How

  • timelineTrimOps.ts is pure math with no store or DOM in it:
    resolveTrimPlan decides which clips an operation touches (and returns null
    when the gesture is impossible), resolveTrimDeltaBounds says how far it may
    travel, applyTrimDelta produces the per-clip patches. Semantics follow FCP /
    Premiere: a head ripple keeps the clip's start and pulls the lane in
    behind it, which is what makes it a ripple rather than a plain trim.
  • timelineTrimSession.ts is the gesture layer. A trim session is a
    TimelineGroupResizeSession, so the whole existing pipeline downstream —
    projection rendering, escape-cancel, and the atomic single-undo commit through
    commitTimelineGroupResize — is reused unchanged. Only the preview math
    differs. No new persistence plumbing.
  • All four are lane-scoped (matching the existing gap tooling), clamped by
    available source media and the minimum clip duration, and snap the edge that
    actually moves, with every clip that rides along excluded from the snap grid.
  • A tool that cannot act on a clip refuses at pointerdown and says why,
    reusing the existing blocked-edit report — which was already being handed an
    intent it threw away.
  • Tool metadata (label, shortcut, tooltip) has one owner, read by the toolbar,
    the keyboard handler and the shortcuts panel.

Two adjacent cleanups came along because the repo gates flagged them once these
files were in scope: the duplicated remove-element POST behind the two delete
paths moved into one helper, and the timeline shell's inline style moved into
the theme module (both files were at the 600-line cap).

Test plan

  • Unit tests added/updated — 43 new tests: the trim math (bounds, clamps,
    playback-rate scaling, adjacency invariants for every op), the session
    layer (lane scoping, capability refusal, snapping), and six end-to-end
    gesture tests through the drag hook asserting exactly what gets persisted.

  • Manual testing performed — drove a real Chrome against Studio and read
    back the project file after each gesture rather than trusting the UI.
    Each gesture dragged +2s:

    Gesture Result on disk
    Ripple A's out A 0+10 B 10+4 C 14+4 — second lane untouched
    Ripple B's in A 0+8 B 8+2 C 10+4 — B's start pinned, lane closed behind
    Roll A|B A 0+10 B 10+2 C 12+4 — C never moved
    Slide B A 0+10 B 10+4 C 14+2 — track length unchanged
    Slip the sub-comp clip 4+6 unchanged, in point 3 → 4.5
    Slip a source-less clip refused, nothing written, toast shown
    Plain trim / plain drag (Select) unchanged from before this branch

    One ⌘Z after a ripple restored the whole lane, confirming the batch is a
    single undo step. All four shortcuts (T, ⇧T, Y, ⇧Y), V back to
    Select, and the toolbar buttons were driven through the real UI.

  • Full packages/studio suite green (4308 tests), typecheck, oxlint, oxfmt.

Not covered

  • The FCP-style two-up precision view from the issue (marked optional there)
    is not in this PR; it is a separate component and follows on its own branch.
  • Cross-track (sync-locked) ripple. Every operation stays on the grabbed
    clip's own lane, matching the existing "close gap" tooling.
  • Pre-existing, left alone deliberately: a plain Select-tool trim writes
    data-playback-start onto clips that have no source at all (a div gets
    in=0 on an out-trim, in=2 on a head-trim). The trim tools here do not do
    that — they only write an in point on a clip that has one — but fixing the
    shared plain-resize path changes what every head-trim writes and wants its own
    change with its own regression tests. The root is that the runtime clip
    manifest reports playbackStart: 0 for every element, so nothing downstream
    can tell "no in point" from "in point at zero". Happy to pick it up next.

Size

~1.6k changed lines against the 1k guideline, about 900 of which are the new
pure-math and gesture modules and ~450 are tests. The four operations share one
plan/bounds/apply module and one session type, so splitting them into two PRs
would cut that module in half and leave the first half untestable on its own.
Flagging rather than hiding it.

The timeline could only trim one clip's own in/out, so every re-edit of a
multi-clip track left a gap or an overlap to close by hand. Four tools now
own the standard NLE edit operations, each committing as one atomic,
single-undo batch through the existing group-resize persist.

- Ripple (T): trim an edge, every later clip on the track follows.
- Roll (Shift+T): move the cut between two clips; nothing downstream moves.
- Slip (Y): change which part of the source plays; nothing moves.
- Slide (Shift+Y): move a clip; its neighbours absorb the move.

All four are lane-scoped, clamped by available source media and the minimum
clip duration, snap the edge that actually moves, and refuse up front when
the tool cannot act on the clip (a roll with no clip across the cut, a slip
on generated pixels) with a message that says which.

Two adjacent cleanups came with it: the blocked-edit toast now reads the
intent it was already being handed instead of ignoring it, and the
duplicated remove-element POST behind the two delete paths moved into one
helper.
Comment on lines +157 to +164
return fetch(
`/api/projects/${projectId}/file-mutations/remove-element/${encodeURIComponent(targetPath)}`,
{
method: "POST",
headers: { "Content-Type": "application/json", ...studioWriteHeaders() },
body: JSON.stringify({ target }),
},
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: dedicated trim mode (ripple/roll/slip/slide) for the Studio timeline

2 participants