feat(studio): timeline leaf helpers — audio inspector, zoom math, UI prefs#2192
feat(studio): timeline leaf helpers — audio inspector, zoom math, UI prefs#2192ukimsanov wants to merge 1 commit into
Conversation
68911ca to
1f6c9fe
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.
🟢 LGTM — leaf helpers land clean, persistence key stable, math checks out.
Focus items — verified.
Persistence key (studioUiPreferences.ts). The STUDIO_UI_PREFERENCES_KEY = "hf-studio-ui-preferences" constant is byte-identical to main, and every new field (timelineSnapEnabled, timeDisplayMode, timelineZoomMode, timelineManualZoomPercent) is strictly additive to StudioUiPreferences — existing users' saved prefs still parse under the same key, and each new field is guarded by its own runtime type-check in readStorage so garbage / missing fields fall through to undefined. No migration risk. snapEnabled (canvas) and timelineSnapEnabled (timeline magnet) coexist as distinct fields — that's a naming close-call but it's an addition, not a rename, so no upgrade loss.
computePinnedZoomPercent math (timelineZoom.ts:110-123). In manual mode getTimelinePixelsPerSecond computes pps = fitPps × (percent / 100). Solving for percent gives (currentPps / fitPps) × 100, which is exactly what this returns. The four-way guard (!isFinite, <= 0 on both inputs) covers zero-divide + NaN/Infinity + negative — falls back to 100 (a no-op pin at the current fit), which is the right degenerate. Clamp to [MIN, MAX] prevents a ratio outside slider bounds from silently becoming an unreachable zoom. Round-trip test on line 62 verifies getTimelinePixelsPerSecond(fitPps, "manual", percent) restores currentPps — closes the loop.
Log-slider math (timelineZoomPercentToSlider / timelineSliderToZoomPercent). LOG_MIN = ln(10) ≈ 2.303, LOG_MAX = ln(2000) ≈ 7.601. At percent = 100: (ln(100) - ln(10)) / (ln(2000) - ln(10)) × 100 ≈ 43.4 — test at line 35 asserts this exact formula. Round-trip test on lines 80-88 verifies slider→zoom→slider stays within ±1% for [10, 100, 500, 2000] — sufficient tolerance for a slider that only takes integer pixel positions. Slider input is clamped [0, 100] before applying, so out-of-range inputs can't produce non-finite output.
Test coverage. Every new export has a direct test: computePinnedZoomPercent (4 tests: no-op / round-trip / clamp / degenerate fallback), timelineZoomPercentToSlider / timelineSliderToZoomPercent (3+2 boundary + round-trip), timelineSnapEnabled (2: round-trip + non-boolean reject), timelineZoomMode+timelineManualZoomPercent (2: round-trip pinned mode + invalid-input reject), isAudioTimelineElement (5), isMusicTrack (5), resolveBeatSourceTrack (7). No dark exports.
One nit, non-blocking. resolveBeatSourceTrack docstring says "the LONGEST untagged audio clip is used as a fallback" — actual code (if (!best || el.duration > best.duration) best = el) picks the first max on ties. Not wrong, just worth knowing if the ordering matters for beat analysis. Tests don't exercise a tie.
R1 by Via
1f6c9fe to
c161c77
Compare
c161c77 to
df6ca19
Compare
df6ca19 to
063395d
Compare
…prefs What: extends three leaf modules to their final NLE-stack form, tests in the same change: timelineInspector (isAudioTimelineElement, resolveBeatSourceTrack), timelineZoom (zoom/pps math incl. computePinnedZoomPercent), and studioUiPreferences (persisted editor prefs). Why: leaf dependencies of the NLE timeline stack; landing them first keeps the later glue PRs to wiring. How: additive from the consumer side — every export main already uses is unchanged (typecheck against main's consumers passes untouched); every new export is exercised by a test in this PR. Test plan: bunx vitest run on the three test files; tsc --noEmit in packages/studio; fallow audit --base origin/main clean.
063395d to
4a87da0
Compare

What
extends three leaf modules to their final NLE-stack form, tests in the same change: timelineInspector (isAudioTimelineElement, resolveBeatSourceTrack), timelineZoom (zoom/pps math incl. computePinnedZoomPercent), and studioUiPreferences (persisted editor prefs).
Why
leaf dependencies of the NLE timeline stack; landing them first keeps the later glue PRs to wiring.
How
additive from the consumer side — every export main already uses is unchanged (typecheck against main's consumers passes untouched); every new export is exercised by a test in this PR.
Test plan
bunx vitest run on the three test files; tsc --noEmit in packages/studio; fallow audit --base origin/main clean.
Stack position 1/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.