feat(studio): timeline z-stacking sync model#2198
Open
ukimsanov wants to merge 1 commit into
Open
Conversation
This was referenced Jul 11, 2026
Collaborator
Author
This was referenced Jul 11, 2026
a3c734a to
e90bfcc
Compare
4622bb9 to
d0a6ab1
Compare
e90bfcc to
af7dd22
Compare
d0a6ab1 to
206097c
Compare
miga-heygen
reviewed
Jul 11, 2026
miga-heygen
left a comment
Contributor
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
Collaborator
There was a problem hiding this comment.
Verdict: LGTM (green) — permutation-preserving, cascade minimal
The z-stacking resolver's cleverness is in its restraint: try the single-clip patch first (authored z sacred), fall through to cascade only when ties or the z=0 floor make a single value inexpressible. And the cascade lifts only the neighbours that would otherwise no longer paint strictly above. Handsomely done.
The pointed queries
- Output is a valid permutation of inputs.
packages/studio/src/player/components/timelineStackingSync.ts:230–:258— patches are emitted through achanged: Map<key, zIndex>deduplicated byemitted: Set. No key appears twice; no key fromelementsis dropped from consideration (the resolver iterates every edited clip and every overlapping neighbour). By construction the patch set is a subset-permutation of the input keys, one z per key. No drops, no duplicates. Tested throughout — see the multi-clip test at :115–:120 ({ a: 7, b: 6 }is single-emit per key) and the cascade at :166–:180. - Round-trip through
normalizeToZones(cross-PR invariant with #2199). This is verified directly in#2199's test file attimelineZones.test.ts:332–:405—lane-move → z patch → re-discovery orders lanes by that same z → identical lanesandqa-clean: drag video BELOW ralu → z patch → re-pack keeps it below, no oscillation. Both close the loop:computeStackingPatches(#2198)output projected onto TimelineElement.zIndex, fed back throughnormalizeToZones(#2199), yields a fixed point. That is the invariant this batch turns on and it is empirically nailed. - Half-open overlap.
overlapsInTimeat :57–:59 uses<with anEPS = 1e-6guard on the right side (a.start < b.start + b.duration - EPS). Different from#2195's exact strict-<— the epsilon defends against floating-point fuzz creating spurious overlaps in the stacking resolver (a visible artifact) where#2195doesn't need it because a collision decision is boolean and idempotent. Justified divergence. Test at :109–:113:touching-but-not-overlapping intervals do NOT count—a(0..5) andb(5..5+5), no patch. Correct. - Authored z sacred (fast path).
trySingleZat :162–:176 attempts a strictly-between integer first; only whenminAbove - maxBelow < 2or when the only above-neighbour is at z=0 does it fall to the cascade. Test at :50–:54 confirms already-correct edits yield no patch; :91–:101 confirms only-below / only-above bounds. - Cascade minimality.
liftAboveat :183–:196 sorts above-neighbours lowest-first and bumps each only if its current z ≤ the floor. Test at :166–:180 pinsonly the blockers move— a non-overlapping neighbourxis not touched. - DOM-order tie-breaking.
paintsAboveat :78–:82 usesdomIndexwhen zs are equal — the sync sees the actual paint reality and doesn't under-patch on equal-z ties. Test at :145–:153 usesdomIndexon equal-z clips and verifies the cascade fires. - Audio exclusion. :123 filters out
isAudioneighbours before the overlap set is computed.musicneighbour test at :85–:89 pins the invariant.
Nits (non-blocking)
overlapsInTimeuses- EPSwhiletimeRangesOverlapin#2195uses exact<. The divergence is intentional and defensive here, but a one-line comment noting the choice would help a future reader who reads both files back-to-back and wonders whether the two are meant to align.
R1 by Via
af7dd22 to
bf91abd
Compare
1ed5c27 to
f2fbc25
Compare
bf91abd to
0691107
Compare
0691107 to
9e58745
Compare
f2fbc25 to
6730438
Compare
What: new pure module timelineStackingSync — lane order ↔ z-index reconciliation (laneIsAbove, computeStackingPatches) with tests. Why: the single source of truth for how timeline lane order maps to canvas stacking; the ordering rules and tie-breaks live here. How: new files only; consumed later by timelineZones and the stacking-sync hook. Test plan: bunx vitest run timelineStackingSync.test.ts; tsc --noEmit; fallow audit clean.
6730438 to
7f34c4e
Compare
9e58745 to
468cb87
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

What
new pure module timelineStackingSync — lane order ↔ z-index reconciliation (laneIsAbove, computeStackingPatches) with tests.
Why
the single source of truth for how timeline lane order maps to canvas stacking; the ordering rules and tie-breaks live here.
How
new files only; consumed later by timelineZones and the stacking-sync hook.
Test plan
bunx vitest run timelineStackingSync.test.ts; tsc --noEmit; fallow audit clean.
Stack position 7/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.