feat(studio): add variable timeline timing and layout

This commit is contained in:
Miguel Angel Simon Sierra
2026-07-27 19:52:05 +02:00
parent 521bba6437
commit d518972f8b
14 changed files with 671 additions and 188 deletions
@@ -2,6 +2,7 @@ import { describe, it, expect } from "vitest";
import {
RULER_H,
TRACK_H,
LANE_H,
TRACKS_TOP_PAD,
TRACKS_BOTTOM_PAD,
GUTTER,
@@ -9,10 +10,84 @@ import {
getTimelineRowTop,
getTimelineScrubTime,
getTimelineRowFromY,
getTimelineRowOffsets,
getTimelineCanvasHeight,
trackHeights,
resolveTimelineAssetDrop,
} from "./timelineLayout";
describe("variable timeline row geometry", () => {
const tracks = [
[{ clipId: "a", laneCount: 0 }],
[{ clipId: "b", laneCount: 2 }],
[{ clipId: "c", laneCount: 1 }],
];
it("resolves every row to the base height when no clip is expanded", () => {
expect(trackHeights(tracks)).toEqual([TRACK_H, TRACK_H, TRACK_H]);
expect(trackHeights(3)).toEqual([TRACK_H, TRACK_H, TRACK_H]);
});
it("adds one lane height per lane on an expanded clip", () => {
expect(trackHeights(tracks, new Set(["b"]))).toEqual([TRACK_H, TRACK_H + 2 * LANE_H, TRACK_H]);
});
it("derives row tops from cumulative offsets", () => {
const heights = trackHeights(tracks, new Set(["b"]));
expect(getTimelineRowOffsets(heights)).toEqual([
0,
TRACK_H,
2 * TRACK_H + 2 * LANE_H,
3 * TRACK_H + 2 * LANE_H,
]);
expect(getTimelineRowTop(2, heights)).toBe(RULER_H + TRACKS_TOP_PAD + 2 * TRACK_H + 2 * LANE_H);
});
it("maps y inside an expanded lane region back to the expanded track", () => {
const heights = trackHeights(tracks, new Set(["b"]));
const yInSecondExpandedLane = getTimelineRowTop(1, heights) + TRACK_H + LANE_H * 1.5;
const row = getTimelineRowFromY(yInSecondExpandedLane, heights);
expect(Math.floor(row)).toBe(1);
expect(row).toBeGreaterThan(1.5);
expect(row).toBeLessThan(2);
});
it("sums resolved row heights into the canvas height", () => {
const heights = trackHeights(tracks, new Set(["b"]));
expect(getTimelineCanvasHeight(heights)).toBe(
RULER_H + TRACKS_TOP_PAD + 3 * TRACK_H + 2 * LANE_H + TRACKS_BOTTOM_PAD,
);
});
});
describe("collapsed timeline row geometry characterization", () => {
it.each([
[0, 74],
[1, 122],
[4, 266],
])("keeps row %i at content y=%i", (row, expectedTop) => {
expect(getTimelineRowTop(row)).toBe(expectedTop);
});
it.each([
[74, 0],
[86, 0.25],
[146, 1.5],
[290, 4.5],
])("maps content y=%i to fractional row %f", (contentY, expectedRow) => {
expect(getTimelineRowFromY(contentY)).toBe(expectedRow);
});
it.each([
[0, 146],
[1, 194],
[3, 290],
[5, 386],
])("keeps the %i-track canvas height at %i", (trackCount, expectedHeight) => {
expect(getTimelineCanvasHeight(trackCount)).toBe(expectedHeight);
});
});
describe("track-area breathing pad y-math", () => {
describe("getTimelineRowTop", () => {
it("offsets the first lane below the ruler by the top pad", () => {