@real-music-packages/web-core 0.39.12 → 0.40.0

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.
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  hitTestMeasureAt,
3
3
  measureColumnsFromLayout
4
- } from "./chunk-WKJM527Q.js";
4
+ } from "./chunk-HIELHWEZ.js";
5
5
 
6
6
  // src/notationCommon.ts
7
7
  function computeReflowScrollDelta(oldLayout, newLayout, anchorX, anchorY) {
@@ -150,4 +150,4 @@ export {
150
150
  hasReachedProgrammaticTarget,
151
151
  createFollowController
152
152
  };
153
- //# sourceMappingURL=chunk-CULHIX4S.js.map
153
+ //# sourceMappingURL=chunk-F4VYTGJQ.js.map
@@ -470,6 +470,7 @@ export {
470
470
  distinctMeasureIndices,
471
471
  followBoxAt,
472
472
  followWindowStart,
473
+ systemIndexOfBox,
473
474
  measureSystemMap,
474
475
  systemBox,
475
476
  vstackFollowBox,
@@ -481,4 +482,4 @@ export {
481
482
  audioPlayheadLine,
482
483
  vstackAudioPlayheadLine
483
484
  };
484
- //# sourceMappingURL=chunk-WKJM527Q.js.map
485
+ //# sourceMappingURL=chunk-HIELHWEZ.js.map
@@ -0,0 +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. */\n/** Which system (row) box's vertical band contains `box`'s center — or, when\n * none does, whichever system's center is closest. Exported (0.40.0) so\n * notationPlayerVerovio.ts's `verovioEngravedNotes` can assign\n * `EngravedNote.systemIndex` from real rendered note geometry using the\n * SAME row-assignment rule `measureSystemMap` already uses for measures —\n * one system-membership test, not two independently-derived ones. */\nexport function 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 out through the last column of the active system, then pins it\n// to the next engraved onset while the camera brings that system in — so the\n// 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 // 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 next engraved onset,\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: the first half of the gap sweeps OUT\n // to the current row's right edge. The second half rides the incoming row at\n // the next engraved onset. Do not sweep from the system's left edge: that\n // blank clef/key margin is not a musical position and looks like a phantom\n // jump to \"before bar 1\" before the cursor reaches the actual next bar.\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 x = b.x;\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"]}
@@ -7,7 +7,7 @@ import {
7
7
  notationLayout,
8
8
  vstackAudioPlayheadLine,
9
9
  vstackFollowBox
10
- } from "./chunk-WKJM527Q.js";
10
+ } from "./chunk-HIELHWEZ.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-BKGAQETO.js.map
278
+ //# sourceMappingURL=chunk-UADJXSFM.js.map
@@ -2,7 +2,7 @@ import {
2
2
  getNotationEngraving,
3
3
  notationFactory,
4
4
  scrollCursorFactory
5
- } from "./chunk-BKGAQETO.js";
5
+ } from "./chunk-UADJXSFM.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-WKJM527Q.js";
13
+ } from "./chunk-HIELHWEZ.js";
14
14
  import {
15
15
  safeBox
16
16
  } from "./chunk-HXTRNE74.js";
@@ -4,12 +4,12 @@ import {
4
4
  createFollowController,
5
5
  hasReachedProgrammaticTarget,
6
6
  isWithinProgrammaticScroll
7
- } from "./chunk-CULHIX4S.js";
7
+ } from "./chunk-F4VYTGJQ.js";
8
8
  import {
9
9
  distinctOnsets,
10
10
  hitTestMeasureAt,
11
11
  vstackAudioPlayheadLine
12
- } from "./chunk-WKJM527Q.js";
12
+ } from "./chunk-HIELHWEZ.js";
13
13
  import "./chunk-HXTRNE74.js";
14
14
 
15
15
  // src/notationPlayerSvg.ts
@@ -1,6 +1,76 @@
1
1
  import { N as NotationLayout } from './notationGeometry-DqVBgL7F.js';
2
+ import { EngravedNote } from './notationPlayerSvg.js';
2
3
  import './promo.js';
3
4
 
5
+ /** One `<note>`'s MODEL identity — the ground truth `EngravedNote`
6
+ * (notationPlayerSvg.ts) needs that Verovio's rendered SVG cannot supply on
7
+ * its own (a rendered `g.note`/`g.rest` carries geometry, not pitch/tie/
8
+ * duration semantics). Keyed by the note's stamped `id` in
9
+ * `notePositions()`'s return of `verovioEngravedNotes`
10
+ * (notationPlayerVerovio.ts). */
11
+ interface NoteModel {
12
+ /** `12*(octave+1) + stepSemitone + alter` — the standard MusicXML→MIDI
13
+ * conversion (the same formula stave-web-sightread's own
14
+ * `practice/probeInputs.ts:pitchMidi` uses — a universal formula, not app
15
+ * logic, so this is not an owned-layer violation to restate here). `null`
16
+ * for a rest or an `<unpitched>` note (no `<pitch>` child) — mirrors
17
+ * `EngravedNote.midi`'s own "null covers both" contract. */
18
+ midi: number | null;
19
+ /** Has a `<rest/>` child. */
20
+ isRest: boolean;
21
+ /** True for the STOP half of a tie — a direct `<tie type="stop">` child OR
22
+ * `<notations><tied type="stop">` (exporters vary on which they emit;
23
+ * either counts) — matches `EngravedNote.tieContinuation`'s "continuation
24
+ * note of a tie, not the struck start" contract. */
25
+ tieContinuation: boolean;
26
+ /** 0-based, matching `EngravedNote.staffIndex`'s "0 = top staff of the
27
+ * system" contract: every `<part>` is walked in document order, and
28
+ * every DISTINCT staff within it (by `<attributes><staves>` when
29
+ * present, else the highest `<staff>` number any of its notes uses, else
30
+ * 1) is assigned the next index — so a single-part 2-staff piano score
31
+ * numbers 0/1 by `<staff>`, and a 2-part 1-staff-per-part reduction (this
32
+ * app's own chord+bass shape) numbers 0/1 by PART, with neither case
33
+ * needing different code. */
34
+ staffIndex: number;
35
+ /** Whole-note fraction (0.25 = quarter) — `duration / divisions / 4`,
36
+ * divisions tracked per-part from the LAST `<attributes><divisions>`
37
+ * seen at or before this note (MusicXML: divisions persist until
38
+ * overridden, default 1). 0 for a grace note (no `<duration>` child —
39
+ * the true, spec-correct signal; never guessed from `<type>`). */
40
+ durationReal: number;
41
+ /** 0-based position of this note's `<measure>` among its OWN `<part>`'s
42
+ * measure children, in document order. Informational only — the live
43
+ * join (`verovioOnsetColumns`/`verovioEngravedNotes`, both in
44
+ * notationPlayerVerovio.ts) resolves the note's RENDERED measure index
45
+ * from the live SVG DOM independently (Verovio's own render order, which
46
+ * is what geometry/hit-testing must agree with), never from this field. */
47
+ measureIndex: number;
48
+ /** `print-object="no"` on the source `<note>` (stave's `hideDoubledNotes`
49
+ * sets this on editorially-doubled notes/rests before handing MusicXML to
50
+ * this player). Verovio's importer HONORS this for `<note>` elements
51
+ * carrying a `<pitch>` (renders `visibility="hidden"` on its own,
52
+ * empirically confirmed against a real 6.2.0 render) but does NOT honor
53
+ * it for `<rest>` notes (the `<g class="rest">` renders fully visible
54
+ * regardless — same empirical check). `createVerovioNotationPlayer`
55
+ * reads this field to force `visibility="hidden"` after every render for
56
+ * ANY id where it's true — a no-op re-application on notes Verovio
57
+ * already hid, and the actual fix on the rests it doesn't (see that
58
+ * module's `applyPrintObjectHiding`). */
59
+ hidden: boolean;
60
+ }
61
+
62
+ /** Restated independently, not imported as a VALUE from notationPlayerSvg.ts
63
+ * — same reasoning as `MAX_ENGRAVE_WIDTH_VRV` vs `MAX_ENGRAVE_WIDTH_SVG`
64
+ * (below): a runtime (non-`type`) import from notationPlayerSvg.ts would
65
+ * pull that module's ENTIRE implementation — including its own
66
+ * `createSvgNotationPlayer` closure (harmless at runtime, since its OSMD
67
+ * import stays lazy/dynamic either way) — into this entry's own tsup
68
+ * chunk graph, which is exactly the cross-entry bundle coupling the
69
+ * module doc's "IMPORT PATH" section (and the build report's dist-grep
70
+ * gate) exists to prevent between the two SVG-family players. Must stay
71
+ * byte-identical to `notationPlayerSvg.ts`'s own `MARKED_NOTE_CLASS` — a
72
+ * test in this file's own suite pins that. */
73
+ declare const MARKED_NOTE_CLASS = "rmp-note-marked";
4
74
  /** One distinct note-onset instant: the audio-clock time it sounds at, and
5
75
  * the stamped `xml:id`s (Task 1, stave repo) of every note that sounds at
6
76
  * that instant (>1 for a chord). Multiple entries sharing the same `tMs`
@@ -47,6 +117,24 @@ interface CreateVerovioNotationPlayerOpts {
47
117
  * /width but perform no real re-engrave (there is nothing to re-engrave).
48
118
  */
49
119
  rendered?: NotationLayout;
120
+ /**
121
+ * Narrow passthrough matching `CreateSvgNotationPlayerOpts.osmdOptions`'s
122
+ * shape exactly, so a caller driving BOTH players behind one interface
123
+ * (the swap this player exists for) never has to branch per backend.
124
+ * `autoBeam` has NO Verovio equivalent — Verovio always beams straight
125
+ * from the MusicXML's own `<beam>` elements (it has no "re-beam from
126
+ * scratch" pass the way OSMD's `autoBeam` option does) — so this is
127
+ * accepted and silently ignored, with a ONE-TIME `console.warn` (module-
128
+ * level, not per-instance — a caller that builds many players with the
129
+ * same options object should not get spammed) rather than a hard error,
130
+ * since ignoring it is genuinely harmless: synthesized rhythm XML with no
131
+ * `<beam>` data renders unbeamed either way, which is a cosmetic
132
+ * difference the caller can fix upstream (emit real `<beam>` elements)
133
+ * rather than this player faking OSMD's re-beam heuristic.
134
+ */
135
+ osmdOptions?: {
136
+ autoBeam?: boolean;
137
+ };
50
138
  }
51
139
  interface VerovioNotationPlayer {
52
140
  /** Resolves once the engraving is in the DOM and ready to draw. `setTime`/
@@ -73,6 +161,26 @@ interface VerovioNotationPlayer {
73
161
  /** Register a measure-click handler (measure index, matching the DOM-order
74
162
  * index `verovioNotationLayout` assigns). Returns an unsubscribe fn. */
75
163
  onMeasureClick(cb: (measureIndex: number) => void): () => void;
164
+ /**
165
+ * Mark the engraved noteheads/rests nearest the given columns (same
166
+ * `measureIndex + fraction-through-the-measure` units as `onsets`'
167
+ * derived columns); pass `null` or `[]` to clear. Same contract and CSS
168
+ * class (`MARKED_NOTE_CLASS` = `'rmp-note-marked'`) as
169
+ * `SvgNotationPlayer.markNotes` — a consumer's existing CSS keeps working
170
+ * unmodified across a backend swap. Survives re-engraving: reapplied
171
+ * after every `setZoom`/`resize`.
172
+ */
173
+ markNotes(cols: number[] | null): void;
174
+ /** Every engraved note/rest with MODEL identity (pitch, rest, tie, staff)
175
+ * and host-relative notehead position — same `EngravedNote` shape
176
+ * `SvgNotationPlayer.notePositions()` returns (re-exported from this
177
+ * module, not redefined). `headEl` is Verovio's rendered `.notehead`
178
+ * sub-group when present (a tighter box than the outer `g.note`, closer
179
+ * in spirit to OSMD's `.vf-notehead`), else the outer `g.note`/`g.rest`
180
+ * group. `[]` before the initial engrave resolves or under the
181
+ * `rendered` test seam (no live SVG to join against — same graceful
182
+ * degradation as `verovioOnsetColumns`). */
183
+ notePositions(): EngravedNote[];
76
184
  /** Tear down: removes the mounted DOM (engraving + playhead overlay) from
77
185
  * `host`, and drops every listener this instance added (click, window
78
186
  * scroll) and pending async work (a token guard drops any in-flight
@@ -115,6 +223,72 @@ declare function verovioNotationLayout(root: Element): NotationLayout;
115
223
  * only when there is no usable layout/DOM at all to resolve against.
116
224
  */
117
225
  declare function verovioOnsetColumns(root: Element, layout: NotationLayout, onsets: VerovioOnset[]): number[] | undefined;
226
+ /**
227
+ * Every rendered note/rest joined to its MODEL identity (`noteModel`, from
228
+ * `noteModelFromXml` — ./notationXml.ts) by stamped id — this player's
229
+ * `notePositions()`. Same shape and host-relative-px contract as
230
+ * notationPlayerSvg.ts's `engravedNotes` (its own doc: "Positions are px
231
+ * relative to the HOST element, at the notehead's center"), but the join
232
+ * itself is simpler here: unlike OSMD (one graphical group per CHORD,
233
+ * requiring the pitch-rank head-sorting `engravedNotes` does), Verovio
234
+ * renders every chord member as its OWN `<g id="..." class="note">`
235
+ * (empirically confirmed — see the module doc's point 2) — so this is a
236
+ * flat id→element→model join, no grouping/sorting.
237
+ *
238
+ * `headEl` prefers the note's own `.notehead` child (Verovio's real nested
239
+ * glyph group — empirically confirmed as `<g class="notehead">` inside
240
+ * `<g class="note">`) over the outer `g.note`/`g.rest` wrapper when
241
+ * present — a TIGHTER box (closer to notationPlayerSvg.ts's
242
+ * `.vf-notehead`-only box) than the wrapper, which also spans the
243
+ * stem/flag/accidental. Falls back to the wrapper itself for a rest (no
244
+ * `.notehead` child) or if that lookup's own `getBBox` fails.
245
+ *
246
+ * `root` is the container holding every rendered `.vrv-page` (this
247
+ * player's `svgHost`); `host` is the player's OWN host element — `x`/`y`
248
+ * are computed relative to `host` (per `EngravedNote`'s contract), NOT to
249
+ * `root`, which may itself sit offset within `host`. Never throws; returns
250
+ * `[]` for any missing/malformed structure (same defensive style as
251
+ * `verovioNotationLayout`).
252
+ */
253
+ declare function verovioEngravedNotes(root: Element, host: HTMLElement, noteModel: Map<string, NoteModel>): EngravedNote[];
254
+ /**
255
+ * Every rendered note/rest nearest engraved column `col`
256
+ * (`measureIndex + fraction`, same units as `markNotes`' argument and
257
+ * `noteCols`), one per staff of that measure — the Verovio-backend
258
+ * counterpart to notationPlayerSvg.ts's `graphicalNotesAtColumn`. That
259
+ * function searches OSMD's object model (`relInMeasureTimestamp` vs bar
260
+ * duration); this searches the live rendered geometry instead (Verovio
261
+ * exposes no per-element timestamp) — same "nearest entry by position gap,
262
+ * search every staff of the bar" shape, just sourced from `getBBox` instead
263
+ * of a timeline field. The fractional-position FORMULA itself
264
+ * (`(box.x - sx) / denom`) is the exact one `verovioOnsetColumns` already
265
+ * uses (not re-derived) — this function runs it in the opposite direction
266
+ * (nearest note TO a column, rather than a column FROM a note id).
267
+ */
268
+ declare function verovioNotesAtColumn(root: Element, layout: NotationLayout, col: number): Element[];
269
+ /**
270
+ * CRITICAL empirical finding (verified against a real 6.2.0 render — see
271
+ * the migration spike's follow-up, `.superpowers/…/print-object-test.mjs`
272
+ * equivalent run for this task): Verovio's MusicXML importer HONORS
273
+ * `print-object="no"` for a pitched `<note>` (renders `visibility="hidden"`
274
+ * on its own `<g class="note">`) but does NOT honor it for a `<note><rest/>`
275
+ * — the `<g class="rest">` renders fully visible regardless. This matters
276
+ * because stave-web-sightread's `hideDoubledNotes` feature sets
277
+ * `print-object="no"` on BOTH doubled notes AND the rests of a voice that
278
+ * lost every visible note (see that function's own "second pass" doc) —
279
+ * without this fix, a hidden-doubled-note's voice would still show a
280
+ * floating rest, exactly the "extra voice" clutter that feature exists to
281
+ * remove.
282
+ *
283
+ * Fix: force `visibility="hidden"` on every rendered id whose model says
284
+ * `hidden` (`NoteModel.hidden`, from `noteModelFromXml`). Re-applying it to
285
+ * a NOTE Verovio already hid itself is a harmless no-op; applying it to a
286
+ * REST is the actual fix. Call after every render (`renderAllPages`) — a
287
+ * fresh render is a fresh DOM, so a prior call's effect never carries over
288
+ * (nothing to "undo" on a note/rest that's no longer print-object="no").
289
+ * Never throws.
290
+ */
291
+ declare function applyPrintObjectHiding(root: Element, noteModel: Map<string, NoteModel>): void;
118
292
  /** Verovio glyph-size percent at semantic zoom 1 (matches the migration
119
293
  * spike's own default — a normal, readable size at typical host widths). */
120
294
  declare const VEROVIO_BASE_SCALE = 40;
@@ -169,4 +343,4 @@ declare const MAX_ENGRAVE_WIDTH_VRV = 1200;
169
343
  * (in stave-web-sightread) §2 for the full design. */
170
344
  declare function createVerovioNotationPlayer(opts: CreateVerovioNotationPlayerOpts): VerovioNotationPlayer;
171
345
 
172
- export { type CreateVerovioNotationPlayerOpts, MAX_ENGRAVE_WIDTH_VRV, VEROVIO_BASE_SCALE, VEROVIO_MAX_SCALE, VEROVIO_MIN_SCALE, type VerovioNotationPlayer, type VerovioOnset, type VerovioRenderOptions, createVerovioNotationPlayer, verovioNotationLayout, verovioOnsetColumns, verovioZoomOptions };
346
+ export { type CreateVerovioNotationPlayerOpts, EngravedNote, MARKED_NOTE_CLASS, MAX_ENGRAVE_WIDTH_VRV, VEROVIO_BASE_SCALE, VEROVIO_MAX_SCALE, VEROVIO_MIN_SCALE, type VerovioNotationPlayer, type VerovioOnset, type VerovioRenderOptions, applyPrintObjectHiding, createVerovioNotationPlayer, verovioEngravedNotes, verovioNotationLayout, verovioNotesAtColumn, verovioOnsetColumns, verovioZoomOptions };