@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
@@ -0,0 +1,641 @@
1
+ import { BrowserSemanticResourceManager, } from "./gpu/browserSemanticResources.js";
2
+ import { profileResourceDescriptors, } from "./resourceProvider.js";
3
+ import { LocalizationProviderManager, } from "./localizationProvider.js";
4
+ import { FontProviderManager, } from "./fontProvider.js";
5
+ import { semanticCommandPlanFromCoreSnapshot, } from "./gpu/semanticCommandPlanner.js";
6
+ import { SemanticWebglSceneRenderer } from "./gpu/semanticWebglSceneRenderer.js";
7
+ import { buildSdfAtlas, disposeWorkerAtlasSessions, } from "./fontSdfAtlas.js";
8
+ import { buildPrebuiltSdfAtlas, } from "./prebuiltSdfAtlas.js";
9
+ import { RendererWorkerClient, } from "./worker-client.js";
10
+ import { RendererRuntimeTelemetry, } from "./telemetry/rendererTelemetry.js";
11
+ const CARD_WIDTH = 1830;
12
+ const CARD_HEIGHT = 812;
13
+ /**
14
+ * WebGL2-only browser renderer. Rust/WASM scene, text layout, and glyph work
15
+ * stay in one worker; the main thread owns resource decoding and GPU state.
16
+ */
17
+ export class BrowserRenderer {
18
+ gl;
19
+ worker;
20
+ region;
21
+ prebuiltSdfAtlasProvider;
22
+ resources;
23
+ localizations;
24
+ providedFonts;
25
+ fonts = new Map();
26
+ fontRegistrations = new Map();
27
+ scenes = new Set();
28
+ canvas;
29
+ telemetryOptions;
30
+ contextLost = false;
31
+ restorePromise = null;
32
+ lifetime = new AbortController();
33
+ destroyed = false;
34
+ constructor(gl, worker, region, canvas, resourceProvider, resourceConcurrency, localizationProvider, localizationConcurrency, fontProvider, fontConcurrency, prebuiltSdfAtlasProvider, telemetryOptions = {}) {
35
+ this.gl = gl;
36
+ this.worker = worker;
37
+ this.region = region;
38
+ this.prebuiltSdfAtlasProvider = prebuiltSdfAtlasProvider;
39
+ this.canvas = canvas;
40
+ this.telemetryOptions = telemetryOptions;
41
+ this.resources = new BrowserSemanticResourceManager({
42
+ provider: resourceProvider,
43
+ concurrency: resourceConcurrency,
44
+ });
45
+ this.localizations = localizationProvider
46
+ ? new LocalizationProviderManager({
47
+ provider: localizationProvider,
48
+ concurrency: localizationConcurrency,
49
+ })
50
+ : null;
51
+ this.providedFonts = fontProvider
52
+ ? new FontProviderManager({
53
+ provider: fontProvider,
54
+ concurrency: fontConcurrency,
55
+ })
56
+ : null;
57
+ if (isHtmlCanvas(canvas)) {
58
+ canvas.addEventListener("webglcontextlost", this.handleContextLost);
59
+ canvas.addEventListener("webglcontextrestored", this.handleContextRestored);
60
+ }
61
+ }
62
+ static async create(options) {
63
+ const gl = options.canvas.getContext("webgl2", {
64
+ alpha: true,
65
+ antialias: true,
66
+ depth: false,
67
+ premultipliedAlpha: true,
68
+ preserveDrawingBuffer: false,
69
+ });
70
+ if (!gl)
71
+ throw new BrowserRendererError("WEBGL2_UNAVAILABLE", "WebGL2 is required by renderer-wasm 0.2");
72
+ options.canvas.width = CARD_WIDTH;
73
+ options.canvas.height = CARD_HEIGHT;
74
+ const worker = await RendererWorkerClient.create(options);
75
+ return new BrowserRenderer(gl, worker, options.region, options.canvas, options.resourceProvider, options.resourceConcurrency, options.localizationProvider, options.localizationConcurrency, options.fontProvider, options.fontConcurrency, options.prebuiltSdfAtlasProvider, options.telemetry);
76
+ }
77
+ async createProfileScene(options) {
78
+ this.assertAlive();
79
+ let resources = null;
80
+ let core = null;
81
+ let atlas = null;
82
+ const abort = combinedAbortSignal(this.lifetime.signal, options.signal);
83
+ try {
84
+ const locale = options.locale ?? this.region;
85
+ let localizedText;
86
+ if (this.localizations) {
87
+ const demandPreparation = await options.masterData.prepareProfile({
88
+ documentKey: options.documentKey,
89
+ card: options.card,
90
+ profile: options.profile,
91
+ locale,
92
+ demandOnly: true,
93
+ });
94
+ localizedText = await this.localizations.resolve(preparedLocalizationDemands(demandPreparation), abort.signal);
95
+ }
96
+ if (this.providedFonts) {
97
+ const fontPreparation = await options.masterData.prepareProfile({
98
+ documentKey: options.documentKey,
99
+ card: options.card,
100
+ profile: options.profile,
101
+ locale,
102
+ localizedText,
103
+ fontDemandOnly: true,
104
+ });
105
+ const missing = preparedFontDemands(fontPreparation, this.region)
106
+ .filter((request) => !this.fonts.has(request.family));
107
+ const provided = await this.providedFonts.resolve(missing, abort.signal);
108
+ await Promise.all([...provided].map(([family, bytes]) => this.registerFont({ family, bytes })));
109
+ }
110
+ const preparation = await options.masterData.prepareProfile({
111
+ documentKey: options.documentKey,
112
+ card: options.card,
113
+ profile: options.profile,
114
+ locale,
115
+ localizedText,
116
+ });
117
+ const glyphRequests = preparedGlyphRequests(preparation);
118
+ if (glyphRequests.length === 0) {
119
+ atlas = null;
120
+ }
121
+ else {
122
+ atlas = this.prebuiltSdfAtlasProvider
123
+ ? await buildPrebuiltSdfAtlas(this.prebuiltSdfAtlasProvider, glyphRequests, abort.signal)
124
+ : null;
125
+ atlas ??= await buildSdfAtlas(requiredFontSources(glyphRequests, this.fonts), glyphRequests, { worker: this.worker, persistence: options.sdf?.persistence ?? "origin" }, options.sdf?.supersample, options.sdf?.backend);
126
+ }
127
+ const requestedResources = profileResourceDescriptors(preparation);
128
+ let acquired;
129
+ acquired = await this.resources.acquire(requestedResources, abort.signal);
130
+ resources = acquired;
131
+ const compiled = await options.masterData.createProfileScene({
132
+ documentKey: options.documentKey,
133
+ card: options.card,
134
+ profile: options.profile,
135
+ locale,
136
+ localizedText,
137
+ frameMode: options.frameMode ?? "animate",
138
+ resourceMetrics: requestedResources.map((resource) => {
139
+ const image = acquired.sources.get(resource.id);
140
+ return {
141
+ namespace: resource.namespace,
142
+ key: resource.key,
143
+ width: image?.width ?? 0,
144
+ height: image?.height ?? 0,
145
+ available: acquired.availability.get(resource.id) ?? false,
146
+ };
147
+ }),
148
+ }, preparedLayoutRequest(preparation, atlas));
149
+ core = compiled.scene;
150
+ const plan = semanticCommandPlanFromCoreSnapshot(core.initial.snapshot);
151
+ assertPreparedResources(plan.resourceRequests(), acquired.sources);
152
+ const renderer = new SemanticWebglSceneRenderer(this.gl);
153
+ const bootstrap = await renderer.setScene({
154
+ plan,
155
+ atlas,
156
+ layout: compiled.layout,
157
+ imageSources: acquired.sources,
158
+ });
159
+ const scene = new BrowserScene(core, renderer, acquired, atlas, {
160
+ dynamicProgramCount: compiled.layout.dynamicPrograms.length,
161
+ canvas: this.canvas,
162
+ telemetry: this.telemetryOptions,
163
+ bootstrap,
164
+ onDestroy: (destroyed) => this.scenes.delete(destroyed),
165
+ });
166
+ this.scenes.add(scene);
167
+ return scene;
168
+ }
169
+ catch (error) {
170
+ atlas?.release();
171
+ resources?.release();
172
+ await core?.destroy().catch(() => undefined);
173
+ throw error;
174
+ }
175
+ finally {
176
+ abort.dispose();
177
+ }
178
+ }
179
+ async createMasterData(revision) {
180
+ this.assertAlive();
181
+ return this.worker.createMasterData(this.region, revision);
182
+ }
183
+ async loadMasterData(revision, loadTable, options = {}) {
184
+ if (typeof loadTable !== "function")
185
+ throw new BrowserRendererError("MASTERDATA_LOADER_REQUIRED", "A master-data table loader is required");
186
+ const session = await this.createMasterData(revision);
187
+ const signal = options.signal ?? new AbortController().signal;
188
+ const concurrency = options.concurrency ?? 4;
189
+ if (!Number.isInteger(concurrency) || concurrency <= 0) {
190
+ await session.destroy().catch(() => undefined);
191
+ throw new BrowserRendererError("INVALID_MASTERDATA_CONCURRENCY", "Master-data concurrency must be a positive integer");
192
+ }
193
+ try {
194
+ let cursor = 0;
195
+ const workers = Array.from({ length: Math.min(concurrency, session.requiredTables.length) }, async () => {
196
+ while (cursor < session.requiredTables.length) {
197
+ if (signal.aborted)
198
+ throw abortReason(signal);
199
+ const table = session.requiredTables[cursor++];
200
+ const value = await loadTable({ table, region: this.region, revision }, { signal });
201
+ await session.putTable(table, value);
202
+ }
203
+ });
204
+ await Promise.all(workers);
205
+ await session.seal();
206
+ return session;
207
+ }
208
+ catch (error) {
209
+ await session.destroy().catch(() => undefined);
210
+ throw error;
211
+ }
212
+ }
213
+ async registerFont(font) {
214
+ this.assertAlive();
215
+ if (typeof font.family !== "string" || font.family.length === 0) {
216
+ throw new BrowserRendererError("INVALID_FONT_FAMILY", "Font family must be a non-empty string");
217
+ }
218
+ if (!(font.bytes instanceof ArrayBuffer) || font.bytes.byteLength === 0) {
219
+ throw new BrowserRendererError("INVALID_FONT_BYTES", `Font bytes are empty: ${font.family}`);
220
+ }
221
+ const sourceHash = await sha256Hex(font.bytes);
222
+ const pending = this.fontRegistrations.get(font.family);
223
+ if (pending)
224
+ await pending;
225
+ const registered = this.fonts.get(font.family);
226
+ if (registered) {
227
+ if (registered.sourceHash === sourceHash)
228
+ return;
229
+ throw new BrowserRendererError("FONT_IDENTITY_CONFLICT", `Font family is already registered with different bytes: ${font.family}`);
230
+ }
231
+ const source = { region: this.region, family: font.family, sourceHash };
232
+ const registration = this.worker.registerFont({ ...source, bytes: font.bytes })
233
+ .then(() => { this.fonts.set(font.family, source); });
234
+ this.fontRegistrations.set(font.family, registration);
235
+ try {
236
+ await registration;
237
+ }
238
+ finally {
239
+ this.fontRegistrations.delete(font.family);
240
+ }
241
+ }
242
+ async stats() {
243
+ if (this.destroyed)
244
+ throw new BrowserRendererError("RENDERER_DESTROYED", "Browser renderer is destroyed");
245
+ return {
246
+ worker: await this.worker.stats(),
247
+ resources: this.resources.stats(),
248
+ fonts: {
249
+ registered: this.fonts.size,
250
+ provider: this.providedFonts?.stats() ?? null,
251
+ },
252
+ localizations: this.localizations?.stats() ?? null,
253
+ scenes: {
254
+ active: this.scenes.size,
255
+ contextLost: [...this.scenes].filter((scene) => scene.stats().state === "context-lost").length,
256
+ },
257
+ };
258
+ }
259
+ async restoreContext() {
260
+ if (this.destroyed)
261
+ throw new BrowserRendererError("RENDERER_DESTROYED", "Browser renderer is destroyed");
262
+ if (this.restorePromise)
263
+ return this.restorePromise;
264
+ this.restorePromise = (async () => {
265
+ const started = monotonicNow();
266
+ const gl = this.canvas.getContext("webgl2", {
267
+ alpha: true,
268
+ antialias: true,
269
+ depth: false,
270
+ premultipliedAlpha: true,
271
+ preserveDrawingBuffer: false,
272
+ });
273
+ if (!gl) {
274
+ const restoreMs = monotonicNow() - started;
275
+ for (const scene of this.scenes)
276
+ scene.notifyContextRestoreFailed(restoreMs);
277
+ throw new BrowserRendererError("WEBGL2_RESTORE_FAILED", "WebGL2 context restoration failed");
278
+ }
279
+ this.gl = gl;
280
+ await Promise.all([...this.scenes].map((scene) => scene.restoreContext(gl)));
281
+ this.contextLost = false;
282
+ })();
283
+ try {
284
+ await this.restorePromise;
285
+ }
286
+ finally {
287
+ this.restorePromise = null;
288
+ }
289
+ }
290
+ destroy() {
291
+ if (this.destroyed)
292
+ return;
293
+ this.destroyed = true;
294
+ this.lifetime.abort(new BrowserRendererError("RENDERER_DESTROYED", "Browser renderer is destroyed"));
295
+ if (isHtmlCanvas(this.canvas)) {
296
+ this.canvas.removeEventListener("webglcontextlost", this.handleContextLost);
297
+ this.canvas.removeEventListener("webglcontextrestored", this.handleContextRestored);
298
+ }
299
+ disposeWorkerAtlasSessions(this.worker);
300
+ this.worker.terminate();
301
+ }
302
+ assertAlive() {
303
+ if (this.destroyed)
304
+ throw new BrowserRendererError("RENDERER_DESTROYED", "Browser renderer is destroyed");
305
+ if (this.contextLost)
306
+ throw new BrowserRendererError("WEBGL_CONTEXT_LOST", "WebGL context is lost");
307
+ }
308
+ handleContextLost = (event) => {
309
+ event.preventDefault();
310
+ this.contextLost = true;
311
+ for (const scene of this.scenes)
312
+ scene.notifyContextLost();
313
+ };
314
+ handleContextRestored = () => {
315
+ void this.restoreContext().catch(() => {
316
+ // Scene recovery counters retain the failure. Call restoreContext() explicitly
317
+ // when the host needs an actionable rejection.
318
+ });
319
+ };
320
+ }
321
+ function atlasLayoutInput(atlas) {
322
+ return {
323
+ baseSize: atlas.baseSize,
324
+ spread: atlas.spread,
325
+ glyphs: Array.from(atlas.glyphs.values()),
326
+ };
327
+ }
328
+ function preparedGlyphRequests(preparation) {
329
+ const demand = preparation.glyph_demand;
330
+ if (!demand || typeof demand !== "object" || Array.isArray(demand)) {
331
+ throw new BrowserRendererError("INVALID_PROFILE_PREPARATION", "Profile preparation did not return glyph demand");
332
+ }
333
+ const requests = demand.requests;
334
+ if (!Array.isArray(requests)) {
335
+ throw new BrowserRendererError("INVALID_PROFILE_PREPARATION", "Profile glyph demand is invalid");
336
+ }
337
+ return requests.map((entry) => {
338
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
339
+ throw new BrowserRendererError("INVALID_PROFILE_PREPARATION", "Profile glyph request is invalid");
340
+ }
341
+ const request = entry;
342
+ if (typeof request.region !== "string"
343
+ || typeof request.family !== "string"
344
+ || typeof request.font_source_hash !== "string"
345
+ || typeof request.char !== "string") {
346
+ throw new BrowserRendererError("INVALID_PROFILE_PREPARATION", "Profile glyph identity is invalid");
347
+ }
348
+ return {
349
+ region: request.region,
350
+ family: request.family,
351
+ fontSourceHash: request.font_source_hash,
352
+ char: request.char,
353
+ };
354
+ });
355
+ }
356
+ function preparedFontDemands(preparation, region) {
357
+ const values = preparation.fontDemands;
358
+ if (!Array.isArray(values)) {
359
+ throw new BrowserRendererError("INVALID_PROFILE_PREPARATION", "Profile preparation did not return font demands");
360
+ }
361
+ return values.map((family, index) => {
362
+ if (typeof family !== "string" || family.length === 0) {
363
+ throw new BrowserRendererError("INVALID_PROFILE_PREPARATION", `Profile font demand is invalid at index ${index}`);
364
+ }
365
+ return { region, family };
366
+ });
367
+ }
368
+ function preparedLayoutRequest(preparation, atlas) {
369
+ const request = preparation.layout_request;
370
+ if (!request || typeof request !== "object" || Array.isArray(request)) {
371
+ throw new BrowserRendererError("INVALID_PROFILE_PREPARATION", "Profile preparation did not return an authored layout request");
372
+ }
373
+ if (!atlas) {
374
+ const layers = request.layers;
375
+ if (!Array.isArray(layers) || layers.length !== 0) {
376
+ throw new BrowserRendererError("INVALID_PROFILE_PREPARATION", "Profile preparation returned text layers without glyph demand");
377
+ }
378
+ return {
379
+ ...request,
380
+ atlas: { baseSize: 1, spread: 0, glyphs: [] },
381
+ };
382
+ }
383
+ return {
384
+ ...request,
385
+ atlas: atlasLayoutInput(atlas),
386
+ };
387
+ }
388
+ function requiredFontSources(requests, fonts) {
389
+ const output = new Map();
390
+ for (const request of requests) {
391
+ const source = fonts.get(request.family);
392
+ if (!source || source.sourceHash !== request.fontSourceHash) {
393
+ throw new BrowserRendererError("FONT_NOT_REGISTERED", `Required font is not registered: ${request.family}`);
394
+ }
395
+ output.set(`${source.region}\0${source.sourceHash}\0${source.family}`, source);
396
+ }
397
+ return [...output.values()];
398
+ }
399
+ function assertPreparedResources(requested, sources) {
400
+ for (const resource of requested) {
401
+ if (!sources.has(`${resource.namespace}\0${resource.key}`)) {
402
+ throw new BrowserRendererError("RESOURCE_PREPARATION_DRIFT", `Scene requested an unprepared resource: ${resource.namespace}/${resource.key}`);
403
+ }
404
+ }
405
+ }
406
+ function preparedLocalizationDemands(preparation) {
407
+ const raw = preparation.localizationDemands;
408
+ if (!Array.isArray(raw))
409
+ return [];
410
+ return raw.map((value, index) => {
411
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
412
+ throw new BrowserRendererError("INVALID_LOCALIZATION_DEMAND", `Invalid localization demand at index ${index}`);
413
+ }
414
+ const { region, locale, key } = value;
415
+ if (typeof region !== "string" || typeof locale !== "string" || typeof key !== "string") {
416
+ throw new BrowserRendererError("INVALID_LOCALIZATION_DEMAND", `Invalid localization demand at index ${index}`);
417
+ }
418
+ return { region, locale, key };
419
+ });
420
+ }
421
+ function combinedAbortSignal(lifetime, request) {
422
+ if (!request || lifetime.aborted || request.aborted) {
423
+ return { signal: request?.aborted ? request : lifetime, dispose() { } };
424
+ }
425
+ const controller = new AbortController();
426
+ const abortLifetime = () => controller.abort(lifetime.reason);
427
+ const abortRequest = () => controller.abort(request.reason);
428
+ lifetime.addEventListener("abort", abortLifetime, { once: true });
429
+ request.addEventListener("abort", abortRequest, { once: true });
430
+ return {
431
+ signal: controller.signal,
432
+ dispose() {
433
+ lifetime.removeEventListener("abort", abortLifetime);
434
+ request.removeEventListener("abort", abortRequest);
435
+ },
436
+ };
437
+ }
438
+ function abortReason(signal) {
439
+ return signal.reason ?? new DOMException("The operation was aborted", "AbortError");
440
+ }
441
+ export class BrowserScene {
442
+ core;
443
+ renderer;
444
+ resources;
445
+ atlas;
446
+ destroyed = false;
447
+ runtime;
448
+ onDestroy;
449
+ constructor(core, renderer, resources, atlas, options) {
450
+ this.core = core;
451
+ this.renderer = renderer;
452
+ this.resources = resources;
453
+ this.atlas = atlas;
454
+ this.dynamicProgramCount = options.dynamicProgramCount;
455
+ this.animated = this.dynamicProgramCount > 0;
456
+ this.canvas = options.canvas;
457
+ this.runtime = new RendererRuntimeTelemetry(options.telemetry ?? {}, atlasSummary(atlas), options.bootstrap);
458
+ this.onDestroy = options.onDestroy;
459
+ }
460
+ /** Whether this scene owns renderer-driven programs that require timeline advancement. */
461
+ animated;
462
+ /** Number of renderer-compiled dynamic programs retained by this scene. */
463
+ dynamicProgramCount;
464
+ canvas;
465
+ draw() {
466
+ this.assertAlive();
467
+ this.assertGpuReady();
468
+ const started = monotonicNow();
469
+ const metrics = this.renderer.draw();
470
+ this.runtime.recordDraw(metrics, monotonicNow() - started);
471
+ return metrics;
472
+ }
473
+ /** Draws the current frame and encodes an exact 1830×812 PNG snapshot. */
474
+ async exportPng() {
475
+ this.assertAlive();
476
+ this.draw();
477
+ if (typeof document !== "undefined") {
478
+ const snapshot = document.createElement("canvas");
479
+ snapshot.width = CARD_WIDTH;
480
+ snapshot.height = CARD_HEIGHT;
481
+ const context = snapshot.getContext("2d");
482
+ if (!context)
483
+ throw new BrowserRendererError("PNG_EXPORT_UNAVAILABLE", "Canvas 2D is required for PNG export");
484
+ context.drawImage(this.canvas, 0, 0, CARD_WIDTH, CARD_HEIGHT);
485
+ return new Promise((resolve, reject) => {
486
+ snapshot.toBlob((blob) => blob
487
+ ? resolve(blob)
488
+ : reject(new BrowserRendererError("PNG_EXPORT_FAILED", "Canvas PNG encoding returned no data")), "image/png");
489
+ });
490
+ }
491
+ if (typeof OffscreenCanvas === "undefined") {
492
+ throw new BrowserRendererError("PNG_EXPORT_UNAVAILABLE", "OffscreenCanvas is required outside a document");
493
+ }
494
+ const snapshot = new OffscreenCanvas(CARD_WIDTH, CARD_HEIGHT);
495
+ const context = snapshot.getContext("2d");
496
+ if (!context)
497
+ throw new BrowserRendererError("PNG_EXPORT_UNAVAILABLE", "Canvas 2D is required for PNG export");
498
+ context.drawImage(this.canvas, 0, 0, CARD_WIDTH, CARD_HEIGHT);
499
+ return snapshot.convertToBlob({ type: "image/png" });
500
+ }
501
+ async advance(tick) { this.assertGpuReady(); return this.apply(await this.core.advance(tick)); }
502
+ async setLayerVisible(layerId, visible) { this.assertGpuReady(); return this.apply(await this.core.setLayerVisible(layerId, visible)); }
503
+ async setLayerMasks(expectedRevision, overrides) {
504
+ this.assertGpuReady();
505
+ return this.apply(await this.core.setLayerMasks(expectedRevision, overrides));
506
+ }
507
+ async setTab(controlId, value) { this.assertGpuReady(); return this.apply(await this.core.setTab(controlId, value)); }
508
+ async setScrollOffset(controlId, offset) { this.assertGpuReady(); return this.apply(await this.core.setScrollOffset(controlId, offset)); }
509
+ async scrollBy(controlId, delta) { this.assertGpuReady(); return this.apply(await this.core.scrollBy(controlId, delta)); }
510
+ setLayerPreviewTransform(layerId, matrix) {
511
+ this.assertGpuReady();
512
+ const gpu = this.renderer.setLayerPreviewTransform(layerId, matrix);
513
+ const draw = this.draw();
514
+ return { gpu, draw };
515
+ }
516
+ async dump() {
517
+ this.assertAlive();
518
+ const dump = await this.core.dump();
519
+ const textGeometry = this.renderer.authoredTextHitGeometry();
520
+ const interactionRegions = dump.interaction_regions.map((region) => {
521
+ const geometry = region.role === "primary" ? textGeometry.get(region.layer_id) : undefined;
522
+ return geometry ? {
523
+ ...region,
524
+ hit_geometry: geometry.quad,
525
+ } : region;
526
+ });
527
+ return {
528
+ ...dump,
529
+ interaction_regions: interactionRegions,
530
+ numeric_text_regions: this.renderer.interactionRegions(),
531
+ };
532
+ }
533
+ async destroy() {
534
+ if (this.destroyed)
535
+ return;
536
+ this.destroyed = true;
537
+ this.renderer.destroy();
538
+ this.resources.release();
539
+ await this.atlas?.release();
540
+ await this.core.destroy();
541
+ this.runtime.markDestroyed();
542
+ this.onDestroy?.(this);
543
+ }
544
+ notifyContextLost() {
545
+ if (this.destroyed)
546
+ return;
547
+ this.renderer.notifyContextLost();
548
+ this.runtime.markContextLost();
549
+ }
550
+ notifyContextRestoreFailed(restoreMs) {
551
+ if (this.destroyed)
552
+ return;
553
+ this.runtime.markRestoreFailed(restoreMs);
554
+ }
555
+ async restoreContext(gl) {
556
+ this.assertAlive();
557
+ const started = monotonicNow();
558
+ try {
559
+ const uploads = await this.renderer.restoreContext(gl);
560
+ this.runtime.markContextRestored(monotonicNow() - started, uploads);
561
+ }
562
+ catch (error) {
563
+ this.runtime.markRestoreFailed(monotonicNow() - started);
564
+ throw error;
565
+ }
566
+ }
567
+ stats() {
568
+ return this.runtime.snapshot();
569
+ }
570
+ apply(delta) {
571
+ this.assertAlive();
572
+ const gpu = this.renderer.applyCoreDelta(delta);
573
+ this.runtime.recordPatch(gpu);
574
+ return { delta, gpu };
575
+ }
576
+ assertAlive() {
577
+ if (this.destroyed)
578
+ throw new BrowserRendererError("SCENE_DESTROYED", "Browser scene is destroyed");
579
+ }
580
+ assertGpuReady() {
581
+ this.assertAlive();
582
+ if (this.runtime.state() === "context-lost") {
583
+ throw new BrowserRendererError("WEBGL_CONTEXT_LOST", "WebGL context is lost");
584
+ }
585
+ }
586
+ }
587
+ export class BrowserRendererError extends Error {
588
+ code;
589
+ constructor(code, message) {
590
+ super(message);
591
+ this.code = code;
592
+ this.name = "BrowserRendererError";
593
+ }
594
+ }
595
+ function atlasSummary(atlas) {
596
+ if (!atlas) {
597
+ return {
598
+ backend: "edt",
599
+ pages: 0,
600
+ glyphs: 0,
601
+ missingGlyphs: 0,
602
+ generation: { glyphs: 0, pixels: 0, glyphMs: 0, faceLoadMs: 0 },
603
+ cache: null,
604
+ };
605
+ }
606
+ const cache = atlas.perf.cache;
607
+ return {
608
+ backend: atlas.sdfBackend,
609
+ pages: atlas.depth,
610
+ glyphs: atlas.glyphs.size,
611
+ missingGlyphs: atlas.missing.length,
612
+ generation: {
613
+ glyphs: atlas.perf.totalGlyphCount,
614
+ pixels: atlas.perf.totalPixelCount,
615
+ glyphMs: atlas.perf.totalGlyphMs,
616
+ faceLoadMs: atlas.perf.totalFaceLoadMs,
617
+ },
618
+ cache: cache ? {
619
+ hits: cache.hits,
620
+ misses: cache.misses,
621
+ generations: cache.generations,
622
+ bytes: cache.bytes,
623
+ sessionHits: cache.sessionHits,
624
+ persistentHits: cache.persistentHits,
625
+ persistentMisses: cache.persistentMisses,
626
+ persistentWritesQueued: cache.persistentWritesQueued,
627
+ pinnedPages: cache.pinnedPages,
628
+ pageEvictions: cache.pageEvictions,
629
+ } : null,
630
+ };
631
+ }
632
+ function monotonicNow() {
633
+ return typeof performance === "undefined" ? Date.now() : performance.now();
634
+ }
635
+ async function sha256Hex(bytes) {
636
+ const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
637
+ return Array.from(digest, (value) => value.toString(16).padStart(2, "0")).join("");
638
+ }
639
+ function isHtmlCanvas(canvas) {
640
+ return typeof HTMLCanvasElement !== "undefined" && canvas instanceof HTMLCanvasElement;
641
+ }
@@ -0,0 +1,24 @@
1
+ export type ResourceDescriptor = {
2
+ id: string;
3
+ namespace: string;
4
+ key: string;
5
+ role: string;
6
+ provenance: Record<string, unknown>;
7
+ expectedSize?: {
8
+ width: number;
9
+ height: number;
10
+ };
11
+ };
12
+ export type ResourceSource = Blob | ArrayBuffer | Uint8Array | TexImageSource;
13
+ export type ProvidedResource = {
14
+ source: ResourceSource;
15
+ };
16
+ export type ResourceContext = {
17
+ signal: AbortSignal;
18
+ };
19
+ /** Resolves a semantic renderer resource through arbitrary caller-owned logic. */
20
+ export interface ResourceProvider {
21
+ provide(descriptor: ResourceDescriptor, context: ResourceContext): Promise<ProvidedResource | null>;
22
+ cacheIdentity?(descriptor: ResourceDescriptor): string | null;
23
+ }
24
+ export declare function profileResourceDescriptors(preparation: Record<string, unknown>): ResourceDescriptor[];