@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,296 @@
1
+ import { GlyphPersistentCache, createPersistentGlyphRecord, } from "./cache/glyphPersistentCache.js";
2
+ import { createOriginGlyphPersistentCache } from "./cache/indexedDbGlyphRecordStore.js";
3
+ const engines = new WeakMap();
4
+ const engineRegistry = new Map();
5
+ let originPersistentCache = null;
6
+ export function glyphKey(region, fontSourceHash, family, char) {
7
+ return `${region}\u0000${fontSourceHash}\u0000${family}\u0000${char}`;
8
+ }
9
+ export async function buildSdfAtlas(fontSources, requests, options, supersample, backend) {
10
+ validateFontSources(fontSources);
11
+ const sdfBackend = backend ?? (supersample != null && supersample <= 0 ? "analytic" : "edt");
12
+ const persistent = options.persistentCache ?? (options.persistence === "origin" ? getOriginPersistentCache() : new GlyphPersistentCache());
13
+ const uniqueRequests = deduplicateRequests(requests);
14
+ const plan = await planGlyphs(options.worker, fontSources, uniqueRequests, sdfBackend, supersample ?? 2);
15
+ const engine = await getEngine(options.worker, plan.contractId);
16
+ const planned = new Map(plan.glyphs.map((glyph) => [
17
+ glyphKey(glyph.region, glyph.fontSourceHash, glyph.family, glyph.ch),
18
+ glyph,
19
+ ]));
20
+ for (const request of uniqueRequests) {
21
+ const glyph = planned.get(requestKey(request));
22
+ if (glyph)
23
+ engine.charToGlyphIndex.set(requestKey(request), glyph.glyphIndex);
24
+ else
25
+ engine.missingChars.add(requestKey(request));
26
+ }
27
+ const resolved = uniqueRequests.flatMap((request) => {
28
+ const glyph = planned.get(requestKey(request));
29
+ return glyph ? [{ request, glyphIndex: glyph.glyphIndex, identity: glyph.identity }] : [];
30
+ });
31
+ const unique = new Map(resolved.map((value) => [value.identity.opaqueKey, value]));
32
+ const placementsBefore = new Map([...unique.keys()].flatMap((key) => {
33
+ const placement = engine.placements.get(key);
34
+ return placement ? [[key, placement]] : [];
35
+ }));
36
+ const identities = [...unique.values()].map((value) => value.identity);
37
+ const persisted = await persistent.getMany(identities);
38
+ const cached = [...persisted.values()].map(recordToAtlas);
39
+ for (const record of cached)
40
+ engine.metrics.set(record.key, withoutPixels(record));
41
+ const missing = [...unique.values()].filter((value) => !persisted.has(value.identity.opaqueKey));
42
+ const generate = generationGroups(fontSources, missing, plan.backend, plan.supersample);
43
+ const workerBefore = await options.worker.stats();
44
+ const result = await engine.atlas.resolve([...unique.keys()], cached, generate);
45
+ engine.latestStats = result.stats;
46
+ const placementByKey = new Map(result.placements.map((entry) => [entry.key, entry.placement]));
47
+ for (const [key, placement] of placementByKey) {
48
+ engine.placements.set(key, { page: placement.page, pageEpoch: placement.pageEpoch });
49
+ }
50
+ const sessionHitKeys = new Set([...unique.keys()].filter((key) => {
51
+ const before = placementsBefore.get(key);
52
+ const after = placementByKey.get(key);
53
+ return before != null && after != null && before.page === after.page && before.pageEpoch === after.pageEpoch;
54
+ }));
55
+ const sessionHits = sessionHitKeys.size;
56
+ const persistentHits = [...persisted.keys()].filter((key) => !sessionHitKeys.has(key)).length;
57
+ for (const record of result.generated)
58
+ engine.metrics.set(record.key, withoutPixels(record));
59
+ if (result.generated.length > 0) {
60
+ const writeEpoch = persistent.beginWrite();
61
+ const records = await Promise.all(result.generated.map((record) => {
62
+ const identity = unique.get(record.key)?.identity;
63
+ if (!identity)
64
+ throw new Error(`generated atlas resource has unknown identity ${record.key}`);
65
+ return createPersistentGlyphRecord(identity, record);
66
+ }));
67
+ void persistent.putMany(records, writeEpoch).catch(() => undefined);
68
+ }
69
+ const glyphs = new Map();
70
+ for (const value of resolved) {
71
+ const resourceId = value.identity.opaqueKey;
72
+ const placement = placementByKey.get(resourceId);
73
+ const metrics = engine.metrics.get(resourceId);
74
+ if (!placement || !metrics)
75
+ throw new Error(`WASM atlas omitted glyph resource ${resourceId}`);
76
+ glyphs.set(requestKey(value.request), {
77
+ key: requestKey(value.request),
78
+ resourceId,
79
+ glyphIndex: value.glyphIndex,
80
+ page: placement.page,
81
+ pageEpoch: placement.pageEpoch,
82
+ region: value.request.region,
83
+ family: value.request.family,
84
+ fontSourceHash: value.request.fontSourceHash,
85
+ char: value.request.char,
86
+ u0: placement.u0,
87
+ v0: placement.v0,
88
+ u1: placement.u1,
89
+ v1: placement.v1,
90
+ width: metrics.width,
91
+ height: metrics.height,
92
+ advance: metrics.advance,
93
+ xOffset: metrics.xOffset,
94
+ yOffset: metrics.yOffset,
95
+ planeBearingX: metrics.planeBearingX,
96
+ planeBearingY: metrics.planeBearingY,
97
+ planeWidth: metrics.planeWidth,
98
+ planeHeight: metrics.planeHeight,
99
+ drawable: metrics.drawable,
100
+ });
101
+ }
102
+ const workerAfter = await options.worker.stats();
103
+ let released = false;
104
+ return {
105
+ width: plan.atlasWidth,
106
+ height: plan.atlasHeight,
107
+ depth: result.stats.pages,
108
+ baseSize: plan.baseSize,
109
+ spread: plan.spread,
110
+ glyphs,
111
+ missing: uniqueRequests.filter((request) => engine.missingChars.has(requestKey(request))).map(requestKey),
112
+ backend: "freetype-wasm",
113
+ sdfBackend: plan.backend,
114
+ contractId: plan.contractId,
115
+ fontEngineFingerprint: plan.fontEngineFingerprint,
116
+ perf: {
117
+ backend: sdfBackend,
118
+ fontBatches: [],
119
+ totalGlyphMs: Math.max(0, workerAfter.wasmMs - workerBefore.wasmMs),
120
+ totalFaceLoadMs: 0,
121
+ totalGlyphCount: result.generated.length,
122
+ totalPixelCount: result.generated.reduce((sum, record) => sum + record.pixels.byteLength, 0),
123
+ worker: workerAfter,
124
+ cache: {
125
+ hits: sessionHits + persistentHits,
126
+ misses: unique.size - sessionHits - persistentHits,
127
+ generations: result.generated.length,
128
+ bytes: result.stats.atlasBytes,
129
+ sessionHits,
130
+ persistentHits,
131
+ persistentMisses: unique.size - sessionHits - persistentHits,
132
+ persistentWritesQueued: result.generated.length,
133
+ pages: result.stats.pages,
134
+ pinnedPages: result.stats.pinnedPages,
135
+ pageEvictions: result.stats.evictions,
136
+ },
137
+ },
138
+ pageUpdates: (revisions) => engine.atlas.pages(revisions),
139
+ release: async () => {
140
+ if (released)
141
+ return;
142
+ released = true;
143
+ await Promise.all(result.leases.map((lease) => engine.atlas.release(lease)));
144
+ },
145
+ };
146
+ }
147
+ export function getPersistentGlyphCache() {
148
+ return getOriginPersistentCache();
149
+ }
150
+ export function getSessionAtlasStats() {
151
+ return [...engineRegistry.values()].flatMap((sessions) => [...sessions]
152
+ .map((engine) => ({ contractId: engine.contractId, stats: engine.latestStats })));
153
+ }
154
+ export function disposeWorkerAtlasSessions(worker) {
155
+ engineRegistry.delete(worker);
156
+ engines.delete(worker);
157
+ }
158
+ async function getEngine(worker, contractId) {
159
+ let byContract = engines.get(worker);
160
+ if (!byContract) {
161
+ byContract = new Map();
162
+ engines.set(worker, byContract);
163
+ }
164
+ let pending = byContract.get(contractId);
165
+ if (!pending) {
166
+ pending = worker.createAtlas().then((atlas) => {
167
+ const engine = {
168
+ atlas,
169
+ contractId,
170
+ charToGlyphIndex: new Map(),
171
+ missingChars: new Set(),
172
+ metrics: new Map(),
173
+ placements: new Map(),
174
+ latestStats: atlas.initialStats,
175
+ };
176
+ let registered = engineRegistry.get(worker);
177
+ if (!registered) {
178
+ registered = new Set();
179
+ engineRegistry.set(worker, registered);
180
+ }
181
+ registered.add(engine);
182
+ return engine;
183
+ });
184
+ byContract.set(contractId, pending);
185
+ }
186
+ return pending;
187
+ }
188
+ async function planGlyphs(worker, fonts, requests, backend, supersample) {
189
+ const groups = new Map();
190
+ for (const font of fonts)
191
+ groups.set(fontSourceKey(font), { font, chars: [], seen: new Set() });
192
+ for (const request of requests) {
193
+ const group = groups.get(fontRequestKey(request));
194
+ if (!group || group.seen.has(request.char))
195
+ continue;
196
+ group.seen.add(request.char);
197
+ group.chars.push(request.char);
198
+ }
199
+ const activeGroups = [...groups.values()].filter((group) => group.chars.length > 0);
200
+ if (activeGroups.length === 0 && fonts[0])
201
+ activeGroups.push({ font: fonts[0], chars: [], seen: new Set() });
202
+ const plans = await Promise.all(activeGroups.map((group) => worker.planGlyphs({
203
+ region: group.font.region,
204
+ family: group.font.family,
205
+ sourceHash: group.font.sourceHash,
206
+ chars: group.chars,
207
+ backend,
208
+ supersample,
209
+ })));
210
+ const first = plans[0];
211
+ if (!first)
212
+ throw new Error("WASM glyph raster plan requires at least one glyph request");
213
+ for (const plan of plans) {
214
+ if (plan.contractId !== first.contractId
215
+ || plan.baseSize !== first.baseSize
216
+ || plan.spread !== first.spread
217
+ || plan.atlasWidth !== first.atlasWidth
218
+ || plan.atlasHeight !== first.atlasHeight)
219
+ throw new Error("WASM returned incompatible glyph raster plans for one atlas");
220
+ }
221
+ return {
222
+ ...first,
223
+ glyphs: plans.flatMap((plan) => plan.glyphs.map((glyph) => ({
224
+ ...glyph,
225
+ region: plan.region,
226
+ family: plan.family,
227
+ fontSourceHash: plan.fontSourceHash,
228
+ }))),
229
+ };
230
+ }
231
+ function generationGroups(fonts, missing, backend, supersample) {
232
+ const groups = new Map();
233
+ for (const value of missing) {
234
+ const font = fonts.find((candidate) => fontSourceKey(candidate) === fontRequestKey(value.request));
235
+ if (!font)
236
+ throw new Error(`font source missing for ${value.request.region}/${value.request.family}`);
237
+ const key = fontSourceKey(font);
238
+ let group = groups.get(key);
239
+ if (!group) {
240
+ group = { region: font.region, family: font.family, sourceHash: font.sourceHash, chars: [], backend, supersample, glyphs: [] };
241
+ groups.set(key, group);
242
+ }
243
+ group.chars.push(value.request.char);
244
+ group.glyphs.push({ key: value.identity.opaqueKey, char: value.request.char, glyphIndex: value.glyphIndex });
245
+ }
246
+ return [...groups.values()];
247
+ }
248
+ function recordToAtlas(record) {
249
+ return {
250
+ key: record.opaqueKey,
251
+ glyphIndex: 0,
252
+ width: record.width,
253
+ height: record.height,
254
+ advance: record.advance,
255
+ xOffset: record.xOffset,
256
+ yOffset: record.yOffset,
257
+ planeBearingX: record.planeBearingX,
258
+ planeBearingY: record.planeBearingY,
259
+ planeWidth: record.planeWidth,
260
+ planeHeight: record.planeHeight,
261
+ drawable: record.drawable,
262
+ pixels: new Uint8Array(record.pixels),
263
+ };
264
+ }
265
+ function withoutPixels(record) {
266
+ const { pixels: _, ...metrics } = record;
267
+ return metrics;
268
+ }
269
+ function getOriginPersistentCache() {
270
+ originPersistentCache ??= typeof indexedDB === "undefined" ? new GlyphPersistentCache() : createOriginGlyphPersistentCache();
271
+ return originPersistentCache;
272
+ }
273
+ function validateFontSources(fonts) {
274
+ for (const font of fonts) {
275
+ if (!font.region || !font.family)
276
+ throw new Error("font region/family must be non-empty");
277
+ if (!/^[0-9a-f]{64}$/i.test(font.sourceHash))
278
+ throw new Error(`font ${font.region}/${font.family} requires a full SHA-256 digest`);
279
+ }
280
+ }
281
+ function deduplicateRequests(requests) {
282
+ const result = new Map();
283
+ for (const request of requests)
284
+ if (request.char !== "\n" && request.char !== "\r" && !result.has(requestKey(request)))
285
+ result.set(requestKey(request), request);
286
+ return [...result.values()];
287
+ }
288
+ function fontSourceKey(source) {
289
+ return `${source.region}\u0000${source.sourceHash}\u0000${source.family}`;
290
+ }
291
+ function fontRequestKey(request) {
292
+ return `${request.region}\u0000${request.fontSourceHash}\u0000${request.family}`;
293
+ }
294
+ function requestKey(request) {
295
+ return glyphKey(request.region, request.fontSourceHash, request.family, request.char);
296
+ }
@@ -0,0 +1,64 @@
1
+ import type { ResourceDescriptor, ResourceProvider } from "../resourceProvider.js";
2
+ export type BrowserImageSource = {
3
+ source: TexImageSource;
4
+ width: number;
5
+ height: number;
6
+ };
7
+ export type BrowserSemanticResourceOptions = {
8
+ provider: ResourceProvider;
9
+ concurrency?: number;
10
+ decode?: (blob: Blob) => Promise<TexImageSource>;
11
+ softBytes?: number;
12
+ hardBytes?: number;
13
+ };
14
+ export type BrowserSemanticResourceSet = {
15
+ sources: Map<string, BrowserImageSource>;
16
+ availability: Map<string, boolean>;
17
+ release(): void;
18
+ };
19
+ export type ResourceProviderRuntimeStats = {
20
+ requested: number;
21
+ loaded: number;
22
+ failures: number;
23
+ cancellations: number;
24
+ encodedBytes: number;
25
+ resolveMs: number;
26
+ active: number;
27
+ queued: number;
28
+ limit: number;
29
+ peak: number;
30
+ };
31
+ /** The provider owns acquisition and persistent caching. This manager only
32
+ * schedules provider calls and retains bounded decoded session resources. */
33
+ export declare class BrowserSemanticResourceManager {
34
+ private readonly provider;
35
+ private readonly decode;
36
+ private readonly scheduler;
37
+ private readonly cache;
38
+ private readonly providerCounters;
39
+ constructor(options: BrowserSemanticResourceOptions);
40
+ acquire(resources: ResourceDescriptor[], signal?: AbortSignal): Promise<BrowserSemanticResourceSet>;
41
+ stats(): {
42
+ provider: {
43
+ active: number;
44
+ queued: number;
45
+ limit: number;
46
+ peak: number;
47
+ requested: number;
48
+ loaded: number;
49
+ failures: number;
50
+ cancellations: number;
51
+ encodedBytes: number;
52
+ resolveMs: number;
53
+ };
54
+ entries: number;
55
+ bytes: number;
56
+ pinned: number;
57
+ loads: number;
58
+ hits: number;
59
+ evictions: number;
60
+ };
61
+ private load;
62
+ private resolveSource;
63
+ }
64
+ export declare function resourceIdentity(resource: Pick<ResourceDescriptor, "id">): string;
@@ -0,0 +1,232 @@
1
+ import { SessionImageResourceCache } from "../cache/sessionImageResourceCache.js";
2
+ /** The provider owns acquisition and persistent caching. This manager only
3
+ * schedules provider calls and retains bounded decoded session resources. */
4
+ export class BrowserSemanticResourceManager {
5
+ provider;
6
+ decode;
7
+ scheduler;
8
+ cache;
9
+ providerCounters = {
10
+ requested: 0,
11
+ loaded: 0,
12
+ failures: 0,
13
+ cancellations: 0,
14
+ encodedBytes: 0,
15
+ resolveMs: 0,
16
+ };
17
+ constructor(options) {
18
+ if (!options.provider || typeof options.provider.provide !== "function") {
19
+ throw new Error("semantic resource provider is required");
20
+ }
21
+ this.provider = options.provider;
22
+ this.decode = options.decode ?? ((blob) => createImageBitmap(blob, {
23
+ premultiplyAlpha: "none",
24
+ colorSpaceConversion: "none",
25
+ }));
26
+ this.scheduler = new BoundedScheduler(options.concurrency ?? 8);
27
+ this.cache = new SessionImageResourceCache({
28
+ softBytes: options.softBytes ?? 64 * 1024 * 1024,
29
+ hardBytes: options.hardBytes ?? 96 * 1024 * 1024,
30
+ dispose: (value) => closeImageSource(value.image.source),
31
+ });
32
+ }
33
+ async acquire(resources, signal = new AbortController().signal) {
34
+ const unique = new Map(resources.map((resource) => [resourceIdentity(resource), resource]));
35
+ const leases = [];
36
+ try {
37
+ await Promise.all([...unique].map(async ([identity, resource]) => {
38
+ const cacheIdentity = this.provider.cacheIdentity?.(resource) ?? identity;
39
+ const lease = await this.cache.acquire(cacheIdentity, async (sharedSignal) => {
40
+ const value = await this.load(resource, sharedSignal);
41
+ return { value, bytes: decodedBytes(value.image) };
42
+ }, signal);
43
+ leases.push({ identity, lease });
44
+ }));
45
+ }
46
+ catch (error) {
47
+ for (const { lease } of leases)
48
+ lease.release();
49
+ throw error;
50
+ }
51
+ const sources = new Map(leases.map(({ identity, lease }) => [identity, lease.value.image]));
52
+ const availability = new Map(leases.map(({ identity, lease }) => [identity, lease.value.available]));
53
+ let released = false;
54
+ return {
55
+ sources,
56
+ availability,
57
+ release: () => {
58
+ if (released)
59
+ return;
60
+ released = true;
61
+ for (const { lease } of leases)
62
+ lease.release();
63
+ },
64
+ };
65
+ }
66
+ stats() {
67
+ return {
68
+ ...this.cache.stats(),
69
+ provider: {
70
+ ...this.providerCounters,
71
+ ...this.scheduler.stats(),
72
+ },
73
+ };
74
+ }
75
+ async load(resource, signal) {
76
+ this.providerCounters.requested += 1;
77
+ const started = performance.now();
78
+ try {
79
+ const provided = await this.scheduler.run(() => this.provider.provide(resource, { signal }), signal);
80
+ if (signal.aborted)
81
+ throw abortReason(signal);
82
+ if (!provided)
83
+ throw new Error("provider returned no resource");
84
+ const image = await this.resolveSource(provided);
85
+ this.providerCounters.loaded += 1;
86
+ this.providerCounters.encodedBytes += providedByteLength(provided);
87
+ return { image, available: true };
88
+ }
89
+ catch (error) {
90
+ if (signal.aborted || isAbortError(error)) {
91
+ this.providerCounters.cancellations += 1;
92
+ throw error;
93
+ }
94
+ this.providerCounters.failures += 1;
95
+ warnMissingResource(resource, error);
96
+ return {
97
+ image: normalizeSource(await this.decode(transparentPngBlob())),
98
+ available: false,
99
+ };
100
+ }
101
+ finally {
102
+ this.providerCounters.resolveMs += Math.max(0, performance.now() - started);
103
+ }
104
+ }
105
+ async resolveSource(provided) {
106
+ const source = provided.source;
107
+ if (source instanceof Blob)
108
+ return normalizeSource(await this.decode(source));
109
+ if (source instanceof ArrayBuffer)
110
+ return normalizeSource(await this.decode(new Blob([source])));
111
+ if (ArrayBuffer.isView(source)) {
112
+ const view = new Uint8Array(source.buffer, source.byteOffset, source.byteLength);
113
+ const bytes = new Uint8Array(view.byteLength);
114
+ bytes.set(view);
115
+ return normalizeSource(await this.decode(new Blob([bytes])));
116
+ }
117
+ const dimensions = sourceDimensions(source);
118
+ if (!dimensions)
119
+ throw new Error("direct image source has no finite dimensions");
120
+ return { source, ...dimensions };
121
+ }
122
+ }
123
+ export function resourceIdentity(resource) {
124
+ if (!resource.id)
125
+ throw new Error("semantic resource stable id is required");
126
+ return resource.id;
127
+ }
128
+ class BoundedScheduler {
129
+ limit;
130
+ active = 0;
131
+ peak = 0;
132
+ queue = [];
133
+ constructor(limit) {
134
+ if (!Number.isInteger(limit) || limit <= 0)
135
+ throw new Error("semantic resource concurrency must be a positive integer");
136
+ this.limit = limit;
137
+ }
138
+ run(task, signal) {
139
+ return new Promise((resolve, reject) => {
140
+ const start = () => {
141
+ if (signal.aborted) {
142
+ reject(abortReason(signal));
143
+ this.drain();
144
+ return;
145
+ }
146
+ this.active += 1;
147
+ this.peak = Math.max(this.peak, this.active);
148
+ void task().then(resolve, reject).finally(() => {
149
+ this.active -= 1;
150
+ this.drain();
151
+ });
152
+ };
153
+ this.queue.push(start);
154
+ this.drain();
155
+ });
156
+ }
157
+ stats() {
158
+ return { active: this.active, queued: this.queue.length, limit: this.limit, peak: this.peak };
159
+ }
160
+ drain() {
161
+ while (this.active < this.limit) {
162
+ const start = this.queue.shift();
163
+ if (!start)
164
+ return;
165
+ start();
166
+ }
167
+ }
168
+ }
169
+ function sourceDimensions(source) {
170
+ const value = source;
171
+ const width = firstPositiveDimension(value.videoWidth, value.naturalWidth, value.displayWidth, value.width);
172
+ const height = firstPositiveDimension(value.videoHeight, value.naturalHeight, value.displayHeight, value.height);
173
+ return width != null && height != null
174
+ ? { width, height }
175
+ : null;
176
+ }
177
+ function decodedBytes(source) {
178
+ return Math.max(0, Math.floor(source.width * source.height * 4));
179
+ }
180
+ function providedByteLength(provided) {
181
+ const source = provided.source;
182
+ if (source instanceof Blob)
183
+ return source.size;
184
+ if (source instanceof ArrayBuffer)
185
+ return source.byteLength;
186
+ if (ArrayBuffer.isView(source))
187
+ return source.byteLength;
188
+ return 0;
189
+ }
190
+ function normalizeSource(source) {
191
+ const dimensions = sourceDimensions(source);
192
+ if (!dimensions)
193
+ throw new Error("decoded image source has no finite dimensions");
194
+ return { source, ...dimensions };
195
+ }
196
+ function firstPositiveDimension(...values) {
197
+ for (const value of values) {
198
+ const dimension = numericDimension(value);
199
+ if (Number.isFinite(dimension) && dimension > 0)
200
+ return dimension;
201
+ }
202
+ return null;
203
+ }
204
+ function numericDimension(value) {
205
+ if (value && typeof value === "object") {
206
+ const animated = value;
207
+ const resolved = animated.animVal?.value ?? animated.baseVal?.value;
208
+ if (resolved != null)
209
+ return Number(resolved);
210
+ }
211
+ return Number(value);
212
+ }
213
+ function closeImageSource(source) {
214
+ const close = source.close;
215
+ if (typeof close === "function")
216
+ close.call(source);
217
+ }
218
+ function isAbortError(error) {
219
+ return error instanceof DOMException && error.name === "AbortError";
220
+ }
221
+ function abortReason(signal) {
222
+ return signal.reason ?? new DOMException("The operation was aborted", "AbortError");
223
+ }
224
+ const TRANSPARENT_PNG_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGNgYGBgAAAABQABpfZFQAAAAABJRU5ErkJggg==";
225
+ function transparentPngBlob() {
226
+ const bytes = Uint8Array.from(atob(TRANSPARENT_PNG_BASE64), (value) => value.charCodeAt(0));
227
+ return new Blob([bytes], { type: "image/png" });
228
+ }
229
+ function warnMissingResource(resource, error) {
230
+ const reason = error instanceof Error ? error.message : String(error);
231
+ console.warn(`[sekai-custom-profile-sdk] semantic resource unavailable ${resource.namespace}/${resource.key} role=${resource.role}: ${reason}; using transparent placeholder`);
232
+ }
@@ -0,0 +1,8 @@
1
+ import type { GlyphInstance } from "../types/glyph.js";
2
+ import type { SemanticDrawOperation } from "./semanticCommandPlanner.js";
3
+ /**
4
+ * Applies General's measured draw anchors after the Rust TMP layout has
5
+ * finished. The returned instances retain the exact layout metrics and glyph
6
+ * relationships; only final local/device draw coordinates are translated.
7
+ */
8
+ export declare function placeGeneralTextInstances(instances: readonly GlyphInstance[], operations: readonly SemanticDrawOperation[]): GlyphInstance[];