@viritura/score-engine 0.0.0 → 0.1.0-next.1
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/CHANGELOG.md +50 -0
- package/README.md +125 -1
- package/dist/LICENSE +21 -0
- package/dist/LICENSES/OFL-1.1.txt +80 -0
- package/dist/THIRD_PARTY_NOTICES.md +95 -0
- package/dist/fonts/Bravura.otf +0 -0
- package/dist/fonts/LibertinusSerif-Bold.otf +0 -0
- package/dist/fonts/LibertinusSerif-BoldItalic.otf +0 -0
- package/dist/fonts/LibertinusSerif-Italic.otf +0 -0
- package/dist/fonts/LibertinusSerif-Regular.otf +0 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5740 -0
- package/dist/index.js.map +7 -0
- package/dist/score-engine.worker.js +136 -0
- package/dist/score-engine.worker.js.map +7 -0
- package/dist/types/engine.d.ts +56 -0
- package/dist/types/errors.d.ts +36 -0
- package/dist/types/index.d.ts +23 -0
- package/dist/types/timeline.d.ts +67 -0
- package/dist/types/types.d.ts +255 -0
- package/dist/wasm/viritura_wasm.js +975 -0
- package/dist/wasm/viritura_wasm_bg.wasm +0 -0
- package/package.json +56 -10
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
2
|
+
var __esm = (fn, res, err) => function() {
|
|
3
|
+
if (err) throw err[0];
|
|
4
|
+
try {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
} catch (e) {
|
|
7
|
+
throw err = [e], e;
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// <define:import.meta.env>
|
|
12
|
+
var define_import_meta_env_default, init_define_import_meta_env = __esm({
|
|
13
|
+
"<define:import.meta.env>"() {
|
|
14
|
+
define_import_meta_env_default = {};
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// packages/score-engine/src/layoutWorker.ts
|
|
19
|
+
init_define_import_meta_env();
|
|
20
|
+
|
|
21
|
+
// packages/renderer/src/index.ts
|
|
22
|
+
init_define_import_meta_env();
|
|
23
|
+
|
|
24
|
+
// packages/renderer/src/basePath.ts
|
|
25
|
+
init_define_import_meta_env();
|
|
26
|
+
var assetBasePathOverride = null;
|
|
27
|
+
function setAssetBasePath(basePath) {
|
|
28
|
+
if (!basePath) {
|
|
29
|
+
assetBasePathOverride = null;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
assetBasePathOverride = basePath.endsWith("/") ? basePath : `${basePath}/`;
|
|
33
|
+
}
|
|
34
|
+
function resolveBasePath() {
|
|
35
|
+
if (assetBasePathOverride)
|
|
36
|
+
return assetBasePathOverride;
|
|
37
|
+
if (typeof document < "u") {
|
|
38
|
+
let baseEl = document.querySelector("base");
|
|
39
|
+
if (baseEl) {
|
|
40
|
+
let href = baseEl.getAttribute("href");
|
|
41
|
+
if (href)
|
|
42
|
+
try {
|
|
43
|
+
let base = new URL(href, window.location.origin).pathname;
|
|
44
|
+
return base.endsWith("/") ? base : base + "/";
|
|
45
|
+
} catch {
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
let viteBase = define_import_meta_env_default?.BASE_URL;
|
|
51
|
+
if (viteBase)
|
|
52
|
+
return viteBase.endsWith("/") ? viteBase : viteBase + "/";
|
|
53
|
+
} catch {
|
|
54
|
+
}
|
|
55
|
+
return "/";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// packages/renderer/src/wasm.ts
|
|
59
|
+
init_define_import_meta_env();
|
|
60
|
+
var wasmModule = null, initPromise = null, lastInitError = null;
|
|
61
|
+
function getWasmInitError() {
|
|
62
|
+
return lastInitError;
|
|
63
|
+
}
|
|
64
|
+
async function initWasm() {
|
|
65
|
+
if (wasmModule) return;
|
|
66
|
+
if (initPromise) return initPromise;
|
|
67
|
+
let attempt = (async () => {
|
|
68
|
+
try {
|
|
69
|
+
let basePath = resolveBasePath(), assetHash = "", wasmResponse = await fetch(`${basePath}wasm/viritura_wasm_bg${assetHash}.wasm`);
|
|
70
|
+
if (!wasmResponse.ok)
|
|
71
|
+
throw new Error(`Failed to fetch WASM binary: ${wasmResponse.status}`);
|
|
72
|
+
let wasmBytes = await wasmResponse.arrayBuffer(), glue = await import(
|
|
73
|
+
/* @vite-ignore */
|
|
74
|
+
/* webpackIgnore: true */
|
|
75
|
+
`${basePath}wasm/viritura_wasm${assetHash}.js`
|
|
76
|
+
), wasmModule_ = new WebAssembly.Module(wasmBytes);
|
|
77
|
+
glue.initSync({ module: wasmModule_ }), wasmModule = glue, lastInitError = null, _flushPendingDebugFlag(), console.log(`Viritura WASM engine v${glue.engine_version()} loaded`);
|
|
78
|
+
} catch (e) {
|
|
79
|
+
lastInitError = e, console.warn("WASM engine failed to load:", e), e instanceof Error && console.warn("Error details:", e.message), wasmModule = null;
|
|
80
|
+
}
|
|
81
|
+
})();
|
|
82
|
+
initPromise = attempt;
|
|
83
|
+
try {
|
|
84
|
+
await attempt;
|
|
85
|
+
} finally {
|
|
86
|
+
!wasmModule && initPromise === attempt && (initPromise = null);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function isWasmReady() {
|
|
90
|
+
return wasmModule !== null;
|
|
91
|
+
}
|
|
92
|
+
function computeMnxScoreLayoutBinary(mnxJson, spatium, pageWidth = 0, scoreIndex = 0, pageSetupJson) {
|
|
93
|
+
if (!wasmModule)
|
|
94
|
+
throw new Error("WASM engine not initialized. Call initWasm() first.");
|
|
95
|
+
return wasmModule.compute_mnx_score_layout_binary(mnxJson, spatium, pageWidth, scoreIndex, pageSetupJson);
|
|
96
|
+
}
|
|
97
|
+
var pendingDebugFlag = null;
|
|
98
|
+
function _flushPendingDebugFlag() {
|
|
99
|
+
pendingDebugFlag !== null && wasmModule?.set_emit_layout_debug && (wasmModule.set_emit_layout_debug(pendingDebugFlag), pendingDebugFlag = null);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// packages/score-engine/src/layoutRequest.ts
|
|
103
|
+
init_define_import_meta_env();
|
|
104
|
+
function messageOf(err) {
|
|
105
|
+
return err instanceof Error ? err.message : String(err);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// packages/score-engine/src/layoutWorker.ts
|
|
109
|
+
var ready = null;
|
|
110
|
+
async function ensureEngine(assetBaseUrl) {
|
|
111
|
+
if (setAssetBasePath(assetBaseUrl), await initWasm(), !isWasmReady()) throw new Error(messageOf(getWasmInitError() ?? "WASM engine failed to load"));
|
|
112
|
+
}
|
|
113
|
+
function reply(message, transfer = []) {
|
|
114
|
+
self.postMessage(message, transfer);
|
|
115
|
+
}
|
|
116
|
+
self.onmessage = async (event) => {
|
|
117
|
+
let req = event.data;
|
|
118
|
+
if (req.type === "init") {
|
|
119
|
+
ready = ensureEngine(req.assetBaseUrl), ready.then(
|
|
120
|
+
() => reply({ id: req.id, ok: !0 }),
|
|
121
|
+
(err) => {
|
|
122
|
+
ready = null, reply({ id: req.id, ok: !1, kind: "load", message: messageOf(err) });
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
if (!ready) throw new Error("Layout worker used before init");
|
|
129
|
+
await ready;
|
|
130
|
+
let { json, spatium, pageWidth, scoreIndex, pageSetupJson } = req.args, packed = computeMnxScoreLayoutBinary(json, spatium, pageWidth, scoreIndex, pageSetupJson), data = new Float32Array(packed);
|
|
131
|
+
reply({ id: req.id, ok: !0, data }, [data.buffer]);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
reply({ id: req.id, ok: !1, kind: "layout", message: messageOf(err) });
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
//# sourceMappingURL=score-engine.worker.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["<define:import.meta.env>", "../src/layoutWorker.ts", "../../renderer/src/index.ts", "../../renderer/src/basePath.ts", "../../renderer/src/wasm.ts", "../src/layoutRequest.ts"],
|
|
4
|
+
"sourcesContent": ["{}", "/// <reference lib=\"webworker\" />\n/**\n * Layout worker: runs the WASM layout engine off the main thread and\n * transfers the packed binary display list back zero-copy.\n */\n\nimport {\n getWasmInitError,\n initWasm,\n isWasmReady,\n setAssetBasePath,\n wasmComputeMnxScoreLayoutBinary,\n} from \"@viritura/renderer\";\nimport { messageOf } from \"./layoutRequest\";\nimport type { LayoutWorkerRequest, LayoutWorkerResponse } from \"./layoutWorkerProtocol\";\n\ndeclare const self: DedicatedWorkerGlobalScope;\n\nlet ready: Promise<void> | null = null;\n\nasync function ensureEngine(assetBaseUrl: string): Promise<void> {\n setAssetBasePath(assetBaseUrl);\n await initWasm();\n if (!isWasmReady()) throw new Error(messageOf(getWasmInitError() ?? \"WASM engine failed to load\"));\n}\n\nfunction reply(message: LayoutWorkerResponse, transfer: Transferable[] = []): void {\n self.postMessage(message, transfer);\n}\n\nself.onmessage = async (event: MessageEvent<LayoutWorkerRequest>) => {\n const req = event.data;\n if (req.type === \"init\") {\n ready = ensureEngine(req.assetBaseUrl);\n ready.then(\n () => reply({ id: req.id, ok: true }),\n (err: unknown) => {\n ready = null;\n reply({ id: req.id, ok: false, kind: \"load\", message: messageOf(err) });\n },\n );\n return;\n }\n try {\n if (!ready) throw new Error(\"Layout worker used before init\");\n await ready;\n const { json, spatium, pageWidth, scoreIndex, pageSetupJson } = req.args;\n const packed = wasmComputeMnxScoreLayoutBinary(json, spatium, pageWidth, scoreIndex, pageSetupJson);\n // Copy out of WASM memory into a standalone, transferable buffer.\n const data = new Float32Array(packed);\n reply({ id: req.id, ok: true, data }, [data.buffer]);\n } catch (err) {\n reply({ id: req.id, ok: false, kind: \"layout\", message: messageOf(err) });\n }\n};\n", "/**\n * @viritura/renderer \u2014 Canvas 2D score renderer.\n */\n\nexport { ScoreRenderer } from \"./ScoreRenderer\";\nexport { renderScore } from \"./renderScore\";\nexport { paintDisplayList, paintCommand, loadMusicFont } from \"./displayListPainter\";\nexport type { FontLoadResult, LoadMusicFontOptions } from \"./displayListPainter\";\nexport { canvasFontFamily, canvasTextFont, TEXT_FONT_FAMILY } from \"./textFont\";\nexport {\n decodeBinaryDisplayList,\n paintBinaryDisplayList,\n getBinaryDisplayListDimensions,\n getBinaryDisplayListPages,\n getBinaryDisplayListBboxes,\n} from \"./binaryDisplayList\";\nexport { decodeFrame, decodePatchFrame, PatchReconstructor } from \"./patchFrame\";\nexport type { DecodedFrame, PatchFrame, Placement } from \"./patchFrame\";\nexport { GlyphAtlas, COMMON_GLYPHS } from \"./glyphAtlas\";\nexport type { GlyphAtlasConfig } from \"./glyphAtlas\";\nexport { PageCache, splitCommandsByPage } from \"./pageCache\";\nexport {\n SpatialIndex,\n paintSelectionOverlay,\n paintMeasureSelectionOverlay,\n paintHitboxDebug,\n paintHitboxDebugLegend,\n getElementType,\n hitTestSpannerHandle,\n paintSpannerDragPreview,\n} from \"./hitTest\";\nexport { getHitboxDebugKind, HITBOX_DEBUG_STYLES } from \"./hitboxDebug\";\nexport type { HitboxDebugBounds, HitboxDebugKind, HitboxDebugOptions } from \"./hitboxDebug\";\nexport { hitTestBeamInk, isBeamElementId, selectionGroupMembers } from \"./beamHitTest\";\nexport type { ElementBBox, ScoreElementType, SpannerHandleHit, SpannerHandleEnd, DragSnapPoint } from \"./hitTest\";\nexport { paintBeatRuler } from \"./beatRuler\";\nexport type { RulerTick, RulerConfig } from \"./beatRuler\";\nexport {\n initWasm,\n isWasmReady,\n getWasmInitError,\n getEngineVersion,\n computeLayout as wasmComputeLayout,\n computeFullScoreLayout as wasmComputeFullScoreLayout,\n computeLayoutBinary as wasmComputeLayoutBinary,\n computeFullScoreLayoutBinary as wasmComputeFullScoreLayoutBinary,\n computeMnxScoreLayout as wasmComputeMnxScoreLayout,\n computeMnxScoreLayoutBinary as wasmComputeMnxScoreLayoutBinary,\n computeSlurPreview as wasmComputeSlurPreview,\n computeNotePreview as wasmComputeNotePreview,\n getScoreInfo,\n wasmExportSvg,\n createCachedLayoutEngine,\n setEmitLayoutDebug,\n getEmitLayoutDebug,\n EMPTY_LAYOUT_METRICS,\n setWasmTiming,\n} from \"./wasm\";\nexport { createLayoutService } from \"./worker\";\nexport type { LayoutService, AsyncCachedLayoutEngine, LayoutWorkerApi } from \"./worker\";\nexport type {\n DisplayList,\n PartSummary,\n RenderCommand,\n ScoreInfo,\n PageLayout,\n BoundingBox,\n MeasureBounds,\n SelectionGroup,\n SlurGeometry,\n SlurPreview,\n SlurPreviewInput,\n NotePreviewInput,\n WasmSvgPage,\n CachedLayoutEngine,\n LayoutDebugInfo,\n PageTurnWarning,\n SystemDebug,\n AboveBreakdown,\n BelowBreakdown,\n MeasureExtreme,\n MeasureSpacing,\n StaffPairDebug,\n GapInfo,\n PlacementDebug,\n LayoutMetrics,\n} from \"./wasm\";\nexport {\n detectStaves,\n detectHorizonStaves,\n detectStavesForViewMode,\n findStaffAtPosition,\n snapToStaffPosition,\n getStaffPosition,\n paintInputCursor,\n paintGhostNote,\n paintGhostClef,\n extractStickyClefInfo,\n paintStickyClefs,\n paintMeasureNumber,\n} from \"./overlayPainter\";\nexport type { StaffInfo, GhostNoteOptions, GhostClefOptions, StickyClefInfo } from \"./overlayPainter\";\nexport {\n PerfTracker,\n isPerfEnabled,\n enablePerfOverlay,\n disablePerfOverlay,\n isTileCacheDisabled,\n disableTileCache,\n enableTileCache,\n getGlobalPerfTracker,\n} from \"./perfOverlay\";\nexport type { PatchInfo } from \"./perfOverlay\";\nexport { TileCache, DEFAULT_TILE_SIZE, computeDisplayListContentBounds, paintCommandsCulled } from \"./tileCache\";\nexport {\n computeHorizonPaperGeometry,\n paintPaperPage,\n HORIZON_PAPER_PADDING,\n PAPER_SHADOW_MARGIN,\n} from \"./paperPagePainter\";\nexport type { HorizonPaperGeometry } from \"./paperPagePainter\";\nexport { beatToX, paintPlayhead, paintPlayheadAtPosition, findSystemYExtent, findPageForY } from \"./playheadPainter\";\nexport type { PlayheadPosition, PlayheadDraw } from \"./playheadPainter\";\nexport { exportPdf } from \"./pdfPainter\";\nexport type { PdfExportOptions } from \"./pdfPainter\";\nexport { exportSvg, exportSvgPages } from \"./svgPainter\";\nexport type { SvgExportOptions, SvgPage } from \"./svgPainter\";\nexport { resolveBasePath, setAssetBasePath } from \"./basePath\";\n", "let assetBasePathOverride: string | null = null;\n\n/**\n * Override the static asset base used by renderer consumers that cannot rely\n * on a normal web root, such as VS Code webviews.\n */\nexport function setAssetBasePath(basePath: string | null): void {\n if (!basePath) {\n assetBasePathOverride = null;\n return;\n }\n\n assetBasePathOverride = basePath.endsWith(\"/\") ? basePath : `${basePath}/`;\n}\n\n/**\n * Resolve the base URL path for fetching static assets (WASM, fonts, scores).\n *\n * Priority:\n * 1. Explicit override set by embedding hosts such as VS Code webviews\n * 2. HTML <base> tag (set by Storybook / Vite when using a subpath)\n * 3. Vite's import.meta.env.BASE_URL (set at build time)\n * 4. Fallback to \"/\"\n *\n * Always returns a path ending with \"/\".\n */\nexport function resolveBasePath(): string {\n if (assetBasePathOverride) {\n return assetBasePathOverride;\n }\n\n // 1. Check for <base href=\"...\"> tag\n if (typeof document !== \"undefined\") {\n const baseEl = document.querySelector(\"base\");\n if (baseEl) {\n const href = baseEl.getAttribute(\"href\");\n if (href) {\n try {\n const base = new URL(href, window.location.origin).pathname;\n return base.endsWith(\"/\") ? base : base + \"/\";\n } catch {\n // ignore malformed base href\n }\n }\n }\n }\n\n // 2. Vite injects BASE_URL at build time\n try {\n const viteBase = (import.meta as { env?: { BASE_URL?: string } }).env?.BASE_URL;\n if (viteBase) {\n return viteBase.endsWith(\"/\") ? viteBase : viteBase + \"/\";\n }\n } catch {\n // import.meta.env may not exist outside Vite\n }\n\n // 3. Default\n return \"/\";\n}\n", "/**\n * WASM Engine wrapper \u2014 loads and initializes the Viritura WASM module.\n *\n * This wraps the wasm-bindgen generated JS glue code and provides\n * a clean TypeScript API for the editor to use.\n */\n\nimport { decodeBinaryDisplayList } from \"./binaryDisplayList\";\n\n// Type re-exports \u2014 definitions live in wasmTypes.ts.\nexport * from \"./wasmTypes\";\nimport type {\n BoundingBox,\n DisplayList,\n NotePreviewInput,\n RenderCommand,\n ScoreInfo,\n SlurPreview,\n SlurPreviewInput,\n} from \"./wasmTypes\";\n\ndeclare const __VIRITURA_WASM_ASSET_HASH__: string | undefined;\n\n// Module-level state\nlet wasmModule: {\n compute_layout: (\n mnx_json: string,\n part_index: number,\n spatium: number,\n page_width: number,\n page_setup_json?: string,\n ) => string;\n compute_layout_binary: (\n mnx_json: string,\n part_index: number,\n spatium: number,\n page_width: number,\n page_setup_json?: string,\n ) => Float32Array;\n compute_full_score_layout: (\n mnx_json: string,\n spatium: number,\n page_width: number,\n page_setup_json?: string,\n ) => string;\n compute_full_score_layout_binary: (\n mnx_json: string,\n spatium: number,\n page_width: number,\n page_setup_json?: string,\n ) => Float32Array;\n compute_mnx_score_layout: (\n mnx_json: string,\n spatium: number,\n page_width: number,\n score_index: number,\n page_setup_json?: string,\n ) => string;\n compute_mnx_score_layout_binary: (\n mnx_json: string,\n spatium: number,\n page_width: number,\n score_index: number,\n page_setup_json?: string,\n ) => Float32Array;\n get_score_info: (mnx_json: string) => string;\n engine_version: () => string;\n compute_slur_preview: (preview_json: string) => string;\n compute_note_preview?: (preview_json: string) => string;\n export_svg: (\n display_list_json: string,\n bravura_data: Uint8Array,\n text_font_data: Uint8Array,\n spatium_mm: number,\n sp_pixels: number,\n page_width_mm: number,\n page_height_mm: number,\n ) => string;\n set_emit_layout_debug?: (enabled: boolean) => void;\n get_emit_layout_debug?: () => boolean;\n set_wasm_timing?: (enabled: boolean) => void;\n} | null = null;\n\nlet initPromise: Promise<void> | null = null;\nlet lastInitError: unknown = null;\n\n/** The error from the most recent failed {@link initWasm} attempt, or null. */\nexport function getWasmInitError(): unknown {\n return lastInitError;\n}\n\nimport { resolveBasePath } from \"./basePath\";\n\n/**\n * Initialize the WASM engine. Must be called before any other function.\n * Safe to call multiple times \u2014 only initializes once.\n */\nexport async function initWasm(): Promise<void> {\n if (wasmModule) return;\n if (initPromise) return initPromise;\n\n const attempt = (async () => {\n try {\n const basePath = resolveBasePath();\n const assetHash =\n typeof __VIRITURA_WASM_ASSET_HASH__ === \"string\" && __VIRITURA_WASM_ASSET_HASH__.length > 0\n ? `.${__VIRITURA_WASM_ASSET_HASH__}`\n : \"\";\n\n // Step 1: Fetch the WASM binary\n const wasmResponse = await fetch(`${basePath}wasm/viritura_wasm_bg${assetHash}.wasm`);\n if (!wasmResponse.ok) {\n throw new Error(`Failed to fetch WASM binary: ${wasmResponse.status}`);\n }\n const wasmBytes = await wasmResponse.arrayBuffer();\n\n // Step 2: Import the same-origin generated glue directly. Avoiding a\n // blob: module lets production enforce `script-src 'self'`.\n const glue = await import(\n /* @vite-ignore */ /* webpackIgnore: true */ `${basePath}wasm/viritura_wasm${assetHash}.js`\n );\n\n // Step 3: Initialize with the binary bytes (avoids a second WASM fetch).\n const wasmModule_ = new WebAssembly.Module(wasmBytes);\n glue.initSync({ module: wasmModule_ });\n\n wasmModule = glue;\n lastInitError = null;\n _flushPendingDebugFlag();\n console.log(`Viritura WASM engine v${glue.engine_version()} loaded`);\n } catch (e) {\n lastInitError = e;\n console.warn(\"WASM engine failed to load:\", e);\n if (e instanceof Error) {\n console.warn(\"Error details:\", e.message);\n }\n wasmModule = null;\n }\n })();\n initPromise = attempt;\n try {\n await attempt;\n } finally {\n // A transient fetch/CSP/cache failure must not poison initialization for\n // the lifetime of this realm. Successful initialization keeps the resolved\n // promise as the idempotent fast path; failure clears it so a later caller\n // can retry.\n if (!wasmModule && initPromise === attempt) initPromise = null;\n }\n}\n\n/**\n * Check if the WASM engine is loaded and ready.\n */\nexport function isWasmReady(): boolean {\n return wasmModule !== null;\n}\n\n/** Semantic version reported by the loaded WASM engine, or null before init. */\nexport function getEngineVersion(): string | null {\n return wasmModule ? wasmModule.engine_version() : null;\n}\n\nexport function computeSlurPreview(input: SlurPreviewInput): SlurPreview {\n if (!wasmModule) {\n throw new Error(\"WASM engine not initialized. Call initWasm() first.\");\n }\n\n return JSON.parse(wasmModule.compute_slur_preview(JSON.stringify(input))) as SlurPreview;\n}\n\n/** Older/uninitialized WASM can still show the cursor, but cannot engrave a ghost. */\nexport function computeNotePreview(input: NotePreviewInput): RenderCommand[] {\n if (!wasmModule?.compute_note_preview) return [];\n return JSON.parse(wasmModule.compute_note_preview(JSON.stringify(input))) as RenderCommand[];\n}\n\n/**\n * Compute layout using the WASM engine.\n * Returns a DisplayList of render commands.\n * @param pageWidth - Available page width in pixels (0 = no system breaking)\n */\n/**\n * Map snake_case keys from Rust JSON to camelCase TypeScript DisplayList.\n * Handles: element_ids \u2192 elementIds, element_bboxes \u2192 elementBboxes\n */\nfunction normalizeDisplayList(raw: Record<string, unknown>): DisplayList {\n const dl = raw as unknown as DisplayList;\n // Remap snake_case \u2192 camelCase if needed\n if (!dl.elementIds && (raw as Record<string, unknown>)[\"element_ids\"]) {\n dl.elementIds = (raw as Record<string, unknown>)[\"element_ids\"] as (string | null)[];\n }\n if (!dl.elementBboxes && (raw as Record<string, unknown>)[\"element_bboxes\"]) {\n const rawBboxes = (raw as Record<string, unknown>)[\"element_bboxes\"] as Record<string, unknown>[];\n dl.elementBboxes = rawBboxes.map((b) => ({\n elementId: (b[\"elementId\"] ?? b[\"element_id\"]) as string,\n bbox: (b[\"bbox\"] ?? b) as BoundingBox,\n }));\n }\n if (!dl.selectionGroups && (raw as Record<string, unknown>)[\"selection_groups\"]) {\n const rawGroups = (raw as Record<string, unknown>)[\"selection_groups\"] as Record<string, unknown>[];\n dl.selectionGroups = rawGroups.map((group) => ({\n elementId: (group[\"elementId\"] ?? group[\"element_id\"]) as string,\n memberIds: (group[\"memberIds\"] ?? group[\"member_ids\"]) as string[],\n }));\n }\n // Normalize measure_bounds \u2192 measureBounds\n if (!dl.measureBounds && (raw as Record<string, unknown>)[\"measure_bounds\"]) {\n const rawBounds = (raw as Record<string, unknown>)[\"measure_bounds\"] as Record<string, unknown>[];\n dl.measureBounds = rawBounds.map((b) => ({\n index: b[\"index\"] as number,\n measureId: (b[\"measureId\"] ?? b[\"measure_id\"]) as string | undefined,\n partIndex: (b[\"partIndex\"] ?? b[\"part_index\"]) as number,\n partId: (b[\"partId\"] ?? b[\"part_id\"]) as string,\n sourcePartIndices: (b[\"sourcePartIndices\"] ?? b[\"source_part_indices\"]) as number[] | undefined,\n staffIndex: (b[\"staffIndex\"] ?? b[\"staff_index\"] ?? b[\"partIndex\"] ?? b[\"part_index\"]) as number,\n systemIndex: (b[\"systemIndex\"] ?? b[\"system_index\"]) as number | undefined,\n x: b[\"x\"] as number,\n width: b[\"width\"] as number,\n y: b[\"y\"] as number,\n height: b[\"height\"] as number,\n prefixWidth: (b[\"prefixWidth\"] ?? b[\"prefix_width\"]) as number,\n totalBeats: (b[\"totalBeats\"] ?? b[\"total_beats\"]) as number,\n beatAnchors: (b[\"beatAnchors\"] ?? b[\"beat_anchors\"]) as [number, number][],\n ghostStaff: (b[\"ghostStaff\"] ?? b[\"ghost_staff\"]) as boolean | undefined,\n isHidden: (b[\"isHidden\"] ?? b[\"is_hidden\"]) as boolean | undefined,\n hasMusicHidden: (b[\"hasMusicHidden\"] ?? b[\"has_music_hidden\"]) as boolean | undefined,\n isExpansion: (b[\"isExpansion\"] ?? b[\"is_expansion\"]) as boolean | undefined,\n }));\n }\n // Normalize layout_debug \u2192 layoutDebug\n if (!dl.layoutDebug && (raw as Record<string, unknown>)[\"layout_debug\"]) {\n dl.layoutDebug = (raw as Record<string, unknown>)[\"layout_debug\"] as DisplayList[\"layoutDebug\"];\n }\n // Normalize slur_geometries \u2192 slurGeometries\n if (!dl.slurGeometries && (raw as Record<string, unknown>)[\"slur_geometries\"]) {\n const rawSlurs = (raw as Record<string, unknown>)[\"slur_geometries\"] as Record<string, unknown>[];\n dl.slurGeometries = rawSlurs.map((s) => ({\n elementId: (s[\"elementId\"] ?? s[\"element_id\"]) as string,\n p0x: (s[\"p0x\"] ?? s[\"p0_x\"]) as number,\n p0y: (s[\"p0y\"] ?? s[\"p0_y\"]) as number,\n p1x: (s[\"p1x\"] ?? s[\"p1_x\"]) as number,\n p1y: (s[\"p1y\"] ?? s[\"p1_y\"]) as number,\n p2x: (s[\"p2x\"] ?? s[\"p2_x\"]) as number,\n p2y: (s[\"p2y\"] ?? s[\"p2_y\"]) as number,\n p3x: (s[\"p3x\"] ?? s[\"p3_x\"]) as number,\n p3y: (s[\"p3y\"] ?? s[\"p3_y\"]) as number,\n thickness: s[\"thickness\"] as number,\n curveDir: (s[\"curveDir\"] ?? s[\"curve_dir\"]) as number,\n sp: s[\"sp\"] as number,\n }));\n }\n // Normalize page_turn_warnings \u2192 pageTurnWarnings\n if (!dl.pageTurnWarnings && (raw as Record<string, unknown>)[\"page_turn_warnings\"]) {\n const rawWarnings = (raw as Record<string, unknown>)[\"page_turn_warnings\"] as Record<string, unknown>[];\n dl.pageTurnWarnings = rawWarnings.map((w) => ({\n boundaryMeasure: (w[\"boundaryMeasure\"] ?? w[\"boundary_measure\"]) as number,\n kind: w[\"kind\"] as string,\n turnSeconds: (w[\"turnSeconds\"] ?? w[\"turn_seconds\"]) as number,\n }));\n }\n return dl;\n}\n\nexport function computeLayout(\n mnxJson: string,\n partIndex: number,\n spatium: number,\n pageWidth: number = 0,\n pageSetupJson?: string,\n): DisplayList {\n if (!wasmModule) {\n throw new Error(\"WASM engine not initialized. Call initWasm() first.\");\n }\n const resultJson = wasmModule.compute_layout(mnxJson, partIndex, spatium, pageWidth, pageSetupJson);\n return normalizeDisplayList(JSON.parse(resultJson));\n}\n\n/**\n * Compute layout for all parts stacked vertically using the WASM engine.\n * Returns a DisplayList of render commands for the full score.\n * @param pageWidth - Available page width in pixels (0 = no system breaking)\n */\nexport function computeFullScoreLayout(\n mnxJson: string,\n spatium: number,\n pageWidth: number = 0,\n pageSetupJson?: string,\n): DisplayList {\n if (!wasmModule) {\n throw new Error(\"WASM engine not initialized. Call initWasm() first.\");\n }\n const resultJson = wasmModule.compute_full_score_layout(mnxJson, spatium, pageWidth, pageSetupJson);\n return normalizeDisplayList(JSON.parse(resultJson));\n}\n\n/**\n * Compute layout using the WASM engine, returning binary Float32Array.\n * Significantly faster than JSON for large scores (>50 measures).\n * @param pageWidth - Available page width in pixels (0 = no system breaking)\n */\nexport function computeLayoutBinary(\n mnxJson: string,\n partIndex: number,\n spatium: number,\n pageWidth: number = 0,\n pageSetupJson?: string,\n): Float32Array {\n if (!wasmModule) {\n throw new Error(\"WASM engine not initialized. Call initWasm() first.\");\n }\n return wasmModule.compute_layout_binary(mnxJson, partIndex, spatium, pageWidth, pageSetupJson);\n}\n\n/**\n * Compute layout for all parts using the WASM engine, returning binary Float32Array.\n * Significantly faster than JSON for large scores (>50 measures).\n * @param pageWidth - Available page width in pixels (0 = no system breaking)\n */\nexport function computeFullScoreLayoutBinary(\n mnxJson: string,\n spatium: number,\n pageWidth: number = 0,\n pageSetupJson?: string,\n): Float32Array {\n if (!wasmModule) {\n throw new Error(\"WASM engine not initialized. Call initWasm() first.\");\n }\n return wasmModule.compute_full_score_layout_binary(mnxJson, spatium, pageWidth, pageSetupJson);\n}\n\n/**\n * Compute layout using a specific MNX score definition index.\n * Returns a DisplayList of render commands.\n * @param scoreIndex - Index into the MNX scores[] array\n */\nexport function computeMnxScoreLayout(\n mnxJson: string,\n spatium: number,\n pageWidth: number = 0,\n scoreIndex: number = 0,\n pageSetupJson?: string,\n): DisplayList {\n if (!wasmModule) {\n throw new Error(\"WASM engine not initialized. Call initWasm() first.\");\n }\n const resultJson = wasmModule.compute_mnx_score_layout(mnxJson, spatium, pageWidth, scoreIndex, pageSetupJson);\n return normalizeDisplayList(JSON.parse(resultJson));\n}\n\n/**\n * Binary sibling of {@link computeMnxScoreLayout}: returns the raw packed\n * Float32Array (no decode) so a worker host can `transfer` it to the main\n * thread zero-copy instead of structured-cloning a deep DisplayList.\n * @param scoreIndex - Index into the MNX scores[] array\n */\nexport function computeMnxScoreLayoutBinary(\n mnxJson: string,\n spatium: number,\n pageWidth: number = 0,\n scoreIndex: number = 0,\n pageSetupJson?: string,\n): Float32Array {\n if (!wasmModule) {\n throw new Error(\"WASM engine not initialized. Call initWasm() first.\");\n }\n return wasmModule.compute_mnx_score_layout_binary(mnxJson, spatium, pageWidth, scoreIndex, pageSetupJson);\n}\n\n/**\n * Get score metadata from MNX JSON.\n */\nexport function getScoreInfo(mnxJson: string): ScoreInfo {\n if (!wasmModule) {\n throw new Error(\"WASM engine not initialized. Call initWasm() first.\");\n }\n const resultJson = wasmModule.get_score_info(mnxJson);\n return JSON.parse(resultJson) as ScoreInfo;\n}\n\n/**\n * Toggle the layout-debug sidecar. When enabled, every subsequent layout\n * call emits a `layoutDebug` field on the returned `DisplayList`. Used by\n * the editor's spacing debug overlay.\n *\n * Safe to call before `initWasm()` resolves \u2014 the value is replayed onto\n * the module once it's ready.\n */\nlet pendingDebugFlag: boolean | null = null;\nexport function setEmitLayoutDebug(enabled: boolean): void {\n if (!wasmModule) {\n pendingDebugFlag = enabled;\n return;\n }\n if (typeof wasmModule.set_emit_layout_debug === \"function\") {\n wasmModule.set_emit_layout_debug(enabled);\n }\n}\n\n/** Internal \u2014 called from initWasm once the module is ready. */\nfunction _flushPendingDebugFlag(): void {\n if (pendingDebugFlag !== null && wasmModule?.set_emit_layout_debug) {\n wasmModule.set_emit_layout_debug(pendingDebugFlag);\n pendingDebugFlag = null;\n }\n}\n\nexport function getEmitLayoutDebug(): boolean {\n return Boolean(wasmModule?.get_emit_layout_debug?.());\n}\n\n/** Per-page SVG result from WASM export. */\nexport interface WasmSvgPage {\n pageNumber: number;\n svg: string;\n widthMm: number;\n heightMm: number;\n}\n\n/**\n * Export a DisplayList to per-page SVG strings via the WASM engine.\n *\n * All music glyphs and text are expanded to vector path outlines using\n * font data, producing self-contained SVGs with no external dependencies.\n *\n * @param displayListJson - Serialised DisplayList (from any compute_* function).\n * @param bravuraData - Raw bytes of Bravura.otf (SMuFL music font).\n * @param textFontData - Raw bytes of a text font (e.g. BravuraText.otf). Pass empty for fallback.\n * @returns Array of per-page SVG results.\n */\nexport function wasmExportSvg(\n displayListJson: string,\n bravuraData: Uint8Array,\n textFontData: Uint8Array,\n spatiumMm: number,\n spPixels: number,\n pageWidthMm: number,\n pageHeightMm: number,\n): WasmSvgPage[] {\n if (!wasmModule) {\n throw new Error(\"WASM engine not initialized. Call initWasm() first.\");\n }\n const resultJson = wasmModule.export_svg(\n displayListJson,\n bravuraData,\n textFontData,\n spatiumMm,\n spPixels,\n pageWidthMm,\n pageHeightMm,\n );\n return JSON.parse(resultJson) as WasmSvgPage[];\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Stateful cached layout engine\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/** Opaque handle to a WASM LayoutEngine instance with measure-level cache. */\ninterface WasmLayoutEngine {\n compute_layout_cached(\n mnx_json: string,\n part_index: number,\n spatium: number,\n page_width: number,\n page_setup_json?: string,\n ): string;\n compute_full_score_layout_cached(\n mnx_json: string,\n spatium: number,\n page_width: number,\n page_setup_json?: string,\n score_index?: number,\n ): string;\n compute_full_score_layout_cached_binary(\n mnx_json: string,\n spatium: number,\n page_width: number,\n page_setup_json?: string,\n score_index?: number,\n ): Float32Array;\n relayout_retained_score_cached(\n spatium: number,\n page_width: number,\n page_setup_json?: string,\n score_index?: number,\n ): string;\n relayout_retained_score_cached_binary(\n spatium: number,\n page_width: number,\n page_setup_json?: string,\n score_index?: number,\n ): Float32Array;\n apply_patch_and_layout(\n patch_json: string,\n spatium: number,\n page_width: number,\n page_setup_json?: string,\n score_index?: number,\n ): string;\n apply_patch_and_layout_binary(\n patch_json: string,\n spatium: number,\n page_width: number,\n page_setup_json?: string,\n score_index?: number,\n ): Float32Array;\n apply_patch_and_layout_patch_frame_binary(\n patch_json: string,\n spatium: number,\n page_width: number,\n page_setup_json?: string,\n score_index?: number,\n ): Float32Array;\n full_layout(\n mnx_json: string,\n spatium: number,\n page_width: number,\n page_setup_json?: string,\n score_index?: number,\n ): string;\n invalidate_cache(): void;\n cache_stats(): number[];\n layout_metrics_json(): string;\n take_timings_json(): string | undefined;\n has_retained_score(): boolean;\n /**\n * Lever 2 step 4 (B-full): enable the per-system wholesale layout-reuse\n * store. Off by default in the engine; the factory opts in. Byte-identical\n * to the per-measure path (proven by the engine oracle).\n */\n set_system_layout_reuse?(enabled: boolean): void;\n free(): void;\n}\n\n/** Enable detailed engine phase timings in this realm's WASM instance. */\nexport function setWasmTiming(enabled: boolean): void {\n wasmModule?.set_wasm_timing?.(enabled);\n}\n\n/** Deterministic counters from the most recent retained-engine layout pass. */\nexport interface LayoutMetrics {\n resolvedCells: number;\n resolvedFullCells: number;\n widthCells: number;\n widthFullCells: number;\n freshSystems: number;\n reusedSystems: number;\n staffContentReuses: number;\n staffContentReuseRuns: number;\n staffAuxReuses: number;\n systemMeasureReuses: number;\n spannerBoundsFull: number;\n spannerBounds: number;\n mmrPlanReused: boolean;\n frameBytes: number;\n patchFrame: boolean;\n horizonChunksReused: boolean;\n horizonStaffExtentsReused: number;\n horizonTieMapsReused: number;\n cacheHits: number;\n cacheMisses: number;\n}\n\nexport const EMPTY_LAYOUT_METRICS: LayoutMetrics = {\n resolvedCells: 0,\n resolvedFullCells: 0,\n widthCells: 0,\n widthFullCells: 0,\n freshSystems: 0,\n reusedSystems: 0,\n staffContentReuses: 0,\n staffContentReuseRuns: 0,\n staffAuxReuses: 0,\n systemMeasureReuses: 0,\n spannerBoundsFull: 0,\n spannerBounds: 0,\n mmrPlanReused: false,\n frameBytes: 0,\n patchFrame: false,\n horizonChunksReused: false,\n horizonStaffExtentsReused: 0,\n horizonTieMapsReused: 0,\n cacheHits: 0,\n cacheMisses: 0,\n};\n\n/**\n * Stateful layout engine with measure-level cache.\n * Reuses natural width computations for unchanged measures across re-layouts.\n *\n * Usage:\n * ```ts\n * const engine = createCachedLayoutEngine();\n * const dl1 = engine.computeLayout(mnx, 0, sp, pw); // full computation\n * const dl2 = engine.computeLayout(mnx2, 0, sp, pw); // only changed measures recomputed\n * engine.dispose();\n * ```\n */\nexport interface CachedLayoutEngine {\n computeLayout(\n mnxJson: string,\n partIndex: number,\n spatium: number,\n pageWidth?: number,\n pageSetupJson?: string,\n ): DisplayList;\n computeFullScoreLayout(\n mnxJson: string,\n spatium: number,\n pageWidth?: number,\n pageSetupJson?: string,\n scoreIndex?: number,\n ): DisplayList;\n /** Binary sibling of {@link computeFullScoreLayout}: returns the raw packed\n * Float32Array (no decode) so a worker host can `transfer` it zero-copy,\n * avoiding a multi-megabyte JSON serialize + clone + parse on initial load. */\n computeFullScoreLayoutBinary(\n mnxJson: string,\n spatium: number,\n pageWidth?: number,\n pageSetupJson?: string,\n scoreIndex?: number,\n ): Float32Array;\n /** Re-layout the already-retained Score for a different score view / config\n * WITHOUT re-parsing MNX JSON. Requires a prior full layout call; returns\n * null when no score is retained so the caller can fall back. */\n relayoutRetainedScore(\n spatium: number,\n pageWidth?: number,\n pageSetupJson?: string,\n scoreIndex?: number,\n ): DisplayList | null;\n /** Binary sibling of {@link relayoutRetainedScore}: returns the raw packed\n * Float32Array (no decode) so a worker host can `transfer` it zero-copy.\n * Returns null when no score is retained. */\n relayoutRetainedScoreBinary(\n spatium: number,\n pageWidth?: number,\n pageSetupJson?: string,\n scoreIndex?: number,\n ): Float32Array | null;\n /** Apply a measure-level patch and re-layout incrementally.\n * Only changed measures are re-parsed. Requires a prior full layout call. */\n applyPatchAndLayout(\n patchJson: string,\n spatium: number,\n pageWidth?: number,\n pageSetupJson?: string,\n scoreIndex?: number,\n ): DisplayList;\n /** Binary sibling of {@link applyPatchAndLayout}: returns the raw packed\n * Float32Array (no decode) for zero-copy worker transfer. */\n applyPatchAndLayoutBinary(\n patchJson: string,\n spatium: number,\n pageWidth?: number,\n pageSetupJson?: string,\n scoreIndex?: number,\n ): Float32Array;\n /** Incremental sibling of {@link applyPatchAndLayoutBinary} that returns a\n * PATCH-FRAME delta (tagged at element 0: `0.0` = full display list, `1.0` =\n * patch frame) instead of a full display list, when the paged auto-flow path\n * can produce one. The caller reassembles it against a retained per-system\n * segment list (see `PatchReconstructor`). Falls back to a tagged full frame\n * transparently for every other layout path. */\n applyPatchAndLayoutPatchFrameBinary(\n patchJson: string,\n spatium: number,\n pageWidth?: number,\n pageSetupJson?: string,\n scoreIndex?: number,\n ): Float32Array;\n /** Full parse + layout with cache invalidation (for structural changes). */\n fullLayout(\n mnxJson: string,\n spatium: number,\n pageWidth?: number,\n pageSetupJson?: string,\n scoreIndex?: number,\n ): DisplayList;\n invalidateCache(): void;\n /** Returns [hits, misses] from the last layout pass. */\n cacheStats(): [number, number];\n /** Deterministic work/transport counters from the last layout pass. */\n layoutMetrics(): LayoutMetrics;\n /** Drain the optional detailed phase timing JSON for the last patch. */\n takeTimings(): string | null;\n /** True if the engine has a retained Score (patch API available). */\n hasRetainedScore(): boolean;\n dispose(): void;\n}\n\n/**\n * Create a stateful cached layout engine.\n * Must call initWasm() first.\n */\nexport function createCachedLayoutEngine(): CachedLayoutEngine {\n if (!wasmModule) {\n throw new Error(\"WASM engine not initialized. Call initWasm() first.\");\n }\n\n // The LayoutEngine constructor is exposed via wasm_bindgen\n const EngineClass = (wasmModule as Record<string, unknown>)[\"LayoutEngine\"] as {\n new (): WasmLayoutEngine;\n };\n if (!EngineClass) {\n throw new Error(\"LayoutEngine not found in WASM module. Rebuild WASM.\");\n }\n const engine = new EngineClass();\n\n // Lever 2 step 4 (B-full): opt into the per-system wholesale layout-reuse\n // store. Byte-identical to the per-measure path (engine oracle), and ~18ms\n // faster per warm edit on large paged scores. Guarded with `?.` so an older\n // WASM build without the setter still loads.\n engine.set_system_layout_reuse?.(true);\n\n return {\n computeLayout(mnxJson, partIndex, spatium, pageWidth = 0, pageSetupJson?) {\n const resultJson = engine.compute_layout_cached(mnxJson, partIndex, spatium, pageWidth, pageSetupJson);\n return normalizeDisplayList(JSON.parse(resultJson));\n },\n computeFullScoreLayout(mnxJson, spatium, pageWidth = 0, pageSetupJson?, scoreIndex?) {\n const resultJson = engine.compute_full_score_layout_cached(\n mnxJson,\n spatium,\n pageWidth,\n pageSetupJson,\n scoreIndex,\n );\n return normalizeDisplayList(JSON.parse(resultJson));\n },\n computeFullScoreLayoutBinary(mnxJson, spatium, pageWidth = 0, pageSetupJson?, scoreIndex?) {\n return engine.compute_full_score_layout_cached_binary(mnxJson, spatium, pageWidth, pageSetupJson, scoreIndex);\n },\n relayoutRetainedScore(spatium, pageWidth = 0, pageSetupJson?, scoreIndex?) {\n if (!engine.has_retained_score()) return null;\n const resultJson = engine.relayout_retained_score_cached(spatium, pageWidth, pageSetupJson, scoreIndex);\n return normalizeDisplayList(JSON.parse(resultJson));\n },\n relayoutRetainedScoreBinary(spatium, pageWidth = 0, pageSetupJson?, scoreIndex?) {\n if (!engine.has_retained_score()) return null;\n return engine.relayout_retained_score_cached_binary(spatium, pageWidth, pageSetupJson, scoreIndex);\n },\n applyPatchAndLayout(patchJson, spatium, pageWidth = 0, pageSetupJson?, scoreIndex?) {\n performance.mark(\"viritura:wasm-raw-start\");\n const binary = engine.apply_patch_and_layout_binary(patchJson, spatium, pageWidth, pageSetupJson, scoreIndex);\n performance.mark(\"viritura:wasm-raw-end\");\n performance.measure(\"viritura:wasm-raw\", \"viritura:wasm-raw-start\", \"viritura:wasm-raw-end\");\n\n performance.mark(\"viritura:displaylist-parse-start\");\n const normalized = decodeBinaryDisplayList(binary);\n performance.mark(\"viritura:displaylist-parse-end\");\n performance.measure(\n \"viritura:displaylist-parse\",\n \"viritura:displaylist-parse-start\",\n \"viritura:displaylist-parse-end\",\n );\n return normalized;\n },\n applyPatchAndLayoutBinary(patchJson, spatium, pageWidth = 0, pageSetupJson?, scoreIndex?) {\n return engine.apply_patch_and_layout_binary(patchJson, spatium, pageWidth, pageSetupJson, scoreIndex);\n },\n applyPatchAndLayoutPatchFrameBinary(patchJson, spatium, pageWidth = 0, pageSetupJson?, scoreIndex?) {\n return engine.apply_patch_and_layout_patch_frame_binary(patchJson, spatium, pageWidth, pageSetupJson, scoreIndex);\n },\n fullLayout(mnxJson, spatium, pageWidth = 0, pageSetupJson?, scoreIndex?) {\n const resultJson = engine.full_layout(mnxJson, spatium, pageWidth, pageSetupJson, scoreIndex);\n return normalizeDisplayList(JSON.parse(resultJson));\n },\n invalidateCache() {\n engine.invalidate_cache();\n },\n cacheStats(): [number, number] {\n const stats = engine.cache_stats();\n return [stats[0] ?? 0, stats[1] ?? 0];\n },\n layoutMetrics(): LayoutMetrics {\n try {\n return JSON.parse(engine.layout_metrics_json()) as LayoutMetrics;\n } catch {\n return { ...EMPTY_LAYOUT_METRICS };\n }\n },\n takeTimings(): string | null {\n return engine.take_timings_json() ?? null;\n },\n hasRetainedScore() {\n return engine.has_retained_score();\n },\n dispose() {\n engine.free();\n },\n };\n}\n", "/** Translation of public layout options into engine calls, shared by main thread and worker. */\n\nimport { LayoutError, ParseError } from \"./errors\";\nimport type { LayoutOptions } from \"./types\";\n\nexport interface LayoutArgs {\n readonly json: string;\n readonly spatium: number;\n readonly pageWidth: number;\n readonly scoreIndex: number;\n readonly pageSetupJson: string | undefined;\n}\n\nexport function mnxJson(mnx: string | object): string {\n if (typeof mnx === \"string\") return mnx;\n try {\n return JSON.stringify(mnx);\n } catch (err) {\n throw new ParseError(`MNX input is not JSON-serializable: ${messageOf(err)}`, \"json\", err);\n }\n}\n\nexport function layoutArgs(mnx: string | object, opts: LayoutOptions): LayoutArgs {\n const spatium = opts.spatium ?? 7;\n if (!(spatium > 0)) throw new RangeError(`spatium must be positive, got ${spatium}`);\n const setup = opts.pageSetup;\n return {\n json: mnxJson(mnx),\n spatium,\n pageWidth: opts.pageWidth,\n scoreIndex: opts.scoreIndex ?? 0,\n // The engine takes page geometry in staff spaces.\n pageSetupJson: setup\n ? JSON.stringify({\n page_height: setup.height / spatium,\n page_margin_top: setup.margins.top / spatium,\n page_margin_right: setup.margins.right / spatium,\n page_margin_bottom: setup.margins.bottom / spatium,\n page_margin_left: setup.margins.left / spatium,\n })\n : undefined,\n };\n}\n\nexport function messageOf(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n/** Classify a raw engine failure. The Rust engine reports errors as strings. */\nexport function layoutFailure(err: unknown): ParseError | LayoutError {\n if (err instanceof ParseError || err instanceof LayoutError) return err;\n const msg = messageOf(err);\n if (/parse|json|schema|missing field|expected|invalid type|unknown variant/i.test(msg)) {\n return new ParseError(`Failed to parse MNX: ${msg}`, \"schema\", err);\n }\n if (/out of memory|memory access|allocation/i.test(msg)) {\n return new LayoutError(`Layout failed: ${msg}`, \"oom\", err);\n }\n return new LayoutError(`Layout failed: ${msg}`, \"wasm\", err);\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;AAAA;AAAA;AAAA,sCAAC;AAAA;AAAA;;;ACAD;;;ACAA;;;ACAA;AAAA,IAAI,wBAAuC;AAMpC,SAAS,iBAAiB,UAA+B;AAC9D,MAAI,CAAC,UAAU;AACb,4BAAwB;AACxB;AAAA,EACF;AAEA,0BAAwB,SAAS,SAAS,GAAG,IAAI,WAAW,GAAG,QAAQ;AACzE;AAaO,SAAS,kBAA0B;AACxC,MAAI;AACF,WAAO;AAIT,MAAI,OAAO,WAAa,KAAa;AACnC,QAAM,SAAS,SAAS,cAAc,MAAM;AAC5C,QAAI,QAAQ;AACV,UAAM,OAAO,OAAO,aAAa,MAAM;AACvC,UAAI;AACF,YAAI;AACF,cAAM,OAAO,IAAI,IAAI,MAAM,OAAO,SAAS,MAAM,EAAE;AACnD,iBAAO,KAAK,SAAS,GAAG,IAAI,OAAO,OAAO;AAAA,QAC5C,QAAQ;AAAA,QAER;AAAA,IAEJ;AAAA,EACF;AAGA,MAAI;AACF,QAAM,WAAY,gCAAqD;AACvE,QAAI;AACF,aAAO,SAAS,SAAS,GAAG,IAAI,WAAW,WAAW;AAAA,EAE1D,QAAQ;AAAA,EAER;AAGA,SAAO;AACT;;;AC3DA;AAwBA,IAAI,aAyDO,MAEP,cAAoC,MACpC,gBAAyB;AAGtB,SAAS,mBAA4B;AAC1C,SAAO;AACT;AAQA,eAAsB,WAA0B;AAC9C,MAAI,WAAY;AAChB,MAAI,YAAa,QAAO;AAExB,MAAM,WAAW,YAAY;AAC3B,QAAI;AACF,UAAM,WAAW,gBAAgB,GAC3B,YAGA,IAGA,eAAe,MAAM,MAAM,GAAG,QAAQ,wBAAwB,SAAS,OAAO;AACpF,UAAI,CAAC,aAAa;AAChB,cAAM,IAAI,MAAM,gCAAgC,aAAa,MAAM,EAAE;AAEvE,UAAM,YAAY,MAAM,aAAa,YAAY,GAI3C,OAAO,MAAM;AAAA;AAAA;AAAA,QAC4B,GAAG,QAAQ,qBAAqB,SAAS;AAAA,SAIlF,cAAc,IAAI,YAAY,OAAO,SAAS;AACpD,WAAK,SAAS,EAAE,QAAQ,YAAY,CAAC,GAErC,aAAa,MACb,gBAAgB,MAChB,uBAAuB,GACvB,QAAQ,IAAI,yBAAyB,KAAK,eAAe,CAAC,SAAS;AAAA,IACrE,SAAS,GAAG;AACV,sBAAgB,GAChB,QAAQ,KAAK,+BAA+B,CAAC,GACzC,aAAa,SACf,QAAQ,KAAK,kBAAkB,EAAE,OAAO,GAE1C,aAAa;AAAA,IACf;AAAA,EACF,GAAG;AACH,gBAAc;AACd,MAAI;AACF,UAAM;AAAA,EACR,UAAE;AAKA,IAAI,CAAC,cAAc,gBAAgB,YAAS,cAAc;AAAA,EAC5D;AACF;AAKO,SAAS,cAAuB;AACrC,SAAO,eAAe;AACxB;AAwMO,SAAS,4BACd,SACA,SACA,YAAoB,GACpB,aAAqB,GACrB,eACc;AACd,MAAI,CAAC;AACH,UAAM,IAAI,MAAM,qDAAqD;AAEvE,SAAO,WAAW,gCAAgC,SAAS,SAAS,WAAW,YAAY,aAAa;AAC1G;AAqBA,IAAI,mBAAmC;AAYvC,SAAS,yBAA+B;AACtC,EAAI,qBAAqB,QAAQ,YAAY,0BAC3C,WAAW,sBAAsB,gBAAgB,GACjD,mBAAmB;AAEvB;;;ACrZA;AA4CO,SAAS,UAAU,KAAsB;AAC9C,SAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AACxD;;;AJ5BA,IAAI,QAA8B;AAElC,eAAe,aAAa,cAAqC;AAG/D,MAFA,iBAAiB,YAAY,GAC7B,MAAM,SAAS,GACX,CAAC,YAAY,EAAG,OAAM,IAAI,MAAM,UAAU,iBAAiB,KAAK,4BAA4B,CAAC;AACnG;AAEA,SAAS,MAAM,SAA+B,WAA2B,CAAC,GAAS;AACjF,OAAK,YAAY,SAAS,QAAQ;AACpC;AAEA,KAAK,YAAY,OAAO,UAA6C;AACnE,MAAM,MAAM,MAAM;AAClB,MAAI,IAAI,SAAS,QAAQ;AACvB,YAAQ,aAAa,IAAI,YAAY,GACrC,MAAM;AAAA,MACJ,MAAM,MAAM,EAAE,IAAI,IAAI,IAAI,IAAI,GAAK,CAAC;AAAA,MACpC,CAAC,QAAiB;AAChB,gBAAQ,MACR,MAAM,EAAE,IAAI,IAAI,IAAI,IAAI,IAAO,MAAM,QAAQ,SAAS,UAAU,GAAG,EAAE,CAAC;AAAA,MACxE;AAAA,IACF;AACA;AAAA,EACF;AACA,MAAI;AACF,QAAI,CAAC,MAAO,OAAM,IAAI,MAAM,gCAAgC;AAC5D,UAAM;AACN,QAAM,EAAE,MAAM,SAAS,WAAW,YAAY,cAAc,IAAI,IAAI,MAC9D,SAAS,4BAAgC,MAAM,SAAS,WAAW,YAAY,aAAa,GAE5F,OAAO,IAAI,aAAa,MAAM;AACpC,UAAM,EAAE,IAAI,IAAI,IAAI,IAAI,IAAM,KAAK,GAAG,CAAC,KAAK,MAAM,CAAC;AAAA,EACrD,SAAS,KAAK;AACZ,UAAM,EAAE,IAAI,IAAI,IAAI,IAAI,IAAO,MAAM,UAAU,SAAS,UAAU,GAAG,EAAE,CAAC;AAAA,EAC1E;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `loadEngine()` and the `Engine` handle it resolves to.
|
|
3
|
+
*
|
|
4
|
+
* The engine is the rendering kernel: MNX in, layout + ink + geometry +
|
|
5
|
+
* timeline out. It never touches the DOM beyond the canvas context it is
|
|
6
|
+
* given; page presentation, zoom, scrolling and UI live in the viewer layer.
|
|
7
|
+
*/
|
|
8
|
+
import type { Timeline, TimelineOptions } from "./timeline";
|
|
9
|
+
import type { CanvasBeatHit, CanvasBeatPosition, DisplayList, EngineVersion, HorizonPaper, LayoutOptions, LayoutWorker, LayoutWorkerOptions, LoadEngineOptions, MeasureGeometry, PaintOptions, PlayheadGeometry, ScoreInfo, ScoreMeasurements, ScorePosition, SvgOptions, SystemGeometry, TileRenderer } from "./types";
|
|
10
|
+
/** The loaded score engine. Acquire via {@link loadEngine}. */
|
|
11
|
+
export interface Engine {
|
|
12
|
+
readonly version: EngineVersion;
|
|
13
|
+
/**
|
|
14
|
+
* Lay out an MNX document. Throws `ParseError` for unreadable input and
|
|
15
|
+
* `LayoutError` if the engine fails.
|
|
16
|
+
*/
|
|
17
|
+
layout(mnx: string | object, opts: LayoutOptions): DisplayList;
|
|
18
|
+
/**
|
|
19
|
+
* Paint one page's ink into `ctx` under the caller's current transform.
|
|
20
|
+
* Never clears the canvas or paints paper unless `opts.background` is set.
|
|
21
|
+
*/
|
|
22
|
+
paint(ctx: CanvasRenderingContext2D, displayList: DisplayList, opts?: PaintOptions): void;
|
|
23
|
+
/** Page sizes and parts of a layout. */
|
|
24
|
+
measure(displayList: DisplayList): ScoreMeasurements;
|
|
25
|
+
/** Systems in reading order, page-local. */
|
|
26
|
+
systems(displayList: DisplayList): SystemGeometry[];
|
|
27
|
+
/** Every rendered measure/staff, page-local. */
|
|
28
|
+
measures(displayList: DisplayList): MeasureGeometry[];
|
|
29
|
+
/** Paper rectangle for unpaged (horizon) layouts. */
|
|
30
|
+
horizonPaper(displayList: DisplayList): HorizonPaper;
|
|
31
|
+
/** Where a musical position lands on its part's (top) staff. */
|
|
32
|
+
positionToCanvas(displayList: DisplayList, position: ScorePosition): CanvasBeatPosition | null;
|
|
33
|
+
/** A cursor spanning the whole system at a musical position. */
|
|
34
|
+
playhead(displayList: DisplayList, position: ScorePosition): PlayheadGeometry | null;
|
|
35
|
+
/** Hit-test a page-local point to a musical position. */
|
|
36
|
+
canvasToBeat(displayList: DisplayList, page: number, x: number, y: number): CanvasBeatHit | null;
|
|
37
|
+
/** Parts, measure count and score entries, without running layout. */
|
|
38
|
+
info(mnx: string | object): ScoreInfo;
|
|
39
|
+
/** Layout-independent playback timeline. Deterministic. */
|
|
40
|
+
timeline(mnx: string | object, opts?: TimelineOptions): Timeline;
|
|
41
|
+
/** Standalone SVG for one page (fonts converted to outlines). */
|
|
42
|
+
toSvg(displayList: DisplayList, opts?: SvgOptions): Promise<string>;
|
|
43
|
+
/** Run layout in a dedicated worker. */
|
|
44
|
+
createLayoutWorker(opts?: LayoutWorkerOptions): LayoutWorker;
|
|
45
|
+
/** Cached tile painter for large unpaged layouts. */
|
|
46
|
+
createTileRenderer(): TileRenderer;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Load the WASM engine and (by default) its fonts. Concurrent calls share one
|
|
50
|
+
* load; a failed load rejects with `EngineLoadError` and may be retried.
|
|
51
|
+
*/
|
|
52
|
+
export declare function loadEngine(opts?: LoadEngineOptions): Promise<Engine>;
|
|
53
|
+
/** True once {@link loadEngine} has resolved. */
|
|
54
|
+
export declare function isEngineReady(): boolean;
|
|
55
|
+
/** Build the engine facade. Requires WASM only for layout, info and SVG. */
|
|
56
|
+
export declare function createEngine(): Engine;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed errors for @viritura/score-engine.
|
|
3
|
+
*
|
|
4
|
+
* Public consumers can `instanceof`-check these to differentiate failure
|
|
5
|
+
* modes without parsing error message strings.
|
|
6
|
+
*/
|
|
7
|
+
/** Engine asset (WASM, font) failed to load. */
|
|
8
|
+
export declare class EngineLoadError extends Error {
|
|
9
|
+
/** What failed to load: `wasm`, `font`, or `unknown`. */
|
|
10
|
+
readonly code: "wasm" | "font" | "unknown";
|
|
11
|
+
readonly cause?: unknown | undefined;
|
|
12
|
+
readonly name = "EngineLoadError";
|
|
13
|
+
constructor(message: string,
|
|
14
|
+
/** What failed to load: `wasm`, `font`, or `unknown`. */
|
|
15
|
+
code: "wasm" | "font" | "unknown", cause?: unknown | undefined);
|
|
16
|
+
}
|
|
17
|
+
/** Input MNX could not be parsed (malformed JSON or invalid schema). */
|
|
18
|
+
export declare class ParseError extends Error {
|
|
19
|
+
/** Reason: `json` (syntax), `schema` (shape), or `unsupported` (unknown feature). */
|
|
20
|
+
readonly code: "json" | "schema" | "unsupported";
|
|
21
|
+
readonly cause?: unknown | undefined;
|
|
22
|
+
readonly name = "ParseError";
|
|
23
|
+
constructor(message: string,
|
|
24
|
+
/** Reason: `json` (syntax), `schema` (shape), or `unsupported` (unknown feature). */
|
|
25
|
+
code: "json" | "schema" | "unsupported", cause?: unknown | undefined);
|
|
26
|
+
}
|
|
27
|
+
/** Layout engine failed (typically WASM panic or out-of-memory). */
|
|
28
|
+
export declare class LayoutError extends Error {
|
|
29
|
+
/** Reason: `wasm` (engine panic), `oom`, or `unknown`. */
|
|
30
|
+
readonly code: "wasm" | "oom" | "unknown";
|
|
31
|
+
readonly cause?: unknown | undefined;
|
|
32
|
+
readonly name = "LayoutError";
|
|
33
|
+
constructor(message: string,
|
|
34
|
+
/** Reason: `wasm` (engine panic), `oom`, or `unknown`. */
|
|
35
|
+
code: "wasm" | "oom" | "unknown", cause?: unknown | undefined);
|
|
36
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @viritura/score-engine — framework-free music notation rendering kernel.
|
|
3
|
+
*
|
|
4
|
+
* MNX in; layout, ink-only Canvas painting, SVG, geometry queries and a
|
|
5
|
+
* playback timeline out. Page presentation (paper, zoom, scrolling, view
|
|
6
|
+
* modes) belongs to `@viritura/score-viewer`.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { loadEngine } from "@viritura/score-engine";
|
|
10
|
+
*
|
|
11
|
+
* const engine = await loadEngine();
|
|
12
|
+
* const dl = engine.layout(mnx, { pageWidth: 800 });
|
|
13
|
+
* const ctx = canvas.getContext("2d")!;
|
|
14
|
+
* ctx.fillStyle = "#fff";
|
|
15
|
+
* ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
16
|
+
* engine.paint(ctx, dl, { page: 0 });
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export { loadEngine, isEngineReady } from "./engine";
|
|
20
|
+
export type { Engine } from "./engine";
|
|
21
|
+
export { EngineLoadError, ParseError, LayoutError } from "./errors";
|
|
22
|
+
export type { CanvasBeatHit, CanvasBeatPosition, DisplayList, EngineVersion, HorizonPaper, LayoutOptions, LayoutPageSetup, LayoutWorker, LayoutWorkerOptions, LoadEngineOptions, MeasureGeometry, PageGeometry, PaintOptions, PartInfo, PlayheadGeometry, Rect, ScoreInfo, ScoreMeasurements, ScorePosition, SvgOptions, SystemGeometry, TilePaintOptions, TileRenderer, } from "./types";
|
|
23
|
+
export type { DynamicMark, TempoSegment, TimedEvent, Timeline, TimelineOptions } from "./timeline";
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public timeline + beat-mapping types for @viritura/score-engine.
|
|
3
|
+
*
|
|
4
|
+
* The timeline is the audio engine's input: a deterministic, layout-
|
|
5
|
+
* independent representation of "what plays when". The engine doesn't
|
|
6
|
+
* know about audio, samples, or MIDI hardware — that's the audio
|
|
7
|
+
* package's job. Keeping these types audio-free is what lets the
|
|
8
|
+
* playback engine be swapped without touching the renderer.
|
|
9
|
+
*/
|
|
10
|
+
/** Symbolic dynamic markings as written in the score. */
|
|
11
|
+
export type DynamicMark = "ppp" | "pp" | "p" | "mp" | "mf" | "f" | "ff" | "fff";
|
|
12
|
+
/** A timed playback note (rest positions are represented by gaps between events). */
|
|
13
|
+
export interface TimedEvent {
|
|
14
|
+
/** Stable part identifier from the source MNX. */
|
|
15
|
+
partId: string;
|
|
16
|
+
/** Written quarter-note beat from the start of the expanded score, not inferred from performance time. */
|
|
17
|
+
beat: number;
|
|
18
|
+
/** Written duration in quarter-note beats (not shortened by articulation). */
|
|
19
|
+
durationBeats: number;
|
|
20
|
+
/** Absolute time in seconds from the start of the score. */
|
|
21
|
+
timeSeconds: number;
|
|
22
|
+
/** MIDI note number (0–127). Omit for rests. */
|
|
23
|
+
midiPitch?: number;
|
|
24
|
+
/** True for rests, when a rest event is supplied by a timeline source. */
|
|
25
|
+
isRest: boolean;
|
|
26
|
+
/** Symbolic dynamic if explicitly marked at this event. */
|
|
27
|
+
dynamic?: DynamicMark;
|
|
28
|
+
/** Stable event ID from the source MNX (when the source assigned one). */
|
|
29
|
+
eventId?: string;
|
|
30
|
+
}
|
|
31
|
+
/** Beat → BPM transition for tempo automation. */
|
|
32
|
+
export interface TempoSegment {
|
|
33
|
+
/** Global beat where this tempo takes effect. */
|
|
34
|
+
beat: number;
|
|
35
|
+
/** Absolute time in seconds when this tempo takes effect. */
|
|
36
|
+
timeSeconds: number;
|
|
37
|
+
/** Tempo in beats per minute (quarter-note = 1 beat). */
|
|
38
|
+
bpm: number;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The complete playback timeline derived from MNX.
|
|
42
|
+
*
|
|
43
|
+
* Layout-independent: two scores with identical music but different
|
|
44
|
+
* page breaks produce identical timelines. This is what makes the
|
|
45
|
+
* timeline safe to ship to a server-side audio renderer.
|
|
46
|
+
*/
|
|
47
|
+
export interface Timeline {
|
|
48
|
+
/** Total notated length of the (expanded) score in quarter-note beats. */
|
|
49
|
+
totalBeats: number;
|
|
50
|
+
/** Total length of the (expanded) score in seconds. */
|
|
51
|
+
totalSeconds: number;
|
|
52
|
+
/** Stable part IDs, in score order. */
|
|
53
|
+
partIds: readonly string[];
|
|
54
|
+
/** Playback note events, sorted by written `beat`; silence is a gap. */
|
|
55
|
+
events: readonly TimedEvent[];
|
|
56
|
+
/** Tempo automation on the expanded written quarter-note beat axis. */
|
|
57
|
+
tempoMap: readonly TempoSegment[];
|
|
58
|
+
}
|
|
59
|
+
/** Options for `engine.timeline()`. */
|
|
60
|
+
export interface TimelineOptions {
|
|
61
|
+
/**
|
|
62
|
+
* Whether to expand DC/DS/repeat directions when computing the
|
|
63
|
+
* timeline. Default `"expand"`. Use `"ignore"` to get a one-pass
|
|
64
|
+
* timeline matching the visual measure order.
|
|
65
|
+
*/
|
|
66
|
+
repeatExpansion?: "expand" | "ignore";
|
|
67
|
+
}
|