@real-music-packages/web-core 0.46.0 → 0.48.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.
- package/dist/chunk-JCDNEJBT.js +176 -0
- package/dist/chunk-JCDNEJBT.js.map +1 -0
- package/dist/notationPlayerVerovio.d.ts +42 -58
- package/dist/notationPlayerVerovio.js +88 -145
- package/dist/notationPlayerVerovio.js.map +1 -1
- package/dist/notationXml.d.ts +175 -0
- package/dist/notationXml.js +19 -0
- package/dist/notationXml.js.map +1 -0
- package/package.json +5 -1
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// src/notationXml.ts
|
|
2
|
+
function stampNoteIds(xml) {
|
|
3
|
+
const doc = new DOMParser().parseFromString(xml, "application/xml");
|
|
4
|
+
if (doc.querySelector("parsererror")) return xml;
|
|
5
|
+
const parts = Array.from(doc.querySelectorAll("score-partwise > part"));
|
|
6
|
+
parts.forEach((part, partIdx) => {
|
|
7
|
+
for (const measure of Array.from(part.querySelectorAll("measure"))) {
|
|
8
|
+
const number = measure.getAttribute("number") ?? "0";
|
|
9
|
+
const notes = Array.from(measure.children).filter((el) => el.tagName === "note");
|
|
10
|
+
notes.forEach((note, noteIdx) => {
|
|
11
|
+
note.setAttribute("id", `n-${partIdx}-${number}-${noteIdx}`);
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
return new XMLSerializer().serializeToString(doc);
|
|
16
|
+
}
|
|
17
|
+
function measureCount(xml) {
|
|
18
|
+
const doc = new DOMParser().parseFromString(xml, "application/xml");
|
|
19
|
+
if (doc.querySelector("parsererror")) return 0;
|
|
20
|
+
const root = doc.documentElement;
|
|
21
|
+
if (!root || root.tagName !== "score-partwise") return 0;
|
|
22
|
+
const [firstPart] = childrenNamed(root, "part");
|
|
23
|
+
return firstPart ? childrenNamed(firstPart, "measure").length : 0;
|
|
24
|
+
}
|
|
25
|
+
function injectSystemBreaks(xml, breakBeforeBarIndexes) {
|
|
26
|
+
const doc = new DOMParser().parseFromString(xml, "application/xml");
|
|
27
|
+
if (doc.querySelector("parsererror")) return xml;
|
|
28
|
+
const root = doc.documentElement;
|
|
29
|
+
if (!root || root.tagName !== "score-partwise") return xml;
|
|
30
|
+
for (const part of childrenNamed(root, "part")) {
|
|
31
|
+
const measures = childrenNamed(part, "measure");
|
|
32
|
+
for (const pos of breakBeforeBarIndexes) {
|
|
33
|
+
if (!Number.isInteger(pos) || pos <= 0 || pos >= measures.length) continue;
|
|
34
|
+
const measure = measures[pos];
|
|
35
|
+
const existingPrint = firstChildNamed(measure, "print");
|
|
36
|
+
if (existingPrint) {
|
|
37
|
+
existingPrint.setAttribute("new-system", "yes");
|
|
38
|
+
} else {
|
|
39
|
+
const printEl = doc.createElement("print");
|
|
40
|
+
printEl.setAttribute("new-system", "yes");
|
|
41
|
+
measure.insertBefore(printEl, measure.firstChild);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return new XMLSerializer().serializeToString(doc);
|
|
46
|
+
}
|
|
47
|
+
function balancedSystemBreaks(totalMeasures, systems) {
|
|
48
|
+
if (systems <= 1 || totalMeasures <= 0) return [];
|
|
49
|
+
const per = Math.ceil(totalMeasures / systems);
|
|
50
|
+
if (per <= 0) return [];
|
|
51
|
+
const breaks = [];
|
|
52
|
+
for (let b = per; b < totalMeasures; b += per) breaks.push(b);
|
|
53
|
+
return breaks;
|
|
54
|
+
}
|
|
55
|
+
function barsPerLineBreaks(totalBars, every) {
|
|
56
|
+
if (!Number.isFinite(totalBars) || !Number.isFinite(every)) return [];
|
|
57
|
+
if (every <= 0 || totalBars <= 0) return [];
|
|
58
|
+
let lines = Math.ceil(totalBars / every);
|
|
59
|
+
let per = Math.ceil(totalBars / lines);
|
|
60
|
+
while (lines > 1 && totalBars - (lines - 1) * per < 2) {
|
|
61
|
+
lines--;
|
|
62
|
+
per = Math.ceil(totalBars / lines);
|
|
63
|
+
}
|
|
64
|
+
return balancedSystemBreaks(totalBars, lines);
|
|
65
|
+
}
|
|
66
|
+
function sectionAwareBreaks(totalBars, sectionStarts, every) {
|
|
67
|
+
if (!Number.isFinite(totalBars) || totalBars <= 0) return [];
|
|
68
|
+
const usable = sectionStarts.filter((b) => Number.isInteger(b) && b > 0 && b < totalBars);
|
|
69
|
+
const boundaries = Array.from(/* @__PURE__ */ new Set([0, ...usable])).sort((a, b) => a - b);
|
|
70
|
+
const result = /* @__PURE__ */ new Set();
|
|
71
|
+
if (every > 0) {
|
|
72
|
+
for (let i = 0; i < boundaries.length; i++) {
|
|
73
|
+
const start = boundaries[i];
|
|
74
|
+
const end = i + 1 < boundaries.length ? boundaries[i + 1] : totalBars;
|
|
75
|
+
for (const pos of barsPerLineBreaks(end - start, every)) result.add(start + pos);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
for (const s of usable) result.add(s);
|
|
79
|
+
return Array.from(result).sort((a, b) => a - b);
|
|
80
|
+
}
|
|
81
|
+
var STEP_SEMITONE = { C: 0, D: 2, E: 4, F: 5, G: 7, A: 9, B: 11 };
|
|
82
|
+
function firstChildNamed(el, tag) {
|
|
83
|
+
for (const c of Array.from(el.children)) if (c.tagName === tag) return c;
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
function childrenNamed(el, tag) {
|
|
87
|
+
return Array.from(el.children).filter((c) => c.tagName === tag);
|
|
88
|
+
}
|
|
89
|
+
function textOf(el) {
|
|
90
|
+
return el && el.textContent != null ? el.textContent.trim() : null;
|
|
91
|
+
}
|
|
92
|
+
function pitchMidiFromElement(pitchEl) {
|
|
93
|
+
const step = textOf(firstChildNamed(pitchEl, "step")) ?? "C";
|
|
94
|
+
const octave = Number(textOf(firstChildNamed(pitchEl, "octave")) ?? "4");
|
|
95
|
+
const alter = Number(textOf(firstChildNamed(pitchEl, "alter")) ?? "0");
|
|
96
|
+
return 12 * (octave + 1) + (STEP_SEMITONE[step] ?? 0) + Math.trunc(alter);
|
|
97
|
+
}
|
|
98
|
+
function partStaffCount(part) {
|
|
99
|
+
let maxDeclared = 0;
|
|
100
|
+
for (const attrs of childrenNamed(part, "measure").flatMap((m) => childrenNamed(m, "attributes"))) {
|
|
101
|
+
const staves = Number(textOf(firstChildNamed(attrs, "staves")) ?? "0");
|
|
102
|
+
if (staves > maxDeclared) maxDeclared = staves;
|
|
103
|
+
}
|
|
104
|
+
if (maxDeclared > 0) return maxDeclared;
|
|
105
|
+
let maxStaff = 0;
|
|
106
|
+
for (const measure of childrenNamed(part, "measure")) {
|
|
107
|
+
for (const note of childrenNamed(measure, "note")) {
|
|
108
|
+
const staff = Number(textOf(firstChildNamed(note, "staff")) ?? "0");
|
|
109
|
+
if (staff > maxStaff) maxStaff = staff;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return Math.max(1, maxStaff);
|
|
113
|
+
}
|
|
114
|
+
function noteModelFromXml(stampedXml) {
|
|
115
|
+
const model = /* @__PURE__ */ new Map();
|
|
116
|
+
let doc;
|
|
117
|
+
try {
|
|
118
|
+
doc = new DOMParser().parseFromString(stampedXml, "application/xml");
|
|
119
|
+
} catch {
|
|
120
|
+
return model;
|
|
121
|
+
}
|
|
122
|
+
if (doc.querySelector("parsererror")) return model;
|
|
123
|
+
const root = doc.documentElement;
|
|
124
|
+
if (!root || root.tagName !== "score-partwise") return model;
|
|
125
|
+
const parts = childrenNamed(root, "part");
|
|
126
|
+
let staffOffset = 0;
|
|
127
|
+
for (const part of parts) {
|
|
128
|
+
const staffCount = partStaffCount(part);
|
|
129
|
+
let divisions = 1;
|
|
130
|
+
const measureEls = childrenNamed(part, "measure");
|
|
131
|
+
measureEls.forEach((measureEl, measureIndex) => {
|
|
132
|
+
for (const child of Array.from(measureEl.children)) {
|
|
133
|
+
if (child.tagName === "attributes") {
|
|
134
|
+
const divText = textOf(firstChildNamed(child, "divisions"));
|
|
135
|
+
if (divText) divisions = Number(divText) || divisions;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (child.tagName !== "note") continue;
|
|
139
|
+
const note = child;
|
|
140
|
+
const id = note.getAttribute("id");
|
|
141
|
+
if (!id) continue;
|
|
142
|
+
const isGrace = !!firstChildNamed(note, "grace");
|
|
143
|
+
const isRest = !!firstChildNamed(note, "rest");
|
|
144
|
+
const pitchEl = firstChildNamed(note, "pitch");
|
|
145
|
+
const staffNumber = Number(textOf(firstChildNamed(note, "staff")) ?? "1") || 1;
|
|
146
|
+
const durationText = textOf(firstChildNamed(note, "duration"));
|
|
147
|
+
const durationReal = isGrace || !durationText ? 0 : Number(durationText) / divisions / 4;
|
|
148
|
+
const tieStopDirect = childrenNamed(note, "tie").some((t) => t.getAttribute("type") === "stop");
|
|
149
|
+
const notationsEl = firstChildNamed(note, "notations");
|
|
150
|
+
const tieStopNotated = notationsEl ? childrenNamed(notationsEl, "tied").some((t) => t.getAttribute("type") === "stop") : false;
|
|
151
|
+
model.set(id, {
|
|
152
|
+
midi: pitchEl ? pitchMidiFromElement(pitchEl) : null,
|
|
153
|
+
isRest,
|
|
154
|
+
tieContinuation: tieStopDirect || tieStopNotated,
|
|
155
|
+
staffIndex: staffOffset + Math.max(0, staffNumber - 1),
|
|
156
|
+
durationReal,
|
|
157
|
+
measureIndex,
|
|
158
|
+
hidden: note.getAttribute("print-object") === "no"
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
staffOffset += staffCount;
|
|
163
|
+
}
|
|
164
|
+
return model;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export {
|
|
168
|
+
stampNoteIds,
|
|
169
|
+
measureCount,
|
|
170
|
+
injectSystemBreaks,
|
|
171
|
+
balancedSystemBreaks,
|
|
172
|
+
barsPerLineBreaks,
|
|
173
|
+
sectionAwareBreaks,
|
|
174
|
+
noteModelFromXml
|
|
175
|
+
};
|
|
176
|
+
//# sourceMappingURL=chunk-JCDNEJBT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/notationXml.ts"],"sourcesContent":["// Pure MusicXML→MusicXML/model helpers for `createVerovioNotationPlayer`\n// (notationPlayerVerovio.ts, 0.40.0) — the id-stamping + note-model half of\n// its \"join model ids to Verovio's rendered SVG\" approach (that module's\n// header doc, point 1). No DOM rendering here; everything in this file is\n// pure XML-in → XML/data-out, unit-testable with no live SVG at all.\n//\n// OWNERSHIP NOTE — `stampNoteIds`'s deterministic id scheme\n// (`n-{partIdx}-{measureNumber}-{noteIdxInMeasure}`) is DELIBERATELY\n// duplicated, byte-for-byte, from stave-web-sightread's\n// `src/lib/bach/xmlTransforms.ts:stampNoteIds` (part of that repo's Verovio\n// transform pipeline, docs/superpowers/specs/2026-08-11-verovio-player-design.md\n// §1). stave's copy is the one actually wired into `parseReduction` (its\n// `noteIds` per span must match what got stamped onto the MusicXML BEFORE\n// Verovio ever saw it), so stave remains that scheme's owner for the\n// pipeline's purposes. This copy exists so `createVerovioNotationPlayer`\n// works correctly for ANY caller — including ones that hand in un-stamped\n// MusicXML — without a runtime dependency from web-core back into an app\n// repo (the wrong direction for a shared package). Both copies are pure\n// functions of a note's POSITION (never of any id already present), so\n// calling either one on already-stamped input reproduces the exact same\n// ids — the two copies can never drift apart in OBSERVABLE behavior even\n// though they are physically two files. If the scheme ever needs to change,\n// change it in BOTH places (this file's own tests pin the scheme\n// independently of stave's, so a one-sided edit fails a test here).\nexport function stampNoteIds(xml: string): string {\n const doc = new DOMParser().parseFromString(xml, 'application/xml');\n if (doc.querySelector('parsererror')) return xml;\n\n const parts = Array.from(doc.querySelectorAll('score-partwise > part'));\n parts.forEach((part, partIdx) => {\n for (const measure of Array.from(part.querySelectorAll('measure'))) {\n const number = measure.getAttribute('number') ?? '0';\n const notes = Array.from(measure.children).filter((el) => el.tagName === 'note');\n notes.forEach((note, noteIdx) => {\n note.setAttribute('id', `n-${partIdx}-${number}-${noteIdx}`);\n });\n }\n });\n\n return new XMLSerializer().serializeToString(doc);\n}\n\n// ─── injectSystemBreaks / balancedSystemBreaks — widow-avoidance helpers ──\n//\n// Used by notationPlayerVerovio.ts's widow pass (`engraveOnce`): when\n// `breaks: 'auto'` leaves a lone trailing measure on its own system (a\n// \"widow\" — Verovio's own `breaksNoWidow` only guards the last PAGE, not the\n// last SYSTEM, per that module's doc), the player forces exact line breaks\n// via `injectSystemBreaks` + `breaks: 'line'`, choosing the break points with\n// `balancedSystemBreaks` so the same number of systems comes out evenly\n// filled instead of front-loaded-then-orphaned.\n//\n// CONTRACT DUPLICATION NOTICE — stave-web-sightread's\n// `src/lib/bach/xmlTransforms.ts` has its OWN `injectSystemBreaks`,\n// `barsPerLineBreaks` and `sectionAwareBreaks`, written on the reasoning\n// that \"a shared package must not runtime-depend on an app repo for\n// something this small\". That reasoning is backwards for break POLICY: the\n// engraver is what owns where systems begin, so those copies are being\n// RETIRED in favour of the ones here, reached through\n// `VerovioLayoutOptions.barsPerLine` / `.sectionStarts` rather than by the\n// app hand-injecting breaks of its own.\n//\n// Until that app-side change lands, both copies exist and must stay in sync.\n// (The app's `injectSystemBreaks` uses `querySelectorAll('measure')` where\n// this one walks direct children only — this one is the stricter of the two\n// on scores with nested measure-like elements.)\n\n/**\n * Insert `<print new-system=\"yes\"/>` as the FIRST child of the `<measure>`\n * at each 0-based position in `breakBeforeBarIndexes`, in EVERY `<part>`\n * (a score's parts share one measure timeline, so a break must be encoded\n * once per part for Verovio to line the systems up across staves). A\n * position that is ≤ 0 or ≥ that part's own measure count is silently\n * ignored FOR THAT PART (0 is a no-op — a part already starts a new system\n * at its own first measure). Idempotent: a measure that already carries a\n * `<print>` element (from a prior call, or from the source data) gets\n * `new-system=\"yes\"` SET on that existing element rather than gaining a\n * second one — calling this twice with the same positions serializes\n * identically both times. Malformed input (fails to parse) is returned\n * unchanged, same defensive style as `stampNoteIds` above. Only ever ADDS\n * `<print>` elements — never touches a `<note>` — so `stampNoteIds`'s\n * position-based id scheme is unaffected by a prior or subsequent call to\n * this function (see this file's own tests for the pinned invariant).\n */\n/**\n * How many `<measure>` elements the FIRST `<part>` has — the `totalBars`\n * that `barsPerLineBreaks`/`sectionAwareBreaks` plan against.\n *\n * The first part specifically, because that is the coordinate system\n * `injectSystemBreaks` positions live in: a score's parts share one measure\n * timeline, so the first part's ordinals index every part. And ORDINALS, not\n * `<measure number>` attributes — scores skip and repeat bar numbers, so a\n * count is the only safe answer. Malformed input, or a document with no\n * part, returns 0 rather than throwing.\n */\nexport function measureCount(xml: string): number {\n const doc = new DOMParser().parseFromString(xml, 'application/xml');\n if (doc.querySelector('parsererror')) return 0;\n const root = doc.documentElement;\n if (!root || root.tagName !== 'score-partwise') return 0;\n const [firstPart] = childrenNamed(root, 'part');\n return firstPart ? childrenNamed(firstPart, 'measure').length : 0;\n}\n\nexport function injectSystemBreaks(xml: string, breakBeforeBarIndexes: readonly number[]): string {\n const doc = new DOMParser().parseFromString(xml, 'application/xml');\n if (doc.querySelector('parsererror')) return xml;\n\n const root = doc.documentElement;\n if (!root || root.tagName !== 'score-partwise') return xml;\n\n for (const part of childrenNamed(root, 'part')) {\n const measures = childrenNamed(part, 'measure');\n for (const pos of breakBeforeBarIndexes) {\n if (!Number.isInteger(pos) || pos <= 0 || pos >= measures.length) continue;\n const measure = measures[pos];\n const existingPrint = firstChildNamed(measure, 'print');\n if (existingPrint) {\n existingPrint.setAttribute('new-system', 'yes');\n } else {\n const printEl = doc.createElement('print');\n printEl.setAttribute('new-system', 'yes');\n measure.insertBefore(printEl, measure.firstChild);\n }\n }\n }\n\n return new XMLSerializer().serializeToString(doc);\n}\n\n/**\n * Evenly-spread system-break positions for `totalMeasures` measures across\n * `systems` systems: `[per, 2·per, …]` (each strictly `< totalMeasures`),\n * with `per = Math.ceil(totalMeasures / systems)` — `Math.ceil` naturally\n * front-loads any remainder into the EARLIER systems (e.g. 7 measures / 2\n * systems -> per=4 -> systems of 4+3, never 3+4), which is what keeps a\n * later system from being the short/orphaned one. `systems <= 1` (nothing to\n * break between) returns `[]`.\n */\nexport function balancedSystemBreaks(totalMeasures: number, systems: number): number[] {\n if (systems <= 1 || totalMeasures <= 0) return [];\n const per = Math.ceil(totalMeasures / systems);\n if (per <= 0) return [];\n const breaks: number[] = [];\n for (let b = per; b < totalMeasures; b += per) breaks.push(b);\n return breaks;\n}\n\n/**\n * Balanced system-break positions for a target BARS-PER-LINE layout — the\n * caller-facing sibling of `balancedSystemBreaks`, which takes a system\n * COUNT. Rather than stepping `every` bars at a time from bar 0 (which\n * strands a short remainder — a 5-bar excerpt at `every: 4` renders 4+1, a\n * widow of our own making), this decides how many LINES the excerpt needs\n * (`ceil(totalBars / every)`) and hands that count to\n * `balancedSystemBreaks`. `(5, 4)` is `[3]` (3+2) instead of `[4]` (4+1);\n * `(9, 4)` is `[3, 6]` (3+3+3) instead of `[4, 8]` (4+4+1).\n *\n * WIDOW BACK-OFF: a small `every` against a small `totalBars` can make the\n * straight `ceil(totalBars / every)` line count land on a trailing line of\n * exactly 1 bar — the same widow this function exists to remove, reappearing\n * at a different total/every combination (`lines = 3` on 7 bars: `per = 3`\n * fills two 3-bar lines and leaves 1 bar for the third). The line count is\n * therefore backed down (never below 1) until the trailing line holds at\n * least 2 bars — 7 bars lands on 2 lines (4+3) instead of 3 (3+3+1), which\n * is the same \"fewer, fuller lines\" direction `every` already asks for.\n * This never changes the result for any total/every whose straight `ceil`\n * count already avoids a 1-bar trailing line.\n *\n * `every <= 0` (the \"auto — no forced breaks\" sentinel callers resolve to)\n * or a non-positive/non-finite `totalBars` returns `[]`. Never throws.\n *\n * Ported from stave-web-sightread (`src/lib/bach/xmlTransforms.ts`), whose\n * copy this replaces — see the CONTRACT DUPLICATION NOTICE above. Reached by\n * consumers through `VerovioLayoutOptions.barsPerLine` rather than called\n * directly, so the player owns both the plan and its interaction with the\n * widow pass and the readability floor.\n */\nexport function barsPerLineBreaks(totalBars: number, every: number): number[] {\n if (!Number.isFinite(totalBars) || !Number.isFinite(every)) return [];\n if (every <= 0 || totalBars <= 0) return [];\n let lines = Math.ceil(totalBars / every);\n let per = Math.ceil(totalBars / lines);\n while (lines > 1 && totalBars - (lines - 1) * per < 2) {\n lines--;\n per = Math.ceil(totalBars / lines);\n }\n return balancedSystemBreaks(totalBars, lines);\n}\n\n/**\n * Like `barsPerLineBreaks`, but each SECTION's span is balanced on its own\n * instead of one balance running across the whole excerpt — so a line never\n * ends part-way into a neighbouring section's opening bars, and a section's\n * own bars are never split into a widowed last line either.\n *\n * `sectionStarts` are 0-BASED POSITIONS into the excerpt (the same\n * coordinate `injectSystemBreaks` consumes), NOT MusicXML `<measure\n * number>` attributes — scores skip bar numbers, so a caller holding\n * bar-numbered section labels must convert first. `0` (the excerpt's own\n * start) is never a break and is ignored if present.\n *\n * Every section start > 0 is a HARD break in its own right — a new section\n * always begins its own line, not subject to balancing — unioned with\n * `barsPerLineBreaks`' balanced positions computed WITHIN each section's\n * span (its start up to the next section's start, or `totalBars` for the\n * last) and offset back into excerpt-absolute positions.\n *\n * `every <= 0` suppresses the balanced component, matching\n * `barsPerLineBreaks`, but section-start breaks still apply — that is the\n * \"sections only, otherwise let the engraver decide\" mode. Out-of-range and\n * non-integer starts are ignored. The result is sorted, de-duplicated, and\n * every entry is a valid `injectSystemBreaks` position (integer, `> 0`,\n * `< totalBars`). Never throws.\n */\nexport function sectionAwareBreaks(\n totalBars: number,\n sectionStarts: readonly number[],\n every: number,\n): number[] {\n if (!Number.isFinite(totalBars) || totalBars <= 0) return [];\n const usable = sectionStarts.filter((b) => Number.isInteger(b) && b > 0 && b < totalBars);\n const boundaries = Array.from(new Set([0, ...usable])).sort((a, b) => a - b);\n\n const result = new Set<number>();\n\n if (every > 0) {\n for (let i = 0; i < boundaries.length; i++) {\n const start = boundaries[i];\n const end = i + 1 < boundaries.length ? boundaries[i + 1] : totalBars;\n for (const pos of barsPerLineBreaks(end - start, every)) result.add(start + pos);\n }\n }\n\n for (const s of usable) result.add(s);\n\n return Array.from(result).sort((a, b) => a - b);\n}\n\n// ─── noteModelFromXml — the note MODEL half of the join ───────────────────\n\n/** One `<note>`'s MODEL identity — the ground truth `EngravedNote`\n * (notationPlayerSvg.ts) needs that Verovio's rendered SVG cannot supply on\n * its own (a rendered `g.note`/`g.rest` carries geometry, not pitch/tie/\n * duration semantics). Keyed by the note's stamped `id` in\n * `notePositions()`'s return of `verovioEngravedNotes`\n * (notationPlayerVerovio.ts). */\nexport interface NoteModel {\n /** `12*(octave+1) + stepSemitone + alter` — the standard MusicXML→MIDI\n * conversion (the same formula stave-web-sightread's own\n * `practice/probeInputs.ts:pitchMidi` uses — a universal formula, not app\n * logic, so this is not an owned-layer violation to restate here). `null`\n * for a rest or an `<unpitched>` note (no `<pitch>` child) — mirrors\n * `EngravedNote.midi`'s own \"null covers both\" contract. */\n midi: number | null;\n /** Has a `<rest/>` child. */\n isRest: boolean;\n /** True for the STOP half of a tie — a direct `<tie type=\"stop\">` child OR\n * `<notations><tied type=\"stop\">` (exporters vary on which they emit;\n * either counts) — matches `EngravedNote.tieContinuation`'s \"continuation\n * note of a tie, not the struck start\" contract. */\n tieContinuation: boolean;\n /** 0-based, matching `EngravedNote.staffIndex`'s \"0 = top staff of the\n * system\" contract: every `<part>` is walked in document order, and\n * every DISTINCT staff within it (by `<attributes><staves>` when\n * present, else the highest `<staff>` number any of its notes uses, else\n * 1) is assigned the next index — so a single-part 2-staff piano score\n * numbers 0/1 by `<staff>`, and a 2-part 1-staff-per-part reduction (this\n * app's own chord+bass shape) numbers 0/1 by PART, with neither case\n * needing different code. */\n staffIndex: number;\n /** Whole-note fraction (0.25 = quarter) — `duration / divisions / 4`,\n * divisions tracked per-part from the LAST `<attributes><divisions>`\n * seen at or before this note (MusicXML: divisions persist until\n * overridden, default 1). 0 for a grace note (no `<duration>` child —\n * the true, spec-correct signal; never guessed from `<type>`). */\n durationReal: number;\n /** 0-based position of this note's `<measure>` among its OWN `<part>`'s\n * measure children, in document order. Informational only — the live\n * join (`verovioOnsetColumns`/`verovioEngravedNotes`, both in\n * notationPlayerVerovio.ts) resolves the note's RENDERED measure index\n * from the live SVG DOM independently (Verovio's own render order, which\n * is what geometry/hit-testing must agree with), never from this field. */\n measureIndex: number;\n /** `print-object=\"no\"` on the source `<note>` (stave's `hideDoubledNotes`\n * sets this on editorially-doubled notes/rests before handing MusicXML to\n * this player). Verovio's importer HONORS this for `<note>` elements\n * carrying a `<pitch>` (renders `visibility=\"hidden\"` on its own,\n * empirically confirmed against a real 6.2.0 render) but does NOT honor\n * it for `<rest>` notes (the `<g class=\"rest\">` renders fully visible\n * regardless — same empirical check). `createVerovioNotationPlayer`\n * reads this field to force `visibility=\"hidden\"` after every render for\n * ANY id where it's true — a no-op re-application on notes Verovio\n * already hid, and the actual fix on the rests it doesn't (see that\n * module's `applyPrintObjectHiding`). */\n hidden: boolean;\n}\n\nconst STEP_SEMITONE: Record<string, number> = { C: 0, D: 2, E: 4, F: 5, G: 7, A: 9, B: 11 };\n\nfunction firstChildNamed(el: Element, tag: string): Element | null {\n for (const c of Array.from(el.children)) if (c.tagName === tag) return c;\n return null;\n}\nfunction childrenNamed(el: Element, tag: string): Element[] {\n return Array.from(el.children).filter((c) => c.tagName === tag);\n}\nfunction textOf(el: Element | null): string | null {\n return el && el.textContent != null ? el.textContent.trim() : null;\n}\n\nfunction pitchMidiFromElement(pitchEl: Element): number {\n const step = textOf(firstChildNamed(pitchEl, 'step')) ?? 'C';\n const octave = Number(textOf(firstChildNamed(pitchEl, 'octave')) ?? '4');\n const alter = Number(textOf(firstChildNamed(pitchEl, 'alter')) ?? '0');\n return 12 * (octave + 1) + (STEP_SEMITONE[step] ?? 0) + Math.trunc(alter);\n}\n\n/** How many staves `part` uses: the max `<attributes><staves>N</staves>`\n * seen anywhere in it (the authoritative declaration when present), else\n * the highest `<note><staff>N</staff></note>` number any of its notes\n * uses, else 1 (a plain single-staff part declares neither). */\nfunction partStaffCount(part: Element): number {\n let maxDeclared = 0;\n for (const attrs of childrenNamed(part, 'measure').flatMap((m) => childrenNamed(m, 'attributes'))) {\n const staves = Number(textOf(firstChildNamed(attrs, 'staves')) ?? '0');\n if (staves > maxDeclared) maxDeclared = staves;\n }\n if (maxDeclared > 0) return maxDeclared;\n\n let maxStaff = 0;\n for (const measure of childrenNamed(part, 'measure')) {\n for (const note of childrenNamed(measure, 'note')) {\n const staff = Number(textOf(firstChildNamed(note, 'staff')) ?? '0');\n if (staff > maxStaff) maxStaff = staff;\n }\n }\n return Math.max(1, maxStaff);\n}\n\n/**\n * Pure(ish) — MusicXML-in, `id → NoteModel`-out. Walks every `<part>` in\n * document order, then every `<measure>` in document order, then every\n * DIRECT child in document order — `<attributes>` updates the part's own\n * `divisions` cursor; every other non-`<note>` child (`<backup>`,\n * `<forward>`, `<direction>`, …) is a structural/timeline element this\n * function has NO use for (it reads each note's OWN `<duration>` directly,\n * never a cursor POSITION — see `durationReal`'s doc — so unlike\n * `walkMeasureNotes`-style position walks, `<backup>`/`<forward>` need no\n * special handling here beyond being correctly skipped, which plain\n * tag-name filtering already does) — and every `<note>` becomes one model\n * entry, keyed by its `id` attribute (a note with NO `id` — i.e. input that\n * was never run through `stampNoteIds` — is silently skipped: it has no key\n * to join the render against, so there is nothing useful to record).\n *\n * Never throws: malformed input (fails to parse, or no `<score-partwise>`\n * root) returns an empty Map.\n */\nexport function noteModelFromXml(stampedXml: string): Map<string, NoteModel> {\n const model = new Map<string, NoteModel>();\n let doc: Document;\n try {\n doc = new DOMParser().parseFromString(stampedXml, 'application/xml');\n } catch {\n return model;\n }\n if (doc.querySelector('parsererror')) return model;\n const root = doc.documentElement;\n if (!root || root.tagName !== 'score-partwise') return model;\n\n const parts = childrenNamed(root, 'part');\n let staffOffset = 0;\n\n for (const part of parts) {\n const staffCount = partStaffCount(part);\n let divisions = 1;\n const measureEls = childrenNamed(part, 'measure');\n\n measureEls.forEach((measureEl, measureIndex) => {\n for (const child of Array.from(measureEl.children)) {\n if (child.tagName === 'attributes') {\n const divText = textOf(firstChildNamed(child, 'divisions'));\n if (divText) divisions = Number(divText) || divisions;\n continue;\n }\n if (child.tagName !== 'note') continue;\n const note = child;\n const id = note.getAttribute('id');\n if (!id) continue;\n\n const isGrace = !!firstChildNamed(note, 'grace');\n const isRest = !!firstChildNamed(note, 'rest');\n const pitchEl = firstChildNamed(note, 'pitch');\n const staffNumber = Number(textOf(firstChildNamed(note, 'staff')) ?? '1') || 1;\n const durationText = textOf(firstChildNamed(note, 'duration'));\n const durationReal =\n isGrace || !durationText ? 0 : Number(durationText) / divisions / 4;\n\n const tieStopDirect = childrenNamed(note, 'tie').some((t) => t.getAttribute('type') === 'stop');\n const notationsEl = firstChildNamed(note, 'notations');\n const tieStopNotated = notationsEl\n ? childrenNamed(notationsEl, 'tied').some((t) => t.getAttribute('type') === 'stop')\n : false;\n\n model.set(id, {\n midi: pitchEl ? pitchMidiFromElement(pitchEl) : null,\n isRest,\n tieContinuation: tieStopDirect || tieStopNotated,\n staffIndex: staffOffset + Math.max(0, staffNumber - 1),\n durationReal,\n measureIndex,\n hidden: note.getAttribute('print-object') === 'no',\n });\n }\n });\n\n staffOffset += staffCount;\n }\n\n return model;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,64 +1,8 @@
|
|
|
1
1
|
import { N as NotationLayout } from './notationGeometry-DqVBgL7F.js';
|
|
2
2
|
import { EngravedNote } from './notationPlayerSvg.js';
|
|
3
|
+
import { NoteModel } from './notationXml.js';
|
|
3
4
|
import './promo.js';
|
|
4
5
|
|
|
5
|
-
/** One `<note>`'s MODEL identity — the ground truth `EngravedNote`
|
|
6
|
-
* (notationPlayerSvg.ts) needs that Verovio's rendered SVG cannot supply on
|
|
7
|
-
* its own (a rendered `g.note`/`g.rest` carries geometry, not pitch/tie/
|
|
8
|
-
* duration semantics). Keyed by the note's stamped `id` in
|
|
9
|
-
* `notePositions()`'s return of `verovioEngravedNotes`
|
|
10
|
-
* (notationPlayerVerovio.ts). */
|
|
11
|
-
interface NoteModel {
|
|
12
|
-
/** `12*(octave+1) + stepSemitone + alter` — the standard MusicXML→MIDI
|
|
13
|
-
* conversion (the same formula stave-web-sightread's own
|
|
14
|
-
* `practice/probeInputs.ts:pitchMidi` uses — a universal formula, not app
|
|
15
|
-
* logic, so this is not an owned-layer violation to restate here). `null`
|
|
16
|
-
* for a rest or an `<unpitched>` note (no `<pitch>` child) — mirrors
|
|
17
|
-
* `EngravedNote.midi`'s own "null covers both" contract. */
|
|
18
|
-
midi: number | null;
|
|
19
|
-
/** Has a `<rest/>` child. */
|
|
20
|
-
isRest: boolean;
|
|
21
|
-
/** True for the STOP half of a tie — a direct `<tie type="stop">` child OR
|
|
22
|
-
* `<notations><tied type="stop">` (exporters vary on which they emit;
|
|
23
|
-
* either counts) — matches `EngravedNote.tieContinuation`'s "continuation
|
|
24
|
-
* note of a tie, not the struck start" contract. */
|
|
25
|
-
tieContinuation: boolean;
|
|
26
|
-
/** 0-based, matching `EngravedNote.staffIndex`'s "0 = top staff of the
|
|
27
|
-
* system" contract: every `<part>` is walked in document order, and
|
|
28
|
-
* every DISTINCT staff within it (by `<attributes><staves>` when
|
|
29
|
-
* present, else the highest `<staff>` number any of its notes uses, else
|
|
30
|
-
* 1) is assigned the next index — so a single-part 2-staff piano score
|
|
31
|
-
* numbers 0/1 by `<staff>`, and a 2-part 1-staff-per-part reduction (this
|
|
32
|
-
* app's own chord+bass shape) numbers 0/1 by PART, with neither case
|
|
33
|
-
* needing different code. */
|
|
34
|
-
staffIndex: number;
|
|
35
|
-
/** Whole-note fraction (0.25 = quarter) — `duration / divisions / 4`,
|
|
36
|
-
* divisions tracked per-part from the LAST `<attributes><divisions>`
|
|
37
|
-
* seen at or before this note (MusicXML: divisions persist until
|
|
38
|
-
* overridden, default 1). 0 for a grace note (no `<duration>` child —
|
|
39
|
-
* the true, spec-correct signal; never guessed from `<type>`). */
|
|
40
|
-
durationReal: number;
|
|
41
|
-
/** 0-based position of this note's `<measure>` among its OWN `<part>`'s
|
|
42
|
-
* measure children, in document order. Informational only — the live
|
|
43
|
-
* join (`verovioOnsetColumns`/`verovioEngravedNotes`, both in
|
|
44
|
-
* notationPlayerVerovio.ts) resolves the note's RENDERED measure index
|
|
45
|
-
* from the live SVG DOM independently (Verovio's own render order, which
|
|
46
|
-
* is what geometry/hit-testing must agree with), never from this field. */
|
|
47
|
-
measureIndex: number;
|
|
48
|
-
/** `print-object="no"` on the source `<note>` (stave's `hideDoubledNotes`
|
|
49
|
-
* sets this on editorially-doubled notes/rests before handing MusicXML to
|
|
50
|
-
* this player). Verovio's importer HONORS this for `<note>` elements
|
|
51
|
-
* carrying a `<pitch>` (renders `visibility="hidden"` on its own,
|
|
52
|
-
* empirically confirmed against a real 6.2.0 render) but does NOT honor
|
|
53
|
-
* it for `<rest>` notes (the `<g class="rest">` renders fully visible
|
|
54
|
-
* regardless — same empirical check). `createVerovioNotationPlayer`
|
|
55
|
-
* reads this field to force `visibility="hidden"` after every render for
|
|
56
|
-
* ANY id where it's true — a no-op re-application on notes Verovio
|
|
57
|
-
* already hid, and the actual fix on the rests it doesn't (see that
|
|
58
|
-
* module's `applyPrintObjectHiding`). */
|
|
59
|
-
hidden: boolean;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
6
|
/** Restated independently, not imported as a VALUE from notationPlayerSvg.ts
|
|
63
7
|
* — same reasoning as `MAX_ENGRAVE_WIDTH_VRV` vs `MAX_ENGRAVE_WIDTH_SVG`
|
|
64
8
|
* (below): a runtime (non-`type`) import from notationPlayerSvg.ts would
|
|
@@ -397,6 +341,31 @@ interface VerovioLayoutOptions {
|
|
|
397
341
|
* glyphs get, can opt back into the pre-existing behavior.
|
|
398
342
|
*/
|
|
399
343
|
minFitFactor?: number;
|
|
344
|
+
/**
|
|
345
|
+
* BREAK PLAN — target bars per line. When set (`> 0`), the player plans
|
|
346
|
+
* system breaks itself (`sectionAwareBreaks`, balanced with a widow
|
|
347
|
+
* back-off) and encodes them into the document before the first engrave,
|
|
348
|
+
* forcing `breaks: 'line'`. Leave unset (or `0`) to let Verovio's own
|
|
349
|
+
* `breaks: 'auto'` decide, which is the default and is content-aware.
|
|
350
|
+
*
|
|
351
|
+
* Combine with `sectionStarts` to keep the balancing inside each section.
|
|
352
|
+
* A PLAYER option, not a Verovio one — stripped in `verovioRenderOptions`
|
|
353
|
+
* before `setOptions` sees it.
|
|
354
|
+
*/
|
|
355
|
+
barsPerLine?: number;
|
|
356
|
+
/**
|
|
357
|
+
* BREAK PLAN — 0-based measure POSITIONS at which a musical section
|
|
358
|
+
* begins. Each one > 0 becomes a hard system break, so a section always
|
|
359
|
+
* starts its own line; `barsPerLine`'s balancing then runs within each
|
|
360
|
+
* section's span rather than across the whole excerpt.
|
|
361
|
+
*
|
|
362
|
+
* POSITIONS, not MusicXML `<measure number>` attributes — scores skip bar
|
|
363
|
+
* numbers, so a caller holding bar-numbered section labels must convert
|
|
364
|
+
* first (`measureCount`'s doc explains the coordinate). Setting this alone
|
|
365
|
+
* (no `barsPerLine`) is the "break at sections, otherwise let the engraver
|
|
366
|
+
* decide" mode. A PLAYER option — stripped in `verovioRenderOptions`.
|
|
367
|
+
*/
|
|
368
|
+
sectionStarts?: readonly number[];
|
|
400
369
|
}
|
|
401
370
|
/**
|
|
402
371
|
* Layout defaults applied to EVERY engrave (initial + every reflow) unless a
|
|
@@ -511,6 +480,21 @@ declare const DEFAULT_MIN_FIT_FACTOR = 0.75;
|
|
|
511
480
|
* condition is unit-testable independent of a real engrave.
|
|
512
481
|
*/
|
|
513
482
|
declare function shouldFallbackToAutoBreaks(factor: number, breaks: VerovioLayoutOptions['breaks'] | undefined, minFitFactor: number): boolean;
|
|
483
|
+
/** True when fitting the current engraving would cross the caller's
|
|
484
|
+
* readability floor. Shared by the encoded-break fallback and auto's
|
|
485
|
+
* narrower re-plan path so the two policies cannot drift. */
|
|
486
|
+
declare function isBelowReadabilityFloor(factor: number, minFitFactor: number): boolean;
|
|
487
|
+
/** One narrower candidate for the auto-break readability floor, or null when
|
|
488
|
+
* the measured fit already holds the floor (or one bar is the terminal
|
|
489
|
+
* layout). Uses ceiling-halves so 4 -> 2 -> 1 and 3 -> 2 -> 1. */
|
|
490
|
+
declare function nextReadabilityFloorBarsPerLine(currentBarsPerLine: number, fittedZoomFactor: number, minFitFactor: number): number | null;
|
|
491
|
+
/**
|
|
492
|
+
* Pure planner seam for the width-aware auto floor. `fittedZoomAt` is the
|
|
493
|
+
* measured-layout oracle: production obtains each measurement by engraving
|
|
494
|
+
* that candidate, while jsdom-free tests can supply deterministic factors.
|
|
495
|
+
* The returned list is the sequence to try; its final `1` is terminal.
|
|
496
|
+
*/
|
|
497
|
+
declare function readabilityFloorPlan(initialBarsPerLine: number, widthPx: number, minFitFactor: number, fittedZoomAt: (barsPerLine: number, widthPx: number) => number): number[];
|
|
514
498
|
/**
|
|
515
499
|
* Semantic zoom → Verovio options. The migration spike's `zoom-test*.mjs`
|
|
516
500
|
* proved `pageWidth` (Verovio's line-breaking width, in ITS OWN units)
|
|
@@ -557,4 +541,4 @@ declare const MAX_ENGRAVE_WIDTH_VRV = 1400;
|
|
|
557
541
|
* (in stave-web-sightread) §2 for the full design. */
|
|
558
542
|
declare function createVerovioNotationPlayer(opts: CreateVerovioNotationPlayerOpts): VerovioNotationPlayer;
|
|
559
543
|
|
|
560
|
-
export { type CreateVerovioNotationPlayerOpts, DEFAULT_MIN_FIT_FACTOR, EngravedNote, MARKED_NOTE_CLASS, MAX_ENGRAVE_WIDTH_VRV, VEROVIO_BASE_SCALE, VEROVIO_LAYOUT_DEFAULTS, VEROVIO_MAX_SCALE, VEROVIO_MIN_SCALE, type VerovioLayoutOptions, type VerovioNotationPlayer, type VerovioOnset, type VerovioRenderOptions, applyPrintObjectHiding, createVerovioNotationPlayer, fitZoomFactor, shouldFallbackToAutoBreaks, systemMeasureCounts, verovioEngravedNotes, verovioNotationLayout, verovioNotesAtColumn, verovioOnsetColumns, verovioRenderOptions, verovioZoomOptions };
|
|
544
|
+
export { type CreateVerovioNotationPlayerOpts, DEFAULT_MIN_FIT_FACTOR, EngravedNote, MARKED_NOTE_CLASS, MAX_ENGRAVE_WIDTH_VRV, 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, systemMeasureCounts, verovioEngravedNotes, verovioNotationLayout, verovioNotesAtColumn, verovioOnsetColumns, verovioRenderOptions, verovioZoomOptions };
|
|
@@ -10,138 +10,14 @@ import {
|
|
|
10
10
|
vstackAudioPlayheadLine
|
|
11
11
|
} from "./chunk-DBFGX6OK.js";
|
|
12
12
|
import "./chunk-PVZXLC23.js";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const number = measure.getAttribute("number") ?? "0";
|
|
22
|
-
const notes = Array.from(measure.children).filter((el) => el.tagName === "note");
|
|
23
|
-
notes.forEach((note, noteIdx) => {
|
|
24
|
-
note.setAttribute("id", `n-${partIdx}-${number}-${noteIdx}`);
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
return new XMLSerializer().serializeToString(doc);
|
|
29
|
-
}
|
|
30
|
-
function injectSystemBreaks(xml, breakBeforeBarIndexes) {
|
|
31
|
-
const doc = new DOMParser().parseFromString(xml, "application/xml");
|
|
32
|
-
if (doc.querySelector("parsererror")) return xml;
|
|
33
|
-
const root = doc.documentElement;
|
|
34
|
-
if (!root || root.tagName !== "score-partwise") return xml;
|
|
35
|
-
for (const part of childrenNamed(root, "part")) {
|
|
36
|
-
const measures = childrenNamed(part, "measure");
|
|
37
|
-
for (const pos of breakBeforeBarIndexes) {
|
|
38
|
-
if (!Number.isInteger(pos) || pos <= 0 || pos >= measures.length) continue;
|
|
39
|
-
const measure = measures[pos];
|
|
40
|
-
const existingPrint = firstChildNamed(measure, "print");
|
|
41
|
-
if (existingPrint) {
|
|
42
|
-
existingPrint.setAttribute("new-system", "yes");
|
|
43
|
-
} else {
|
|
44
|
-
const printEl = doc.createElement("print");
|
|
45
|
-
printEl.setAttribute("new-system", "yes");
|
|
46
|
-
measure.insertBefore(printEl, measure.firstChild);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return new XMLSerializer().serializeToString(doc);
|
|
51
|
-
}
|
|
52
|
-
function balancedSystemBreaks(totalMeasures, systems) {
|
|
53
|
-
if (systems <= 1 || totalMeasures <= 0) return [];
|
|
54
|
-
const per = Math.ceil(totalMeasures / systems);
|
|
55
|
-
if (per <= 0) return [];
|
|
56
|
-
const breaks = [];
|
|
57
|
-
for (let b = per; b < totalMeasures; b += per) breaks.push(b);
|
|
58
|
-
return breaks;
|
|
59
|
-
}
|
|
60
|
-
var STEP_SEMITONE = { C: 0, D: 2, E: 4, F: 5, G: 7, A: 9, B: 11 };
|
|
61
|
-
function firstChildNamed(el, tag) {
|
|
62
|
-
for (const c of Array.from(el.children)) if (c.tagName === tag) return c;
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
function childrenNamed(el, tag) {
|
|
66
|
-
return Array.from(el.children).filter((c) => c.tagName === tag);
|
|
67
|
-
}
|
|
68
|
-
function textOf(el) {
|
|
69
|
-
return el && el.textContent != null ? el.textContent.trim() : null;
|
|
70
|
-
}
|
|
71
|
-
function pitchMidiFromElement(pitchEl) {
|
|
72
|
-
const step = textOf(firstChildNamed(pitchEl, "step")) ?? "C";
|
|
73
|
-
const octave = Number(textOf(firstChildNamed(pitchEl, "octave")) ?? "4");
|
|
74
|
-
const alter = Number(textOf(firstChildNamed(pitchEl, "alter")) ?? "0");
|
|
75
|
-
return 12 * (octave + 1) + (STEP_SEMITONE[step] ?? 0) + Math.trunc(alter);
|
|
76
|
-
}
|
|
77
|
-
function partStaffCount(part) {
|
|
78
|
-
let maxDeclared = 0;
|
|
79
|
-
for (const attrs of childrenNamed(part, "measure").flatMap((m) => childrenNamed(m, "attributes"))) {
|
|
80
|
-
const staves = Number(textOf(firstChildNamed(attrs, "staves")) ?? "0");
|
|
81
|
-
if (staves > maxDeclared) maxDeclared = staves;
|
|
82
|
-
}
|
|
83
|
-
if (maxDeclared > 0) return maxDeclared;
|
|
84
|
-
let maxStaff = 0;
|
|
85
|
-
for (const measure of childrenNamed(part, "measure")) {
|
|
86
|
-
for (const note of childrenNamed(measure, "note")) {
|
|
87
|
-
const staff = Number(textOf(firstChildNamed(note, "staff")) ?? "0");
|
|
88
|
-
if (staff > maxStaff) maxStaff = staff;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return Math.max(1, maxStaff);
|
|
92
|
-
}
|
|
93
|
-
function noteModelFromXml(stampedXml) {
|
|
94
|
-
const model = /* @__PURE__ */ new Map();
|
|
95
|
-
let doc;
|
|
96
|
-
try {
|
|
97
|
-
doc = new DOMParser().parseFromString(stampedXml, "application/xml");
|
|
98
|
-
} catch {
|
|
99
|
-
return model;
|
|
100
|
-
}
|
|
101
|
-
if (doc.querySelector("parsererror")) return model;
|
|
102
|
-
const root = doc.documentElement;
|
|
103
|
-
if (!root || root.tagName !== "score-partwise") return model;
|
|
104
|
-
const parts = childrenNamed(root, "part");
|
|
105
|
-
let staffOffset = 0;
|
|
106
|
-
for (const part of parts) {
|
|
107
|
-
const staffCount = partStaffCount(part);
|
|
108
|
-
let divisions = 1;
|
|
109
|
-
const measureEls = childrenNamed(part, "measure");
|
|
110
|
-
measureEls.forEach((measureEl, measureIndex) => {
|
|
111
|
-
for (const child of Array.from(measureEl.children)) {
|
|
112
|
-
if (child.tagName === "attributes") {
|
|
113
|
-
const divText = textOf(firstChildNamed(child, "divisions"));
|
|
114
|
-
if (divText) divisions = Number(divText) || divisions;
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
if (child.tagName !== "note") continue;
|
|
118
|
-
const note = child;
|
|
119
|
-
const id = note.getAttribute("id");
|
|
120
|
-
if (!id) continue;
|
|
121
|
-
const isGrace = !!firstChildNamed(note, "grace");
|
|
122
|
-
const isRest = !!firstChildNamed(note, "rest");
|
|
123
|
-
const pitchEl = firstChildNamed(note, "pitch");
|
|
124
|
-
const staffNumber = Number(textOf(firstChildNamed(note, "staff")) ?? "1") || 1;
|
|
125
|
-
const durationText = textOf(firstChildNamed(note, "duration"));
|
|
126
|
-
const durationReal = isGrace || !durationText ? 0 : Number(durationText) / divisions / 4;
|
|
127
|
-
const tieStopDirect = childrenNamed(note, "tie").some((t) => t.getAttribute("type") === "stop");
|
|
128
|
-
const notationsEl = firstChildNamed(note, "notations");
|
|
129
|
-
const tieStopNotated = notationsEl ? childrenNamed(notationsEl, "tied").some((t) => t.getAttribute("type") === "stop") : false;
|
|
130
|
-
model.set(id, {
|
|
131
|
-
midi: pitchEl ? pitchMidiFromElement(pitchEl) : null,
|
|
132
|
-
isRest,
|
|
133
|
-
tieContinuation: tieStopDirect || tieStopNotated,
|
|
134
|
-
staffIndex: staffOffset + Math.max(0, staffNumber - 1),
|
|
135
|
-
durationReal,
|
|
136
|
-
measureIndex,
|
|
137
|
-
hidden: note.getAttribute("print-object") === "no"
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
staffOffset += staffCount;
|
|
142
|
-
}
|
|
143
|
-
return model;
|
|
144
|
-
}
|
|
13
|
+
import {
|
|
14
|
+
balancedSystemBreaks,
|
|
15
|
+
injectSystemBreaks,
|
|
16
|
+
measureCount,
|
|
17
|
+
noteModelFromXml,
|
|
18
|
+
sectionAwareBreaks,
|
|
19
|
+
stampNoteIds
|
|
20
|
+
} from "./chunk-JCDNEJBT.js";
|
|
145
21
|
|
|
146
22
|
// src/notationPlayerVerovio.ts
|
|
147
23
|
var MARKED_NOTE_CLASS = "rmp-note-marked";
|
|
@@ -396,7 +272,13 @@ var VEROVIO_LAYOUT_DEFAULTS = {
|
|
|
396
272
|
};
|
|
397
273
|
function verovioRenderOptions(hostWidthPx, zoom, layout) {
|
|
398
274
|
const { scale, pageWidth } = verovioZoomOptions(hostWidthPx, zoom);
|
|
399
|
-
const {
|
|
275
|
+
const {
|
|
276
|
+
avoidWidows: _avoidWidows,
|
|
277
|
+
minFitFactor: _minFitFactor,
|
|
278
|
+
barsPerLine: _barsPerLine,
|
|
279
|
+
sectionStarts: _sectionStarts,
|
|
280
|
+
...verovioLayout
|
|
281
|
+
} = layout ?? {};
|
|
400
282
|
return {
|
|
401
283
|
...VEROVIO_LAYOUT_DEFAULTS,
|
|
402
284
|
...verovioLayout,
|
|
@@ -419,10 +301,31 @@ function fitZoomFactor(systemWidthsPx, engraveWidthPx) {
|
|
|
419
301
|
}
|
|
420
302
|
var DEFAULT_MIN_FIT_FACTOR = 0.75;
|
|
421
303
|
function shouldFallbackToAutoBreaks(factor, breaks, minFitFactor) {
|
|
422
|
-
if (!
|
|
423
|
-
if (!(minFitFactor > 0)) return false;
|
|
304
|
+
if (!isBelowReadabilityFloor(factor, minFitFactor)) return false;
|
|
424
305
|
if (breaks !== "line" && breaks !== "encoded") return false;
|
|
425
|
-
return
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
function isBelowReadabilityFloor(factor, minFitFactor) {
|
|
309
|
+
return Number.isFinite(factor) && Number.isFinite(minFitFactor) && minFitFactor > 0 && factor < minFitFactor;
|
|
310
|
+
}
|
|
311
|
+
function nextReadabilityFloorBarsPerLine(currentBarsPerLine, fittedZoomFactor, minFitFactor) {
|
|
312
|
+
if (!isBelowReadabilityFloor(fittedZoomFactor, minFitFactor)) return null;
|
|
313
|
+
if (!Number.isFinite(currentBarsPerLine) || currentBarsPerLine <= 1) return null;
|
|
314
|
+
return Math.max(1, Math.ceil(currentBarsPerLine / 2));
|
|
315
|
+
}
|
|
316
|
+
function readabilityFloorPlan(initialBarsPerLine, widthPx, minFitFactor, fittedZoomAt) {
|
|
317
|
+
if (!Number.isFinite(initialBarsPerLine) || initialBarsPerLine <= 0 || !(widthPx > 0)) return [];
|
|
318
|
+
let bars = Math.max(1, Math.floor(initialBarsPerLine));
|
|
319
|
+
let factor = fittedZoomAt(bars, widthPx);
|
|
320
|
+
const plan = [];
|
|
321
|
+
for (; ; ) {
|
|
322
|
+
const next = nextReadabilityFloorBarsPerLine(bars, factor, minFitFactor);
|
|
323
|
+
if (next === null) return plan;
|
|
324
|
+
plan.push(next);
|
|
325
|
+
bars = next;
|
|
326
|
+
factor = fittedZoomAt(bars, widthPx);
|
|
327
|
+
if (bars === 1) return plan;
|
|
328
|
+
}
|
|
426
329
|
}
|
|
427
330
|
function verovioZoomOptions(hostWidthPx, zoom) {
|
|
428
331
|
const z = Number.isFinite(zoom) && zoom > 0 ? zoom : 1;
|
|
@@ -470,6 +373,19 @@ function createVerovioNotationPlayer(opts) {
|
|
|
470
373
|
}
|
|
471
374
|
const stampedXml = opts.rendered ? musicXml : stampNoteIds(musicXml);
|
|
472
375
|
const noteModel = opts.rendered ? /* @__PURE__ */ new Map() : noteModelFromXml(stampedXml);
|
|
376
|
+
const measureTotal = measureCount(stampedXml);
|
|
377
|
+
const breakPlan = (() => {
|
|
378
|
+
const layout = opts.verovioOptions;
|
|
379
|
+
const starts = layout?.sectionStarts ?? [];
|
|
380
|
+
const every = layout?.barsPerLine ?? 0;
|
|
381
|
+
if (!starts.length && !(every > 0)) return [];
|
|
382
|
+
return sectionAwareBreaks(measureTotal, starts, every);
|
|
383
|
+
})();
|
|
384
|
+
const plannedXml = breakPlan.length ? injectSystemBreaks(stampedXml, breakPlan) : stampedXml;
|
|
385
|
+
function autoFloorPlanXml(barsPerLine) {
|
|
386
|
+
const positions = barsPerLine <= 1 ? Array.from({ length: Math.max(0, measureTotal - 1) }, (_, i) => i + 1) : sectionAwareBreaks(measureTotal, [], barsPerLine);
|
|
387
|
+
return positions.length ? injectSystemBreaks(stampedXml, positions) : stampedXml;
|
|
388
|
+
}
|
|
473
389
|
const root = document.createElement("div");
|
|
474
390
|
root.style.position = "relative";
|
|
475
391
|
root.style.width = "100%";
|
|
@@ -581,23 +497,47 @@ function createVerovioNotationPlayer(opts) {
|
|
|
581
497
|
}
|
|
582
498
|
function engraveOnce(toolkit, widthPx, zoom) {
|
|
583
499
|
const layoutOpts = opts.verovioOptions;
|
|
584
|
-
const
|
|
500
|
+
const planLayoutOpts = breakPlan.length ? { ...layoutOpts, breaks: "line" } : layoutOpts;
|
|
501
|
+
const effectiveBreaks = planLayoutOpts?.breaks ?? VEROVIO_LAYOUT_DEFAULTS.breaks;
|
|
585
502
|
const minFitFactor = layoutOpts?.minFitFactor ?? DEFAULT_MIN_FIT_FACTOR;
|
|
586
503
|
const avoidWidows = layoutOpts?.avoidWidows !== false;
|
|
587
|
-
let xmlToRender =
|
|
588
|
-
let renderLayoutOpts =
|
|
504
|
+
let xmlToRender = plannedXml;
|
|
505
|
+
let renderLayoutOpts = planLayoutOpts;
|
|
589
506
|
toolkit.setOptions(verovioRenderOptions(widthPx, zoom, renderLayoutOpts));
|
|
590
507
|
const ok = !!toolkit.loadData(xmlToRender);
|
|
591
508
|
if (ok) renderAllPages(toolkit);
|
|
592
509
|
rebuildLayoutFromDom();
|
|
593
|
-
if (ok && avoidWidows && effectiveBreaks === "auto") {
|
|
594
|
-
const widowed = runWidowPass(toolkit, widthPx, zoom, xmlToRender, renderLayoutOpts);
|
|
595
|
-
xmlToRender = widowed.xml;
|
|
596
|
-
renderLayoutOpts = widowed.layoutOpts;
|
|
597
|
-
}
|
|
598
510
|
if (ok && currentLayout) {
|
|
599
511
|
const systemWidthsPx = () => opts.measureSystemWidths ? opts.measureSystemWidths(svgHost) : currentLayout.systems.map((s) => s.w);
|
|
600
512
|
let factor = fitZoomFactor(systemWidthsPx(), widthPx);
|
|
513
|
+
let terminalAutoFloorPlan = false;
|
|
514
|
+
if (effectiveBreaks === "auto") {
|
|
515
|
+
if (!isBelowReadabilityFloor(factor, minFitFactor) && avoidWidows) {
|
|
516
|
+
const widowed = runWidowPass(toolkit, widthPx, zoom, xmlToRender, renderLayoutOpts);
|
|
517
|
+
xmlToRender = widowed.xml;
|
|
518
|
+
renderLayoutOpts = widowed.layoutOpts;
|
|
519
|
+
factor = fitZoomFactor(systemWidthsPx(), widthPx);
|
|
520
|
+
}
|
|
521
|
+
let barsPerLine = Math.max(1, ...systemMeasureCounts(svgHost));
|
|
522
|
+
for (; ; ) {
|
|
523
|
+
const nextBarsPerLine = nextReadabilityFloorBarsPerLine(barsPerLine, factor, minFitFactor);
|
|
524
|
+
if (nextBarsPerLine === null) break;
|
|
525
|
+
const floorXml = autoFloorPlanXml(nextBarsPerLine);
|
|
526
|
+
const floorLayoutOpts = { ...layoutOpts, breaks: "line" };
|
|
527
|
+
toolkit.setOptions(verovioRenderOptions(widthPx, zoom, floorLayoutOpts));
|
|
528
|
+
if (!toolkit.loadData(floorXml)) break;
|
|
529
|
+
renderAllPages(toolkit);
|
|
530
|
+
rebuildLayoutFromDom();
|
|
531
|
+
xmlToRender = floorXml;
|
|
532
|
+
renderLayoutOpts = floorLayoutOpts;
|
|
533
|
+
factor = fitZoomFactor(systemWidthsPx(), widthPx);
|
|
534
|
+
barsPerLine = nextBarsPerLine;
|
|
535
|
+
if (barsPerLine === 1 && isBelowReadabilityFloor(factor, minFitFactor)) {
|
|
536
|
+
terminalAutoFloorPlan = true;
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
601
541
|
if (shouldFallbackToAutoBreaks(factor, effectiveBreaks, minFitFactor)) {
|
|
602
542
|
const autoLayoutOpts = { ...layoutOpts, breaks: "auto" };
|
|
603
543
|
toolkit.setOptions(verovioRenderOptions(widthPx, zoom, autoLayoutOpts));
|
|
@@ -615,7 +555,7 @@ function createVerovioNotationPlayer(opts) {
|
|
|
615
555
|
factor = fitZoomFactor(systemWidthsPx(), widthPx);
|
|
616
556
|
}
|
|
617
557
|
}
|
|
618
|
-
if (factor < 1) {
|
|
558
|
+
if (factor < 1 && !terminalAutoFloorPlan) {
|
|
619
559
|
const effectiveZoom = zoom * factor;
|
|
620
560
|
toolkit.setOptions(verovioRenderOptions(widthPx, effectiveZoom, renderLayoutOpts));
|
|
621
561
|
const ok2 = !!toolkit.loadData(xmlToRender);
|
|
@@ -746,6 +686,9 @@ export {
|
|
|
746
686
|
applyPrintObjectHiding,
|
|
747
687
|
createVerovioNotationPlayer,
|
|
748
688
|
fitZoomFactor,
|
|
689
|
+
isBelowReadabilityFloor,
|
|
690
|
+
nextReadabilityFloorBarsPerLine,
|
|
691
|
+
readabilityFloorPlan,
|
|
749
692
|
shouldFallbackToAutoBreaks,
|
|
750
693
|
systemMeasureCounts,
|
|
751
694
|
verovioEngravedNotes,
|