@real-music-packages/web-core 0.39.3 → 0.39.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,7 +7,7 @@ import {
7
7
  notationLayout,
8
8
  vstackAudioPlayheadLine,
9
9
  vstackFollowBox
10
- } from "./chunk-BBWVFH7X.js";
10
+ } from "./chunk-SG7KION4.js";
11
11
  import {
12
12
  safeBox
13
13
  } from "./chunk-HXTRNE74.js";
@@ -275,4 +275,4 @@ export {
275
275
  notationFactory,
276
276
  scrollCursorFactory
277
277
  };
278
- //# sourceMappingURL=chunk-5XKPJUQY.js.map
278
+ //# sourceMappingURL=chunk-AJSFJCQW.js.map
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  hitTestMeasureAt,
3
3
  measureColumnsFromLayout
4
- } from "./chunk-BBWVFH7X.js";
4
+ } from "./chunk-SG7KION4.js";
5
5
 
6
6
  // src/notationCommon.ts
7
7
  function computeReflowScrollDelta(oldLayout, newLayout, anchorX, anchorY) {
@@ -37,6 +37,7 @@ function nowMs() {
37
37
  var SYSTEM_TOP_MARGIN_PX = 20;
38
38
  var FOLLOW_REARM_IDLE_MS = 5e3;
39
39
  function createFollowController() {
40
+ let enabled = true;
40
41
  let autoFollowSuspended = false;
41
42
  let programmaticStart = null;
42
43
  let programmaticTarget = null;
@@ -66,6 +67,7 @@ function createFollowController() {
66
67
  idleRearmTimer = setTimeout(() => {
67
68
  idleRearmTimer = null;
68
69
  autoFollowSuspended = false;
70
+ if (!enabled) return;
69
71
  const phRect = lastGetPlayheadRect?.();
70
72
  if (!phRect || typeof window === "undefined") return;
71
73
  const vh = window.innerHeight;
@@ -117,17 +119,20 @@ function createFollowController() {
117
119
  const prev = lastSystemIndex;
118
120
  lastSystemIndex = input.systemIndex;
119
121
  const changed = prev != null && prev >= 0 && input.systemIndex >= 0 && input.systemIndex !== prev;
120
- if (destroyed || !changed || autoFollowSuspended || followRafPending) return;
122
+ if (destroyed || !enabled || !changed || autoFollowSuspended || followRafPending) return;
121
123
  if (typeof window === "undefined" || typeof window.requestAnimationFrame !== "function") return;
122
124
  followRafPending = true;
123
125
  window.requestAnimationFrame(() => {
124
126
  followRafPending = false;
125
- if (destroyed || autoFollowSuspended) return;
127
+ if (destroyed || !enabled || autoFollowSuspended) return;
126
128
  const rect = lastGetSystemRect?.();
127
129
  if (!rect) return;
128
130
  scrollSystemToTop(rect);
129
131
  });
130
132
  },
133
+ setEnabled(on) {
134
+ enabled = on;
135
+ },
131
136
  destroy() {
132
137
  if (destroyed) return;
133
138
  destroyed = true;
@@ -144,4 +149,4 @@ export {
144
149
  hasReachedProgrammaticTarget,
145
150
  createFollowController
146
151
  };
147
- //# sourceMappingURL=chunk-NTTAAUH6.js.map
152
+ //# sourceMappingURL=chunk-PZJRRJZM.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/notationCommon.ts"],"sourcesContent":["// Shared internal logic for the SVG-family notation players\n// (notationPlayerSvg.ts + notationPlayerVerovio.ts, 0.39.0). EXTRACTED\n// verbatim (behavior-preserving refactor, not a rewrite) out of\n// notationPlayerSvg.ts, which was the sole owner of this logic through\n// 0.38.0 — see docs/superpowers/specs/2026-08-11-verovio-player-design.md\n// (web-core) §2: \"Shared logic (follow discriminator, scroll preservation)\n// is EXTRACTED from notationPlayerSvg.ts into a common internal module both\n// import — no copy-paste drift between the two SVG-family players.\"\n//\n// notationPlayerSvg.ts re-imports (and re-exports, for backward-compat\n// import paths) the pure pieces below; ITS OWN TEST SUITE IS THE PARITY\n// GATE for this extraction — those tests were not touched, so any behavior\n// drift introduced here would fail them.\n//\n// NOT exported from the root barrel or any package.json `exports` subpath —\n// this is an internal implementation detail of the two SVG-family players,\n// not public API in its own right.\n//\n// Two tiers:\n// - PURE math: computeReflowScrollDelta (zoom/resize position\n// preservation) + the auto-follow discriminator predicates\n// (isWithinProgrammaticScroll / hasReachedProgrammaticTarget). Moved\n// with zero logic changes from notationPlayerSvg.ts.\n// - A small STATEFUL controller (createFollowController) wrapping the\n// imperative wiring (window scroll listener, rAF-scheduled\n// scroll-into-view, seek-discontinuity re-arm) that both players\n// previously duplicated as private closures. Same control flow, same\n// window/rAF calls, just packaged so it isn't re-typed per player.\n\nimport { hitTestMeasureAt, measureColumnsFromLayout, type NotationLayout } from './scene/notationGeometry';\n\n// ─── Zoom/resize position preservation (pure, unit-tested) ────────────────\n\n/**\n * Pure reflow position-preservation math: given the OLD layout (before a\n * `setZoom`/`resize` re-engrave), the NEW layout (after it), and an anchor\n * point `(anchorX, anchorY)` in the OLD layout's coordinate space (typically\n * \"whatever content sat at the vertical center of the viewport\" — see the\n * call site), return the vertical delta (new px minus old px) to add to the\n * window's scroll position so that SAME content stays in the SAME on-screen\n * position across the reflow.\n *\n * Deliberately NOT playhead interpolation: it's a one-shot \"where did this\n * content move to\" lookup built from two existing, reused primitives —\n * `hitTestMeasureAt` (which measure box contains/is nearest the anchor) and\n * `measureColumnsFromLayout` (that same measure's box in each layout) — plus\n * a linear fraction-within-the-box preservation so an anchor near the bottom\n * of a tall measure box stays near the bottom after reflow, not snapped to\n * the box's top edge. Returns 0 when the anchor measure can't be resolved in\n * either layout (e.g. an empty layout) — a safe no-op scroll.\n *\n * Works identically for either player's `NotationLayout` — it only reads the\n * backend-agnostic shape (measure index/box/noteStartX), never anything\n * OSMD- or Verovio-specific.\n */\nexport function computeReflowScrollDelta(\n oldLayout: NotationLayout,\n newLayout: NotationLayout,\n anchorX: number,\n anchorY: number,\n): number {\n const idx = hitTestMeasureAt(oldLayout, anchorX, anchorY);\n if (idx == null) return 0;\n\n const oldIndices = [...new Set(oldLayout.measures.map((m) => m.index))].sort((a, b) => a - b);\n const newIndices = [...new Set(newLayout.measures.map((m) => m.index))].sort((a, b) => a - b);\n const oi = oldIndices.indexOf(idx);\n const ni = newIndices.indexOf(idx);\n if (oi < 0 || ni < 0) return 0;\n\n const oldCols = measureColumnsFromLayout(oldLayout.measures);\n const newCols = measureColumnsFromLayout(newLayout.measures);\n const oldBox = oldCols[oi];\n const newBox = newCols[ni];\n if (!oldBox || !newBox) return 0;\n\n const fracY = oldBox.h > 0 ? (anchorY - oldBox.y) / oldBox.h : 0;\n const newAnchorY = newBox.y + fracY * newBox.h;\n return newAnchorY - anchorY;\n}\n\n// ─── Auto-follow discriminator (design doc §4, moved verbatim) ────────────\n//\n// The canvas scroll-mode player (notationPlayer.ts's `createScrollPlayer`)\n// discriminates \"our own programmatic scroll\" from \"a manual user scroll\" via\n// a fixed 600ms grace window after every `scrollIntoView` call. That has a\n// real failure mode (flagged in the canvas module's own review): a\n// longer-than-600ms smooth-scroll animation's LATER frames land after the\n// window closes and get misread as manual, suspending auto-follow mid-\n// animation; conversely a genuinely fast manual scroll DURING the window can\n// slip through unsuspended.\n//\n// Fix: compare against the EXACT target position this component itself\n// requested, not elapsed time. We control the scroll call ourselves\n// (`window.scrollTo({top, behavior:'smooth'})`, not `element.scrollIntoView`,\n// specifically so we know the target precisely) — every intermediate frame of\n// OUR OWN smooth-scroll animation reports a `window.scrollY` somewhere on the\n// straight line between where we started and where we're headed; it can never\n// overshoot or reverse past that. A scroll event reporting a position OUTSIDE\n// that span can therefore only be a manual scroll, and is classified as such\n// immediately, independent of animation duration.\n\n/** Tolerance, px, for \"close enough\" to count as the same position (float/\n * sub-pixel scroll settling noise). */\nexport const PROGRAMMATIC_SCROLL_EPSILON_PX = 2;\n\n/**\n * True while `y` lies within `[min(start,target), max(start,target)]`\n * (± `epsilon`) — i.e. anywhere along the straight-line path of a\n * `window.scrollTo({top: target, behavior: 'smooth'})` call issued from\n * `start`. A scroll event reporting a position outside this span cannot be a\n * frame of that animation.\n */\nexport function isWithinProgrammaticScroll(\n y: number,\n start: number,\n target: number,\n epsilon: number = PROGRAMMATIC_SCROLL_EPSILON_PX,\n): boolean {\n const lo = Math.min(start, target) - epsilon;\n const hi = Math.max(start, target) + epsilon;\n return y >= lo && y <= hi;\n}\n\n/** True once `y` has settled at (within `epsilon` of) the programmatic\n * scroll's target — the animation is complete, so tracking can be cleared. */\nexport function hasReachedProgrammaticTarget(\n y: number,\n target: number,\n epsilon: number = PROGRAMMATIC_SCROLL_EPSILON_PX,\n): boolean {\n return Math.abs(y - target) <= epsilon;\n}\n\n// ─── Seek-discontinuity re-arm (control-flow bookkeeping, not geometry) ───\n\n/** Same re-arm rule as the canvas scroll player's discontinuity detector —\n * control-flow bookkeeping, copied verbatim: if the audio clock jumps by\n * more than this relative to wall-clock elapsed time, treat it as a seek\n * (not natural playback drift) and un-suspend auto-follow. */\nexport const SEEK_DISCONTINUITY_MS = 400;\n\nfunction nowMs(): number {\n return typeof performance !== 'undefined' ? performance.now() : Date.now();\n}\n\n// ─── Stateful follow controller ────────────────────────────────────────────\n//\n// UX (0.39.2, reader's-layout follow): the follow TARGET is no longer\n// \"keep the playhead somewhere in the middle band of the viewport\" — it's\n// \"when the playhead JUMPS to a new printed SYSTEM (a carriage return),\n// scroll so that system's own TOP edge lands just under the viewport top\" —\n// a reader's layout: the system that just started playing is the first\n// thing visible, upcoming systems fill the rest of the screen. See\n// `PlayheadLine.sys` (scene/notationGeometry.ts, populated by\n// `vstackAudioPlayheadLine`) for where the system index comes from.\n//\n// Manual scrolling still suspends auto-follow immediately (unchanged). It\n// now RE-ARMS `FOLLOW_REARM_IDLE_MS` after the LAST manual scroll event —\n// not immediately, and not via a fixed grace window on the NEXT scroll\n// event — a plain idle timer, reset on every manual scroll. Re-arming itself\n// does not scroll: the (less jarring) default is to wait for the next real\n// system jump. EXCEPTION: if the playhead has scrolled entirely out of view\n// by the time the idle timer fires, silently waiting could strand the\n// reader off the currently-playing system indefinitely (until whatever the\n// next jump happens to be) — so that one case scrolls back immediately, on\n// re-arm, rather than waiting.\n//\n// The existing seek/play re-arm (`onSetTime`'s audio-clock-discontinuity\n// detector) is UNCHANGED in effect (still un-suspends immediately, no\n// off-screen check — a seek is itself often a manual jump-and-look, so\n// forcing a scroll on top of it would be the jarring choice) but now ALSO\n// cancels any pending idle-rearm timer, so a stale timer can't fire a\n// surprise scroll well after a seek has already legitimately re-armed follow\n// through the other path.\n\n/** CSS px margin between the viewport top and the newly-playing system's own\n * top edge after a system-jump (\"carriage return\") auto-scroll — small\n * enough that the system's own top line is comfortably the first thing\n * visible, per the UX spec's \"~16-24px\". */\nexport const SYSTEM_TOP_MARGIN_PX = 20;\n\n/** How long after the LAST manual scroll event auto-follow re-arms. Reset on\n * every manual scroll event; a programmatic (our own) follow-scroll never\n * touches it (see `onWindowScroll` — it's only (re)scheduled from the\n * MANUAL-scroll branch). */\nexport const FOLLOW_REARM_IDLE_MS = 5_000;\n\n/** Minimal viewport-relative rect (subset of `DOMRect`) for the currently\n * playing system's own on-screen box. */\nexport interface FollowRect {\n top: number;\n bottom: number;\n}\n\nexport interface FollowInput {\n /** The printed-system (row) index the playhead is drawn in THIS frame —\n * `PlayheadLine.sys` from `vstackAudioPlayheadLine`. A negative value (the\n * \"no system geometry\" sentinel) is treated as \"unknown\" — the controller\n * never fires a system-jump scroll from it (matches the degenerate-layout\n * fallback already documented on `vstackAudioPlayheadLine`), though it is\n * still tracked so a later real index doesn't spuriously read as unchanged. */\n systemIndex: number;\n /** Lazy: the CURRENT system's on-screen rect (viewport-relative CSS px,\n * same space as `getBoundingClientRect()`). Read at animation-frame time\n * on a detected system-index change, and by the idle re-arm timer's\n * off-screen-immediate-scroll case — never unconditionally every frame. */\n getSystemRect: () => FollowRect | null;\n /** Lazy: the playhead's own on-screen rect. Used ONLY by the idle re-arm\n * timer, to decide \"is the playhead entirely out of view right now\" (the\n * off-screen re-arm exception) — the on-jump scroll target itself is\n * always `getSystemRect`, never this. */\n getPlayheadRect: () => DOMRect | null;\n}\n\nexport interface FollowController {\n /** Feed the current audio-clock time; detects seek/tempo discontinuities\n * (the clock jumping further than wall-clock time elapsed) and re-arms\n * auto-follow when one is seen, exactly as a manual seek should (and\n * cancels any pending idle-rearm timer — see the module doc). Call from\n * every `setTime`. */\n onSetTime(tMs: number): void;\n /** Call after the playhead has been positioned for this frame. On a\n * system-index CHANGE from the previous call (a carriage return) — and\n * only while auto-follow isn't currently suspended — schedules (at most\n * once per animation frame) ONE `window.scrollTo({..., behavior:\n * 'smooth'})` call targeting the new system's top edge\n * (`SYSTEM_TOP_MARGIN_PX` below the viewport top), and records its exact\n * target so the resulting scroll events are recognized as our own (via\n * `isWithinProgrammaticScroll`) rather than a manual scroll. No-ops on\n * the first call ever (no previous index to compare against), when the\n * index didn't change, when auto-follow is suspended, or outside a\n * browser (`window`/rAF unavailable). */\n follow(input: FollowInput): void;\n /** Remove the window scroll listener and cancel any pending idle-rearm\n * timer. Idempotent. */\n destroy(): void;\n}\n\n/** Build a fresh auto-follow controller. Wires a `window` scroll listener\n * immediately (no-ops outside a browser/jsdom-with-window environment). */\nexport function createFollowController(): FollowController {\n let autoFollowSuspended = false;\n let programmaticStart: number | null = null;\n let programmaticTarget: number | null = null;\n let followRafPending = false;\n let lastWallMs: number | null = null;\n let lastMusicMs = 0;\n let destroyed = false;\n\n // System-jump tracking — updated on EVERY `follow()` call regardless of\n // suspension, so (a) a jump that happens while suspended is never\n // mis-read as \"the next jump\" the instant follow re-arms, and (b) the\n // idle-rearm timer always has the freshest rect getters to check.\n let lastSystemIndex: number | null = null;\n let lastGetSystemRect: (() => FollowRect | null) | null = null;\n let lastGetPlayheadRect: (() => DOMRect | null) | null = null;\n let idleRearmTimer: ReturnType<typeof setTimeout> | null = null;\n\n function clearIdleRearmTimer(): void {\n if (idleRearmTimer != null) {\n clearTimeout(idleRearmTimer);\n idleRearmTimer = null;\n }\n }\n\n function scrollSystemToTop(rect: FollowRect): void {\n const start = window.scrollY;\n const target = Math.max(0, start + rect.top - SYSTEM_TOP_MARGIN_PX);\n programmaticStart = start;\n programmaticTarget = target;\n window.scrollTo({ top: target, left: window.scrollX, behavior: 'smooth' });\n }\n\n /** (Re)schedule the idle re-arm timer. Called ONLY from the MANUAL-scroll\n * branch of `onWindowScroll` — never for a frame of our own programmatic\n * scroll (that branch returns early above it) — so a programmatic\n * follow-scroll can never reset it, per the module doc. */\n function scheduleIdleRearm(): void {\n clearIdleRearmTimer();\n idleRearmTimer = setTimeout(() => {\n idleRearmTimer = null;\n autoFollowSuspended = false;\n // \"Pick the less jarring\": normally just un-suspend and wait for the\n // NEXT system jump. Exception — the playhead is ENTIRELY off-screen\n // right now, so silently waiting could strand the reader off the\n // currently-playing system for an arbitrarily long time; scroll back\n // immediately instead.\n const phRect = lastGetPlayheadRect?.();\n if (!phRect || typeof window === 'undefined') return;\n const vh = window.innerHeight;\n const entirelyOffscreen = phRect.bottom <= 0 || phRect.top >= vh;\n if (!entirelyOffscreen) return;\n const sysRect = lastGetSystemRect?.();\n if (sysRect) scrollSystemToTop(sysRect);\n }, FOLLOW_REARM_IDLE_MS);\n }\n\n function onWindowScroll(): void {\n if (programmaticTarget != null && programmaticStart != null) {\n const y = window.scrollY;\n if (isWithinProgrammaticScroll(y, programmaticStart, programmaticTarget)) {\n if (hasReachedProgrammaticTarget(y, programmaticTarget)) {\n programmaticStart = null;\n programmaticTarget = null;\n }\n return; // still (or just finished) our own scroll — don't suspend, don't touch the idle timer\n }\n }\n autoFollowSuspended = true;\n programmaticStart = null;\n programmaticTarget = null;\n scheduleIdleRearm();\n }\n\n const hasWindow = typeof window !== 'undefined' && typeof window.addEventListener === 'function';\n if (hasWindow) window.addEventListener('scroll', onWindowScroll, { passive: true });\n\n return {\n onSetTime(tMs: number): void {\n const now = nowMs();\n let reArmed = false;\n if (lastWallMs != null) {\n const dtMusic = tMs - lastMusicMs;\n const dtWall = now - lastWallMs;\n if (dtMusic < 0 || Math.abs(dtMusic - dtWall) > SEEK_DISCONTINUITY_MS) reArmed = true;\n } else {\n reArmed = true;\n }\n if (reArmed) {\n autoFollowSuspended = false;\n clearIdleRearmTimer(); // this path already re-armed — a stale idle timer must not also fire later\n }\n lastWallMs = now;\n lastMusicMs = tMs;\n },\n follow(input: FollowInput): void {\n lastGetSystemRect = input.getSystemRect;\n lastGetPlayheadRect = input.getPlayheadRect;\n const prev = lastSystemIndex;\n lastSystemIndex = input.systemIndex;\n const changed = prev != null && prev >= 0 && input.systemIndex >= 0 && input.systemIndex !== prev;\n if (destroyed || !changed || autoFollowSuspended || followRafPending) return;\n if (typeof window === 'undefined' || typeof window.requestAnimationFrame !== 'function') return;\n followRafPending = true;\n window.requestAnimationFrame(() => {\n followRafPending = false;\n if (destroyed || autoFollowSuspended) return;\n const rect = lastGetSystemRect?.();\n if (!rect) return;\n scrollSystemToTop(rect);\n });\n },\n destroy(): void {\n if (destroyed) return;\n destroyed = true;\n clearIdleRearmTimer();\n if (hasWindow) window.removeEventListener('scroll', onWindowScroll);\n },\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/notationCommon.ts"],"sourcesContent":["// Shared internal logic for the SVG-family notation players\n// (notationPlayerSvg.ts + notationPlayerVerovio.ts, 0.39.0). EXTRACTED\n// verbatim (behavior-preserving refactor, not a rewrite) out of\n// notationPlayerSvg.ts, which was the sole owner of this logic through\n// 0.38.0 — see docs/superpowers/specs/2026-08-11-verovio-player-design.md\n// (web-core) §2: \"Shared logic (follow discriminator, scroll preservation)\n// is EXTRACTED from notationPlayerSvg.ts into a common internal module both\n// import — no copy-paste drift between the two SVG-family players.\"\n//\n// notationPlayerSvg.ts re-imports (and re-exports, for backward-compat\n// import paths) the pure pieces below; ITS OWN TEST SUITE IS THE PARITY\n// GATE for this extraction — those tests were not touched, so any behavior\n// drift introduced here would fail them.\n//\n// NOT exported from the root barrel or any package.json `exports` subpath —\n// this is an internal implementation detail of the two SVG-family players,\n// not public API in its own right.\n//\n// Two tiers:\n// - PURE math: computeReflowScrollDelta (zoom/resize position\n// preservation) + the auto-follow discriminator predicates\n// (isWithinProgrammaticScroll / hasReachedProgrammaticTarget). Moved\n// with zero logic changes from notationPlayerSvg.ts.\n// - A small STATEFUL controller (createFollowController) wrapping the\n// imperative wiring (window scroll listener, rAF-scheduled\n// scroll-into-view, seek-discontinuity re-arm) that both players\n// previously duplicated as private closures. Same control flow, same\n// window/rAF calls, just packaged so it isn't re-typed per player.\n\nimport { hitTestMeasureAt, measureColumnsFromLayout, type NotationLayout } from './scene/notationGeometry';\n\n// ─── Zoom/resize position preservation (pure, unit-tested) ────────────────\n\n/**\n * Pure reflow position-preservation math: given the OLD layout (before a\n * `setZoom`/`resize` re-engrave), the NEW layout (after it), and an anchor\n * point `(anchorX, anchorY)` in the OLD layout's coordinate space (typically\n * \"whatever content sat at the vertical center of the viewport\" — see the\n * call site), return the vertical delta (new px minus old px) to add to the\n * window's scroll position so that SAME content stays in the SAME on-screen\n * position across the reflow.\n *\n * Deliberately NOT playhead interpolation: it's a one-shot \"where did this\n * content move to\" lookup built from two existing, reused primitives —\n * `hitTestMeasureAt` (which measure box contains/is nearest the anchor) and\n * `measureColumnsFromLayout` (that same measure's box in each layout) — plus\n * a linear fraction-within-the-box preservation so an anchor near the bottom\n * of a tall measure box stays near the bottom after reflow, not snapped to\n * the box's top edge. Returns 0 when the anchor measure can't be resolved in\n * either layout (e.g. an empty layout) — a safe no-op scroll.\n *\n * Works identically for either player's `NotationLayout` — it only reads the\n * backend-agnostic shape (measure index/box/noteStartX), never anything\n * OSMD- or Verovio-specific.\n */\nexport function computeReflowScrollDelta(\n oldLayout: NotationLayout,\n newLayout: NotationLayout,\n anchorX: number,\n anchorY: number,\n): number {\n const idx = hitTestMeasureAt(oldLayout, anchorX, anchorY);\n if (idx == null) return 0;\n\n const oldIndices = [...new Set(oldLayout.measures.map((m) => m.index))].sort((a, b) => a - b);\n const newIndices = [...new Set(newLayout.measures.map((m) => m.index))].sort((a, b) => a - b);\n const oi = oldIndices.indexOf(idx);\n const ni = newIndices.indexOf(idx);\n if (oi < 0 || ni < 0) return 0;\n\n const oldCols = measureColumnsFromLayout(oldLayout.measures);\n const newCols = measureColumnsFromLayout(newLayout.measures);\n const oldBox = oldCols[oi];\n const newBox = newCols[ni];\n if (!oldBox || !newBox) return 0;\n\n const fracY = oldBox.h > 0 ? (anchorY - oldBox.y) / oldBox.h : 0;\n const newAnchorY = newBox.y + fracY * newBox.h;\n return newAnchorY - anchorY;\n}\n\n// ─── Auto-follow discriminator (design doc §4, moved verbatim) ────────────\n//\n// The canvas scroll-mode player (notationPlayer.ts's `createScrollPlayer`)\n// discriminates \"our own programmatic scroll\" from \"a manual user scroll\" via\n// a fixed 600ms grace window after every `scrollIntoView` call. That has a\n// real failure mode (flagged in the canvas module's own review): a\n// longer-than-600ms smooth-scroll animation's LATER frames land after the\n// window closes and get misread as manual, suspending auto-follow mid-\n// animation; conversely a genuinely fast manual scroll DURING the window can\n// slip through unsuspended.\n//\n// Fix: compare against the EXACT target position this component itself\n// requested, not elapsed time. We control the scroll call ourselves\n// (`window.scrollTo({top, behavior:'smooth'})`, not `element.scrollIntoView`,\n// specifically so we know the target precisely) — every intermediate frame of\n// OUR OWN smooth-scroll animation reports a `window.scrollY` somewhere on the\n// straight line between where we started and where we're headed; it can never\n// overshoot or reverse past that. A scroll event reporting a position OUTSIDE\n// that span can therefore only be a manual scroll, and is classified as such\n// immediately, independent of animation duration.\n\n/** Tolerance, px, for \"close enough\" to count as the same position (float/\n * sub-pixel scroll settling noise). */\nexport const PROGRAMMATIC_SCROLL_EPSILON_PX = 2;\n\n/**\n * True while `y` lies within `[min(start,target), max(start,target)]`\n * (± `epsilon`) — i.e. anywhere along the straight-line path of a\n * `window.scrollTo({top: target, behavior: 'smooth'})` call issued from\n * `start`. A scroll event reporting a position outside this span cannot be a\n * frame of that animation.\n */\nexport function isWithinProgrammaticScroll(\n y: number,\n start: number,\n target: number,\n epsilon: number = PROGRAMMATIC_SCROLL_EPSILON_PX,\n): boolean {\n const lo = Math.min(start, target) - epsilon;\n const hi = Math.max(start, target) + epsilon;\n return y >= lo && y <= hi;\n}\n\n/** True once `y` has settled at (within `epsilon` of) the programmatic\n * scroll's target — the animation is complete, so tracking can be cleared. */\nexport function hasReachedProgrammaticTarget(\n y: number,\n target: number,\n epsilon: number = PROGRAMMATIC_SCROLL_EPSILON_PX,\n): boolean {\n return Math.abs(y - target) <= epsilon;\n}\n\n// ─── Seek-discontinuity re-arm (control-flow bookkeeping, not geometry) ───\n\n/** Same re-arm rule as the canvas scroll player's discontinuity detector —\n * control-flow bookkeeping, copied verbatim: if the audio clock jumps by\n * more than this relative to wall-clock elapsed time, treat it as a seek\n * (not natural playback drift) and un-suspend auto-follow. */\nexport const SEEK_DISCONTINUITY_MS = 400;\n\nfunction nowMs(): number {\n return typeof performance !== 'undefined' ? performance.now() : Date.now();\n}\n\n// ─── Stateful follow controller ────────────────────────────────────────────\n//\n// UX (0.39.2, reader's-layout follow): the follow TARGET is no longer\n// \"keep the playhead somewhere in the middle band of the viewport\" — it's\n// \"when the playhead JUMPS to a new printed SYSTEM (a carriage return),\n// scroll so that system's own TOP edge lands just under the viewport top\" —\n// a reader's layout: the system that just started playing is the first\n// thing visible, upcoming systems fill the rest of the screen. See\n// `PlayheadLine.sys` (scene/notationGeometry.ts, populated by\n// `vstackAudioPlayheadLine`) for where the system index comes from.\n//\n// Manual scrolling still suspends auto-follow immediately (unchanged). It\n// now RE-ARMS `FOLLOW_REARM_IDLE_MS` after the LAST manual scroll event —\n// not immediately, and not via a fixed grace window on the NEXT scroll\n// event — a plain idle timer, reset on every manual scroll. Re-arming itself\n// does not scroll: the (less jarring) default is to wait for the next real\n// system jump. EXCEPTION: if the playhead has scrolled entirely out of view\n// by the time the idle timer fires, silently waiting could strand the\n// reader off the currently-playing system indefinitely (until whatever the\n// next jump happens to be) — so that one case scrolls back immediately, on\n// re-arm, rather than waiting.\n//\n// The existing seek/play re-arm (`onSetTime`'s audio-clock-discontinuity\n// detector) is UNCHANGED in effect (still un-suspends immediately, no\n// off-screen check — a seek is itself often a manual jump-and-look, so\n// forcing a scroll on top of it would be the jarring choice) but now ALSO\n// cancels any pending idle-rearm timer, so a stale timer can't fire a\n// surprise scroll well after a seek has already legitimately re-armed follow\n// through the other path.\n\n/** CSS px margin between the viewport top and the newly-playing system's own\n * top edge after a system-jump (\"carriage return\") auto-scroll — small\n * enough that the system's own top line is comfortably the first thing\n * visible, per the UX spec's \"~16-24px\". */\nexport const SYSTEM_TOP_MARGIN_PX = 20;\n\n/** How long after the LAST manual scroll event auto-follow re-arms. Reset on\n * every manual scroll event; a programmatic (our own) follow-scroll never\n * touches it (see `onWindowScroll` — it's only (re)scheduled from the\n * MANUAL-scroll branch). */\nexport const FOLLOW_REARM_IDLE_MS = 5_000;\n\n/** Minimal viewport-relative rect (subset of `DOMRect`) for the currently\n * playing system's own on-screen box. */\nexport interface FollowRect {\n top: number;\n bottom: number;\n}\n\nexport interface FollowInput {\n /** The printed-system (row) index the playhead is drawn in THIS frame —\n * `PlayheadLine.sys` from `vstackAudioPlayheadLine`. A negative value (the\n * \"no system geometry\" sentinel) is treated as \"unknown\" — the controller\n * never fires a system-jump scroll from it (matches the degenerate-layout\n * fallback already documented on `vstackAudioPlayheadLine`), though it is\n * still tracked so a later real index doesn't spuriously read as unchanged. */\n systemIndex: number;\n /** Lazy: the CURRENT system's on-screen rect (viewport-relative CSS px,\n * same space as `getBoundingClientRect()`). Read at animation-frame time\n * on a detected system-index change, and by the idle re-arm timer's\n * off-screen-immediate-scroll case — never unconditionally every frame. */\n getSystemRect: () => FollowRect | null;\n /** Lazy: the playhead's own on-screen rect. Used ONLY by the idle re-arm\n * timer, to decide \"is the playhead entirely out of view right now\" (the\n * off-screen re-arm exception) — the on-jump scroll target itself is\n * always `getSystemRect`, never this. */\n getPlayheadRect: () => DOMRect | null;\n}\n\nexport interface FollowController {\n /** Feed the current audio-clock time; detects seek/tempo discontinuities\n * (the clock jumping further than wall-clock time elapsed) and re-arms\n * auto-follow when one is seen, exactly as a manual seek should (and\n * cancels any pending idle-rearm timer — see the module doc). Call from\n * every `setTime`. */\n onSetTime(tMs: number): void;\n /** Call after the playhead has been positioned for this frame. On a\n * system-index CHANGE from the previous call (a carriage return) — and\n * only while auto-follow isn't currently suspended — schedules (at most\n * once per animation frame) ONE `window.scrollTo({..., behavior:\n * 'smooth'})` call targeting the new system's top edge\n * (`SYSTEM_TOP_MARGIN_PX` below the viewport top), and records its exact\n * target so the resulting scroll events are recognized as our own (via\n * `isWithinProgrammaticScroll`) rather than a manual scroll. No-ops on\n * the first call ever (no previous index to compare against), when the\n * index didn't change, when auto-follow is suspended, or outside a\n * browser (`window`/rAF unavailable). */\n follow(input: FollowInput): void;\n /** Gate every scroll this controller can issue (0.39.4). The controller\n * itself has no concept of transport state — it just sees `setTime`s —\n * so the APP tells it when following is wanted (playing) vs not (paused/\n * stopped: the reader is studying the sheet; nothing may move it — the\n * user report behind this was the idle-rearm's off-screen rescue yanking\n * a PAUSED sheet back 5s after a manual scroll). While disabled,\n * system-jump tracking and the getters stay fresh (so re-enabling never\n * misreads a jump that happened while disabled) but `follow()` never\n * scrolls and the idle-rearm timer only lifts suspension, never\n * rescue-scrolls. Defaults to enabled (pre-0.39.4 behavior). */\n setEnabled(on: boolean): void;\n /** Remove the window scroll listener and cancel any pending idle-rearm\n * timer. Idempotent. */\n destroy(): void;\n}\n\n/** Build a fresh auto-follow controller. Wires a `window` scroll listener\n * immediately (no-ops outside a browser/jsdom-with-window environment). */\nexport function createFollowController(): FollowController {\n let enabled = true;\n let autoFollowSuspended = false;\n let programmaticStart: number | null = null;\n let programmaticTarget: number | null = null;\n let followRafPending = false;\n let lastWallMs: number | null = null;\n let lastMusicMs = 0;\n let destroyed = false;\n\n // System-jump tracking — updated on EVERY `follow()` call regardless of\n // suspension, so (a) a jump that happens while suspended is never\n // mis-read as \"the next jump\" the instant follow re-arms, and (b) the\n // idle-rearm timer always has the freshest rect getters to check.\n let lastSystemIndex: number | null = null;\n let lastGetSystemRect: (() => FollowRect | null) | null = null;\n let lastGetPlayheadRect: (() => DOMRect | null) | null = null;\n let idleRearmTimer: ReturnType<typeof setTimeout> | null = null;\n\n function clearIdleRearmTimer(): void {\n if (idleRearmTimer != null) {\n clearTimeout(idleRearmTimer);\n idleRearmTimer = null;\n }\n }\n\n function scrollSystemToTop(rect: FollowRect): void {\n const start = window.scrollY;\n const target = Math.max(0, start + rect.top - SYSTEM_TOP_MARGIN_PX);\n programmaticStart = start;\n programmaticTarget = target;\n window.scrollTo({ top: target, left: window.scrollX, behavior: 'smooth' });\n }\n\n /** (Re)schedule the idle re-arm timer. Called ONLY from the MANUAL-scroll\n * branch of `onWindowScroll` — never for a frame of our own programmatic\n * scroll (that branch returns early above it) — so a programmatic\n * follow-scroll can never reset it, per the module doc. */\n function scheduleIdleRearm(): void {\n clearIdleRearmTimer();\n idleRearmTimer = setTimeout(() => {\n idleRearmTimer = null;\n autoFollowSuspended = false;\n // \"Pick the less jarring\": normally just un-suspend and wait for the\n // NEXT system jump. Exception — the playhead is ENTIRELY off-screen\n // right now, so silently waiting could strand the reader off the\n // currently-playing system for an arbitrarily long time; scroll back\n // immediately instead. NEVER while disabled (paused/stopped — see\n // `setEnabled`): a parked sheet must stay exactly where the reader\n // put it.\n if (!enabled) return;\n const phRect = lastGetPlayheadRect?.();\n if (!phRect || typeof window === 'undefined') return;\n const vh = window.innerHeight;\n const entirelyOffscreen = phRect.bottom <= 0 || phRect.top >= vh;\n if (!entirelyOffscreen) return;\n const sysRect = lastGetSystemRect?.();\n if (sysRect) scrollSystemToTop(sysRect);\n }, FOLLOW_REARM_IDLE_MS);\n }\n\n function onWindowScroll(): void {\n if (programmaticTarget != null && programmaticStart != null) {\n const y = window.scrollY;\n if (isWithinProgrammaticScroll(y, programmaticStart, programmaticTarget)) {\n if (hasReachedProgrammaticTarget(y, programmaticTarget)) {\n programmaticStart = null;\n programmaticTarget = null;\n }\n return; // still (or just finished) our own scroll — don't suspend, don't touch the idle timer\n }\n }\n autoFollowSuspended = true;\n programmaticStart = null;\n programmaticTarget = null;\n scheduleIdleRearm();\n }\n\n const hasWindow = typeof window !== 'undefined' && typeof window.addEventListener === 'function';\n if (hasWindow) window.addEventListener('scroll', onWindowScroll, { passive: true });\n\n return {\n onSetTime(tMs: number): void {\n const now = nowMs();\n let reArmed = false;\n if (lastWallMs != null) {\n const dtMusic = tMs - lastMusicMs;\n const dtWall = now - lastWallMs;\n if (dtMusic < 0 || Math.abs(dtMusic - dtWall) > SEEK_DISCONTINUITY_MS) reArmed = true;\n } else {\n reArmed = true;\n }\n if (reArmed) {\n autoFollowSuspended = false;\n clearIdleRearmTimer(); // this path already re-armed — a stale idle timer must not also fire later\n }\n lastWallMs = now;\n lastMusicMs = tMs;\n },\n follow(input: FollowInput): void {\n lastGetSystemRect = input.getSystemRect;\n lastGetPlayheadRect = input.getPlayheadRect;\n const prev = lastSystemIndex;\n lastSystemIndex = input.systemIndex;\n const changed = prev != null && prev >= 0 && input.systemIndex >= 0 && input.systemIndex !== prev;\n if (destroyed || !enabled || !changed || autoFollowSuspended || followRafPending) return;\n if (typeof window === 'undefined' || typeof window.requestAnimationFrame !== 'function') return;\n followRafPending = true;\n window.requestAnimationFrame(() => {\n followRafPending = false;\n if (destroyed || !enabled || autoFollowSuspended) return;\n const rect = lastGetSystemRect?.();\n if (!rect) return;\n scrollSystemToTop(rect);\n });\n },\n setEnabled(on: boolean): void {\n enabled = on;\n },\n destroy(): void {\n if (destroyed) return;\n destroyed = true;\n clearIdleRearmTimer();\n if (hasWindow) window.removeEventListener('scroll', onWindowScroll);\n },\n };\n}\n"],"mappings":"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","names":[]}
@@ -431,6 +431,14 @@ function vstackAudioPlayheadLine(layout, onsetsMs, tMs, nBars, noteCols) {
431
431
  sys = b.sys;
432
432
  }
433
433
  }
434
+ if (systems.length && sys >= 0) {
435
+ const rowCols = cols.filter((_, i) => colSys[i] === sys);
436
+ if (rowCols.length) {
437
+ const envY = Math.min(...rowCols.map((c) => c.y));
438
+ rowH = Math.max(...rowCols.map((c) => c.y + c.h)) - envY;
439
+ rowY = envY;
440
+ }
441
+ }
434
442
  let y0 = rowY - padV;
435
443
  let y1 = rowY + rowH + padV;
436
444
  const bandY0 = layout.rect.dy;
@@ -478,4 +486,4 @@ export {
478
486
  audioPlayheadLine,
479
487
  vstackAudioPlayheadLine
480
488
  };
481
- //# sourceMappingURL=chunk-BBWVFH7X.js.map
489
+ //# sourceMappingURL=chunk-SG7KION4.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/scene/notationGeometry.ts"],"sourcesContent":["// Notation + scrolling-cursor geometry (S2) — the pure math extracted FAITHFULLY\n// from RSR's stave-web-sightread/src/routes/promo/+page.svelte. No canvas, no\n// behaviour change: these are the exact functions RSR uses to crop/scale the\n// engraving bitmap, scroll the 2-bar follow window, and place the playhead, lifted\n// verbatim so the notation + scroll-cursor layers can reuse them AND a test can\n// prove pixel-equivalence by comparing the geometry these produce against RSR's.\n//\n// Coordinate spaces (kept identical to RSR):\n// - canvas space: pixels of the rasterized OSMD bitmap (RenderedNotation.canvas).\n// - screen space: the W×H output frame. drawNotation maps a canvas `src` rect\n// onto a screen `dest` rect; cursor/highlight read the mapped boxes.\n//\n// IMPORTANT — easing: RSR uses a CUBIC ease-in-out for the zoom + scroll\n// (`cubicEaseInOut` below). web-core's math.easeInOut is SMOOTHSTEP — a different\n// curve. To stay pixel-faithful the extracted geometry uses RSR's cubic, NOT the\n// shared smoothstep. (Documented difference: none in output; the curve is ported.)\n\nimport type { Box, MeasureColumnBox, RenderedNotation, StaffMeasureBox } from '../promo';\nimport { safeBox } from '../video';\n\n// ─── Constants (verbatim from RSR +page.svelte:222-223) ──────────────────────\n\n/** measures (= rows) visible at once in the follow window. */\nexport const FOLLOW_BARS = 2;\n/** breathing room around the follow window. */\nexport const FOLLOW_PAD = 1.06;\n\n// ─── Easing (verbatim from RSR +page.svelte:175-179) ─────────────────────────\n\n/** Cubic ease-in-out, t in [0,1] (RSR port of stave-video core/camera.py). */\nexport function cubicEaseInOut(t: number): number {\n if (t < 0.5) return 4 * t * t * t;\n const f = 2 * t - 2;\n return 0.5 * f * f * f + 1;\n}\n\n// ─── Ledger-line vertical headroom ───────────────────────────────────────────\n//\n// BUG FIX (web-core 0.20.0): the follow/crop boxes below union OSMD's per-(measure,\n// staff) `box`, which is the STAFF's bounding box — it does NOT include ledger\n// lines, high/low notes, or stems that extend above/below the 5 staff lines. The\n// crop derived from those boxes therefore brutally clips ledger-line notes in BOTH\n// hstack and vstack. `rn.content` IS the true ink box (notes + ledgers + stems,\n// from inkBoundingBox), so we expand any measure-derived box VERTICALLY to:\n// (a) a generous fixed headroom of several staff-spaces above & below (covers\n// the common ±2-3 ledger-line case even when content can't bound it), and\n// (b) the full ink extent (rn.content.y .. content.y+content.h) — so nothing\n// engraved on the page is ever cropped away vertically.\n// Clamped to the canvas. Horizontal extent is untouched (the follow window's\n// purpose is to pick the visible MEASURES; their notes' x stay inside the staff x).\n\n/** ~one staff-space in canvas px, estimated from the median staff-measure height\n * (a single staff spans ~4 spaces, so height/4). Falls back to a sane default. */\nfunction staffSpacePx(rn: RenderedNotation): number {\n const hs = (rn.measures ?? []).map((m) => m.box.h).filter((h) => h > 1).sort((a, b) => a - b);\n if (!hs.length) return 14;\n const med = hs[Math.floor(hs.length / 2)];\n return Math.max(6, med / 4);\n}\n\n/**\n * Expand a measure-derived box VERTICALLY so it can never clip ledger-line notes /\n * stems above or below the staff. Adds `spaces` staff-spaces of headroom top &\n * bottom. When `unionInk` is set (hstack — a single row, so the ink box IS this\n * row), also unions with the canvas ink extent (rn.content) so every engraved mark\n * is visible. In vstack `unionInk` MUST be false: rn.content spans every stacked\n * system, so unioning it would balloon a per-system box to the whole page and\n * defeat the fixed-system framing — generous fixed headroom covers ledger lines\n * there instead. Horizontal extent unchanged. Result clamped to the canvas.\n */\nfunction withLedgerHeadroom(rn: RenderedNotation, box: Box, spaces: number, unionInk: boolean): Box {\n const pad = staffSpacePx(rn) * spaces;\n const ch = rn.canvas.height || box.y + box.h;\n let y0 = box.y - pad;\n let y1 = box.y + box.h + pad;\n if (unionInk) {\n const c = rn.content;\n if (c && c.h > 0) {\n y0 = Math.min(y0, c.y);\n y1 = Math.max(y1, c.y + c.h);\n }\n }\n y0 = Math.max(0, y0);\n y1 = Math.min(ch, y1);\n return { x: box.x, y: y0, w: box.w, h: Math.max(1, y1 - y0) };\n}\n\n// ─── Box helpers (verbatim from RSR) ─────────────────────────────────────────\n\n/** Linear interpolation of two boxes. (RSR lerpBox) */\nexport function lerpBox(a: Box, b: Box, e: number): Box {\n return {\n x: a.x + (b.x - a.x) * e,\n y: a.y + (b.y - a.y) * e,\n w: a.w + (b.w - a.w) * e,\n h: a.h + (b.h - a.h) * e,\n };\n}\n\n/** Smallest crop of `aspect` containing `box` padded by `pad`, clamped to the\n * canvas. (RSR cropAroundBox) */\nexport function cropAroundBox(box: Box, aspect: number, pad: number, cw: number, ch: number): Box {\n const bw = box.w * pad;\n const bh = box.h * pad;\n let w = Math.max(bw, bh * aspect);\n let h = w / aspect;\n w = Math.min(w, cw);\n h = Math.min(h, ch);\n const ccx = box.x + box.w / 2;\n const ccy = box.y + box.h / 2;\n let x = ccx - w / 2;\n let y = ccy - h / 2;\n x = Math.max(0, Math.min(cw - w, x));\n y = Math.max(0, Math.min(ch - h, y));\n return { x, y, w, h };\n}\n\n/** Union (canvas coords) of all staff measure boxes with index in [lo,hi).\n * (RSR measureSpanBox) */\nexport function measureSpanBox(rn: RenderedNotation, lo: number, hi: number): Box | null {\n const ms = (rn.measures ?? []).filter((m) => m.index >= lo && m.index < hi).map((m) => m.box);\n if (!ms.length) return null;\n const x0 = Math.min(...ms.map((b) => b.x));\n const y0 = Math.min(...ms.map((b) => b.y));\n const x1 = Math.max(...ms.map((b) => b.x + b.w));\n const y1 = Math.max(...ms.map((b) => b.y + b.h));\n // hstack: all measures share one row, so the ink box IS this row — union it so\n // ledger-line notes above/below the staff are never cropped.\n return withLedgerHeadroom(rn, { x: x0, y: y0, w: x1 - x0, h: y1 - y0 }, 6, true);\n}\n\n/** Union of the index-0 measure boxes — the opening focal. (RSR firstMeasureBox) */\nexport function firstMeasureBox(rn: RenderedNotation): Box | null {\n const first = (rn.measures ?? []).filter((m) => m.index === 0).map((m) => m.box);\n if (!first.length) return rn.systems?.[0] ?? null;\n const x0 = Math.min(...first.map((b) => b.x));\n const y0 = Math.min(...first.map((b) => b.y));\n const x1 = Math.max(...first.map((b) => b.x + b.w));\n const y1 = Math.max(...first.map((b) => b.y + b.h));\n return withLedgerHeadroom(rn, { x: x0, y: y0, w: x1 - x0, h: y1 - y0 }, 6, false);\n}\n\n/** Number of distinct measures in the notation. (RSR measureCount) */\nexport function measureCount(rn: RenderedNotation): number {\n const idx = (rn.measures ?? []).map((m) => m.index);\n return idx.length ? Math.max(...idx) + 1 : 0;\n}\n\n/** The DISTINCT measure indices present in the engraving, ASCENDING. For excerpts\n * these do NOT start at 0 (e.g. bars 5-8 → [4,5,6,7]) and may be non-contiguous,\n * so the follow window must scroll across THESE indices, not a 0..measureCount\n * range. (measureCount returns maxIndex+1 — a count valid only for 0-based scores.) */\nexport function distinctMeasureIndices(rn: RenderedNotation): number[] {\n const set = new Set<number>();\n for (const m of rn.measures ?? []) set.add(m.index);\n return [...set].sort((a, b) => a - b);\n}\n\n/** The follow window (canvas coords) for a continuous measure-start position,\n * spanning FOLLOW_BARS and lerping between adjacent windows. (RSR followBoxAt) */\nexport function followBoxAt(rn: RenderedNotation, posMeasures: number): Box | null {\n const cur = Math.floor(posMeasures);\n const frac = posMeasures - cur;\n const a = measureSpanBox(rn, cur, cur + FOLLOW_BARS);\n const b = measureSpanBox(rn, cur + 1, cur + 1 + FOLLOW_BARS) ?? a;\n if (!a) return b;\n if (!b) return a;\n return lerpBox(a, b, frac);\n}\n\n/**\n * The continuous follow-window START measure for an audio-clock progress 0..1.\n *\n * INVARIANT: the bar the playhead is in (`curBar`) is ALWAYS fully inside the\n * FOLLOW_BARS-wide window. We anchor `curBar` as the BOTTOM (last) visible bar —\n * with the preceding FOLLOW_BARS-1 bars above it for context — so the window\n * holds steady while the playhead works through the visible bars and only scrolls\n * once the playhead reaches the bottom bar. Concretely the resting window start is\n * `curBar - (FOLLOW_BARS - 1)`; over the tail of each bar we ease that forward by\n * one bar so the NEXT bar slides into the bottom slot just as the playhead crosses\n * into it. Because the eased start stays within `[curBar-(FOLLOW_BARS-1),\n * curBar-(FOLLOW_BARS-2)]`, `curBar` never leaves `[start, start+FOLLOW_BARS)` —\n * the current measure (and its playhead) can never ride off the top.\n *\n * The scroll is cubic-eased (smooth, no hard jump) over the last portion of each\n * bar; result is clamped to [0, nBars-FOLLOW_BARS]. (Replaces RSR's original\n * +page.svelte:738-744 logic, which advanced the window to the NEXT bar over the\n * last third of EVERY bar and so pushed the still-current bar off the top.)\n */\n/** Fraction of the FOLLOW_BARS window the playhead holds from the LEFT in hstack —\n * the rest is look-ahead. ~0.35 keeps the playing bar comfortably on screen and\n * off the edges. */\nexport const HSTACK_PLAYHEAD_LEAD = 0.35;\n\n/**\n * The continuous follow-window START measure (ABSOLUTE index) for an hstack\n * audio-clock progress 0..1.\n *\n * hstack (single horizontal staffline): pan CONTINUOUSLY so the playhead sits at a\n * stable fraction (`HSTACK_PLAYHEAD_LEAD`) from the LEFT of the window — the playing\n * bar is always on screen with look-ahead to its right, and the cursor never drifts\n * to the screen edge.\n *\n * BUG FIX (web-core 0.21.0) — playhead pinned at the right edge for EXCERPTS. This\n * used to take `nBars = measureCount(rn) = maxIndex+1` and pan progress across a\n * 0..nBars range. That is only correct when measure indices are 0-based: RSR's\n * excerpts engrave e.g. bars 5-8 → indices [4,5,6,7], so measureCount=8 but only 4\n * measures exist. The window then panned a phantom 0..8 axis while the playhead\n * (which steps over the 4 REAL columns) raced ahead — pinning the cursor at the far\n * right while the music barely scrolled. We now pan across the ACTUAL distinct\n * indices: progress 0..1 maps to absolute index `firstIndex .. firstIndex+nReal`,\n * and `followBoxAt` (which already indexes absolute measures via measureSpanBox)\n * frames the right bars. Clamped so the window never runs past the last measure.\n *\n * Pass the RenderedNotation so the real index range is known. (The legacy 0-based\n * `nBars`-only call still works via the `firstIndex=0, nReal=nBars` fallback.)\n */\nexport function followWindowStart(rn: RenderedNotation | number, camProgress01: number): number {\n let firstIndex: number;\n let nReal: number;\n if (typeof rn === 'number') {\n // Legacy signature: a plain bar count, assumed 0-based & contiguous.\n firstIndex = 0;\n nReal = rn;\n } else {\n const idx = distinctMeasureIndices(rn);\n firstIndex = idx.length ? idx[0] : 0;\n nReal = idx.length;\n }\n const p = Math.max(0, Math.min(1, camProgress01));\n // posBars in ABSOLUTE index space: 0 → firstIndex, 1 → firstIndex + nReal.\n const posBars = firstIndex + p * nReal;\n const start = posBars - HSTACK_PLAYHEAD_LEAD * FOLLOW_BARS;\n const lastIndex = firstIndex + Math.max(0, nReal - 1);\n // The window's leftmost start can be at most `lastIndex - (FOLLOW_BARS-1)` so the\n // final FOLLOW_BARS measures are still framed; never start before firstIndex.\n const minStart = firstIndex;\n const maxStart = Math.max(minStart, lastIndex - (FOLLOW_BARS - 1));\n return Math.max(minStart, Math.min(maxStart, start));\n}\n\n// ─── vstack follow geometry (fixed active system, vertical scroll) ───────────\n//\n// hstack (single horizontal staffline) lets `followBoxAt` pan a HORIZONTAL slice\n// — all measures share one row, so the FOLLOW_BARS window never changes y. vstack\n// re-introduces stacked systems (multiple rows), where `followBoxAt`'s measure\n// window WOULD jump vertically as consecutive bars land on different rows.\n//\n// The vstack camera avoids that by scrolling on the SYSTEM axis, not the measure\n// axis: it frames ONE system (a full row) at a fixed band position and, as the\n// playhead crosses from one system to the next, eases the focusBox VERTICALLY so\n// the next system slides up into the active slot. The playhead then moves L→R\n// within the framed system (its x comes from the bar's column inside that row).\n// The only horizontal reset is right→left WITHIN the same screen row when the\n// camera advances a system — never a leap over intervening staff lines.\n\n/** The system index a measure box belongs to (nearest by vertical center).\n * rn.systems are y-sorted row boxes; we pick the row whose center is closest. */\nfunction systemIndexOfBox(systems: Box[], box: Box): number {\n if (!systems.length) return 0;\n const cy = box.y + box.h / 2;\n let best = 0;\n let bestD = Infinity;\n for (let i = 0; i < systems.length; i++) {\n const s = systems[i];\n // Inside the system's vertical band → exact match.\n if (cy >= s.y && cy <= s.y + s.h) return i;\n const sc = s.y + s.h / 2;\n const d = Math.abs(cy - sc);\n if (d < bestD) { bestD = d; best = i; }\n }\n return best;\n}\n\n/** For each measure index, the system (row) it lives on — ordered by index.\n * Empty when geometry is missing. */\nexport function measureSystemMap(rn: RenderedNotation): number[] {\n const systems = (rn.systems ?? []);\n const n = measureCount(rn);\n const map = new Array<number>(n).fill(0);\n // For each measure index, use any staff's box to find its row.\n for (let idx = 0; idx < n; idx++) {\n const m = (rn.measures ?? []).find((mm) => mm.index === idx);\n map[idx] = m ? systemIndexOfBox(systems, m.box) : 0;\n }\n return map;\n}\n\n/** Union (canvas coords) of every measure box on system `sys`. Falls back to the\n * system's own box when no measures map to it. */\nexport function systemBox(rn: RenderedNotation, sys: number, sysMap?: number[]): Box | null {\n const map = sysMap ?? measureSystemMap(rn);\n const boxes = (rn.measures ?? [])\n .filter((m) => map[m.index] === sys)\n .map((m) => m.box);\n if (!boxes.length) return (rn.systems ?? [])[sys] ?? null;\n const x0 = Math.min(...boxes.map((b) => b.x));\n const y0 = Math.min(...boxes.map((b) => b.y));\n const x1 = Math.max(...boxes.map((b) => b.x + b.w));\n const y1 = Math.max(...boxes.map((b) => b.y + b.h));\n // vstack: per-system box — generous fixed headroom for ledger lines, but NO ink\n // union (rn.content spans every system; unioning would grab the whole page). 8\n // staff-spaces ≈ 3-4 ledger lines of clearance above & below, which covers the\n // overwhelming majority of real notation without zooming the system out much.\n return withLedgerHeadroom(rn, { x: x0, y: y0, w: x1 - x0, h: y1 - y0 }, 8, false);\n}\n\n/**\n * vstack follow window (canvas coords) for an audio-clock progress 0..1. Frames\n * the ACTIVE system (the row the playhead's current bar is on) and eases\n * VERTICALLY to the NEXT system over the tail of the active system's last bar, so\n * the next row slides up just as the playhead crosses into it. Within a system\n * the box is fixed (the playhead pans L→R inside it). No measure-window vertical\n * jumps: the focusBox always spans an integer-or-tween of FULL systems.\n *\n * The box keeps a constant size (the larger of the two framed systems' extents)\n * so the dest mapping doesn't breathe as it scrolls — the camera just translates.\n */\nexport function vstackFollowBox(rn: RenderedNotation, camProgress01: number): Box | null {\n const sysMap = measureSystemMap(rn);\n const nBars = measureCount(rn);\n const nSys = (rn.systems ?? []).length || (sysMap.length ? Math.max(...sysMap) + 1 : 0);\n if (nBars <= 0 || nSys <= 0) return null;\n\n const posBars = Math.max(0, Math.min(1, camProgress01)) * nBars;\n const curBar = Math.min(nBars - 1, Math.floor(posBars));\n const barFrac = posBars - Math.floor(posBars);\n const curSys = sysMap[curBar] ?? 0;\n\n // Is curBar the LAST bar of its system? If so, ease toward the next system over\n // the tail of this bar so the next row scrolls up as the playhead exits.\n const isLastBarOfSys =\n curBar + 1 >= nBars || (sysMap[curBar + 1] ?? curSys) !== curSys;\n const scrollFrac = isLastBarOfSys\n ? cubicEaseInOut(Math.min(1, Math.max(0, (barFrac - 0.66) / 0.34)))\n : 0;\n const nextSys = Math.min(nSys - 1, curSys + 1);\n\n const a = systemBox(rn, curSys, sysMap);\n const b = systemBox(rn, nextSys, sysMap) ?? a;\n if (!a) return b;\n if (!b) return a;\n\n // Constant-size box (max extent of the two systems) translated between them, so\n // the camera only scrolls (no zoom-breathing as rows differ slightly in size).\n const w = Math.max(a.w, b.w);\n const h = Math.max(a.h, b.h);\n const x = a.x + (b.x - a.x) * scrollFrac;\n const y = a.y + (b.y - a.y) * scrollFrac;\n return { x, y, w, h };\n}\n\n// ─── drawNotation geometry (verbatim from RSR +page.svelte:262-331) ──────────\n\n/** The dest rect a notation frame is drawn into, on screen. */\nexport interface NotationRect {\n dx: number;\n dy: number;\n dw: number;\n dh: number;\n}\n\n/** Geometry result of laying out the notation for one frame: which canvas `src`\n * rect is blitted to which screen `dest` rect, plus the mapped boxes. */\nexport interface NotationLayout {\n /** Canvas-space crop blitted this frame. */\n src: Box;\n /** Screen-space dest rect. */\n rect: NotationRect;\n /** Per-row system boxes mapped into screen coords (for the playhead fallback). */\n systems: Box[];\n /** Per-(measure,staff) boxes mapped into screen coords (for highlights/cursor). */\n measures: StaffMeasureBox[];\n}\n\nexport interface NotationLayoutOpts {\n /** 0 = zoomed on bar 1, 1 = full excerpt (opening camera). Default 1. */\n zoom01?: number;\n /** Follow window (canvas coords); when set, overrides zoom and scrolls. */\n focusBox?: Box | null;\n /**\n * Explicit horizontal fit box (screen px), overriding the default\n * `safeBox(W,H).centeredW`. `safeBox`'s left/right margins (`SAFE_ZONE` in\n * video.ts) reserve ~24% of width for TikTok/Reels-style caption/share-button\n * chrome — correct for the promo/video card this geometry was built for, but\n * wrong for a plain in-app player canvas with no such overlay, which wants\n * (up to) the FULL available width. Omit to keep the existing promo/video\n * framing unchanged (every caller before this option existed still gets\n * exactly `sb.centeredW`).\n */\n boxWidth?: number;\n}\n\n/**\n * Compute the notation layout (src crop + dest rect + mapped boxes) for one\n * frame. This is RSR's `drawNotation` with the single `ctx.drawImage` call REMOVED\n * — pure geometry, so it is testable headless and shared by the notation +\n * scroll-cursor layers. The layer does the drawImage using `src` + `rect`.\n */\nexport function notationLayout(\n rn: RenderedNotation,\n W: number,\n H: number,\n boxTop: number,\n boxH: number,\n opts: NotationLayoutOpts = {},\n): NotationLayout {\n const { zoom01 = 1, focusBox = null } = opts;\n const sb = safeBox(W, H);\n const maxW = opts.boxWidth ?? sb.centeredW;\n const c =\n rn.content && rn.content.w > 0 && rn.content.h > 0\n ? rn.content\n : { x: 0, y: 0, w: rn.canvas.width || 1400, h: rn.canvas.height || 300 };\n\n let src: Box;\n if (focusBox) {\n const px = (focusBox.w * (FOLLOW_PAD - 1)) / 2;\n const py = (focusBox.h * (FOLLOW_PAD - 1)) / 2;\n src = {\n x: Math.max(0, focusBox.x - px),\n y: Math.max(0, focusBox.y - py),\n w: focusBox.w + 2 * px,\n h: focusBox.h + 2 * py,\n };\n src.w = Math.min(src.w, rn.canvas.width - src.x);\n src.h = Math.min(src.h, rn.canvas.height - src.y);\n } else {\n src = c;\n const focal = firstMeasureBox(rn);\n if (zoom01 < 1 && focal) {\n const start = cropAroundBox(focal, c.w / c.h, 1.25, rn.canvas.width, rn.canvas.height);\n src = lerpBox(start, c, cubicEaseInOut(zoom01));\n }\n }\n\n const srcAspect = src.w / src.h;\n let dw = maxW;\n let dh = dw / srcAspect;\n if (dh > boxH) {\n dh = boxH;\n dw = dh * srcAspect;\n }\n const dx = (W - dw) / 2;\n const dy = boxTop + (boxH - dh) / 2;\n const fx = dw / src.w;\n const fy = dh / src.h;\n const map = (b: Box): Box => ({\n x: dx + (b.x - src.x) * fx,\n y: dy + (b.y - src.y) * fy,\n w: b.w * fx,\n h: b.h * fy,\n });\n const systems = (rn.systems ?? []).map(map);\n const measures = (rn.measures ?? []).map((m) => ({\n ...m,\n box: map(m.box),\n noteStartX: dx + (m.noteStartX - src.x) * fx,\n }));\n return { src, rect: { dx, dy, dw, dh }, systems, measures };\n}\n\n// ─── Playhead geometry (verbatim from RSR +page.svelte:446-518) ──────────────\n\n/** Per-measure grand-staff column boxes (both staves unioned) from a frame's\n * mapped measures, in render order. (RSR measureColumns) */\nexport function measureColumnsFromLayout(measures: StaffMeasureBox[]): MeasureColumnBox[] {\n const byIndex = new Map<number, MeasureColumnBox>();\n for (const m of measures) {\n const cur = byIndex.get(m.index);\n if (!cur) {\n byIndex.set(m.index, { ...m.box, noteStartX: m.noteStartX });\n } else {\n const x0 = Math.min(cur.x, m.box.x);\n const y0 = Math.min(cur.y, m.box.y);\n const x1 = Math.max(cur.x + cur.w, m.box.x + m.box.w);\n const y1 = Math.max(cur.y + cur.h, m.box.y + m.box.h);\n byIndex.set(m.index, {\n x: x0,\n y: y0,\n w: x1 - x0,\n h: y1 - y0,\n noteStartX: Math.min(cur.noteStartX, m.noteStartX),\n });\n }\n }\n return [...byIndex.keys()].sort((a, b) => a - b).map((k) => byIndex.get(k)!);\n}\n\n/**\n * Pure hit-test: which measure (by its engraved index) contains — or is\n * nearest to — point (mx, my) in a given followed layout. Reuses\n * `measureColumnsFromLayout` (the SAME per-measure union boxes the playhead\n * anchors to) for the boxes; only the index bookkeeping (matching each\n * returned box back to its measure index, which `measureColumnsFromLayout`\n * intentionally drops — the playhead has no use for it) is new here, and it\n * is pure array/index bookkeeping, not geometry math. Exported standalone so\n * it is unit-testable without a DOM/canvas.\n *\n * Lives here (not in notationPlayer.ts, its original home through web-core\n * 0.38.0) so BOTH the canvas player (notationPlayer.ts, which re-exports this\n * for backward compat — its public API is unchanged) and the SVG player\n * (notationPlayerSvg.ts) can import it without either one's bundle pulling in\n * the other's implementation. It has no canvas/raster-specific logic — it\n * only ever reads `NotationLayout`, the same backend-agnostic shape both\n * players produce — so this is its natural, shared home alongside\n * `measureColumnsFromLayout`/`vstackAudioPlayheadLine`, not a canvas-only\n * concept that happens to be reusable.\n *\n * COORDINATE SPACE — read this before calling directly: (mx, my) MUST be in\n * the same space `layout.measures[].box` is already mapped into by whichever\n * layout builder produced it — the canvas player's `notationLayout()` maps\n * into DEST/DEVICE-PIXEL space (i.e. the player's own `<canvas>` device\n * pixels, origin top-left, NOT CSS px — the same space its own\n * `onCanvasClick` computes via `(clientX - rect.left) * (canvas.width /\n * rect.width)`); the SVG player's `svgNotationLayout()` maps into real CSS px\n * (there is no separate raster/device-px space for an SVG backend). It is NOT\n * the canvas path's raster/src space (`RenderedNotation.canvas`, OSMD's own\n * pre-map bitmap px) — passing src-space coordinates here is exactly the\n * \"classic scale gotcha\" (`extractGeometry`'s `canvas.width / pageW` vs\n * `/(contentRight+contentLeft)`, see promo.ts's module doc) this component\n * exists to make impossible; don't reintroduce it at the call site.\n */\nexport function hitTestMeasureAt(layout: NotationLayout, mx: number, my: number): number | null {\n if (!layout.measures.length) return null;\n const indices = [...new Set(layout.measures.map((m) => m.index))].sort((a, b) => a - b);\n const cols = measureColumnsFromLayout(layout.measures);\n let nearest: number | null = null;\n let nearestD = Infinity;\n for (let i = 0; i < cols.length; i++) {\n const b = cols[i];\n if (mx >= b.x && mx <= b.x + b.w && my >= b.y && my <= b.y + b.h) return indices[i];\n const cx = b.x + b.w / 2;\n const cy = b.y + b.h / 2;\n const d = (mx - cx) * (mx - cx) + (my - cy) * (my - cy);\n if (d < nearestD) {\n nearestD = d;\n nearest = indices[i];\n }\n }\n return nearest;\n}\n\n/** The playhead line + alpha for progress `t01`. null when fully faded. (RSR\n * drawPlayhead, with the actual stroke factored out into the layer.) */\nexport interface PlayheadLine {\n x: number;\n y0: number;\n y1: number;\n alpha: number;\n /**\n * The printed SYSTEM (row) index this frame's line is drawn on — `-1`/\n * `undefined` when the producing function has no system geometry to\n * resolve one from (e.g. `audioPlayheadLine`, which never sets it — it has\n * no `layout.systems` concept — or `vstackAudioPlayheadLine` on a\n * degenerate layout with an empty `systems` array). Only\n * `vstackAudioPlayheadLine` populates this (web-core 0.39.2), specifically\n * so a caller can detect a \"carriage return\" (a `sys` CHANGE frame-to-frame)\n * without re-deriving row membership itself — see\n * `notationCommon.ts`'s `createFollowController`, the consumer this was\n * added for.\n */\n sys?: number;\n}\n\nexport function playheadLine(layout: NotationLayout, t01: number): PlayheadLine | null {\n const tt = Math.max(0, Math.min(1, t01));\n let alpha = 0.85;\n if (tt < 0.03) alpha *= tt / 0.03; // fade in\n if (tt > 0.94) alpha *= Math.max(0, (1 - tt) / 0.06); // fade out\n if (alpha <= 0.02) return null;\n\n const padV = 10;\n let x: number, y0: number, y1: number;\n const cols = measureColumnsFromLayout(layout.measures);\n if (cols.length) {\n const pos = tt * cols.length;\n const i = Math.min(cols.length - 1, Math.floor(pos));\n const m = cols[i];\n const startX = Math.min(m.noteStartX, m.x + m.w);\n x = startX + (pos - i) * (m.x + m.w - startX);\n y0 = m.y - padV;\n y1 = m.y + m.h + padV;\n } else if (layout.systems.length) {\n const pos = tt * layout.systems.length;\n const row = Math.min(layout.systems.length - 1, Math.floor(pos));\n const s = layout.systems[row];\n x = s.x + (pos - row) * s.w;\n y0 = s.y - padV;\n y1 = s.y + s.h + padV;\n } else {\n const r = layout.rect;\n x = r.dx + tt * r.dw;\n y0 = r.dy - padV;\n y1 = r.dy + r.dh + padV;\n }\n return { x, y0, y1, alpha };\n}\n\n// ─── Audio-driven playhead (FOOLPROOF default) ───────────────────────────────\n//\n// The foolproof cursor never paces a single 0..1 progress across the whole\n// engraving width (the linear `playheadLine` above — the whozart footgun). It\n// instead anchors EACH note to a geometry X (from the same followed layout the\n// notation blits) and steps the cursor BETWEEN those anchors keyed on the audio\n// clock `tMs` against the notes' own `onsetMs`. So:\n// - it lands on each note's X exactly when that note onsets,\n// - it only ever traverses notes whose onset has been reached — a long score\n// with a short audio window can only reach the notes that play, so it\n// CANNOT race the full width regardless of segment duration / score length,\n// - before the first note it holds on the first anchor (faded in); after the\n// last it holds on the last; rests/gaps are spanned by the same time-lerp\n// between the bracketing onsets (smooth hold-then-glide).\n//\n// Per-note anchor X: notes don't carry an engraved X (Score is parsed headless,\n// notation is rasterized separately), but BOTH derive from the same MusicXML in\n// the same order. We therefore spread the score's distinct onsets, IN ORDER,\n// across the measure-column note positions, and read each onset's X off the\n// followed layout's mapped columns. The anchor is a pure function of the note's\n// ORDINAL (not of t / musicMs), so the time axis stays strictly the onset clock\n// — that is what makes the race impossible.\n\nconst clamp01 = (x: number): number => (x < 0 ? 0 : x > 1 ? 1 : x);\n\n/** A note anchor on the engraving: its onset time and its mapped screen X/row. */\ninterface NoteAnchor {\n onsetMs: number;\n x: number;\n y0: number;\n y1: number;\n}\n\n/** Map a fractional column position `pos` (0..nCols) to an X + vertical row span\n * using the followed layout's measure columns. `pos` is clamped to the columns. */\nfunction anchorAtColumnPos(cols: MeasureColumnBox[], pos: number, padV: number): NoteAnchor {\n const i = Math.min(cols.length - 1, Math.max(0, Math.floor(pos)));\n const m = cols[i];\n const startX = Math.min(m.noteStartX, m.x + m.w);\n const frac = Math.min(1, Math.max(0, pos - i));\n return {\n onsetMs: 0,\n x: startX + frac * (m.x + m.w - startX),\n y0: m.y - padV,\n y1: m.y + m.h + padV,\n };\n}\n\n/**\n * Build per-note anchors from the followed layout + the played notes. One anchor\n * per DISTINCT onset (chords share an onset and thus one cursor X), ordered by\n * onset. The k-th of N distinct onsets sits at column position `k/(N-1) * nCols`,\n * so the cursor visits the engraving left→right exactly as the notes sound.\n */\nfunction noteAnchors(\n layout: NotationLayout,\n onsetsMs: number[],\n padV: number,\n barDurMs?: number,\n noteCols?: number[],\n): NoteAnchor[] {\n const cols = measureColumnsFromLayout(layout.measures);\n if (!cols.length || !onsetsMs.length) return [];\n const n = onsetsMs.length;\n const first = onsetsMs[0];\n // BEST: `noteCols[k]` is the note's REAL engraved column (measureIndex + frac\n // across the note region), so the cursor lands exactly on the notehead. Used\n // only when it aligns 1:1 with the onsets (same count). Else TIME-based\n // (onset/barDurMs — rhythmically right but linear-within-measure). Else the\n // legacy ORDINAL spread (k/(n-1)*nCols — drifts on real rhythm).\n const useEngraved = !!noteCols && noteCols.length === n;\n return onsetsMs.map((onsetMs, k) => {\n const pos = useEngraved\n ? noteCols![k]\n : barDurMs && barDurMs > 0\n ? (onsetMs - first) / barDurMs\n : (n === 1 ? 0 : k / (n - 1)) * cols.length;\n const a = anchorAtColumnPos(cols, pos, padV);\n return { ...a, onsetMs };\n });\n}\n\n/** Distinct, sorted note onsets from a Score's notes. */\nexport function distinctOnsets(notes: { onsetMs: number }[]): number[] {\n const set = new Set<number>();\n for (const n of notes) set.add(n.onsetMs);\n return [...set].sort((a, b) => a - b);\n}\n\n/**\n * The FOOLPROOF audio-driven playhead line for absolute audio time `tMs`.\n *\n * `tMs` is the audio clock; `onsetsMs` are the score's distinct note onsets\n * (sorted). The cursor is the time-lerp of the two anchors bracketing `tMs`:\n * before the first onset it holds on anchor 0; after the last it holds on the\n * last; in a gap it eases between the bracketing onsets. Pure function of\n * (layout, onsetsMs, tMs). Returns null only when there is no geometry to anchor\n * to (falls back to the rect sweep in the layer, same as `playheadLine`).\n *\n * Fade in/out mirrors `playheadLine` but is keyed on position in the ONSET span\n * (first→last onset), not on musicMs — so the fade tracks the notes too.\n */\nexport function audioPlayheadLine(\n layout: NotationLayout,\n onsetsMs: number[],\n tMs: number,\n barDurMs?: number,\n noteCols?: number[],\n): PlayheadLine | null {\n const padV = 10;\n const anchors = noteAnchors(layout, onsetsMs, padV, barDurMs, noteCols);\n if (!anchors.length) return null;\n\n const first = anchors[0].onsetMs;\n const last = anchors[anchors.length - 1].onsetMs;\n const span = last - first;\n\n // Locate the bracketing anchors for tMs and the inter-onset fraction.\n let a: NoteAnchor, b: NoteAnchor, frac: number;\n if (tMs <= first || anchors.length === 1) {\n a = b = anchors[0];\n frac = 0;\n } else if (tMs >= last) {\n a = b = anchors[anchors.length - 1];\n frac = 0;\n } else {\n // Find the last anchor with onset <= tMs (binary search; onsets are sorted).\n let lo = 0;\n let hi = anchors.length - 1;\n while (lo < hi) {\n const mid = (lo + hi + 1) >> 1;\n if (anchors[mid].onsetMs <= tMs) lo = mid;\n else hi = mid - 1;\n }\n a = anchors[lo];\n b = anchors[lo + 1];\n const dt = b.onsetMs - a.onsetMs;\n // LINEAR between onsets — the cursor must advance at the SAME constant rate the\n // notation scrolls (the follow camera, notesProgress, lerps linearly between\n // onsets). cubicEaseInOut here made the line lag in the first half of every\n // note gap then RUSH AHEAD in the second half — read as \"playhead moves faster\n // than the audio, getting worse every note\" on dense pieces. Both must use the\n // same interpolation so the line stays glued to its notehead as bars scroll.\n frac = dt > 0 ? (tMs - a.onsetMs) / dt : 0;\n }\n\n const x = a.x + (b.x - a.x) * frac;\n const y0 = a.y0 + (b.y0 - a.y0) * frac;\n const y1 = a.y1 + (b.y1 - a.y1) * frac;\n\n // Edge fades, time-based (NOT a fraction of the span — a fraction would make\n // the cursor vanish for seconds at the start/end of a long score, the very\n // case the foolproof cursor must handle). Fade in over the lead-up to the first\n // onset, fade out after the last; clamp the window so a short clip still fades.\n let alpha = 0.85;\n if (span > 0) {\n const fadeIn = Math.min(250, span * 0.5);\n const fadeOut = Math.min(400, span * 0.5);\n if (tMs < first + fadeIn) alpha *= clamp01((tMs - (first - fadeIn)) / (2 * fadeIn));\n if (tMs > last - fadeOut) alpha *= clamp01((last + fadeOut - tMs) / (2 * fadeOut));\n }\n if (alpha <= 0.02) return null;\n\n return { x, y0, y1, alpha };\n}\n\n// ─── vstack audio playhead (camera-synced, no vertical leap) ──────────────────\n//\n// BUG FIX (web-core 0.20.0) — whozart vstack vertical leap. The generic\n// `audioPlayheadLine` spreads onsets across ALL columns of the followed layout and\n// time-lerps between bracketing onsets. In vstack the followed layout frames ONE\n// system; a note on the NEXT system maps off-band, so lerping to it (or holding on\n// a note that the camera is scrolling off-screen) sent the cursor leaping across\n// staff lines. The whozart audio path (mode:'audio' + scrollMode:'vstack') hit\n// this every system boundary.\n//\n// This variant is PHASE-LOCKED to `vstackFollowBox`: it walks measure-columns by\n// the same onset progress that drives the camera, and at a system boundary it\n// eases the cursor from the last column of the active system to the FIRST column of\n// the next system over the SAME barFrac∈[0.66,1] scroll window the camera uses — so\n// the cursor rides DOWN with the incoming system and is never stranded off-band.\n// Within a system it tracks each note's column left→right on the onset clock\n// (foolproof: never races, lands on each note as it sounds). Fade matches\n// audioPlayheadLine (onset-span based).\nexport function vstackAudioPlayheadLine(\n layout: NotationLayout,\n onsetsMs: number[],\n tMs: number,\n nBars: number,\n noteCols?: number[],\n): PlayheadLine | null {\n const padV = 10;\n const cols = measureColumnsFromLayout(layout.measures);\n if (!cols.length || !onsetsMs.length || nBars <= 0) return null;\n const n = onsetsMs.length;\n const first = onsetsMs[0];\n const last = onsetsMs[n - 1];\n const span = last - first;\n\n const colX = (m: MeasureColumnBox, f: number): number => {\n const sx = Math.min(m.noteStartX, m.x + m.w);\n return sx + Math.min(1, Math.max(0, f)) * (m.x + m.w - sx);\n };\n\n // Row (= printed SYSTEM) membership per column, resolved from the layout's own\n // system geometry (`layout.systems` — the same per-row boxes ALL THREE\n // extractors, canvas raster / OSMD-SVG / Verovio, already populate) via\n // `systemIndexOfBox` (nearest system band by vertical center; exact match when\n // inside a band). `-1` sentinel when no system geometry is available at all —\n // see `sameRow` below for the fallback this forces.\n const systems = layout.systems ?? [];\n const colSys: number[] = systems.length ? cols.map((m) => systemIndexOfBox(systems, m)) : [];\n\n // Per-onset anchor: its engraved column X + the staff row it sits on. Uses the\n // real engraved noteCols when they pair 1:1 with the onsets (lands on the actual\n // notehead); else the legacy ordinal spread.\n const useEng = !!noteCols && noteCols.length === n;\n const anchorFor = (k: number) => {\n const pos = useEng ? noteCols![k] : (n === 1 ? 0 : k / (n - 1)) * cols.length;\n const i = Math.min(cols.length - 1, Math.max(0, Math.floor(pos)));\n const m = cols[i];\n return { x: colX(m, pos - i), rowY: m.y, rowH: m.h, sys: colSys.length ? colSys[i] : -1 };\n };\n\n // \"Same printed row\" (same engraved SYSTEM), NOT a fixed y-tolerance.\n //\n // BUG FIX (web-core 0.39.1): this used to be a fixed 8px absolute-y tolerance\n // (`Math.abs(ya - yb) < 8`). Verovio-engraved layouts have intra-system\n // measure-column height/y variance up to ~16px — ties, accidentals, register,\n // AND (the most COMMON trigger in practice) a staff RESTING for a beat: a rest\n // yields a shorter/shifted measure-column bounding box (no notehead/stem\n // extending it), just like a bare chord vs. a tall ledger-line note does (see\n // stave-web-sightread's `.superpowers/sdd/one-clock-report.md` Part 3, and a\n // real confirmed instance: `Chopin_NocturneCsharpPosth`'s harmony chord-layer\n // render has a genuine RH rest — `.superpowers/verovio-audit/svg/\n // reduction-Chopin_NocturneCsharpPosth-playable.svg`, measure id `t49ii2d`,\n // rest id `n-0-8-1` — whose measure-column sits ~14px off its \"iiø7\" chord\n // neighbor at a representative production canvas scale, same order of\n // magnitude as the report's 8–16px spurious band). This variance, whatever its\n // source, exceeds 8px and got misclassified as a system boundary, firing the\n // full sweep-right/re-enter-left carriage-return animation MID-ROW (29\n // spurious vs. 11 real boundary events measured over a 589-sample\n // written-view run) — the user-visible \"runs through the line and comes back\n // round from the left.\"\n //\n // Fix: classify row membership from the LAYOUT's own system structure\n // (`colSys`, above) instead of a y epsilon — two columns are the \"same row\" iff\n // they resolve to the same system index, however much their raw box y/h differs\n // within that system. This is IMMUNE BY CONSTRUCTION to the variance's cause\n // (ties/accidentals/register/rests/anything else): it never diffs two columns'\n // y/h against each other or against a fixed epsilon at all — each column is\n // independently placed by `systemIndexOfBox` against its nearest/containing\n // system BAND, so no amount of shape variance within a system can flip the\n // classification as long as the column's vertical center stays inside that\n // system's real engraved extent (which it always does — engraving never\n // straddles two systems). A data-derived epsilon (e.g. half the median\n // inter-row gap) would still just be a bigger constant tuned to the LAST\n // dataset measured; it could not make the same construction-level guarantee\n // against a not-yet-measured variance source. This needed no new field on\n // `MeasureColumnBox`/`StaffMeasureBox`: `NotationLayout.systems` (the real\n // per-row system boxes each extractor already knows from its own source — OSMD\n // `MusicSystems` / Verovio `g.system`) was already reaching this function\n // unchanged, and `systemIndexOfBox` (used above by `measureSystemMap`) already\n // existed to map a box to its nearest system band.\n //\n // Falls back to the legacy fixed-8px tolerance only when `layout.systems` is\n // empty — `verovioNotationLayout` can yield non-empty `measures` with no\n // `g.system` elements on malformed/degenerate SVG input (its `systems` array is\n // built independently of the `measures.length` guard) — so a degenerate layout\n // still degrades to the old (imperfect but pre-existing, not a regression)\n // behaviour instead of collapsing every column into \"row 0\".\n const sameRow = (a: { rowY: number; sys: number }, b: { rowY: number; sys: number }) =>\n systems.length ? a.sys === b.sys : Math.abs(a.rowY - b.rowY) < 8;\n const rowRightEdge = (a: { rowY: number; sys: number }) =>\n Math.max(\n ...cols\n .filter((c, i) => (colSys.length ? colSys[i] === a.sys : Math.abs(c.y - a.rowY) < 8))\n .map((c) => c.x + c.w),\n );\n const rowLeftEdge = (b: { rowY: number; sys: number }) =>\n Math.min(\n ...cols\n .filter((c, i) => (colSys.length ? colSys[i] === b.sys : Math.abs(c.y - b.rowY) < 8))\n .map((c) => Math.min(c.noteStartX, c.x)),\n );\n\n // Bracketing onsets + linear inter-onset fraction.\n let a: ReturnType<typeof anchorFor>, b: ReturnType<typeof anchorFor>, frac: number;\n if (tMs <= first || n === 1) { a = b = anchorFor(0); frac = 0; }\n else if (tMs >= last) { a = b = anchorFor(n - 1); frac = 0; }\n else {\n let lo = 0, hi = n - 1;\n while (lo < hi) { const mid = (lo + hi + 1) >> 1; if (onsetsMs[mid] <= tMs) lo = mid; else hi = mid - 1; }\n a = anchorFor(lo); b = anchorFor(lo + 1);\n frac = (tMs - onsetsMs[lo]) / (onsetsMs[lo + 1] - onsetsMs[lo]);\n }\n\n // `sys` — the CURRENT frame's system index (`PlayheadLine.sys`, 0.39.2):\n // whichever row `rowY`/`rowH` (below) actually land on this frame, i.e.\n // `a.sys` while still visually on the old row (same-row glide, or the\n // first half of a carriage-return sweep) and `b.sys` once the cursor has\n // re-entered on the new row (second half of the sweep). This is exactly\n // the frame where a caller's \"system CHANGED\" check should fire — the\n // moment the cursor visually appears on the new row from its left edge,\n // not the moment the bracketing onset pair first straddles a boundary.\n let x: number, rowY: number, rowH: number, sys: number;\n if (sameRow(a, b)) {\n // Same staff row → glide horizontally between the two noteheads.\n x = a.x + (b.x - a.x) * frac;\n rowY = a.rowY; rowH = a.rowH; sys = a.sys;\n } else {\n // System boundary → CARRIAGE RETURN, read like text: the first half of the gap\n // sweeps OUT to the current row's right edge (still on that row); the second\n // half comes IN from the next row's left edge to the next notehead (on the next\n // row). The bars stay stacked; the cursor wraps left→right, row to row.\n if (frac < 0.5) {\n const s = frac / 0.5;\n x = a.x + (rowRightEdge(a) - a.x) * s;\n rowY = a.rowY; rowH = a.rowH; sys = a.sys;\n } else {\n const s = (frac - 0.5) / 0.5;\n const nl = rowLeftEdge(b);\n x = nl + (b.x - nl) * s;\n rowY = b.rowY; rowH = b.rowH; sys = b.sys;\n }\n }\n let y0 = rowY - padV;\n let y1 = rowY + rowH + padV;\n\n // Clamp into the drawn band so the cursor can never ride off-screen.\n const bandY0 = layout.rect.dy;\n const bandY1 = layout.rect.dy + layout.rect.dh;\n const h = Math.max(1, y1 - y0);\n if (y0 < bandY0) { y0 = bandY0; y1 = bandY0 + h; }\n if (y1 > bandY1) { y1 = bandY1; y0 = bandY1 - h; }\n\n let alpha = 0.85;\n if (span > 0) {\n const fadeIn = Math.min(250, span * 0.5);\n const fadeOut = Math.min(400, span * 0.5);\n if (tMs < first + fadeIn) alpha *= clamp01((tMs - (first - fadeIn)) / (2 * fadeIn));\n if (tMs > last - fadeOut) alpha *= clamp01((last + fadeOut - tMs) / (2 * fadeOut));\n }\n if (alpha <= 0.02) return null;\n\n return { x, y0, y1, alpha, sys };\n}\n"],"mappings":"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","names":["a","b"]}
1
+ {"version":3,"sources":["../src/scene/notationGeometry.ts"],"sourcesContent":["// Notation + scrolling-cursor geometry (S2) — the pure math extracted FAITHFULLY\n// from RSR's stave-web-sightread/src/routes/promo/+page.svelte. No canvas, no\n// behaviour change: these are the exact functions RSR uses to crop/scale the\n// engraving bitmap, scroll the 2-bar follow window, and place the playhead, lifted\n// verbatim so the notation + scroll-cursor layers can reuse them AND a test can\n// prove pixel-equivalence by comparing the geometry these produce against RSR's.\n//\n// Coordinate spaces (kept identical to RSR):\n// - canvas space: pixels of the rasterized OSMD bitmap (RenderedNotation.canvas).\n// - screen space: the W×H output frame. drawNotation maps a canvas `src` rect\n// onto a screen `dest` rect; cursor/highlight read the mapped boxes.\n//\n// IMPORTANT — easing: RSR uses a CUBIC ease-in-out for the zoom + scroll\n// (`cubicEaseInOut` below). web-core's math.easeInOut is SMOOTHSTEP — a different\n// curve. To stay pixel-faithful the extracted geometry uses RSR's cubic, NOT the\n// shared smoothstep. (Documented difference: none in output; the curve is ported.)\n\nimport type { Box, MeasureColumnBox, RenderedNotation, StaffMeasureBox } from '../promo';\nimport { safeBox } from '../video';\n\n// ─── Constants (verbatim from RSR +page.svelte:222-223) ──────────────────────\n\n/** measures (= rows) visible at once in the follow window. */\nexport const FOLLOW_BARS = 2;\n/** breathing room around the follow window. */\nexport const FOLLOW_PAD = 1.06;\n\n// ─── Easing (verbatim from RSR +page.svelte:175-179) ─────────────────────────\n\n/** Cubic ease-in-out, t in [0,1] (RSR port of stave-video core/camera.py). */\nexport function cubicEaseInOut(t: number): number {\n if (t < 0.5) return 4 * t * t * t;\n const f = 2 * t - 2;\n return 0.5 * f * f * f + 1;\n}\n\n// ─── Ledger-line vertical headroom ───────────────────────────────────────────\n//\n// BUG FIX (web-core 0.20.0): the follow/crop boxes below union OSMD's per-(measure,\n// staff) `box`, which is the STAFF's bounding box — it does NOT include ledger\n// lines, high/low notes, or stems that extend above/below the 5 staff lines. The\n// crop derived from those boxes therefore brutally clips ledger-line notes in BOTH\n// hstack and vstack. `rn.content` IS the true ink box (notes + ledgers + stems,\n// from inkBoundingBox), so we expand any measure-derived box VERTICALLY to:\n// (a) a generous fixed headroom of several staff-spaces above & below (covers\n// the common ±2-3 ledger-line case even when content can't bound it), and\n// (b) the full ink extent (rn.content.y .. content.y+content.h) — so nothing\n// engraved on the page is ever cropped away vertically.\n// Clamped to the canvas. Horizontal extent is untouched (the follow window's\n// purpose is to pick the visible MEASURES; their notes' x stay inside the staff x).\n\n/** ~one staff-space in canvas px, estimated from the median staff-measure height\n * (a single staff spans ~4 spaces, so height/4). Falls back to a sane default. */\nfunction staffSpacePx(rn: RenderedNotation): number {\n const hs = (rn.measures ?? []).map((m) => m.box.h).filter((h) => h > 1).sort((a, b) => a - b);\n if (!hs.length) return 14;\n const med = hs[Math.floor(hs.length / 2)];\n return Math.max(6, med / 4);\n}\n\n/**\n * Expand a measure-derived box VERTICALLY so it can never clip ledger-line notes /\n * stems above or below the staff. Adds `spaces` staff-spaces of headroom top &\n * bottom. When `unionInk` is set (hstack — a single row, so the ink box IS this\n * row), also unions with the canvas ink extent (rn.content) so every engraved mark\n * is visible. In vstack `unionInk` MUST be false: rn.content spans every stacked\n * system, so unioning it would balloon a per-system box to the whole page and\n * defeat the fixed-system framing — generous fixed headroom covers ledger lines\n * there instead. Horizontal extent unchanged. Result clamped to the canvas.\n */\nfunction withLedgerHeadroom(rn: RenderedNotation, box: Box, spaces: number, unionInk: boolean): Box {\n const pad = staffSpacePx(rn) * spaces;\n const ch = rn.canvas.height || box.y + box.h;\n let y0 = box.y - pad;\n let y1 = box.y + box.h + pad;\n if (unionInk) {\n const c = rn.content;\n if (c && c.h > 0) {\n y0 = Math.min(y0, c.y);\n y1 = Math.max(y1, c.y + c.h);\n }\n }\n y0 = Math.max(0, y0);\n y1 = Math.min(ch, y1);\n return { x: box.x, y: y0, w: box.w, h: Math.max(1, y1 - y0) };\n}\n\n// ─── Box helpers (verbatim from RSR) ─────────────────────────────────────────\n\n/** Linear interpolation of two boxes. (RSR lerpBox) */\nexport function lerpBox(a: Box, b: Box, e: number): Box {\n return {\n x: a.x + (b.x - a.x) * e,\n y: a.y + (b.y - a.y) * e,\n w: a.w + (b.w - a.w) * e,\n h: a.h + (b.h - a.h) * e,\n };\n}\n\n/** Smallest crop of `aspect` containing `box` padded by `pad`, clamped to the\n * canvas. (RSR cropAroundBox) */\nexport function cropAroundBox(box: Box, aspect: number, pad: number, cw: number, ch: number): Box {\n const bw = box.w * pad;\n const bh = box.h * pad;\n let w = Math.max(bw, bh * aspect);\n let h = w / aspect;\n w = Math.min(w, cw);\n h = Math.min(h, ch);\n const ccx = box.x + box.w / 2;\n const ccy = box.y + box.h / 2;\n let x = ccx - w / 2;\n let y = ccy - h / 2;\n x = Math.max(0, Math.min(cw - w, x));\n y = Math.max(0, Math.min(ch - h, y));\n return { x, y, w, h };\n}\n\n/** Union (canvas coords) of all staff measure boxes with index in [lo,hi).\n * (RSR measureSpanBox) */\nexport function measureSpanBox(rn: RenderedNotation, lo: number, hi: number): Box | null {\n const ms = (rn.measures ?? []).filter((m) => m.index >= lo && m.index < hi).map((m) => m.box);\n if (!ms.length) return null;\n const x0 = Math.min(...ms.map((b) => b.x));\n const y0 = Math.min(...ms.map((b) => b.y));\n const x1 = Math.max(...ms.map((b) => b.x + b.w));\n const y1 = Math.max(...ms.map((b) => b.y + b.h));\n // hstack: all measures share one row, so the ink box IS this row — union it so\n // ledger-line notes above/below the staff are never cropped.\n return withLedgerHeadroom(rn, { x: x0, y: y0, w: x1 - x0, h: y1 - y0 }, 6, true);\n}\n\n/** Union of the index-0 measure boxes — the opening focal. (RSR firstMeasureBox) */\nexport function firstMeasureBox(rn: RenderedNotation): Box | null {\n const first = (rn.measures ?? []).filter((m) => m.index === 0).map((m) => m.box);\n if (!first.length) return rn.systems?.[0] ?? null;\n const x0 = Math.min(...first.map((b) => b.x));\n const y0 = Math.min(...first.map((b) => b.y));\n const x1 = Math.max(...first.map((b) => b.x + b.w));\n const y1 = Math.max(...first.map((b) => b.y + b.h));\n return withLedgerHeadroom(rn, { x: x0, y: y0, w: x1 - x0, h: y1 - y0 }, 6, false);\n}\n\n/** Number of distinct measures in the notation. (RSR measureCount) */\nexport function measureCount(rn: RenderedNotation): number {\n const idx = (rn.measures ?? []).map((m) => m.index);\n return idx.length ? Math.max(...idx) + 1 : 0;\n}\n\n/** The DISTINCT measure indices present in the engraving, ASCENDING. For excerpts\n * these do NOT start at 0 (e.g. bars 5-8 → [4,5,6,7]) and may be non-contiguous,\n * so the follow window must scroll across THESE indices, not a 0..measureCount\n * range. (measureCount returns maxIndex+1 — a count valid only for 0-based scores.) */\nexport function distinctMeasureIndices(rn: RenderedNotation): number[] {\n const set = new Set<number>();\n for (const m of rn.measures ?? []) set.add(m.index);\n return [...set].sort((a, b) => a - b);\n}\n\n/** The follow window (canvas coords) for a continuous measure-start position,\n * spanning FOLLOW_BARS and lerping between adjacent windows. (RSR followBoxAt) */\nexport function followBoxAt(rn: RenderedNotation, posMeasures: number): Box | null {\n const cur = Math.floor(posMeasures);\n const frac = posMeasures - cur;\n const a = measureSpanBox(rn, cur, cur + FOLLOW_BARS);\n const b = measureSpanBox(rn, cur + 1, cur + 1 + FOLLOW_BARS) ?? a;\n if (!a) return b;\n if (!b) return a;\n return lerpBox(a, b, frac);\n}\n\n/**\n * The continuous follow-window START measure for an audio-clock progress 0..1.\n *\n * INVARIANT: the bar the playhead is in (`curBar`) is ALWAYS fully inside the\n * FOLLOW_BARS-wide window. We anchor `curBar` as the BOTTOM (last) visible bar —\n * with the preceding FOLLOW_BARS-1 bars above it for context — so the window\n * holds steady while the playhead works through the visible bars and only scrolls\n * once the playhead reaches the bottom bar. Concretely the resting window start is\n * `curBar - (FOLLOW_BARS - 1)`; over the tail of each bar we ease that forward by\n * one bar so the NEXT bar slides into the bottom slot just as the playhead crosses\n * into it. Because the eased start stays within `[curBar-(FOLLOW_BARS-1),\n * curBar-(FOLLOW_BARS-2)]`, `curBar` never leaves `[start, start+FOLLOW_BARS)` —\n * the current measure (and its playhead) can never ride off the top.\n *\n * The scroll is cubic-eased (smooth, no hard jump) over the last portion of each\n * bar; result is clamped to [0, nBars-FOLLOW_BARS]. (Replaces RSR's original\n * +page.svelte:738-744 logic, which advanced the window to the NEXT bar over the\n * last third of EVERY bar and so pushed the still-current bar off the top.)\n */\n/** Fraction of the FOLLOW_BARS window the playhead holds from the LEFT in hstack —\n * the rest is look-ahead. ~0.35 keeps the playing bar comfortably on screen and\n * off the edges. */\nexport const HSTACK_PLAYHEAD_LEAD = 0.35;\n\n/**\n * The continuous follow-window START measure (ABSOLUTE index) for an hstack\n * audio-clock progress 0..1.\n *\n * hstack (single horizontal staffline): pan CONTINUOUSLY so the playhead sits at a\n * stable fraction (`HSTACK_PLAYHEAD_LEAD`) from the LEFT of the window — the playing\n * bar is always on screen with look-ahead to its right, and the cursor never drifts\n * to the screen edge.\n *\n * BUG FIX (web-core 0.21.0) — playhead pinned at the right edge for EXCERPTS. This\n * used to take `nBars = measureCount(rn) = maxIndex+1` and pan progress across a\n * 0..nBars range. That is only correct when measure indices are 0-based: RSR's\n * excerpts engrave e.g. bars 5-8 → indices [4,5,6,7], so measureCount=8 but only 4\n * measures exist. The window then panned a phantom 0..8 axis while the playhead\n * (which steps over the 4 REAL columns) raced ahead — pinning the cursor at the far\n * right while the music barely scrolled. We now pan across the ACTUAL distinct\n * indices: progress 0..1 maps to absolute index `firstIndex .. firstIndex+nReal`,\n * and `followBoxAt` (which already indexes absolute measures via measureSpanBox)\n * frames the right bars. Clamped so the window never runs past the last measure.\n *\n * Pass the RenderedNotation so the real index range is known. (The legacy 0-based\n * `nBars`-only call still works via the `firstIndex=0, nReal=nBars` fallback.)\n */\nexport function followWindowStart(rn: RenderedNotation | number, camProgress01: number): number {\n let firstIndex: number;\n let nReal: number;\n if (typeof rn === 'number') {\n // Legacy signature: a plain bar count, assumed 0-based & contiguous.\n firstIndex = 0;\n nReal = rn;\n } else {\n const idx = distinctMeasureIndices(rn);\n firstIndex = idx.length ? idx[0] : 0;\n nReal = idx.length;\n }\n const p = Math.max(0, Math.min(1, camProgress01));\n // posBars in ABSOLUTE index space: 0 → firstIndex, 1 → firstIndex + nReal.\n const posBars = firstIndex + p * nReal;\n const start = posBars - HSTACK_PLAYHEAD_LEAD * FOLLOW_BARS;\n const lastIndex = firstIndex + Math.max(0, nReal - 1);\n // The window's leftmost start can be at most `lastIndex - (FOLLOW_BARS-1)` so the\n // final FOLLOW_BARS measures are still framed; never start before firstIndex.\n const minStart = firstIndex;\n const maxStart = Math.max(minStart, lastIndex - (FOLLOW_BARS - 1));\n return Math.max(minStart, Math.min(maxStart, start));\n}\n\n// ─── vstack follow geometry (fixed active system, vertical scroll) ───────────\n//\n// hstack (single horizontal staffline) lets `followBoxAt` pan a HORIZONTAL slice\n// — all measures share one row, so the FOLLOW_BARS window never changes y. vstack\n// re-introduces stacked systems (multiple rows), where `followBoxAt`'s measure\n// window WOULD jump vertically as consecutive bars land on different rows.\n//\n// The vstack camera avoids that by scrolling on the SYSTEM axis, not the measure\n// axis: it frames ONE system (a full row) at a fixed band position and, as the\n// playhead crosses from one system to the next, eases the focusBox VERTICALLY so\n// the next system slides up into the active slot. The playhead then moves L→R\n// within the framed system (its x comes from the bar's column inside that row).\n// The only horizontal reset is right→left WITHIN the same screen row when the\n// camera advances a system — never a leap over intervening staff lines.\n\n/** The system index a measure box belongs to (nearest by vertical center).\n * rn.systems are y-sorted row boxes; we pick the row whose center is closest. */\nfunction systemIndexOfBox(systems: Box[], box: Box): number {\n if (!systems.length) return 0;\n const cy = box.y + box.h / 2;\n let best = 0;\n let bestD = Infinity;\n for (let i = 0; i < systems.length; i++) {\n const s = systems[i];\n // Inside the system's vertical band → exact match.\n if (cy >= s.y && cy <= s.y + s.h) return i;\n const sc = s.y + s.h / 2;\n const d = Math.abs(cy - sc);\n if (d < bestD) { bestD = d; best = i; }\n }\n return best;\n}\n\n/** For each measure index, the system (row) it lives on — ordered by index.\n * Empty when geometry is missing. */\nexport function measureSystemMap(rn: RenderedNotation): number[] {\n const systems = (rn.systems ?? []);\n const n = measureCount(rn);\n const map = new Array<number>(n).fill(0);\n // For each measure index, use any staff's box to find its row.\n for (let idx = 0; idx < n; idx++) {\n const m = (rn.measures ?? []).find((mm) => mm.index === idx);\n map[idx] = m ? systemIndexOfBox(systems, m.box) : 0;\n }\n return map;\n}\n\n/** Union (canvas coords) of every measure box on system `sys`. Falls back to the\n * system's own box when no measures map to it. */\nexport function systemBox(rn: RenderedNotation, sys: number, sysMap?: number[]): Box | null {\n const map = sysMap ?? measureSystemMap(rn);\n const boxes = (rn.measures ?? [])\n .filter((m) => map[m.index] === sys)\n .map((m) => m.box);\n if (!boxes.length) return (rn.systems ?? [])[sys] ?? null;\n const x0 = Math.min(...boxes.map((b) => b.x));\n const y0 = Math.min(...boxes.map((b) => b.y));\n const x1 = Math.max(...boxes.map((b) => b.x + b.w));\n const y1 = Math.max(...boxes.map((b) => b.y + b.h));\n // vstack: per-system box — generous fixed headroom for ledger lines, but NO ink\n // union (rn.content spans every system; unioning would grab the whole page). 8\n // staff-spaces ≈ 3-4 ledger lines of clearance above & below, which covers the\n // overwhelming majority of real notation without zooming the system out much.\n return withLedgerHeadroom(rn, { x: x0, y: y0, w: x1 - x0, h: y1 - y0 }, 8, false);\n}\n\n/**\n * vstack follow window (canvas coords) for an audio-clock progress 0..1. Frames\n * the ACTIVE system (the row the playhead's current bar is on) and eases\n * VERTICALLY to the NEXT system over the tail of the active system's last bar, so\n * the next row slides up just as the playhead crosses into it. Within a system\n * the box is fixed (the playhead pans L→R inside it). No measure-window vertical\n * jumps: the focusBox always spans an integer-or-tween of FULL systems.\n *\n * The box keeps a constant size (the larger of the two framed systems' extents)\n * so the dest mapping doesn't breathe as it scrolls — the camera just translates.\n */\nexport function vstackFollowBox(rn: RenderedNotation, camProgress01: number): Box | null {\n const sysMap = measureSystemMap(rn);\n const nBars = measureCount(rn);\n const nSys = (rn.systems ?? []).length || (sysMap.length ? Math.max(...sysMap) + 1 : 0);\n if (nBars <= 0 || nSys <= 0) return null;\n\n const posBars = Math.max(0, Math.min(1, camProgress01)) * nBars;\n const curBar = Math.min(nBars - 1, Math.floor(posBars));\n const barFrac = posBars - Math.floor(posBars);\n const curSys = sysMap[curBar] ?? 0;\n\n // Is curBar the LAST bar of its system? If so, ease toward the next system over\n // the tail of this bar so the next row scrolls up as the playhead exits.\n const isLastBarOfSys =\n curBar + 1 >= nBars || (sysMap[curBar + 1] ?? curSys) !== curSys;\n const scrollFrac = isLastBarOfSys\n ? cubicEaseInOut(Math.min(1, Math.max(0, (barFrac - 0.66) / 0.34)))\n : 0;\n const nextSys = Math.min(nSys - 1, curSys + 1);\n\n const a = systemBox(rn, curSys, sysMap);\n const b = systemBox(rn, nextSys, sysMap) ?? a;\n if (!a) return b;\n if (!b) return a;\n\n // Constant-size box (max extent of the two systems) translated between them, so\n // the camera only scrolls (no zoom-breathing as rows differ slightly in size).\n const w = Math.max(a.w, b.w);\n const h = Math.max(a.h, b.h);\n const x = a.x + (b.x - a.x) * scrollFrac;\n const y = a.y + (b.y - a.y) * scrollFrac;\n return { x, y, w, h };\n}\n\n// ─── drawNotation geometry (verbatim from RSR +page.svelte:262-331) ──────────\n\n/** The dest rect a notation frame is drawn into, on screen. */\nexport interface NotationRect {\n dx: number;\n dy: number;\n dw: number;\n dh: number;\n}\n\n/** Geometry result of laying out the notation for one frame: which canvas `src`\n * rect is blitted to which screen `dest` rect, plus the mapped boxes. */\nexport interface NotationLayout {\n /** Canvas-space crop blitted this frame. */\n src: Box;\n /** Screen-space dest rect. */\n rect: NotationRect;\n /** Per-row system boxes mapped into screen coords (for the playhead fallback). */\n systems: Box[];\n /** Per-(measure,staff) boxes mapped into screen coords (for highlights/cursor). */\n measures: StaffMeasureBox[];\n}\n\nexport interface NotationLayoutOpts {\n /** 0 = zoomed on bar 1, 1 = full excerpt (opening camera). Default 1. */\n zoom01?: number;\n /** Follow window (canvas coords); when set, overrides zoom and scrolls. */\n focusBox?: Box | null;\n /**\n * Explicit horizontal fit box (screen px), overriding the default\n * `safeBox(W,H).centeredW`. `safeBox`'s left/right margins (`SAFE_ZONE` in\n * video.ts) reserve ~24% of width for TikTok/Reels-style caption/share-button\n * chrome — correct for the promo/video card this geometry was built for, but\n * wrong for a plain in-app player canvas with no such overlay, which wants\n * (up to) the FULL available width. Omit to keep the existing promo/video\n * framing unchanged (every caller before this option existed still gets\n * exactly `sb.centeredW`).\n */\n boxWidth?: number;\n}\n\n/**\n * Compute the notation layout (src crop + dest rect + mapped boxes) for one\n * frame. This is RSR's `drawNotation` with the single `ctx.drawImage` call REMOVED\n * — pure geometry, so it is testable headless and shared by the notation +\n * scroll-cursor layers. The layer does the drawImage using `src` + `rect`.\n */\nexport function notationLayout(\n rn: RenderedNotation,\n W: number,\n H: number,\n boxTop: number,\n boxH: number,\n opts: NotationLayoutOpts = {},\n): NotationLayout {\n const { zoom01 = 1, focusBox = null } = opts;\n const sb = safeBox(W, H);\n const maxW = opts.boxWidth ?? sb.centeredW;\n const c =\n rn.content && rn.content.w > 0 && rn.content.h > 0\n ? rn.content\n : { x: 0, y: 0, w: rn.canvas.width || 1400, h: rn.canvas.height || 300 };\n\n let src: Box;\n if (focusBox) {\n const px = (focusBox.w * (FOLLOW_PAD - 1)) / 2;\n const py = (focusBox.h * (FOLLOW_PAD - 1)) / 2;\n src = {\n x: Math.max(0, focusBox.x - px),\n y: Math.max(0, focusBox.y - py),\n w: focusBox.w + 2 * px,\n h: focusBox.h + 2 * py,\n };\n src.w = Math.min(src.w, rn.canvas.width - src.x);\n src.h = Math.min(src.h, rn.canvas.height - src.y);\n } else {\n src = c;\n const focal = firstMeasureBox(rn);\n if (zoom01 < 1 && focal) {\n const start = cropAroundBox(focal, c.w / c.h, 1.25, rn.canvas.width, rn.canvas.height);\n src = lerpBox(start, c, cubicEaseInOut(zoom01));\n }\n }\n\n const srcAspect = src.w / src.h;\n let dw = maxW;\n let dh = dw / srcAspect;\n if (dh > boxH) {\n dh = boxH;\n dw = dh * srcAspect;\n }\n const dx = (W - dw) / 2;\n const dy = boxTop + (boxH - dh) / 2;\n const fx = dw / src.w;\n const fy = dh / src.h;\n const map = (b: Box): Box => ({\n x: dx + (b.x - src.x) * fx,\n y: dy + (b.y - src.y) * fy,\n w: b.w * fx,\n h: b.h * fy,\n });\n const systems = (rn.systems ?? []).map(map);\n const measures = (rn.measures ?? []).map((m) => ({\n ...m,\n box: map(m.box),\n noteStartX: dx + (m.noteStartX - src.x) * fx,\n }));\n return { src, rect: { dx, dy, dw, dh }, systems, measures };\n}\n\n// ─── Playhead geometry (verbatim from RSR +page.svelte:446-518) ──────────────\n\n/** Per-measure grand-staff column boxes (both staves unioned) from a frame's\n * mapped measures, in render order. (RSR measureColumns) */\nexport function measureColumnsFromLayout(measures: StaffMeasureBox[]): MeasureColumnBox[] {\n const byIndex = new Map<number, MeasureColumnBox>();\n for (const m of measures) {\n const cur = byIndex.get(m.index);\n if (!cur) {\n byIndex.set(m.index, { ...m.box, noteStartX: m.noteStartX });\n } else {\n const x0 = Math.min(cur.x, m.box.x);\n const y0 = Math.min(cur.y, m.box.y);\n const x1 = Math.max(cur.x + cur.w, m.box.x + m.box.w);\n const y1 = Math.max(cur.y + cur.h, m.box.y + m.box.h);\n byIndex.set(m.index, {\n x: x0,\n y: y0,\n w: x1 - x0,\n h: y1 - y0,\n noteStartX: Math.min(cur.noteStartX, m.noteStartX),\n });\n }\n }\n return [...byIndex.keys()].sort((a, b) => a - b).map((k) => byIndex.get(k)!);\n}\n\n/**\n * Pure hit-test: which measure (by its engraved index) contains — or is\n * nearest to — point (mx, my) in a given followed layout. Reuses\n * `measureColumnsFromLayout` (the SAME per-measure union boxes the playhead\n * anchors to) for the boxes; only the index bookkeeping (matching each\n * returned box back to its measure index, which `measureColumnsFromLayout`\n * intentionally drops — the playhead has no use for it) is new here, and it\n * is pure array/index bookkeeping, not geometry math. Exported standalone so\n * it is unit-testable without a DOM/canvas.\n *\n * Lives here (not in notationPlayer.ts, its original home through web-core\n * 0.38.0) so BOTH the canvas player (notationPlayer.ts, which re-exports this\n * for backward compat — its public API is unchanged) and the SVG player\n * (notationPlayerSvg.ts) can import it without either one's bundle pulling in\n * the other's implementation. It has no canvas/raster-specific logic — it\n * only ever reads `NotationLayout`, the same backend-agnostic shape both\n * players produce — so this is its natural, shared home alongside\n * `measureColumnsFromLayout`/`vstackAudioPlayheadLine`, not a canvas-only\n * concept that happens to be reusable.\n *\n * COORDINATE SPACE — read this before calling directly: (mx, my) MUST be in\n * the same space `layout.measures[].box` is already mapped into by whichever\n * layout builder produced it — the canvas player's `notationLayout()` maps\n * into DEST/DEVICE-PIXEL space (i.e. the player's own `<canvas>` device\n * pixels, origin top-left, NOT CSS px — the same space its own\n * `onCanvasClick` computes via `(clientX - rect.left) * (canvas.width /\n * rect.width)`); the SVG player's `svgNotationLayout()` maps into real CSS px\n * (there is no separate raster/device-px space for an SVG backend). It is NOT\n * the canvas path's raster/src space (`RenderedNotation.canvas`, OSMD's own\n * pre-map bitmap px) — passing src-space coordinates here is exactly the\n * \"classic scale gotcha\" (`extractGeometry`'s `canvas.width / pageW` vs\n * `/(contentRight+contentLeft)`, see promo.ts's module doc) this component\n * exists to make impossible; don't reintroduce it at the call site.\n */\nexport function hitTestMeasureAt(layout: NotationLayout, mx: number, my: number): number | null {\n if (!layout.measures.length) return null;\n const indices = [...new Set(layout.measures.map((m) => m.index))].sort((a, b) => a - b);\n const cols = measureColumnsFromLayout(layout.measures);\n let nearest: number | null = null;\n let nearestD = Infinity;\n for (let i = 0; i < cols.length; i++) {\n const b = cols[i];\n if (mx >= b.x && mx <= b.x + b.w && my >= b.y && my <= b.y + b.h) return indices[i];\n const cx = b.x + b.w / 2;\n const cy = b.y + b.h / 2;\n const d = (mx - cx) * (mx - cx) + (my - cy) * (my - cy);\n if (d < nearestD) {\n nearestD = d;\n nearest = indices[i];\n }\n }\n return nearest;\n}\n\n/** The playhead line + alpha for progress `t01`. null when fully faded. (RSR\n * drawPlayhead, with the actual stroke factored out into the layer.) */\nexport interface PlayheadLine {\n x: number;\n y0: number;\n y1: number;\n alpha: number;\n /**\n * The printed SYSTEM (row) index this frame's line is drawn on — `-1`/\n * `undefined` when the producing function has no system geometry to\n * resolve one from (e.g. `audioPlayheadLine`, which never sets it — it has\n * no `layout.systems` concept — or `vstackAudioPlayheadLine` on a\n * degenerate layout with an empty `systems` array). Only\n * `vstackAudioPlayheadLine` populates this (web-core 0.39.2), specifically\n * so a caller can detect a \"carriage return\" (a `sys` CHANGE frame-to-frame)\n * without re-deriving row membership itself — see\n * `notationCommon.ts`'s `createFollowController`, the consumer this was\n * added for.\n */\n sys?: number;\n}\n\nexport function playheadLine(layout: NotationLayout, t01: number): PlayheadLine | null {\n const tt = Math.max(0, Math.min(1, t01));\n let alpha = 0.85;\n if (tt < 0.03) alpha *= tt / 0.03; // fade in\n if (tt > 0.94) alpha *= Math.max(0, (1 - tt) / 0.06); // fade out\n if (alpha <= 0.02) return null;\n\n const padV = 10;\n let x: number, y0: number, y1: number;\n const cols = measureColumnsFromLayout(layout.measures);\n if (cols.length) {\n const pos = tt * cols.length;\n const i = Math.min(cols.length - 1, Math.floor(pos));\n const m = cols[i];\n const startX = Math.min(m.noteStartX, m.x + m.w);\n x = startX + (pos - i) * (m.x + m.w - startX);\n y0 = m.y - padV;\n y1 = m.y + m.h + padV;\n } else if (layout.systems.length) {\n const pos = tt * layout.systems.length;\n const row = Math.min(layout.systems.length - 1, Math.floor(pos));\n const s = layout.systems[row];\n x = s.x + (pos - row) * s.w;\n y0 = s.y - padV;\n y1 = s.y + s.h + padV;\n } else {\n const r = layout.rect;\n x = r.dx + tt * r.dw;\n y0 = r.dy - padV;\n y1 = r.dy + r.dh + padV;\n }\n return { x, y0, y1, alpha };\n}\n\n// ─── Audio-driven playhead (FOOLPROOF default) ───────────────────────────────\n//\n// The foolproof cursor never paces a single 0..1 progress across the whole\n// engraving width (the linear `playheadLine` above — the whozart footgun). It\n// instead anchors EACH note to a geometry X (from the same followed layout the\n// notation blits) and steps the cursor BETWEEN those anchors keyed on the audio\n// clock `tMs` against the notes' own `onsetMs`. So:\n// - it lands on each note's X exactly when that note onsets,\n// - it only ever traverses notes whose onset has been reached — a long score\n// with a short audio window can only reach the notes that play, so it\n// CANNOT race the full width regardless of segment duration / score length,\n// - before the first note it holds on the first anchor (faded in); after the\n// last it holds on the last; rests/gaps are spanned by the same time-lerp\n// between the bracketing onsets (smooth hold-then-glide).\n//\n// Per-note anchor X: notes don't carry an engraved X (Score is parsed headless,\n// notation is rasterized separately), but BOTH derive from the same MusicXML in\n// the same order. We therefore spread the score's distinct onsets, IN ORDER,\n// across the measure-column note positions, and read each onset's X off the\n// followed layout's mapped columns. The anchor is a pure function of the note's\n// ORDINAL (not of t / musicMs), so the time axis stays strictly the onset clock\n// — that is what makes the race impossible.\n\nconst clamp01 = (x: number): number => (x < 0 ? 0 : x > 1 ? 1 : x);\n\n/** A note anchor on the engraving: its onset time and its mapped screen X/row. */\ninterface NoteAnchor {\n onsetMs: number;\n x: number;\n y0: number;\n y1: number;\n}\n\n/** Map a fractional column position `pos` (0..nCols) to an X + vertical row span\n * using the followed layout's measure columns. `pos` is clamped to the columns. */\nfunction anchorAtColumnPos(cols: MeasureColumnBox[], pos: number, padV: number): NoteAnchor {\n const i = Math.min(cols.length - 1, Math.max(0, Math.floor(pos)));\n const m = cols[i];\n const startX = Math.min(m.noteStartX, m.x + m.w);\n const frac = Math.min(1, Math.max(0, pos - i));\n return {\n onsetMs: 0,\n x: startX + frac * (m.x + m.w - startX),\n y0: m.y - padV,\n y1: m.y + m.h + padV,\n };\n}\n\n/**\n * Build per-note anchors from the followed layout + the played notes. One anchor\n * per DISTINCT onset (chords share an onset and thus one cursor X), ordered by\n * onset. The k-th of N distinct onsets sits at column position `k/(N-1) * nCols`,\n * so the cursor visits the engraving left→right exactly as the notes sound.\n */\nfunction noteAnchors(\n layout: NotationLayout,\n onsetsMs: number[],\n padV: number,\n barDurMs?: number,\n noteCols?: number[],\n): NoteAnchor[] {\n const cols = measureColumnsFromLayout(layout.measures);\n if (!cols.length || !onsetsMs.length) return [];\n const n = onsetsMs.length;\n const first = onsetsMs[0];\n // BEST: `noteCols[k]` is the note's REAL engraved column (measureIndex + frac\n // across the note region), so the cursor lands exactly on the notehead. Used\n // only when it aligns 1:1 with the onsets (same count). Else TIME-based\n // (onset/barDurMs — rhythmically right but linear-within-measure). Else the\n // legacy ORDINAL spread (k/(n-1)*nCols — drifts on real rhythm).\n const useEngraved = !!noteCols && noteCols.length === n;\n return onsetsMs.map((onsetMs, k) => {\n const pos = useEngraved\n ? noteCols![k]\n : barDurMs && barDurMs > 0\n ? (onsetMs - first) / barDurMs\n : (n === 1 ? 0 : k / (n - 1)) * cols.length;\n const a = anchorAtColumnPos(cols, pos, padV);\n return { ...a, onsetMs };\n });\n}\n\n/** Distinct, sorted note onsets from a Score's notes. */\nexport function distinctOnsets(notes: { onsetMs: number }[]): number[] {\n const set = new Set<number>();\n for (const n of notes) set.add(n.onsetMs);\n return [...set].sort((a, b) => a - b);\n}\n\n/**\n * The FOOLPROOF audio-driven playhead line for absolute audio time `tMs`.\n *\n * `tMs` is the audio clock; `onsetsMs` are the score's distinct note onsets\n * (sorted). The cursor is the time-lerp of the two anchors bracketing `tMs`:\n * before the first onset it holds on anchor 0; after the last it holds on the\n * last; in a gap it eases between the bracketing onsets. Pure function of\n * (layout, onsetsMs, tMs). Returns null only when there is no geometry to anchor\n * to (falls back to the rect sweep in the layer, same as `playheadLine`).\n *\n * Fade in/out mirrors `playheadLine` but is keyed on position in the ONSET span\n * (first→last onset), not on musicMs — so the fade tracks the notes too.\n */\nexport function audioPlayheadLine(\n layout: NotationLayout,\n onsetsMs: number[],\n tMs: number,\n barDurMs?: number,\n noteCols?: number[],\n): PlayheadLine | null {\n const padV = 10;\n const anchors = noteAnchors(layout, onsetsMs, padV, barDurMs, noteCols);\n if (!anchors.length) return null;\n\n const first = anchors[0].onsetMs;\n const last = anchors[anchors.length - 1].onsetMs;\n const span = last - first;\n\n // Locate the bracketing anchors for tMs and the inter-onset fraction.\n let a: NoteAnchor, b: NoteAnchor, frac: number;\n if (tMs <= first || anchors.length === 1) {\n a = b = anchors[0];\n frac = 0;\n } else if (tMs >= last) {\n a = b = anchors[anchors.length - 1];\n frac = 0;\n } else {\n // Find the last anchor with onset <= tMs (binary search; onsets are sorted).\n let lo = 0;\n let hi = anchors.length - 1;\n while (lo < hi) {\n const mid = (lo + hi + 1) >> 1;\n if (anchors[mid].onsetMs <= tMs) lo = mid;\n else hi = mid - 1;\n }\n a = anchors[lo];\n b = anchors[lo + 1];\n const dt = b.onsetMs - a.onsetMs;\n // LINEAR between onsets — the cursor must advance at the SAME constant rate the\n // notation scrolls (the follow camera, notesProgress, lerps linearly between\n // onsets). cubicEaseInOut here made the line lag in the first half of every\n // note gap then RUSH AHEAD in the second half — read as \"playhead moves faster\n // than the audio, getting worse every note\" on dense pieces. Both must use the\n // same interpolation so the line stays glued to its notehead as bars scroll.\n frac = dt > 0 ? (tMs - a.onsetMs) / dt : 0;\n }\n\n const x = a.x + (b.x - a.x) * frac;\n const y0 = a.y0 + (b.y0 - a.y0) * frac;\n const y1 = a.y1 + (b.y1 - a.y1) * frac;\n\n // Edge fades, time-based (NOT a fraction of the span — a fraction would make\n // the cursor vanish for seconds at the start/end of a long score, the very\n // case the foolproof cursor must handle). Fade in over the lead-up to the first\n // onset, fade out after the last; clamp the window so a short clip still fades.\n let alpha = 0.85;\n if (span > 0) {\n const fadeIn = Math.min(250, span * 0.5);\n const fadeOut = Math.min(400, span * 0.5);\n if (tMs < first + fadeIn) alpha *= clamp01((tMs - (first - fadeIn)) / (2 * fadeIn));\n if (tMs > last - fadeOut) alpha *= clamp01((last + fadeOut - tMs) / (2 * fadeOut));\n }\n if (alpha <= 0.02) return null;\n\n return { x, y0, y1, alpha };\n}\n\n// ─── vstack audio playhead (camera-synced, no vertical leap) ──────────────────\n//\n// BUG FIX (web-core 0.20.0) — whozart vstack vertical leap. The generic\n// `audioPlayheadLine` spreads onsets across ALL columns of the followed layout and\n// time-lerps between bracketing onsets. In vstack the followed layout frames ONE\n// system; a note on the NEXT system maps off-band, so lerping to it (or holding on\n// a note that the camera is scrolling off-screen) sent the cursor leaping across\n// staff lines. The whozart audio path (mode:'audio' + scrollMode:'vstack') hit\n// this every system boundary.\n//\n// This variant is PHASE-LOCKED to `vstackFollowBox`: it walks measure-columns by\n// the same onset progress that drives the camera, and at a system boundary it\n// eases the cursor from the last column of the active system to the FIRST column of\n// the next system over the SAME barFrac∈[0.66,1] scroll window the camera uses — so\n// the cursor rides DOWN with the incoming system and is never stranded off-band.\n// Within a system it tracks each note's column left→right on the onset clock\n// (foolproof: never races, lands on each note as it sounds). Fade matches\n// audioPlayheadLine (onset-span based).\nexport function vstackAudioPlayheadLine(\n layout: NotationLayout,\n onsetsMs: number[],\n tMs: number,\n nBars: number,\n noteCols?: number[],\n): PlayheadLine | null {\n const padV = 10;\n const cols = measureColumnsFromLayout(layout.measures);\n if (!cols.length || !onsetsMs.length || nBars <= 0) return null;\n const n = onsetsMs.length;\n const first = onsetsMs[0];\n const last = onsetsMs[n - 1];\n const span = last - first;\n\n const colX = (m: MeasureColumnBox, f: number): number => {\n const sx = Math.min(m.noteStartX, m.x + m.w);\n return sx + Math.min(1, Math.max(0, f)) * (m.x + m.w - sx);\n };\n\n // Row (= printed SYSTEM) membership per column, resolved from the layout's own\n // system geometry (`layout.systems` — the same per-row boxes ALL THREE\n // extractors, canvas raster / OSMD-SVG / Verovio, already populate) via\n // `systemIndexOfBox` (nearest system band by vertical center; exact match when\n // inside a band). `-1` sentinel when no system geometry is available at all —\n // see `sameRow` below for the fallback this forces.\n const systems = layout.systems ?? [];\n const colSys: number[] = systems.length ? cols.map((m) => systemIndexOfBox(systems, m)) : [];\n\n // Per-onset anchor: its engraved column X + the staff row it sits on. Uses the\n // real engraved noteCols when they pair 1:1 with the onsets (lands on the actual\n // notehead); else the legacy ordinal spread.\n const useEng = !!noteCols && noteCols.length === n;\n const anchorFor = (k: number) => {\n const pos = useEng ? noteCols![k] : (n === 1 ? 0 : k / (n - 1)) * cols.length;\n const i = Math.min(cols.length - 1, Math.max(0, Math.floor(pos)));\n const m = cols[i];\n return { x: colX(m, pos - i), rowY: m.y, rowH: m.h, sys: colSys.length ? colSys[i] : -1 };\n };\n\n // \"Same printed row\" (same engraved SYSTEM), NOT a fixed y-tolerance.\n //\n // BUG FIX (web-core 0.39.1): this used to be a fixed 8px absolute-y tolerance\n // (`Math.abs(ya - yb) < 8`). Verovio-engraved layouts have intra-system\n // measure-column height/y variance up to ~16px — ties, accidentals, register,\n // AND (the most COMMON trigger in practice) a staff RESTING for a beat: a rest\n // yields a shorter/shifted measure-column bounding box (no notehead/stem\n // extending it), just like a bare chord vs. a tall ledger-line note does (see\n // stave-web-sightread's `.superpowers/sdd/one-clock-report.md` Part 3, and a\n // real confirmed instance: `Chopin_NocturneCsharpPosth`'s harmony chord-layer\n // render has a genuine RH rest — `.superpowers/verovio-audit/svg/\n // reduction-Chopin_NocturneCsharpPosth-playable.svg`, measure id `t49ii2d`,\n // rest id `n-0-8-1` — whose measure-column sits ~14px off its \"iiø7\" chord\n // neighbor at a representative production canvas scale, same order of\n // magnitude as the report's 8–16px spurious band). This variance, whatever its\n // source, exceeds 8px and got misclassified as a system boundary, firing the\n // full sweep-right/re-enter-left carriage-return animation MID-ROW (29\n // spurious vs. 11 real boundary events measured over a 589-sample\n // written-view run) — the user-visible \"runs through the line and comes back\n // round from the left.\"\n //\n // Fix: classify row membership from the LAYOUT's own system structure\n // (`colSys`, above) instead of a y epsilon — two columns are the \"same row\" iff\n // they resolve to the same system index, however much their raw box y/h differs\n // within that system. This is IMMUNE BY CONSTRUCTION to the variance's cause\n // (ties/accidentals/register/rests/anything else): it never diffs two columns'\n // y/h against each other or against a fixed epsilon at all — each column is\n // independently placed by `systemIndexOfBox` against its nearest/containing\n // system BAND, so no amount of shape variance within a system can flip the\n // classification as long as the column's vertical center stays inside that\n // system's real engraved extent (which it always does — engraving never\n // straddles two systems). A data-derived epsilon (e.g. half the median\n // inter-row gap) would still just be a bigger constant tuned to the LAST\n // dataset measured; it could not make the same construction-level guarantee\n // against a not-yet-measured variance source. This needed no new field on\n // `MeasureColumnBox`/`StaffMeasureBox`: `NotationLayout.systems` (the real\n // per-row system boxes each extractor already knows from its own source — OSMD\n // `MusicSystems` / Verovio `g.system`) was already reaching this function\n // unchanged, and `systemIndexOfBox` (used above by `measureSystemMap`) already\n // existed to map a box to its nearest system band.\n //\n // Falls back to the legacy fixed-8px tolerance only when `layout.systems` is\n // empty — `verovioNotationLayout` can yield non-empty `measures` with no\n // `g.system` elements on malformed/degenerate SVG input (its `systems` array is\n // built independently of the `measures.length` guard) — so a degenerate layout\n // still degrades to the old (imperfect but pre-existing, not a regression)\n // behaviour instead of collapsing every column into \"row 0\".\n const sameRow = (a: { rowY: number; sys: number }, b: { rowY: number; sys: number }) =>\n systems.length ? a.sys === b.sys : Math.abs(a.rowY - b.rowY) < 8;\n const rowRightEdge = (a: { rowY: number; sys: number }) =>\n Math.max(\n ...cols\n .filter((c, i) => (colSys.length ? colSys[i] === a.sys : Math.abs(c.y - a.rowY) < 8))\n .map((c) => c.x + c.w),\n );\n const rowLeftEdge = (b: { rowY: number; sys: number }) =>\n Math.min(\n ...cols\n .filter((c, i) => (colSys.length ? colSys[i] === b.sys : Math.abs(c.y - b.rowY) < 8))\n .map((c) => Math.min(c.noteStartX, c.x)),\n );\n\n // Bracketing onsets + linear inter-onset fraction.\n let a: ReturnType<typeof anchorFor>, b: ReturnType<typeof anchorFor>, frac: number;\n if (tMs <= first || n === 1) { a = b = anchorFor(0); frac = 0; }\n else if (tMs >= last) { a = b = anchorFor(n - 1); frac = 0; }\n else {\n let lo = 0, hi = n - 1;\n while (lo < hi) { const mid = (lo + hi + 1) >> 1; if (onsetsMs[mid] <= tMs) lo = mid; else hi = mid - 1; }\n a = anchorFor(lo); b = anchorFor(lo + 1);\n frac = (tMs - onsetsMs[lo]) / (onsetsMs[lo + 1] - onsetsMs[lo]);\n }\n\n // `sys` — the CURRENT frame's system index (`PlayheadLine.sys`, 0.39.2):\n // whichever row `rowY`/`rowH` (below) actually land on this frame, i.e.\n // `a.sys` while still visually on the old row (same-row glide, or the\n // first half of a carriage-return sweep) and `b.sys` once the cursor has\n // re-entered on the new row (second half of the sweep). This is exactly\n // the frame where a caller's \"system CHANGED\" check should fire — the\n // moment the cursor visually appears on the new row from its left edge,\n // not the moment the bracketing onset pair first straddles a boundary.\n let x: number, rowY: number, rowH: number, sys: number;\n if (sameRow(a, b)) {\n // Same staff row → glide horizontally between the two noteheads.\n x = a.x + (b.x - a.x) * frac;\n rowY = a.rowY; rowH = a.rowH; sys = a.sys;\n } else {\n // System boundary → CARRIAGE RETURN, read like text: the first half of the gap\n // sweeps OUT to the current row's right edge (still on that row); the second\n // half comes IN from the next row's left edge to the next notehead (on the next\n // row). The bars stay stacked; the cursor wraps left→right, row to row.\n if (frac < 0.5) {\n const s = frac / 0.5;\n x = a.x + (rowRightEdge(a) - a.x) * s;\n rowY = a.rowY; rowH = a.rowH; sys = a.sys;\n } else {\n const s = (frac - 0.5) / 0.5;\n const nl = rowLeftEdge(b);\n x = nl + (b.x - nl) * s;\n rowY = b.rowY; rowH = b.rowH; sys = b.sys;\n }\n }\n\n // Row ENVELOPE (0.39.4): the drawn height comes from the union of the\n // current system's column boxes, not the single current column's box.\n // Per-column boxes hug their own engraved content, so their y-extent\n // varies with register/rests/stems (the same 8–16px variance documented\n // at `sameRow` above) — and since the glide reads the LEFT anchor's box\n // per bracketing pair, the cursor's bottom end visibly bobbed at every\n // onset crossing (user report: \"sometimes bottoms out at the bass clef\n // bottom, sometimes goes higher\"). The 0.39.1 fix made row MEMBERSHIP\n // immune to that variance; this makes the drawn extent immune too:\n // constant within a system, changing only at real line breaks. The\n // per-column box remains the no-systems fallback (same degenerate-layout\n // rationale as `sameRow`'s own fallback).\n if (systems.length && sys >= 0) {\n const rowCols = cols.filter((_, i) => colSys[i] === sys);\n if (rowCols.length) {\n const envY = Math.min(...rowCols.map((c) => c.y));\n rowH = Math.max(...rowCols.map((c) => c.y + c.h)) - envY;\n rowY = envY;\n }\n }\n let y0 = rowY - padV;\n let y1 = rowY + rowH + padV;\n\n // Clamp into the drawn band so the cursor can never ride off-screen.\n const bandY0 = layout.rect.dy;\n const bandY1 = layout.rect.dy + layout.rect.dh;\n const h = Math.max(1, y1 - y0);\n if (y0 < bandY0) { y0 = bandY0; y1 = bandY0 + h; }\n if (y1 > bandY1) { y1 = bandY1; y0 = bandY1 - h; }\n\n let alpha = 0.85;\n if (span > 0) {\n const fadeIn = Math.min(250, span * 0.5);\n const fadeOut = Math.min(400, span * 0.5);\n if (tMs < first + fadeIn) alpha *= clamp01((tMs - (first - fadeIn)) / (2 * fadeIn));\n if (tMs > last - fadeOut) alpha *= clamp01((last + fadeOut - tMs) / (2 * fadeOut));\n }\n if (alpha <= 0.02) return null;\n\n return { x, y0, y1, alpha, sys };\n}\n"],"mappings":"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","names":["a","b"]}
@@ -2,7 +2,7 @@ import {
2
2
  getNotationEngraving,
3
3
  notationFactory,
4
4
  scrollCursorFactory
5
- } from "./chunk-5XKPJUQY.js";
5
+ } from "./chunk-AJSFJCQW.js";
6
6
  import {
7
7
  audioPlayheadLine,
8
8
  distinctOnsets,
@@ -10,7 +10,7 @@ import {
10
10
  measureCount,
11
11
  notationLayout,
12
12
  vstackAudioPlayheadLine
13
- } from "./chunk-BBWVFH7X.js";
13
+ } from "./chunk-SG7KION4.js";
14
14
  import {
15
15
  safeBox
16
16
  } from "./chunk-HXTRNE74.js";
@@ -88,6 +88,11 @@ interface SvgNotationPlayer {
88
88
  * same scroll-position preservation as `setZoom`. Call on host resize /
89
89
  * orientation change. */
90
90
  resize(): Promise<void>;
91
+ /** Gate the auto-follow scroll on the app's transport state: pass `true`
92
+ * on play, `false` on pause/stop (see FollowController.setEnabled — while
93
+ * disabled NOTHING may scroll the sheet, including the idle-rearm's
94
+ * off-screen rescue). Defaults to enabled. */
95
+ setFollowEnabled(on: boolean): void;
91
96
  /** Register a measure-click handler (measure index, matching
92
97
  * `ScoreNote.measure`/the engraved index). Returns an unsubscribe fn. */
93
98
  onMeasureClick(cb: (measureIndex: number) => void): () => void;
@@ -4,12 +4,12 @@ import {
4
4
  createFollowController,
5
5
  hasReachedProgrammaticTarget,
6
6
  isWithinProgrammaticScroll
7
- } from "./chunk-NTTAAUH6.js";
7
+ } from "./chunk-PZJRRJZM.js";
8
8
  import {
9
9
  distinctOnsets,
10
10
  hitTestMeasureAt,
11
11
  vstackAudioPlayheadLine
12
- } from "./chunk-BBWVFH7X.js";
12
+ } from "./chunk-SG7KION4.js";
13
13
  import "./chunk-HXTRNE74.js";
14
14
 
15
15
  // src/notationPlayerSvg.ts
@@ -206,6 +206,9 @@ function createSvgNotationPlayer(opts) {
206
206
  return {
207
207
  ready,
208
208
  setTime,
209
+ setFollowEnabled(on) {
210
+ follow.setEnabled(on);
211
+ },
209
212
  async setZoom(z) {
210
213
  await ready;
211
214
  await reflow(z);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/notationPlayerSvg.ts"],"sourcesContent":["// createSvgNotationPlayer — the SVG (vector) sibling of `createNotationPlayer`\n// (src/notationPlayer.ts). Same job (a live, caller-driven notation +\n// gliding-playhead widget), different medium: OSMD's native SVG backend\n// instead of a rasterized canvas. See\n// docs — stave-web-sightread's\n// docs/superpowers/specs/2026-08-11-svg-notation-player-design.md — for the\n// full design rationale (why: vectors don't blur on pinch/browser zoom, no\n// canvas-area cap, no raster tiling needed for full-score scroll).\n//\n// THE CANVAS MODULE IS NOT MODIFIED OR IMPORTED FOR ITS OSMD/RASTER PATH —\n// this is a parallel component. What IS reused, verbatim, no new math:\n// - `vstackAudioPlayheadLine` (scene/notationGeometry.ts) — the exact same\n// onset-anchored, carriage-return playhead interpolation the canvas path\n// uses. It operates purely on a `NotationLayout` (measure column boxes +\n// a vertical clamp band) — it does not care whether those boxes came from\n// a canvas raster or live SVG DOM geometry, so THIS module's job is only\n// to produce a `NotationLayout` in SVG/CSS px space (see\n// `svgNotationLayout` below) and everything downstream is identical.\n// - `hitTestMeasureAt` (scene/notationGeometry.ts, moved there in 0.38.0\n// specifically so this module never has a build-time edge into\n// notationPlayer.ts's canvas/raster implementation) — the exact same pure\n// point-in-measure-box hit-test, reused for click-to-seek.\n// - `distinctOnsets`, `measureColumnsFromLayout` — small pure helpers,\n// reused as-is.\n//\n// WHAT'S NEW (not a re-derivation of any of the above):\n// - `svgNotationLayout` — an SVG-backend geometry extractor, the SVG\n// counterpart to promo.ts's canvas-only `extractGeometry`. See its own\n// doc comment for the unit-conversion derivation.\n// - `computeReflowScrollDelta` — zoom/resize position-preservation math\n// (not playhead interpolation; a one-shot \"where did this same content\n// move to\" lookup using the existing hit-test + column helpers).\n// - `isWithinProgrammaticScroll` / `hasReachedProgrammaticTarget` — the\n// improved auto-follow discriminator (design doc §4): unlike the canvas\n// scroll-mode player's fixed 600ms \"programmatic scroll\" grace window,\n// this compares the ACTUAL scroll position against the EXACT target this\n// component itself requested, so classification is correct regardless of\n// how long a smooth-scroll animation takes (no grace-window race).\n//\n// IMPORT PATH: subpath-only — `@real-music-packages/web-core/notationPlayerSvg`\n// — not re-exported from the root barrel (same reasoning as notationPlayer.ts:\n// the root barrel is theory-only/zero-dependency).\n\nimport {\n vstackAudioPlayheadLine,\n distinctOnsets,\n hitTestMeasureAt,\n type NotationLayout,\n} from './scene/notationGeometry';\nimport type { Box, StaffMeasureBox } from './promo';\nimport {\n computeReflowScrollDelta,\n createFollowController,\n isWithinProgrammaticScroll,\n hasReachedProgrammaticTarget,\n PROGRAMMATIC_SCROLL_EPSILON_PX,\n} from './notationCommon';\n\n// Re-exported for backward compatibility — this module's own tests (and any\n// external importer) still pull these from `notationPlayerSvg`; the\n// implementations now live in `notationCommon.ts` (shared with\n// notationPlayerVerovio.ts, 0.39.0). See that module's doc for why.\nexport { computeReflowScrollDelta, isWithinProgrammaticScroll, hasReachedProgrammaticTarget, PROGRAMMATIC_SCROLL_EPSILON_PX };\n\nexport interface CreateSvgNotationPlayerOpts {\n /** Element the player's content is mounted into. Takes NATURAL content\n * height (the whole score, page-flow layout) — the host must not clip a\n * fixed height; the PAGE scrolls the score, there is no internal camera. */\n host: HTMLElement;\n /** MusicXML to engrave. Ignored when `rendered` (the test seam) is set. */\n musicXml: string;\n /** Distinct note onsets (ms) the playhead locks to — same contract as\n * `CreateNotationPlayerOpts.onsetsMs` in notationPlayer.ts. */\n onsetsMs: number[];\n /** Per-onset engraved column positions, 1:1 with the DEDUPED/sorted\n * `onsetsMs` — same semantics as the canvas player's `noteCols`. */\n noteCols?: number[];\n /** Playhead line color. Default `'#2f6f4f'`. */\n playheadColor?: string;\n /** Initial OSMD zoom (a pure post-layout visual scale — see\n * `svgNotationLayout`'s doc). Default 1 (OSMD's own default — the\n * engraving fits the host's own width at normal note size). */\n zoom?: number;\n /**\n * Advanced / test seam: a pre-built `NotationLayout`, bypassing the real\n * OSMD SVG engrave (`musicXml` is still required by the type but is\n * ignored when this is set). Mirrors `CreateNotationPlayerOpts.rendered` in\n * notationPlayer.ts for the identical reason: real OSMD *rendering* needs\n * actual browser canvas glyph metrics (for its line-breaking pass) even\n * when the SVG backend is selected — headless/jsdom can't fully provide\n * that — so this is how this module stays unit-testable in Node. Not\n * needed in a real browser host. When set, `setZoom`/`resize` update the\n * tracked zoom/width but perform no real re-engrave (there is nothing to\n * re-engrave).\n */\n rendered?: NotationLayout;\n}\n\nexport interface SvgNotationPlayer {\n /** Resolves once the engraving is in the DOM and ready to draw. `setTime`/\n * click hit-testing are safe to call before this resolves (no-op until\n * ready, same contract as the canvas player). */\n readonly ready: Promise<void>;\n /** Drive the playhead for absolute playback time `tMs`. Caller owns the\n * audio clock + rAF loop. */\n setTime(tMs: number): void;\n /** Re-engrave at a new OSMD zoom (systems reflow). The scroll position is\n * restored afterward so the content that was centered in the viewport\n * before the reflow is still centered after it. */\n setZoom(z: number): Promise<void>;\n /** Re-measure the host and reflow to match its current width, with the\n * same scroll-position preservation as `setZoom`. Call on host resize /\n * orientation change. */\n resize(): Promise<void>;\n /** Register a measure-click handler (measure index, matching\n * `ScoreNote.measure`/the engraved index). Returns an unsubscribe fn. */\n onMeasureClick(cb: (measureIndex: number) => void): () => void;\n /** Tear down: removes the mounted DOM (engraving + playhead overlay) from\n * `host`, and drops every listener this instance added (click, window\n * scroll) and pending async work (a token guard drops any in-flight\n * reflow's effects). Idempotent. */\n destroy(): void;\n}\n\n// ─── svgNotationLayout — the SVG-backend geometry extractor ───────────────\n//\n// UNIT CONVERSION — derived, not guessed (verified against the vendored\n// opensheetmusicdisplay + vexflow source, not just its .d.ts comments, which\n// are stale here — see below):\n//\n// 1. `GraphicalMusicSheet` (`osmd.GraphicSheet`) positions\n// (`PositionAndShape.AbsolutePosition`/`.Size`) are in backend-agnostic\n// \"OSMD units\" — the SAME object model promo.ts's canvas-only\n// `extractGeometry` reads (`osmd.GraphicSheet.MusicPages[0].MusicSystems`,\n// `.MeasureList`). Whichever backend (canvas/svg) was requested, this\n// layer is identical.\n// 2. OSMD's Vexflow draw layer (`VexFlowMusicSheetDrawer`) converts an OSMD\n// unit value to a \"raw\" Vexflow/SVG px value via an EXPORTED constant,\n// `unitInPixels` (currently 10 — NOT `EngravingRules.unit`, which is a\n// different, unrelated field that is NOT the px-per-unit factor despite\n// what its .d.ts comment implies; confirmed by reading the actual\n// compiled source, not trusting the stale doc comment). This raw value\n// is written directly into each SVG element's own coordinate attributes\n// — it does NOT yet include `zoom`.\n// 3. `osmd.Zoom` (current zoom) is applied SEPARATELY, at the SVG-backend\n// level, via a `viewBox` trick (`vexflow/src/svgcontext.js`'s\n// `scale(x,y)`): the `<svg>` element's `width`/`height` ATTRIBUTES are\n// set to the FINAL (zoomed) CSS px size, while its `viewBox` spans\n// `width/zoom .. height/zoom` — i.e. the RAW (unzoomed, step-2) content\n// coordinates. The browser therefore maps that raw coordinate space onto\n// the final CSS px box by exactly `zoom`.\n// Net result: `cssPx = unitValue * unitInPixels * zoom`. Both factors are\n// read from the live library/instance (never a hardcoded literal `10` or an\n// assumed zoom) — see `SvgNotationLayoutOpts.unitInPixels`'s doc — so a\n// future OSMD version changing either is picked up automatically, which is\n// exactly the \"10 units/staff-space assumptions have version drift\" risk\n// the design doc calls out.\n//\n// This mirrors, in spirit, promo.ts's canvas `extractGeometry` self-\n// calibration (`canvas.width / (contentRight+contentLeft)`, chosen there\n// specifically because a naive formula broke on overflowed layouts) — the SVG\n// backend doesn't have that raster-overflow failure mode (there is no\n// backing-store to overflow; the viewBox IS the content, always), so the\n// derived formula above is exact, not an approximation.\n\nexport interface SvgNotationLayoutOpts {\n /** CSS px per OSMD unit at zoom 1 — OSMD's own exported `unitInPixels`\n * constant (see the derivation above). REQUIRED, no default: the point is\n * to FORCE the real call site to source this from the live\n * `opensheetmusicdisplay` import\n * (`const { unitInPixels } = await import('opensheetmusicdisplay')`)\n * rather than this module assuming a value that could drift across OSMD\n * versions. Tests pin it explicitly. */\n unitInPixels: number;\n}\n\nconst EMPTY_LAYOUT: NotationLayout = {\n src: { x: 0, y: 0, w: 0, h: 0 },\n rect: { dx: 0, dy: 0, dw: 0, dh: 0 },\n systems: [],\n measures: [],\n};\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n/**\n * Pure SVG-backend geometry extractor — builds the SAME `NotationLayout`\n * shape the canvas path's `notationLayout()` does (measure column boxes +\n * system rows + a `rect` vertical band), but read directly from OSMD's\n * `GraphicalMusicSheet` in SVG/CSS px space (see the unit-conversion doc\n * above) instead of a rasterized bitmap. Rebuild on every render (zoom /\n * resize / new score) — cheap, pure array/object mapping, no DOM reads.\n *\n * `osmd` is duck-typed `any` — same contract as promo.ts's\n * `extractGeometry(osmd: any, ...)` — so tests can pass a plain fixture\n * object shaped like the minimal slice of a real `OpenSheetMusicDisplay`\n * instance this function reads (`{ Zoom, GraphicSheet: { MusicPages,\n * MeasureList } }`) without needing a real browser OSMD render (see\n * `CreateSvgNotationPlayerOpts.rendered`'s doc for why headless can't do a\n * real one).\n *\n * `rect` spans the FULL engraved page (top-aligned, `dy = 0`) — there is no\n * follow-camera crop in this component (the whole score is always in the\n * DOM; the PAGE scrolls it) — matching the canvas scroll-mode's `flowLayout`\n * in spirit. `vstackAudioPlayheadLine` only reads `rect.dy`/`rect.dh` to\n * clamp the playhead into the drawn band, so this is a correct, minimal\n * `rect` for that consumer.\n */\nexport function svgNotationLayout(osmd: any, opts: SvgNotationLayoutOpts): NotationLayout {\n try {\n const zoom =\n typeof osmd?.Zoom === 'number' && osmd.Zoom > 0\n ? osmd.Zoom\n : typeof osmd?.zoom === 'number' && osmd.zoom > 0\n ? osmd.zoom\n : 1;\n const f = opts.unitInPixels * zoom;\n\n const graphic: any = osmd?.GraphicSheet;\n const page: any = graphic?.MusicPages?.[0];\n const pageSize = page?.PositionAndShape?.Size;\n const musicSystems: any[] = page?.MusicSystems ?? [];\n if (!(pageSize?.width > 0) || !(pageSize?.height > 0) || !musicSystems.length) return EMPTY_LAYOUT;\n\n const toBox = (pas: any): Box | null => {\n const p = pas?.AbsolutePosition;\n const sz = pas?.Size;\n if (!p || !sz) return null;\n return { x: p.x * f, y: p.y * f, w: sz.width * f, h: sz.height * f };\n };\n\n const systems: Box[] = musicSystems\n .map((s) => toBox(s?.PositionAndShape))\n .filter((b): b is Box => !!b && b.w > 1 && b.h > 1)\n .sort((a, b) => a.y - b.y);\n if (!systems.length) return EMPTY_LAYOUT;\n\n const measureList: any[][] = graphic?.MeasureList ?? [];\n const measures: StaffMeasureBox[] = [];\n measureList.forEach((staves, index) => {\n (staves ?? []).forEach((m: any, staff: number) => {\n const box = toBox(m?.PositionAndShape);\n if (box && box.w > 1 && box.h > 1) {\n const seX = (m?.staffEntries ?? [])[0]?.PositionAndShape?.AbsolutePosition?.x;\n const noteStartX = typeof seX === 'number' ? seX * f : box.x;\n measures.push({ index, staff, box, noteStartX });\n }\n });\n });\n\n const dw = pageSize.width * f;\n const dh = pageSize.height * f;\n return {\n src: { x: 0, y: 0, w: dw, h: dh },\n rect: { dx: 0, dy: 0, dw, dh },\n systems,\n measures,\n };\n } catch {\n return EMPTY_LAYOUT;\n }\n}\n/* eslint-enable @typescript-eslint/no-explicit-any */\n\n// ─── createSvgNotationPlayer ────────────────────────────────────────────────\n//\n// Zoom/resize position preservation (`computeReflowScrollDelta`) and the\n// auto-follow discriminator (`isWithinProgrammaticScroll` /\n// `hasReachedProgrammaticTarget` + the stateful `createFollowController`)\n// moved to `./notationCommon` (0.39.0) — shared with notationPlayerVerovio.ts.\n// Re-exported above for backward compatibility.\n\nconst DEFAULT_PLAYHEAD_COLOR = '#2f6f4f';\n/** Engrave-width ceiling, CSS px (design doc §\"Render\": \"cap ~1200px, the\n * 0.36.2 rule\" — the same reasoning as notationPlayer.ts's\n * `MAX_ENGRAVE_WIDTH`, restated here rather than imported since the two\n * players' constants are independently tunable, and this one is spec'd to a\n * slightly different value). */\nexport const MAX_ENGRAVE_WIDTH_SVG = 1200;\n/** Floor so a not-yet-laid-out / zero-width host never engraves at 0px. */\nconst MIN_ENGRAVE_WIDTH_SVG = 280;\n\n/** Build a live, interactive SVG (vector) notation player. See the module doc\n * + `docs/superpowers/specs/2026-08-11-svg-notation-player-design.md` (in\n * stave-web-sightread) for the full design. */\nexport function createSvgNotationPlayer(opts: CreateSvgNotationPlayerOpts): SvgNotationPlayer {\n const { host, musicXml, onsetsMs, noteCols } = opts;\n const playheadColor = opts.playheadColor ?? DEFAULT_PLAYHEAD_COLOR;\n const onsets = distinctOnsets(onsetsMs.map((onsetMs) => ({ onsetMs })));\n\n const root = document.createElement('div');\n root.style.position = 'relative';\n root.style.width = '100%';\n // Let the browser's native pinch-zoom AND vertical page-scroll gestures\n // through — the \"optical zoom is free\" half of the design (§5): vectors\n // stay crisp at any pinch/browser-zoom level, and this component adds\n // nothing to make that work beyond not blocking the gesture.\n root.style.touchAction = 'pan-y pinch-zoom';\n host.appendChild(root);\n\n const svgHost = document.createElement('div');\n root.appendChild(svgHost);\n\n const playheadEl = document.createElement('div');\n playheadEl.style.position = 'absolute';\n playheadEl.style.left = '0px';\n playheadEl.style.top = '0px';\n playheadEl.style.width = '2px';\n playheadEl.style.height = '0px';\n playheadEl.style.background = playheadColor;\n playheadEl.style.opacity = '0';\n playheadEl.style.pointerEvents = 'none';\n root.appendChild(playheadEl);\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let osmd: any = null;\n let currentLayout: NotationLayout | null = null;\n let currentZoom = opts.zoom ?? 1;\n let unitInPixelsConst = 10; // overwritten by the real import before first use (rendered-seam path never reads it)\n let lastEngravedWidthPx = 0;\n let destroyed = false;\n let lastTMs = 0;\n // Stale-table guard (design doc §\"Known risks\" — \"Overlay drift on\n // reflow\"): each async re-engrave captures its own token; if a NEWER\n // reflow starts before an older one's `await` resolves, the older one's\n // completion is a no-op instead of clobbering the newer geometry.\n let rebuildToken = 0;\n\n function desiredEngraveWidthPx(): number {\n const w = host.clientWidth || 0;\n return Math.max(MIN_ENGRAVE_WIDTH_SVG, Math.min(w || MAX_ENGRAVE_WIDTH_SVG, MAX_ENGRAVE_WIDTH_SVG));\n }\n\n // ─── Playhead ──────────────────────────────────────────────────────────\n\n function renderPlayhead(tMs: number): void {\n lastTMs = tMs;\n if (!currentLayout) return;\n const nBars = currentLayout.measures.length\n ? Math.max(...currentLayout.measures.map((m) => m.index)) + 1\n : 0;\n const line = vstackAudioPlayheadLine(currentLayout, onsets, tMs, nBars, noteCols);\n if (!line) {\n playheadEl.style.opacity = '0';\n return;\n }\n playheadEl.style.opacity = String(line.alpha);\n playheadEl.style.left = `${line.x}px`;\n playheadEl.style.top = `${line.y0}px`;\n playheadEl.style.height = `${Math.max(0, line.y1 - line.y0)}px`;\n const layoutForFollow = currentLayout;\n const sys = line.sys ?? -1;\n follow.follow({\n systemIndex: sys,\n getSystemRect: () => {\n const box = layoutForFollow.systems[sys];\n if (!box) return null;\n const rootRect = root.getBoundingClientRect();\n return { top: rootRect.top + box.y, bottom: rootRect.top + box.y + box.h };\n },\n getPlayheadRect: () =>\n typeof playheadEl.getBoundingClientRect === 'function' ? playheadEl.getBoundingClientRect() : null,\n });\n }\n\n // ─── Auto-follow (target-position discriminator — see notationCommon.ts) ─\n\n const follow = createFollowController();\n\n // ─── setTime ───────────────────────────────────────────────────────────\n\n function setTime(tMs: number): void {\n if (destroyed) return;\n follow.onSetTime(tMs);\n renderPlayhead(tMs);\n }\n\n // ─── Engrave / reflow ──────────────────────────────────────────────────\n\n async function initialEngrave(): Promise<void> {\n if (opts.rendered) {\n currentLayout = opts.rendered;\n lastEngravedWidthPx = desiredEngraveWidthPx();\n renderPlayhead(lastTMs);\n return;\n }\n // Literal dynamic import — consumers' bundlers must statically see the\n // specifier (same reasoning as promo.ts/notationPlayer.ts); a caller\n // that only ever uses the `rendered` test seam never pulls OSMD in.\n const { OpenSheetMusicDisplay, unitInPixels } = await import('opensheetmusicdisplay');\n unitInPixelsConst = unitInPixels;\n if (destroyed) return;\n\n const widthPx = desiredEngraveWidthPx();\n svgHost.style.width = `${widthPx}px`;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const inst: any = new OpenSheetMusicDisplay(svgHost, {\n backend: 'svg',\n autoResize: false,\n drawTitle: false,\n drawSubtitle: false,\n drawComposer: false,\n drawLyricist: false,\n drawPartNames: false,\n });\n await inst.load(musicXml);\n if (destroyed) return;\n inst.Zoom = currentZoom;\n inst.render();\n if (destroyed) return;\n\n osmd = inst;\n lastEngravedWidthPx = widthPx;\n currentLayout = svgNotationLayout(inst, { unitInPixels: unitInPixelsConst });\n renderPlayhead(lastTMs);\n }\n\n const ready = initialEngrave();\n\n /** Re-engrave at `newZoom` and the host's CURRENT width, preserving the\n * scroll position of whatever content is centered in the viewport right\n * now. Shared by both `setZoom` and `resize` (resize just passes the\n * unchanged `currentZoom`). No-op when neither the width nor the zoom\n * actually changed, or when using the `rendered` test seam (nothing to\n * re-engrave). */\n async function reflow(newZoom: number): Promise<void> {\n if (destroyed) return;\n const widthPx = desiredEngraveWidthPx();\n if (widthPx === lastEngravedWidthPx && newZoom === currentZoom) return;\n if (opts.rendered || !osmd) {\n currentZoom = newZoom;\n return;\n }\n\n const myToken = ++rebuildToken;\n const oldLayout = currentLayout;\n const hasWin = typeof window !== 'undefined';\n let anchorY: number | null = null;\n const anchorX = widthPx / 2;\n if (hasWin && typeof root.getBoundingClientRect === 'function') {\n const r = root.getBoundingClientRect();\n anchorY = window.innerHeight / 2 - r.top;\n }\n\n svgHost.style.width = `${widthPx}px`;\n osmd.Zoom = newZoom;\n // Re-run layout (not just a redraw): a width change must re-flow which\n // measures land on which system, and — since we can't be certain a pure\n // zoom change never affects line-breaking on every OSMD version — this is\n // called unconditionally rather than gated to the width-only case.\n osmd.updateGraphic();\n osmd.render();\n if (destroyed || myToken !== rebuildToken) return;\n\n lastEngravedWidthPx = widthPx;\n currentZoom = newZoom;\n currentLayout = svgNotationLayout(osmd, { unitInPixels: unitInPixelsConst });\n\n if (oldLayout && anchorY != null && hasWin) {\n const delta = computeReflowScrollDelta(oldLayout, currentLayout, anchorX, anchorY);\n if (Number.isFinite(delta) && Math.abs(delta) > 0.5) {\n window.scrollTo({ top: Math.max(0, window.scrollY + delta), left: window.scrollX, behavior: 'auto' });\n }\n }\n if (!destroyed) renderPlayhead(lastTMs);\n }\n\n // ─── Click-to-seek ─────────────────────────────────────────────────────\n\n const clickListeners: Array<(measureIndex: number) => void> = [];\n function onRootClick(e: MouseEvent): void {\n if (!currentLayout) return;\n const rect = root.getBoundingClientRect();\n const mx = e.clientX - rect.left;\n const my = e.clientY - rect.top;\n const idx = hitTestMeasureAt(currentLayout, mx, my);\n if (idx != null) for (const cb of clickListeners) cb(idx);\n }\n root.addEventListener('click', onRootClick);\n\n return {\n ready,\n setTime,\n async setZoom(z: number): Promise<void> {\n await ready;\n await reflow(z);\n },\n async resize(): Promise<void> {\n await ready;\n await reflow(currentZoom);\n },\n onMeasureClick(cb: (measureIndex: number) => void): () => void {\n clickListeners.push(cb);\n return () => {\n const i = clickListeners.indexOf(cb);\n if (i >= 0) clickListeners.splice(i, 1);\n };\n },\n destroy(): void {\n if (destroyed) return;\n destroyed = true;\n rebuildToken++;\n root.removeEventListener('click', onRootClick);\n follow.destroy();\n clickListeners.length = 0;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n try { (osmd as any)?.clear?.(); } catch { /* best-effort */ }\n osmd = null;\n currentLayout = null;\n if (root.parentNode === host) host.removeChild(root);\n },\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/notationPlayerSvg.ts"],"sourcesContent":["// createSvgNotationPlayer — the SVG (vector) sibling of `createNotationPlayer`\n// (src/notationPlayer.ts). Same job (a live, caller-driven notation +\n// gliding-playhead widget), different medium: OSMD's native SVG backend\n// instead of a rasterized canvas. See\n// docs — stave-web-sightread's\n// docs/superpowers/specs/2026-08-11-svg-notation-player-design.md — for the\n// full design rationale (why: vectors don't blur on pinch/browser zoom, no\n// canvas-area cap, no raster tiling needed for full-score scroll).\n//\n// THE CANVAS MODULE IS NOT MODIFIED OR IMPORTED FOR ITS OSMD/RASTER PATH —\n// this is a parallel component. What IS reused, verbatim, no new math:\n// - `vstackAudioPlayheadLine` (scene/notationGeometry.ts) — the exact same\n// onset-anchored, carriage-return playhead interpolation the canvas path\n// uses. It operates purely on a `NotationLayout` (measure column boxes +\n// a vertical clamp band) — it does not care whether those boxes came from\n// a canvas raster or live SVG DOM geometry, so THIS module's job is only\n// to produce a `NotationLayout` in SVG/CSS px space (see\n// `svgNotationLayout` below) and everything downstream is identical.\n// - `hitTestMeasureAt` (scene/notationGeometry.ts, moved there in 0.38.0\n// specifically so this module never has a build-time edge into\n// notationPlayer.ts's canvas/raster implementation) — the exact same pure\n// point-in-measure-box hit-test, reused for click-to-seek.\n// - `distinctOnsets`, `measureColumnsFromLayout` — small pure helpers,\n// reused as-is.\n//\n// WHAT'S NEW (not a re-derivation of any of the above):\n// - `svgNotationLayout` — an SVG-backend geometry extractor, the SVG\n// counterpart to promo.ts's canvas-only `extractGeometry`. See its own\n// doc comment for the unit-conversion derivation.\n// - `computeReflowScrollDelta` — zoom/resize position-preservation math\n// (not playhead interpolation; a one-shot \"where did this same content\n// move to\" lookup using the existing hit-test + column helpers).\n// - `isWithinProgrammaticScroll` / `hasReachedProgrammaticTarget` — the\n// improved auto-follow discriminator (design doc §4): unlike the canvas\n// scroll-mode player's fixed 600ms \"programmatic scroll\" grace window,\n// this compares the ACTUAL scroll position against the EXACT target this\n// component itself requested, so classification is correct regardless of\n// how long a smooth-scroll animation takes (no grace-window race).\n//\n// IMPORT PATH: subpath-only — `@real-music-packages/web-core/notationPlayerSvg`\n// — not re-exported from the root barrel (same reasoning as notationPlayer.ts:\n// the root barrel is theory-only/zero-dependency).\n\nimport {\n vstackAudioPlayheadLine,\n distinctOnsets,\n hitTestMeasureAt,\n type NotationLayout,\n} from './scene/notationGeometry';\nimport type { Box, StaffMeasureBox } from './promo';\nimport {\n computeReflowScrollDelta,\n createFollowController,\n isWithinProgrammaticScroll,\n hasReachedProgrammaticTarget,\n PROGRAMMATIC_SCROLL_EPSILON_PX,\n} from './notationCommon';\n\n// Re-exported for backward compatibility — this module's own tests (and any\n// external importer) still pull these from `notationPlayerSvg`; the\n// implementations now live in `notationCommon.ts` (shared with\n// notationPlayerVerovio.ts, 0.39.0). See that module's doc for why.\nexport { computeReflowScrollDelta, isWithinProgrammaticScroll, hasReachedProgrammaticTarget, PROGRAMMATIC_SCROLL_EPSILON_PX };\n\nexport interface CreateSvgNotationPlayerOpts {\n /** Element the player's content is mounted into. Takes NATURAL content\n * height (the whole score, page-flow layout) — the host must not clip a\n * fixed height; the PAGE scrolls the score, there is no internal camera. */\n host: HTMLElement;\n /** MusicXML to engrave. Ignored when `rendered` (the test seam) is set. */\n musicXml: string;\n /** Distinct note onsets (ms) the playhead locks to — same contract as\n * `CreateNotationPlayerOpts.onsetsMs` in notationPlayer.ts. */\n onsetsMs: number[];\n /** Per-onset engraved column positions, 1:1 with the DEDUPED/sorted\n * `onsetsMs` — same semantics as the canvas player's `noteCols`. */\n noteCols?: number[];\n /** Playhead line color. Default `'#2f6f4f'`. */\n playheadColor?: string;\n /** Initial OSMD zoom (a pure post-layout visual scale — see\n * `svgNotationLayout`'s doc). Default 1 (OSMD's own default — the\n * engraving fits the host's own width at normal note size). */\n zoom?: number;\n /**\n * Advanced / test seam: a pre-built `NotationLayout`, bypassing the real\n * OSMD SVG engrave (`musicXml` is still required by the type but is\n * ignored when this is set). Mirrors `CreateNotationPlayerOpts.rendered` in\n * notationPlayer.ts for the identical reason: real OSMD *rendering* needs\n * actual browser canvas glyph metrics (for its line-breaking pass) even\n * when the SVG backend is selected — headless/jsdom can't fully provide\n * that — so this is how this module stays unit-testable in Node. Not\n * needed in a real browser host. When set, `setZoom`/`resize` update the\n * tracked zoom/width but perform no real re-engrave (there is nothing to\n * re-engrave).\n */\n rendered?: NotationLayout;\n}\n\nexport interface SvgNotationPlayer {\n /** Resolves once the engraving is in the DOM and ready to draw. `setTime`/\n * click hit-testing are safe to call before this resolves (no-op until\n * ready, same contract as the canvas player). */\n readonly ready: Promise<void>;\n /** Drive the playhead for absolute playback time `tMs`. Caller owns the\n * audio clock + rAF loop. */\n setTime(tMs: number): void;\n /** Re-engrave at a new OSMD zoom (systems reflow). The scroll position is\n * restored afterward so the content that was centered in the viewport\n * before the reflow is still centered after it. */\n setZoom(z: number): Promise<void>;\n /** Re-measure the host and reflow to match its current width, with the\n * same scroll-position preservation as `setZoom`. Call on host resize /\n * orientation change. */\n resize(): Promise<void>;\n /** Gate the auto-follow scroll on the app's transport state: pass `true`\n * on play, `false` on pause/stop (see FollowController.setEnabled — while\n * disabled NOTHING may scroll the sheet, including the idle-rearm's\n * off-screen rescue). Defaults to enabled. */\n setFollowEnabled(on: boolean): void;\n /** Register a measure-click handler (measure index, matching\n * `ScoreNote.measure`/the engraved index). Returns an unsubscribe fn. */\n onMeasureClick(cb: (measureIndex: number) => void): () => void;\n /** Tear down: removes the mounted DOM (engraving + playhead overlay) from\n * `host`, and drops every listener this instance added (click, window\n * scroll) and pending async work (a token guard drops any in-flight\n * reflow's effects). Idempotent. */\n destroy(): void;\n}\n\n// ─── svgNotationLayout — the SVG-backend geometry extractor ───────────────\n//\n// UNIT CONVERSION — derived, not guessed (verified against the vendored\n// opensheetmusicdisplay + vexflow source, not just its .d.ts comments, which\n// are stale here — see below):\n//\n// 1. `GraphicalMusicSheet` (`osmd.GraphicSheet`) positions\n// (`PositionAndShape.AbsolutePosition`/`.Size`) are in backend-agnostic\n// \"OSMD units\" — the SAME object model promo.ts's canvas-only\n// `extractGeometry` reads (`osmd.GraphicSheet.MusicPages[0].MusicSystems`,\n// `.MeasureList`). Whichever backend (canvas/svg) was requested, this\n// layer is identical.\n// 2. OSMD's Vexflow draw layer (`VexFlowMusicSheetDrawer`) converts an OSMD\n// unit value to a \"raw\" Vexflow/SVG px value via an EXPORTED constant,\n// `unitInPixels` (currently 10 — NOT `EngravingRules.unit`, which is a\n// different, unrelated field that is NOT the px-per-unit factor despite\n// what its .d.ts comment implies; confirmed by reading the actual\n// compiled source, not trusting the stale doc comment). This raw value\n// is written directly into each SVG element's own coordinate attributes\n// — it does NOT yet include `zoom`.\n// 3. `osmd.Zoom` (current zoom) is applied SEPARATELY, at the SVG-backend\n// level, via a `viewBox` trick (`vexflow/src/svgcontext.js`'s\n// `scale(x,y)`): the `<svg>` element's `width`/`height` ATTRIBUTES are\n// set to the FINAL (zoomed) CSS px size, while its `viewBox` spans\n// `width/zoom .. height/zoom` — i.e. the RAW (unzoomed, step-2) content\n// coordinates. The browser therefore maps that raw coordinate space onto\n// the final CSS px box by exactly `zoom`.\n// Net result: `cssPx = unitValue * unitInPixels * zoom`. Both factors are\n// read from the live library/instance (never a hardcoded literal `10` or an\n// assumed zoom) — see `SvgNotationLayoutOpts.unitInPixels`'s doc — so a\n// future OSMD version changing either is picked up automatically, which is\n// exactly the \"10 units/staff-space assumptions have version drift\" risk\n// the design doc calls out.\n//\n// This mirrors, in spirit, promo.ts's canvas `extractGeometry` self-\n// calibration (`canvas.width / (contentRight+contentLeft)`, chosen there\n// specifically because a naive formula broke on overflowed layouts) — the SVG\n// backend doesn't have that raster-overflow failure mode (there is no\n// backing-store to overflow; the viewBox IS the content, always), so the\n// derived formula above is exact, not an approximation.\n\nexport interface SvgNotationLayoutOpts {\n /** CSS px per OSMD unit at zoom 1 — OSMD's own exported `unitInPixels`\n * constant (see the derivation above). REQUIRED, no default: the point is\n * to FORCE the real call site to source this from the live\n * `opensheetmusicdisplay` import\n * (`const { unitInPixels } = await import('opensheetmusicdisplay')`)\n * rather than this module assuming a value that could drift across OSMD\n * versions. Tests pin it explicitly. */\n unitInPixels: number;\n}\n\nconst EMPTY_LAYOUT: NotationLayout = {\n src: { x: 0, y: 0, w: 0, h: 0 },\n rect: { dx: 0, dy: 0, dw: 0, dh: 0 },\n systems: [],\n measures: [],\n};\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n/**\n * Pure SVG-backend geometry extractor — builds the SAME `NotationLayout`\n * shape the canvas path's `notationLayout()` does (measure column boxes +\n * system rows + a `rect` vertical band), but read directly from OSMD's\n * `GraphicalMusicSheet` in SVG/CSS px space (see the unit-conversion doc\n * above) instead of a rasterized bitmap. Rebuild on every render (zoom /\n * resize / new score) — cheap, pure array/object mapping, no DOM reads.\n *\n * `osmd` is duck-typed `any` — same contract as promo.ts's\n * `extractGeometry(osmd: any, ...)` — so tests can pass a plain fixture\n * object shaped like the minimal slice of a real `OpenSheetMusicDisplay`\n * instance this function reads (`{ Zoom, GraphicSheet: { MusicPages,\n * MeasureList } }`) without needing a real browser OSMD render (see\n * `CreateSvgNotationPlayerOpts.rendered`'s doc for why headless can't do a\n * real one).\n *\n * `rect` spans the FULL engraved page (top-aligned, `dy = 0`) — there is no\n * follow-camera crop in this component (the whole score is always in the\n * DOM; the PAGE scrolls it) — matching the canvas scroll-mode's `flowLayout`\n * in spirit. `vstackAudioPlayheadLine` only reads `rect.dy`/`rect.dh` to\n * clamp the playhead into the drawn band, so this is a correct, minimal\n * `rect` for that consumer.\n */\nexport function svgNotationLayout(osmd: any, opts: SvgNotationLayoutOpts): NotationLayout {\n try {\n const zoom =\n typeof osmd?.Zoom === 'number' && osmd.Zoom > 0\n ? osmd.Zoom\n : typeof osmd?.zoom === 'number' && osmd.zoom > 0\n ? osmd.zoom\n : 1;\n const f = opts.unitInPixels * zoom;\n\n const graphic: any = osmd?.GraphicSheet;\n const page: any = graphic?.MusicPages?.[0];\n const pageSize = page?.PositionAndShape?.Size;\n const musicSystems: any[] = page?.MusicSystems ?? [];\n if (!(pageSize?.width > 0) || !(pageSize?.height > 0) || !musicSystems.length) return EMPTY_LAYOUT;\n\n const toBox = (pas: any): Box | null => {\n const p = pas?.AbsolutePosition;\n const sz = pas?.Size;\n if (!p || !sz) return null;\n return { x: p.x * f, y: p.y * f, w: sz.width * f, h: sz.height * f };\n };\n\n const systems: Box[] = musicSystems\n .map((s) => toBox(s?.PositionAndShape))\n .filter((b): b is Box => !!b && b.w > 1 && b.h > 1)\n .sort((a, b) => a.y - b.y);\n if (!systems.length) return EMPTY_LAYOUT;\n\n const measureList: any[][] = graphic?.MeasureList ?? [];\n const measures: StaffMeasureBox[] = [];\n measureList.forEach((staves, index) => {\n (staves ?? []).forEach((m: any, staff: number) => {\n const box = toBox(m?.PositionAndShape);\n if (box && box.w > 1 && box.h > 1) {\n const seX = (m?.staffEntries ?? [])[0]?.PositionAndShape?.AbsolutePosition?.x;\n const noteStartX = typeof seX === 'number' ? seX * f : box.x;\n measures.push({ index, staff, box, noteStartX });\n }\n });\n });\n\n const dw = pageSize.width * f;\n const dh = pageSize.height * f;\n return {\n src: { x: 0, y: 0, w: dw, h: dh },\n rect: { dx: 0, dy: 0, dw, dh },\n systems,\n measures,\n };\n } catch {\n return EMPTY_LAYOUT;\n }\n}\n/* eslint-enable @typescript-eslint/no-explicit-any */\n\n// ─── createSvgNotationPlayer ────────────────────────────────────────────────\n//\n// Zoom/resize position preservation (`computeReflowScrollDelta`) and the\n// auto-follow discriminator (`isWithinProgrammaticScroll` /\n// `hasReachedProgrammaticTarget` + the stateful `createFollowController`)\n// moved to `./notationCommon` (0.39.0) — shared with notationPlayerVerovio.ts.\n// Re-exported above for backward compatibility.\n\nconst DEFAULT_PLAYHEAD_COLOR = '#2f6f4f';\n/** Engrave-width ceiling, CSS px (design doc §\"Render\": \"cap ~1200px, the\n * 0.36.2 rule\" — the same reasoning as notationPlayer.ts's\n * `MAX_ENGRAVE_WIDTH`, restated here rather than imported since the two\n * players' constants are independently tunable, and this one is spec'd to a\n * slightly different value). */\nexport const MAX_ENGRAVE_WIDTH_SVG = 1200;\n/** Floor so a not-yet-laid-out / zero-width host never engraves at 0px. */\nconst MIN_ENGRAVE_WIDTH_SVG = 280;\n\n/** Build a live, interactive SVG (vector) notation player. See the module doc\n * + `docs/superpowers/specs/2026-08-11-svg-notation-player-design.md` (in\n * stave-web-sightread) for the full design. */\nexport function createSvgNotationPlayer(opts: CreateSvgNotationPlayerOpts): SvgNotationPlayer {\n const { host, musicXml, onsetsMs, noteCols } = opts;\n const playheadColor = opts.playheadColor ?? DEFAULT_PLAYHEAD_COLOR;\n const onsets = distinctOnsets(onsetsMs.map((onsetMs) => ({ onsetMs })));\n\n const root = document.createElement('div');\n root.style.position = 'relative';\n root.style.width = '100%';\n // Let the browser's native pinch-zoom AND vertical page-scroll gestures\n // through — the \"optical zoom is free\" half of the design (§5): vectors\n // stay crisp at any pinch/browser-zoom level, and this component adds\n // nothing to make that work beyond not blocking the gesture.\n root.style.touchAction = 'pan-y pinch-zoom';\n host.appendChild(root);\n\n const svgHost = document.createElement('div');\n root.appendChild(svgHost);\n\n const playheadEl = document.createElement('div');\n playheadEl.style.position = 'absolute';\n playheadEl.style.left = '0px';\n playheadEl.style.top = '0px';\n playheadEl.style.width = '2px';\n playheadEl.style.height = '0px';\n playheadEl.style.background = playheadColor;\n playheadEl.style.opacity = '0';\n playheadEl.style.pointerEvents = 'none';\n root.appendChild(playheadEl);\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let osmd: any = null;\n let currentLayout: NotationLayout | null = null;\n let currentZoom = opts.zoom ?? 1;\n let unitInPixelsConst = 10; // overwritten by the real import before first use (rendered-seam path never reads it)\n let lastEngravedWidthPx = 0;\n let destroyed = false;\n let lastTMs = 0;\n // Stale-table guard (design doc §\"Known risks\" — \"Overlay drift on\n // reflow\"): each async re-engrave captures its own token; if a NEWER\n // reflow starts before an older one's `await` resolves, the older one's\n // completion is a no-op instead of clobbering the newer geometry.\n let rebuildToken = 0;\n\n function desiredEngraveWidthPx(): number {\n const w = host.clientWidth || 0;\n return Math.max(MIN_ENGRAVE_WIDTH_SVG, Math.min(w || MAX_ENGRAVE_WIDTH_SVG, MAX_ENGRAVE_WIDTH_SVG));\n }\n\n // ─── Playhead ──────────────────────────────────────────────────────────\n\n function renderPlayhead(tMs: number): void {\n lastTMs = tMs;\n if (!currentLayout) return;\n const nBars = currentLayout.measures.length\n ? Math.max(...currentLayout.measures.map((m) => m.index)) + 1\n : 0;\n const line = vstackAudioPlayheadLine(currentLayout, onsets, tMs, nBars, noteCols);\n if (!line) {\n playheadEl.style.opacity = '0';\n return;\n }\n playheadEl.style.opacity = String(line.alpha);\n playheadEl.style.left = `${line.x}px`;\n playheadEl.style.top = `${line.y0}px`;\n playheadEl.style.height = `${Math.max(0, line.y1 - line.y0)}px`;\n const layoutForFollow = currentLayout;\n const sys = line.sys ?? -1;\n follow.follow({\n systemIndex: sys,\n getSystemRect: () => {\n const box = layoutForFollow.systems[sys];\n if (!box) return null;\n const rootRect = root.getBoundingClientRect();\n return { top: rootRect.top + box.y, bottom: rootRect.top + box.y + box.h };\n },\n getPlayheadRect: () =>\n typeof playheadEl.getBoundingClientRect === 'function' ? playheadEl.getBoundingClientRect() : null,\n });\n }\n\n // ─── Auto-follow (target-position discriminator — see notationCommon.ts) ─\n\n const follow = createFollowController();\n\n // ─── setTime ───────────────────────────────────────────────────────────\n\n function setTime(tMs: number): void {\n if (destroyed) return;\n follow.onSetTime(tMs);\n renderPlayhead(tMs);\n }\n\n // ─── Engrave / reflow ──────────────────────────────────────────────────\n\n async function initialEngrave(): Promise<void> {\n if (opts.rendered) {\n currentLayout = opts.rendered;\n lastEngravedWidthPx = desiredEngraveWidthPx();\n renderPlayhead(lastTMs);\n return;\n }\n // Literal dynamic import — consumers' bundlers must statically see the\n // specifier (same reasoning as promo.ts/notationPlayer.ts); a caller\n // that only ever uses the `rendered` test seam never pulls OSMD in.\n const { OpenSheetMusicDisplay, unitInPixels } = await import('opensheetmusicdisplay');\n unitInPixelsConst = unitInPixels;\n if (destroyed) return;\n\n const widthPx = desiredEngraveWidthPx();\n svgHost.style.width = `${widthPx}px`;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const inst: any = new OpenSheetMusicDisplay(svgHost, {\n backend: 'svg',\n autoResize: false,\n drawTitle: false,\n drawSubtitle: false,\n drawComposer: false,\n drawLyricist: false,\n drawPartNames: false,\n });\n await inst.load(musicXml);\n if (destroyed) return;\n inst.Zoom = currentZoom;\n inst.render();\n if (destroyed) return;\n\n osmd = inst;\n lastEngravedWidthPx = widthPx;\n currentLayout = svgNotationLayout(inst, { unitInPixels: unitInPixelsConst });\n renderPlayhead(lastTMs);\n }\n\n const ready = initialEngrave();\n\n /** Re-engrave at `newZoom` and the host's CURRENT width, preserving the\n * scroll position of whatever content is centered in the viewport right\n * now. Shared by both `setZoom` and `resize` (resize just passes the\n * unchanged `currentZoom`). No-op when neither the width nor the zoom\n * actually changed, or when using the `rendered` test seam (nothing to\n * re-engrave). */\n async function reflow(newZoom: number): Promise<void> {\n if (destroyed) return;\n const widthPx = desiredEngraveWidthPx();\n if (widthPx === lastEngravedWidthPx && newZoom === currentZoom) return;\n if (opts.rendered || !osmd) {\n currentZoom = newZoom;\n return;\n }\n\n const myToken = ++rebuildToken;\n const oldLayout = currentLayout;\n const hasWin = typeof window !== 'undefined';\n let anchorY: number | null = null;\n const anchorX = widthPx / 2;\n if (hasWin && typeof root.getBoundingClientRect === 'function') {\n const r = root.getBoundingClientRect();\n anchorY = window.innerHeight / 2 - r.top;\n }\n\n svgHost.style.width = `${widthPx}px`;\n osmd.Zoom = newZoom;\n // Re-run layout (not just a redraw): a width change must re-flow which\n // measures land on which system, and — since we can't be certain a pure\n // zoom change never affects line-breaking on every OSMD version — this is\n // called unconditionally rather than gated to the width-only case.\n osmd.updateGraphic();\n osmd.render();\n if (destroyed || myToken !== rebuildToken) return;\n\n lastEngravedWidthPx = widthPx;\n currentZoom = newZoom;\n currentLayout = svgNotationLayout(osmd, { unitInPixels: unitInPixelsConst });\n\n if (oldLayout && anchorY != null && hasWin) {\n const delta = computeReflowScrollDelta(oldLayout, currentLayout, anchorX, anchorY);\n if (Number.isFinite(delta) && Math.abs(delta) > 0.5) {\n window.scrollTo({ top: Math.max(0, window.scrollY + delta), left: window.scrollX, behavior: 'auto' });\n }\n }\n if (!destroyed) renderPlayhead(lastTMs);\n }\n\n // ─── Click-to-seek ─────────────────────────────────────────────────────\n\n const clickListeners: Array<(measureIndex: number) => void> = [];\n function onRootClick(e: MouseEvent): void {\n if (!currentLayout) return;\n const rect = root.getBoundingClientRect();\n const mx = e.clientX - rect.left;\n const my = e.clientY - rect.top;\n const idx = hitTestMeasureAt(currentLayout, mx, my);\n if (idx != null) for (const cb of clickListeners) cb(idx);\n }\n root.addEventListener('click', onRootClick);\n\n return {\n ready,\n setTime,\n setFollowEnabled(on) {\n follow.setEnabled(on);\n },\n async setZoom(z: number): Promise<void> {\n await ready;\n await reflow(z);\n },\n async resize(): Promise<void> {\n await ready;\n await reflow(currentZoom);\n },\n onMeasureClick(cb: (measureIndex: number) => void): () => void {\n clickListeners.push(cb);\n return () => {\n const i = clickListeners.indexOf(cb);\n if (i >= 0) clickListeners.splice(i, 1);\n };\n },\n destroy(): void {\n if (destroyed) return;\n destroyed = true;\n rebuildToken++;\n root.removeEventListener('click', onRootClick);\n follow.destroy();\n clickListeners.length = 0;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n try { (osmd as any)?.clear?.(); } catch { /* best-effort */ }\n osmd = null;\n currentLayout = null;\n if (root.parentNode === host) host.removeChild(root);\n },\n };\n}\n"],"mappings":"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","names":[]}
@@ -61,6 +61,11 @@ interface VerovioNotationPlayer {
61
61
  * same scroll-position preservation as `setZoom`. Call on host resize /
62
62
  * orientation change. */
63
63
  resize(): Promise<void>;
64
+ /** Gate the auto-follow scroll on the app's transport state: pass `true`
65
+ * on play, `false` on pause/stop (see FollowController.setEnabled — while
66
+ * disabled NOTHING may scroll the sheet, including the idle-rearm's
67
+ * off-screen rescue). Defaults to enabled. */
68
+ setFollowEnabled(on: boolean): void;
64
69
  /** Register a measure-click handler (measure index, matching the DOM-order
65
70
  * index `verovioNotationLayout` assigns). Returns an unsubscribe fn. */
66
71
  onMeasureClick(cb: (measureIndex: number) => void): () => void;
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  computeReflowScrollDelta,
3
3
  createFollowController
4
- } from "./chunk-NTTAAUH6.js";
4
+ } from "./chunk-PZJRRJZM.js";
5
5
  import {
6
6
  distinctOnsets,
7
7
  hitTestMeasureAt,
8
8
  measureColumnsFromLayout,
9
9
  vstackAudioPlayheadLine
10
- } from "./chunk-BBWVFH7X.js";
10
+ } from "./chunk-SG7KION4.js";
11
11
  import "./chunk-HXTRNE74.js";
12
12
 
13
13
  // src/notationPlayerVerovio.ts
@@ -336,6 +336,9 @@ function createVerovioNotationPlayer(opts) {
336
336
  return {
337
337
  ready,
338
338
  setTime,
339
+ setFollowEnabled(on) {
340
+ follow.setEnabled(on);
341
+ },
339
342
  async setZoom(z) {
340
343
  await ready;
341
344
  await reflow(z);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/notationPlayerVerovio.ts"],"sourcesContent":["// createVerovioNotationPlayer — the Verovio (vector) sibling of\n// createSvgNotationPlayer (notationPlayerSvg.ts). Same job (a live,\n// caller-driven notation + gliding-playhead widget) and the same swap-friendly\n// API shape, different rendering engine: Verovio's own MusicXML→SVG engraver\n// instead of OSMD. See\n// docs/superpowers/specs/2026-08-11-verovio-player-design.md (stave-web-sightread)\n// §2 for the full design rationale.\n//\n// THE BINDING REFRAME (design doc, top): \"Timing is ours; Verovio renders.\"\n// This module NEVER calls `renderToTimemap` or `getElementsAtTime` — Verovio's\n// timemap is proven wrong on tuplets (the root-cause finding that motivated\n// this whole migration). All timing (`onsets`) is supplied by the caller,\n// derived from `parseReduction`'s spans/offsets; this module's only job is\n// SVG + id→geometry, exactly like notationPlayerSvg.ts's job is OSMD SVG +\n// id→geometry. (Grep gate — see the build report.)\n//\n// REUSED, VERBATIM, NO NEW MATH (same hard rule as notationPlayerSvg.ts):\n// - `vstackAudioPlayheadLine` (scene/notationGeometry.ts) — the exact same\n// onset-anchored, carriage-return playhead interpolation both SVG-family\n// players use. It only ever reads a `NotationLayout`; it does not care\n// that THIS module's layout came from Verovio's rendered SVG DOM instead\n// of OSMD's `GraphicalMusicSheet` object model.\n// - `hitTestMeasureAt`, `measureColumnsFromLayout`, `distinctOnsets`\n// (scene/notationGeometry.ts) — reused as-is, identical to\n// notationPlayerSvg.ts's usage.\n// - `computeReflowScrollDelta` + the auto-follow discriminator\n// (`createFollowController`) — EXTRACTED (0.39.0) out of\n// notationPlayerSvg.ts into `./notationCommon`, so both SVG-family\n// players share one implementation. See that module's doc.\n//\n// WHAT'S NEW (not a re-derivation of any of the above):\n// - `verovioNotationLayout` — a Verovio-backend geometry extractor, this\n// module's counterpart to notationPlayerSvg.ts's `svgNotationLayout`.\n// Verovio exposes NO object model to JS (unlike OSMD's `GraphicSheet`) —\n// only rendered SVG + MEI/timemap (timemap being off-limits per the\n// binding reframe above) — so this reads the ACTUAL rendered SVG DOM:\n// `<g class=\"measure\">` / `<g class=\"staff\">` / `<g class=\"system\">` /\n// `<g class=\"note\">` are Verovio's own stable, documented SVG output\n// classes (confirmed against a real 6.2.0 render — see the migration\n// spike's `id-test*.mjs`). Measure `index` is assigned by DOCUMENT ORDER\n// (musical order) across however many pages were rendered — no reliance\n// on Verovio's own (internal, non-deterministic) generated ids.\n// - Stamped-id note lookups (`verovioOnsetColumns`): Task 1 (stave repo)\n// stamps a deterministic `xml:id` per note before handing MusicXML to\n// Verovio; Verovio PRESERVES caller-supplied ids as the rendered SVG\n// element's own `id` attribute (confirmed: `<note id=\"n-0-0-0\">` in the\n// source round-trips to `<g id=\"n-0-0-0\" class=\"note\">` in the output —\n// an EXACT lookup, no heuristics, unlike the ordinal-spread fallback\n// `vstackAudioPlayheadLine` uses when no `noteCols` are supplied at all).\n// - `verovioZoomOptions` — the semantic zoom→Verovio-options mapping. The\n// migration spike's `zoom-test*.mjs` proved `pageWidth` (Verovio's\n// line-breaking width, in ITS OWN units) drives measures-per-system\n// while `scale` (glyph size) alone does NOT (avgMeasuresPerSystem stayed\n// 3.61 across scale 40/80/150 at a fixed pageWidth; it moved from 2.24 to\n// 3.61 to 5.91 as pageWidth alone rose 1000→1600→2400). Verovio's\n// rendered SVG width in CSS px is EXACTLY `pageWidth * scale / 100`\n// (confirmed empirically) — so solving that identity for `pageWidth`\n// given a TARGET output width (the host's width, held fixed across zoom\n// levels) and a zoom-driven `scale` makes both halves of the semantic\n// (\"bigger zoom ⇒ bigger glyphs AND fewer measures/system, width still\n// fits host\") fall out of ONE formula, not two independently-tuned ones.\n// - The unit-conversion for any element's `getBBox()` (Verovio's rendered\n// SVG user-unit space) → CSS px: each rendered page is\n// `<svg width=\"Wpx\" height=\"Hpx\">` (no viewBox) wrapping a nested\n// `<svg class=\"definition-scale\" viewBox=\"0 0 VBW VBH\">` (Verovio's own\n// structure) — so `cssPx = userUnit * (W / VBW)`, the SAME \"read the\n// scale factor from the live render, never hardcode it\" principle\n// `svgNotationLayout`'s `unitInPixels` derivation uses, just sourced from\n// the DOM (Verovio exposes no JS-side unit constant) instead of an\n// imported library constant.\n//\n// PAGES: \"all rendered, stacked in flow\" (design doc §2) — every Verovio\n// page (`getPageCount()`) is rendered to its own `<svg>` and appended, in\n// order, inside its own `.vrv-page` wrapper `<div>`, inside `svgHost`. Normal\n// block layout stacks them vertically; `verovioNotationLayout` reads each\n// page's OWN offset (`getBoundingClientRect()` relative to the shared root)\n// so geometry from every page lands in ONE continuous coordinate space, the\n// same space the playhead overlay is positioned in. No \"current page\" /\n// pagination concept anywhere in this module — the whole score is always in\n// the DOM; the PAGE scrolls it (identical framing to notationPlayerSvg.ts).\n//\n// SHARED TOOLKIT INSTANCE: Verovio's own package doc: \"only one instance can\n// be created for now\" (`VerovioToolkit.instances`, a static array the C++\n// bridge expects to hold at most one live toolkit). This module therefore\n// keeps ONE module-level toolkit init promise for the whole session — every\n// player created on the page shares it. This is NOT a \"one live player at a\n// time\" assumption — the real consumer (PlayerPage) keeps a harmony player\n// AND a written player alive SIMULTANEOUSLY (lazy-built, destroyed only on\n// piece switch), so two players' `loadData` calls genuinely interleave over\n// the toolkit's lifetime. Safety comes from RECLAIM: every toolkit-consuming\n// path (initial engrave; `reflow`, shared by `setZoom`/`resize`) re-parses\n// ITS OWN `musicXml` via `loadData` synchronously, immediately before\n// rendering — never assumes the toolkit still holds what it loaded last\n// time. Because the reclaim call and the render calls that follow it have no\n// `await` between them, JS's single-threaded run-to-completion semantics\n// guarantee no other player's reflow can interleave mid-sequence — see\n// `reflow`'s own comment + `tests/notationPlayerVerovio.test.ts`'s\n// two-player interleaved-reflow test (the regression this guards against).\n//\n// IMPORT PATH: subpath-only —\n// `@real-music-packages/web-core/notationPlayerVerovio` — not re-exported\n// from the root barrel (same reasoning as notationPlayer.ts/\n// notationPlayerSvg.ts: the root barrel is theory-only/zero-dependency).\n// `verovio` itself is a dynamic `import('verovio/wasm')` /\n// `import('verovio/esm')` INSIDE this module only, marked `external` in\n// tsup.config.ts, so the ~2.3MB gzip WASM only loads on pages that actually\n// construct a player — see the build report's dist-grep evidence.\n\nimport {\n vstackAudioPlayheadLine,\n distinctOnsets,\n hitTestMeasureAt,\n measureColumnsFromLayout,\n type NotationLayout,\n} from './scene/notationGeometry';\nimport type { Box, StaffMeasureBox } from './promo';\nimport { computeReflowScrollDelta, createFollowController } from './notationCommon';\n\n// ─── Public types ───────────────────────────────────────────────────────────\n\n/** One distinct note-onset instant: the audio-clock time it sounds at, and\n * the stamped `xml:id`s (Task 1, stave repo) of every note that sounds at\n * that instant (>1 for a chord). Multiple entries sharing the same `tMs`\n * are merged (their `noteIds` unioned) — the caller does not need to\n * pre-group chords into one entry. */\nexport interface VerovioOnset {\n tMs: number;\n noteIds: string[];\n}\n\nexport interface CreateVerovioNotationPlayerOpts {\n /** Element the player's content is mounted into. Takes NATURAL content\n * height (the whole score, page-flow layout, ALL Verovio pages stacked)\n * — the host must not clip a fixed height; the PAGE scrolls the score. */\n host: HTMLElement;\n /** Display MusicXML to engrave (Task 1's transform-pipeline output — ids\n * already stamped). Ignored when `rendered` (the test seam) is set. */\n musicXml: string;\n /** Note onsets the playhead locks to, WITH the stamped ids of the notes\n * sounding at each onset — see `VerovioOnset`. Distinct/sorted\n * automatically (duplicates by `tMs` are merged, not required to be\n * pre-sorted). */\n onsets: VerovioOnset[];\n /** Playhead line color. Default `'#2f6f4f'`. */\n playheadColor?: string;\n /** Initial semantic zoom — see `verovioZoomOptions`'s doc for the mapping.\n * Default 1 (a normal readable size that fits the host's width). */\n zoom?: number;\n /**\n * Advanced / test seam: a pre-built `NotationLayout`, bypassing the real\n * Verovio engrave (`musicXml` is still required by the type but is\n * ignored when this is set). Mirrors `CreateSvgNotationPlayerOpts.rendered`\n * in notationPlayerSvg.ts for the identical reason: real Verovio rendering\n * needs a real SVG DOM (`getBBox`/`getBoundingClientRect`) that jsdom can't\n * provide — see notationPlayerSvg.ts's module doc for why headless can't do\n * a real one. When set, `noteIds`-based note lookups have no live DOM to\n * resolve against, so the playhead falls back to `vstackAudioPlayheadLine`'s\n * own ordinal spread (same graceful-degradation path as \"no `noteCols`\n * supplied\" on the SVG player) — fine for a lifecycle test, not for\n * notehead-accurate positioning. `setZoom`/`resize` update the tracked zoom\n * /width but perform no real re-engrave (there is nothing to re-engrave).\n */\n rendered?: NotationLayout;\n}\n\nexport interface VerovioNotationPlayer {\n /** Resolves once the engraving is in the DOM and ready to draw. `setTime`/\n * click hit-testing are safe to call before this resolves (no-op until\n * ready, same contract as the SVG player). */\n readonly ready: Promise<void>;\n /** Drive the playhead for absolute playback time `tMs`. Caller owns the\n * audio clock + rAF loop. */\n setTime(tMs: number): void;\n /** Re-engrave at a new semantic zoom (systems reflow — see\n * `verovioZoomOptions`). The scroll position is restored afterward so the\n * content that was centered in the viewport before the reflow is still\n * centered after it. */\n setZoom(z: number): Promise<void>;\n /** Re-measure the host and reflow to match its current width, with the\n * same scroll-position preservation as `setZoom`. Call on host resize /\n * orientation change. */\n resize(): Promise<void>;\n /** Register a measure-click handler (measure index, matching the DOM-order\n * index `verovioNotationLayout` assigns). Returns an unsubscribe fn. */\n onMeasureClick(cb: (measureIndex: number) => void): () => void;\n /** Tear down: removes the mounted DOM (engraving + playhead overlay) from\n * `host`, and drops every listener this instance added (click, window\n * scroll) and pending async work (a token guard drops any in-flight\n * reflow's effects). Idempotent. Does NOT destroy the shared module-level\n * Verovio toolkit instance (see the module doc's \"SHARED TOOLKIT\n * INSTANCE\" note) — it is reused by the next player, if any. */\n destroy(): void;\n}\n\n// ─── Verovio-backend geometry extraction (DOM-based) ───────────────────────\n\nconst EMPTY_LAYOUT: NotationLayout = {\n src: { x: 0, y: 0, w: 0, h: 0 },\n rect: { dx: 0, dy: 0, dw: 0, dh: 0 },\n systems: [],\n measures: [],\n};\n\n/** Per-page unit-conversion + placement: `cssPx = userUnit * scale`, plus\n * this page's own `(offsetX, offsetY)` within the shared root's coordinate\n * space (see the module doc's \"unit-conversion\" + \"PAGES\" sections). */\ninterface PageGeom {\n scale: number;\n offsetX: number;\n offsetY: number;\n}\n\nfunction safeRect(el: Element): DOMRect | null {\n return typeof el.getBoundingClientRect === 'function' ? el.getBoundingClientRect() : null;\n}\n\nfunction readViewBox(svgEl: SVGSVGElement): { w: number; h: number } | null {\n const baseVal = svgEl.viewBox && svgEl.viewBox.baseVal;\n if (baseVal && baseVal.width > 0) return { w: baseVal.width, h: baseVal.height };\n const attr = svgEl.getAttribute('viewBox');\n if (!attr) return null;\n const parts = attr.trim().split(/[\\s,]+/).map(Number);\n if (parts.length !== 4 || !(parts[2] > 0)) return null;\n return { w: parts[2], h: parts[3] };\n}\n\n/** One page's unit-conversion factor + placement offset, derived ENTIRELY\n * from the live DOM (Verovio's rendered SVG is self-describing — outer\n * `<svg width>` + inner `<svg viewBox>` — no external constant needed; see\n * the module doc). Returns null (never throws) if the page's markup is\n * missing the expected structure. */\nfunction pageGeometry(root: Element, pageEl: Element): PageGeom | null {\n const outerSvg = pageEl.querySelector('svg');\n if (!outerSvg) return null;\n const innerSvg = (outerSvg.querySelector('svg[viewBox]') ?? outerSvg) as unknown as SVGSVGElement;\n const vb = readViewBox(innerSvg);\n if (!vb) return null;\n const outerRect = safeRect(outerSvg);\n const rootRect = safeRect(root);\n const pageRect = safeRect(pageEl);\n if (!outerRect || !rootRect || !pageRect) return null;\n if (!(outerRect.width > 0)) return null;\n const scale = outerRect.width / vb.w;\n if (!(scale > 0) || !Number.isFinite(scale)) return null;\n return { scale, offsetX: pageRect.left - rootRect.left, offsetY: pageRect.top - rootRect.top };\n}\n\n/** `el.getBBox()` (Verovio's rendered user-unit space) converted to this\n * page's CSS px space (`scale` + `offsetX`/`offsetY`). Null for anything\n * that isn't a real `SVGGraphicsElement` or has a degenerate (zero-area)\n * box — same defensive style as `svgNotationLayout`'s box filtering. */\nfunction boxFromElement(el: Element, geom: PageGeom): Box | null {\n const ge = el as unknown as SVGGraphicsElement;\n if (typeof ge.getBBox !== 'function') return null;\n let bbox: DOMRect;\n try {\n bbox = ge.getBBox();\n } catch {\n return null;\n }\n if (!bbox || !(bbox.width > 0) || !(bbox.height > 0)) return null;\n return {\n x: geom.offsetX + bbox.x * geom.scale,\n y: geom.offsetY + bbox.y * geom.scale,\n w: bbox.width * geom.scale,\n h: bbox.height * geom.scale,\n };\n}\n\nfunction computePageGeometries(root: Element): Map<Element, PageGeom> {\n const map = new Map<Element, PageGeom>();\n for (const pageEl of Array.from(root.querySelectorAll('.vrv-page'))) {\n const geom = pageGeometry(root, pageEl);\n if (geom) map.set(pageEl, geom);\n }\n return map;\n}\n\n/**\n * Every `<g class=\"measure\">` across all rendered pages that has at least\n * one `<g class=\"staff\">` child, in DOCUMENT ORDER (= musical order, since\n * pages are stacked in `.vrv-page` DOM order and Verovio renders each page's\n * measures left-to-right/top-to-bottom). This exact list's POSITION is the\n * single source of truth for the `index` every measure/note lookup in this\n * module uses (`verovioNotationLayout` AND `verovioOnsetColumns` both call\n * this, so they can never drift out of sync with each other — no separate\n * re-derivation of \"which position is this measure\" anywhere else).\n */\nfunction collectMeasureElements(pageGeoms: Map<Element, PageGeom>): { el: Element; geom: PageGeom }[] {\n const out: { el: Element; geom: PageGeom }[] = [];\n for (const [pageEl, geom] of pageGeoms) {\n for (const measureEl of Array.from(pageEl.querySelectorAll('g.measure'))) {\n if (measureEl.querySelector('g.staff')) out.push({ el: measureEl, geom });\n }\n }\n return out;\n}\n\n/**\n * Pure(ish) — DOM-in, `NotationLayout`-out — Verovio-backend geometry\n * extractor. `root` is the container holding every rendered `.vrv-page`\n * wrapper `<div>` (this player's `svgHost`; a test fixture must reproduce\n * that same wrapper structure — see the extractor tests). Builds the SAME\n * `NotationLayout` shape `svgNotationLayout` (notationPlayerSvg.ts) does,\n * from Verovio's rendered SVG DOM instead of OSMD's object model — see the\n * module doc for the full derivation (measure/staff/system/note lookup via\n * Verovio's own stable SVG classes, unit conversion via the live\n * width/viewBox on each page). Never throws; returns `EMPTY_LAYOUT` on any\n * missing/malformed structure.\n */\nexport function verovioNotationLayout(root: Element): NotationLayout {\n try {\n const pageGeoms = computePageGeometries(root);\n if (!pageGeoms.size) return EMPTY_LAYOUT;\n const measureEntries = collectMeasureElements(pageGeoms);\n if (!measureEntries.length) return EMPTY_LAYOUT;\n\n const measures: StaffMeasureBox[] = [];\n measureEntries.forEach(({ el: measureEl, geom }, index) => {\n const staffEls = Array.from(measureEl.querySelectorAll('g.staff'));\n const staffBoxes = staffEls\n .map((el) => ({ el, box: boxFromElement(el, geom) }))\n .filter((s): s is { el: Element; box: Box } => !!s.box)\n .sort((a, b) => a.box.y - b.box.y);\n staffBoxes.forEach(({ el: staffEl, box }, staff) => {\n const noteEls = Array.from(measureEl.querySelectorAll('g.note')).filter(\n (n) => n.closest('g.staff') === staffEl,\n );\n const noteXs = noteEls.map((n) => boxFromElement(n, geom)?.x).filter((x): x is number => x != null);\n const noteStartX = noteXs.length ? Math.min(...noteXs) : box.x;\n measures.push({ index, staff, box, noteStartX });\n });\n });\n if (!measures.length) return EMPTY_LAYOUT;\n\n const systems: Box[] = [];\n for (const [pageEl, geom] of pageGeoms) {\n for (const sysEl of Array.from(pageEl.querySelectorAll('g.system'))) {\n const box = boxFromElement(sysEl, geom);\n if (box) systems.push(box);\n }\n }\n\n const rootRect = safeRect(root);\n const fallbackW = Math.max(0, ...measures.map((m) => m.box.x + m.box.w));\n const fallbackH = Math.max(0, ...measures.map((m) => m.box.y + m.box.h));\n const dw = rootRect && rootRect.width > 0 ? rootRect.width : fallbackW;\n const dh = rootRect && rootRect.height > 0 ? rootRect.height : fallbackH;\n\n return { src: { x: 0, y: 0, w: dw, h: dh }, rect: { dx: 0, dy: 0, dw, dh }, systems, measures };\n } catch {\n return EMPTY_LAYOUT;\n }\n}\n\n/**\n * Resolve each DISTINCT onset (see `distinctOnsets`) to a `noteCols` entry —\n * the SAME \"real engraved column\" format `vstackAudioPlayheadLine` already\n * accepts from the SVG/canvas players (`measureIndex + fractionWithinMeasure`\n * — see that function's doc for how it's consumed). For each onset, every\n * stamped `noteId` sounding at that instant (a chord may have several) is\n * looked up by id in the live DOM (`document.getElementById` — Task 1 stamps\n * ids that Verovio preserves verbatim as SVG element ids), mapped to its\n * measure via `collectMeasureElements`'s canonical indexing (so it lines up\n * EXACTLY with `verovioNotationLayout`'s own `index` numbering), and its\n * fractional x-position within that measure's column computed with the exact\n * same formula `vstackAudioPlayheadLine`'s own `colX` uses internally (not\n * re-derived independently — this is the note's ANCHOR position, not new\n * interpolation math). A chord's several ids are averaged. Any onset with NO\n * resolvable id (missing from the DOM, e.g. a `rendered`-seam layout with no\n * live SVG) falls back to the ordinal spread `vstackAudioPlayheadLine` itself\n * uses when `noteCols` is entirely absent — so one bad id degrades ONLY that\n * onset, not the whole piece. Returns `undefined` (not a partially-bad array)\n * only when there is no usable layout/DOM at all to resolve against.\n */\nexport function verovioOnsetColumns(\n root: Element,\n layout: NotationLayout,\n onsets: VerovioOnset[],\n): number[] | undefined {\n try {\n const distinctMs = distinctOnsets(onsets.map((o) => ({ onsetMs: o.tMs })));\n if (!distinctMs.length) return undefined;\n const cols = measureColumnsFromLayout(layout.measures);\n if (!cols.length) return undefined;\n\n const pageGeoms = computePageGeometries(root);\n const measureEntries = collectMeasureElements(pageGeoms);\n if (!measureEntries.length) return undefined;\n const measureIndexOf = new Map<Element, number>();\n measureEntries.forEach((m, i) => measureIndexOf.set(m.el, i));\n\n const idsByOnset = new Map<number, string[]>();\n for (const o of onsets) {\n const arr = idsByOnset.get(o.tMs) ?? [];\n for (const id of o.noteIds) if (!arr.includes(id)) arr.push(id);\n idsByOnset.set(o.tMs, arr);\n }\n\n const doc = root.ownerDocument;\n\n return distinctMs.map((tMs, k) => {\n const ids = idsByOnset.get(tMs) ?? [];\n const positions: number[] = [];\n for (const id of ids) {\n const noteEl = doc ? doc.getElementById(id) : null;\n const measureEl = noteEl ? noteEl.closest('g.measure') : null;\n const index = measureEl ? measureIndexOf.get(measureEl) : undefined;\n if (index == null) continue;\n const m = cols[index];\n const geom = measureEntries[index]?.geom;\n const box = noteEl && geom ? boxFromElement(noteEl, geom) : null;\n if (!box || !m) continue;\n const sx = Math.min(m.noteStartX, m.x + m.w);\n const denom = m.x + m.w - sx;\n const frac = denom > 0 ? Math.min(1, Math.max(0, (box.x - sx) / denom)) : 0;\n positions.push(index + frac);\n }\n if (positions.length) return positions.reduce((a, b) => a + b, 0) / positions.length;\n // Same ordinal-spread fallback vstackAudioPlayheadLine uses internally\n // when no noteCols are supplied at all — degrades this ONE onset only.\n return distinctMs.length === 1 ? 0 : (k / (distinctMs.length - 1)) * cols.length;\n });\n } catch {\n return undefined;\n }\n}\n\n// ─── Semantic zoom mapping ──────────────────────────────────────────────────\n\n/** Verovio glyph-size percent at semantic zoom 1 (matches the migration\n * spike's own default — a normal, readable size at typical host widths). */\nexport const VEROVIO_BASE_SCALE = 40;\n/** Clamp band for the derived `scale`, so an extreme `zoom` never collapses\n * glyphs to unreadable or blows them up past sane bounds. */\nexport const VEROVIO_MIN_SCALE = 20;\nexport const VEROVIO_MAX_SCALE = 120;\n\nexport interface VerovioRenderOptions {\n scale: number;\n pageWidth: number;\n}\n\n/**\n * Semantic zoom → Verovio options. The migration spike's `zoom-test*.mjs`\n * proved `pageWidth` (Verovio's line-breaking width, in ITS OWN units)\n * drives measures-per-system while `scale` (glyph size) alone does NOT\n * (avgMeasuresPerSystem was IDENTICAL — 3.61 — across scale 40/80/150 at a\n * fixed pageWidth 1600; it moved 2.24→3.61→5.91 as pageWidth alone rose\n * 1000→1600→2400 at a fixed scale). Verovio's own rendered SVG width in CSS\n * px is EXACTLY `pageWidth * scale / 100` (confirmed empirically against\n * real 6.2.0 renders) — an identity, not an approximation.\n *\n * This function picks `scale` proportional to `zoom` (bigger zoom ⇒ bigger\n * glyphs) and then SOLVES that identity for the `pageWidth` that makes the\n * rendered width land EXACTLY on `hostWidthPx` regardless of zoom\n * (`pageWidth = hostWidthPx * 100 / scale`). Composing it this way — instead\n * of tuning `scale` and `pageWidth` independently — makes BOTH halves of the\n * spec's semantic fall out of the ONE formula: as `zoom` rises, `scale`\n * rises (glyphs bigger) AND the required `pageWidth` (in Verovio units)\n * SHRINKS proportionally (since it's inversely proportional to `scale` at a\n * fixed target width) — and per the spike's own finding, a smaller\n * `pageWidth` fits FEWER measures per system. So \"bigger zoom ⇒ fewer\n * measures/system, glyphs larger, width still fits host\" (design doc §2) is\n * a direct consequence of this one identity, pinned by\n * `tests/notationPlayerVerovio.test.ts`'s real-Verovio monotonicity test.\n *\n * No floor on `pageWidth` beyond what the identity itself produces: `scale`\n * is already clamped to `[VEROVIO_MIN_SCALE, VEROVIO_MAX_SCALE]` (both > 0)\n * and `hostWidthPx` is floored to a sane fallback when invalid, so\n * `pageWidth = w * 100 / scale` is ALWAYS finite and positive — an\n * additional floor would only ever fire by breaking the width-fits-host\n * identity (clamping the OUTPUT width away from the host's actual width),\n * which is worse than a small `pageWidth`.\n */\nexport function verovioZoomOptions(hostWidthPx: number, zoom: number): VerovioRenderOptions {\n const z = Number.isFinite(zoom) && zoom > 0 ? zoom : 1;\n const scale = Math.max(VEROVIO_MIN_SCALE, Math.min(VEROVIO_MAX_SCALE, VEROVIO_BASE_SCALE * z));\n const w = Number.isFinite(hostWidthPx) && hostWidthPx > 0 ? hostWidthPx : MAX_ENGRAVE_WIDTH_VRV;\n const pageWidth = (w * 100) / scale;\n return { scale, pageWidth };\n}\n\n// ─── Shared module-level toolkit (see the module doc's \"SHARED TOOLKIT\n// INSTANCE\" section) ─────────────────────────────────────────────────\n\n/** The subset of `VerovioToolkit`'s instance API this module calls — see\n * `src/types/verovio.d.ts` for the ambient module declaration backing the\n * dynamic imports below (the `verovio` npm package ships no types of its\n * own). Deliberately duck-typed/local rather than importing the real class\n * type statically, so nothing about this module's own type-checking\n * depends on a STATIC import of `verovio/esm` (only the dynamic one inside\n * `getVerovioToolkit` touches the package at all, which is what tsup's\n * `external` + the build-audit grep gate verify). */\ninterface VerovioToolkitInstance {\n loadData(data: string): boolean;\n getPageCount(): number;\n renderToSVG(page: number): string;\n setOptions(options: Record<string, unknown>): void;\n redoLayout(options?: Record<string, unknown>): void;\n}\n\nlet toolkitPromise: Promise<VerovioToolkitInstance> | null = null;\n\nfunction getVerovioToolkit(): Promise<VerovioToolkitInstance> {\n if (!toolkitPromise) {\n toolkitPromise = (async () => {\n // Literal dynamic imports — consumers' bundlers must statically see\n // these specifiers to code-split Verovio out of every OTHER dist\n // entry (tsup marks `verovio` external — see tsup.config.ts + the\n // build report's dist-grep evidence). A caller that only ever uses\n // the `rendered` test seam never pulls Verovio in at all.\n const [{ default: createVerovioModule }, { VerovioToolkit }] = await Promise.all([\n import('verovio/wasm'),\n import('verovio/esm'),\n ]);\n const VerovioModule = await createVerovioModule();\n return new VerovioToolkit(VerovioModule) as unknown as VerovioToolkitInstance;\n })();\n }\n return toolkitPromise;\n}\n\n// ─── createVerovioNotationPlayer ────────────────────────────────────────────\n\nconst DEFAULT_PLAYHEAD_COLOR = '#2f6f4f';\n/** Engrave-width ceiling, CSS px — same policy as notationPlayerSvg.ts's\n * `MAX_ENGRAVE_WIDTH_SVG` (design doc §\"Render\": \"cap ~1200px\"), restated\n * independently since the two players' constants are independently\n * tunable. */\nexport const MAX_ENGRAVE_WIDTH_VRV = 1200;\n/** Floor so a not-yet-laid-out / zero-width host never engraves at 0px. */\nconst MIN_ENGRAVE_WIDTH_VRV = 280;\n\n/** Build a live, interactive Verovio (vector) notation player. See the\n * module doc + `docs/superpowers/specs/2026-08-11-verovio-player-design.md`\n * (in stave-web-sightread) §2 for the full design. */\nexport function createVerovioNotationPlayer(opts: CreateVerovioNotationPlayerOpts): VerovioNotationPlayer {\n const { host, musicXml, onsets: rawOnsets } = opts;\n const playheadColor = opts.playheadColor ?? DEFAULT_PLAYHEAD_COLOR;\n const onsets = distinctOnsets(rawOnsets.map((o) => ({ onsetMs: o.tMs })));\n\n const root = document.createElement('div');\n root.style.position = 'relative';\n root.style.width = '100%';\n // Same \"optical zoom is free\" reasoning as notationPlayerSvg.ts — let\n // native pinch-zoom + vertical page-scroll gestures through unimpeded.\n root.style.touchAction = 'pan-y pinch-zoom';\n host.appendChild(root);\n\n const svgHost = document.createElement('div');\n root.appendChild(svgHost);\n\n const playheadEl = document.createElement('div');\n playheadEl.style.position = 'absolute';\n playheadEl.style.left = '0px';\n playheadEl.style.top = '0px';\n playheadEl.style.width = '2px';\n playheadEl.style.height = '0px';\n playheadEl.style.background = playheadColor;\n playheadEl.style.opacity = '0';\n playheadEl.style.pointerEvents = 'none';\n root.appendChild(playheadEl);\n\n let currentLayout: NotationLayout | null = null;\n let currentNoteCols: number[] | undefined;\n let currentZoom = opts.zoom ?? 1;\n let lastEngravedWidthPx = 0;\n let loaded = false; // toolkit.loadData succeeded (rendered-seam path never sets this)\n let destroyed = false;\n let lastTMs = 0;\n // Stale-reflow guard — same \"each async re-engrave captures its own\n // token\" rule as notationPlayerSvg.ts's `rebuildToken`.\n let rebuildToken = 0;\n\n function desiredEngraveWidthPx(): number {\n const w = host.clientWidth || 0;\n return Math.max(MIN_ENGRAVE_WIDTH_VRV, Math.min(w || MAX_ENGRAVE_WIDTH_VRV, MAX_ENGRAVE_WIDTH_VRV));\n }\n\n // ─── Playhead + auto-follow (createFollowController — notationCommon.ts) ─\n\n const follow = createFollowController();\n\n function renderPlayhead(tMs: number): void {\n lastTMs = tMs;\n if (!currentLayout) return;\n const nBars = currentLayout.measures.length\n ? Math.max(...currentLayout.measures.map((m) => m.index)) + 1\n : 0;\n const line = vstackAudioPlayheadLine(currentLayout, onsets, tMs, nBars, currentNoteCols);\n if (!line) {\n playheadEl.style.opacity = '0';\n return;\n }\n playheadEl.style.opacity = String(line.alpha);\n playheadEl.style.left = `${line.x}px`;\n playheadEl.style.top = `${line.y0}px`;\n playheadEl.style.height = `${Math.max(0, line.y1 - line.y0)}px`;\n const layoutForFollow = currentLayout;\n const sys = line.sys ?? -1;\n follow.follow({\n systemIndex: sys,\n getSystemRect: () => {\n const box = layoutForFollow.systems[sys];\n if (!box) return null;\n const rootRect = safeRect(root);\n if (!rootRect) return null;\n return { top: rootRect.top + box.y, bottom: rootRect.top + box.y + box.h };\n },\n getPlayheadRect: () => safeRect(playheadEl),\n });\n }\n\n function setTime(tMs: number): void {\n if (destroyed) return;\n follow.onSetTime(tMs);\n renderPlayhead(tMs);\n }\n\n // ─── Engrave / reflow ──────────────────────────────────────────────────\n\n function rebuildLayoutFromDom(): void {\n currentLayout = verovioNotationLayout(svgHost);\n currentNoteCols = verovioOnsetColumns(svgHost, currentLayout, rawOnsets);\n }\n\n function renderAllPages(toolkit: VerovioToolkitInstance): void {\n svgHost.replaceChildren();\n const pageCount = Math.max(0, toolkit.getPageCount());\n for (let p = 1; p <= pageCount; p++) {\n const pageDiv = document.createElement('div');\n pageDiv.className = 'vrv-page';\n pageDiv.innerHTML = toolkit.renderToSVG(p);\n svgHost.appendChild(pageDiv);\n }\n }\n\n async function initialEngrave(): Promise<void> {\n if (opts.rendered) {\n currentLayout = opts.rendered;\n lastEngravedWidthPx = desiredEngraveWidthPx();\n renderPlayhead(lastTMs);\n return;\n }\n\n const toolkit = await getVerovioToolkit();\n if (destroyed) return;\n\n const widthPx = desiredEngraveWidthPx();\n const { scale, pageWidth } = verovioZoomOptions(widthPx, currentZoom);\n toolkit.setOptions({ scale, pageWidth, breaks: 'auto', adjustPageHeight: true });\n loaded = !!toolkit.loadData(musicXml);\n if (destroyed) return;\n\n lastEngravedWidthPx = widthPx;\n if (loaded) renderAllPages(toolkit);\n rebuildLayoutFromDom();\n renderPlayhead(lastTMs);\n }\n\n const ready = initialEngrave();\n\n /** Re-engrave at `newZoom` and the host's CURRENT width, preserving the\n * scroll position of whatever content is centered in the viewport right\n * now. Shared by both `setZoom` and `resize` — same shape as\n * notationPlayerSvg.ts's `reflow`. No-op when neither the width nor the\n * zoom actually changed, or when using the `rendered` test seam / the\n * initial load never succeeded (nothing to re-engrave). */\n async function reflow(newZoom: number): Promise<void> {\n if (destroyed) return;\n const widthPx = desiredEngraveWidthPx();\n if (widthPx === lastEngravedWidthPx && newZoom === currentZoom) return;\n if (opts.rendered || !loaded) {\n currentZoom = newZoom;\n return;\n }\n\n const toolkit = await getVerovioToolkit();\n if (destroyed) return;\n\n const myToken = ++rebuildToken;\n const oldLayout = currentLayout;\n const hasWin = typeof window !== 'undefined';\n let anchorY: number | null = null;\n const anchorX = widthPx / 2;\n if (hasWin && typeof root.getBoundingClientRect === 'function') {\n const r = root.getBoundingClientRect();\n anchorY = window.innerHeight / 2 - r.top;\n }\n\n const { scale, pageWidth } = verovioZoomOptions(widthPx, newZoom);\n toolkit.setOptions({ scale, pageWidth, breaks: 'auto', adjustPageHeight: true });\n // RECLAIM (see the module doc's \"SHARED TOOLKIT INSTANCE\" note): the\n // module-level toolkit is shared across every live player on the page —\n // and the real consumer (PlayerPage) keeps a harmony player AND a\n // written player alive SIMULTANEOUSLY (lazy-built, destroyed only on\n // piece switch), so `loadData` calls from the two players genuinely\n // interleave (e.g. harmony active -> resize while written hidden -> back\n // to written -> writtenPlayer.setZoom()). A plain `redoLayout()` here\n // would silently re-lay-out and render WHATEVER document the toolkit\n // currently holds — which may belong to the OTHER player if it rendered\n // more recently. So every reflow re-parses THIS player's own `musicXml`\n // synchronously, immediately before rendering (loadData does a full\n // parse + layout with the just-set options, making a separate\n // `redoLayout()` call redundant). Because this call and `renderAllPages`\n // below are both synchronous with NO `await` between them, and the only\n // preceding `await` (`getVerovioToolkit()`) already resolved, JS's\n // single-threaded run-to-completion semantics guarantee no other\n // player's reflow/initialEngrave can interleave between the reclaim and\n // the render — the shared singleton is therefore safe under alternating\n // use. See `tests/notationPlayerVerovio.test.ts`'s two-player\n // interleaved-reflow test for the regression this guards against.\n const ok = toolkit.loadData(musicXml);\n if (destroyed || myToken !== rebuildToken) return;\n if (!ok) {\n loaded = false;\n return;\n }\n\n lastEngravedWidthPx = widthPx;\n currentZoom = newZoom;\n renderAllPages(toolkit);\n rebuildLayoutFromDom();\n\n if (oldLayout && currentLayout && anchorY != null && hasWin) {\n const delta = computeReflowScrollDelta(oldLayout, currentLayout, anchorX, anchorY);\n if (Number.isFinite(delta) && Math.abs(delta) > 0.5) {\n window.scrollTo({ top: Math.max(0, window.scrollY + delta), left: window.scrollX, behavior: 'auto' });\n }\n }\n if (!destroyed) renderPlayhead(lastTMs);\n }\n\n // ─── Click-to-seek ─────────────────────────────────────────────────────\n\n const clickListeners: Array<(measureIndex: number) => void> = [];\n function onRootClick(e: MouseEvent): void {\n if (!currentLayout) return;\n const rect = root.getBoundingClientRect();\n const mx = e.clientX - rect.left;\n const my = e.clientY - rect.top;\n const idx = hitTestMeasureAt(currentLayout, mx, my);\n if (idx != null) for (const cb of clickListeners) cb(idx);\n }\n root.addEventListener('click', onRootClick);\n\n return {\n ready,\n setTime,\n async setZoom(z: number): Promise<void> {\n await ready;\n await reflow(z);\n },\n async resize(): Promise<void> {\n await ready;\n await reflow(currentZoom);\n },\n onMeasureClick(cb: (measureIndex: number) => void): () => void {\n clickListeners.push(cb);\n return () => {\n const i = clickListeners.indexOf(cb);\n if (i >= 0) clickListeners.splice(i, 1);\n };\n },\n destroy(): void {\n if (destroyed) return;\n destroyed = true;\n rebuildToken++;\n root.removeEventListener('click', onRootClick);\n follow.destroy();\n clickListeners.length = 0;\n currentLayout = null;\n currentNoteCols = undefined;\n if (root.parentNode === host) host.removeChild(root);\n },\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/notationPlayerVerovio.ts"],"sourcesContent":["// createVerovioNotationPlayer — the Verovio (vector) sibling of\n// createSvgNotationPlayer (notationPlayerSvg.ts). Same job (a live,\n// caller-driven notation + gliding-playhead widget) and the same swap-friendly\n// API shape, different rendering engine: Verovio's own MusicXML→SVG engraver\n// instead of OSMD. See\n// docs/superpowers/specs/2026-08-11-verovio-player-design.md (stave-web-sightread)\n// §2 for the full design rationale.\n//\n// THE BINDING REFRAME (design doc, top): \"Timing is ours; Verovio renders.\"\n// This module NEVER calls `renderToTimemap` or `getElementsAtTime` — Verovio's\n// timemap is proven wrong on tuplets (the root-cause finding that motivated\n// this whole migration). All timing (`onsets`) is supplied by the caller,\n// derived from `parseReduction`'s spans/offsets; this module's only job is\n// SVG + id→geometry, exactly like notationPlayerSvg.ts's job is OSMD SVG +\n// id→geometry. (Grep gate — see the build report.)\n//\n// REUSED, VERBATIM, NO NEW MATH (same hard rule as notationPlayerSvg.ts):\n// - `vstackAudioPlayheadLine` (scene/notationGeometry.ts) — the exact same\n// onset-anchored, carriage-return playhead interpolation both SVG-family\n// players use. It only ever reads a `NotationLayout`; it does not care\n// that THIS module's layout came from Verovio's rendered SVG DOM instead\n// of OSMD's `GraphicalMusicSheet` object model.\n// - `hitTestMeasureAt`, `measureColumnsFromLayout`, `distinctOnsets`\n// (scene/notationGeometry.ts) — reused as-is, identical to\n// notationPlayerSvg.ts's usage.\n// - `computeReflowScrollDelta` + the auto-follow discriminator\n// (`createFollowController`) — EXTRACTED (0.39.0) out of\n// notationPlayerSvg.ts into `./notationCommon`, so both SVG-family\n// players share one implementation. See that module's doc.\n//\n// WHAT'S NEW (not a re-derivation of any of the above):\n// - `verovioNotationLayout` — a Verovio-backend geometry extractor, this\n// module's counterpart to notationPlayerSvg.ts's `svgNotationLayout`.\n// Verovio exposes NO object model to JS (unlike OSMD's `GraphicSheet`) —\n// only rendered SVG + MEI/timemap (timemap being off-limits per the\n// binding reframe above) — so this reads the ACTUAL rendered SVG DOM:\n// `<g class=\"measure\">` / `<g class=\"staff\">` / `<g class=\"system\">` /\n// `<g class=\"note\">` are Verovio's own stable, documented SVG output\n// classes (confirmed against a real 6.2.0 render — see the migration\n// spike's `id-test*.mjs`). Measure `index` is assigned by DOCUMENT ORDER\n// (musical order) across however many pages were rendered — no reliance\n// on Verovio's own (internal, non-deterministic) generated ids.\n// - Stamped-id note lookups (`verovioOnsetColumns`): Task 1 (stave repo)\n// stamps a deterministic `xml:id` per note before handing MusicXML to\n// Verovio; Verovio PRESERVES caller-supplied ids as the rendered SVG\n// element's own `id` attribute (confirmed: `<note id=\"n-0-0-0\">` in the\n// source round-trips to `<g id=\"n-0-0-0\" class=\"note\">` in the output —\n// an EXACT lookup, no heuristics, unlike the ordinal-spread fallback\n// `vstackAudioPlayheadLine` uses when no `noteCols` are supplied at all).\n// - `verovioZoomOptions` — the semantic zoom→Verovio-options mapping. The\n// migration spike's `zoom-test*.mjs` proved `pageWidth` (Verovio's\n// line-breaking width, in ITS OWN units) drives measures-per-system\n// while `scale` (glyph size) alone does NOT (avgMeasuresPerSystem stayed\n// 3.61 across scale 40/80/150 at a fixed pageWidth; it moved from 2.24 to\n// 3.61 to 5.91 as pageWidth alone rose 1000→1600→2400). Verovio's\n// rendered SVG width in CSS px is EXACTLY `pageWidth * scale / 100`\n// (confirmed empirically) — so solving that identity for `pageWidth`\n// given a TARGET output width (the host's width, held fixed across zoom\n// levels) and a zoom-driven `scale` makes both halves of the semantic\n// (\"bigger zoom ⇒ bigger glyphs AND fewer measures/system, width still\n// fits host\") fall out of ONE formula, not two independently-tuned ones.\n// - The unit-conversion for any element's `getBBox()` (Verovio's rendered\n// SVG user-unit space) → CSS px: each rendered page is\n// `<svg width=\"Wpx\" height=\"Hpx\">` (no viewBox) wrapping a nested\n// `<svg class=\"definition-scale\" viewBox=\"0 0 VBW VBH\">` (Verovio's own\n// structure) — so `cssPx = userUnit * (W / VBW)`, the SAME \"read the\n// scale factor from the live render, never hardcode it\" principle\n// `svgNotationLayout`'s `unitInPixels` derivation uses, just sourced from\n// the DOM (Verovio exposes no JS-side unit constant) instead of an\n// imported library constant.\n//\n// PAGES: \"all rendered, stacked in flow\" (design doc §2) — every Verovio\n// page (`getPageCount()`) is rendered to its own `<svg>` and appended, in\n// order, inside its own `.vrv-page` wrapper `<div>`, inside `svgHost`. Normal\n// block layout stacks them vertically; `verovioNotationLayout` reads each\n// page's OWN offset (`getBoundingClientRect()` relative to the shared root)\n// so geometry from every page lands in ONE continuous coordinate space, the\n// same space the playhead overlay is positioned in. No \"current page\" /\n// pagination concept anywhere in this module — the whole score is always in\n// the DOM; the PAGE scrolls it (identical framing to notationPlayerSvg.ts).\n//\n// SHARED TOOLKIT INSTANCE: Verovio's own package doc: \"only one instance can\n// be created for now\" (`VerovioToolkit.instances`, a static array the C++\n// bridge expects to hold at most one live toolkit). This module therefore\n// keeps ONE module-level toolkit init promise for the whole session — every\n// player created on the page shares it. This is NOT a \"one live player at a\n// time\" assumption — the real consumer (PlayerPage) keeps a harmony player\n// AND a written player alive SIMULTANEOUSLY (lazy-built, destroyed only on\n// piece switch), so two players' `loadData` calls genuinely interleave over\n// the toolkit's lifetime. Safety comes from RECLAIM: every toolkit-consuming\n// path (initial engrave; `reflow`, shared by `setZoom`/`resize`) re-parses\n// ITS OWN `musicXml` via `loadData` synchronously, immediately before\n// rendering — never assumes the toolkit still holds what it loaded last\n// time. Because the reclaim call and the render calls that follow it have no\n// `await` between them, JS's single-threaded run-to-completion semantics\n// guarantee no other player's reflow can interleave mid-sequence — see\n// `reflow`'s own comment + `tests/notationPlayerVerovio.test.ts`'s\n// two-player interleaved-reflow test (the regression this guards against).\n//\n// IMPORT PATH: subpath-only —\n// `@real-music-packages/web-core/notationPlayerVerovio` — not re-exported\n// from the root barrel (same reasoning as notationPlayer.ts/\n// notationPlayerSvg.ts: the root barrel is theory-only/zero-dependency).\n// `verovio` itself is a dynamic `import('verovio/wasm')` /\n// `import('verovio/esm')` INSIDE this module only, marked `external` in\n// tsup.config.ts, so the ~2.3MB gzip WASM only loads on pages that actually\n// construct a player — see the build report's dist-grep evidence.\n\nimport {\n vstackAudioPlayheadLine,\n distinctOnsets,\n hitTestMeasureAt,\n measureColumnsFromLayout,\n type NotationLayout,\n} from './scene/notationGeometry';\nimport type { Box, StaffMeasureBox } from './promo';\nimport { computeReflowScrollDelta, createFollowController } from './notationCommon';\n\n// ─── Public types ───────────────────────────────────────────────────────────\n\n/** One distinct note-onset instant: the audio-clock time it sounds at, and\n * the stamped `xml:id`s (Task 1, stave repo) of every note that sounds at\n * that instant (>1 for a chord). Multiple entries sharing the same `tMs`\n * are merged (their `noteIds` unioned) — the caller does not need to\n * pre-group chords into one entry. */\nexport interface VerovioOnset {\n tMs: number;\n noteIds: string[];\n}\n\nexport interface CreateVerovioNotationPlayerOpts {\n /** Element the player's content is mounted into. Takes NATURAL content\n * height (the whole score, page-flow layout, ALL Verovio pages stacked)\n * — the host must not clip a fixed height; the PAGE scrolls the score. */\n host: HTMLElement;\n /** Display MusicXML to engrave (Task 1's transform-pipeline output — ids\n * already stamped). Ignored when `rendered` (the test seam) is set. */\n musicXml: string;\n /** Note onsets the playhead locks to, WITH the stamped ids of the notes\n * sounding at each onset — see `VerovioOnset`. Distinct/sorted\n * automatically (duplicates by `tMs` are merged, not required to be\n * pre-sorted). */\n onsets: VerovioOnset[];\n /** Playhead line color. Default `'#2f6f4f'`. */\n playheadColor?: string;\n /** Initial semantic zoom — see `verovioZoomOptions`'s doc for the mapping.\n * Default 1 (a normal readable size that fits the host's width). */\n zoom?: number;\n /**\n * Advanced / test seam: a pre-built `NotationLayout`, bypassing the real\n * Verovio engrave (`musicXml` is still required by the type but is\n * ignored when this is set). Mirrors `CreateSvgNotationPlayerOpts.rendered`\n * in notationPlayerSvg.ts for the identical reason: real Verovio rendering\n * needs a real SVG DOM (`getBBox`/`getBoundingClientRect`) that jsdom can't\n * provide — see notationPlayerSvg.ts's module doc for why headless can't do\n * a real one. When set, `noteIds`-based note lookups have no live DOM to\n * resolve against, so the playhead falls back to `vstackAudioPlayheadLine`'s\n * own ordinal spread (same graceful-degradation path as \"no `noteCols`\n * supplied\" on the SVG player) — fine for a lifecycle test, not for\n * notehead-accurate positioning. `setZoom`/`resize` update the tracked zoom\n * /width but perform no real re-engrave (there is nothing to re-engrave).\n */\n rendered?: NotationLayout;\n}\n\nexport interface VerovioNotationPlayer {\n /** Resolves once the engraving is in the DOM and ready to draw. `setTime`/\n * click hit-testing are safe to call before this resolves (no-op until\n * ready, same contract as the SVG player). */\n readonly ready: Promise<void>;\n /** Drive the playhead for absolute playback time `tMs`. Caller owns the\n * audio clock + rAF loop. */\n setTime(tMs: number): void;\n /** Re-engrave at a new semantic zoom (systems reflow — see\n * `verovioZoomOptions`). The scroll position is restored afterward so the\n * content that was centered in the viewport before the reflow is still\n * centered after it. */\n setZoom(z: number): Promise<void>;\n /** Re-measure the host and reflow to match its current width, with the\n * same scroll-position preservation as `setZoom`. Call on host resize /\n * orientation change. */\n resize(): Promise<void>;\n /** Gate the auto-follow scroll on the app's transport state: pass `true`\n * on play, `false` on pause/stop (see FollowController.setEnabled — while\n * disabled NOTHING may scroll the sheet, including the idle-rearm's\n * off-screen rescue). Defaults to enabled. */\n setFollowEnabled(on: boolean): void;\n /** Register a measure-click handler (measure index, matching the DOM-order\n * index `verovioNotationLayout` assigns). Returns an unsubscribe fn. */\n onMeasureClick(cb: (measureIndex: number) => void): () => void;\n /** Tear down: removes the mounted DOM (engraving + playhead overlay) from\n * `host`, and drops every listener this instance added (click, window\n * scroll) and pending async work (a token guard drops any in-flight\n * reflow's effects). Idempotent. Does NOT destroy the shared module-level\n * Verovio toolkit instance (see the module doc's \"SHARED TOOLKIT\n * INSTANCE\" note) — it is reused by the next player, if any. */\n destroy(): void;\n}\n\n// ─── Verovio-backend geometry extraction (DOM-based) ───────────────────────\n\nconst EMPTY_LAYOUT: NotationLayout = {\n src: { x: 0, y: 0, w: 0, h: 0 },\n rect: { dx: 0, dy: 0, dw: 0, dh: 0 },\n systems: [],\n measures: [],\n};\n\n/** Per-page unit-conversion + placement: `cssPx = userUnit * scale`, plus\n * this page's own `(offsetX, offsetY)` within the shared root's coordinate\n * space (see the module doc's \"unit-conversion\" + \"PAGES\" sections). */\ninterface PageGeom {\n scale: number;\n offsetX: number;\n offsetY: number;\n}\n\nfunction safeRect(el: Element): DOMRect | null {\n return typeof el.getBoundingClientRect === 'function' ? el.getBoundingClientRect() : null;\n}\n\nfunction readViewBox(svgEl: SVGSVGElement): { w: number; h: number } | null {\n const baseVal = svgEl.viewBox && svgEl.viewBox.baseVal;\n if (baseVal && baseVal.width > 0) return { w: baseVal.width, h: baseVal.height };\n const attr = svgEl.getAttribute('viewBox');\n if (!attr) return null;\n const parts = attr.trim().split(/[\\s,]+/).map(Number);\n if (parts.length !== 4 || !(parts[2] > 0)) return null;\n return { w: parts[2], h: parts[3] };\n}\n\n/** One page's unit-conversion factor + placement offset, derived ENTIRELY\n * from the live DOM (Verovio's rendered SVG is self-describing — outer\n * `<svg width>` + inner `<svg viewBox>` — no external constant needed; see\n * the module doc). Returns null (never throws) if the page's markup is\n * missing the expected structure. */\nfunction pageGeometry(root: Element, pageEl: Element): PageGeom | null {\n const outerSvg = pageEl.querySelector('svg');\n if (!outerSvg) return null;\n const innerSvg = (outerSvg.querySelector('svg[viewBox]') ?? outerSvg) as unknown as SVGSVGElement;\n const vb = readViewBox(innerSvg);\n if (!vb) return null;\n const outerRect = safeRect(outerSvg);\n const rootRect = safeRect(root);\n const pageRect = safeRect(pageEl);\n if (!outerRect || !rootRect || !pageRect) return null;\n if (!(outerRect.width > 0)) return null;\n const scale = outerRect.width / vb.w;\n if (!(scale > 0) || !Number.isFinite(scale)) return null;\n return { scale, offsetX: pageRect.left - rootRect.left, offsetY: pageRect.top - rootRect.top };\n}\n\n/** `el.getBBox()` (Verovio's rendered user-unit space) converted to this\n * page's CSS px space (`scale` + `offsetX`/`offsetY`). Null for anything\n * that isn't a real `SVGGraphicsElement` or has a degenerate (zero-area)\n * box — same defensive style as `svgNotationLayout`'s box filtering. */\nfunction boxFromElement(el: Element, geom: PageGeom): Box | null {\n const ge = el as unknown as SVGGraphicsElement;\n if (typeof ge.getBBox !== 'function') return null;\n let bbox: DOMRect;\n try {\n bbox = ge.getBBox();\n } catch {\n return null;\n }\n if (!bbox || !(bbox.width > 0) || !(bbox.height > 0)) return null;\n return {\n x: geom.offsetX + bbox.x * geom.scale,\n y: geom.offsetY + bbox.y * geom.scale,\n w: bbox.width * geom.scale,\n h: bbox.height * geom.scale,\n };\n}\n\nfunction computePageGeometries(root: Element): Map<Element, PageGeom> {\n const map = new Map<Element, PageGeom>();\n for (const pageEl of Array.from(root.querySelectorAll('.vrv-page'))) {\n const geom = pageGeometry(root, pageEl);\n if (geom) map.set(pageEl, geom);\n }\n return map;\n}\n\n/**\n * Every `<g class=\"measure\">` across all rendered pages that has at least\n * one `<g class=\"staff\">` child, in DOCUMENT ORDER (= musical order, since\n * pages are stacked in `.vrv-page` DOM order and Verovio renders each page's\n * measures left-to-right/top-to-bottom). This exact list's POSITION is the\n * single source of truth for the `index` every measure/note lookup in this\n * module uses (`verovioNotationLayout` AND `verovioOnsetColumns` both call\n * this, so they can never drift out of sync with each other — no separate\n * re-derivation of \"which position is this measure\" anywhere else).\n */\nfunction collectMeasureElements(pageGeoms: Map<Element, PageGeom>): { el: Element; geom: PageGeom }[] {\n const out: { el: Element; geom: PageGeom }[] = [];\n for (const [pageEl, geom] of pageGeoms) {\n for (const measureEl of Array.from(pageEl.querySelectorAll('g.measure'))) {\n if (measureEl.querySelector('g.staff')) out.push({ el: measureEl, geom });\n }\n }\n return out;\n}\n\n/**\n * Pure(ish) — DOM-in, `NotationLayout`-out — Verovio-backend geometry\n * extractor. `root` is the container holding every rendered `.vrv-page`\n * wrapper `<div>` (this player's `svgHost`; a test fixture must reproduce\n * that same wrapper structure — see the extractor tests). Builds the SAME\n * `NotationLayout` shape `svgNotationLayout` (notationPlayerSvg.ts) does,\n * from Verovio's rendered SVG DOM instead of OSMD's object model — see the\n * module doc for the full derivation (measure/staff/system/note lookup via\n * Verovio's own stable SVG classes, unit conversion via the live\n * width/viewBox on each page). Never throws; returns `EMPTY_LAYOUT` on any\n * missing/malformed structure.\n */\nexport function verovioNotationLayout(root: Element): NotationLayout {\n try {\n const pageGeoms = computePageGeometries(root);\n if (!pageGeoms.size) return EMPTY_LAYOUT;\n const measureEntries = collectMeasureElements(pageGeoms);\n if (!measureEntries.length) return EMPTY_LAYOUT;\n\n const measures: StaffMeasureBox[] = [];\n measureEntries.forEach(({ el: measureEl, geom }, index) => {\n const staffEls = Array.from(measureEl.querySelectorAll('g.staff'));\n const staffBoxes = staffEls\n .map((el) => ({ el, box: boxFromElement(el, geom) }))\n .filter((s): s is { el: Element; box: Box } => !!s.box)\n .sort((a, b) => a.box.y - b.box.y);\n staffBoxes.forEach(({ el: staffEl, box }, staff) => {\n const noteEls = Array.from(measureEl.querySelectorAll('g.note')).filter(\n (n) => n.closest('g.staff') === staffEl,\n );\n const noteXs = noteEls.map((n) => boxFromElement(n, geom)?.x).filter((x): x is number => x != null);\n const noteStartX = noteXs.length ? Math.min(...noteXs) : box.x;\n measures.push({ index, staff, box, noteStartX });\n });\n });\n if (!measures.length) return EMPTY_LAYOUT;\n\n const systems: Box[] = [];\n for (const [pageEl, geom] of pageGeoms) {\n for (const sysEl of Array.from(pageEl.querySelectorAll('g.system'))) {\n const box = boxFromElement(sysEl, geom);\n if (box) systems.push(box);\n }\n }\n\n const rootRect = safeRect(root);\n const fallbackW = Math.max(0, ...measures.map((m) => m.box.x + m.box.w));\n const fallbackH = Math.max(0, ...measures.map((m) => m.box.y + m.box.h));\n const dw = rootRect && rootRect.width > 0 ? rootRect.width : fallbackW;\n const dh = rootRect && rootRect.height > 0 ? rootRect.height : fallbackH;\n\n return { src: { x: 0, y: 0, w: dw, h: dh }, rect: { dx: 0, dy: 0, dw, dh }, systems, measures };\n } catch {\n return EMPTY_LAYOUT;\n }\n}\n\n/**\n * Resolve each DISTINCT onset (see `distinctOnsets`) to a `noteCols` entry —\n * the SAME \"real engraved column\" format `vstackAudioPlayheadLine` already\n * accepts from the SVG/canvas players (`measureIndex + fractionWithinMeasure`\n * — see that function's doc for how it's consumed). For each onset, every\n * stamped `noteId` sounding at that instant (a chord may have several) is\n * looked up by id in the live DOM (`document.getElementById` — Task 1 stamps\n * ids that Verovio preserves verbatim as SVG element ids), mapped to its\n * measure via `collectMeasureElements`'s canonical indexing (so it lines up\n * EXACTLY with `verovioNotationLayout`'s own `index` numbering), and its\n * fractional x-position within that measure's column computed with the exact\n * same formula `vstackAudioPlayheadLine`'s own `colX` uses internally (not\n * re-derived independently — this is the note's ANCHOR position, not new\n * interpolation math). A chord's several ids are averaged. Any onset with NO\n * resolvable id (missing from the DOM, e.g. a `rendered`-seam layout with no\n * live SVG) falls back to the ordinal spread `vstackAudioPlayheadLine` itself\n * uses when `noteCols` is entirely absent — so one bad id degrades ONLY that\n * onset, not the whole piece. Returns `undefined` (not a partially-bad array)\n * only when there is no usable layout/DOM at all to resolve against.\n */\nexport function verovioOnsetColumns(\n root: Element,\n layout: NotationLayout,\n onsets: VerovioOnset[],\n): number[] | undefined {\n try {\n const distinctMs = distinctOnsets(onsets.map((o) => ({ onsetMs: o.tMs })));\n if (!distinctMs.length) return undefined;\n const cols = measureColumnsFromLayout(layout.measures);\n if (!cols.length) return undefined;\n\n const pageGeoms = computePageGeometries(root);\n const measureEntries = collectMeasureElements(pageGeoms);\n if (!measureEntries.length) return undefined;\n const measureIndexOf = new Map<Element, number>();\n measureEntries.forEach((m, i) => measureIndexOf.set(m.el, i));\n\n const idsByOnset = new Map<number, string[]>();\n for (const o of onsets) {\n const arr = idsByOnset.get(o.tMs) ?? [];\n for (const id of o.noteIds) if (!arr.includes(id)) arr.push(id);\n idsByOnset.set(o.tMs, arr);\n }\n\n const doc = root.ownerDocument;\n\n return distinctMs.map((tMs, k) => {\n const ids = idsByOnset.get(tMs) ?? [];\n const positions: number[] = [];\n for (const id of ids) {\n const noteEl = doc ? doc.getElementById(id) : null;\n const measureEl = noteEl ? noteEl.closest('g.measure') : null;\n const index = measureEl ? measureIndexOf.get(measureEl) : undefined;\n if (index == null) continue;\n const m = cols[index];\n const geom = measureEntries[index]?.geom;\n const box = noteEl && geom ? boxFromElement(noteEl, geom) : null;\n if (!box || !m) continue;\n const sx = Math.min(m.noteStartX, m.x + m.w);\n const denom = m.x + m.w - sx;\n const frac = denom > 0 ? Math.min(1, Math.max(0, (box.x - sx) / denom)) : 0;\n positions.push(index + frac);\n }\n if (positions.length) return positions.reduce((a, b) => a + b, 0) / positions.length;\n // Same ordinal-spread fallback vstackAudioPlayheadLine uses internally\n // when no noteCols are supplied at all — degrades this ONE onset only.\n return distinctMs.length === 1 ? 0 : (k / (distinctMs.length - 1)) * cols.length;\n });\n } catch {\n return undefined;\n }\n}\n\n// ─── Semantic zoom mapping ──────────────────────────────────────────────────\n\n/** Verovio glyph-size percent at semantic zoom 1 (matches the migration\n * spike's own default — a normal, readable size at typical host widths). */\nexport const VEROVIO_BASE_SCALE = 40;\n/** Clamp band for the derived `scale`, so an extreme `zoom` never collapses\n * glyphs to unreadable or blows them up past sane bounds. */\nexport const VEROVIO_MIN_SCALE = 20;\nexport const VEROVIO_MAX_SCALE = 120;\n\nexport interface VerovioRenderOptions {\n scale: number;\n pageWidth: number;\n}\n\n/**\n * Semantic zoom → Verovio options. The migration spike's `zoom-test*.mjs`\n * proved `pageWidth` (Verovio's line-breaking width, in ITS OWN units)\n * drives measures-per-system while `scale` (glyph size) alone does NOT\n * (avgMeasuresPerSystem was IDENTICAL — 3.61 — across scale 40/80/150 at a\n * fixed pageWidth 1600; it moved 2.24→3.61→5.91 as pageWidth alone rose\n * 1000→1600→2400 at a fixed scale). Verovio's own rendered SVG width in CSS\n * px is EXACTLY `pageWidth * scale / 100` (confirmed empirically against\n * real 6.2.0 renders) — an identity, not an approximation.\n *\n * This function picks `scale` proportional to `zoom` (bigger zoom ⇒ bigger\n * glyphs) and then SOLVES that identity for the `pageWidth` that makes the\n * rendered width land EXACTLY on `hostWidthPx` regardless of zoom\n * (`pageWidth = hostWidthPx * 100 / scale`). Composing it this way — instead\n * of tuning `scale` and `pageWidth` independently — makes BOTH halves of the\n * spec's semantic fall out of the ONE formula: as `zoom` rises, `scale`\n * rises (glyphs bigger) AND the required `pageWidth` (in Verovio units)\n * SHRINKS proportionally (since it's inversely proportional to `scale` at a\n * fixed target width) — and per the spike's own finding, a smaller\n * `pageWidth` fits FEWER measures per system. So \"bigger zoom ⇒ fewer\n * measures/system, glyphs larger, width still fits host\" (design doc §2) is\n * a direct consequence of this one identity, pinned by\n * `tests/notationPlayerVerovio.test.ts`'s real-Verovio monotonicity test.\n *\n * No floor on `pageWidth` beyond what the identity itself produces: `scale`\n * is already clamped to `[VEROVIO_MIN_SCALE, VEROVIO_MAX_SCALE]` (both > 0)\n * and `hostWidthPx` is floored to a sane fallback when invalid, so\n * `pageWidth = w * 100 / scale` is ALWAYS finite and positive — an\n * additional floor would only ever fire by breaking the width-fits-host\n * identity (clamping the OUTPUT width away from the host's actual width),\n * which is worse than a small `pageWidth`.\n */\nexport function verovioZoomOptions(hostWidthPx: number, zoom: number): VerovioRenderOptions {\n const z = Number.isFinite(zoom) && zoom > 0 ? zoom : 1;\n const scale = Math.max(VEROVIO_MIN_SCALE, Math.min(VEROVIO_MAX_SCALE, VEROVIO_BASE_SCALE * z));\n const w = Number.isFinite(hostWidthPx) && hostWidthPx > 0 ? hostWidthPx : MAX_ENGRAVE_WIDTH_VRV;\n const pageWidth = (w * 100) / scale;\n return { scale, pageWidth };\n}\n\n// ─── Shared module-level toolkit (see the module doc's \"SHARED TOOLKIT\n// INSTANCE\" section) ─────────────────────────────────────────────────\n\n/** The subset of `VerovioToolkit`'s instance API this module calls — see\n * `src/types/verovio.d.ts` for the ambient module declaration backing the\n * dynamic imports below (the `verovio` npm package ships no types of its\n * own). Deliberately duck-typed/local rather than importing the real class\n * type statically, so nothing about this module's own type-checking\n * depends on a STATIC import of `verovio/esm` (only the dynamic one inside\n * `getVerovioToolkit` touches the package at all, which is what tsup's\n * `external` + the build-audit grep gate verify). */\ninterface VerovioToolkitInstance {\n loadData(data: string): boolean;\n getPageCount(): number;\n renderToSVG(page: number): string;\n setOptions(options: Record<string, unknown>): void;\n redoLayout(options?: Record<string, unknown>): void;\n}\n\nlet toolkitPromise: Promise<VerovioToolkitInstance> | null = null;\n\nfunction getVerovioToolkit(): Promise<VerovioToolkitInstance> {\n if (!toolkitPromise) {\n toolkitPromise = (async () => {\n // Literal dynamic imports — consumers' bundlers must statically see\n // these specifiers to code-split Verovio out of every OTHER dist\n // entry (tsup marks `verovio` external — see tsup.config.ts + the\n // build report's dist-grep evidence). A caller that only ever uses\n // the `rendered` test seam never pulls Verovio in at all.\n const [{ default: createVerovioModule }, { VerovioToolkit }] = await Promise.all([\n import('verovio/wasm'),\n import('verovio/esm'),\n ]);\n const VerovioModule = await createVerovioModule();\n return new VerovioToolkit(VerovioModule) as unknown as VerovioToolkitInstance;\n })();\n }\n return toolkitPromise;\n}\n\n// ─── createVerovioNotationPlayer ────────────────────────────────────────────\n\nconst DEFAULT_PLAYHEAD_COLOR = '#2f6f4f';\n/** Engrave-width ceiling, CSS px — same policy as notationPlayerSvg.ts's\n * `MAX_ENGRAVE_WIDTH_SVG` (design doc §\"Render\": \"cap ~1200px\"), restated\n * independently since the two players' constants are independently\n * tunable. */\nexport const MAX_ENGRAVE_WIDTH_VRV = 1200;\n/** Floor so a not-yet-laid-out / zero-width host never engraves at 0px. */\nconst MIN_ENGRAVE_WIDTH_VRV = 280;\n\n/** Build a live, interactive Verovio (vector) notation player. See the\n * module doc + `docs/superpowers/specs/2026-08-11-verovio-player-design.md`\n * (in stave-web-sightread) §2 for the full design. */\nexport function createVerovioNotationPlayer(opts: CreateVerovioNotationPlayerOpts): VerovioNotationPlayer {\n const { host, musicXml, onsets: rawOnsets } = opts;\n const playheadColor = opts.playheadColor ?? DEFAULT_PLAYHEAD_COLOR;\n const onsets = distinctOnsets(rawOnsets.map((o) => ({ onsetMs: o.tMs })));\n\n const root = document.createElement('div');\n root.style.position = 'relative';\n root.style.width = '100%';\n // Same \"optical zoom is free\" reasoning as notationPlayerSvg.ts — let\n // native pinch-zoom + vertical page-scroll gestures through unimpeded.\n root.style.touchAction = 'pan-y pinch-zoom';\n host.appendChild(root);\n\n const svgHost = document.createElement('div');\n root.appendChild(svgHost);\n\n const playheadEl = document.createElement('div');\n playheadEl.style.position = 'absolute';\n playheadEl.style.left = '0px';\n playheadEl.style.top = '0px';\n playheadEl.style.width = '2px';\n playheadEl.style.height = '0px';\n playheadEl.style.background = playheadColor;\n playheadEl.style.opacity = '0';\n playheadEl.style.pointerEvents = 'none';\n root.appendChild(playheadEl);\n\n let currentLayout: NotationLayout | null = null;\n let currentNoteCols: number[] | undefined;\n let currentZoom = opts.zoom ?? 1;\n let lastEngravedWidthPx = 0;\n let loaded = false; // toolkit.loadData succeeded (rendered-seam path never sets this)\n let destroyed = false;\n let lastTMs = 0;\n // Stale-reflow guard — same \"each async re-engrave captures its own\n // token\" rule as notationPlayerSvg.ts's `rebuildToken`.\n let rebuildToken = 0;\n\n function desiredEngraveWidthPx(): number {\n const w = host.clientWidth || 0;\n return Math.max(MIN_ENGRAVE_WIDTH_VRV, Math.min(w || MAX_ENGRAVE_WIDTH_VRV, MAX_ENGRAVE_WIDTH_VRV));\n }\n\n // ─── Playhead + auto-follow (createFollowController — notationCommon.ts) ─\n\n const follow = createFollowController();\n\n function renderPlayhead(tMs: number): void {\n lastTMs = tMs;\n if (!currentLayout) return;\n const nBars = currentLayout.measures.length\n ? Math.max(...currentLayout.measures.map((m) => m.index)) + 1\n : 0;\n const line = vstackAudioPlayheadLine(currentLayout, onsets, tMs, nBars, currentNoteCols);\n if (!line) {\n playheadEl.style.opacity = '0';\n return;\n }\n playheadEl.style.opacity = String(line.alpha);\n playheadEl.style.left = `${line.x}px`;\n playheadEl.style.top = `${line.y0}px`;\n playheadEl.style.height = `${Math.max(0, line.y1 - line.y0)}px`;\n const layoutForFollow = currentLayout;\n const sys = line.sys ?? -1;\n follow.follow({\n systemIndex: sys,\n getSystemRect: () => {\n const box = layoutForFollow.systems[sys];\n if (!box) return null;\n const rootRect = safeRect(root);\n if (!rootRect) return null;\n return { top: rootRect.top + box.y, bottom: rootRect.top + box.y + box.h };\n },\n getPlayheadRect: () => safeRect(playheadEl),\n });\n }\n\n function setTime(tMs: number): void {\n if (destroyed) return;\n follow.onSetTime(tMs);\n renderPlayhead(tMs);\n }\n\n // ─── Engrave / reflow ──────────────────────────────────────────────────\n\n function rebuildLayoutFromDom(): void {\n currentLayout = verovioNotationLayout(svgHost);\n currentNoteCols = verovioOnsetColumns(svgHost, currentLayout, rawOnsets);\n }\n\n function renderAllPages(toolkit: VerovioToolkitInstance): void {\n svgHost.replaceChildren();\n const pageCount = Math.max(0, toolkit.getPageCount());\n for (let p = 1; p <= pageCount; p++) {\n const pageDiv = document.createElement('div');\n pageDiv.className = 'vrv-page';\n pageDiv.innerHTML = toolkit.renderToSVG(p);\n svgHost.appendChild(pageDiv);\n }\n }\n\n async function initialEngrave(): Promise<void> {\n if (opts.rendered) {\n currentLayout = opts.rendered;\n lastEngravedWidthPx = desiredEngraveWidthPx();\n renderPlayhead(lastTMs);\n return;\n }\n\n const toolkit = await getVerovioToolkit();\n if (destroyed) return;\n\n const widthPx = desiredEngraveWidthPx();\n const { scale, pageWidth } = verovioZoomOptions(widthPx, currentZoom);\n toolkit.setOptions({ scale, pageWidth, breaks: 'auto', adjustPageHeight: true });\n loaded = !!toolkit.loadData(musicXml);\n if (destroyed) return;\n\n lastEngravedWidthPx = widthPx;\n if (loaded) renderAllPages(toolkit);\n rebuildLayoutFromDom();\n renderPlayhead(lastTMs);\n }\n\n const ready = initialEngrave();\n\n /** Re-engrave at `newZoom` and the host's CURRENT width, preserving the\n * scroll position of whatever content is centered in the viewport right\n * now. Shared by both `setZoom` and `resize` — same shape as\n * notationPlayerSvg.ts's `reflow`. No-op when neither the width nor the\n * zoom actually changed, or when using the `rendered` test seam / the\n * initial load never succeeded (nothing to re-engrave). */\n async function reflow(newZoom: number): Promise<void> {\n if (destroyed) return;\n const widthPx = desiredEngraveWidthPx();\n if (widthPx === lastEngravedWidthPx && newZoom === currentZoom) return;\n if (opts.rendered || !loaded) {\n currentZoom = newZoom;\n return;\n }\n\n const toolkit = await getVerovioToolkit();\n if (destroyed) return;\n\n const myToken = ++rebuildToken;\n const oldLayout = currentLayout;\n const hasWin = typeof window !== 'undefined';\n let anchorY: number | null = null;\n const anchorX = widthPx / 2;\n if (hasWin && typeof root.getBoundingClientRect === 'function') {\n const r = root.getBoundingClientRect();\n anchorY = window.innerHeight / 2 - r.top;\n }\n\n const { scale, pageWidth } = verovioZoomOptions(widthPx, newZoom);\n toolkit.setOptions({ scale, pageWidth, breaks: 'auto', adjustPageHeight: true });\n // RECLAIM (see the module doc's \"SHARED TOOLKIT INSTANCE\" note): the\n // module-level toolkit is shared across every live player on the page —\n // and the real consumer (PlayerPage) keeps a harmony player AND a\n // written player alive SIMULTANEOUSLY (lazy-built, destroyed only on\n // piece switch), so `loadData` calls from the two players genuinely\n // interleave (e.g. harmony active -> resize while written hidden -> back\n // to written -> writtenPlayer.setZoom()). A plain `redoLayout()` here\n // would silently re-lay-out and render WHATEVER document the toolkit\n // currently holds — which may belong to the OTHER player if it rendered\n // more recently. So every reflow re-parses THIS player's own `musicXml`\n // synchronously, immediately before rendering (loadData does a full\n // parse + layout with the just-set options, making a separate\n // `redoLayout()` call redundant). Because this call and `renderAllPages`\n // below are both synchronous with NO `await` between them, and the only\n // preceding `await` (`getVerovioToolkit()`) already resolved, JS's\n // single-threaded run-to-completion semantics guarantee no other\n // player's reflow/initialEngrave can interleave between the reclaim and\n // the render — the shared singleton is therefore safe under alternating\n // use. See `tests/notationPlayerVerovio.test.ts`'s two-player\n // interleaved-reflow test for the regression this guards against.\n const ok = toolkit.loadData(musicXml);\n if (destroyed || myToken !== rebuildToken) return;\n if (!ok) {\n loaded = false;\n return;\n }\n\n lastEngravedWidthPx = widthPx;\n currentZoom = newZoom;\n renderAllPages(toolkit);\n rebuildLayoutFromDom();\n\n if (oldLayout && currentLayout && anchorY != null && hasWin) {\n const delta = computeReflowScrollDelta(oldLayout, currentLayout, anchorX, anchorY);\n if (Number.isFinite(delta) && Math.abs(delta) > 0.5) {\n window.scrollTo({ top: Math.max(0, window.scrollY + delta), left: window.scrollX, behavior: 'auto' });\n }\n }\n if (!destroyed) renderPlayhead(lastTMs);\n }\n\n // ─── Click-to-seek ─────────────────────────────────────────────────────\n\n const clickListeners: Array<(measureIndex: number) => void> = [];\n function onRootClick(e: MouseEvent): void {\n if (!currentLayout) return;\n const rect = root.getBoundingClientRect();\n const mx = e.clientX - rect.left;\n const my = e.clientY - rect.top;\n const idx = hitTestMeasureAt(currentLayout, mx, my);\n if (idx != null) for (const cb of clickListeners) cb(idx);\n }\n root.addEventListener('click', onRootClick);\n\n return {\n ready,\n setTime,\n setFollowEnabled(on) {\n follow.setEnabled(on);\n },\n async setZoom(z: number): Promise<void> {\n await ready;\n await reflow(z);\n },\n async resize(): Promise<void> {\n await ready;\n await reflow(currentZoom);\n },\n onMeasureClick(cb: (measureIndex: number) => void): () => void {\n clickListeners.push(cb);\n return () => {\n const i = clickListeners.indexOf(cb);\n if (i >= 0) clickListeners.splice(i, 1);\n };\n },\n destroy(): void {\n if (destroyed) return;\n destroyed = true;\n rebuildToken++;\n root.removeEventListener('click', onRootClick);\n follow.destroy();\n clickListeners.length = 0;\n currentLayout = null;\n currentNoteCols = undefined;\n if (root.parentNode === host) host.removeChild(root);\n },\n };\n}\n"],"mappings":"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","names":[]}
@@ -15,7 +15,7 @@ import {
15
15
  scrollCursorFactory,
16
16
  setFollowLayoutProvider,
17
17
  setNotationEngraving
18
- } from "../chunk-5XKPJUQY.js";
18
+ } from "../chunk-AJSFJCQW.js";
19
19
  import {
20
20
  FOLLOW_BARS,
21
21
  FOLLOW_PAD,
@@ -37,7 +37,7 @@ import {
37
37
  systemBox,
38
38
  vstackAudioPlayheadLine,
39
39
  vstackFollowBox
40
- } from "../chunk-BBWVFH7X.js";
40
+ } from "../chunk-SG7KION4.js";
41
41
  import {
42
42
  ctaScene,
43
43
  drawSafeGuides,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@real-music-packages/web-core",
3
- "version": "0.39.3",
3
+ "version": "0.39.4",
4
4
  "description": "Shared music-theory + audio primitives for the music-suite web apps",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",