@viritura/score-engine 0.1.0-next.1 → 0.1.1-next.10
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/index.js +11578 -156
- package/dist/index.js.map +4 -4
- package/dist/score-engine.worker.js.map +1 -1
- package/dist/wasm/viritura_wasm.js +9 -6
- package/dist/wasm/viritura_wasm_bg.wasm +0 -0
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
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"],
|
|
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 { paintInkCommands, raiseStaffEraseFrames } from \"./inkMask\";\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
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
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -691,24 +691,27 @@ export function set_wasm_timing(enabled) {
|
|
|
691
691
|
function __wbg_get_imports() {
|
|
692
692
|
const import0 = {
|
|
693
693
|
__proto__: null,
|
|
694
|
-
|
|
694
|
+
__wbg___wbindgen_throw_41e9ee4f547fc59a: function(arg0, arg1) {
|
|
695
695
|
throw new Error(getStringFromWasm0(arg0, arg1));
|
|
696
696
|
},
|
|
697
|
+
__wbg_getRandomValues_a608c4436c19407a: function() { return handleError(function (arg0, arg1) {
|
|
698
|
+
globalThis.crypto.getRandomValues(getArrayU8FromWasm0(arg0, arg1));
|
|
699
|
+
}, arguments); },
|
|
697
700
|
__wbg_getRandomValues_a678b7300e8ed57f: function() { return handleError(function (arg0, arg1) {
|
|
698
701
|
globalThis.crypto.getRandomValues(getArrayU8FromWasm0(arg0, arg1));
|
|
699
702
|
}, arguments); },
|
|
700
|
-
|
|
703
|
+
__wbg_new_from_slice_ca6ad97db1f4779a: function(arg0, arg1) {
|
|
701
704
|
const ret = new Float32Array(getArrayF32FromWasm0(arg0, arg1));
|
|
702
705
|
return ret;
|
|
703
706
|
},
|
|
704
|
-
|
|
707
|
+
__wbg_now_aa4ccb83129e9e55: function() {
|
|
705
708
|
const ret = Date.now();
|
|
706
709
|
return ret;
|
|
707
|
-
},
|
|
708
|
-
|
|
710
|
+
},
|
|
711
|
+
__wbg_now_b72b0bf55c221a9d: function() { return handleError(function () {
|
|
709
712
|
const ret = Date.now();
|
|
710
713
|
return ret;
|
|
711
|
-
},
|
|
714
|
+
}, arguments); },
|
|
712
715
|
__wbindgen_generic_0000000000000001: function(arg0, arg1) {
|
|
713
716
|
// Cast intrinsic for `Ref(String) -> Externref`.
|
|
714
717
|
const ret = getStringFromWasm0(arg0, arg1);
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@viritura/score-engine",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1-next.10",
|
|
4
4
|
"description": "Framework-free music notation rendering kernel: MNX layout (Rust/WASM), ink-only Canvas paint, SVG, geometry queries and a playback timeline.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"music",
|