Skip to content

fix(studio): scope untimed timeline elements to their enclosing clip - #3968

Open
miguel-heygen wants to merge 2 commits into
mainfrom
fix-studio-implicit-timeline-scope
Open

miguel-heygen wants to merge 2 commits into
mainfrom
fix-studio-implicit-timeline-scope

Conversation

@miguel-heygen

Copy link
Copy Markdown
Collaborator

What changed

Studio's timeline showed an untimed element nested inside an authored clip (an off-screen source image or bare canvas a scene's own script draws from) as a row spanning the entire composition, instead of just its enclosing clip's window. Two independent code paths had this defect and both are fixed here:

  • packages/core/src/runtime/timeline.ts — the runtime clip-manifest builder that runs inside every composition. It only recognized sub-composition ancestors when looking for a time window to inherit; a plain authored clip (data-start with no data-composition-id) was invisible to it.
  • packages/studio/src/player/lib/timelineDOM.ts — the Studio frontend's DOM-based fallback, used for elements the runtime manifest doesn't cover (e.g. a bare <canvas>). Same defect: any untimed child of the composition root got start: 0, duration: <full film> hardcoded.

Both now walk up to the nearest ancestor that defines a time window — a sub-composition or a plain authored clip — instead of only recognizing sub-compositions.

Scope change, disclosed

The Studio-side fix is broader than "narrow a too-wide row." Before this change, only the composition root's direct children could ever appear as an implicit timeline row. This generalizes that to every authored clip's direct children, so a composition where scenes have several classed or id'd content elements (headings, text containers, etc.) will now show one new implicit row per such element, per scene — not just for the canvas/image case that motivated the fix. These rows are read-only (implicit rows can't be moved, trimmed, or split), so nothing is written back to the file; the effect is purely on what the Studio timeline lists.

This tradeoff was reviewed and intentionally accepted rather than narrowed to media elements only.

Known residual limitation

An untimed element's inherited window is bounded by its nearest enclosing clip and clamped against the document root, but not against every clip in between when several are nested. A clip with no resolvable duration of its own still hands its children the remainder of its enclosing scope rather than a tighter bound. This is a real improvement over origin/main (which always fell through to the full film), not a complete fix of every nesting case — flagging it rather than claiming full correctness.

Verification

  • Typecheck: clean on both packages/core and packages/studio.
  • packages/core/src/runtime/timeline.test.ts: 64/64 passing (includes a new regression test for a clip expressing its window via data-end instead of data-duration, which previously could pair a start from one ancestor with a duration from another).
  • packages/studio/src/player/lib/timelineDOM.test.ts: 23/23 passing.
  • Lint (oxlint) over the 5 changed files: 0 errors.
  • Manually verified against a real multi-unit composition in Studio: the previously full-width implicit row now matches its enclosing clip's actual window.

An untimed element nested inside an authored clip (an off-screen source
image or bare canvas a scene's own script draws from) inherited the
ROOT composition's full duration instead of its enclosing clip's own
window, in both the runtime clip-manifest builder and the Studio
frontend's DOM fallback. Both now walk up to the nearest ancestor that
defines a time window - a sub-composition or a plain authored clip -
instead of only recognizing sub-compositions.
An untimed element's inherited start and duration each locked
independently at the first ancestor that resolved one, so a clip
authored with data-end (no data-duration) let the start lock there
while the duration search kept climbing to an unrelated farther
ancestor - a window neither clip actually has. Both now lock together
at one ancestor, and duration resolution delegates to the existing
resolveDurationForElement instead of re-deriving a narrower copy of it.

Also renames a Studio test whose name implied a width regression that
never existed (the pre-fix behavior was no row at all, not a full-width
one), and splits per-candidate row construction out of the scope loop
so the loop body reads at a glance.
@miguel-heygen

Copy link
Copy Markdown
Collaborator Author

Timeline before/after on the same project. Before: Ground and U04 Mosaicsrc both span the full ~50s timeline. After: U04's canvas/img rows are scoped to U04's own ~8.5-13.3s window, matching the U04 clip beside them.

pr-attach-before

pr-attach-after

@miguel-heygen

Copy link
Copy Markdown
Collaborator Author

The expansion is real, it was just cropped out of the last screenshot. Pulled the actual row count from this branch: 12 timeline rows before this fix, 80 after (u07 alone contributes 26, since it has that many named child elements). This crop shows labeled per-element rows (U04 Labels, U05 Pist, U05 Over, etc.) each scoped to their own unit's window, not the full 50s.

screenshot-1789510630627-11

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.

1 participant