Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit d940b846aa7d055dd4d13b2d3c8532887eb24515
parent 9d2bd76b6f911152667623f503731478bb54d81b
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Thu,  1 Oct 2026 15:19:57 -0400

umtool e2e: the posts and mute specs follow whole-frame holds and the build's mute fade

Holds round to whole frames (2.5 s at the fixture's 15 fps is 38 frames,
2.533 s), so every pinned start, window and tail moved with them; the mid-move
seek comes from the schedule's own move. The mute mark is silent AT the mark
(the build's fade ends there), not 0.05 s after it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Diffstat:
Mumtool/e2e/clip-bench.spec.ts | 6+++---
Mumtool/e2e/onscreen-posts.spec.ts | 62+++++++++++++++++++++++++++++++++++---------------------------
2 files changed, 38 insertions(+), 30 deletions(-)

diff --git a/umtool/e2e/clip-bench.spec.ts b/umtool/e2e/clip-bench.spec.ts @@ -1009,13 +1009,13 @@ test("the mute mark: picked on the waveform, heard at once, saved, shown, cleare // The pick plays ACROSS the mark -- up to three seconds before it (here // from the selection's start), to the end of the selection -- and the sound - // goes at the mark: the last non-zero frame is where the fade (0.05 s) runs - // out, not the 6.00 the tone runs on to. + // goes at the mark: the fade (MUTE_FADE, the build's) ENDS there, so the + // last non-zero frame is at the mark, not the 6.00 the tone runs on to. expect(play.engine).toBe("webaudio"); expect(play.from).toBeCloseTo(Math.max(3, mark - 3), 2); expect(play.to).toBeCloseTo(6, 2); const { last, sr } = tap!; - const silentAt = play.ctxStart + (mark - play.from) + 0.05; + const silentAt = play.ctxStart + (mark - play.from); expect(Math.abs((last + 1) / sr - silentAt), "silent at the mark, not at the end").toBeLessThan(0.01); // Saved by `save window`, like the edges; the manifest has it. diff --git a/umtool/e2e/onscreen-posts.spec.ts b/umtool/e2e/onscreen-posts.spec.ts @@ -17,13 +17,14 @@ import { deckGeometry, postWindows, postsGeometry, shiftedFootage } from "umtool // // With the defaults (posts.seconds 4, hold 2.5, shift on) and the fixture's // 0.2 s crossfade, the estimate is: -// c01 0 → 5.5 3 s clip + a 2.5 s hold; two posts in 5.3 s share it, -// p-early at 0, p-mid at 2.65 -// c02 5.3 → 10.8 3 s clip + a 2.5 s hold; p-late at 6.6, 4 s before -// its leave at 10.6 -// k01 10.6 → 13.6 the card +// c01 0 → 5.533 3 s clip + a 2.5 s hold, which at the fixture's +// 15 fps is 38 frames (2.533 s); two posts in +// 5.333 s share it, p-early at 0, p-mid at 2.667 +// c02 5.333 → 10.867 the same; p-late at 6.667, 4 s before its leave +// at 10.667 +// k01 10.667 → 13.667 the card // and the footage moves aside as each clip's first post appears (c01 at 0, -// c02 at 6.6), with the posts column at the frame's right edge. +// c02 at 6.667), with the posts column at the frame's right edge. // // The posts region's COMPOSITION is compose-chrome's (`region: "posts"`): the // preview test asserts every window composes and its page reports @@ -275,20 +276,22 @@ test("the preview composes the posts region per window and overlays it while the // The defaults' timing: 4 s per post, each carrying clip held 2.5 s, every // start and the total measured with the holds. expect(pv.schedule.segments.map((s) => [s.id, s.start, s.duration, s.hold ?? 0])).toEqual([ - ["c01", 0, 5.5, 2.5], - ["c02", 5.3, 5.5, 2.5], - ["k01", 10.6, 3, 0], + // The hold is rounded to whole frames: 2.5 s at the fixture's 15 fps is + // 37.5 frames, so 38 -- 2.533 s. + ["c01", 0, 5.533, 2.533], + ["c02", 5.333, 5.533, 2.533], + ["k01", 10.667, 3, 0], ]); - expect(pv.schedule.total).toBe(13.6); + expect(pv.schedule.total).toBe(13.667); expect(pv.schedule.posts.map((p) => [p.id, p.segment, p.appear, p.out])).toEqual([ - ["p-early", "c01", 0, [5.3, 5.5]], - ["p-mid", "c01", 2.65, [5.3, 5.5]], - ["p-late", "c02", 6.6, [10.6, 10.8]], + ["p-early", "c01", 0, [5.333, 5.533]], + ["p-mid", "c01", 2.667, [5.333, 5.533]], + ["p-late", "c02", 6.667, [10.667, 10.867]], ]); - expect(pv.posts.windows.map(({ from, to }) => [from, to])).toEqual([[0, 5.5], [6.6, 10.8]]); + expect(pv.posts.windows.map(({ from, to }) => [from, to])).toEqual([[0, 5.533], [6.667, 10.867]]); // The footage makes room: one move per carrying clip, and the column at the frame's edge. const render = readManifest().render; - expect(pv.schedule.moves?.map((m) => [m.segment, m.at, m.seconds])).toEqual([["c01", 0, 0.6], ["c02", 6.6, 0.6]]); + expect(pv.schedule.moves?.map((m) => [m.segment, m.at, m.seconds])).toEqual([["c01", 0, 0.6], ["c02", 6.667, 0.6]]); expect(pv.schedule.moves?.[0].from).toEqual(deckGeometry(render).footage); expect(pv.schedule.moves?.[0].to).toEqual(shiftedFootage(render)); expect(pv.posts.geometry).toEqual(postsGeometry(render)); @@ -310,25 +313,25 @@ test("the preview composes the posts region per window and overlays it while the await expect(page.locator("[data-posts-window]")).toHaveCount(2); // The scrubber runs the held length, and the window marks sit on its clock. - await expect(page.getByTestId("onscreen-scrubber")).toHaveAttribute("max", "13.6"); - await expect(page.getByTestId("onscreen-time")).toContainText("/ 0:13.6"); - for (const [seg, from, to] of [["c01", 0, 5.5], ["c02", 6.6, 10.8]] as const) { + await expect(page.getByTestId("onscreen-scrubber")).toHaveAttribute("max", "13.667"); + await expect(page.getByTestId("onscreen-time")).toContainText("/ 0:13.7"); + for (const [seg, from, to] of [["c01", 0, 5.533], ["c02", 6.667, 10.867]] as const) { const style = await page.locator(`[data-posts-window="${seg}"]`).evaluate((el) => [ parseFloat((el as HTMLElement).style.left), parseFloat((el as HTMLElement).style.width), ]); - expect(style[0]).toBeCloseTo((from / 13.6) * 100, 2); - expect(style[1]).toBeCloseTo(((to - from) / 13.6) * 100, 2); + expect(style[0]).toBeCloseTo((from / 13.667) * 100, 2); + expect(style[1]).toBeCloseTo(((to - from) / 13.667) * 100, 2); } // A held segment shows its hold: a hatched tail, hold/duration of its block. await expect(page.getByTestId("onscreen-segment-hold")).toHaveCount(2); - await expect(page.locator('[data-seg-jump="c01"]')).toHaveAttribute("data-hold", "2.5"); + await expect(page.locator('[data-seg-jump="c01"]')).toHaveAttribute("data-hold", "2.533"); await expect(page.locator('[data-seg-jump="k01"]')).toHaveAttribute("data-hold", "0"); - await expect(page.locator('[data-seg-jump="c01"]')).toHaveAttribute("title", /held 2\.5 s for its posts/); + await expect(page.locator('[data-seg-jump="c01"]')).toHaveAttribute("title", /held 2\.533 s for its posts/); const block = (await page.locator('[data-seg-jump="c02"]').boundingBox())!; const tail = (await page.locator('[data-seg-hold="c02"]').boundingBox())!; - expect(tail.width / block.width).toBeCloseTo(2.5 / 5.5, 1); + expect(tail.width / block.width).toBeCloseTo(2.533 / 5.533, 1); expect(Math.abs(tail.x + tail.width - (block.x + block.width))).toBeLessThan(2); // Outside every window: nothing over the footage. k01 starts after the last. await page.locator('[data-seg-jump="k01"]').click(); @@ -352,7 +355,7 @@ test("the preview composes the posts region per window and overlays it while the expect(Math.abs(box.width / frame.width - pv.posts.geometry.width / W)).toBeLessThan(0.01); // The backdrop moves inside the window: the window mark put the scrubber at - // 8.7, past c02's move (6.6 → 7.2), so the footage is all the way aside... + // 8.7, past c02's move (6.667 → 7.267), so the footage is all the way aside... const from = pv.schedule.moves![1].from; const to = pv.schedule.moves![1].to; const backdrop = page.getByTestId("onscreen-backdrop-frame"); @@ -364,10 +367,15 @@ test("the preview composes the posts region per window and overlays it while the // ...its box's left edge where the build puts it (in frame pixels)... expect((aside[2] / aside[4]) * W + aside[0] * from.x).toBeCloseTo(to.x, 0); // ...half way through the move at its middle, on the smoothstep curve... - await seek(page, 6.9); - await expect(backdrop).toHaveAttribute("data-move-progress", "0.5"); + // (the move's middle from the schedule itself; the scrubber steps in 0.01 s, + // so the progress lands within a step of 0.5, not on it) + const mid = pv.schedule.moves![1].at + pv.schedule.moves![1].seconds / 2; + await seek(page, Math.round(mid * 100) / 100); + await expect + .poll(async () => Math.abs(Number(await backdrop.getAttribute("data-move-progress")) - 0.5)) + .toBeLessThan(0.02); const half = (await backdropMatrix(page))!; - expect(half[0]).toBeCloseTo((1 + to.width / from.width) / 2, 3); + expect(half[0]).toBeCloseTo((1 + to.width / from.width) / 2, 2); // ...in its box before the move... await seek(page, 6.5); await expect(page.getByTestId("onscreen-current")).toHaveText("c02");