feat(studio): timeline leaf helpers — audio inspector, zoom math, UI prefs (#2192)

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.
This commit is contained in:
Ular Kimsanov
2026-07-11 20:20:29 -04:00
committed by GitHub
parent ae4946e624
commit 920dad35eb
6 changed files with 336 additions and 1 deletions
@@ -1,12 +1,15 @@
import { describe, expect, it } from "vitest";
import {
clampTimelineZoomPercent,
computePinnedZoomPercent,
getNextTimelineZoomPercent,
getPinchTimelineZoomPercent,
getTimelinePixelsPerSecond,
getTimelineZoomPercent,
MAX_TIMELINE_ZOOM_PERCENT,
MIN_TIMELINE_ZOOM_PERCENT,
timelineZoomPercentToSlider,
timelineSliderToZoomPercent,
} from "./timelineZoom";
describe("clampTimelineZoomPercent", () => {
@@ -81,3 +84,67 @@ describe("getPinchTimelineZoomPercent", () => {
expect(getPinchTimelineZoomPercent(-10000, "manual", 100)).toBe(MAX_TIMELINE_ZOOM_PERCENT);
});
});
describe("timelineZoomPercentToSlider", () => {
it("maps min zoom to slider position 0", () => {
expect(timelineZoomPercentToSlider(MIN_TIMELINE_ZOOM_PERCENT)).toBeCloseTo(0, 5);
});
it("maps max zoom to slider position 100", () => {
expect(timelineZoomPercentToSlider(MAX_TIMELINE_ZOOM_PERCENT)).toBeCloseTo(100, 5);
});
it("maps 100% to the log midpoint between 10 and 2000", () => {
const expected = ((Math.log(100) - Math.log(10)) / (Math.log(2000) - Math.log(10))) * 100;
expect(timelineZoomPercentToSlider(100)).toBeCloseTo(expected, 3);
});
});
describe("timelineSliderToZoomPercent", () => {
it("maps slider 0 to min zoom", () => {
expect(timelineSliderToZoomPercent(0)).toBe(MIN_TIMELINE_ZOOM_PERCENT);
});
it("maps slider 100 to max zoom", () => {
expect(timelineSliderToZoomPercent(100)).toBe(MAX_TIMELINE_ZOOM_PERCENT);
});
});
describe("computePinnedZoomPercent", () => {
it("returns 100 when current pps equals the fit pps (a no-op pin at the current fit)", () => {
expect(computePinnedZoomPercent(42, 42)).toBe(100);
});
it("reproduces the current pps: percent × fitPps / 100 === currentPps", () => {
const fitPps = 20;
const currentPps = 50; // user zoomed in 2.5×
const percent = computePinnedZoomPercent(currentPps, fitPps);
expect(percent).toBe(250);
// Round-trips through getTimelinePixelsPerSecond back to the on-screen pps.
expect(getTimelinePixelsPerSecond(fitPps, "manual", percent)).toBeCloseTo(currentPps, 5);
});
it("clamps a pin that would exceed the manual-zoom bounds", () => {
// currentPps 10000 / fitPps 1 = 1_000_000% → clamped to MAX.
expect(computePinnedZoomPercent(10000, 1)).toBe(MAX_TIMELINE_ZOOM_PERCENT);
// Tiny ratio → clamped up to MIN.
expect(computePinnedZoomPercent(0.001, 1000)).toBe(MIN_TIMELINE_ZOOM_PERCENT);
});
it("falls back to 100 for unusable inputs (a safe no-op pin)", () => {
expect(computePinnedZoomPercent(Number.NaN, 20)).toBe(100);
expect(computePinnedZoomPercent(50, 0)).toBe(100);
expect(computePinnedZoomPercent(-5, 20)).toBe(100);
expect(computePinnedZoomPercent(50, Number.POSITIVE_INFINITY)).toBe(100);
});
});
describe("timelineZoomPercentToSlider / timelineSliderToZoomPercent round-trip", () => {
for (const percent of [10, 100, 500, 2000]) {
it(`round-trips ${percent}% within ±1%`, () => {
const slider = timelineZoomPercentToSlider(percent);
const back = timelineSliderToZoomPercent(slider);
expect(Math.abs(back - percent) / percent).toBeLessThan(0.01);
});
}
});