@solidtv/renderer 1.6.0 → 1.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/core/CoreTextureManager.d.ts +31 -0
- package/dist/src/core/CoreTextureManager.js +87 -6
- package/dist/src/core/CoreTextureManager.js.map +1 -1
- package/dist/src/core/Stage.js +2 -1
- package/dist/src/core/Stage.js.map +1 -1
- package/dist/src/core/lib/ImageWorker.js +29 -20
- package/dist/src/core/lib/ImageWorker.js.map +1 -1
- package/dist/src/core/lib/validateImageBitmap.d.ts +13 -7
- package/dist/src/core/lib/validateImageBitmap.js +31 -10
- package/dist/src/core/lib/validateImageBitmap.js.map +1 -1
- package/dist/src/core/text-rendering/CanvasFontHandler.js +30 -15
- package/dist/src/core/text-rendering/CanvasFontHandler.js.map +1 -1
- package/dist/src/core/text-rendering/SdfFontHandler.js +52 -23
- package/dist/src/core/text-rendering/SdfFontHandler.js.map +1 -1
- package/dist/src/core/text-rendering/TextRenderer.d.ts +10 -1
- package/dist/src/core/textures/ImageTexture.js +4 -2
- package/dist/src/core/textures/ImageTexture.js.map +1 -1
- package/dist/src/main-api/Renderer.d.ts +37 -10
- package/dist/src/main-api/Renderer.js +8 -4
- package/dist/src/main-api/Renderer.js.map +1 -1
- package/dist/tsconfig.dist.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/core/CoreTextureManager.test.ts +147 -0
- package/src/core/CoreTextureManager.ts +97 -8
- package/src/core/Stage.ts +2 -0
- package/src/core/lib/ImageWorker.test.ts +19 -0
- package/src/core/lib/ImageWorker.ts +34 -40
- package/src/core/lib/validateImageBitmap.test.ts +17 -10
- package/src/core/lib/validateImageBitmap.ts +32 -14
- package/src/core/text-rendering/CanvasFontHandler.ts +36 -16
- package/src/core/text-rendering/SdfFontHandler.ts +55 -25
- package/src/core/text-rendering/TextRenderer.ts +10 -1
- package/src/core/text-rendering/tests/SdfFontHandler.test.ts +65 -1
- package/src/core/textures/ImageTexture.test.ts +94 -26
- package/src/core/textures/ImageTexture.ts +4 -2
- package/src/main-api/Renderer.ts +47 -14
|
@@ -43,7 +43,7 @@ function createImageWorker() {
|
|
|
43
43
|
options: {
|
|
44
44
|
supportsOptionsCreateImageBitmap: boolean;
|
|
45
45
|
supportsFullCreateImageBitmap: boolean;
|
|
46
|
-
premultiplyAlphaHonored: boolean;
|
|
46
|
+
premultiplyAlphaHonored: boolean | null;
|
|
47
47
|
},
|
|
48
48
|
): Promise<getImageReturn> {
|
|
49
49
|
return new Promise(function (resolve, reject) {
|
|
@@ -108,10 +108,12 @@ function createImageWorker() {
|
|
|
108
108
|
) {
|
|
109
109
|
// Fallback for browsers that do not support createImageBitmap with options
|
|
110
110
|
// this is supported for Chrome v50 to v52/54 that doesn't support options.
|
|
111
|
-
// The browser default premultiplies, so WebGL must not premultiply again
|
|
111
|
+
// The browser default premultiplies, so WebGL must not premultiply again —
|
|
112
|
+
// except on devices whose default returns straight alpha
|
|
113
|
+
// (premultiplyAlphaHonored: false); there WebGL premultiplies on upload.
|
|
112
114
|
createImageBitmap(blob)
|
|
113
115
|
.then(function (data) {
|
|
114
|
-
resolve({ data: data, premultiplyAlpha:
|
|
116
|
+
resolve({ data: data, premultiplyAlpha: useGlPremultiply });
|
|
115
117
|
})
|
|
116
118
|
.catch(function (error) {
|
|
117
119
|
reject(error);
|
|
@@ -150,11 +152,18 @@ function createImageWorker() {
|
|
|
150
152
|
var width = event.data.sw;
|
|
151
153
|
var height = event.data.sh;
|
|
152
154
|
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
//
|
|
157
|
-
|
|
155
|
+
// Capability flags are sent as message DATA (not baked into the worker's
|
|
156
|
+
// source text) because they must survive minification: a production
|
|
157
|
+
// bundler renames these local variable names, which silently breaks any
|
|
158
|
+
// scheme that pattern-matches `createImageWorker.toString()` output for
|
|
159
|
+
// injection (see git history for the string-replace approach this
|
|
160
|
+
// replaced, and the bug it caused - capability detection was always a
|
|
161
|
+
// no-op in a minified build, regardless of device or config).
|
|
162
|
+
var supportsOptionsCreateImageBitmap =
|
|
163
|
+
event.data.supportsOptionsCreateImageBitmap;
|
|
164
|
+
var supportsFullCreateImageBitmap =
|
|
165
|
+
event.data.supportsFullCreateImageBitmap;
|
|
166
|
+
var premultiplyAlphaHonored = event.data.premultiplyAlphaHonored;
|
|
158
167
|
|
|
159
168
|
getImage(src, premultiplyAlpha, x, y, width, height, {
|
|
160
169
|
supportsOptionsCreateImageBitmap,
|
|
@@ -199,7 +208,7 @@ export class ImageWorkerManager {
|
|
|
199
208
|
if (this.workers.length > 0) {
|
|
200
209
|
return;
|
|
201
210
|
}
|
|
202
|
-
this.workerBlob = this.createWorkerBlob(
|
|
211
|
+
this.workerBlob = this.createWorkerBlob();
|
|
203
212
|
for (let i = 0; i < this.maxWorkers; i++) {
|
|
204
213
|
this.spawnWorker();
|
|
205
214
|
}
|
|
@@ -243,38 +252,18 @@ export class ImageWorkerManager {
|
|
|
243
252
|
this.workerLoad[workerIndex] = 0;
|
|
244
253
|
}
|
|
245
254
|
|
|
246
|
-
private createWorkerBlob(
|
|
247
|
-
|
|
248
|
-
|
|
255
|
+
private createWorkerBlob(): Blob {
|
|
256
|
+
// Capability flags (options/full/premultiplyHonored) are NOT injected
|
|
257
|
+
// into this source text. An earlier version pattern-matched
|
|
258
|
+
// `createImageWorker.toString()` against literal variable-name strings
|
|
259
|
+
// (e.g. 'var supportsOptionsCreateImageBitmap = false;') and rewrote
|
|
260
|
+
// them - this silently breaks under any minifier that renames locals
|
|
261
|
+
// (guaranteed in a real production bundle), permanently freezing every
|
|
262
|
+
// device on the hardcoded defaults regardless of actual support. The
|
|
263
|
+
// flags are sent per-request as postMessage data instead (see
|
|
264
|
+
// `getImage` below and `self.onmessage` above) - plain data survives
|
|
265
|
+
// minification untouched.
|
|
249
266
|
let workerCode = `(${createImageWorker.toString()})()`;
|
|
250
|
-
|
|
251
|
-
// Replace placeholders with actual initialization values
|
|
252
|
-
if (createImageBitmapSupport.options === true) {
|
|
253
|
-
workerCode = workerCode.replace(
|
|
254
|
-
'var supportsOptionsCreateImageBitmap = false;',
|
|
255
|
-
'var supportsOptionsCreateImageBitmap = true;',
|
|
256
|
-
);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
if (createImageBitmapSupport.full === true) {
|
|
260
|
-
workerCode = workerCode.replace(
|
|
261
|
-
'var supportsOptionsCreateImageBitmap = false;',
|
|
262
|
-
'var supportsOptionsCreateImageBitmap = true;',
|
|
263
|
-
);
|
|
264
|
-
|
|
265
|
-
workerCode = workerCode.replace(
|
|
266
|
-
'var supportsFullCreateImageBitmap = false;',
|
|
267
|
-
'var supportsFullCreateImageBitmap = true;',
|
|
268
|
-
);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
if (createImageBitmapSupport.premultiplyHonored === false) {
|
|
272
|
-
workerCode = workerCode.replace(
|
|
273
|
-
'var premultiplyAlphaHonored = true;',
|
|
274
|
-
'var premultiplyAlphaHonored = false;',
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
267
|
workerCode = workerCode.replace('"use strict";', '');
|
|
279
268
|
return new Blob([workerCode], {
|
|
280
269
|
type: 'application/javascript',
|
|
@@ -368,6 +357,11 @@ export class ImageWorkerManager {
|
|
|
368
357
|
sy,
|
|
369
358
|
sw,
|
|
370
359
|
sh,
|
|
360
|
+
supportsOptionsCreateImageBitmap:
|
|
361
|
+
this.createImageBitmapSupport.options,
|
|
362
|
+
supportsFullCreateImageBitmap: this.createImageBitmapSupport.full,
|
|
363
|
+
premultiplyAlphaHonored:
|
|
364
|
+
this.createImageBitmapSupport.premultiplyHonored,
|
|
371
365
|
});
|
|
372
366
|
} catch (error) {
|
|
373
367
|
reject(error);
|
|
@@ -65,21 +65,19 @@ function createPlatform(gl: object | null): Platform {
|
|
|
65
65
|
|
|
66
66
|
describe('detectPremultiplyAlphaHonored', () => {
|
|
67
67
|
beforeEach(() => {
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
this.
|
|
75
|
-
this.width = width;
|
|
76
|
-
this.height = height;
|
|
68
|
+
// The probe decodes a real PNG blob (not ImageData), so node's missing
|
|
69
|
+
// Blob global must be stubbed. It just needs to be constructible and
|
|
70
|
+
// preserve the mime type the probe tags it with.
|
|
71
|
+
(globalThis as unknown as { Blob: unknown }).Blob = class {
|
|
72
|
+
constructor(public parts: unknown[], public opts: { type?: string }) {}
|
|
73
|
+
get type() {
|
|
74
|
+
return this.opts?.type ?? '';
|
|
77
75
|
}
|
|
78
76
|
};
|
|
79
77
|
});
|
|
80
78
|
|
|
81
79
|
afterEach(() => {
|
|
82
|
-
delete (globalThis as unknown as {
|
|
80
|
+
delete (globalThis as unknown as { Blob?: unknown }).Blob;
|
|
83
81
|
});
|
|
84
82
|
|
|
85
83
|
it('returns true when the bitmap reads back premultiplied (~128)', async () => {
|
|
@@ -87,6 +85,15 @@ describe('detectPremultiplyAlphaHonored', () => {
|
|
|
87
85
|
expect(await detectPremultiplyAlphaHonored(platform)).toBe(true);
|
|
88
86
|
});
|
|
89
87
|
|
|
88
|
+
it('decodes a real PNG blob with the premultiply option (the path real textures take)', async () => {
|
|
89
|
+
const platform = createPlatform(createFakeGl(128));
|
|
90
|
+
await detectPremultiplyAlphaHonored(platform);
|
|
91
|
+
const call = (platform.createImageBitmap as ReturnType<typeof vi.fn>).mock
|
|
92
|
+
.calls[0];
|
|
93
|
+
expect((call![0] as { type: string }).type).toBe('image/png');
|
|
94
|
+
expect(call![1]).toMatchObject({ premultiplyAlpha: 'premultiply' });
|
|
95
|
+
});
|
|
96
|
+
|
|
90
97
|
it('returns false when the bitmap reads back straight (~255)', async () => {
|
|
91
98
|
const platform = createPlatform(createFakeGl(255));
|
|
92
99
|
expect(await detectPremultiplyAlphaHonored(platform)).toBe(false);
|
|
@@ -95,34 +95,52 @@ export async function validateCreateImageBitmap(
|
|
|
95
95
|
return support;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
// 1x1 PNG, color type 6 (RGBA), single pixel = (255, 0, 0, 128) in straight
|
|
99
|
+
// (un-premultiplied) alpha. Decoding this real PNG blob exercises the same
|
|
100
|
+
// image-decode pipeline that actual textures use — which on embedded WebKit
|
|
101
|
+
// forks can honor `premultiplyAlpha` differently from the raw
|
|
102
|
+
// ImageData->createImageBitmap path (some decoders ignore the option on the
|
|
103
|
+
// PNG path while honoring it for ImageData). Detecting via the path real
|
|
104
|
+
// images take is what makes this correct on devices like the Movistar STB.
|
|
105
|
+
// prettier-ignore
|
|
106
|
+
const STRAIGHT_ALPHA_TEST_PNG = new Uint8Array([
|
|
107
|
+
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
|
|
108
|
+
0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
|
|
109
|
+
0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4, 0x89, 0x00, 0x00, 0x00,
|
|
110
|
+
0x0d, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0x63, 0xf8, 0xcf, 0xc0, 0xd0,
|
|
111
|
+
0x00, 0x00, 0x04, 0x81, 0x01, 0x80, 0x2c, 0x55, 0xce, 0xb0, 0x00, 0x00,
|
|
112
|
+
0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
|
|
113
|
+
]);
|
|
114
|
+
|
|
98
115
|
/**
|
|
99
116
|
* Determine whether `createImageBitmap(..., { premultiplyAlpha: 'premultiply' })`
|
|
100
117
|
* is actually honored by this browser.
|
|
101
118
|
*
|
|
102
|
-
* Strategy:
|
|
103
|
-
* createImageBitmap with 'premultiply', upload it to a
|
|
104
|
-
* GL-side premultiply DISABLED (so we observe the bitmap's
|
|
105
|
-
* read the raw texel back via a framebuffer.
|
|
119
|
+
* Strategy: decode a known straight-alpha pixel (255, 0, 0, 128) delivered as
|
|
120
|
+
* a real PNG blob through createImageBitmap with 'premultiply', upload it to a
|
|
121
|
+
* WebGL texture with GL-side premultiply DISABLED (so we observe the bitmap's
|
|
122
|
+
* own state), then read the raw texel back via a framebuffer.
|
|
106
123
|
*
|
|
107
124
|
* - honored -> red comes back premultiplied (~128)
|
|
108
|
-
* - ignored -> red comes back straight (~255) [older Safari/WebKit
|
|
125
|
+
* - ignored -> red comes back straight (~255) [older Safari/WebKit,
|
|
126
|
+
* Movistar-class embedded STBs]
|
|
127
|
+
*
|
|
128
|
+
* A real PNG blob (not ImageData) is used deliberately: it matches the code
|
|
129
|
+
* path real textures take, and some embedded devices don't even support
|
|
130
|
+
* ImageData->createImageBitmap (the old ImageData probe returned null there,
|
|
131
|
+
* silently disabling detection on exactly the devices that needed it).
|
|
109
132
|
*
|
|
110
133
|
* @returns true if honored, false if ignored, null if it couldn't be measured
|
|
111
|
-
* (no WebGL, createImageBitmap
|
|
112
|
-
*
|
|
134
|
+
* (no WebGL, createImageBitmap unsupported, framebuffer incomplete, etc.) —
|
|
135
|
+
* caller should treat null as "unknown".
|
|
113
136
|
*/
|
|
114
137
|
export async function detectPremultiplyAlphaHonored(
|
|
115
138
|
platform: Platform,
|
|
116
139
|
): Promise<boolean | null> {
|
|
117
140
|
let bitmap: ImageBitmap;
|
|
118
141
|
try {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
new Uint8ClampedArray([255, 0, 0, 128]),
|
|
122
|
-
1,
|
|
123
|
-
1,
|
|
124
|
-
);
|
|
125
|
-
bitmap = await platform.createImageBitmap(imageData, {
|
|
142
|
+
const blob = new Blob([STRAIGHT_ALPHA_TEST_PNG], { type: 'image/png' });
|
|
143
|
+
bitmap = await platform.createImageBitmap(blob, {
|
|
126
144
|
premultiplyAlpha: 'premultiply',
|
|
127
145
|
colorSpaceConversion: 'none',
|
|
128
146
|
imageOrientation: 'none',
|
|
@@ -65,37 +65,57 @@ export const loadFont = (
|
|
|
65
65
|
): Promise<void> => {
|
|
66
66
|
const { fontFamily, fontUrl, metrics } = options;
|
|
67
67
|
|
|
68
|
+
// A single load may register the font under several names. The first entry
|
|
69
|
+
// is the primary name used to key the in-flight promise; the rest are
|
|
70
|
+
// aliases registered against the same source URL (the browser dedupes the
|
|
71
|
+
// download by URL).
|
|
72
|
+
const names = Array.isArray(fontFamily) ? fontFamily : [fontFamily];
|
|
73
|
+
const primary = names[0]!;
|
|
74
|
+
|
|
68
75
|
// If already loaded, return immediately
|
|
69
|
-
if (fontCache.has(
|
|
76
|
+
if (fontCache.has(primary) === true) {
|
|
70
77
|
return Promise.resolve();
|
|
71
78
|
}
|
|
72
79
|
|
|
73
|
-
const existingPromise = fontLoadPromises.get(
|
|
80
|
+
const existingPromise = fontLoadPromises.get(primary);
|
|
74
81
|
// If already loading, return the existing promise
|
|
75
82
|
if (existingPromise !== undefined) {
|
|
76
83
|
return existingPromise;
|
|
77
84
|
}
|
|
78
85
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
for (let i = 0; i < names.length; i++) {
|
|
87
|
+
nodesWaitingForFont[names[i]!] = [];
|
|
88
|
+
}
|
|
89
|
+
// Register a FontFace under every name (all share the same source URL) and
|
|
90
|
+
// wait for all of them to be ready before waking parked nodes.
|
|
91
|
+
const loadPromise = Promise.all(
|
|
92
|
+
names.map((name) =>
|
|
93
|
+
new FontFace(name, `url(${fontUrl})`).load().then((loadedFont) => {
|
|
94
|
+
stage.platform.addFont(loadedFont);
|
|
95
|
+
processFontData(name, loadedFont, metrics);
|
|
96
|
+
}),
|
|
97
|
+
),
|
|
98
|
+
)
|
|
99
|
+
.then(() => {
|
|
100
|
+
fontLoadPromises.delete(primary);
|
|
101
|
+
for (let i = 0; i < names.length; i++) {
|
|
102
|
+
const name = names[i]!;
|
|
103
|
+
const nwff = nodesWaitingForFont[name];
|
|
104
|
+
if (nwff !== undefined) {
|
|
105
|
+
for (let key in nwff) {
|
|
106
|
+
nwff[key]!.setUpdateType(UpdateType.Local);
|
|
107
|
+
}
|
|
108
|
+
delete nodesWaitingForFont[name];
|
|
109
|
+
}
|
|
89
110
|
}
|
|
90
|
-
delete nodesWaitingForFont[fontFamily];
|
|
91
111
|
})
|
|
92
112
|
.catch((error) => {
|
|
93
|
-
fontLoadPromises.delete(
|
|
94
|
-
console.error(`Failed to load font: ${
|
|
113
|
+
fontLoadPromises.delete(primary);
|
|
114
|
+
console.error(`Failed to load font: ${primary}`, error);
|
|
95
115
|
throw error;
|
|
96
116
|
});
|
|
97
117
|
|
|
98
|
-
fontLoadPromises.set(
|
|
118
|
+
fontLoadPromises.set(primary, loadPromise);
|
|
99
119
|
return loadPromise;
|
|
100
120
|
};
|
|
101
121
|
|
|
@@ -309,30 +309,39 @@ export const loadFont = (
|
|
|
309
309
|
options: FontLoadOptions,
|
|
310
310
|
): Promise<void> => {
|
|
311
311
|
const { fontFamily, atlasUrl, atlasDataUrl, metrics } = options;
|
|
312
|
+
// A single load may register the font under several names (the atlas is
|
|
313
|
+
// fetched once and shared). The first entry is the primary name used to key
|
|
314
|
+
// the in-flight promise and drive the fetch; the rest are aliases.
|
|
315
|
+
const names = Array.isArray(fontFamily) ? fontFamily : [fontFamily];
|
|
316
|
+
const primary = names[0]!;
|
|
317
|
+
|
|
312
318
|
// Early return if already loaded
|
|
313
|
-
if (fontCache.get(
|
|
319
|
+
if (fontCache.get(primary) !== undefined) {
|
|
314
320
|
return Promise.resolve();
|
|
315
321
|
}
|
|
316
322
|
|
|
317
323
|
// Early return if already loading
|
|
318
|
-
const existingPromise = fontLoadPromises.get(
|
|
324
|
+
const existingPromise = fontLoadPromises.get(primary);
|
|
319
325
|
if (existingPromise !== undefined) {
|
|
320
326
|
return existingPromise;
|
|
321
327
|
}
|
|
322
328
|
|
|
323
329
|
if (atlasDataUrl === undefined) {
|
|
324
330
|
return Promise.reject(
|
|
325
|
-
new Error(`Atlas data URL must be provided for SDF font: ${
|
|
331
|
+
new Error(`Atlas data URL must be provided for SDF font: ${primary}`),
|
|
326
332
|
);
|
|
327
333
|
}
|
|
328
334
|
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
//
|
|
332
|
-
//
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
335
|
+
// Ensure every name has a waiter list so nodes requesting an alias can park
|
|
336
|
+
// and be woken on load. Reuse existing lists — a previous load attempt for a
|
|
337
|
+
// name may have failed and left nodes parked there; overwriting the list
|
|
338
|
+
// would strand them, so a successful retry could never wake them. Lists are
|
|
339
|
+
// consumed (and deleted) on the next successful load.
|
|
340
|
+
for (let i = 0; i < names.length; i++) {
|
|
341
|
+
const name = names[i]!;
|
|
342
|
+
if (nodesWaitingForFont[name] === undefined) {
|
|
343
|
+
nodesWaitingForFont[name] = [];
|
|
344
|
+
}
|
|
336
345
|
}
|
|
337
346
|
// One attempt at fetching + decoding the JSON atlas description. A fresh
|
|
338
347
|
// XHR runs per attempt so a transient network/parse failure can recover.
|
|
@@ -390,17 +399,34 @@ export const loadFont = (
|
|
|
390
399
|
premultiplyAlpha: false,
|
|
391
400
|
});
|
|
392
401
|
|
|
393
|
-
|
|
402
|
+
// Register every name as a renderable owner so the shared atlas stays
|
|
403
|
+
// alive as long as any of its names is in use.
|
|
404
|
+
for (let i = 0; i < names.length; i++) {
|
|
405
|
+
atlasTexture.setRenderableOwner(names[i]!, true);
|
|
406
|
+
}
|
|
394
407
|
atlasTexture.preventCleanup = true; // Prevent automatic cleanup
|
|
395
408
|
|
|
396
409
|
const onLoaded = () => {
|
|
397
|
-
// Process and cache font data
|
|
398
|
-
processFontData(
|
|
410
|
+
// Process and cache font data under the primary name...
|
|
411
|
+
processFontData(primary, fontData, atlasTexture, metrics);
|
|
412
|
+
// ...then alias the remaining names onto the same cache entry so they
|
|
413
|
+
// resolve to the identical glyph/kerning/atlas data.
|
|
414
|
+
const cached = fontCache.get(primary)!;
|
|
415
|
+
for (let i = 1; i < names.length; i++) {
|
|
416
|
+
fontCache.set(names[i]!, cached);
|
|
417
|
+
}
|
|
399
418
|
|
|
400
|
-
|
|
401
|
-
|
|
419
|
+
// Wake every parked node across all names and clear their lists.
|
|
420
|
+
for (let i = 0; i < names.length; i++) {
|
|
421
|
+
const name = names[i]!;
|
|
422
|
+
const list = nodesWaitingForFont[name];
|
|
423
|
+
if (list !== undefined) {
|
|
424
|
+
for (let key in list) {
|
|
425
|
+
list[key]!.setUpdateType(UpdateType.Local);
|
|
426
|
+
}
|
|
427
|
+
delete nodesWaitingForFont[name];
|
|
428
|
+
}
|
|
402
429
|
}
|
|
403
|
-
delete nodesWaitingForFont[fontFamily];
|
|
404
430
|
resolve();
|
|
405
431
|
};
|
|
406
432
|
|
|
@@ -419,7 +445,9 @@ export const loadFont = (
|
|
|
419
445
|
atlasTexture.on('failed', (_target, error: TextureError) => {
|
|
420
446
|
// Drop the failed atlas so a retry builds a fresh texture rather than
|
|
421
447
|
// getting this dead instance back from the createTexture key-cache.
|
|
422
|
-
|
|
448
|
+
for (let i = 0; i < names.length; i++) {
|
|
449
|
+
atlasTexture.setRenderableOwner(names[i]!, false);
|
|
450
|
+
}
|
|
423
451
|
stage.txManager.removeTextureFromCache(atlasTexture);
|
|
424
452
|
reject(error);
|
|
425
453
|
});
|
|
@@ -434,15 +462,15 @@ export const loadFont = (
|
|
|
434
462
|
await loadAtlas(await fetchFontData());
|
|
435
463
|
// Success: clear the in-flight marker (the font now lives in fontCache)
|
|
436
464
|
// — parked nodes were already woken inside loadAtlas.
|
|
437
|
-
fontLoadPromises.delete(
|
|
465
|
+
fontLoadPromises.delete(primary);
|
|
438
466
|
return;
|
|
439
467
|
} catch (error) {
|
|
440
468
|
lastError = error;
|
|
441
469
|
if (attempt < MAX_FONT_LOAD_RETRIES) {
|
|
442
470
|
console.warn(
|
|
443
|
-
`SDF font "${
|
|
444
|
-
|
|
445
|
-
}
|
|
471
|
+
`SDF font "${primary}" failed to load (attempt ${attempt + 1} of ${
|
|
472
|
+
MAX_FONT_LOAD_RETRIES + 1
|
|
473
|
+
}), retrying.`,
|
|
446
474
|
error,
|
|
447
475
|
);
|
|
448
476
|
}
|
|
@@ -455,13 +483,15 @@ export const loadFont = (
|
|
|
455
483
|
// loadFont() (which reuses the list) can still wake them if the font
|
|
456
484
|
// eventually loads. The list shrinks as nodes self-remove via
|
|
457
485
|
// stopWaitingForFont on destroy.
|
|
458
|
-
fontLoadPromises.delete(
|
|
459
|
-
|
|
460
|
-
|
|
486
|
+
fontLoadPromises.delete(primary);
|
|
487
|
+
for (let i = 0; i < names.length; i++) {
|
|
488
|
+
fontCache.delete(names[i]!);
|
|
489
|
+
}
|
|
490
|
+
console.error(`Failed to load SDF font: ${primary}`, lastError);
|
|
461
491
|
throw lastError;
|
|
462
492
|
})();
|
|
463
493
|
|
|
464
|
-
fontLoadPromises.set(
|
|
494
|
+
fontLoadPromises.set(primary, loadPromise);
|
|
465
495
|
return loadPromise;
|
|
466
496
|
};
|
|
467
497
|
|
|
@@ -349,7 +349,16 @@ export interface TextLayout {
|
|
|
349
349
|
}
|
|
350
350
|
|
|
351
351
|
export interface FontLoadOptions {
|
|
352
|
-
|
|
352
|
+
/**
|
|
353
|
+
* Font family name to register the font under.
|
|
354
|
+
*
|
|
355
|
+
* Pass an array to register the same font under several names in a single
|
|
356
|
+
* load — the atlas/font resource is fetched once and every name resolves to
|
|
357
|
+
* the same loaded font. The first entry is the primary name; the rest are
|
|
358
|
+
* aliases. Useful when one physical font is referenced by multiple families
|
|
359
|
+
* (e.g. `['Roboto', 'Roboto500']`).
|
|
360
|
+
*/
|
|
361
|
+
fontFamily: string | string[];
|
|
353
362
|
metrics?: FontMetrics;
|
|
354
363
|
// For Canvas/traditional font loading
|
|
355
364
|
fontUrl?: string;
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
loadFont,
|
|
4
4
|
waitingForFont,
|
|
5
5
|
isFontLoaded,
|
|
6
|
+
getFontData,
|
|
6
7
|
MAX_FONT_LOAD_RETRIES,
|
|
7
8
|
} from '../SdfFontHandler.js';
|
|
8
9
|
import { EventEmitter } from '../../../common/EventEmitter.js';
|
|
@@ -71,7 +72,7 @@ function makeStage(): { stage: Stage; textures: FakeTexture[] } {
|
|
|
71
72
|
return { stage, textures };
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
const opts = (fontFamily: string) =>
|
|
75
|
+
const opts = (fontFamily: string | string[]) =>
|
|
75
76
|
({
|
|
76
77
|
fontFamily,
|
|
77
78
|
atlasUrl: 'atlas.png',
|
|
@@ -256,3 +257,66 @@ describe('SdfFontHandler loadFont — automatic retry', () => {
|
|
|
256
257
|
expect(node.setUpdateType).toHaveBeenCalledTimes(1);
|
|
257
258
|
});
|
|
258
259
|
});
|
|
260
|
+
|
|
261
|
+
describe('SdfFontHandler loadFont — multiple names for one font', () => {
|
|
262
|
+
let originalXHR: unknown;
|
|
263
|
+
|
|
264
|
+
beforeEach(() => {
|
|
265
|
+
originalXHR = (globalThis as unknown as { XMLHttpRequest: unknown })
|
|
266
|
+
.XMLHttpRequest;
|
|
267
|
+
(globalThis as unknown as { XMLHttpRequest: unknown }).XMLHttpRequest =
|
|
268
|
+
FakeXHR;
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
afterEach(() => {
|
|
272
|
+
(globalThis as unknown as { XMLHttpRequest: unknown }).XMLHttpRequest =
|
|
273
|
+
originalXHR;
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('fetches once and registers every name against the same font data', async () => {
|
|
277
|
+
const { stage, textures } = makeStage();
|
|
278
|
+
const names = ['Roboto', 'Roboto500', 'RobotoMedium'];
|
|
279
|
+
|
|
280
|
+
const promise = loadFont(stage, opts(names));
|
|
281
|
+
await flush();
|
|
282
|
+
textures[0]!.emit('loaded');
|
|
283
|
+
await promise;
|
|
284
|
+
|
|
285
|
+
// A single atlas texture was created for all three names.
|
|
286
|
+
expect(textures.length).toBe(1);
|
|
287
|
+
|
|
288
|
+
// Every name reports loaded and resolves to the identical cache entry.
|
|
289
|
+
const primaryData = getFontData(names[0]!);
|
|
290
|
+
expect(primaryData).not.toBe(undefined);
|
|
291
|
+
for (let i = 0; i < names.length; i++) {
|
|
292
|
+
expect(isFontLoaded(names[i]!)).toBe(true);
|
|
293
|
+
expect(getFontData(names[i]!)).toBe(primaryData);
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('wakes nodes parked under an alias, not just the primary name', async () => {
|
|
298
|
+
const { stage, textures } = makeStage();
|
|
299
|
+
const names = ['MultiWakePrimary', 'MultiWakeAlias'];
|
|
300
|
+
|
|
301
|
+
const promise = loadFont(stage, opts(names));
|
|
302
|
+
|
|
303
|
+
// Park one node under the primary name and one under the alias.
|
|
304
|
+
const primaryNode = { id: 1, setUpdateType: vi.fn() };
|
|
305
|
+
const aliasNode = { id: 2, setUpdateType: vi.fn() };
|
|
306
|
+
waitingForFont(
|
|
307
|
+
names[0]!,
|
|
308
|
+
primaryNode as unknown as Parameters<typeof waitingForFont>[1],
|
|
309
|
+
);
|
|
310
|
+
waitingForFont(
|
|
311
|
+
names[1]!,
|
|
312
|
+
aliasNode as unknown as Parameters<typeof waitingForFont>[1],
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
await flush();
|
|
316
|
+
textures[0]!.emit('loaded');
|
|
317
|
+
await promise;
|
|
318
|
+
|
|
319
|
+
expect(primaryNode.setUpdateType).toHaveBeenCalledTimes(1);
|
|
320
|
+
expect(aliasNode.setUpdateType).toHaveBeenCalledTimes(1);
|
|
321
|
+
});
|
|
322
|
+
});
|