@real-music-packages/web-core 0.49.0 → 0.50.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.
@@ -169,6 +169,14 @@ interface EngravedNote {
169
169
  systemIndex: number;
170
170
  /** Source duration in whole-note units (0.25 = quarter). */
171
171
  durationReal: number;
172
+ /** 0-based index of the onset (vertical time-slice) this note is struck in
173
+ * — SHARED by every note sounding together (a chord is one onset), in
174
+ * score time order. `null` for a note not in any struck onset: a rest, or
175
+ * a tie continuation. Consumers group notes into columns by this instead
176
+ * of reconstructing onsets from x-position. Sourced from the player's own
177
+ * `VerovioOnset[]` grouping (by `tMs`); the OSMD/SVG backend leaves it
178
+ * `null` (Verovio is the app's authoritative engraver). */
179
+ onsetId: number | null;
172
180
  x: number;
173
181
  y: number;
174
182
  w: number;
@@ -88,6 +88,7 @@ function engravedNotes(osmd, host) {
88
88
  staffIndex,
89
89
  systemIndex,
90
90
  durationReal: n.durationReal,
91
+ onsetId: null,
91
92
  x: r.left + r.width / 2 - hostRect.left,
92
93
  y: r.top + r.height / 2 - hostRect.top,
93
94
  w: r.width,
@@ -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 /** Parks a followed system this many px below the viewport top — pass the\n * height of any fixed top chrome (header, docked transport) plus a gap.\n * Default SYSTEM_TOP_MARGIN_PX. */\n followTopMarginPx?: 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 * Narrow OSMD engraving-option passthrough — deliberately not \"pass\n * arbitrary OSMD options\": only what a real caller has needed so far.\n * `autoBeam` (OSMD's own `IOSMDOptions.autoBeam`) re-beams notes at OSMD's\n * own layout pass; it is a no-op for musicXML that already carries\n * `<beam>` elements (OSMD only fills in beaming the source doesn't\n * already specify), so callers can pass it unconditionally for synthesized\n * scores without risking a properly-engraved piece. See CoarseRhythmProbe/\n * RhythmQuiz in stave-web-sightread, which set this same OSMD option\n * directly on their own (non-player) OSMD instances for synthesized\n * rhythm XML with no beam data.\n */\n osmdOptions?: { autoBeam?: boolean };\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 /**\n * Mark the engraved noteheads at the given columns, in the same\n * `measureIndex + fraction-through-the-measure` units as `noteCols`; pass\n * `null` or `[]` to clear.\n *\n * For pointing a learner at the note being asked for — a playhead says\n * WHERE you are, which is not the same as WHICH note, especially on a busy\n * bar. Marked notes carry the `rmp-note-marked` class, so the consumer owns\n * the look; this module does not inject styles.\n *\n * Survives re-engraving: the marks are reapplied after `setZoom`/`resize`.\n */\n markNotes(cols: number[] | null): void;\n /** Every engraved note with MODEL identity (pitch, rest, tie, staff) and\n * host-relative notehead position. See `EngravedNote`. */\n notePositions(): EngravedNote[];\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\n/**\n * Graphical notes of the staff entry nearest a column position, in the same\n * `measureIndex + fraction-through-the-measure` units as `noteCols`.\n *\n * OSMD keeps each staff entry's position inside its bar as\n * `relInMeasureTimestamp`, so the fractional part of a column maps onto an\n * entry by comparing that against the bar's own duration — the same clock the\n * caller built the columns from. Every staff of the bar is searched, so a\n * grand-staff onset marks both hands.\n *\n * `osmd` is duck-typed `any` for the same reason `svgNotationLayout`'s is: it\n * reads a small slice (`GraphicSheet.MeasureList[][].staffEntries[]`) that a\n * plain fixture can stand in for, so this is testable without a browser render.\n */\nexport function graphicalNotesAtColumn(osmd: any, col: number): any[] {\n const measureList: any[][] = osmd?.GraphicSheet?.MeasureList ?? [];\n if (!measureList.length || !Number.isFinite(col)) return [];\n const measureIndex = Math.max(0, Math.min(measureList.length - 1, Math.floor(col)));\n const staves = measureList[measureIndex] ?? [];\n const wanted = col - measureIndex;\n const found: any[] = [];\n\n for (const staffMeasure of staves) {\n const entries: any[] = staffMeasure?.staffEntries ?? [];\n if (!entries.length) continue;\n const barLength = staffMeasure?.parentSourceMeasure?.Duration?.RealValue || 1;\n let best: any = null;\n let bestGap = Number.POSITIVE_INFINITY;\n for (const entry of entries) {\n const rel = entry?.relInMeasureTimestamp?.RealValue ?? 0;\n const gap = Math.abs(rel / barLength - wanted);\n if (gap < bestGap) {\n best = entry;\n bestGap = gap;\n }\n }\n for (const voiceEntry of best?.graphicalVoiceEntries ?? []) {\n for (const note of voiceEntry?.notes ?? []) found.push(note);\n }\n }\n return found;\n}\n\n/** Class put on every marked notehead group. */\nexport const MARKED_NOTE_CLASS = 'rmp-note-marked';\n\n/** One engraved note with its MODEL identity attached — the ground truth a\n * consumer cannot reliably reverse-engineer from the DOM (rest glyphs live in\n * vf-notehead groups; tie continuations are graphical notes of pitches that\n * onset elsewhere; unisons double heads). Positions are px relative to the\n * HOST element, at the notehead's center. */\nexport interface EngravedNote {\n midi: number | null;\n isRest: boolean;\n /** True for the continuation notes of a tie (not the struck start). */\n tieContinuation: boolean;\n /** 0 = top staff of the system (piano RH), 1 = next, … */\n staffIndex: number;\n systemIndex: number;\n /** Source duration in whole-note units (0.25 = quarter). */\n durationReal: number;\n x: number;\n y: number;\n w: number;\n h: number;\n /** The notehead glyph element (null for rests without one). */\n headEl: Element | null;\n}\n\n/** Enumerate every engraved note with identity + position. */\nexport function engravedNotes(osmd: any, host: HTMLElement): EngravedNote[] {\n const out: EngravedNote[] = [];\n const hostRect = host.getBoundingClientRect();\n const measureList: any[][] = osmd?.GraphicSheet?.MeasureList ?? [];\n for (const staves of measureList) {\n (staves ?? []).forEach((staffMeasure: any, staffIndex: number) => {\n const systemIndex =\n staffMeasure?.ParentStaffLine?.ParentMusicSystem?.Id ??\n staffMeasure?.parentStaffLine?.parentMusicSystem?.Id ?? 0;\n for (const entry of staffMeasure?.staffEntries ?? []) {\n for (const voiceEntry of entry?.graphicalVoiceEntries ?? []) {\n const notes: any[] = [...(voiceEntry?.notes ?? [])];\n if (!notes.length) continue;\n // `getSVGGElement` returns the SAME stavenote group for every note\n // of a chord — its vf-noteheads must be paired to the notes\n // explicitly: heads top→bottom ↔ pitches high→low.\n const groupEl = typeof notes[0]?.getSVGGElement === 'function'\n ? notes[0].getSVGGElement() : null;\n const groupHeads: Element[] = groupEl\n ? (groupEl.matches?.('.vf-notehead')\n ? [groupEl]\n : [...groupEl.querySelectorAll?.('.vf-notehead') ?? []])\n : [];\n groupHeads.sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top);\n const meta = notes.map((note: any) => {\n const source = note?.sourceNote;\n const isRest = source\n ? (typeof source.isRest === 'function' ? !!source.isRest() : !!source.IsRestFlag)\n : true;\n const tie = source?.NoteTie ?? source?.noteTie ?? null;\n const pitch = source?.Pitch ?? source?.pitch ?? null;\n return {\n source,\n isRest,\n tieContinuation: !!tie && tie.StartNote !== source,\n // OSMD halfTone 0 = C0 = MIDI 12.\n midi: pitch && typeof pitch.getHalfTone === 'function'\n ? pitch.getHalfTone() + 12 : null,\n durationReal: source?.Length?.RealValue ?? 0,\n };\n });\n const pitched = meta.filter((n) => n.midi !== null)\n .sort((a, b) => (b.midi as number) - (a.midi as number));\n for (const n of meta) {\n let headEl: Element | null = null;\n if (n.midi !== null && groupHeads.length) {\n const rank = pitched.indexOf(n);\n headEl = groupHeads[Math.min(rank < 0 ? 0 : rank, groupHeads.length - 1)] ?? null;\n } else if (groupHeads.length) {\n headEl = groupHeads[0];\n }\n const rectEl = headEl ?? groupEl;\n if (!rectEl) continue;\n const r = rectEl.getBoundingClientRect();\n out.push({\n midi: n.midi, isRest: n.isRest, tieContinuation: n.tieContinuation,\n staffIndex, systemIndex, durationReal: n.durationReal,\n x: r.left + r.width / 2 - hostRect.left,\n y: r.top + r.height / 2 - hostRect.top,\n w: r.width, h: r.height,\n headEl,\n });\n }\n }\n }\n });\n }\n return out;\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 // OSMD's own page Size can under-report height for a short, single-system\n // render (all title/subtitle/composer draw options off, as every reading\n // surface here sets them): `page.PositionAndShape.Size.height` comes back\n // equal to just the LAST system's own height, NOT that system's Y offset\n // plus its height — so a page holding one compact system reports a page\n // shorter than the system it contains. `rect` is documented (see the\n // function doc above) to span the FULL engraved page, top-aligned at\n // dy=0 — `vstackAudioPlayheadLine` trusts `rect.dy + rect.dh` as the\n // bottom clamp bound for the playhead band, so an under-tall `dh` clamps\n // the band's bottom ABOVE its top, and the \"preserve height, slide\"\n // clamp math sends the whole band's top deep negative — the playhead\n // renders far above the staff instead of on it. Flooring `dh` at the\n // real bottom edge of the tallest system keeps the contract honest\n // regardless of which OSMD quirk under-reports the raw page size.\n const systemsBottom = Math.max(...systems.map((s) => s.y + s.h));\n const dh = Math.max(pageSize.height * f, systemsBottom);\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 // Published contract: hosts may locate the playhead (e.g. to keep it in\n // view inside their own scroll container) via [data-rmp-playhead]. The\n // element stays owned by this component — position/size are not API.\n playheadEl.dataset.rmpPlayhead = '1';\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({ topMarginPx: opts.followTopMarginPx });\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 ...(opts.osmdOptions?.autoBeam !== undefined ? { autoBeam: opts.osmdOptions.autoBeam } : {}),\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 applyMarks();\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 let markedCols: number[] = [];\n\n function applyMarks(): void {\n // Clear first: a re-engrave hands back different elements, and stale marks\n // would otherwise stay lit on notes nobody asked about.\n for (const el of root.querySelectorAll(`.${MARKED_NOTE_CLASS}`)) {\n el.classList.remove(MARKED_NOTE_CLASS);\n }\n if (!osmd) return;\n for (const col of markedCols) {\n for (const note of graphicalNotesAtColumn(osmd, col)) {\n const el = typeof note?.getSVGGElement === 'function' ? note.getSVGGElement() : null;\n if (el) el.classList.add(MARKED_NOTE_CLASS);\n }\n }\n }\n\n return {\n ready,\n setTime,\n markNotes(cols: number[] | null): void {\n markedCols = cols ?? [];\n applyMarks();\n },\n notePositions(): EngravedNote[] {\n return osmd ? engravedNotes(osmd, host) : [];\n },\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":[]}
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 /** Parks a followed system this many px below the viewport top — pass the\n * height of any fixed top chrome (header, docked transport) plus a gap.\n * Default SYSTEM_TOP_MARGIN_PX. */\n followTopMarginPx?: 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 * Narrow OSMD engraving-option passthrough — deliberately not \"pass\n * arbitrary OSMD options\": only what a real caller has needed so far.\n * `autoBeam` (OSMD's own `IOSMDOptions.autoBeam`) re-beams notes at OSMD's\n * own layout pass; it is a no-op for musicXML that already carries\n * `<beam>` elements (OSMD only fills in beaming the source doesn't\n * already specify), so callers can pass it unconditionally for synthesized\n * scores without risking a properly-engraved piece. See CoarseRhythmProbe/\n * RhythmQuiz in stave-web-sightread, which set this same OSMD option\n * directly on their own (non-player) OSMD instances for synthesized\n * rhythm XML with no beam data.\n */\n osmdOptions?: { autoBeam?: boolean };\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 /**\n * Mark the engraved noteheads at the given columns, in the same\n * `measureIndex + fraction-through-the-measure` units as `noteCols`; pass\n * `null` or `[]` to clear.\n *\n * For pointing a learner at the note being asked for — a playhead says\n * WHERE you are, which is not the same as WHICH note, especially on a busy\n * bar. Marked notes carry the `rmp-note-marked` class, so the consumer owns\n * the look; this module does not inject styles.\n *\n * Survives re-engraving: the marks are reapplied after `setZoom`/`resize`.\n */\n markNotes(cols: number[] | null): void;\n /** Every engraved note with MODEL identity (pitch, rest, tie, staff) and\n * host-relative notehead position. See `EngravedNote`. */\n notePositions(): EngravedNote[];\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\n/**\n * Graphical notes of the staff entry nearest a column position, in the same\n * `measureIndex + fraction-through-the-measure` units as `noteCols`.\n *\n * OSMD keeps each staff entry's position inside its bar as\n * `relInMeasureTimestamp`, so the fractional part of a column maps onto an\n * entry by comparing that against the bar's own duration — the same clock the\n * caller built the columns from. Every staff of the bar is searched, so a\n * grand-staff onset marks both hands.\n *\n * `osmd` is duck-typed `any` for the same reason `svgNotationLayout`'s is: it\n * reads a small slice (`GraphicSheet.MeasureList[][].staffEntries[]`) that a\n * plain fixture can stand in for, so this is testable without a browser render.\n */\nexport function graphicalNotesAtColumn(osmd: any, col: number): any[] {\n const measureList: any[][] = osmd?.GraphicSheet?.MeasureList ?? [];\n if (!measureList.length || !Number.isFinite(col)) return [];\n const measureIndex = Math.max(0, Math.min(measureList.length - 1, Math.floor(col)));\n const staves = measureList[measureIndex] ?? [];\n const wanted = col - measureIndex;\n const found: any[] = [];\n\n for (const staffMeasure of staves) {\n const entries: any[] = staffMeasure?.staffEntries ?? [];\n if (!entries.length) continue;\n const barLength = staffMeasure?.parentSourceMeasure?.Duration?.RealValue || 1;\n let best: any = null;\n let bestGap = Number.POSITIVE_INFINITY;\n for (const entry of entries) {\n const rel = entry?.relInMeasureTimestamp?.RealValue ?? 0;\n const gap = Math.abs(rel / barLength - wanted);\n if (gap < bestGap) {\n best = entry;\n bestGap = gap;\n }\n }\n for (const voiceEntry of best?.graphicalVoiceEntries ?? []) {\n for (const note of voiceEntry?.notes ?? []) found.push(note);\n }\n }\n return found;\n}\n\n/** Class put on every marked notehead group. */\nexport const MARKED_NOTE_CLASS = 'rmp-note-marked';\n\n/** One engraved note with its MODEL identity attached — the ground truth a\n * consumer cannot reliably reverse-engineer from the DOM (rest glyphs live in\n * vf-notehead groups; tie continuations are graphical notes of pitches that\n * onset elsewhere; unisons double heads). Positions are px relative to the\n * HOST element, at the notehead's center. */\nexport interface EngravedNote {\n midi: number | null;\n isRest: boolean;\n /** True for the continuation notes of a tie (not the struck start). */\n tieContinuation: boolean;\n /** 0 = top staff of the system (piano RH), 1 = next, … */\n staffIndex: number;\n systemIndex: number;\n /** Source duration in whole-note units (0.25 = quarter). */\n durationReal: number;\n /** 0-based index of the onset (vertical time-slice) this note is struck in\n * — SHARED by every note sounding together (a chord is one onset), in\n * score time order. `null` for a note not in any struck onset: a rest, or\n * a tie continuation. Consumers group notes into columns by this instead\n * of reconstructing onsets from x-position. Sourced from the player's own\n * `VerovioOnset[]` grouping (by `tMs`); the OSMD/SVG backend leaves it\n * `null` (Verovio is the app's authoritative engraver). */\n onsetId: number | null;\n x: number;\n y: number;\n w: number;\n h: number;\n /** The notehead glyph element (null for rests without one). */\n headEl: Element | null;\n}\n\n/** Enumerate every engraved note with identity + position. */\nexport function engravedNotes(osmd: any, host: HTMLElement): EngravedNote[] {\n const out: EngravedNote[] = [];\n const hostRect = host.getBoundingClientRect();\n const measureList: any[][] = osmd?.GraphicSheet?.MeasureList ?? [];\n for (const staves of measureList) {\n (staves ?? []).forEach((staffMeasure: any, staffIndex: number) => {\n const systemIndex =\n staffMeasure?.ParentStaffLine?.ParentMusicSystem?.Id ??\n staffMeasure?.parentStaffLine?.parentMusicSystem?.Id ?? 0;\n for (const entry of staffMeasure?.staffEntries ?? []) {\n for (const voiceEntry of entry?.graphicalVoiceEntries ?? []) {\n const notes: any[] = [...(voiceEntry?.notes ?? [])];\n if (!notes.length) continue;\n // `getSVGGElement` returns the SAME stavenote group for every note\n // of a chord — its vf-noteheads must be paired to the notes\n // explicitly: heads top→bottom ↔ pitches high→low.\n const groupEl = typeof notes[0]?.getSVGGElement === 'function'\n ? notes[0].getSVGGElement() : null;\n const groupHeads: Element[] = groupEl\n ? (groupEl.matches?.('.vf-notehead')\n ? [groupEl]\n : [...groupEl.querySelectorAll?.('.vf-notehead') ?? []])\n : [];\n groupHeads.sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top);\n const meta = notes.map((note: any) => {\n const source = note?.sourceNote;\n const isRest = source\n ? (typeof source.isRest === 'function' ? !!source.isRest() : !!source.IsRestFlag)\n : true;\n const tie = source?.NoteTie ?? source?.noteTie ?? null;\n const pitch = source?.Pitch ?? source?.pitch ?? null;\n return {\n source,\n isRest,\n tieContinuation: !!tie && tie.StartNote !== source,\n // OSMD halfTone 0 = C0 = MIDI 12.\n midi: pitch && typeof pitch.getHalfTone === 'function'\n ? pitch.getHalfTone() + 12 : null,\n durationReal: source?.Length?.RealValue ?? 0,\n };\n });\n const pitched = meta.filter((n) => n.midi !== null)\n .sort((a, b) => (b.midi as number) - (a.midi as number));\n for (const n of meta) {\n let headEl: Element | null = null;\n if (n.midi !== null && groupHeads.length) {\n const rank = pitched.indexOf(n);\n headEl = groupHeads[Math.min(rank < 0 ? 0 : rank, groupHeads.length - 1)] ?? null;\n } else if (groupHeads.length) {\n headEl = groupHeads[0];\n }\n const rectEl = headEl ?? groupEl;\n if (!rectEl) continue;\n const r = rectEl.getBoundingClientRect();\n out.push({\n midi: n.midi, isRest: n.isRest, tieContinuation: n.tieContinuation,\n staffIndex, systemIndex, durationReal: n.durationReal, onsetId: null,\n x: r.left + r.width / 2 - hostRect.left,\n y: r.top + r.height / 2 - hostRect.top,\n w: r.width, h: r.height,\n headEl,\n });\n }\n }\n }\n });\n }\n return out;\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 // OSMD's own page Size can under-report height for a short, single-system\n // render (all title/subtitle/composer draw options off, as every reading\n // surface here sets them): `page.PositionAndShape.Size.height` comes back\n // equal to just the LAST system's own height, NOT that system's Y offset\n // plus its height — so a page holding one compact system reports a page\n // shorter than the system it contains. `rect` is documented (see the\n // function doc above) to span the FULL engraved page, top-aligned at\n // dy=0 — `vstackAudioPlayheadLine` trusts `rect.dy + rect.dh` as the\n // bottom clamp bound for the playhead band, so an under-tall `dh` clamps\n // the band's bottom ABOVE its top, and the \"preserve height, slide\"\n // clamp math sends the whole band's top deep negative — the playhead\n // renders far above the staff instead of on it. Flooring `dh` at the\n // real bottom edge of the tallest system keeps the contract honest\n // regardless of which OSMD quirk under-reports the raw page size.\n const systemsBottom = Math.max(...systems.map((s) => s.y + s.h));\n const dh = Math.max(pageSize.height * f, systemsBottom);\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 // Published contract: hosts may locate the playhead (e.g. to keep it in\n // view inside their own scroll container) via [data-rmp-playhead]. The\n // element stays owned by this component — position/size are not API.\n playheadEl.dataset.rmpPlayhead = '1';\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({ topMarginPx: opts.followTopMarginPx });\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 ...(opts.osmdOptions?.autoBeam !== undefined ? { autoBeam: opts.osmdOptions.autoBeam } : {}),\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 applyMarks();\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 let markedCols: number[] = [];\n\n function applyMarks(): void {\n // Clear first: a re-engrave hands back different elements, and stale marks\n // would otherwise stay lit on notes nobody asked about.\n for (const el of root.querySelectorAll(`.${MARKED_NOTE_CLASS}`)) {\n el.classList.remove(MARKED_NOTE_CLASS);\n }\n if (!osmd) return;\n for (const col of markedCols) {\n for (const note of graphicalNotesAtColumn(osmd, col)) {\n const el = typeof note?.getSVGGElement === 'function' ? note.getSVGGElement() : null;\n if (el) el.classList.add(MARKED_NOTE_CLASS);\n }\n }\n }\n\n return {\n ready,\n setTime,\n markNotes(cols: number[] | null): void {\n markedCols = cols ?? [];\n applyMarks();\n },\n notePositions(): EngravedNote[] {\n return osmd ? engravedNotes(osmd, host) : [];\n },\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":[]}
@@ -238,7 +238,11 @@ declare function verovioOnsetColumns(root: Element, layout: NotationLayout, onse
238
238
  * `[]` for any missing/malformed structure (same defensive style as
239
239
  * `verovioNotationLayout`).
240
240
  */
241
- declare function verovioEngravedNotes(root: Element, host: HTMLElement, noteModel: Map<string, NoteModel>): EngravedNote[];
241
+ /** noteId 0-based onset index, from the player's own onset grouping (by
242
+ * `tMs`). The join key `EngravedNote.onsetId` uses so consumers never
243
+ * reconstruct onsets from x. */
244
+ declare function onsetIdByNoteId(onsets: readonly VerovioOnset[]): Map<string, number>;
245
+ declare function verovioEngravedNotes(root: Element, host: HTMLElement, noteModel: Map<string, NoteModel>, onsetIdById?: ReadonlyMap<string, number>): EngravedNote[];
242
246
  /**
243
247
  * Every rendered note/rest nearest engraved column `col`
244
248
  * (`measureIndex + fraction`, same units as `markNotes`' argument and
@@ -563,4 +567,4 @@ declare const MAX_ENGRAVE_WIDTH_VRV = 1400;
563
567
  * (in stave-web-sightread) §2 for the full design. */
564
568
  declare function createVerovioNotationPlayer(opts: CreateVerovioNotationPlayerOpts): VerovioNotationPlayer;
565
569
 
566
- export { type CreateVerovioNotationPlayerOpts, DEFAULT_MIN_FIT_FACTOR, EngravedNote, MARKED_NOTE_CLASS, MAX_ENGRAVE_WIDTH_VRV, MIN_AUTO_MEASURE_WIDTH_PX, VEROVIO_BASE_SCALE, VEROVIO_LAYOUT_DEFAULTS, VEROVIO_MAX_SCALE, VEROVIO_MIN_SCALE, type VerovioLayoutOptions, type VerovioNotationPlayer, type VerovioOnset, type VerovioRenderOptions, applyPrintObjectHiding, createVerovioNotationPlayer, fitZoomFactor, isBelowReadabilityFloor, nextReadabilityFloorBarsPerLine, readabilityFloorPlan, shouldFallbackToAutoBreaks, shouldReplanAutoBreaks, shouldUseCompactAutoMargins, systemMeasureCounts, verovioEngravedNotes, verovioNotationLayout, verovioNotesAtColumn, verovioOnsetColumns, verovioRenderOptions, verovioZoomOptions };
570
+ export { type CreateVerovioNotationPlayerOpts, DEFAULT_MIN_FIT_FACTOR, EngravedNote, MARKED_NOTE_CLASS, MAX_ENGRAVE_WIDTH_VRV, MIN_AUTO_MEASURE_WIDTH_PX, VEROVIO_BASE_SCALE, VEROVIO_LAYOUT_DEFAULTS, VEROVIO_MAX_SCALE, VEROVIO_MIN_SCALE, type VerovioLayoutOptions, type VerovioNotationPlayer, type VerovioOnset, type VerovioRenderOptions, applyPrintObjectHiding, createVerovioNotationPlayer, fitZoomFactor, isBelowReadabilityFloor, nextReadabilityFloorBarsPerLine, onsetIdByNoteId, readabilityFloorPlan, shouldFallbackToAutoBreaks, shouldReplanAutoBreaks, shouldUseCompactAutoMargins, systemMeasureCounts, verovioEngravedNotes, verovioNotationLayout, verovioNotesAtColumn, verovioOnsetColumns, verovioRenderOptions, verovioZoomOptions };
@@ -163,7 +163,18 @@ function verovioOnsetColumns(root, layout, onsets) {
163
163
  return void 0;
164
164
  }
165
165
  }
166
- function verovioEngravedNotes(root, host, noteModel) {
166
+ function onsetIdByNoteId(onsets) {
167
+ const distinct = [...new Set(onsets.map((o) => o.tMs))].sort((a, b) => a - b);
168
+ const indexOf = new Map(distinct.map((t, i) => [t, i]));
169
+ const out = /* @__PURE__ */ new Map();
170
+ for (const o of onsets) {
171
+ const i = indexOf.get(o.tMs);
172
+ if (i == null) continue;
173
+ for (const id of o.noteIds) if (!out.has(id)) out.set(id, i);
174
+ }
175
+ return out;
176
+ }
177
+ function verovioEngravedNotes(root, host, noteModel, onsetIdById) {
167
178
  try {
168
179
  const pageGeoms = computePageGeometries(root);
169
180
  if (!pageGeoms.size || !noteModel.size) return [];
@@ -203,6 +214,7 @@ function verovioEngravedNotes(root, host, noteModel) {
203
214
  staffIndex: nm.staffIndex,
204
215
  systemIndex: systemIndexOfBox(systems, box),
205
216
  durationReal: nm.durationReal,
217
+ onsetId: onsetIdById?.get(id) ?? null,
206
218
  x: offX + box.x + box.w / 2,
207
219
  y: offY + box.y + box.h / 2,
208
220
  w: box.w,
@@ -709,7 +721,7 @@ function createVerovioNotationPlayer(opts) {
709
721
  applyMarks();
710
722
  },
711
723
  notePositions() {
712
- return verovioEngravedNotes(svgHost, host, noteModel);
724
+ return verovioEngravedNotes(svgHost, host, noteModel, onsetIdByNoteId(rawOnsets));
713
725
  },
714
726
  setFollowEnabled(on) {
715
727
  follow.setEnabled(on);
@@ -757,6 +769,7 @@ export {
757
769
  fitZoomFactor,
758
770
  isBelowReadabilityFloor,
759
771
  nextReadabilityFloorBarsPerLine,
772
+ onsetIdByNoteId,
760
773
  readabilityFloorPlan,
761
774
  shouldFallbackToAutoBreaks,
762
775
  shouldReplanAutoBreaks,
@@ -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//\n// 0.40.0 — FULL API PARITY WITH notationPlayerSvg.ts (stave-web-sightread's\n// reading/recording stack can now swap backends without touching a call\n// site): `notePositions()`, `markNotes()`, the `[data-rmp-playhead]`\n// attribute, and `osmdOptions.autoBeam` (accepted + ignored, logged once —\n// Verovio always beams from the source MusicXML's own `<beam>` data, so\n// there is nothing for this option to toggle). The join that makes\n// `notePositions()`/`markNotes()` possible: `stampNoteIds`/`noteModelFromXml`\n// (./notationXml.ts, NEW) turn the input MusicXML into `id → NoteModel`\n// (pitch/rest/tie/staff/duration — the ground truth Verovio's rendered SVG\n// alone cannot supply), and `verovioEngravedNotes`/`verovioNotesAtColumn`\n// below join those ids to the live rendered `g.note`/`g.rest` elements —\n// same id-preservation guarantee `verovioOnsetColumns` already relies on\n// (see point 2 above), just consumed for notehead identity/geometry instead\n// of playhead columns. `musicXml` is stamped INTERNALLY (idempotent — a\n// caller that already ran it through stave's own `stampNoteIds` gets\n// byte-identical ids back) so this module never assumes the caller stamped\n// first. See ./notationXml.ts's own doc for why that scheme is duplicated\n// rather than imported from stave-web-sightread (wrong dependency\n// direction for a shared package) and `applyPrintObjectHiding`'s doc below\n// for the print-object empirical finding (Verovio honors it for notes, NOT\n// for rests).\n\nimport {\n vstackAudioPlayheadLine,\n distinctOnsets,\n hitTestMeasureAt,\n measureColumnsFromLayout,\n systemIndexOfBox,\n type NotationLayout,\n} from './scene/notationGeometry';\nimport type { Box, StaffMeasureBox } from './promo';\nimport { computeReflowScrollDelta, createFollowController } from './notationCommon';\nimport type { EngravedNote } from './notationPlayerSvg';\nimport {\n stampNoteIds,\n noteModelFromXml,\n injectSystemBreaks,\n balancedSystemBreaks,\n sectionAwareBreaks,\n autoSectionBreakPlan,\n fixedBarsPerLineBreaks,\n measureCount,\n type NoteModel,\n} from './notationXml';\n\n// Re-exported (TYPE-ONLY import above — erased at compile time, zero\n// runtime cost) so a caller that only imports from `notationPlayerVerovio`\n// still gets the SAME type `notePositions()` returns (identical shape to\n// notationPlayerSvg.ts's own export — not redefined here, to guarantee the\n// \"same exported interface\" contract can never drift between the two\n// SVG-family players).\nexport type { EngravedNote };\n\n/** Restated independently, not imported as a VALUE from notationPlayerSvg.ts\n * — same reasoning as `MAX_ENGRAVE_WIDTH_VRV` vs `MAX_ENGRAVE_WIDTH_SVG`\n * (below): a runtime (non-`type`) import from notationPlayerSvg.ts would\n * pull that module's ENTIRE implementation — including its own\n * `createSvgNotationPlayer` closure (harmless at runtime, since its OSMD\n * import stays lazy/dynamic either way) — into this entry's own tsup\n * chunk graph, which is exactly the cross-entry bundle coupling the\n * module doc's \"IMPORT PATH\" section (and the build report's dist-grep\n * gate) exists to prevent between the two SVG-family players. Must stay\n * byte-identical to `notationPlayerSvg.ts`'s own `MARKED_NOTE_CLASS` — a\n * test in this file's own suite pins that. */\nexport const MARKED_NOTE_CLASS = 'rmp-note-marked';\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 /** Parks a followed system this many px below the viewport top — pass the\n * height of any fixed top chrome (header, docked transport) plus a gap.\n * Default SYSTEM_TOP_MARGIN_PX. */\n followTopMarginPx?: number;\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 * Narrow passthrough matching `CreateSvgNotationPlayerOpts.osmdOptions`'s\n * shape exactly, so a caller driving BOTH players behind one interface\n * (the swap this player exists for) never has to branch per backend.\n * `autoBeam` has NO Verovio equivalent — Verovio always beams straight\n * from the MusicXML's own `<beam>` elements (it has no \"re-beam from\n * scratch\" pass the way OSMD's `autoBeam` option does) — so this is\n * accepted and silently ignored, with a ONE-TIME `console.warn` (module-\n * level, not per-instance — a caller that builds many players with the\n * same options object should not get spammed) rather than a hard error,\n * since ignoring it is genuinely harmless: synthesized rhythm XML with no\n * `<beam>` data renders unbeamed either way, which is a cosmetic\n * difference the caller can fix upstream (emit real `<beam>` elements)\n * rather than this player faking OSMD's re-beam heuristic.\n */\n osmdOptions?: { autoBeam?: boolean };\n /**\n * Passthrough for Verovio's own layout/line-breaking options — merged\n * UNDER `VEROVIO_LAYOUT_DEFAULTS` and OVER `scale`/`pageWidth`/\n * `adjustPageHeight` (see `verovioRenderOptions`'s own doc for the exact\n * merge order and why: a caller's `breaks` must be able to override the\n * default, but a caller can never smuggle in `scale`/`pageWidth` — those\n * stay derived from `zoom`/host width, not caller-suppliable). Stave's own\n * use case: inject `<print new-system=\"yes\"/>` into MusicXML and pass\n * `breaks: 'line'` to get exact N-bars-per-line, rather than Verovio's own\n * automatic line-breaking (see `VEROVIO_LAYOUT_DEFAULTS`'s doc for the\n * \"why\" behind the defaults themselves).\n */\n verovioOptions?: VerovioLayoutOptions;\n /**\n * TEST-ONLY SEAM — not part of this player's real production behavior.\n * When set, `engraveOnce`'s fit-pass/fallback-decision math reads system\n * widths from this function (called with `svgHost`) instead of\n * `currentLayout.systems`. Exists because jsdom implements neither\n * `getBBox` nor a real `getBoundingClientRect` (both always report\n * zero-size boxes), so `currentLayout.systems` widths — and therefore\n * `fitZoomFactor`'s result — are always 0/`1` (fits) in a headless test\n * regardless of how dense the underlying MusicXML actually is. A test can\n * pass a fixed fake (e.g. `() => [1200]`) to exercise the readability-floor\n * fallback (`VerovioLayoutOptions.minFitFactor`) end-to-end against a REAL\n * Verovio engrave, asserting on the resulting DOM shape\n * (`systemMeasureCounts`) rather than on geometry a real browser would be\n * needed to produce. Never used by any real caller.\n */\n measureSystemWidths?: (root: Element) => number[];\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 /**\n * Mark the engraved noteheads/rests nearest the given columns (same\n * `measureIndex + fraction-through-the-measure` units as `onsets`'\n * derived columns); pass `null` or `[]` to clear. Same contract and CSS\n * class (`MARKED_NOTE_CLASS` = `'rmp-note-marked'`) as\n * `SvgNotationPlayer.markNotes` — a consumer's existing CSS keeps working\n * unmodified across a backend swap. Survives re-engraving: reapplied\n * after every `setZoom`/`resize`.\n */\n markNotes(cols: number[] | null): void;\n /** Every engraved note/rest with MODEL identity (pitch, rest, tie, staff)\n * and host-relative notehead position — same `EngravedNote` shape\n * `SvgNotationPlayer.notePositions()` returns (re-exported from this\n * module, not redefined). `headEl` is Verovio's rendered `.notehead`\n * sub-group when present (a tighter box than the outer `g.note`, closer\n * in spirit to OSMD's `.vf-notehead`), else the outer `g.note`/`g.rest`\n * group. `[]` before the initial engrave resolves or under the\n * `rendered` test seam (no live SVG to join against — same graceful\n * degradation as `verovioOnsetColumns`). */\n notePositions(): EngravedNote[];\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).\n * `root` is carried alongside purely so `boxFromElement` can go straight to\n * `getBoundingClientRect()` diffing (see that function's own doc for why) —\n * `scale`/`offsetX`/`offsetY` stay as the page-validity check\n * (`pageGeometry`'s own \"is this page's markup well-formed\" guard) and are\n * no longer consumed for box math. */\ninterface PageGeom {\n scale: number;\n offsetX: number;\n offsetY: number;\n root: Element;\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, root };\n}\n\n/**\n * An element's box, in CSS px relative to the shared `root` —\n * `getBoundingClientRect()` diffed against `geom.root`'s own rect. Same\n * approach `notationPlayerSvg.ts` uses throughout for its OSMD geometry\n * (`hostRect`/`rootRect` diffing — see that module), chosen here for the\n * SAME reason: `getBoundingClientRect()` already resolves every ancestor\n * transform between the element and the viewport, so it needs no separate\n * per-page `scale`/`offsetX`/`offsetY` math layered on top.\n *\n * PRIOR BUG (found via the OSMD->Verovio default-backend swap's ghost-\n * placement acceptance check, ~17px off on real pieces): this used to do\n * `el.getBBox()` (Verovio's rendered user-unit space) converted via\n * `geom.offsetX/offsetY + bbox.x/y * geom.scale` — correct ONLY if the only\n * transform between `el` and the page's own outer `<svg>` is the page's own\n * placement + viewBox scale. Verovio's real output wraps EVERY page's\n * content in `<g class=\"page-margin\" transform=\"translate(500, 500)\">`\n * (confirmed against a real 6.2.0 render) — an ancestor transform `getBBox()`\n * does NOT bake in (`getBBox()` is local to the element's own user space,\n * before any ancestor's transform is applied) and the old formula never\n * accounted for. Empirically: `500 * scale` (~17px at this fixture's\n * `outerRect.width / viewBox.width` ratio) matched the observed drift\n * exactly in both axes — every note landed ~17px up-and-left of its real\n * rendered position. `getBoundingClientRect()` has no such blind spot: it\n * is the browser's own answer to \"where is this actually painted,\" immune\n * to however many ancestor groups carry their own transform.\n *\n * Null for anything that isn't a real element or has a degenerate\n * (zero-area) box — same defensive style the old implementation had.\n */\nfunction boxFromElement(el: Element, geom: PageGeom): Box | null {\n const r = safeRect(el);\n const rootRect = safeRect(geom.root);\n if (!r || !rootRect || !(r.width > 0) || !(r.height > 0)) return null;\n return { x: r.left - rootRect.left, y: r.top - rootRect.top, w: r.width, h: r.height };\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 * Rendered measure count per SYSTEM, in DOCUMENT ORDER (`.system` elements\n * across every `.vrv-page`, each one's OWN `.measure` descendant count) —\n * the widow-detection input for `engraveOnce`'s widow pass (see that\n * function's own doc). Deliberately PLAIN DOM traversal — no\n * `getBoundingClientRect`/geometry involved at all, unlike\n * `verovioNotationLayout`/`collectMeasureElements` (which both need a real\n * layout engine to report non-zero boxes) — so this works against ANY\n * rendered SVG DOM, including jsdom's own (which reports zero-size rects for\n * everything by default): a real Verovio render loaded into jsdom is enough\n * to exercise the widow pass end-to-end purely by DOM shape, with no\n * `getBoundingClientRect` mocking required (see this module's real-Verovio\n * widow test). Never throws; a root with no `.system` elements yields `[]`.\n */\nexport function systemMeasureCounts(root: Element): number[] {\n return Array.from(root.querySelectorAll('.system')).map((sysEl) => sysEl.querySelectorAll('.measure').length);\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 // Anchor the onset at the NOTEHEAD CENTER, matching the OSMD\n // backend's column semantics. The whole-group left edge biased every\n // time->x position (and so every ghost) ~half a notehead LEFT — the\n // group box also includes accidentals/stems (user-visible drift,\n // 2026-08-20).\n const headGlyph = noteEl ? (noteEl.querySelector('.notehead') ?? noteEl) : null;\n const box = headGlyph && geom ? boxFromElement(headGlyph, 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 anchorX = box.x + box.w / 2;\n const frac = denom > 0 ? Math.min(1, Math.max(0, (anchorX - 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// ─── Note geometry (notePositions) + column→note lookup (markNotes) ───────\n\n/**\n * Every rendered note/rest joined to its MODEL identity (`noteModel`, from\n * `noteModelFromXml` — ./notationXml.ts) by stamped id — this player's\n * `notePositions()`. Same shape and host-relative-px contract as\n * notationPlayerSvg.ts's `engravedNotes` (its own doc: \"Positions are px\n * relative to the HOST element, at the notehead's center\"), but the join\n * itself is simpler here: unlike OSMD (one graphical group per CHORD,\n * requiring the pitch-rank head-sorting `engravedNotes` does), Verovio\n * renders every chord member as its OWN `<g id=\"...\" class=\"note\">`\n * (empirically confirmed — see the module doc's point 2) — so this is a\n * flat id→element→model join, no grouping/sorting.\n *\n * `headEl` prefers the note's own `.notehead` child (Verovio's real nested\n * glyph group — empirically confirmed as `<g class=\"notehead\">` inside\n * `<g class=\"note\">`) over the outer `g.note`/`g.rest` wrapper when\n * present — a TIGHTER box (closer to notationPlayerSvg.ts's\n * `.vf-notehead`-only box) than the wrapper, which also spans the\n * stem/flag/accidental. Falls back to the wrapper itself for a rest (no\n * `.notehead` child) or if that lookup's own `getBBox` fails.\n *\n * `root` is the container holding every rendered `.vrv-page` (this\n * player's `svgHost`); `host` is the player's OWN host element — `x`/`y`\n * are computed relative to `host` (per `EngravedNote`'s contract), NOT to\n * `root`, which may itself sit offset within `host`. Never throws; returns\n * `[]` for any missing/malformed structure (same defensive style as\n * `verovioNotationLayout`).\n */\nexport function verovioEngravedNotes(\n root: Element,\n host: HTMLElement,\n noteModel: Map<string, NoteModel>,\n): EngravedNote[] {\n try {\n const pageGeoms = computePageGeometries(root);\n if (!pageGeoms.size || !noteModel.size) return [];\n const measureEntries = collectMeasureElements(pageGeoms);\n if (!measureEntries.length) return [];\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 hostRect = safeRect(host);\n const offX = rootRect && hostRect ? rootRect.left - hostRect.left : 0;\n const offY = rootRect && hostRect ? rootRect.top - hostRect.top : 0;\n\n const out: EngravedNote[] = [];\n for (const { el: measureEl, geom } of measureEntries) {\n const noteEls = Array.from(measureEl.querySelectorAll('g.note, g.rest'));\n for (const noteEl of noteEls) {\n const id = noteEl.getAttribute('id');\n if (!id) continue;\n const nm = noteModel.get(id);\n if (!nm) continue;\n\n const glyphEl = noteEl.querySelector('.notehead') ?? noteEl;\n let box = boxFromElement(glyphEl, geom);\n let headEl: Element = glyphEl;\n if (!box && glyphEl !== noteEl) {\n box = boxFromElement(noteEl, geom);\n headEl = noteEl;\n }\n if (!box) continue;\n\n out.push({\n midi: nm.midi,\n isRest: nm.isRest,\n tieContinuation: nm.tieContinuation,\n staffIndex: nm.staffIndex,\n systemIndex: systemIndexOfBox(systems, box),\n durationReal: nm.durationReal,\n x: offX + box.x + box.w / 2,\n y: offY + box.y + box.h / 2,\n w: box.w,\n h: box.h,\n headEl,\n });\n }\n }\n return out;\n } catch {\n return [];\n }\n}\n\n/**\n * Every rendered note/rest nearest engraved column `col`\n * (`measureIndex + fraction`, same units as `markNotes`' argument and\n * `noteCols`), one per staff of that measure — the Verovio-backend\n * counterpart to notationPlayerSvg.ts's `graphicalNotesAtColumn`. That\n * function searches OSMD's object model (`relInMeasureTimestamp` vs bar\n * duration); this searches the live rendered geometry instead (Verovio\n * exposes no per-element timestamp) — same \"nearest entry by position gap,\n * search every staff of the bar\" shape, just sourced from `getBBox` instead\n * of a timeline field. The fractional-position FORMULA itself\n * (`(box.x - sx) / denom`) is the exact one `verovioOnsetColumns` already\n * uses (not re-derived) — this function runs it in the opposite direction\n * (nearest note TO a column, rather than a column FROM a note id).\n */\nexport function verovioNotesAtColumn(root: Element, layout: NotationLayout, col: number): Element[] {\n try {\n if (!Number.isFinite(col)) return [];\n const cols = measureColumnsFromLayout(layout.measures);\n if (!cols.length) return [];\n const measureIndex = Math.max(0, Math.min(cols.length - 1, Math.floor(col)));\n const wanted = col - measureIndex;\n\n const pageGeoms = computePageGeometries(root);\n const measureEntries = collectMeasureElements(pageGeoms);\n const entry = measureEntries[measureIndex];\n if (!entry) return [];\n const m = cols[measureIndex];\n const sx = Math.min(m.noteStartX, m.x + m.w);\n const denom = m.x + m.w - sx;\n\n const found: Element[] = [];\n for (const staffEl of Array.from(entry.el.querySelectorAll('g.staff'))) {\n const noteEls = Array.from(staffEl.querySelectorAll('g.note, g.rest'));\n let best: Element | null = null;\n let bestGap = Number.POSITIVE_INFINITY;\n for (const noteEl of noteEls) {\n const box = boxFromElement(noteEl, entry.geom);\n if (!box) continue;\n const frac = denom > 0 ? (box.x - sx) / denom : 0;\n const gap = Math.abs(frac - wanted);\n if (gap < bestGap) {\n bestGap = gap;\n best = noteEl;\n }\n }\n if (best) found.push(best);\n }\n return found;\n } catch {\n return [];\n }\n}\n\n/**\n * CRITICAL empirical finding (verified against a real 6.2.0 render — see\n * the migration spike's follow-up, `.superpowers/…/print-object-test.mjs`\n * equivalent run for this task): Verovio's MusicXML importer HONORS\n * `print-object=\"no\"` for a pitched `<note>` (renders `visibility=\"hidden\"`\n * on its own `<g class=\"note\">`) but does NOT honor it for a `<note><rest/>`\n * — the `<g class=\"rest\">` renders fully visible regardless. This matters\n * because stave-web-sightread's `hideDoubledNotes` feature sets\n * `print-object=\"no\"` on BOTH doubled notes AND the rests of a voice that\n * lost every visible note (see that function's own \"second pass\" doc) —\n * without this fix, a hidden-doubled-note's voice would still show a\n * floating rest, exactly the \"extra voice\" clutter that feature exists to\n * remove.\n *\n * Fix: force `visibility=\"hidden\"` on every rendered id whose model says\n * `hidden` (`NoteModel.hidden`, from `noteModelFromXml`). Re-applying it to\n * a NOTE Verovio already hid itself is a harmless no-op; applying it to a\n * REST is the actual fix. Call after every render (`renderAllPages`) — a\n * fresh render is a fresh DOM, so a prior call's effect never carries over\n * (nothing to \"undo\" on a note/rest that's no longer print-object=\"no\").\n * Never throws.\n */\nexport function applyPrintObjectHiding(root: Element, noteModel: Map<string, NoteModel>): void {\n try {\n const doc = root.ownerDocument;\n if (!doc || !noteModel.size) return;\n for (const [id, nm] of noteModel) {\n if (!nm.hidden) continue;\n const el = doc.getElementById(id);\n if (el) el.setAttribute('visibility', 'hidden');\n }\n } catch {\n /* best-effort — a render this can't patch stays as Verovio drew it */\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 * Verovio layout/line-breaking options a caller may override — see\n * `CreateVerovioNotationPlayerOpts.verovioOptions`'s doc for the merge\n * contract and Stave's own motivating use case (encoded `<print\n * new-system=\"yes\"/>` breaks + `breaks: 'line'`, exact N-bars-per-line).\n * Deliberately NARROWER than Verovio's full option surface — only the knobs\n * this integration has an actual caller for; add more here (not a raw\n * `Record<string, unknown>` passthrough) as real needs arise, so a typo in a\n * caller's options object is a compile error, not a silently-ignored no-op.\n */\nexport interface VerovioLayoutOptions {\n breaks?: 'none' | 'auto' | 'line' | 'smart' | 'encoded';\n breaksSmartSb?: number;\n breaksNoWidow?: boolean;\n minLastJustification?: number;\n spacingSystem?: number;\n spacingStaff?: number;\n pageMarginLeft?: number;\n pageMarginRight?: number;\n /**\n * Player-level widow guard — see `engraveOnce`'s own \"Widow pass\" doc.\n * NOT the same mechanism as `breaksNoWidow` above (Verovio's own option,\n * which only prevents a lone measure on the last PAGE — confirmed on a\n * live page to do nothing for a lone measure on the last SYSTEM of a\n * single-page excerpt, the common case for this player). This is a\n * PLAYER option, not a Verovio one: it is stripped out in\n * `verovioRenderOptions` before `setOptions` ever sees it (Verovio has no\n * such option of its own to receive it). Default `true` — every engrave\n * gets the widow guard unless a caller opts out. Only takes effect when\n * the EFFECTIVE `breaks` is `'auto'` (this object's `breaks`, or\n * `VEROVIO_LAYOUT_DEFAULTS.breaks` when unset) — a caller who already\n * encoded exact break points (`breaks: 'line'`, e.g. Stave's own\n * per-line-break usage) has made their own layout choice, which this pass\n * never second-guesses.\n */\n avoidWidows?: boolean;\n /**\n * Readability floor for CALLER-ENCODED breaks (`breaks: 'line'` or\n * `'encoded'`) — see `engraveOnce`'s own \"Fallback pass\" doc. Stave's own\n * motivating case: `<print new-system=\"yes\"/>` every 4 bars + `breaks:\n * 'line'` gets exact N-bars-per-line on normal music, but on dense music\n * (e.g. a Bach fugue excerpt) the one system between two encoded breaks\n * can be too wide to fit even at Verovio's minimum spacing — the ONLY\n * lever `fitZoomFactor` then has left is shrinking the effective zoom,\n * which on a dense-enough system means unreadably small glyphs. Good\n * sight-reading software prefers readable glyphs over a fixed bar count.\n *\n * A PLAYER option, not a Verovio one — like `avoidWidows`, stripped out in\n * `verovioRenderOptions` before `setOptions` ever sees it. Default 0.75\n * (see `shouldFallbackToAutoBreaks`'s own doc for the exact trigger\n * condition). `minFitFactor: 0` disables the fallback entirely — a caller\n * who always wants their exact encoded bar count, however small the\n * glyphs get, can opt back into the pre-existing behavior.\n */\n minFitFactor?: number;\n /**\n * BREAK PLAN — target bars per line. When set (`> 0`), the player plans\n * system breaks itself (`sectionAwareBreaks`, balanced with a widow\n * back-off) and encodes them into the document before the first engrave,\n * forcing `breaks: 'line'`. Leave unset (or `0`) to let Verovio's own\n * `breaks: 'auto'` decide, which is the default and is content-aware.\n *\n * Combine with `sectionStarts` to keep the balancing inside each section.\n * A PLAYER option, not a Verovio one — stripped in `verovioRenderOptions`\n * before `setOptions` sees it.\n */\n barsPerLine?: number;\n /**\n * BREAK PLAN — 0-based measure POSITIONS at which a musical section\n * begins. Each one > 0 becomes a hard system break, so a section always\n * starts its own line; `barsPerLine`'s balancing then runs within each\n * section's span rather than across the whole excerpt.\n *\n * POSITIONS, not MusicXML `<measure number>` attributes — scores skip bar\n * numbers, so a caller holding bar-numbered section labels must convert\n * first (`measureCount`'s doc explains the coordinate). Setting this alone\n * (no `barsPerLine`) is the AUTO + SECTIONS mode: the document is engraved\n * under `breaks: 'auto'` exactly as with no plan (compact margins, widow\n * pass, readability floor), then — if a section start is not already a\n * system start — re-planned ONCE at auto's own settled density with a hard\n * break at each section (`autoSectionBreakPlan`), so the engraver still\n * decides how many bars fit a line. A PLAYER option — stripped in\n * `verovioRenderOptions`.\n */\n sectionStarts?: readonly number[];\n}\n\n/**\n * Layout defaults applied to EVERY engrave (initial + every reflow) unless a\n * caller's own `verovioOptions` overrides a given key — see\n * `verovioRenderOptions`'s doc for the merge order.\n *\n * WHY: Verovio only justifies a page's LAST system when its unstretched\n * width is already ≥ `minLastJustification` (Verovio's own default: 0.8) of\n * the page width — confirmed empirically (a 4-bar-of-whole-notes fixture at\n * pageWidth 2400 renders its single system at ~41% of the page width with\n * Verovio's own default, ~96% with `minLastJustification: 0`; see this\n * module's real-Verovio justification test). A short excerpt — the common\n * case for both this player's normal usage (a few bars at a time) AND\n * Stave's per-line-break usage (section below) — is ALWAYS a \"last system\"\n * (it's the only one), so Verovio's default leaves it looking\n * left-justified/shrunk rather than filling the available width. Overriding\n * to 0 makes every system justify unconditionally, matching this player's\n * own \"fill the host width\" semantic (`verovioZoomOptions`'s own doc).\n * `breaksNoWidow: true` prevents Verovio leaving a single trailing bar\n * orphaned on its own system (a related \"short excerpt\" artifact, same\n * spirit as the justification fix). `breaks: 'auto'` (Verovio's own default\n * line-breaking algorithm) is the base default; Stave overrides it to\n * `'line'` when it has ALREADY encoded exact break points (see\n * `VerovioLayoutOptions`'s doc) — see the \"caller's `breaks` wins\" case in\n * `verovioRenderOptions`'s own tests.\n */\nexport const VEROVIO_LAYOUT_DEFAULTS = {\n breaks: 'auto',\n minLastJustification: 0,\n breaksNoWidow: true,\n} as const;\n\n/**\n * The full `toolkit.setOptions(...)` argument for one engrave: layout\n * defaults, overridden by the caller's own `layout` (a caller's `breaks`\n * WINS over `VEROVIO_LAYOUT_DEFAULTS.breaks` — this is the whole point of\n * exposing the override), overridden AGAIN by `scale`/`pageWidth` (derived\n * from `hostWidthPx`/`zoom` via `verovioZoomOptions` — a caller's\n * `verovioOptions` has no `scale`/`pageWidth` keys per `VerovioLayoutOptions`\n * own (narrower) type, so this last spread is really just making the\n * derived-vs-defaulted precedence explicit, not fighting a real collision)\n * and `adjustPageHeight: true` (always on — this player's own \"whole score,\n * page-flow, no pagination\" framing, see the module doc's \"PAGES\" section).\n * Pure — exists so the merge itself is unit-testable without a DOM or a real\n * Verovio toolkit (`tests/notationPlayerVerovio.test.ts`'s \"options merge\"\n * tests call this directly).\n *\n * `avoidWidows`/`minFitFactor` (see `VerovioLayoutOptions`'s own docs) are\n * PLAYER options, not Verovio ones — destructured out here and never\n * forwarded to `setOptions`, same \"narrow the passthrough\" spirit as this\n * function only accepting `VerovioLayoutOptions`'s typed surface at all.\n *\n * `pageHeight: 60000` (Verovio's own max) + `pageMarginTop`/`pageMarginBottom:\n * 0` are unconditional, like `adjustPageHeight` — not in `VerovioLayoutOptions`\n * at all, so a caller cannot override them. WHY: this player's own \"whole\n * score, page-flow, no pagination\" framing (module doc, \"PAGES\" section)\n * renders every Verovio PAGE as its own `.vrv-page` block; Verovio's default\n * page height (~2970, a real printed-page height) paginates a long score\n * into several such blocks with a page-margin gap between them, which reads\n * as a broken PDF rather than one continuous score. A tall-enough\n * `pageHeight` (combined with `adjustPageHeight: true`, already unconditional\n * above) keeps `getPageCount() === 1` regardless of how long the piece is —\n * confirmed against a real 40-bar fixture (2 pages at Verovio's own default\n * height, 1 page at 60000 — see this module's real-Verovio pageHeight test).\n * Zeroing the page margins removes the (now pointless, since there is only\n * ever one page) top/bottom whitespace Verovio would otherwise still budget\n * for a \"page\".\n */\nexport function verovioRenderOptions(\n hostWidthPx: number,\n zoom: number,\n layout?: VerovioLayoutOptions,\n): Record<string, unknown> {\n const { scale, pageWidth } = verovioZoomOptions(hostWidthPx, zoom);\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const {\n avoidWidows: _avoidWidows,\n minFitFactor: _minFitFactor,\n barsPerLine: _barsPerLine,\n sectionStarts: _sectionStarts,\n ...verovioLayout\n } = layout ?? {};\n return {\n ...VEROVIO_LAYOUT_DEFAULTS,\n ...verovioLayout,\n scale,\n pageWidth,\n adjustPageHeight: true,\n pageHeight: 60000,\n pageMarginTop: 0,\n pageMarginBottom: 0,\n };\n}\n\n/**\n * 1 when every system already fits within `engraveWidthPx` (widest ≤ width\n * × 1.01 — a small tolerance for sub-pixel rounding in the DOM geometry, not\n * a real \"close enough\" fudge); otherwise `engraveWidthPx / widest` (< 1) —\n * the zoom-scaling factor that would bring the widest system back down to\n * exactly `engraveWidthPx`. Never NaN/Infinity: an empty `systemWidthsPx`,\n * a non-finite/non-positive widest width, or a non-finite/non-positive\n * `engraveWidthPx` all return 1 (the safe \"don't touch the zoom\" default —\n * see the module doc's \"Fit-to-width\" section for how the caller uses this:\n * a factor < 1 triggers exactly ONE re-engrave at `requestedZoom * factor`,\n * never a loop).\n *\n * WHY THIS IS NEEDED (see `VEROVIO_LAYOUT_DEFAULTS`'s doc for the\n * complementary justification fix): with ENCODED breaks (`breaks: 'line'` +\n * Stave's own `<print new-system=\"yes\"/>` markers), Verovio must put\n * whatever notes fall between two encoded break points onto one system —\n * unlike `breaks: 'auto'`, it cannot relieve overcrowding by moving a\n * measure to the next line. If that system is too dense to fit\n * `engraveWidthPx` even at Verovio's own minimum inter-note spacing, Verovio\n * renders it WIDER than the requested page width instead of silently\n * clipping it. Shrinking the effective zoom (smaller glyphs, smaller\n * spacing) is the only lever left to bring it back within the host's\n * available width.\n */\nexport function fitZoomFactor(systemWidthsPx: readonly number[], engraveWidthPx: number): number {\n if (!systemWidthsPx.length) return 1;\n if (!(engraveWidthPx > 0) || !Number.isFinite(engraveWidthPx)) return 1;\n const widest = Math.max(...systemWidthsPx);\n if (!(widest > 0) || !Number.isFinite(widest)) return 1;\n if (widest <= engraveWidthPx * 1.01) return 1;\n const factor = engraveWidthPx / widest;\n return Number.isFinite(factor) && factor > 0 ? factor : 1;\n}\n\n/** Default `VerovioLayoutOptions.minFitFactor` — see that field's own doc. */\nexport const DEFAULT_MIN_FIT_FACTOR = 0.75;\n\n/**\n * Pure decision for the readability floor (`VerovioLayoutOptions.minFitFactor`,\n * `engraveOnce`'s \"Fallback pass\") — true exactly when ALL of:\n * - `factor` (a `fitZoomFactor` result) is finite and strictly below\n * `minFitFactor`;\n * - `minFitFactor` is a positive, finite floor (0 — or anything\n * non-positive/non-finite — disables the fallback unconditionally, the\n * documented opt-out);\n * - `breaks` is `'line'` or `'encoded'` — the two ways a caller can hand\n * Verovio EXACT, pre-decided break points (as opposed to `'auto'`/\n * `'smart'`/`'none'`/unset, where Verovio already owns the line-breaking\n * decision and there is no \"caller's encoded bar count\" to fall back\n * FROM in the first place).\n *\n * Never throws; no DOM/Verovio access — this is the single source of truth\n * `engraveOnce` calls after every fit-factor computation, so the trigger\n * condition is unit-testable independent of a real engrave.\n */\nexport function shouldFallbackToAutoBreaks(\n factor: number,\n breaks: VerovioLayoutOptions['breaks'] | undefined,\n minFitFactor: number,\n): boolean {\n if (!isBelowReadabilityFloor(factor, minFitFactor)) return false;\n if (breaks !== 'line' && breaks !== 'encoded') return false;\n return true;\n}\n\n/** True when fitting the current engraving would cross the caller's\n * readability floor. Shared by the encoded-break fallback and auto's\n * narrower re-plan path so the two policies cannot drift. */\nexport function isBelowReadabilityFloor(factor: number, minFitFactor: number): boolean {\n return Number.isFinite(factor) && Number.isFinite(minFitFactor) && minFitFactor > 0 && factor < minFitFactor;\n}\n\n/** Auto can fit geometrically without ever entering the fit-zoom pass. This\n * measured per-measure width is the companion floor for that case: at a\n * 390px viewport the notation host is about 324–356px wide, so a 4-bar line\n * is only ~80px/bar; a 1-bar line preserves a usable engraving scale. */\nexport const MIN_AUTO_MEASURE_WIDTH_PX = 180;\n\n/** Auto needs a narrower plan either when the normal fit floor would be\n * crossed, or when the rendered systems show too little width per measure.\n * The latter is based on rendered layout, never a viewport breakpoint. */\nexport function shouldReplanAutoBreaks(\n factor: number,\n minFitFactor: number,\n systemWidthsPx: readonly number[],\n systemMeasureCounts: readonly number[],\n): boolean {\n if (isBelowReadabilityFloor(factor, minFitFactor)) return true;\n return systemWidthsPx.some((width, i) => {\n const measures = systemMeasureCounts[i] ?? 0;\n return Number.isFinite(width) && width > 0 && measures > 0 && width / measures < MIN_AUTO_MEASURE_WIDTH_PX;\n });\n}\n\n/** Verovio's printed-page default horizontal margins are proportionally too\n * large for a narrow *measured* engraving: a fully justified one-bar system\n * can still occupy under 90% of its host. Keep the established desktop\n * coordinate system unless the rendered result proves it needs the compact\n * margins; this is a layout measurement, not a viewport breakpoint. */\nexport function shouldUseCompactAutoMargins(\n systemWidthsPx: readonly number[],\n engraveWidthPx: number,\n): boolean {\n return Number.isFinite(engraveWidthPx) && engraveWidthPx > 0 &&\n systemWidthsPx.some((width) => Number.isFinite(width) && width > 0 && width < engraveWidthPx * 0.9);\n}\n\n/** One narrower candidate for the auto-break readability floor, or null when\n * the measured fit already holds the floor (or one bar is the terminal\n * layout). Uses ceiling-halves so 4 -> 2 -> 1 and 3 -> 2 -> 1. */\nexport function nextReadabilityFloorBarsPerLine(\n currentBarsPerLine: number,\n fittedZoomFactor: number,\n minFitFactor: number,\n measuredDensityBelowFloor = false,\n): number | null {\n if (!measuredDensityBelowFloor && !isBelowReadabilityFloor(fittedZoomFactor, minFitFactor)) return null;\n if (!Number.isFinite(currentBarsPerLine) || currentBarsPerLine <= 1) return null;\n return Math.max(1, Math.ceil(currentBarsPerLine / 2));\n}\n\n/**\n * Pure planner seam for the width-aware auto floor. `fittedZoomAt` is the\n * measured-layout oracle: production obtains each measurement by engraving\n * that candidate, while jsdom-free tests can supply deterministic factors.\n * The returned list is the sequence to try; its final `1` is terminal.\n */\nexport function readabilityFloorPlan(\n initialBarsPerLine: number,\n widthPx: number,\n minFitFactor: number,\n fittedZoomAt: (barsPerLine: number, widthPx: number) => number,\n): number[] {\n if (!Number.isFinite(initialBarsPerLine) || initialBarsPerLine <= 0 || !(widthPx > 0)) return [];\n let bars = Math.max(1, Math.floor(initialBarsPerLine));\n let factor = fittedZoomAt(bars, widthPx);\n const plan: number[] = [];\n for (;;) {\n const next = nextReadabilityFloorBarsPerLine(bars, factor, minFitFactor);\n if (next === null) return plan;\n plan.push(next);\n bars = next;\n factor = fittedZoomAt(bars, widthPx);\n if (bars === 1) return plan;\n }\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/**\n * Module-level SERIAL queue over the shared toolkit — bug E7M-322: in\n * production, remounting a player component several times in quick\n * succession (destroying each instance before its `ready` resolved, EVERY\n * instance awaiting the SAME shared `getVerovioToolkit()` promise) produced\n * an Emscripten \"null function\" error from the WASM bridge. `withToolkit`\n * makes every `setOptions -> loadData -> getPageCount/renderToSVG` sequence,\n * across EVERY live player, run to completion strictly one at a time — even\n * across the async wasm-load boundary the very first call incurs. Each\n * queued task is chained off the PREVIOUS task's own settled promise (not\n * off `toolkitPromise` directly), so two tasks queued back-to-back before\n * the toolkit even exists yet still serialize correctly once it resolves;\n * without this, both would `await` the same not-yet-resolved\n * `getVerovioToolkit()` promise and their synchronous toolkit-touching code\n * could run in whatever order the two continuations happen to be scheduled,\n * which is exactly the \"several players hammering the one shared instance\n * at once\" shape the production bug had.\n *\n * `fn` MUST stay synchronous (no `await` inside it) — same \"no interleaving\n * because no await between reclaim and render\" rule `reflow`'s own doc\n * already relied on; the queue is what now makes that rule hold ACROSS\n * players' tasks, not just within one player's own call. A task that has\n * gone stale while queued (its player was destroyed while waiting for its\n * turn) must check `destroyed` as the FIRST thing inside `fn` and return a\n * no-op result — `withToolkit` itself has no opinion on staleness, only\n * serialization (see `initialEngrave`/`reflow`'s own `fn` bodies).\n *\n * A rejecting task never wedges the queue for tasks queued after it (the\n * internal chain swallows the rejection); the ORIGINAL caller still sees\n * their own task's rejection via the promise `withToolkit` returns, since\n * that is tracked separately from the internal queue chain.\n */\nlet toolkitQueue: Promise<unknown> = Promise.resolve();\n\nfunction withToolkit<T>(fn: (toolkit: VerovioToolkitInstance) => T): Promise<T> {\n const run = toolkitQueue.then(() => getVerovioToolkit()).then((toolkit) => fn(toolkit));\n toolkitQueue = run.then(\n () => undefined,\n () => undefined,\n );\n return run;\n}\n\n// ─── createVerovioNotationPlayer ────────────────────────────────────────────\n\n// Module-level (not per-instance) — see `osmdOptions`'s doc: a caller\n// building many players with the same options object should see this once\n// per SESSION, not once per player.\nlet loggedAutoBeamIgnored = false;\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. Raised 1200 -> 1400: Stave's encoded-break usage\n * (`verovioOptions.breaks: 'line'`, exact N-bars-per-line) wants more\n * breathing room per system than the original OSMD-parity cap allowed\n * before glyphs get cramped at typical desktop widths. */\nexport const MAX_ENGRAVE_WIDTH_VRV = 1400;\n/** Floor so a not-yet-laid-out / zero-width host never engraves at 0px. */\nconst MIN_ENGRAVE_WIDTH_VRV = 280;\n/** Used only for the terminal auto-floor plan. The regular page margin is\n * intentionally left unchanged so desktop geometry/playhead alignment stays\n * on Verovio's established coordinate system. */\n// Verovio treats 0 as \"unset\" and restores its 500-unit default; 1 is the\n// smallest honored value and is visually equivalent to zero here.\nconst AUTO_FLOOR_PAGE_MARGIN = 1;\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 if (opts.osmdOptions?.autoBeam !== undefined && !loggedAutoBeamIgnored) {\n loggedAutoBeamIgnored = true;\n // eslint-disable-next-line no-console\n console.warn(\n '[notationPlayerVerovio] osmdOptions.autoBeam has no Verovio equivalent (Verovio always beams from the ' +\n 'MusicXML <beam> data) — ignored.',\n );\n }\n\n // Stamp once, up front — pure/idempotent (./notationXml.ts's doc), so\n // re-stamping a caller's already-stamped xml (e.g. stave's own transform\n // pipeline) reproduces the exact same ids. `noteModel` is likewise a pure\n // function of this same stamped string, computed once and reused across\n // every re-engrave (`setZoom`/`resize` never change note IDENTITY, only\n // geometry). The `rendered` test seam has no real document to stamp/model\n // against — `notePositions()`/`markNotes()` degrade gracefully to `[]`/\n // no-op there, same as `verovioOnsetColumns` already does for that seam.\n const stampedXml = opts.rendered ? musicXml : stampNoteIds(musicXml);\n const noteModel: Map<string, NoteModel> = opts.rendered ? new Map() : noteModelFromXml(stampedXml);\n\n // ── BREAK PLAN (`VerovioLayoutOptions.barsPerLine` / `.sectionStarts`) ──\n // Break POLICY lives in this layer, not in the consumer. A caller's\n // explicit bars-per-line target and/or section starts are computed ONCE\n // here and encoded into `plannedXml`, which every engrave renders with\n // `breaks: 'line'`. Auto's readability-floor plan is deliberately separate\n // below: it is computed once per (document, measured width) from stampedXml.\n //\n // Computing it from `stampedXml` once, rather than per engrave from\n // whatever was last rendered, is what makes repeated engraves idempotent:\n // a zoom change or reflow can never compound a second set of `<print>`s\n // onto an already-broken document. `injectSystemBreaks` is itself\n // idempotent, so even a double application would be harmless — but the\n // plan positions would be wrong the second time, having been computed\n // against a document whose layout already changed.\n //\n // No plan options → `plannedXml === stampedXml` and nothing below changes:\n // `breaks: 'auto'` plus the widow pass, exactly as before.\n const measureTotal = measureCount(stampedXml);\n // `sectionStarts` WITHOUT `barsPerLine` is not a plan up front: it is the\n // auto pipeline plus one re-plan at auto's settled density — see the\n // \"AUTO + SECTIONS PASS\" in `engraveOnce`.\n const autoWithSections = (opts.verovioOptions?.sectionStarts?.length ?? 0) > 0\n && !((opts.verovioOptions?.barsPerLine ?? 0) > 0);\n const breakPlan: number[] = (() => {\n const layout = opts.verovioOptions;\n const starts = layout?.sectionStarts ?? [];\n const every = layout?.barsPerLine ?? 0;\n if (!starts.length && !(every > 0)) return [];\n if (autoWithSections) return [];\n // A concrete caller preference (2/4/8) is a maximum, not a suggestion:\n // never widen 2 bars per line into 3 merely to avoid a final widow.\n if (!starts.length && every > 0) return fixedBarsPerLineBreaks(measureTotal, every);\n return sectionAwareBreaks(measureTotal, starts, every);\n })();\n const plannedXml = breakPlan.length ? injectSystemBreaks(stampedXml, breakPlan) : stampedXml;\n\n function autoFloorPlanXml(barsPerLine: number): string {\n // The normal planner intentionally avoids one-measure widows. At the\n // floor's terminal state that would undo the whole point, so force every\n // measure onto its own system instead.\n const positions = barsPerLine <= 1\n ? Array.from({ length: Math.max(0, measureTotal - 1) }, (_, i) => i + 1)\n : sectionAwareBreaks(measureTotal, [], barsPerLine);\n return positions.length ? injectSystemBreaks(stampedXml, positions) : stampedXml;\n }\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 // Published contract: hosts may locate the playhead (e.g. to keep it in\n // view inside their own scroll container) via [data-rmp-playhead] — same\n // attribute notationPlayerSvg.ts sets, same reasoning (see its own\n // comment). The element stays owned by this component — position/size\n // are not API.\n playheadEl.dataset.rmpPlayhead = '1';\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({ topMarginPx: opts.followTopMarginPx });\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 // Verovio honors print-object=\"no\" for pitched notes on its own but NOT\n // for rests (empirical finding — see applyPrintObjectHiding's own doc);\n // this patches the gap. Every fresh render is a fresh DOM, so this must\n // run after EVERY renderAllPages call, not just the initial one.\n applyPrintObjectHiding(svgHost, noteModel);\n }\n\n // ─── markNotes (survives re-engraving — reapplied after every render) ──\n\n let markedCols: number[] = [];\n\n function applyMarks(): void {\n for (const el of svgHost.querySelectorAll(`.${MARKED_NOTE_CLASS}`)) {\n el.classList.remove(MARKED_NOTE_CLASS);\n }\n if (!currentLayout || !markedCols.length) return;\n for (const col of markedCols) {\n for (const el of verovioNotesAtColumn(svgHost, currentLayout, col)) {\n el.classList.add(MARKED_NOTE_CLASS);\n }\n }\n }\n\n /**\n * WIDOW PASS helper (E7M — \"good sight-reading software never shows a\n * one-bar last line\") — extracted so `engraveOnce` can run it from TWO\n * places: after the normal first pass (breaks:'auto', as before), AND\n * after the readability-floor fallback pass forces `breaks: 'auto'` on a\n * caller-encoded document (see `engraveOnce`'s own \"Fallback pass\" doc).\n * Assumes `xml`/`layoutOpts` is ALREADY loaded+rendered into\n * `svgHost`/`currentLayout` (the caller just did that) — this only reads\n * `svgHost`'s current DOM shape and, if a widow is found, re-engraves.\n *\n * Verovio's own `breaksNoWidow` (in `VEROVIO_LAYOUT_DEFAULTS`) only\n * prevents a lone measure on the last PAGE — verified on a live page that\n * a 6-bar excerpt under `breaks: 'auto'` still renders as systems of 5+1\n * within a SINGLE page. This reads the rendered measure count per system\n * straight from the DOM (`systemMeasureCounts` — plain `.system`/\n * `.measure` traversal, no geometry needed). If there are ≥ 2 systems and\n * the LAST one has exactly 1 measure, it computes evenly-spread break\n * points for the SAME number of systems (`balancedSystemBreaks`) and\n * re-engraves ONCE with those breaks encoded (`injectSystemBreaks` +\n * `breaks: 'line'`) in place of `xml`/`layoutOpts`'s own render —\n * `injectSystemBreaks` only ever ADDS `<print>` elements (never touches a\n * `<note>`), so this never disturbs `stampedXml`'s own note ids /\n * `noteModel` join (pinned by this file's own re-balanced-render test).\n *\n * Returns the (xml, layoutOpts) pair that now reflects `svgHost`'s\n * content: unchanged on \"no widow found\" or \"re-engrave failed\" (the\n * caller's own already-rendered pass is left in place — same \"degrade to\n * the prior render, never blank\" style as the rest of `engraveOnce`), or\n * the widow-fixed pair on success.\n */\n function runWidowPass(\n toolkit: VerovioToolkitInstance,\n widthPx: number,\n zoom: number,\n xml: string,\n layoutOpts: VerovioLayoutOptions | undefined,\n ): { xml: string; layoutOpts: VerovioLayoutOptions | undefined } {\n const counts = systemMeasureCounts(svgHost);\n if (counts.length >= 2 && counts[counts.length - 1] === 1) {\n const totalMeasures = counts.reduce((a, b) => a + b, 0);\n const breakPositions = balancedSystemBreaks(totalMeasures, counts.length);\n if (breakPositions.length) {\n const widowXml = injectSystemBreaks(xml, breakPositions);\n const widowLayoutOpts: VerovioLayoutOptions = { ...layoutOpts, breaks: 'line' };\n toolkit.setOptions(verovioRenderOptions(widthPx, zoom, widowLayoutOpts));\n const okWidow = !!toolkit.loadData(widowXml);\n if (okWidow) {\n renderAllPages(toolkit);\n rebuildLayoutFromDom();\n return { xml: widowXml, layoutOpts: widowLayoutOpts };\n }\n // A failed widow re-engrave leaves the caller's own render (still in\n // svgHost/currentLayout from before this call) in place.\n }\n }\n return { xml, layoutOpts };\n }\n\n /**\n * Engrave `stampedXml` into the shared toolkit at (`widthPx`, `zoom`),\n * read the resulting layout back from the DOM, run the WIDOW PASS\n * (`runWidowPass`, above), then — if the effective breaks are still\n * caller-encoded (`'line'`/`'encoded'`) and the fit factor is below the\n * readability floor — run the FALLBACK PASS (below); finally, if whatever\n * ended up rendered produced a system wider than the page, re-engrave ONCE\n * more at a proportionally smaller effective zoom (see `fitZoomFactor`'s\n * own doc for why this can happen and the module doc's \"Fit-to-width\"\n * section). MUST run synchronously start-to-finish (no `await` inside) —\n * this is the `fn` passed to `withToolkit`, and its\n * single-sequence-at-a-time guarantee depends on that (see `withToolkit`'s\n * own doc). `currentZoom` is intentionally NOT updated here for the fit\n * correction — only the caller (`initialEngrave`/`reflow`) tracks the\n * user's REQUESTED zoom, so a later `setZoom(z)` steps from that requested\n * value, not from whatever the fit correction happened to render at; the\n * fit is recomputed fresh on every engrave rather than remembered. Returns\n * whether the FIRST pass's `loadData` succeeded — a failed later\n * re-engrave (rare) just leaves the most-recent successful pass's\n * already-rendered content in place rather than blanking the player.\n *\n * FALLBACK PASS (readability floor — `VerovioLayoutOptions.minFitFactor`,\n * default `DEFAULT_MIN_FIT_FACTOR`): with ENCODED breaks (Stave's own\n * `<print new-system=\"yes\"/>` + `breaks: 'line'` usage), Verovio must put\n * whatever notes fall between two encoded break points onto one system,\n * however dense — unlike `breaks: 'auto'`, it cannot relieve overcrowding\n * by moving a measure to the next line (see `fitZoomFactor`'s own doc).\n * The fit pass's only lever for a too-wide system is shrinking the\n * effective zoom, which on dense-enough music (e.g. a Bach fugue excerpt\n * that only fits 2 bars/line at zoom 0.85) shrinks glyphs to unreadable —\n * \"good sight-reading software prefers readable glyphs over a fixed bar\n * count.\" `shouldFallbackToAutoBreaks` (pure, own tests) is the exact\n * trigger condition, checked against the effective `breaks` the CALLER\n * asked for (`layoutOpts?.breaks` — never the widow pass's OWN\n * auto→'line' rewrite, since that never fires on already-caller-encoded\n * breaks in the first place, per the widow pass's own \"only ever applies\n * to breaks:'auto'\" rule). When it fires, this re-engraves ONCE with\n * `breaks: 'auto'` on the ORIGINAL `stampedXml` (ignoring the caller's\n * encoded breaks entirely — Verovio's own line-breaking algorithm decides\n * bar count instead), then runs the widow pass again on THAT result\n * (auto-breaking a short excerpt can itself produce a widow, same as the\n * normal 'auto' path). A failed fallback re-engrave leaves the\n * caller-encoded (post-widow-pass) render in place — degrade to \"unfit but\n * rendered,\" never blank. At most one fallback re-engrave per\n * `engraveOnce` call (the check runs once, off the FIRST pass's — or its\n * own widow pass's — fit factor only).\n */\n function engraveOnce(toolkit: VerovioToolkitInstance, widthPx: number, zoom: number): boolean {\n const layoutOpts = opts.verovioOptions;\n // A break plan IS encoded breaks, so it takes over `breaks` for every\n // downstream decision: the widow pass is skipped (the plan is already\n // balanced and widow-free by construction), and the readability-floor\n // fallback below treats the plan the same way it treats any\n // caller-encoded layout — see `shouldFallbackToAutoBreaks`.\n const planLayoutOpts: VerovioLayoutOptions | undefined = breakPlan.length\n ? { ...layoutOpts, breaks: 'line' }\n : layoutOpts;\n const effectiveBreaks = planLayoutOpts?.breaks ?? VEROVIO_LAYOUT_DEFAULTS.breaks;\n const minFitFactor = layoutOpts?.minFitFactor ?? DEFAULT_MIN_FIT_FACTOR;\n const avoidWidows = layoutOpts?.avoidWidows !== false;\n let xmlToRender = plannedXml;\n let renderLayoutOpts = planLayoutOpts;\n\n toolkit.setOptions(verovioRenderOptions(widthPx, zoom, renderLayoutOpts));\n const ok = !!toolkit.loadData(xmlToRender);\n if (ok) renderAllPages(toolkit);\n rebuildLayoutFromDom();\n\n if (ok && currentLayout) {\n const systemWidthsPx = () =>\n opts.measureSystemWidths ? opts.measureSystemWidths(svgHost) : currentLayout!.systems.map((s) => s.w);\n let factor = fitZoomFactor(systemWidthsPx(), widthPx);\n let terminalAutoFloorPlan = false;\n\n // Auto normally owns line breaking, but its result can either overflow\n // into the fit pass below OR fit geometrically while still giving each\n // measure too little rendered width. Re-plan from the pristine stamped\n // document at fewer bars per line instead (4 -> 2 -> 1); every\n // candidate is measured after engraving, so this is width-aware rather\n // than a viewport breakpoint. The 1-bar plan deliberately bypasses the\n // widow pass and the fit correction: it is the readability terminal.\n if (effectiveBreaks === 'auto') {\n const autoNeedsFloorPlan = () => shouldReplanAutoBreaks(\n factor,\n minFitFactor,\n systemWidthsPx(),\n systemMeasureCounts(svgHost),\n );\n\n // A naturally one-bar auto layout may already clear the density/fit\n // floor, yet still be visibly short because Verovio's print-page\n // side margins consume too much of a narrow host. Re-engrave the\n // SAME auto plan with compact margins only when its measured systems\n // miss the host-width floor. This preserves desktop's established\n // margins and coordinate system.\n if (!autoNeedsFloorPlan() && shouldUseCompactAutoMargins(systemWidthsPx(), widthPx)) {\n const compactAutoLayoutOpts: VerovioLayoutOptions = {\n ...layoutOpts,\n breaks: 'auto',\n pageMarginLeft: AUTO_FLOOR_PAGE_MARGIN,\n pageMarginRight: AUTO_FLOOR_PAGE_MARGIN,\n };\n toolkit.setOptions(verovioRenderOptions(widthPx, zoom, compactAutoLayoutOpts));\n if (toolkit.loadData(stampedXml)) {\n renderAllPages(toolkit);\n rebuildLayoutFromDom();\n xmlToRender = stampedXml;\n renderLayoutOpts = compactAutoLayoutOpts;\n factor = fitZoomFactor(systemWidthsPx(), widthPx);\n }\n }\n\n if (!autoNeedsFloorPlan() && avoidWidows) {\n const widowed = runWidowPass(toolkit, widthPx, zoom, xmlToRender, renderLayoutOpts);\n xmlToRender = widowed.xml;\n renderLayoutOpts = widowed.layoutOpts;\n factor = fitZoomFactor(systemWidthsPx(), widthPx);\n }\n\n let barsPerLine = Math.max(1, ...systemMeasureCounts(svgHost));\n for (;;) {\n const nextBarsPerLine = nextReadabilityFloorBarsPerLine(\n barsPerLine,\n factor,\n minFitFactor,\n autoNeedsFloorPlan(),\n );\n if (nextBarsPerLine === null) break;\n\n const floorXml = autoFloorPlanXml(nextBarsPerLine);\n const floorLayoutOpts: VerovioLayoutOptions = {\n ...layoutOpts,\n breaks: 'line',\n pageMarginLeft: AUTO_FLOOR_PAGE_MARGIN,\n pageMarginRight: AUTO_FLOOR_PAGE_MARGIN,\n };\n toolkit.setOptions(verovioRenderOptions(widthPx, zoom, floorLayoutOpts));\n if (!toolkit.loadData(floorXml)) break;\n\n renderAllPages(toolkit);\n rebuildLayoutFromDom();\n xmlToRender = floorXml;\n renderLayoutOpts = floorLayoutOpts;\n factor = fitZoomFactor(systemWidthsPx(), widthPx);\n barsPerLine = nextBarsPerLine;\n\n if (barsPerLine === 1 && autoNeedsFloorPlan()) {\n terminalAutoFloorPlan = true;\n break;\n }\n }\n\n // AUTO + SECTIONS PASS (`sectionStarts` without `barsPerLine`): the\n // settled auto render above is the density oracle. Re-plan ONCE so\n // every section starts its own line, balanced within sections at\n // auto's own typical count (`autoSectionBreakPlan` returns [] when\n // nothing needs fixing). The one-bar terminal already breaks at\n // every bar, so it is left alone. Whatever margins the auto passes\n // settled on are kept; the fit-to-width pass below still applies.\n if (autoWithSections && !terminalAutoFloorPlan) {\n const sectionPlan = autoSectionBreakPlan(\n measureTotal,\n layoutOpts?.sectionStarts ?? [],\n systemMeasureCounts(svgHost),\n );\n if (sectionPlan.length) {\n const sectionXml = injectSystemBreaks(stampedXml, sectionPlan);\n const sectionLayoutOpts: VerovioLayoutOptions = { ...renderLayoutOpts, breaks: 'line' };\n toolkit.setOptions(verovioRenderOptions(widthPx, zoom, sectionLayoutOpts));\n if (toolkit.loadData(sectionXml)) {\n renderAllPages(toolkit);\n rebuildLayoutFromDom();\n xmlToRender = sectionXml;\n renderLayoutOpts = sectionLayoutOpts;\n factor = fitZoomFactor(systemWidthsPx(), widthPx);\n }\n }\n }\n }\n\n if (shouldFallbackToAutoBreaks(factor, effectiveBreaks, minFitFactor)) {\n const autoLayoutOpts: VerovioLayoutOptions = { ...layoutOpts, breaks: 'auto' };\n toolkit.setOptions(verovioRenderOptions(widthPx, zoom, autoLayoutOpts));\n const okAuto = !!toolkit.loadData(stampedXml);\n if (okAuto) {\n renderAllPages(toolkit);\n rebuildLayoutFromDom();\n xmlToRender = stampedXml;\n renderLayoutOpts = autoLayoutOpts;\n\n if (avoidWidows) {\n const widowed = runWidowPass(toolkit, widthPx, zoom, xmlToRender, renderLayoutOpts);\n xmlToRender = widowed.xml;\n renderLayoutOpts = widowed.layoutOpts;\n }\n factor = fitZoomFactor(systemWidthsPx(), widthPx);\n }\n // A failed fallback re-engrave leaves the caller-encoded render\n // (still in svgHost/currentLayout from before this block) in place —\n // xmlToRender/renderLayoutOpts/factor stay at their pre-fallback\n // values, so the fit-to-width pass below reflows THAT document.\n }\n\n if (factor < 1 && !terminalAutoFloorPlan) {\n const effectiveZoom = zoom * factor;\n toolkit.setOptions(verovioRenderOptions(widthPx, effectiveZoom, renderLayoutOpts));\n const ok2 = !!toolkit.loadData(xmlToRender);\n if (ok2) {\n renderAllPages(toolkit);\n rebuildLayoutFromDom();\n }\n // A failed second loadData leaves the prior render (still in\n // svgHost/currentLayout from just above) untouched — degrade to\n // \"unfit but rendered\" rather than blank.\n }\n }\n return ok;\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 widthPx = desiredEngraveWidthPx();\n const ok = await withToolkit((toolkit) => {\n if (destroyed) return false; // stale — this player died while queued\n return engraveOnce(toolkit, widthPx, currentZoom);\n });\n if (destroyed) return;\n\n lastEngravedWidthPx = widthPx;\n loaded = ok;\n applyMarks();\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 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 // RECLAIM (see the module doc's \"SHARED TOOLKIT INSTANCE\" note, and\n // `withToolkit`'s own doc for bug E7M-322): the module-level toolkit is\n // shared across every live player on the page — and the real consumer\n // (PlayerPage) keeps a harmony player AND a written player alive\n // SIMULTANEOUSLY (lazy-built, destroyed only on piece switch), so\n // `loadData` calls from the two players genuinely interleave (e.g.\n // harmony active -> resize while written hidden -> back to written ->\n // writtenPlayer.setZoom()). A plain `redoLayout()` here would silently\n // re-lay-out and render WHATEVER document the toolkit currently holds —\n // which may belong to the OTHER player if it rendered more recently. So\n // `engraveOnce` re-parses THIS player's own `musicXml` synchronously,\n // immediately before rendering (loadData does a full parse + layout with\n // the just-set options, making a separate `redoLayout()` call\n // redundant). `withToolkit` now additionally guarantees no OTHER\n // player's queued task can run its own setOptions/loadData/render\n // sequence in between this task's own steps — the shared singleton is\n // therefore safe under alternating AND concurrent use. See\n // `tests/notationPlayerVerovio.test.ts`'s two-player interleaved-reflow\n // test for the regression this guards against.\n const ok = await withToolkit((toolkit) => {\n if (destroyed || myToken !== rebuildToken) return false; // stale\n return engraveOnce(toolkit, widthPx, newZoom);\n });\n\n if (destroyed || myToken !== rebuildToken) return;\n if (!ok) {\n loaded = false;\n return;\n }\n\n lastEngravedWidthPx = widthPx;\n currentZoom = newZoom;\n applyMarks();\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 markNotes(cols: number[] | null): void {\n markedCols = cols ?? [];\n applyMarks();\n },\n notePositions(): EngravedNote[] {\n return verovioEngravedNotes(svgHost, host, noteModel);\n },\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 markedCols = [];\n currentLayout = null;\n currentNoteCols = undefined;\n if (root.parentNode === host) host.removeChild(root);\n },\n };\n}\n"],"mappings":"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","names":["systemMeasureCounts"]}
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//\n// 0.40.0 — FULL API PARITY WITH notationPlayerSvg.ts (stave-web-sightread's\n// reading/recording stack can now swap backends without touching a call\n// site): `notePositions()`, `markNotes()`, the `[data-rmp-playhead]`\n// attribute, and `osmdOptions.autoBeam` (accepted + ignored, logged once —\n// Verovio always beams from the source MusicXML's own `<beam>` data, so\n// there is nothing for this option to toggle). The join that makes\n// `notePositions()`/`markNotes()` possible: `stampNoteIds`/`noteModelFromXml`\n// (./notationXml.ts, NEW) turn the input MusicXML into `id → NoteModel`\n// (pitch/rest/tie/staff/duration — the ground truth Verovio's rendered SVG\n// alone cannot supply), and `verovioEngravedNotes`/`verovioNotesAtColumn`\n// below join those ids to the live rendered `g.note`/`g.rest` elements —\n// same id-preservation guarantee `verovioOnsetColumns` already relies on\n// (see point 2 above), just consumed for notehead identity/geometry instead\n// of playhead columns. `musicXml` is stamped INTERNALLY (idempotent — a\n// caller that already ran it through stave's own `stampNoteIds` gets\n// byte-identical ids back) so this module never assumes the caller stamped\n// first. See ./notationXml.ts's own doc for why that scheme is duplicated\n// rather than imported from stave-web-sightread (wrong dependency\n// direction for a shared package) and `applyPrintObjectHiding`'s doc below\n// for the print-object empirical finding (Verovio honors it for notes, NOT\n// for rests).\n\nimport {\n vstackAudioPlayheadLine,\n distinctOnsets,\n hitTestMeasureAt,\n measureColumnsFromLayout,\n systemIndexOfBox,\n type NotationLayout,\n} from './scene/notationGeometry';\nimport type { Box, StaffMeasureBox } from './promo';\nimport { computeReflowScrollDelta, createFollowController } from './notationCommon';\nimport type { EngravedNote } from './notationPlayerSvg';\nimport {\n stampNoteIds,\n noteModelFromXml,\n injectSystemBreaks,\n balancedSystemBreaks,\n sectionAwareBreaks,\n autoSectionBreakPlan,\n fixedBarsPerLineBreaks,\n measureCount,\n type NoteModel,\n} from './notationXml';\n\n// Re-exported (TYPE-ONLY import above — erased at compile time, zero\n// runtime cost) so a caller that only imports from `notationPlayerVerovio`\n// still gets the SAME type `notePositions()` returns (identical shape to\n// notationPlayerSvg.ts's own export — not redefined here, to guarantee the\n// \"same exported interface\" contract can never drift between the two\n// SVG-family players).\nexport type { EngravedNote };\n\n/** Restated independently, not imported as a VALUE from notationPlayerSvg.ts\n * — same reasoning as `MAX_ENGRAVE_WIDTH_VRV` vs `MAX_ENGRAVE_WIDTH_SVG`\n * (below): a runtime (non-`type`) import from notationPlayerSvg.ts would\n * pull that module's ENTIRE implementation — including its own\n * `createSvgNotationPlayer` closure (harmless at runtime, since its OSMD\n * import stays lazy/dynamic either way) — into this entry's own tsup\n * chunk graph, which is exactly the cross-entry bundle coupling the\n * module doc's \"IMPORT PATH\" section (and the build report's dist-grep\n * gate) exists to prevent between the two SVG-family players. Must stay\n * byte-identical to `notationPlayerSvg.ts`'s own `MARKED_NOTE_CLASS` — a\n * test in this file's own suite pins that. */\nexport const MARKED_NOTE_CLASS = 'rmp-note-marked';\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 /** Parks a followed system this many px below the viewport top — pass the\n * height of any fixed top chrome (header, docked transport) plus a gap.\n * Default SYSTEM_TOP_MARGIN_PX. */\n followTopMarginPx?: number;\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 * Narrow passthrough matching `CreateSvgNotationPlayerOpts.osmdOptions`'s\n * shape exactly, so a caller driving BOTH players behind one interface\n * (the swap this player exists for) never has to branch per backend.\n * `autoBeam` has NO Verovio equivalent — Verovio always beams straight\n * from the MusicXML's own `<beam>` elements (it has no \"re-beam from\n * scratch\" pass the way OSMD's `autoBeam` option does) — so this is\n * accepted and silently ignored, with a ONE-TIME `console.warn` (module-\n * level, not per-instance — a caller that builds many players with the\n * same options object should not get spammed) rather than a hard error,\n * since ignoring it is genuinely harmless: synthesized rhythm XML with no\n * `<beam>` data renders unbeamed either way, which is a cosmetic\n * difference the caller can fix upstream (emit real `<beam>` elements)\n * rather than this player faking OSMD's re-beam heuristic.\n */\n osmdOptions?: { autoBeam?: boolean };\n /**\n * Passthrough for Verovio's own layout/line-breaking options — merged\n * UNDER `VEROVIO_LAYOUT_DEFAULTS` and OVER `scale`/`pageWidth`/\n * `adjustPageHeight` (see `verovioRenderOptions`'s own doc for the exact\n * merge order and why: a caller's `breaks` must be able to override the\n * default, but a caller can never smuggle in `scale`/`pageWidth` — those\n * stay derived from `zoom`/host width, not caller-suppliable). Stave's own\n * use case: inject `<print new-system=\"yes\"/>` into MusicXML and pass\n * `breaks: 'line'` to get exact N-bars-per-line, rather than Verovio's own\n * automatic line-breaking (see `VEROVIO_LAYOUT_DEFAULTS`'s doc for the\n * \"why\" behind the defaults themselves).\n */\n verovioOptions?: VerovioLayoutOptions;\n /**\n * TEST-ONLY SEAM — not part of this player's real production behavior.\n * When set, `engraveOnce`'s fit-pass/fallback-decision math reads system\n * widths from this function (called with `svgHost`) instead of\n * `currentLayout.systems`. Exists because jsdom implements neither\n * `getBBox` nor a real `getBoundingClientRect` (both always report\n * zero-size boxes), so `currentLayout.systems` widths — and therefore\n * `fitZoomFactor`'s result — are always 0/`1` (fits) in a headless test\n * regardless of how dense the underlying MusicXML actually is. A test can\n * pass a fixed fake (e.g. `() => [1200]`) to exercise the readability-floor\n * fallback (`VerovioLayoutOptions.minFitFactor`) end-to-end against a REAL\n * Verovio engrave, asserting on the resulting DOM shape\n * (`systemMeasureCounts`) rather than on geometry a real browser would be\n * needed to produce. Never used by any real caller.\n */\n measureSystemWidths?: (root: Element) => number[];\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 /**\n * Mark the engraved noteheads/rests nearest the given columns (same\n * `measureIndex + fraction-through-the-measure` units as `onsets`'\n * derived columns); pass `null` or `[]` to clear. Same contract and CSS\n * class (`MARKED_NOTE_CLASS` = `'rmp-note-marked'`) as\n * `SvgNotationPlayer.markNotes` — a consumer's existing CSS keeps working\n * unmodified across a backend swap. Survives re-engraving: reapplied\n * after every `setZoom`/`resize`.\n */\n markNotes(cols: number[] | null): void;\n /** Every engraved note/rest with MODEL identity (pitch, rest, tie, staff)\n * and host-relative notehead position — same `EngravedNote` shape\n * `SvgNotationPlayer.notePositions()` returns (re-exported from this\n * module, not redefined). `headEl` is Verovio's rendered `.notehead`\n * sub-group when present (a tighter box than the outer `g.note`, closer\n * in spirit to OSMD's `.vf-notehead`), else the outer `g.note`/`g.rest`\n * group. `[]` before the initial engrave resolves or under the\n * `rendered` test seam (no live SVG to join against — same graceful\n * degradation as `verovioOnsetColumns`). */\n notePositions(): EngravedNote[];\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).\n * `root` is carried alongside purely so `boxFromElement` can go straight to\n * `getBoundingClientRect()` diffing (see that function's own doc for why) —\n * `scale`/`offsetX`/`offsetY` stay as the page-validity check\n * (`pageGeometry`'s own \"is this page's markup well-formed\" guard) and are\n * no longer consumed for box math. */\ninterface PageGeom {\n scale: number;\n offsetX: number;\n offsetY: number;\n root: Element;\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, root };\n}\n\n/**\n * An element's box, in CSS px relative to the shared `root` —\n * `getBoundingClientRect()` diffed against `geom.root`'s own rect. Same\n * approach `notationPlayerSvg.ts` uses throughout for its OSMD geometry\n * (`hostRect`/`rootRect` diffing — see that module), chosen here for the\n * SAME reason: `getBoundingClientRect()` already resolves every ancestor\n * transform between the element and the viewport, so it needs no separate\n * per-page `scale`/`offsetX`/`offsetY` math layered on top.\n *\n * PRIOR BUG (found via the OSMD->Verovio default-backend swap's ghost-\n * placement acceptance check, ~17px off on real pieces): this used to do\n * `el.getBBox()` (Verovio's rendered user-unit space) converted via\n * `geom.offsetX/offsetY + bbox.x/y * geom.scale` — correct ONLY if the only\n * transform between `el` and the page's own outer `<svg>` is the page's own\n * placement + viewBox scale. Verovio's real output wraps EVERY page's\n * content in `<g class=\"page-margin\" transform=\"translate(500, 500)\">`\n * (confirmed against a real 6.2.0 render) — an ancestor transform `getBBox()`\n * does NOT bake in (`getBBox()` is local to the element's own user space,\n * before any ancestor's transform is applied) and the old formula never\n * accounted for. Empirically: `500 * scale` (~17px at this fixture's\n * `outerRect.width / viewBox.width` ratio) matched the observed drift\n * exactly in both axes — every note landed ~17px up-and-left of its real\n * rendered position. `getBoundingClientRect()` has no such blind spot: it\n * is the browser's own answer to \"where is this actually painted,\" immune\n * to however many ancestor groups carry their own transform.\n *\n * Null for anything that isn't a real element or has a degenerate\n * (zero-area) box — same defensive style the old implementation had.\n */\nfunction boxFromElement(el: Element, geom: PageGeom): Box | null {\n const r = safeRect(el);\n const rootRect = safeRect(geom.root);\n if (!r || !rootRect || !(r.width > 0) || !(r.height > 0)) return null;\n return { x: r.left - rootRect.left, y: r.top - rootRect.top, w: r.width, h: r.height };\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 * Rendered measure count per SYSTEM, in DOCUMENT ORDER (`.system` elements\n * across every `.vrv-page`, each one's OWN `.measure` descendant count) —\n * the widow-detection input for `engraveOnce`'s widow pass (see that\n * function's own doc). Deliberately PLAIN DOM traversal — no\n * `getBoundingClientRect`/geometry involved at all, unlike\n * `verovioNotationLayout`/`collectMeasureElements` (which both need a real\n * layout engine to report non-zero boxes) — so this works against ANY\n * rendered SVG DOM, including jsdom's own (which reports zero-size rects for\n * everything by default): a real Verovio render loaded into jsdom is enough\n * to exercise the widow pass end-to-end purely by DOM shape, with no\n * `getBoundingClientRect` mocking required (see this module's real-Verovio\n * widow test). Never throws; a root with no `.system` elements yields `[]`.\n */\nexport function systemMeasureCounts(root: Element): number[] {\n return Array.from(root.querySelectorAll('.system')).map((sysEl) => sysEl.querySelectorAll('.measure').length);\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 // Anchor the onset at the NOTEHEAD CENTER, matching the OSMD\n // backend's column semantics. The whole-group left edge biased every\n // time->x position (and so every ghost) ~half a notehead LEFT — the\n // group box also includes accidentals/stems (user-visible drift,\n // 2026-08-20).\n const headGlyph = noteEl ? (noteEl.querySelector('.notehead') ?? noteEl) : null;\n const box = headGlyph && geom ? boxFromElement(headGlyph, 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 anchorX = box.x + box.w / 2;\n const frac = denom > 0 ? Math.min(1, Math.max(0, (anchorX - 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// ─── Note geometry (notePositions) + column→note lookup (markNotes) ───────\n\n/**\n * Every rendered note/rest joined to its MODEL identity (`noteModel`, from\n * `noteModelFromXml` — ./notationXml.ts) by stamped id — this player's\n * `notePositions()`. Same shape and host-relative-px contract as\n * notationPlayerSvg.ts's `engravedNotes` (its own doc: \"Positions are px\n * relative to the HOST element, at the notehead's center\"), but the join\n * itself is simpler here: unlike OSMD (one graphical group per CHORD,\n * requiring the pitch-rank head-sorting `engravedNotes` does), Verovio\n * renders every chord member as its OWN `<g id=\"...\" class=\"note\">`\n * (empirically confirmed — see the module doc's point 2) — so this is a\n * flat id→element→model join, no grouping/sorting.\n *\n * `headEl` prefers the note's own `.notehead` child (Verovio's real nested\n * glyph group — empirically confirmed as `<g class=\"notehead\">` inside\n * `<g class=\"note\">`) over the outer `g.note`/`g.rest` wrapper when\n * present — a TIGHTER box (closer to notationPlayerSvg.ts's\n * `.vf-notehead`-only box) than the wrapper, which also spans the\n * stem/flag/accidental. Falls back to the wrapper itself for a rest (no\n * `.notehead` child) or if that lookup's own `getBBox` fails.\n *\n * `root` is the container holding every rendered `.vrv-page` (this\n * player's `svgHost`); `host` is the player's OWN host element — `x`/`y`\n * are computed relative to `host` (per `EngravedNote`'s contract), NOT to\n * `root`, which may itself sit offset within `host`. Never throws; returns\n * `[]` for any missing/malformed structure (same defensive style as\n * `verovioNotationLayout`).\n */\n/** noteId → 0-based onset index, from the player's own onset grouping (by\n * `tMs`). The join key `EngravedNote.onsetId` uses so consumers never\n * reconstruct onsets from x. */\nexport function onsetIdByNoteId(onsets: readonly VerovioOnset[]): Map<string, number> {\n const distinct = [...new Set(onsets.map((o) => o.tMs))].sort((a, b) => a - b);\n const indexOf = new Map(distinct.map((t, i) => [t, i] as const));\n const out = new Map<string, number>();\n for (const o of onsets) {\n const i = indexOf.get(o.tMs);\n if (i == null) continue;\n for (const id of o.noteIds) if (!out.has(id)) out.set(id, i);\n }\n return out;\n}\n\nexport function verovioEngravedNotes(\n root: Element,\n host: HTMLElement,\n noteModel: Map<string, NoteModel>,\n onsetIdById?: ReadonlyMap<string, number>,\n): EngravedNote[] {\n try {\n const pageGeoms = computePageGeometries(root);\n if (!pageGeoms.size || !noteModel.size) return [];\n const measureEntries = collectMeasureElements(pageGeoms);\n if (!measureEntries.length) return [];\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 hostRect = safeRect(host);\n const offX = rootRect && hostRect ? rootRect.left - hostRect.left : 0;\n const offY = rootRect && hostRect ? rootRect.top - hostRect.top : 0;\n\n const out: EngravedNote[] = [];\n for (const { el: measureEl, geom } of measureEntries) {\n const noteEls = Array.from(measureEl.querySelectorAll('g.note, g.rest'));\n for (const noteEl of noteEls) {\n const id = noteEl.getAttribute('id');\n if (!id) continue;\n const nm = noteModel.get(id);\n if (!nm) continue;\n\n const glyphEl = noteEl.querySelector('.notehead') ?? noteEl;\n let box = boxFromElement(glyphEl, geom);\n let headEl: Element = glyphEl;\n if (!box && glyphEl !== noteEl) {\n box = boxFromElement(noteEl, geom);\n headEl = noteEl;\n }\n if (!box) continue;\n\n out.push({\n midi: nm.midi,\n isRest: nm.isRest,\n tieContinuation: nm.tieContinuation,\n staffIndex: nm.staffIndex,\n systemIndex: systemIndexOfBox(systems, box),\n durationReal: nm.durationReal,\n onsetId: onsetIdById?.get(id) ?? null,\n x: offX + box.x + box.w / 2,\n y: offY + box.y + box.h / 2,\n w: box.w,\n h: box.h,\n headEl,\n });\n }\n }\n return out;\n } catch {\n return [];\n }\n}\n\n/**\n * Every rendered note/rest nearest engraved column `col`\n * (`measureIndex + fraction`, same units as `markNotes`' argument and\n * `noteCols`), one per staff of that measure — the Verovio-backend\n * counterpart to notationPlayerSvg.ts's `graphicalNotesAtColumn`. That\n * function searches OSMD's object model (`relInMeasureTimestamp` vs bar\n * duration); this searches the live rendered geometry instead (Verovio\n * exposes no per-element timestamp) — same \"nearest entry by position gap,\n * search every staff of the bar\" shape, just sourced from `getBBox` instead\n * of a timeline field. The fractional-position FORMULA itself\n * (`(box.x - sx) / denom`) is the exact one `verovioOnsetColumns` already\n * uses (not re-derived) — this function runs it in the opposite direction\n * (nearest note TO a column, rather than a column FROM a note id).\n */\nexport function verovioNotesAtColumn(root: Element, layout: NotationLayout, col: number): Element[] {\n try {\n if (!Number.isFinite(col)) return [];\n const cols = measureColumnsFromLayout(layout.measures);\n if (!cols.length) return [];\n const measureIndex = Math.max(0, Math.min(cols.length - 1, Math.floor(col)));\n const wanted = col - measureIndex;\n\n const pageGeoms = computePageGeometries(root);\n const measureEntries = collectMeasureElements(pageGeoms);\n const entry = measureEntries[measureIndex];\n if (!entry) return [];\n const m = cols[measureIndex];\n const sx = Math.min(m.noteStartX, m.x + m.w);\n const denom = m.x + m.w - sx;\n\n const found: Element[] = [];\n for (const staffEl of Array.from(entry.el.querySelectorAll('g.staff'))) {\n const noteEls = Array.from(staffEl.querySelectorAll('g.note, g.rest'));\n let best: Element | null = null;\n let bestGap = Number.POSITIVE_INFINITY;\n for (const noteEl of noteEls) {\n const box = boxFromElement(noteEl, entry.geom);\n if (!box) continue;\n const frac = denom > 0 ? (box.x - sx) / denom : 0;\n const gap = Math.abs(frac - wanted);\n if (gap < bestGap) {\n bestGap = gap;\n best = noteEl;\n }\n }\n if (best) found.push(best);\n }\n return found;\n } catch {\n return [];\n }\n}\n\n/**\n * CRITICAL empirical finding (verified against a real 6.2.0 render — see\n * the migration spike's follow-up, `.superpowers/…/print-object-test.mjs`\n * equivalent run for this task): Verovio's MusicXML importer HONORS\n * `print-object=\"no\"` for a pitched `<note>` (renders `visibility=\"hidden\"`\n * on its own `<g class=\"note\">`) but does NOT honor it for a `<note><rest/>`\n * — the `<g class=\"rest\">` renders fully visible regardless. This matters\n * because stave-web-sightread's `hideDoubledNotes` feature sets\n * `print-object=\"no\"` on BOTH doubled notes AND the rests of a voice that\n * lost every visible note (see that function's own \"second pass\" doc) —\n * without this fix, a hidden-doubled-note's voice would still show a\n * floating rest, exactly the \"extra voice\" clutter that feature exists to\n * remove.\n *\n * Fix: force `visibility=\"hidden\"` on every rendered id whose model says\n * `hidden` (`NoteModel.hidden`, from `noteModelFromXml`). Re-applying it to\n * a NOTE Verovio already hid itself is a harmless no-op; applying it to a\n * REST is the actual fix. Call after every render (`renderAllPages`) — a\n * fresh render is a fresh DOM, so a prior call's effect never carries over\n * (nothing to \"undo\" on a note/rest that's no longer print-object=\"no\").\n * Never throws.\n */\nexport function applyPrintObjectHiding(root: Element, noteModel: Map<string, NoteModel>): void {\n try {\n const doc = root.ownerDocument;\n if (!doc || !noteModel.size) return;\n for (const [id, nm] of noteModel) {\n if (!nm.hidden) continue;\n const el = doc.getElementById(id);\n if (el) el.setAttribute('visibility', 'hidden');\n }\n } catch {\n /* best-effort — a render this can't patch stays as Verovio drew it */\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 * Verovio layout/line-breaking options a caller may override — see\n * `CreateVerovioNotationPlayerOpts.verovioOptions`'s doc for the merge\n * contract and Stave's own motivating use case (encoded `<print\n * new-system=\"yes\"/>` breaks + `breaks: 'line'`, exact N-bars-per-line).\n * Deliberately NARROWER than Verovio's full option surface — only the knobs\n * this integration has an actual caller for; add more here (not a raw\n * `Record<string, unknown>` passthrough) as real needs arise, so a typo in a\n * caller's options object is a compile error, not a silently-ignored no-op.\n */\nexport interface VerovioLayoutOptions {\n breaks?: 'none' | 'auto' | 'line' | 'smart' | 'encoded';\n breaksSmartSb?: number;\n breaksNoWidow?: boolean;\n minLastJustification?: number;\n spacingSystem?: number;\n spacingStaff?: number;\n pageMarginLeft?: number;\n pageMarginRight?: number;\n /**\n * Player-level widow guard — see `engraveOnce`'s own \"Widow pass\" doc.\n * NOT the same mechanism as `breaksNoWidow` above (Verovio's own option,\n * which only prevents a lone measure on the last PAGE — confirmed on a\n * live page to do nothing for a lone measure on the last SYSTEM of a\n * single-page excerpt, the common case for this player). This is a\n * PLAYER option, not a Verovio one: it is stripped out in\n * `verovioRenderOptions` before `setOptions` ever sees it (Verovio has no\n * such option of its own to receive it). Default `true` — every engrave\n * gets the widow guard unless a caller opts out. Only takes effect when\n * the EFFECTIVE `breaks` is `'auto'` (this object's `breaks`, or\n * `VEROVIO_LAYOUT_DEFAULTS.breaks` when unset) — a caller who already\n * encoded exact break points (`breaks: 'line'`, e.g. Stave's own\n * per-line-break usage) has made their own layout choice, which this pass\n * never second-guesses.\n */\n avoidWidows?: boolean;\n /**\n * Readability floor for CALLER-ENCODED breaks (`breaks: 'line'` or\n * `'encoded'`) — see `engraveOnce`'s own \"Fallback pass\" doc. Stave's own\n * motivating case: `<print new-system=\"yes\"/>` every 4 bars + `breaks:\n * 'line'` gets exact N-bars-per-line on normal music, but on dense music\n * (e.g. a Bach fugue excerpt) the one system between two encoded breaks\n * can be too wide to fit even at Verovio's minimum spacing — the ONLY\n * lever `fitZoomFactor` then has left is shrinking the effective zoom,\n * which on a dense-enough system means unreadably small glyphs. Good\n * sight-reading software prefers readable glyphs over a fixed bar count.\n *\n * A PLAYER option, not a Verovio one — like `avoidWidows`, stripped out in\n * `verovioRenderOptions` before `setOptions` ever sees it. Default 0.75\n * (see `shouldFallbackToAutoBreaks`'s own doc for the exact trigger\n * condition). `minFitFactor: 0` disables the fallback entirely — a caller\n * who always wants their exact encoded bar count, however small the\n * glyphs get, can opt back into the pre-existing behavior.\n */\n minFitFactor?: number;\n /**\n * BREAK PLAN — target bars per line. When set (`> 0`), the player plans\n * system breaks itself (`sectionAwareBreaks`, balanced with a widow\n * back-off) and encodes them into the document before the first engrave,\n * forcing `breaks: 'line'`. Leave unset (or `0`) to let Verovio's own\n * `breaks: 'auto'` decide, which is the default and is content-aware.\n *\n * Combine with `sectionStarts` to keep the balancing inside each section.\n * A PLAYER option, not a Verovio one — stripped in `verovioRenderOptions`\n * before `setOptions` sees it.\n */\n barsPerLine?: number;\n /**\n * BREAK PLAN — 0-based measure POSITIONS at which a musical section\n * begins. Each one > 0 becomes a hard system break, so a section always\n * starts its own line; `barsPerLine`'s balancing then runs within each\n * section's span rather than across the whole excerpt.\n *\n * POSITIONS, not MusicXML `<measure number>` attributes — scores skip bar\n * numbers, so a caller holding bar-numbered section labels must convert\n * first (`measureCount`'s doc explains the coordinate). Setting this alone\n * (no `barsPerLine`) is the AUTO + SECTIONS mode: the document is engraved\n * under `breaks: 'auto'` exactly as with no plan (compact margins, widow\n * pass, readability floor), then — if a section start is not already a\n * system start — re-planned ONCE at auto's own settled density with a hard\n * break at each section (`autoSectionBreakPlan`), so the engraver still\n * decides how many bars fit a line. A PLAYER option — stripped in\n * `verovioRenderOptions`.\n */\n sectionStarts?: readonly number[];\n}\n\n/**\n * Layout defaults applied to EVERY engrave (initial + every reflow) unless a\n * caller's own `verovioOptions` overrides a given key — see\n * `verovioRenderOptions`'s doc for the merge order.\n *\n * WHY: Verovio only justifies a page's LAST system when its unstretched\n * width is already ≥ `minLastJustification` (Verovio's own default: 0.8) of\n * the page width — confirmed empirically (a 4-bar-of-whole-notes fixture at\n * pageWidth 2400 renders its single system at ~41% of the page width with\n * Verovio's own default, ~96% with `minLastJustification: 0`; see this\n * module's real-Verovio justification test). A short excerpt — the common\n * case for both this player's normal usage (a few bars at a time) AND\n * Stave's per-line-break usage (section below) — is ALWAYS a \"last system\"\n * (it's the only one), so Verovio's default leaves it looking\n * left-justified/shrunk rather than filling the available width. Overriding\n * to 0 makes every system justify unconditionally, matching this player's\n * own \"fill the host width\" semantic (`verovioZoomOptions`'s own doc).\n * `breaksNoWidow: true` prevents Verovio leaving a single trailing bar\n * orphaned on its own system (a related \"short excerpt\" artifact, same\n * spirit as the justification fix). `breaks: 'auto'` (Verovio's own default\n * line-breaking algorithm) is the base default; Stave overrides it to\n * `'line'` when it has ALREADY encoded exact break points (see\n * `VerovioLayoutOptions`'s doc) — see the \"caller's `breaks` wins\" case in\n * `verovioRenderOptions`'s own tests.\n */\nexport const VEROVIO_LAYOUT_DEFAULTS = {\n breaks: 'auto',\n minLastJustification: 0,\n breaksNoWidow: true,\n} as const;\n\n/**\n * The full `toolkit.setOptions(...)` argument for one engrave: layout\n * defaults, overridden by the caller's own `layout` (a caller's `breaks`\n * WINS over `VEROVIO_LAYOUT_DEFAULTS.breaks` — this is the whole point of\n * exposing the override), overridden AGAIN by `scale`/`pageWidth` (derived\n * from `hostWidthPx`/`zoom` via `verovioZoomOptions` — a caller's\n * `verovioOptions` has no `scale`/`pageWidth` keys per `VerovioLayoutOptions`\n * own (narrower) type, so this last spread is really just making the\n * derived-vs-defaulted precedence explicit, not fighting a real collision)\n * and `adjustPageHeight: true` (always on — this player's own \"whole score,\n * page-flow, no pagination\" framing, see the module doc's \"PAGES\" section).\n * Pure — exists so the merge itself is unit-testable without a DOM or a real\n * Verovio toolkit (`tests/notationPlayerVerovio.test.ts`'s \"options merge\"\n * tests call this directly).\n *\n * `avoidWidows`/`minFitFactor` (see `VerovioLayoutOptions`'s own docs) are\n * PLAYER options, not Verovio ones — destructured out here and never\n * forwarded to `setOptions`, same \"narrow the passthrough\" spirit as this\n * function only accepting `VerovioLayoutOptions`'s typed surface at all.\n *\n * `pageHeight: 60000` (Verovio's own max) + `pageMarginTop`/`pageMarginBottom:\n * 0` are unconditional, like `adjustPageHeight` — not in `VerovioLayoutOptions`\n * at all, so a caller cannot override them. WHY: this player's own \"whole\n * score, page-flow, no pagination\" framing (module doc, \"PAGES\" section)\n * renders every Verovio PAGE as its own `.vrv-page` block; Verovio's default\n * page height (~2970, a real printed-page height) paginates a long score\n * into several such blocks with a page-margin gap between them, which reads\n * as a broken PDF rather than one continuous score. A tall-enough\n * `pageHeight` (combined with `adjustPageHeight: true`, already unconditional\n * above) keeps `getPageCount() === 1` regardless of how long the piece is —\n * confirmed against a real 40-bar fixture (2 pages at Verovio's own default\n * height, 1 page at 60000 — see this module's real-Verovio pageHeight test).\n * Zeroing the page margins removes the (now pointless, since there is only\n * ever one page) top/bottom whitespace Verovio would otherwise still budget\n * for a \"page\".\n */\nexport function verovioRenderOptions(\n hostWidthPx: number,\n zoom: number,\n layout?: VerovioLayoutOptions,\n): Record<string, unknown> {\n const { scale, pageWidth } = verovioZoomOptions(hostWidthPx, zoom);\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const {\n avoidWidows: _avoidWidows,\n minFitFactor: _minFitFactor,\n barsPerLine: _barsPerLine,\n sectionStarts: _sectionStarts,\n ...verovioLayout\n } = layout ?? {};\n return {\n ...VEROVIO_LAYOUT_DEFAULTS,\n ...verovioLayout,\n scale,\n pageWidth,\n adjustPageHeight: true,\n pageHeight: 60000,\n pageMarginTop: 0,\n pageMarginBottom: 0,\n };\n}\n\n/**\n * 1 when every system already fits within `engraveWidthPx` (widest ≤ width\n * × 1.01 — a small tolerance for sub-pixel rounding in the DOM geometry, not\n * a real \"close enough\" fudge); otherwise `engraveWidthPx / widest` (< 1) —\n * the zoom-scaling factor that would bring the widest system back down to\n * exactly `engraveWidthPx`. Never NaN/Infinity: an empty `systemWidthsPx`,\n * a non-finite/non-positive widest width, or a non-finite/non-positive\n * `engraveWidthPx` all return 1 (the safe \"don't touch the zoom\" default —\n * see the module doc's \"Fit-to-width\" section for how the caller uses this:\n * a factor < 1 triggers exactly ONE re-engrave at `requestedZoom * factor`,\n * never a loop).\n *\n * WHY THIS IS NEEDED (see `VEROVIO_LAYOUT_DEFAULTS`'s doc for the\n * complementary justification fix): with ENCODED breaks (`breaks: 'line'` +\n * Stave's own `<print new-system=\"yes\"/>` markers), Verovio must put\n * whatever notes fall between two encoded break points onto one system —\n * unlike `breaks: 'auto'`, it cannot relieve overcrowding by moving a\n * measure to the next line. If that system is too dense to fit\n * `engraveWidthPx` even at Verovio's own minimum inter-note spacing, Verovio\n * renders it WIDER than the requested page width instead of silently\n * clipping it. Shrinking the effective zoom (smaller glyphs, smaller\n * spacing) is the only lever left to bring it back within the host's\n * available width.\n */\nexport function fitZoomFactor(systemWidthsPx: readonly number[], engraveWidthPx: number): number {\n if (!systemWidthsPx.length) return 1;\n if (!(engraveWidthPx > 0) || !Number.isFinite(engraveWidthPx)) return 1;\n const widest = Math.max(...systemWidthsPx);\n if (!(widest > 0) || !Number.isFinite(widest)) return 1;\n if (widest <= engraveWidthPx * 1.01) return 1;\n const factor = engraveWidthPx / widest;\n return Number.isFinite(factor) && factor > 0 ? factor : 1;\n}\n\n/** Default `VerovioLayoutOptions.minFitFactor` — see that field's own doc. */\nexport const DEFAULT_MIN_FIT_FACTOR = 0.75;\n\n/**\n * Pure decision for the readability floor (`VerovioLayoutOptions.minFitFactor`,\n * `engraveOnce`'s \"Fallback pass\") — true exactly when ALL of:\n * - `factor` (a `fitZoomFactor` result) is finite and strictly below\n * `minFitFactor`;\n * - `minFitFactor` is a positive, finite floor (0 — or anything\n * non-positive/non-finite — disables the fallback unconditionally, the\n * documented opt-out);\n * - `breaks` is `'line'` or `'encoded'` — the two ways a caller can hand\n * Verovio EXACT, pre-decided break points (as opposed to `'auto'`/\n * `'smart'`/`'none'`/unset, where Verovio already owns the line-breaking\n * decision and there is no \"caller's encoded bar count\" to fall back\n * FROM in the first place).\n *\n * Never throws; no DOM/Verovio access — this is the single source of truth\n * `engraveOnce` calls after every fit-factor computation, so the trigger\n * condition is unit-testable independent of a real engrave.\n */\nexport function shouldFallbackToAutoBreaks(\n factor: number,\n breaks: VerovioLayoutOptions['breaks'] | undefined,\n minFitFactor: number,\n): boolean {\n if (!isBelowReadabilityFloor(factor, minFitFactor)) return false;\n if (breaks !== 'line' && breaks !== 'encoded') return false;\n return true;\n}\n\n/** True when fitting the current engraving would cross the caller's\n * readability floor. Shared by the encoded-break fallback and auto's\n * narrower re-plan path so the two policies cannot drift. */\nexport function isBelowReadabilityFloor(factor: number, minFitFactor: number): boolean {\n return Number.isFinite(factor) && Number.isFinite(minFitFactor) && minFitFactor > 0 && factor < minFitFactor;\n}\n\n/** Auto can fit geometrically without ever entering the fit-zoom pass. This\n * measured per-measure width is the companion floor for that case: at a\n * 390px viewport the notation host is about 324–356px wide, so a 4-bar line\n * is only ~80px/bar; a 1-bar line preserves a usable engraving scale. */\nexport const MIN_AUTO_MEASURE_WIDTH_PX = 180;\n\n/** Auto needs a narrower plan either when the normal fit floor would be\n * crossed, or when the rendered systems show too little width per measure.\n * The latter is based on rendered layout, never a viewport breakpoint. */\nexport function shouldReplanAutoBreaks(\n factor: number,\n minFitFactor: number,\n systemWidthsPx: readonly number[],\n systemMeasureCounts: readonly number[],\n): boolean {\n if (isBelowReadabilityFloor(factor, minFitFactor)) return true;\n return systemWidthsPx.some((width, i) => {\n const measures = systemMeasureCounts[i] ?? 0;\n return Number.isFinite(width) && width > 0 && measures > 0 && width / measures < MIN_AUTO_MEASURE_WIDTH_PX;\n });\n}\n\n/** Verovio's printed-page default horizontal margins are proportionally too\n * large for a narrow *measured* engraving: a fully justified one-bar system\n * can still occupy under 90% of its host. Keep the established desktop\n * coordinate system unless the rendered result proves it needs the compact\n * margins; this is a layout measurement, not a viewport breakpoint. */\nexport function shouldUseCompactAutoMargins(\n systemWidthsPx: readonly number[],\n engraveWidthPx: number,\n): boolean {\n return Number.isFinite(engraveWidthPx) && engraveWidthPx > 0 &&\n systemWidthsPx.some((width) => Number.isFinite(width) && width > 0 && width < engraveWidthPx * 0.9);\n}\n\n/** One narrower candidate for the auto-break readability floor, or null when\n * the measured fit already holds the floor (or one bar is the terminal\n * layout). Uses ceiling-halves so 4 -> 2 -> 1 and 3 -> 2 -> 1. */\nexport function nextReadabilityFloorBarsPerLine(\n currentBarsPerLine: number,\n fittedZoomFactor: number,\n minFitFactor: number,\n measuredDensityBelowFloor = false,\n): number | null {\n if (!measuredDensityBelowFloor && !isBelowReadabilityFloor(fittedZoomFactor, minFitFactor)) return null;\n if (!Number.isFinite(currentBarsPerLine) || currentBarsPerLine <= 1) return null;\n return Math.max(1, Math.ceil(currentBarsPerLine / 2));\n}\n\n/**\n * Pure planner seam for the width-aware auto floor. `fittedZoomAt` is the\n * measured-layout oracle: production obtains each measurement by engraving\n * that candidate, while jsdom-free tests can supply deterministic factors.\n * The returned list is the sequence to try; its final `1` is terminal.\n */\nexport function readabilityFloorPlan(\n initialBarsPerLine: number,\n widthPx: number,\n minFitFactor: number,\n fittedZoomAt: (barsPerLine: number, widthPx: number) => number,\n): number[] {\n if (!Number.isFinite(initialBarsPerLine) || initialBarsPerLine <= 0 || !(widthPx > 0)) return [];\n let bars = Math.max(1, Math.floor(initialBarsPerLine));\n let factor = fittedZoomAt(bars, widthPx);\n const plan: number[] = [];\n for (;;) {\n const next = nextReadabilityFloorBarsPerLine(bars, factor, minFitFactor);\n if (next === null) return plan;\n plan.push(next);\n bars = next;\n factor = fittedZoomAt(bars, widthPx);\n if (bars === 1) return plan;\n }\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/**\n * Module-level SERIAL queue over the shared toolkit — bug E7M-322: in\n * production, remounting a player component several times in quick\n * succession (destroying each instance before its `ready` resolved, EVERY\n * instance awaiting the SAME shared `getVerovioToolkit()` promise) produced\n * an Emscripten \"null function\" error from the WASM bridge. `withToolkit`\n * makes every `setOptions -> loadData -> getPageCount/renderToSVG` sequence,\n * across EVERY live player, run to completion strictly one at a time — even\n * across the async wasm-load boundary the very first call incurs. Each\n * queued task is chained off the PREVIOUS task's own settled promise (not\n * off `toolkitPromise` directly), so two tasks queued back-to-back before\n * the toolkit even exists yet still serialize correctly once it resolves;\n * without this, both would `await` the same not-yet-resolved\n * `getVerovioToolkit()` promise and their synchronous toolkit-touching code\n * could run in whatever order the two continuations happen to be scheduled,\n * which is exactly the \"several players hammering the one shared instance\n * at once\" shape the production bug had.\n *\n * `fn` MUST stay synchronous (no `await` inside it) — same \"no interleaving\n * because no await between reclaim and render\" rule `reflow`'s own doc\n * already relied on; the queue is what now makes that rule hold ACROSS\n * players' tasks, not just within one player's own call. A task that has\n * gone stale while queued (its player was destroyed while waiting for its\n * turn) must check `destroyed` as the FIRST thing inside `fn` and return a\n * no-op result — `withToolkit` itself has no opinion on staleness, only\n * serialization (see `initialEngrave`/`reflow`'s own `fn` bodies).\n *\n * A rejecting task never wedges the queue for tasks queued after it (the\n * internal chain swallows the rejection); the ORIGINAL caller still sees\n * their own task's rejection via the promise `withToolkit` returns, since\n * that is tracked separately from the internal queue chain.\n */\nlet toolkitQueue: Promise<unknown> = Promise.resolve();\n\nfunction withToolkit<T>(fn: (toolkit: VerovioToolkitInstance) => T): Promise<T> {\n const run = toolkitQueue.then(() => getVerovioToolkit()).then((toolkit) => fn(toolkit));\n toolkitQueue = run.then(\n () => undefined,\n () => undefined,\n );\n return run;\n}\n\n// ─── createVerovioNotationPlayer ────────────────────────────────────────────\n\n// Module-level (not per-instance) — see `osmdOptions`'s doc: a caller\n// building many players with the same options object should see this once\n// per SESSION, not once per player.\nlet loggedAutoBeamIgnored = false;\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. Raised 1200 -> 1400: Stave's encoded-break usage\n * (`verovioOptions.breaks: 'line'`, exact N-bars-per-line) wants more\n * breathing room per system than the original OSMD-parity cap allowed\n * before glyphs get cramped at typical desktop widths. */\nexport const MAX_ENGRAVE_WIDTH_VRV = 1400;\n/** Floor so a not-yet-laid-out / zero-width host never engraves at 0px. */\nconst MIN_ENGRAVE_WIDTH_VRV = 280;\n/** Used only for the terminal auto-floor plan. The regular page margin is\n * intentionally left unchanged so desktop geometry/playhead alignment stays\n * on Verovio's established coordinate system. */\n// Verovio treats 0 as \"unset\" and restores its 500-unit default; 1 is the\n// smallest honored value and is visually equivalent to zero here.\nconst AUTO_FLOOR_PAGE_MARGIN = 1;\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 if (opts.osmdOptions?.autoBeam !== undefined && !loggedAutoBeamIgnored) {\n loggedAutoBeamIgnored = true;\n // eslint-disable-next-line no-console\n console.warn(\n '[notationPlayerVerovio] osmdOptions.autoBeam has no Verovio equivalent (Verovio always beams from the ' +\n 'MusicXML <beam> data) — ignored.',\n );\n }\n\n // Stamp once, up front — pure/idempotent (./notationXml.ts's doc), so\n // re-stamping a caller's already-stamped xml (e.g. stave's own transform\n // pipeline) reproduces the exact same ids. `noteModel` is likewise a pure\n // function of this same stamped string, computed once and reused across\n // every re-engrave (`setZoom`/`resize` never change note IDENTITY, only\n // geometry). The `rendered` test seam has no real document to stamp/model\n // against — `notePositions()`/`markNotes()` degrade gracefully to `[]`/\n // no-op there, same as `verovioOnsetColumns` already does for that seam.\n const stampedXml = opts.rendered ? musicXml : stampNoteIds(musicXml);\n const noteModel: Map<string, NoteModel> = opts.rendered ? new Map() : noteModelFromXml(stampedXml);\n\n // ── BREAK PLAN (`VerovioLayoutOptions.barsPerLine` / `.sectionStarts`) ──\n // Break POLICY lives in this layer, not in the consumer. A caller's\n // explicit bars-per-line target and/or section starts are computed ONCE\n // here and encoded into `plannedXml`, which every engrave renders with\n // `breaks: 'line'`. Auto's readability-floor plan is deliberately separate\n // below: it is computed once per (document, measured width) from stampedXml.\n //\n // Computing it from `stampedXml` once, rather than per engrave from\n // whatever was last rendered, is what makes repeated engraves idempotent:\n // a zoom change or reflow can never compound a second set of `<print>`s\n // onto an already-broken document. `injectSystemBreaks` is itself\n // idempotent, so even a double application would be harmless — but the\n // plan positions would be wrong the second time, having been computed\n // against a document whose layout already changed.\n //\n // No plan options → `plannedXml === stampedXml` and nothing below changes:\n // `breaks: 'auto'` plus the widow pass, exactly as before.\n const measureTotal = measureCount(stampedXml);\n // `sectionStarts` WITHOUT `barsPerLine` is not a plan up front: it is the\n // auto pipeline plus one re-plan at auto's settled density — see the\n // \"AUTO + SECTIONS PASS\" in `engraveOnce`.\n const autoWithSections = (opts.verovioOptions?.sectionStarts?.length ?? 0) > 0\n && !((opts.verovioOptions?.barsPerLine ?? 0) > 0);\n const breakPlan: number[] = (() => {\n const layout = opts.verovioOptions;\n const starts = layout?.sectionStarts ?? [];\n const every = layout?.barsPerLine ?? 0;\n if (!starts.length && !(every > 0)) return [];\n if (autoWithSections) return [];\n // A concrete caller preference (2/4/8) is a maximum, not a suggestion:\n // never widen 2 bars per line into 3 merely to avoid a final widow.\n if (!starts.length && every > 0) return fixedBarsPerLineBreaks(measureTotal, every);\n return sectionAwareBreaks(measureTotal, starts, every);\n })();\n const plannedXml = breakPlan.length ? injectSystemBreaks(stampedXml, breakPlan) : stampedXml;\n\n function autoFloorPlanXml(barsPerLine: number): string {\n // The normal planner intentionally avoids one-measure widows. At the\n // floor's terminal state that would undo the whole point, so force every\n // measure onto its own system instead.\n const positions = barsPerLine <= 1\n ? Array.from({ length: Math.max(0, measureTotal - 1) }, (_, i) => i + 1)\n : sectionAwareBreaks(measureTotal, [], barsPerLine);\n return positions.length ? injectSystemBreaks(stampedXml, positions) : stampedXml;\n }\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 // Published contract: hosts may locate the playhead (e.g. to keep it in\n // view inside their own scroll container) via [data-rmp-playhead] — same\n // attribute notationPlayerSvg.ts sets, same reasoning (see its own\n // comment). The element stays owned by this component — position/size\n // are not API.\n playheadEl.dataset.rmpPlayhead = '1';\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({ topMarginPx: opts.followTopMarginPx });\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 // Verovio honors print-object=\"no\" for pitched notes on its own but NOT\n // for rests (empirical finding — see applyPrintObjectHiding's own doc);\n // this patches the gap. Every fresh render is a fresh DOM, so this must\n // run after EVERY renderAllPages call, not just the initial one.\n applyPrintObjectHiding(svgHost, noteModel);\n }\n\n // ─── markNotes (survives re-engraving — reapplied after every render) ──\n\n let markedCols: number[] = [];\n\n function applyMarks(): void {\n for (const el of svgHost.querySelectorAll(`.${MARKED_NOTE_CLASS}`)) {\n el.classList.remove(MARKED_NOTE_CLASS);\n }\n if (!currentLayout || !markedCols.length) return;\n for (const col of markedCols) {\n for (const el of verovioNotesAtColumn(svgHost, currentLayout, col)) {\n el.classList.add(MARKED_NOTE_CLASS);\n }\n }\n }\n\n /**\n * WIDOW PASS helper (E7M — \"good sight-reading software never shows a\n * one-bar last line\") — extracted so `engraveOnce` can run it from TWO\n * places: after the normal first pass (breaks:'auto', as before), AND\n * after the readability-floor fallback pass forces `breaks: 'auto'` on a\n * caller-encoded document (see `engraveOnce`'s own \"Fallback pass\" doc).\n * Assumes `xml`/`layoutOpts` is ALREADY loaded+rendered into\n * `svgHost`/`currentLayout` (the caller just did that) — this only reads\n * `svgHost`'s current DOM shape and, if a widow is found, re-engraves.\n *\n * Verovio's own `breaksNoWidow` (in `VEROVIO_LAYOUT_DEFAULTS`) only\n * prevents a lone measure on the last PAGE — verified on a live page that\n * a 6-bar excerpt under `breaks: 'auto'` still renders as systems of 5+1\n * within a SINGLE page. This reads the rendered measure count per system\n * straight from the DOM (`systemMeasureCounts` — plain `.system`/\n * `.measure` traversal, no geometry needed). If there are ≥ 2 systems and\n * the LAST one has exactly 1 measure, it computes evenly-spread break\n * points for the SAME number of systems (`balancedSystemBreaks`) and\n * re-engraves ONCE with those breaks encoded (`injectSystemBreaks` +\n * `breaks: 'line'`) in place of `xml`/`layoutOpts`'s own render —\n * `injectSystemBreaks` only ever ADDS `<print>` elements (never touches a\n * `<note>`), so this never disturbs `stampedXml`'s own note ids /\n * `noteModel` join (pinned by this file's own re-balanced-render test).\n *\n * Returns the (xml, layoutOpts) pair that now reflects `svgHost`'s\n * content: unchanged on \"no widow found\" or \"re-engrave failed\" (the\n * caller's own already-rendered pass is left in place — same \"degrade to\n * the prior render, never blank\" style as the rest of `engraveOnce`), or\n * the widow-fixed pair on success.\n */\n function runWidowPass(\n toolkit: VerovioToolkitInstance,\n widthPx: number,\n zoom: number,\n xml: string,\n layoutOpts: VerovioLayoutOptions | undefined,\n ): { xml: string; layoutOpts: VerovioLayoutOptions | undefined } {\n const counts = systemMeasureCounts(svgHost);\n if (counts.length >= 2 && counts[counts.length - 1] === 1) {\n const totalMeasures = counts.reduce((a, b) => a + b, 0);\n const breakPositions = balancedSystemBreaks(totalMeasures, counts.length);\n if (breakPositions.length) {\n const widowXml = injectSystemBreaks(xml, breakPositions);\n const widowLayoutOpts: VerovioLayoutOptions = { ...layoutOpts, breaks: 'line' };\n toolkit.setOptions(verovioRenderOptions(widthPx, zoom, widowLayoutOpts));\n const okWidow = !!toolkit.loadData(widowXml);\n if (okWidow) {\n renderAllPages(toolkit);\n rebuildLayoutFromDom();\n return { xml: widowXml, layoutOpts: widowLayoutOpts };\n }\n // A failed widow re-engrave leaves the caller's own render (still in\n // svgHost/currentLayout from before this call) in place.\n }\n }\n return { xml, layoutOpts };\n }\n\n /**\n * Engrave `stampedXml` into the shared toolkit at (`widthPx`, `zoom`),\n * read the resulting layout back from the DOM, run the WIDOW PASS\n * (`runWidowPass`, above), then — if the effective breaks are still\n * caller-encoded (`'line'`/`'encoded'`) and the fit factor is below the\n * readability floor — run the FALLBACK PASS (below); finally, if whatever\n * ended up rendered produced a system wider than the page, re-engrave ONCE\n * more at a proportionally smaller effective zoom (see `fitZoomFactor`'s\n * own doc for why this can happen and the module doc's \"Fit-to-width\"\n * section). MUST run synchronously start-to-finish (no `await` inside) —\n * this is the `fn` passed to `withToolkit`, and its\n * single-sequence-at-a-time guarantee depends on that (see `withToolkit`'s\n * own doc). `currentZoom` is intentionally NOT updated here for the fit\n * correction — only the caller (`initialEngrave`/`reflow`) tracks the\n * user's REQUESTED zoom, so a later `setZoom(z)` steps from that requested\n * value, not from whatever the fit correction happened to render at; the\n * fit is recomputed fresh on every engrave rather than remembered. Returns\n * whether the FIRST pass's `loadData` succeeded — a failed later\n * re-engrave (rare) just leaves the most-recent successful pass's\n * already-rendered content in place rather than blanking the player.\n *\n * FALLBACK PASS (readability floor — `VerovioLayoutOptions.minFitFactor`,\n * default `DEFAULT_MIN_FIT_FACTOR`): with ENCODED breaks (Stave's own\n * `<print new-system=\"yes\"/>` + `breaks: 'line'` usage), Verovio must put\n * whatever notes fall between two encoded break points onto one system,\n * however dense — unlike `breaks: 'auto'`, it cannot relieve overcrowding\n * by moving a measure to the next line (see `fitZoomFactor`'s own doc).\n * The fit pass's only lever for a too-wide system is shrinking the\n * effective zoom, which on dense-enough music (e.g. a Bach fugue excerpt\n * that only fits 2 bars/line at zoom 0.85) shrinks glyphs to unreadable —\n * \"good sight-reading software prefers readable glyphs over a fixed bar\n * count.\" `shouldFallbackToAutoBreaks` (pure, own tests) is the exact\n * trigger condition, checked against the effective `breaks` the CALLER\n * asked for (`layoutOpts?.breaks` — never the widow pass's OWN\n * auto→'line' rewrite, since that never fires on already-caller-encoded\n * breaks in the first place, per the widow pass's own \"only ever applies\n * to breaks:'auto'\" rule). When it fires, this re-engraves ONCE with\n * `breaks: 'auto'` on the ORIGINAL `stampedXml` (ignoring the caller's\n * encoded breaks entirely — Verovio's own line-breaking algorithm decides\n * bar count instead), then runs the widow pass again on THAT result\n * (auto-breaking a short excerpt can itself produce a widow, same as the\n * normal 'auto' path). A failed fallback re-engrave leaves the\n * caller-encoded (post-widow-pass) render in place — degrade to \"unfit but\n * rendered,\" never blank. At most one fallback re-engrave per\n * `engraveOnce` call (the check runs once, off the FIRST pass's — or its\n * own widow pass's — fit factor only).\n */\n function engraveOnce(toolkit: VerovioToolkitInstance, widthPx: number, zoom: number): boolean {\n const layoutOpts = opts.verovioOptions;\n // A break plan IS encoded breaks, so it takes over `breaks` for every\n // downstream decision: the widow pass is skipped (the plan is already\n // balanced and widow-free by construction), and the readability-floor\n // fallback below treats the plan the same way it treats any\n // caller-encoded layout — see `shouldFallbackToAutoBreaks`.\n const planLayoutOpts: VerovioLayoutOptions | undefined = breakPlan.length\n ? { ...layoutOpts, breaks: 'line' }\n : layoutOpts;\n const effectiveBreaks = planLayoutOpts?.breaks ?? VEROVIO_LAYOUT_DEFAULTS.breaks;\n const minFitFactor = layoutOpts?.minFitFactor ?? DEFAULT_MIN_FIT_FACTOR;\n const avoidWidows = layoutOpts?.avoidWidows !== false;\n let xmlToRender = plannedXml;\n let renderLayoutOpts = planLayoutOpts;\n\n toolkit.setOptions(verovioRenderOptions(widthPx, zoom, renderLayoutOpts));\n const ok = !!toolkit.loadData(xmlToRender);\n if (ok) renderAllPages(toolkit);\n rebuildLayoutFromDom();\n\n if (ok && currentLayout) {\n const systemWidthsPx = () =>\n opts.measureSystemWidths ? opts.measureSystemWidths(svgHost) : currentLayout!.systems.map((s) => s.w);\n let factor = fitZoomFactor(systemWidthsPx(), widthPx);\n let terminalAutoFloorPlan = false;\n\n // Auto normally owns line breaking, but its result can either overflow\n // into the fit pass below OR fit geometrically while still giving each\n // measure too little rendered width. Re-plan from the pristine stamped\n // document at fewer bars per line instead (4 -> 2 -> 1); every\n // candidate is measured after engraving, so this is width-aware rather\n // than a viewport breakpoint. The 1-bar plan deliberately bypasses the\n // widow pass and the fit correction: it is the readability terminal.\n if (effectiveBreaks === 'auto') {\n const autoNeedsFloorPlan = () => shouldReplanAutoBreaks(\n factor,\n minFitFactor,\n systemWidthsPx(),\n systemMeasureCounts(svgHost),\n );\n\n // A naturally one-bar auto layout may already clear the density/fit\n // floor, yet still be visibly short because Verovio's print-page\n // side margins consume too much of a narrow host. Re-engrave the\n // SAME auto plan with compact margins only when its measured systems\n // miss the host-width floor. This preserves desktop's established\n // margins and coordinate system.\n if (!autoNeedsFloorPlan() && shouldUseCompactAutoMargins(systemWidthsPx(), widthPx)) {\n const compactAutoLayoutOpts: VerovioLayoutOptions = {\n ...layoutOpts,\n breaks: 'auto',\n pageMarginLeft: AUTO_FLOOR_PAGE_MARGIN,\n pageMarginRight: AUTO_FLOOR_PAGE_MARGIN,\n };\n toolkit.setOptions(verovioRenderOptions(widthPx, zoom, compactAutoLayoutOpts));\n if (toolkit.loadData(stampedXml)) {\n renderAllPages(toolkit);\n rebuildLayoutFromDom();\n xmlToRender = stampedXml;\n renderLayoutOpts = compactAutoLayoutOpts;\n factor = fitZoomFactor(systemWidthsPx(), widthPx);\n }\n }\n\n if (!autoNeedsFloorPlan() && avoidWidows) {\n const widowed = runWidowPass(toolkit, widthPx, zoom, xmlToRender, renderLayoutOpts);\n xmlToRender = widowed.xml;\n renderLayoutOpts = widowed.layoutOpts;\n factor = fitZoomFactor(systemWidthsPx(), widthPx);\n }\n\n let barsPerLine = Math.max(1, ...systemMeasureCounts(svgHost));\n for (;;) {\n const nextBarsPerLine = nextReadabilityFloorBarsPerLine(\n barsPerLine,\n factor,\n minFitFactor,\n autoNeedsFloorPlan(),\n );\n if (nextBarsPerLine === null) break;\n\n const floorXml = autoFloorPlanXml(nextBarsPerLine);\n const floorLayoutOpts: VerovioLayoutOptions = {\n ...layoutOpts,\n breaks: 'line',\n pageMarginLeft: AUTO_FLOOR_PAGE_MARGIN,\n pageMarginRight: AUTO_FLOOR_PAGE_MARGIN,\n };\n toolkit.setOptions(verovioRenderOptions(widthPx, zoom, floorLayoutOpts));\n if (!toolkit.loadData(floorXml)) break;\n\n renderAllPages(toolkit);\n rebuildLayoutFromDom();\n xmlToRender = floorXml;\n renderLayoutOpts = floorLayoutOpts;\n factor = fitZoomFactor(systemWidthsPx(), widthPx);\n barsPerLine = nextBarsPerLine;\n\n if (barsPerLine === 1 && autoNeedsFloorPlan()) {\n terminalAutoFloorPlan = true;\n break;\n }\n }\n\n // AUTO + SECTIONS PASS (`sectionStarts` without `barsPerLine`): the\n // settled auto render above is the density oracle. Re-plan ONCE so\n // every section starts its own line, balanced within sections at\n // auto's own typical count (`autoSectionBreakPlan` returns [] when\n // nothing needs fixing). The one-bar terminal already breaks at\n // every bar, so it is left alone. Whatever margins the auto passes\n // settled on are kept; the fit-to-width pass below still applies.\n if (autoWithSections && !terminalAutoFloorPlan) {\n const sectionPlan = autoSectionBreakPlan(\n measureTotal,\n layoutOpts?.sectionStarts ?? [],\n systemMeasureCounts(svgHost),\n );\n if (sectionPlan.length) {\n const sectionXml = injectSystemBreaks(stampedXml, sectionPlan);\n const sectionLayoutOpts: VerovioLayoutOptions = { ...renderLayoutOpts, breaks: 'line' };\n toolkit.setOptions(verovioRenderOptions(widthPx, zoom, sectionLayoutOpts));\n if (toolkit.loadData(sectionXml)) {\n renderAllPages(toolkit);\n rebuildLayoutFromDom();\n xmlToRender = sectionXml;\n renderLayoutOpts = sectionLayoutOpts;\n factor = fitZoomFactor(systemWidthsPx(), widthPx);\n }\n }\n }\n }\n\n if (shouldFallbackToAutoBreaks(factor, effectiveBreaks, minFitFactor)) {\n const autoLayoutOpts: VerovioLayoutOptions = { ...layoutOpts, breaks: 'auto' };\n toolkit.setOptions(verovioRenderOptions(widthPx, zoom, autoLayoutOpts));\n const okAuto = !!toolkit.loadData(stampedXml);\n if (okAuto) {\n renderAllPages(toolkit);\n rebuildLayoutFromDom();\n xmlToRender = stampedXml;\n renderLayoutOpts = autoLayoutOpts;\n\n if (avoidWidows) {\n const widowed = runWidowPass(toolkit, widthPx, zoom, xmlToRender, renderLayoutOpts);\n xmlToRender = widowed.xml;\n renderLayoutOpts = widowed.layoutOpts;\n }\n factor = fitZoomFactor(systemWidthsPx(), widthPx);\n }\n // A failed fallback re-engrave leaves the caller-encoded render\n // (still in svgHost/currentLayout from before this block) in place —\n // xmlToRender/renderLayoutOpts/factor stay at their pre-fallback\n // values, so the fit-to-width pass below reflows THAT document.\n }\n\n if (factor < 1 && !terminalAutoFloorPlan) {\n const effectiveZoom = zoom * factor;\n toolkit.setOptions(verovioRenderOptions(widthPx, effectiveZoom, renderLayoutOpts));\n const ok2 = !!toolkit.loadData(xmlToRender);\n if (ok2) {\n renderAllPages(toolkit);\n rebuildLayoutFromDom();\n }\n // A failed second loadData leaves the prior render (still in\n // svgHost/currentLayout from just above) untouched — degrade to\n // \"unfit but rendered\" rather than blank.\n }\n }\n return ok;\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 widthPx = desiredEngraveWidthPx();\n const ok = await withToolkit((toolkit) => {\n if (destroyed) return false; // stale — this player died while queued\n return engraveOnce(toolkit, widthPx, currentZoom);\n });\n if (destroyed) return;\n\n lastEngravedWidthPx = widthPx;\n loaded = ok;\n applyMarks();\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 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 // RECLAIM (see the module doc's \"SHARED TOOLKIT INSTANCE\" note, and\n // `withToolkit`'s own doc for bug E7M-322): the module-level toolkit is\n // shared across every live player on the page — and the real consumer\n // (PlayerPage) keeps a harmony player AND a written player alive\n // SIMULTANEOUSLY (lazy-built, destroyed only on piece switch), so\n // `loadData` calls from the two players genuinely interleave (e.g.\n // harmony active -> resize while written hidden -> back to written ->\n // writtenPlayer.setZoom()). A plain `redoLayout()` here would silently\n // re-lay-out and render WHATEVER document the toolkit currently holds —\n // which may belong to the OTHER player if it rendered more recently. So\n // `engraveOnce` re-parses THIS player's own `musicXml` synchronously,\n // immediately before rendering (loadData does a full parse + layout with\n // the just-set options, making a separate `redoLayout()` call\n // redundant). `withToolkit` now additionally guarantees no OTHER\n // player's queued task can run its own setOptions/loadData/render\n // sequence in between this task's own steps — the shared singleton is\n // therefore safe under alternating AND concurrent use. See\n // `tests/notationPlayerVerovio.test.ts`'s two-player interleaved-reflow\n // test for the regression this guards against.\n const ok = await withToolkit((toolkit) => {\n if (destroyed || myToken !== rebuildToken) return false; // stale\n return engraveOnce(toolkit, widthPx, newZoom);\n });\n\n if (destroyed || myToken !== rebuildToken) return;\n if (!ok) {\n loaded = false;\n return;\n }\n\n lastEngravedWidthPx = widthPx;\n currentZoom = newZoom;\n applyMarks();\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 markNotes(cols: number[] | null): void {\n markedCols = cols ?? [];\n applyMarks();\n },\n notePositions(): EngravedNote[] {\n return verovioEngravedNotes(svgHost, host, noteModel, onsetIdByNoteId(rawOnsets));\n },\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 markedCols = [];\n currentLayout = null;\n currentNoteCols = undefined;\n if (root.parentNode === host) host.removeChild(root);\n },\n };\n}\n"],"mappings":"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","names":["systemMeasureCounts"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@real-music-packages/web-core",
3
- "version": "0.49.0",
3
+ "version": "0.50.0",
4
4
  "description": "Shared music-theory + audio primitives for the music-suite web apps",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",