From ecd444b53b612ecd5b7c40f7313321ec61d7fb6f Mon Sep 17 00:00:00 2001 From: BenStullsBets Date: Tue, 30 Jun 2026 14:18:16 -0700 Subject: [PATCH] fix(reduce-motion): re-roll the clip on an instant-cut altitude change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With reduce motion on, selecting a new altitude jumps (autoScrub's reduced branch → setPos) and skips the segment that would re-roll the destination clip, so activeClipId stayed on the previous altitude's clip and the video didn't change. Self-heal at the landing point: if the landed altitude's pool doesn't contain activeClipId, pickPoolClip a fresh one. TDD: new altitude-lock e2e. Co-Authored-By: Claude Opus 4.8 (1M context) --- simulator/e2e/tests/altitude-lock.spec.ts | 14 ++++++++++++++ simulator/static/app.js | 6 ++++++ 2 files changed, 20 insertions(+) diff --git a/simulator/e2e/tests/altitude-lock.spec.ts b/simulator/e2e/tests/altitude-lock.spec.ts index 32d9844..a98f86f 100644 --- a/simulator/e2e/tests/altitude-lock.spec.ts +++ b/simulator/e2e/tests/altitude-lock.spec.ts @@ -108,6 +108,20 @@ test("dragging the dial scrubs morph currentTime and audio gains", async ({ page await page.mouse.up(); }); +test("reduce motion: selecting a new altitude still swaps to a fresh clip (instant cut)", async ({ page }) => { + await boot(page); + // #scale-name reads " · (i/n)"; extract the clip id. + const clipId = (s: string) => (s.split("·")[1] || "").split("(")[0].trim(); + await page.locator('label[for="reduce-motion"]').click(); // engage reduce motion + await expect(page.locator("#reduce-motion")).toBeChecked(); + const before = (await page.locator("#scale-name").textContent())!; + await wheelOnStage(page, 60); // descend one altitude (instant cut) + await page.waitForFunction(() => /orbit/.test(document.querySelector("#scale-name")?.textContent || ""), null, { timeout: 20000 }); + const after = (await page.locator("#scale-name").textContent())!; + expect(after).toContain("orbit"); + expect(clipId(after)).not.toBe(clipId(before)); // the clip actually changed (not stuck on the old scale's clip) +}); + test("wheel auto-scrubs one altitude and locks", async ({ page }) => { await boot(page); const start = (await page.locator("#scale-name").textContent())!; diff --git a/simulator/static/app.js b/simulator/static/app.js index e47af7b..98e1461 100644 --- a/simulator/static/app.js +++ b/simulator/static/app.js @@ -1015,6 +1015,12 @@ function setPos(next) { const { lo, frac } = HEFScrub.segmentOf(pos); if (frac === 0) { // settled exactly on an altitude → lock + base loop + single soundtrack busy = false; + // If we JUMPED straight here (a reduced-motion instant cut skips the segment that + // would re-roll the destination), activeClipId still points at the previous + // altitude's clip. Re-roll from this altitude's pool so the video actually changes. + if (!poolIds(ring.scales[ringIndex]).includes(activeClipId)) { + activeClipId = pickPoolClip(ringIndex, activeClipId); + } delete vid.dataset.morph; // clear so re-entering the same segment reloads the morph (not the base) currentClipId = null; // force ensureClipMedia to reload + loop the locked clip // Land the loop on the frame the morph last showed for THIS travel direction: