@empty-sekai/sekai-custom-profile-sdk 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/LICENSE +235 -0
  2. package/LICENSE-EXCEPTION +32 -0
  3. package/NOTICE +11 -0
  4. package/README.en.md +456 -0
  5. package/README.md +458 -0
  6. package/dist/allium_renderer_wasm.js +14 -0
  7. package/dist/allium_renderer_wasm.wasm +0 -0
  8. package/dist/authoring.d.ts +14 -0
  9. package/dist/authoring.js +34 -0
  10. package/dist/cache/glyphPersistentCache.d.ts +145 -0
  11. package/dist/cache/glyphPersistentCache.js +346 -0
  12. package/dist/cache/indexedDbGlyphRecordStore.d.ts +26 -0
  13. package/dist/cache/indexedDbGlyphRecordStore.js +214 -0
  14. package/dist/cache/sessionImageResourceCache.d.ts +39 -0
  15. package/dist/cache/sessionImageResourceCache.js +156 -0
  16. package/dist/emscripten.d.ts +11 -0
  17. package/dist/emscripten.js +1 -0
  18. package/dist/fontProvider.d.ts +36 -0
  19. package/dist/fontProvider.js +84 -0
  20. package/dist/fontSdfAtlas.d.ts +92 -0
  21. package/dist/fontSdfAtlas.js +296 -0
  22. package/dist/gpu/browserSemanticResources.d.ts +64 -0
  23. package/dist/gpu/browserSemanticResources.js +232 -0
  24. package/dist/gpu/generalTextRenderPlacement.d.ts +8 -0
  25. package/dist/gpu/generalTextRenderPlacement.js +131 -0
  26. package/dist/gpu/previewTransformTextureLayout.d.ts +1 -0
  27. package/dist/gpu/previewTransformTextureLayout.js +12 -0
  28. package/dist/gpu/semanticCommandGeometry.d.ts +21 -0
  29. package/dist/gpu/semanticCommandGeometry.js +226 -0
  30. package/dist/gpu/semanticCommandPlanner.d.ts +131 -0
  31. package/dist/gpu/semanticCommandPlanner.js +203 -0
  32. package/dist/gpu/semanticTextGlyphBridge.d.ts +3 -0
  33. package/dist/gpu/semanticTextGlyphBridge.js +22 -0
  34. package/dist/gpu/semanticWebglSceneRenderer.d.ts +70 -0
  35. package/dist/gpu/semanticWebglSceneRenderer.js +211 -0
  36. package/dist/gpu/slotRanges.d.ts +5 -0
  37. package/dist/gpu/slotRanges.js +14 -0
  38. package/dist/gpu/webglSdfAtlasTexture.d.ts +17 -0
  39. package/dist/gpu/webglSdfAtlasTexture.js +72 -0
  40. package/dist/gpu/webglSdfGlyphPipeline.d.ts +26 -0
  41. package/dist/gpu/webglSdfGlyphPipeline.js +267 -0
  42. package/dist/gpu/webglSemanticCommandExecutor.d.ts +79 -0
  43. package/dist/gpu/webglSemanticCommandExecutor.js +686 -0
  44. package/dist/index.d.ts +18 -0
  45. package/dist/index.js +8 -0
  46. package/dist/interaction/numericTextRegions.d.ts +46 -0
  47. package/dist/interaction/numericTextRegions.js +44 -0
  48. package/dist/localizationProvider.d.ts +33 -0
  49. package/dist/localizationProvider.js +74 -0
  50. package/dist/originPrebuiltSdfAtlasPackage.d.ts +39 -0
  51. package/dist/originPrebuiltSdfAtlasPackage.js +316 -0
  52. package/dist/prebuiltSdfAtlas.d.ts +36 -0
  53. package/dist/prebuiltSdfAtlas.js +261 -0
  54. package/dist/protocol.d.ts +456 -0
  55. package/dist/protocol.js +1 -0
  56. package/dist/renderer.d.ts +218 -0
  57. package/dist/renderer.js +641 -0
  58. package/dist/resourceProvider.d.ts +24 -0
  59. package/dist/resourceProvider.js +41 -0
  60. package/dist/telemetry/rendererTelemetry.d.ts +158 -0
  61. package/dist/telemetry/rendererTelemetry.js +232 -0
  62. package/dist/third-party/freetype/FTL.txt +169 -0
  63. package/dist/types/atlas.d.ts +90 -0
  64. package/dist/types/atlas.js +1 -0
  65. package/dist/types/authoring.d.ts +81 -0
  66. package/dist/types/authoring.js +1 -0
  67. package/dist/types/core.d.ts +128 -0
  68. package/dist/types/core.js +1 -0
  69. package/dist/types/freeType.d.ts +54 -0
  70. package/dist/types/freeType.js +13 -0
  71. package/dist/types/glyph.d.ts +33 -0
  72. package/dist/types/glyph.js +1 -0
  73. package/dist/types/layout.d.ts +25 -0
  74. package/dist/types/layout.js +1 -0
  75. package/dist/types/text.d.ts +28 -0
  76. package/dist/types/text.js +1 -0
  77. package/dist/worker/glyphWorkScheduler.d.ts +29 -0
  78. package/dist/worker/glyphWorkScheduler.js +107 -0
  79. package/dist/worker-client.d.ts +114 -0
  80. package/dist/worker-client.js +413 -0
  81. package/dist/worker.d.ts +1 -0
  82. package/dist/worker.js +708 -0
  83. package/package.json +46 -0
  84. package/src/atlas.rs +652 -0
  85. package/src/authoring.ts +42 -0
  86. package/src/authoring_runtime.rs +303 -0
  87. package/src/cache/glyphPersistentCache.ts +463 -0
  88. package/src/cache/indexedDbGlyphRecordStore.ts +238 -0
  89. package/src/cache/sessionImageResourceCache.ts +186 -0
  90. package/src/edt.rs +97 -0
  91. package/src/emscripten.ts +17 -0
  92. package/src/fontProvider.ts +127 -0
  93. package/src/fontSdfAtlas.ts +431 -0
  94. package/src/geometry.rs +149 -0
  95. package/src/glyph_plan.rs +226 -0
  96. package/src/gpu/browserSemanticResources.ts +294 -0
  97. package/src/gpu/generalTextRenderPlacement.ts +175 -0
  98. package/src/gpu/previewTransformTextureLayout.ts +12 -0
  99. package/src/gpu/semanticCommandGeometry.ts +260 -0
  100. package/src/gpu/semanticCommandPlanner.ts +280 -0
  101. package/src/gpu/semanticTextGlyphBridge.ts +35 -0
  102. package/src/gpu/semanticWebglSceneRenderer.ts +261 -0
  103. package/src/gpu/slotRanges.ts +15 -0
  104. package/src/gpu/webglSdfAtlasTexture.ts +69 -0
  105. package/src/gpu/webglSdfGlyphPipeline.ts +299 -0
  106. package/src/gpu/webglSemanticCommandExecutor.ts +739 -0
  107. package/src/index.ts +84 -0
  108. package/src/interaction/numericTextRegions.ts +80 -0
  109. package/src/layout.rs +1882 -0
  110. package/src/lib.rs +1443 -0
  111. package/src/localizationProvider.ts +113 -0
  112. package/src/main.rs +55 -0
  113. package/src/masterdata_runtime.rs +496 -0
  114. package/src/originPrebuiltSdfAtlasPackage.ts +403 -0
  115. package/src/prebuiltSdfAtlas.ts +306 -0
  116. package/src/protocol.ts +150 -0
  117. package/src/renderer.ts +792 -0
  118. package/src/resourceProvider.ts +69 -0
  119. package/src/scene.rs +665 -0
  120. package/src/telemetry/rendererTelemetry.ts +358 -0
  121. package/src/types/atlas.ts +80 -0
  122. package/src/types/authoring.ts +85 -0
  123. package/src/types/core.ts +110 -0
  124. package/src/types/freeType.ts +63 -0
  125. package/src/types/glyph.ts +33 -0
  126. package/src/types/layout.ts +28 -0
  127. package/src/types/text.ts +28 -0
  128. package/src/worker/glyphWorkScheduler.ts +130 -0
  129. package/src/worker-client.ts +484 -0
  130. package/src/worker.ts +895 -0
package/README.en.md ADDED
@@ -0,0 +1,456 @@
1
+ # @empty-sekai/sekai-custom-profile-sdk
2
+
3
+ [简体中文](README.md) | [English](README.en.md)
4
+
5
+ `@empty-sekai/sekai-custom-profile-sdk` is an unofficial browser SDK for Project SEKAI (PJSK) custom profiles, combining authoring, scene resolution, WebGL2 rendering, interaction, export, and font-atlas support.
6
+
7
+ TMP rich text and layout implement a compatibility model for the Unity TextMesh Pro data and behavior used by PJSK. It covers the tags, layout, material, and dynamic semantics currently modeled and verified by the runtime. Unmodeled game behavior and later game updates may differ from the client, so complete reproduction of the game's rendering logic or final pixels is not guaranteed.
8
+
9
+ Rust/WASM owns profile resolution, TMP rich text, layout, dynamic formulas, stable semantic IDs, glyph demand, FreeType metrics, glyph SDF generation, and atlas placement. TypeScript owns the worker boundary, asynchronous resource scheduling, cache I/O, and GPU resource orchestration. WebGL2 consumes the semantic command stream and compact state tables.
10
+
11
+ Version 0.2 provides a stateful scene API built from a Rust/WASM semantic runtime, dedicated worker, FreeType/SDF atlas, and WebGL2 renderer.
12
+
13
+ ## Requirements
14
+
15
+ - WebGL2;
16
+ - ES modules and Web Workers;
17
+ - optional IndexedDB for persistent opaque glyph records;
18
+ - host-provided profile/card JSON, masterdata, a font provider or pre-registered fonts, and a `ResourceProvider`.
19
+
20
+ The host supplies fonts, player data, masterdata, and image assets. Host-provided font bytes with fixed source hashes, FreeType metrics, TMP parsing, and the SDF pipeline jointly define text layout and glyph pixels.
21
+
22
+ ## Installation
23
+
24
+ ```sh
25
+ npm install @empty-sekai/sekai-custom-profile-sdk
26
+ ```
27
+
28
+ By default, the worker, Emscripten glue, and WASM files load from the `dist/` directory adjacent to the package entry. Supply `workerUrl`, `moduleUrl`, and `wasmUrl` when a bundler or deployment uses another layout.
29
+
30
+ ## Minimal integration
31
+
32
+ ```ts
33
+ import {
34
+ BrowserRenderer,
35
+ type FontProvider,
36
+ type ResourceProvider,
37
+ } from "@empty-sekai/sekai-custom-profile-sdk";
38
+
39
+ const fontProvider: FontProvider = {
40
+ async provide({ region, family }, { signal }) {
41
+ const bytes = await loadApplicationFont({ region, family, signal });
42
+ return bytes ? { bytes } : null;
43
+ },
44
+ };
45
+
46
+ const resourceProvider: ResourceProvider = {
47
+ async provide(descriptor, { signal }) {
48
+ const request = await resolveResourceRequest(descriptor);
49
+ if (!request) return null;
50
+
51
+ const response = await fetch(request, { signal, cache: "default" });
52
+ if (!response.ok) return null;
53
+ return { source: await response.blob() };
54
+ },
55
+ };
56
+
57
+ const renderer = await BrowserRenderer.create({
58
+ canvas: document.querySelector("canvas")!,
59
+ region: "en",
60
+ resourceProvider,
61
+ fontProvider,
62
+ });
63
+
64
+ const masterData = await renderer.loadMasterData(
65
+ "latest",
66
+ ({ table, region, revision }, { signal }) =>
67
+ loadApplicationMasterData({ table, region, revision, signal }),
68
+ );
69
+ const scene = await renderer.createProfileScene({
70
+ masterData,
71
+ documentKey: "profile-preview",
72
+ card,
73
+ profile,
74
+ frameMode: "animate",
75
+ });
76
+
77
+ scene.draw();
78
+
79
+ // Always exports the native 1830×812 card size after drawing and copying the frame.
80
+ const png = await scene.exportPng();
81
+ ```
82
+
83
+ ## Custom-profile authoring documents
84
+
85
+ `BrowserAuthoringClient` owns a game-compatible editable document inside the dedicated worker. Importing a complete public Profile extracts only `userCustomProfileCards`; exports contain no worker handles, stable element IDs, selection state, or other browser metadata.
86
+
87
+ ```ts
88
+ import { BrowserAuthoringClient, type AuthoringCommand } from "@empty-sekai/sekai-custom-profile-sdk";
89
+
90
+ const authoring = await BrowserAuthoringClient.create();
91
+ const document = profile
92
+ ? await authoring.importProfile(profile)
93
+ : await authoring.createBlank();
94
+
95
+ const command: AuthoringCommand = {
96
+ kind: "set_transform",
97
+ id: selectedElementId,
98
+ position: [120, -40, 0],
99
+ };
100
+ const delta = await document.apply(command);
101
+
102
+ await document.undo();
103
+ await document.redo();
104
+ const gameDocument = await document.export();
105
+
106
+ await document.destroy();
107
+ authoring.destroy();
108
+ ```
109
+
110
+ The authoring core retains at most 150 history transactions and validates the 150-element page limit, all 12 game arrays, finite numbers, layer order, and the `objectData` boundary. Create, duplicate, delete, transform, lock, visibility, parameter, and layer commands return incremental changes; callers apply those changes to their existing scene and UI instead of maintaining a second TypeScript command history.
111
+
112
+ ## Complete scene flow
113
+
114
+ `createProfileScene()` performs the following work:
115
+
116
+ 1. The worker sends the card, profile, locale, and masterdata session to WASM.
117
+ 2. The shared semantic core collects scene-local localization demand, and the host provider returns an immutable text snapshot.
118
+ 3. WASM resolves authored elements and components with that snapshot and emits the font-family demand actually used by the scene.
119
+ 4. The main thread invokes the optional `FontProvider` through a bounded queue, hashes returned bytes, and fixes each family-to-hash mapping for the renderer lifetime. The host may instead call `registerFont()` before scene creation.
120
+ 5. WASM emits complete glyph demand, a layout request, and stable resource descriptors.
121
+ 6. The main thread deduplicates descriptors by stable ID and invokes the `ResourceProvider` through a bounded queue.
122
+ 7. WASM uses registered fonts for FreeType measurement, TMP layout, glyph SDF generation, and atlas placement.
123
+ 8. The core compiles the authored layer tree, semantic commands, control bindings, interaction regions, and initial dynamic state.
124
+ 9. WebGL2 uploads atlas pages, decoded images, geometry, command state, and layer-mask buffers.
125
+ 10. The host decides when to draw, advance the timeline, mutate layers, process interaction, or export a dump.
126
+
127
+ The glyph atlas is created from actual glyph demand. A missing resource, provider error, or image decode failure records a warning and uses a transparent placeholder while the remaining scene continues. Schema, ABI, memory-budget, and scene-contract violations fail explicitly.
128
+
129
+ ## ResourceProvider
130
+
131
+ The renderer produces and consumes semantic descriptors. The host maps those descriptors to URLs, CDNs, object storage, files, manifests, authenticated requests, or another resource system.
132
+
133
+ ```ts
134
+ export type ResourceDescriptor = {
135
+ id: string;
136
+ namespace: string;
137
+ key: string;
138
+ role: string;
139
+ provenance: Record<string, unknown>;
140
+ expectedSize?: { width: number; height: number };
141
+ };
142
+
143
+ export interface ResourceProvider {
144
+ provide(
145
+ descriptor: ResourceDescriptor,
146
+ context: { signal: AbortSignal },
147
+ ): Promise<{
148
+ source: Blob | ArrayBuffer | Uint8Array | TexImageSource;
149
+ } | null>;
150
+
151
+ cacheIdentity?(descriptor: ResourceDescriptor): string | null;
152
+ }
153
+ ```
154
+
155
+ `namespace`, `key`, and `role` are renderer semantics, not filesystem conventions. The host may interpret them in any way.
156
+
157
+ ### Arbitrary asynchronous sources
158
+
159
+ ```ts
160
+ const provider: ResourceProvider = {
161
+ cacheIdentity(descriptor) {
162
+ return `${assetRevision}:${descriptor.id}`;
163
+ },
164
+
165
+ async provide(descriptor, { signal }) {
166
+ const memoryHit = memoryImages.get(descriptor.id);
167
+ if (memoryHit) return { source: memoryHit };
168
+
169
+ const stored = await assetDatabase.get(assetRevision, descriptor.id);
170
+ if (stored) return { source: stored };
171
+
172
+ const request = await applicationAssetRouter.resolve(descriptor);
173
+ if (!request) return null;
174
+
175
+ const response = await authenticatedFetch(request, { signal });
176
+ if (!response.ok) return null;
177
+
178
+ const blob = await response.blob();
179
+ await assetDatabase.put(assetRevision, descriptor.id, blob);
180
+ return { source: blob };
181
+ },
182
+ };
183
+ ```
184
+
185
+ A provider may combine local files, HTTP caching, Cache Storage, IndexedDB, user-selected files, a Service Worker, signed URLs, GraphQL, authenticated APIs, or an already decoded `ImageBitmap`. The provider retains full authority over path interpretation and invalidation.
186
+
187
+ ### Concurrency, deduplication, and failures
188
+
189
+ - `resourceConcurrency` defaults to 8 and must be a positive integer.
190
+ - A batch is deduplicated by descriptor stable `id`.
191
+ - Concurrent scenes owned by the same renderer share in-flight singleflight.
192
+ - Each waiter responds independently to its own cancellation signal; the provider request is cancelled only after every waiter has left.
193
+ - Decoded-cache hits reuse session leases while provider concurrency remains available for actual loads.
194
+ - The provider receives an `AbortSignal`.
195
+ - A `null` result, thrown error, or decode failure becomes a warning and transparent placeholder.
196
+ - Exhausting the decoded-image hard budget with pinned leases produces an explicit resource error.
197
+
198
+ `cacheIdentity()` defines decoded session-cache identity. Include every catalog revision, region, user scope, or authentication domain that can change the bytes. The stable descriptor ID is used when the method is omitted. Persistent encoded-asset policy belongs entirely to the provider.
199
+
200
+ ## LocalizationProvider
201
+
202
+ Renderer-owned UI text such as General titles, tabs, and difficulty names is requested through stable localization keys. Player names, signatures, and other user content remain unchanged from the profile. The host may resolve `region + locale + key` through arbitrary logic:
203
+
204
+ ```ts
205
+ const renderer = await BrowserRenderer.create({
206
+ canvas,
207
+ region: "tw",
208
+ resourceProvider,
209
+ localizationProvider: {
210
+ async provide({ region, locale, key }, { signal }) {
211
+ return applicationMessages.resolve({ region, locale, key, signal });
212
+ },
213
+ },
214
+ localizationConcurrency: 8,
215
+ });
216
+ ```
217
+
218
+ WASM first collects the deduplicated keys used by the current scene. The main thread invokes the provider through a bounded queue and returns one complete immutable `key → UTF-8 value` snapshot to WASM. A missing required key fails scene creation explicitly without guessing across regions. The runtime uses its masterdata/runtime localization source when no `LocalizationProvider` is supplied.
219
+
220
+ ## Masterdata
221
+
222
+ `loadMasterData()` accepts an arbitrary asynchronous table loader supplied by the host. The host defines table naming, location, transport, authentication, and caching:
223
+
224
+ ```ts
225
+ const masterData = await renderer.loadMasterData(
226
+ "catalog-2026-07",
227
+ async ({ table, region, revision }, { signal }) => {
228
+ const request = await applicationMasterDataRouter.resolve({ table, region, revision });
229
+ return applicationMasterDataLoader.load(request, { signal });
230
+ },
231
+ { concurrency: 4 },
232
+ );
233
+ ```
234
+
235
+ Drive the masterdata session directly when the application needs per-table lifecycle control:
236
+
237
+ ```ts
238
+ const masterData = await renderer.createMasterData("catalog-2026-07");
239
+
240
+ for (const table of masterData.requiredTables) {
241
+ await masterData.putTable(table, await applicationMasterData.load(table));
242
+ }
243
+
244
+ await masterData.seal();
245
+ ```
246
+
247
+ A session must be sealed before scene creation and should be destroyed when no longer used.
248
+
249
+ ## Fonts
250
+
251
+ Font bytes are always host-provided. `family` is the opaque logical identity referenced by masterdata and text layers and is passed unchanged to the host's font-resolution policy.
252
+
253
+ The recommended integration supplies a `FontProvider` to `BrowserRenderer.create()`. WASM requests only families used by the current scene, and the main thread invokes the provider with bounded `fontConcurrency`, which defaults to 4:
254
+
255
+ ```ts
256
+ const renderer = await BrowserRenderer.create({
257
+ canvas,
258
+ region: "cn",
259
+ resourceProvider,
260
+ fontProvider: {
261
+ async provide({ region, family }, { signal }) {
262
+ const bytes = await applicationFonts.resolve({ region, family, signal });
263
+ return bytes ? { bytes } : null;
264
+ },
265
+ },
266
+ fontConcurrency: 4,
267
+ });
268
+ ```
269
+
270
+ The provider may use user-selected files, memory, IndexedDB, an application bundle, network requests, or any other implementation. Returned bytes are copied into an immutable snapshot for that resolution. Scene creation fails explicitly when a demanded family is unavailable.
271
+
272
+ Applications that want fully manual lifecycle control may register fonts before scene creation:
273
+
274
+ ```ts
275
+ const bytes = await file.arrayBuffer();
276
+
277
+ await renderer.registerFont({ family: "FOT-RodinNTLGPro-DB", bytes });
278
+ await renderer.registerFont({ family: "Application Alias", bytes });
279
+ ```
280
+
281
+ One file may be registered under multiple logical aliases. Glyph identity includes region, family, source hash, and character.
282
+
283
+ The first successful registration fixes a family to one source hash for the renderer lifetime. Re-registering identical bytes is idempotent; replacing the same family with different bytes returns `FONT_IDENTITY_CONFLICT`. Create a new renderer when switching font versions so font snapshots, glyph identities, atlases, and persistent-cache boundaries remain explicit.
284
+
285
+ ## Optional prebuilt atlas packages (0.3)
286
+
287
+ Scenes still generate SDF glyphs from their actual demand by default and reuse the origin IndexedDB glyph cache. Prebuilt atlases are never downloaded automatically. A host can use a local/HTTP provider directly, or install a complete atlas package into origin IndexedDB from an explicit user action so multiple renderers, editors, and viewers share one installation.
288
+
289
+ ```ts
290
+ import {
291
+ BrowserRenderer,
292
+ createHttpPrebuiltSdfAtlasProvider,
293
+ createOriginPrebuiltSdfAtlasPackage,
294
+ } from "@empty-sekai/sekai-custom-profile-sdk";
295
+
296
+ const source = createHttpPrebuiltSdfAtlasProvider("/font-atlases");
297
+ const atlasPackage = createOriginPrebuiltSdfAtlasPackage({
298
+ namespace: "cn-6.0.0-font-atlas-v1",
299
+ source,
300
+ });
301
+
302
+ // Call only from an explicit user action. Manifests become visible after every page is stored.
303
+ await atlasPackage.install([
304
+ "FZLanTingHei-DB-GBK",
305
+ "FZZhengHei-EB-GBK",
306
+ "FZShaoEr-M11-JF",
307
+ ], {
308
+ concurrency: 4,
309
+ requestPersistence: true,
310
+ onProgress(progress) {
311
+ updateDownloadProgress(progress.completedPages / progress.totalPages);
312
+ },
313
+ });
314
+
315
+ const renderer = await BrowserRenderer.create({
316
+ canvas,
317
+ region: "cn",
318
+ resourceProvider,
319
+ fontProvider,
320
+ prebuiltSdfAtlasProvider: atlasPackage.provider,
321
+ });
322
+ ```
323
+
324
+ `atlasPackage.provider` reads only fully installed families. Missing, in-progress, removed, or unavailable IndexedDB state returns a `null` manifest, so scene creation falls back to demand-driven glyph generation. Installation checks the browser-reported quota, verifies every page SHA-256, and removes incomplete target families after failure. `requestPersistence` defaults to false; the renderer never requests persistent storage without a user action. Atlas packages store no profile, text, user ID, layout, or scene dump.
325
+
326
+ When browser persistence is unnecessary, pass `createHttpPrebuiltSdfAtlasProvider()` directly to `BrowserRenderer.create()` and let the host's local files, Service Worker, or HTTP cache own the lifecycle.
327
+
328
+ ## Scene state
329
+
330
+ ```ts
331
+ await scene.advance(tick);
332
+ await scene.setLayerVisible(layerId, false);
333
+ await scene.setLayerMasks(layerTableRevision, [
334
+ { layerId, visible: true },
335
+ ]);
336
+ await scene.setTab(controlId, value);
337
+ await scene.setScrollOffset(controlId, offset);
338
+ await scene.scrollBy(controlId, delta);
339
+ scene.draw();
340
+ ```
341
+
342
+ These mutations reuse the authored layer table, timeline, layout, glyph atlas, and persistent glyph cache while updating WASM scene state and compact GPU buffers. Layer visibility changes preserve the current dynamic playback position.
343
+
344
+ Public layers correspond to game-authored elements. Shapes, glyphs, masks, and other draw primitives remain commands owned by those layers.
345
+
346
+ ## Dumps, layers, and interaction
347
+
348
+ ```ts
349
+ const dump = await scene.dump();
350
+ ```
351
+
352
+ The dump includes:
353
+
354
+ - stable layer, glyph, command, control, and region IDs;
355
+ - the layer table and parent tree;
356
+ - source content and resolved parameters;
357
+ - authored visibility, render masks, and dynamic state;
358
+ - bounds, quads, matrices, clips, and hit geometry;
359
+ - semantic commands and component controls;
360
+ - interaction regions and continuous numeric runs detected after TMP markup is removed.
361
+
362
+ Scroll components expose separate fixed viewport, offset-translated content, and proportional thumb regions. The host may handle wheel input over any of them and call `setScrollOffset()` from thumb dragging; the renderer owns clamping, state patches, and updated hit geometry, while pointer behavior remains host-owned.
363
+
364
+ The host implements product interaction using renderer-provided capabilities, resolved data, control bindings, and geometry for:
365
+
366
+ - honor, character, card, event, music, or story navigation;
367
+ - tab switching and scrolling;
368
+ - numeric-text copying;
369
+ - hover cards, selection, and editing;
370
+ - accessible DOM/SVG overlays.
371
+
372
+ Do not rebuild the layer-tree DOM on every animation tick. Keep stable layer rows and update only the canvas, overlay, and dynamic details that need to move.
373
+
374
+ ## Cache model
375
+
376
+ | Layer | Lifetime | Owner | Contents |
377
+ | --- | --- | --- | --- |
378
+ | provider persistent cache | application-defined | host | encoded images or application records |
379
+ | decoded image cache | renderer session | TypeScript runtime | bounded decoded `TexImageSource` leases |
380
+ | glyph persistent cache | browser origin | renderer | opaque, version-validated glyph records |
381
+ | glyph session atlas | worker session | WASM | atlas pages, placement, leases, and revisions |
382
+ | GPU texture/buffer | WebGL context | WebGL2 runtime | atlases, images, geometry, and state buffers |
383
+
384
+ `sdf.persistence` defaults to `"origin"` and uses IndexedDB. Use `"memory-only"` for session-only glyph caching. IndexedDB records contain opaque glyph identities, version data, metrics, and R8 SDF payloads.
385
+
386
+ ```ts
387
+ const scene = await renderer.createProfileScene({
388
+ masterData,
389
+ documentKey: "preview",
390
+ card,
391
+ sdf: { persistence: "memory-only" },
392
+ });
393
+ ```
394
+
395
+ WebGL context restoration reuploads retained atlas, image, and buffer state while reusing completed TMP parsing, layout, and SDF generation results.
396
+
397
+ ## Debugging and telemetry
398
+
399
+ ```ts
400
+ const renderer = await BrowserRenderer.create({
401
+ canvas,
402
+ region: "en",
403
+ resourceProvider,
404
+ telemetry: { level: "trace", maxSamples: 240 },
405
+ });
406
+
407
+ const sceneStats = scene.stats();
408
+ const rendererStats = await renderer.stats();
409
+ ```
410
+
411
+ Telemetry covers worker requests; font-provider requests, peak concurrency, bytes, failures, and elapsed time; localization-provider requests, resolved values, peak concurrency, failures, and elapsed time; resource-provider calls, peak concurrency, known encoded bytes, failures, cancellations, and cumulative resolution time, together with decoded session-cache entries, bytes, pins, hits, loads, and evictions. It also reports glyph generation, persistent-cache hits and misses, atlas pages, texture uploads, GPU buffers, frame timing, and context recovery. `summary` retains aggregates, `trace` retains raw samples within a fixed bound, and `off` exposes immediate runtime state.
412
+
413
+ Telemetry uses a privacy-safe schema containing runtime counts, timings, capacities, and recovery state. Unavailable or disjoint GPU timings use `null` to represent measurement state.
414
+
415
+ ## Lifecycle and cancellation
416
+
417
+ ```ts
418
+ const controller = new AbortController();
419
+
420
+ const pendingScene = renderer.createProfileScene({
421
+ masterData,
422
+ documentKey: "preview",
423
+ card,
424
+ signal: controller.signal,
425
+ });
426
+
427
+ controller.abort();
428
+
429
+ await pendingScene; // rejects if creation was still in progress
430
+
431
+ const scene = await renderer.createProfileScene({ masterData, documentKey: "preview", card });
432
+ await scene.destroy();
433
+ await masterData.destroy();
434
+ renderer.destroy();
435
+ ```
436
+
437
+ The signal only cancels scene creation while it is in progress; an already-created scene is released through `destroy()`. Destroying a scene releases image leases, atlas leases, GPU textures, and core scene state. `renderer.destroy()` terminates the worker and aborts resource acquisition still in progress.
438
+
439
+ ## Building from source
440
+
441
+ Use the repository container toolchain, which pins the FreeType, Emscripten, and Rust target environment.
442
+
443
+ ```sh
444
+ npm run build
445
+ npm run typecheck
446
+ npm run test:gates
447
+ npm run verify:wasm:runtime
448
+ npm run measure:wasm:size
449
+ npm run audit:public
450
+ ```
451
+
452
+ The FreeType build enables only the TrueType, CFF, SFNT, PS auxiliary/name, and smooth raster modules used by the runtime. CPU EDT is the production glyph SDF backend; the analytic backend is an explicit debug and parity option.
453
+
454
+ ## License
455
+
456
+ AGPL-3.0-only with the browser linking exception in `LICENSE-EXCEPTION`. The exception applies to unmodified browser use of the package. Modified renderer builds, server use, and non-browser use remain subject to the full AGPL, including its network-use source requirement.