@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.
- package/LICENSE +235 -0
- package/LICENSE-EXCEPTION +32 -0
- package/NOTICE +11 -0
- package/README.en.md +456 -0
- package/README.md +458 -0
- package/dist/allium_renderer_wasm.js +14 -0
- package/dist/allium_renderer_wasm.wasm +0 -0
- package/dist/authoring.d.ts +14 -0
- package/dist/authoring.js +34 -0
- package/dist/cache/glyphPersistentCache.d.ts +145 -0
- package/dist/cache/glyphPersistentCache.js +346 -0
- package/dist/cache/indexedDbGlyphRecordStore.d.ts +26 -0
- package/dist/cache/indexedDbGlyphRecordStore.js +214 -0
- package/dist/cache/sessionImageResourceCache.d.ts +39 -0
- package/dist/cache/sessionImageResourceCache.js +156 -0
- package/dist/emscripten.d.ts +11 -0
- package/dist/emscripten.js +1 -0
- package/dist/fontProvider.d.ts +36 -0
- package/dist/fontProvider.js +84 -0
- package/dist/fontSdfAtlas.d.ts +92 -0
- package/dist/fontSdfAtlas.js +296 -0
- package/dist/gpu/browserSemanticResources.d.ts +64 -0
- package/dist/gpu/browserSemanticResources.js +232 -0
- package/dist/gpu/generalTextRenderPlacement.d.ts +8 -0
- package/dist/gpu/generalTextRenderPlacement.js +131 -0
- package/dist/gpu/previewTransformTextureLayout.d.ts +1 -0
- package/dist/gpu/previewTransformTextureLayout.js +12 -0
- package/dist/gpu/semanticCommandGeometry.d.ts +21 -0
- package/dist/gpu/semanticCommandGeometry.js +226 -0
- package/dist/gpu/semanticCommandPlanner.d.ts +131 -0
- package/dist/gpu/semanticCommandPlanner.js +203 -0
- package/dist/gpu/semanticTextGlyphBridge.d.ts +3 -0
- package/dist/gpu/semanticTextGlyphBridge.js +22 -0
- package/dist/gpu/semanticWebglSceneRenderer.d.ts +70 -0
- package/dist/gpu/semanticWebglSceneRenderer.js +211 -0
- package/dist/gpu/slotRanges.d.ts +5 -0
- package/dist/gpu/slotRanges.js +14 -0
- package/dist/gpu/webglSdfAtlasTexture.d.ts +17 -0
- package/dist/gpu/webglSdfAtlasTexture.js +72 -0
- package/dist/gpu/webglSdfGlyphPipeline.d.ts +26 -0
- package/dist/gpu/webglSdfGlyphPipeline.js +267 -0
- package/dist/gpu/webglSemanticCommandExecutor.d.ts +79 -0
- package/dist/gpu/webglSemanticCommandExecutor.js +686 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +8 -0
- package/dist/interaction/numericTextRegions.d.ts +46 -0
- package/dist/interaction/numericTextRegions.js +44 -0
- package/dist/localizationProvider.d.ts +33 -0
- package/dist/localizationProvider.js +74 -0
- package/dist/originPrebuiltSdfAtlasPackage.d.ts +39 -0
- package/dist/originPrebuiltSdfAtlasPackage.js +316 -0
- package/dist/prebuiltSdfAtlas.d.ts +36 -0
- package/dist/prebuiltSdfAtlas.js +261 -0
- package/dist/protocol.d.ts +456 -0
- package/dist/protocol.js +1 -0
- package/dist/renderer.d.ts +218 -0
- package/dist/renderer.js +641 -0
- package/dist/resourceProvider.d.ts +24 -0
- package/dist/resourceProvider.js +41 -0
- package/dist/telemetry/rendererTelemetry.d.ts +158 -0
- package/dist/telemetry/rendererTelemetry.js +232 -0
- package/dist/third-party/freetype/FTL.txt +169 -0
- package/dist/types/atlas.d.ts +90 -0
- package/dist/types/atlas.js +1 -0
- package/dist/types/authoring.d.ts +81 -0
- package/dist/types/authoring.js +1 -0
- package/dist/types/core.d.ts +128 -0
- package/dist/types/core.js +1 -0
- package/dist/types/freeType.d.ts +54 -0
- package/dist/types/freeType.js +13 -0
- package/dist/types/glyph.d.ts +33 -0
- package/dist/types/glyph.js +1 -0
- package/dist/types/layout.d.ts +25 -0
- package/dist/types/layout.js +1 -0
- package/dist/types/text.d.ts +28 -0
- package/dist/types/text.js +1 -0
- package/dist/worker/glyphWorkScheduler.d.ts +29 -0
- package/dist/worker/glyphWorkScheduler.js +107 -0
- package/dist/worker-client.d.ts +114 -0
- package/dist/worker-client.js +413 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +708 -0
- package/package.json +46 -0
- package/src/atlas.rs +652 -0
- package/src/authoring.ts +42 -0
- package/src/authoring_runtime.rs +303 -0
- package/src/cache/glyphPersistentCache.ts +463 -0
- package/src/cache/indexedDbGlyphRecordStore.ts +238 -0
- package/src/cache/sessionImageResourceCache.ts +186 -0
- package/src/edt.rs +97 -0
- package/src/emscripten.ts +17 -0
- package/src/fontProvider.ts +127 -0
- package/src/fontSdfAtlas.ts +431 -0
- package/src/geometry.rs +149 -0
- package/src/glyph_plan.rs +226 -0
- package/src/gpu/browserSemanticResources.ts +294 -0
- package/src/gpu/generalTextRenderPlacement.ts +175 -0
- package/src/gpu/previewTransformTextureLayout.ts +12 -0
- package/src/gpu/semanticCommandGeometry.ts +260 -0
- package/src/gpu/semanticCommandPlanner.ts +280 -0
- package/src/gpu/semanticTextGlyphBridge.ts +35 -0
- package/src/gpu/semanticWebglSceneRenderer.ts +261 -0
- package/src/gpu/slotRanges.ts +15 -0
- package/src/gpu/webglSdfAtlasTexture.ts +69 -0
- package/src/gpu/webglSdfGlyphPipeline.ts +299 -0
- package/src/gpu/webglSemanticCommandExecutor.ts +739 -0
- package/src/index.ts +84 -0
- package/src/interaction/numericTextRegions.ts +80 -0
- package/src/layout.rs +1882 -0
- package/src/lib.rs +1443 -0
- package/src/localizationProvider.ts +113 -0
- package/src/main.rs +55 -0
- package/src/masterdata_runtime.rs +496 -0
- package/src/originPrebuiltSdfAtlasPackage.ts +403 -0
- package/src/prebuiltSdfAtlas.ts +306 -0
- package/src/protocol.ts +150 -0
- package/src/renderer.ts +792 -0
- package/src/resourceProvider.ts +69 -0
- package/src/scene.rs +665 -0
- package/src/telemetry/rendererTelemetry.ts +358 -0
- package/src/types/atlas.ts +80 -0
- package/src/types/authoring.ts +85 -0
- package/src/types/core.ts +110 -0
- package/src/types/freeType.ts +63 -0
- package/src/types/glyph.ts +33 -0
- package/src/types/layout.ts +28 -0
- package/src/types/text.ts +28 -0
- package/src/worker/glyphWorkScheduler.ts +130 -0
- package/src/worker-client.ts +484 -0
- package/src/worker.ts +895 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import {
|
|
2
|
+
GlyphPersistentCache,
|
|
3
|
+
createPersistentGlyphRecord,
|
|
4
|
+
type GlyphRasterIdentity,
|
|
5
|
+
type PersistentGlyphRecord,
|
|
6
|
+
} from "./cache/glyphPersistentCache.js";
|
|
7
|
+
import { createOriginGlyphPersistentCache } from "./cache/indexedDbGlyphRecordStore.js";
|
|
8
|
+
import { RendererAtlas, RendererWorkerClient } from "./worker-client.js";
|
|
9
|
+
import type { RendererWorkerStats } from "./protocol.js";
|
|
10
|
+
import type {
|
|
11
|
+
AtlasGenerateRequest,
|
|
12
|
+
AtlasGlyphRecord,
|
|
13
|
+
AtlasPageUpdate,
|
|
14
|
+
AtlasStats,
|
|
15
|
+
GlyphRasterPlan,
|
|
16
|
+
} from "./types/atlas.js";
|
|
17
|
+
|
|
18
|
+
export type GlyphRequest = {
|
|
19
|
+
region: string;
|
|
20
|
+
family: string;
|
|
21
|
+
fontSourceHash: string;
|
|
22
|
+
char: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type FontSource = {
|
|
26
|
+
region: string;
|
|
27
|
+
family: string;
|
|
28
|
+
sourceHash: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type GlyphInfo = {
|
|
32
|
+
key: string;
|
|
33
|
+
resourceId: string;
|
|
34
|
+
glyphIndex: number;
|
|
35
|
+
page: number;
|
|
36
|
+
pageEpoch: number;
|
|
37
|
+
region: string;
|
|
38
|
+
family: string;
|
|
39
|
+
fontSourceHash: string;
|
|
40
|
+
char: string;
|
|
41
|
+
u0: number;
|
|
42
|
+
v0: number;
|
|
43
|
+
u1: number;
|
|
44
|
+
v1: number;
|
|
45
|
+
width: number;
|
|
46
|
+
height: number;
|
|
47
|
+
advance: number;
|
|
48
|
+
xOffset: number;
|
|
49
|
+
yOffset: number;
|
|
50
|
+
planeBearingX: number;
|
|
51
|
+
planeBearingY: number;
|
|
52
|
+
planeWidth: number;
|
|
53
|
+
planeHeight: number;
|
|
54
|
+
drawable: boolean;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export type SdfBackend = "edt" | "analytic";
|
|
58
|
+
export type PersistentCacheSelection = "memory-only" | "origin";
|
|
59
|
+
|
|
60
|
+
export type SdfAtlasPerf = {
|
|
61
|
+
backend: SdfBackend;
|
|
62
|
+
fontBatches: [];
|
|
63
|
+
totalGlyphMs: number;
|
|
64
|
+
totalFaceLoadMs: number;
|
|
65
|
+
totalGlyphCount: number;
|
|
66
|
+
totalPixelCount: number;
|
|
67
|
+
worker: RendererWorkerStats;
|
|
68
|
+
cache: {
|
|
69
|
+
hits: number;
|
|
70
|
+
misses: number;
|
|
71
|
+
generations: number;
|
|
72
|
+
bytes: number;
|
|
73
|
+
sessionHits: number;
|
|
74
|
+
persistentHits: number;
|
|
75
|
+
persistentMisses: number;
|
|
76
|
+
persistentWritesQueued: number;
|
|
77
|
+
pages: number;
|
|
78
|
+
pinnedPages: number;
|
|
79
|
+
pageEvictions: number;
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export type SdfAtlas = {
|
|
84
|
+
width: number;
|
|
85
|
+
height: number;
|
|
86
|
+
depth: number;
|
|
87
|
+
baseSize: number;
|
|
88
|
+
spread: number;
|
|
89
|
+
glyphs: Map<string, GlyphInfo>;
|
|
90
|
+
missing: string[];
|
|
91
|
+
backend: "freetype-wasm";
|
|
92
|
+
sdfBackend: SdfBackend;
|
|
93
|
+
contractId: string;
|
|
94
|
+
fontEngineFingerprint: string;
|
|
95
|
+
perf: SdfAtlasPerf;
|
|
96
|
+
pageUpdates(revisions: ReadonlyMap<number, number>): Promise<AtlasPageUpdate[]>;
|
|
97
|
+
release(): Promise<void>;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
type ResolvedRequest = {
|
|
101
|
+
request: GlyphRequest;
|
|
102
|
+
glyphIndex: number;
|
|
103
|
+
identity: GlyphRasterIdentity;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
type CanonicalGlyphRasterPlan = Omit<GlyphRasterPlan, "region" | "family" | "fontSourceHash" | "glyphs" | "missing"> & {
|
|
107
|
+
glyphs: Array<GlyphRasterPlan["glyphs"][number] & Pick<GlyphRasterPlan, "region" | "family" | "fontSourceHash">>;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
type SessionEngine = {
|
|
111
|
+
atlas: RendererAtlas;
|
|
112
|
+
contractId: string;
|
|
113
|
+
charToGlyphIndex: Map<string, number>;
|
|
114
|
+
missingChars: Set<string>;
|
|
115
|
+
metrics: Map<string, Omit<AtlasGlyphRecord, "pixels">>;
|
|
116
|
+
placements: Map<string, { page: number; pageEpoch: number }>;
|
|
117
|
+
latestStats: AtlasStats;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const engines = new WeakMap<RendererWorkerClient, Map<string, Promise<SessionEngine>>>();
|
|
121
|
+
const engineRegistry = new Map<RendererWorkerClient, Set<SessionEngine>>();
|
|
122
|
+
let originPersistentCache: GlyphPersistentCache | null = null;
|
|
123
|
+
|
|
124
|
+
export function glyphKey(region: string, fontSourceHash: string, family: string, char: string): string {
|
|
125
|
+
return `${region}\u0000${fontSourceHash}\u0000${family}\u0000${char}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function buildSdfAtlas(
|
|
129
|
+
fontSources: FontSource[],
|
|
130
|
+
requests: GlyphRequest[],
|
|
131
|
+
options: { worker: RendererWorkerClient; persistence?: PersistentCacheSelection; persistentCache?: GlyphPersistentCache },
|
|
132
|
+
supersample?: number,
|
|
133
|
+
backend?: SdfBackend,
|
|
134
|
+
): Promise<SdfAtlas> {
|
|
135
|
+
validateFontSources(fontSources);
|
|
136
|
+
const sdfBackend = backend ?? (supersample != null && supersample <= 0 ? "analytic" : "edt");
|
|
137
|
+
const persistent = options.persistentCache ?? (options.persistence === "origin" ? getOriginPersistentCache() : new GlyphPersistentCache());
|
|
138
|
+
const uniqueRequests = deduplicateRequests(requests);
|
|
139
|
+
const plan = await planGlyphs(options.worker, fontSources, uniqueRequests, sdfBackend, supersample ?? 2);
|
|
140
|
+
const engine = await getEngine(options.worker, plan.contractId);
|
|
141
|
+
const planned = new Map(plan.glyphs.map((glyph) => [
|
|
142
|
+
glyphKey(glyph.region, glyph.fontSourceHash, glyph.family, glyph.ch),
|
|
143
|
+
glyph,
|
|
144
|
+
] as const));
|
|
145
|
+
for (const request of uniqueRequests) {
|
|
146
|
+
const glyph = planned.get(requestKey(request));
|
|
147
|
+
if (glyph) engine.charToGlyphIndex.set(requestKey(request), glyph.glyphIndex);
|
|
148
|
+
else engine.missingChars.add(requestKey(request));
|
|
149
|
+
}
|
|
150
|
+
const resolved = uniqueRequests.flatMap((request): ResolvedRequest[] => {
|
|
151
|
+
const glyph = planned.get(requestKey(request));
|
|
152
|
+
return glyph ? [{ request, glyphIndex: glyph.glyphIndex, identity: glyph.identity }] : [];
|
|
153
|
+
});
|
|
154
|
+
const unique = new Map(resolved.map((value) => [value.identity.opaqueKey, value] as const));
|
|
155
|
+
const placementsBefore = new Map([...unique.keys()].flatMap((key) => {
|
|
156
|
+
const placement = engine.placements.get(key);
|
|
157
|
+
return placement ? [[key, placement] as const] : [];
|
|
158
|
+
}));
|
|
159
|
+
const identities = [...unique.values()].map((value) => value.identity);
|
|
160
|
+
const persisted = await persistent.getMany(identities);
|
|
161
|
+
const cached = [...persisted.values()].map(recordToAtlas);
|
|
162
|
+
for (const record of cached) engine.metrics.set(record.key, withoutPixels(record));
|
|
163
|
+
const missing = [...unique.values()].filter((value) => !persisted.has(value.identity.opaqueKey));
|
|
164
|
+
const generate = generationGroups(fontSources, missing, plan.backend, plan.supersample);
|
|
165
|
+
const workerBefore = await options.worker.stats();
|
|
166
|
+
const result = await engine.atlas.resolve([...unique.keys()], cached, generate);
|
|
167
|
+
engine.latestStats = result.stats;
|
|
168
|
+
const placementByKey = new Map(result.placements.map((entry) => [entry.key, entry.placement] as const));
|
|
169
|
+
for (const [key, placement] of placementByKey) {
|
|
170
|
+
engine.placements.set(key, { page: placement.page, pageEpoch: placement.pageEpoch });
|
|
171
|
+
}
|
|
172
|
+
const sessionHitKeys = new Set([...unique.keys()].filter((key) => {
|
|
173
|
+
const before = placementsBefore.get(key);
|
|
174
|
+
const after = placementByKey.get(key);
|
|
175
|
+
return before != null && after != null && before.page === after.page && before.pageEpoch === after.pageEpoch;
|
|
176
|
+
}));
|
|
177
|
+
const sessionHits = sessionHitKeys.size;
|
|
178
|
+
const persistentHits = [...persisted.keys()].filter((key) => !sessionHitKeys.has(key)).length;
|
|
179
|
+
for (const record of result.generated) engine.metrics.set(record.key, withoutPixels(record));
|
|
180
|
+
if (result.generated.length > 0) {
|
|
181
|
+
const writeEpoch = persistent.beginWrite();
|
|
182
|
+
const records = await Promise.all(result.generated.map((record) => {
|
|
183
|
+
const identity = unique.get(record.key)?.identity;
|
|
184
|
+
if (!identity) throw new Error(`generated atlas resource has unknown identity ${record.key}`);
|
|
185
|
+
return createPersistentGlyphRecord(identity, record);
|
|
186
|
+
}));
|
|
187
|
+
void persistent.putMany(records, writeEpoch).catch(() => undefined);
|
|
188
|
+
}
|
|
189
|
+
const glyphs = new Map<string, GlyphInfo>();
|
|
190
|
+
for (const value of resolved) {
|
|
191
|
+
const resourceId = value.identity.opaqueKey;
|
|
192
|
+
const placement = placementByKey.get(resourceId);
|
|
193
|
+
const metrics = engine.metrics.get(resourceId);
|
|
194
|
+
if (!placement || !metrics) throw new Error(`WASM atlas omitted glyph resource ${resourceId}`);
|
|
195
|
+
glyphs.set(requestKey(value.request), {
|
|
196
|
+
key: requestKey(value.request),
|
|
197
|
+
resourceId,
|
|
198
|
+
glyphIndex: value.glyphIndex,
|
|
199
|
+
page: placement.page,
|
|
200
|
+
pageEpoch: placement.pageEpoch,
|
|
201
|
+
region: value.request.region,
|
|
202
|
+
family: value.request.family,
|
|
203
|
+
fontSourceHash: value.request.fontSourceHash,
|
|
204
|
+
char: value.request.char,
|
|
205
|
+
u0: placement.u0,
|
|
206
|
+
v0: placement.v0,
|
|
207
|
+
u1: placement.u1,
|
|
208
|
+
v1: placement.v1,
|
|
209
|
+
width: metrics.width,
|
|
210
|
+
height: metrics.height,
|
|
211
|
+
advance: metrics.advance,
|
|
212
|
+
xOffset: metrics.xOffset,
|
|
213
|
+
yOffset: metrics.yOffset,
|
|
214
|
+
planeBearingX: metrics.planeBearingX,
|
|
215
|
+
planeBearingY: metrics.planeBearingY,
|
|
216
|
+
planeWidth: metrics.planeWidth,
|
|
217
|
+
planeHeight: metrics.planeHeight,
|
|
218
|
+
drawable: metrics.drawable,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
const workerAfter = await options.worker.stats();
|
|
222
|
+
let released = false;
|
|
223
|
+
return {
|
|
224
|
+
width: plan.atlasWidth,
|
|
225
|
+
height: plan.atlasHeight,
|
|
226
|
+
depth: result.stats.pages,
|
|
227
|
+
baseSize: plan.baseSize,
|
|
228
|
+
spread: plan.spread,
|
|
229
|
+
glyphs,
|
|
230
|
+
missing: uniqueRequests.filter((request) => engine.missingChars.has(requestKey(request))).map(requestKey),
|
|
231
|
+
backend: "freetype-wasm",
|
|
232
|
+
sdfBackend: plan.backend,
|
|
233
|
+
contractId: plan.contractId,
|
|
234
|
+
fontEngineFingerprint: plan.fontEngineFingerprint,
|
|
235
|
+
perf: {
|
|
236
|
+
backend: sdfBackend,
|
|
237
|
+
fontBatches: [],
|
|
238
|
+
totalGlyphMs: Math.max(0, workerAfter.wasmMs - workerBefore.wasmMs),
|
|
239
|
+
totalFaceLoadMs: 0,
|
|
240
|
+
totalGlyphCount: result.generated.length,
|
|
241
|
+
totalPixelCount: result.generated.reduce((sum, record) => sum + record.pixels.byteLength, 0),
|
|
242
|
+
worker: workerAfter,
|
|
243
|
+
cache: {
|
|
244
|
+
hits: sessionHits + persistentHits,
|
|
245
|
+
misses: unique.size - sessionHits - persistentHits,
|
|
246
|
+
generations: result.generated.length,
|
|
247
|
+
bytes: result.stats.atlasBytes,
|
|
248
|
+
sessionHits,
|
|
249
|
+
persistentHits,
|
|
250
|
+
persistentMisses: unique.size - sessionHits - persistentHits,
|
|
251
|
+
persistentWritesQueued: result.generated.length,
|
|
252
|
+
pages: result.stats.pages,
|
|
253
|
+
pinnedPages: result.stats.pinnedPages,
|
|
254
|
+
pageEvictions: result.stats.evictions,
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
pageUpdates: (revisions) => engine.atlas.pages(revisions),
|
|
258
|
+
release: async () => {
|
|
259
|
+
if (released) return;
|
|
260
|
+
released = true;
|
|
261
|
+
await Promise.all(result.leases.map((lease) => engine.atlas.release(lease)));
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function getPersistentGlyphCache(): GlyphPersistentCache {
|
|
267
|
+
return getOriginPersistentCache();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function getSessionAtlasStats(): Array<{ contractId: string; stats: AtlasStats }> {
|
|
271
|
+
return [...engineRegistry.values()].flatMap((sessions) => [...sessions]
|
|
272
|
+
.map((engine) => ({ contractId: engine.contractId, stats: engine.latestStats })));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function disposeWorkerAtlasSessions(worker: RendererWorkerClient): void {
|
|
276
|
+
engineRegistry.delete(worker);
|
|
277
|
+
engines.delete(worker);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function getEngine(worker: RendererWorkerClient, contractId: string): Promise<SessionEngine> {
|
|
281
|
+
let byContract = engines.get(worker);
|
|
282
|
+
if (!byContract) {
|
|
283
|
+
byContract = new Map();
|
|
284
|
+
engines.set(worker, byContract);
|
|
285
|
+
}
|
|
286
|
+
let pending = byContract.get(contractId);
|
|
287
|
+
if (!pending) {
|
|
288
|
+
pending = worker.createAtlas().then((atlas) => {
|
|
289
|
+
const engine: SessionEngine = {
|
|
290
|
+
atlas,
|
|
291
|
+
contractId,
|
|
292
|
+
charToGlyphIndex: new Map(),
|
|
293
|
+
missingChars: new Set(),
|
|
294
|
+
metrics: new Map(),
|
|
295
|
+
placements: new Map(),
|
|
296
|
+
latestStats: atlas.initialStats,
|
|
297
|
+
};
|
|
298
|
+
let registered = engineRegistry.get(worker);
|
|
299
|
+
if (!registered) {
|
|
300
|
+
registered = new Set();
|
|
301
|
+
engineRegistry.set(worker, registered);
|
|
302
|
+
}
|
|
303
|
+
registered.add(engine);
|
|
304
|
+
return engine;
|
|
305
|
+
});
|
|
306
|
+
byContract.set(contractId, pending);
|
|
307
|
+
}
|
|
308
|
+
return pending;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function planGlyphs(
|
|
312
|
+
worker: RendererWorkerClient,
|
|
313
|
+
fonts: readonly FontSource[],
|
|
314
|
+
requests: readonly GlyphRequest[],
|
|
315
|
+
backend: SdfBackend,
|
|
316
|
+
supersample: number,
|
|
317
|
+
): Promise<CanonicalGlyphRasterPlan> {
|
|
318
|
+
const groups = new Map<string, { font: FontSource; chars: string[]; seen: Set<string> }>();
|
|
319
|
+
for (const font of fonts) groups.set(fontSourceKey(font), { font, chars: [], seen: new Set() });
|
|
320
|
+
for (const request of requests) {
|
|
321
|
+
const group = groups.get(fontRequestKey(request));
|
|
322
|
+
if (!group || group.seen.has(request.char)) continue;
|
|
323
|
+
group.seen.add(request.char);
|
|
324
|
+
group.chars.push(request.char);
|
|
325
|
+
}
|
|
326
|
+
const activeGroups = [...groups.values()].filter((group) => group.chars.length > 0);
|
|
327
|
+
if (activeGroups.length === 0 && fonts[0]) activeGroups.push({ font: fonts[0], chars: [], seen: new Set() });
|
|
328
|
+
const plans = await Promise.all(activeGroups.map((group) => worker.planGlyphs({
|
|
329
|
+
region: group.font.region,
|
|
330
|
+
family: group.font.family,
|
|
331
|
+
sourceHash: group.font.sourceHash,
|
|
332
|
+
chars: group.chars,
|
|
333
|
+
backend,
|
|
334
|
+
supersample,
|
|
335
|
+
})));
|
|
336
|
+
const first = plans[0];
|
|
337
|
+
if (!first) throw new Error("WASM glyph raster plan requires at least one glyph request");
|
|
338
|
+
for (const plan of plans) {
|
|
339
|
+
if (
|
|
340
|
+
plan.contractId !== first.contractId
|
|
341
|
+
|| plan.baseSize !== first.baseSize
|
|
342
|
+
|| plan.spread !== first.spread
|
|
343
|
+
|| plan.atlasWidth !== first.atlasWidth
|
|
344
|
+
|| plan.atlasHeight !== first.atlasHeight
|
|
345
|
+
) throw new Error("WASM returned incompatible glyph raster plans for one atlas");
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
...first,
|
|
349
|
+
glyphs: plans.flatMap((plan) => plan.glyphs.map((glyph) => ({
|
|
350
|
+
...glyph,
|
|
351
|
+
region: plan.region,
|
|
352
|
+
family: plan.family,
|
|
353
|
+
fontSourceHash: plan.fontSourceHash,
|
|
354
|
+
}))),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function generationGroups(
|
|
359
|
+
fonts: readonly FontSource[],
|
|
360
|
+
missing: readonly ResolvedRequest[],
|
|
361
|
+
backend: SdfBackend,
|
|
362
|
+
supersample: number,
|
|
363
|
+
): AtlasGenerateRequest[] {
|
|
364
|
+
const groups = new Map<string, AtlasGenerateRequest>();
|
|
365
|
+
for (const value of missing) {
|
|
366
|
+
const font = fonts.find((candidate) => fontSourceKey(candidate) === fontRequestKey(value.request));
|
|
367
|
+
if (!font) throw new Error(`font source missing for ${value.request.region}/${value.request.family}`);
|
|
368
|
+
const key = fontSourceKey(font);
|
|
369
|
+
let group = groups.get(key);
|
|
370
|
+
if (!group) {
|
|
371
|
+
group = { region: font.region, family: font.family, sourceHash: font.sourceHash, chars: [], backend, supersample, glyphs: [] };
|
|
372
|
+
groups.set(key, group);
|
|
373
|
+
}
|
|
374
|
+
group.chars.push(value.request.char);
|
|
375
|
+
group.glyphs.push({ key: value.identity.opaqueKey, char: value.request.char, glyphIndex: value.glyphIndex });
|
|
376
|
+
}
|
|
377
|
+
return [...groups.values()];
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function recordToAtlas(record: PersistentGlyphRecord): AtlasGlyphRecord {
|
|
381
|
+
return {
|
|
382
|
+
key: record.opaqueKey,
|
|
383
|
+
glyphIndex: 0,
|
|
384
|
+
width: record.width,
|
|
385
|
+
height: record.height,
|
|
386
|
+
advance: record.advance,
|
|
387
|
+
xOffset: record.xOffset,
|
|
388
|
+
yOffset: record.yOffset,
|
|
389
|
+
planeBearingX: record.planeBearingX,
|
|
390
|
+
planeBearingY: record.planeBearingY,
|
|
391
|
+
planeWidth: record.planeWidth,
|
|
392
|
+
planeHeight: record.planeHeight,
|
|
393
|
+
drawable: record.drawable,
|
|
394
|
+
pixels: new Uint8Array(record.pixels),
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function withoutPixels(record: AtlasGlyphRecord): Omit<AtlasGlyphRecord, "pixels"> {
|
|
399
|
+
const { pixels: _, ...metrics } = record;
|
|
400
|
+
return metrics;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function getOriginPersistentCache(): GlyphPersistentCache {
|
|
404
|
+
originPersistentCache ??= typeof indexedDB === "undefined" ? new GlyphPersistentCache() : createOriginGlyphPersistentCache();
|
|
405
|
+
return originPersistentCache;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function validateFontSources(fonts: readonly FontSource[]): void {
|
|
409
|
+
for (const font of fonts) {
|
|
410
|
+
if (!font.region || !font.family) throw new Error("font region/family must be non-empty");
|
|
411
|
+
if (!/^[0-9a-f]{64}$/i.test(font.sourceHash)) throw new Error(`font ${font.region}/${font.family} requires a full SHA-256 digest`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function deduplicateRequests(requests: readonly GlyphRequest[]): GlyphRequest[] {
|
|
416
|
+
const result = new Map<string, GlyphRequest>();
|
|
417
|
+
for (const request of requests) if (request.char !== "\n" && request.char !== "\r" && !result.has(requestKey(request))) result.set(requestKey(request), request);
|
|
418
|
+
return [...result.values()];
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function fontSourceKey(source: FontSource): string {
|
|
422
|
+
return `${source.region}\u0000${source.sourceHash}\u0000${source.family}`;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function fontRequestKey(request: GlyphRequest): string {
|
|
426
|
+
return `${request.region}\u0000${request.fontSourceHash}\u0000${request.family}`;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function requestKey(request: GlyphRequest): string {
|
|
430
|
+
return glyphKey(request.region, request.fontSourceHash, request.family, request.char);
|
|
431
|
+
}
|
package/src/geometry.rs
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
use std::slice;
|
|
2
|
+
|
|
3
|
+
pub(super) use allium_renderer_core::sdf_geometry::{CubicSeg, LineSeg, QuadSeg, Segment, Vec2};
|
|
4
|
+
use freetype::ffi;
|
|
5
|
+
|
|
6
|
+
const FT_POINT_TAG_ON_CURVE: u8 = 0x01;
|
|
7
|
+
const FT_POINT_TAG_CUBIC_CONTROL: u8 = 0x02;
|
|
8
|
+
|
|
9
|
+
pub(super) unsafe fn extract_segments(outline: &ffi::FT_Outline) -> Vec<Vec<Segment>> {
|
|
10
|
+
let contour_ends = slice::from_raw_parts(outline.contours, outline.n_contours as usize);
|
|
11
|
+
let points = slice::from_raw_parts(outline.points, outline.n_points as usize);
|
|
12
|
+
let tags = slice::from_raw_parts(outline.tags, outline.n_points as usize);
|
|
13
|
+
let mut current_point_index = 0usize;
|
|
14
|
+
let mut contours = Vec::with_capacity(contour_ends.len());
|
|
15
|
+
|
|
16
|
+
for &last_point_index_in_contour in contour_ends {
|
|
17
|
+
let last_point_index_in_contour = last_point_index_in_contour as usize;
|
|
18
|
+
let (mut first_point, first_tag) = get_point(
|
|
19
|
+
&mut current_point_index,
|
|
20
|
+
points,
|
|
21
|
+
tags,
|
|
22
|
+
last_point_index_in_contour,
|
|
23
|
+
);
|
|
24
|
+
if (first_tag & FT_POINT_TAG_ON_CURVE) == 0 {
|
|
25
|
+
let mut temp_point_index = last_point_index_in_contour;
|
|
26
|
+
let (last_point, last_tag) = get_point(
|
|
27
|
+
&mut temp_point_index,
|
|
28
|
+
points,
|
|
29
|
+
tags,
|
|
30
|
+
last_point_index_in_contour,
|
|
31
|
+
);
|
|
32
|
+
first_point = if (last_tag & FT_POINT_TAG_ON_CURVE) != 0 {
|
|
33
|
+
last_point
|
|
34
|
+
} else {
|
|
35
|
+
last_point.lerp(first_point, 0.5)
|
|
36
|
+
};
|
|
37
|
+
current_point_index = current_point_index.saturating_sub(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let mut contour = Vec::new();
|
|
41
|
+
let mut current_pen = first_point;
|
|
42
|
+
while current_point_index <= last_point_index_in_contour {
|
|
43
|
+
let (mut point0, tag0) = get_point(
|
|
44
|
+
&mut current_point_index,
|
|
45
|
+
points,
|
|
46
|
+
tags,
|
|
47
|
+
last_point_index_in_contour,
|
|
48
|
+
);
|
|
49
|
+
if (tag0 & FT_POINT_TAG_ON_CURVE) != 0 {
|
|
50
|
+
contour.push(Segment::Line(LineSeg {
|
|
51
|
+
p0: current_pen,
|
|
52
|
+
p1: point0,
|
|
53
|
+
}));
|
|
54
|
+
current_pen = point0;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
loop {
|
|
59
|
+
if current_point_index > last_point_index_in_contour {
|
|
60
|
+
contour.push(Segment::Quad(QuadSeg {
|
|
61
|
+
p0: current_pen,
|
|
62
|
+
p1: point0,
|
|
63
|
+
p2: first_point,
|
|
64
|
+
}));
|
|
65
|
+
current_pen = first_point;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let (point1, tag1) = get_point(
|
|
70
|
+
&mut current_point_index,
|
|
71
|
+
points,
|
|
72
|
+
tags,
|
|
73
|
+
last_point_index_in_contour,
|
|
74
|
+
);
|
|
75
|
+
if (tag0 & FT_POINT_TAG_CUBIC_CONTROL) != 0 {
|
|
76
|
+
let end = if current_point_index <= last_point_index_in_contour {
|
|
77
|
+
let (point2, _) = get_point(
|
|
78
|
+
&mut current_point_index,
|
|
79
|
+
points,
|
|
80
|
+
tags,
|
|
81
|
+
last_point_index_in_contour,
|
|
82
|
+
);
|
|
83
|
+
point2
|
|
84
|
+
} else {
|
|
85
|
+
first_point
|
|
86
|
+
};
|
|
87
|
+
contour.push(Segment::Cubic(CubicSeg {
|
|
88
|
+
p0: current_pen,
|
|
89
|
+
p1: point0,
|
|
90
|
+
p2: point1,
|
|
91
|
+
p3: end,
|
|
92
|
+
}));
|
|
93
|
+
current_pen = end;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (tag1 & FT_POINT_TAG_ON_CURVE) != 0 {
|
|
98
|
+
contour.push(Segment::Quad(QuadSeg {
|
|
99
|
+
p0: current_pen,
|
|
100
|
+
p1: point0,
|
|
101
|
+
p2: point1,
|
|
102
|
+
}));
|
|
103
|
+
current_pen = point1;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let point_half = point0.lerp(point1, 0.5);
|
|
108
|
+
contour.push(Segment::Quad(QuadSeg {
|
|
109
|
+
p0: current_pen,
|
|
110
|
+
p1: point0,
|
|
111
|
+
p2: point_half,
|
|
112
|
+
}));
|
|
113
|
+
current_pen = point_half;
|
|
114
|
+
point0 = point1;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if !contour.is_empty() && !same_point(current_pen, first_point) {
|
|
119
|
+
contour.push(Segment::Line(LineSeg {
|
|
120
|
+
p0: current_pen,
|
|
121
|
+
p1: first_point,
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
if !contour.is_empty() {
|
|
125
|
+
contours.push(contour);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
contours
|
|
130
|
+
}
|
|
131
|
+
unsafe fn get_point(
|
|
132
|
+
current_point_index: &mut usize,
|
|
133
|
+
point_positions: &[ffi::FT_Vector],
|
|
134
|
+
point_tags: &[i8],
|
|
135
|
+
last_point_index_in_contour: usize,
|
|
136
|
+
) -> (Vec2, u8) {
|
|
137
|
+
debug_assert!(*current_point_index <= last_point_index_in_contour);
|
|
138
|
+
let point_position = point_positions[*current_point_index];
|
|
139
|
+
let point_tag = point_tags[*current_point_index] as u8;
|
|
140
|
+
*current_point_index += 1;
|
|
141
|
+
(
|
|
142
|
+
Vec2::new(point_position.x as f32, point_position.y as f32),
|
|
143
|
+
point_tag,
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
fn same_point(a: Vec2, b: Vec2) -> bool {
|
|
148
|
+
(a.x - b.x).abs() <= 1e-6 && (a.y - b.y).abs() <= 1e-6
|
|
149
|
+
}
|