@solidtv/renderer 1.6.1 → 1.6.3
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/exports/index.d.ts +1 -0
- package/dist/exports/index.js +3 -0
- package/dist/exports/index.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 +51 -16
- package/dist/src/core/text-rendering/CanvasFontHandler.js.map +1 -1
- package/dist/src/core/text-rendering/FontPrefetch.d.ts +55 -0
- package/dist/src/core/text-rendering/FontPrefetch.js +140 -0
- package/dist/src/core/text-rendering/FontPrefetch.js.map +1 -0
- package/dist/src/core/text-rendering/SdfFontHandler.js +91 -30
- 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 +8 -4
- package/dist/src/main-api/Renderer.js.map +1 -1
- package/dist/tsconfig.dist.tsbuildinfo +1 -1
- package/exports/index.ts +7 -0
- package/package.json +1 -1
- 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 +57 -16
- package/src/core/text-rendering/FontPrefetch.ts +195 -0
- package/src/core/text-rendering/SdfFontHandler.ts +102 -32
- package/src/core/text-rendering/TextRenderer.ts +10 -1
- package/src/core/text-rendering/tests/FontPrefetch.test.ts +341 -0
- package/src/core/text-rendering/tests/SdfFontHandler.test.ts +108 -1
- package/src/core/textures/ImageTexture.test.ts +94 -26
- package/src/core/textures/ImageTexture.ts +4 -2
- package/src/main-api/Renderer.ts +8 -4
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
defaultFontMetrics,
|
|
13
13
|
normalizeFontMetrics,
|
|
14
14
|
} from './TextLayoutEngine.js';
|
|
15
|
+
import { takeCanvasPrefetch } from './FontPrefetch.js';
|
|
15
16
|
|
|
16
17
|
interface CanvasFont {
|
|
17
18
|
fontFamily: string;
|
|
@@ -65,37 +66,77 @@ export const loadFont = (
|
|
|
65
66
|
): Promise<void> => {
|
|
66
67
|
const { fontFamily, fontUrl, metrics } = options;
|
|
67
68
|
|
|
69
|
+
// A single load may register the font under several names. The first entry
|
|
70
|
+
// is the primary name used to key the in-flight promise; the rest are
|
|
71
|
+
// aliases registered against the same source URL (the browser dedupes the
|
|
72
|
+
// download by URL).
|
|
73
|
+
const names = Array.isArray(fontFamily) ? fontFamily : [fontFamily];
|
|
74
|
+
const primary = names[0]!;
|
|
75
|
+
|
|
76
|
+
// Faces whose download was started before the renderer existed, if any.
|
|
77
|
+
// Claimed ahead of the early returns so an unneeded prefetch is released.
|
|
78
|
+
const prefetched = takeCanvasPrefetch(primary);
|
|
79
|
+
|
|
68
80
|
// If already loaded, return immediately
|
|
69
|
-
if (fontCache.has(
|
|
81
|
+
if (fontCache.has(primary) === true) {
|
|
70
82
|
return Promise.resolve();
|
|
71
83
|
}
|
|
72
84
|
|
|
73
|
-
const existingPromise = fontLoadPromises.get(
|
|
85
|
+
const existingPromise = fontLoadPromises.get(primary);
|
|
74
86
|
// If already loading, return the existing promise
|
|
75
87
|
if (existingPromise !== undefined) {
|
|
76
88
|
return existingPromise;
|
|
77
89
|
}
|
|
78
90
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
91
|
+
for (let i = 0; i < names.length; i++) {
|
|
92
|
+
nodesWaitingForFont[names[i]!] = [];
|
|
93
|
+
}
|
|
94
|
+
// Register a FontFace under every name (all share the same source URL) and
|
|
95
|
+
// wait for all of them to be ready before waking parked nodes. Prefetched
|
|
96
|
+
// faces are already loaded — they only still need registering with the
|
|
97
|
+
// platform, which is the one step that required a stage.
|
|
98
|
+
const loadPromise = Promise.resolve(prefetched)
|
|
99
|
+
.then((faces) =>
|
|
100
|
+
faces != null && faces.length === names.length
|
|
101
|
+
? faces
|
|
102
|
+
: Promise.all(
|
|
103
|
+
names.map((name) => new FontFace(name, `url(${fontUrl})`).load()),
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
.then((faces) => {
|
|
107
|
+
for (let i = 0; i < names.length; i++) {
|
|
108
|
+
const loadedFont = faces[i]!;
|
|
109
|
+
stage.platform.addFont(loadedFont);
|
|
110
|
+
processFontData(names[i]!, loadedFont, metrics);
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
.then(() => {
|
|
114
|
+
for (let i = 0; i < names.length; i++) {
|
|
115
|
+
const name = names[i]!;
|
|
116
|
+
fontLoadPromises.delete(name);
|
|
117
|
+
const nwff = nodesWaitingForFont[name];
|
|
118
|
+
if (nwff !== undefined) {
|
|
119
|
+
for (let key in nwff) {
|
|
120
|
+
nwff[key]!.setUpdateType(UpdateType.Local);
|
|
121
|
+
}
|
|
122
|
+
delete nodesWaitingForFont[name];
|
|
123
|
+
}
|
|
89
124
|
}
|
|
90
|
-
delete nodesWaitingForFont[fontFamily];
|
|
91
125
|
})
|
|
92
126
|
.catch((error) => {
|
|
93
|
-
|
|
94
|
-
|
|
127
|
+
for (let i = 0; i < names.length; i++) {
|
|
128
|
+
fontLoadPromises.delete(names[i]!);
|
|
129
|
+
}
|
|
130
|
+
console.error(`Failed to load font: ${primary}`, error);
|
|
95
131
|
throw error;
|
|
96
132
|
});
|
|
97
133
|
|
|
98
|
-
|
|
134
|
+
// Keyed under every name so a later loadFont() for an alias dedupes onto
|
|
135
|
+
// this load rather than starting a second one. (Canvas `canRenderFont` is
|
|
136
|
+
// unconditionally true, so unlike SDF this does not affect renderer choice.)
|
|
137
|
+
for (let i = 0; i < names.length; i++) {
|
|
138
|
+
fontLoadPromises.set(names[i]!, loadPromise);
|
|
139
|
+
}
|
|
99
140
|
return loadPromise;
|
|
100
141
|
};
|
|
101
142
|
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Font prefetching — stage-free.
|
|
3
|
+
*
|
|
4
|
+
* Font bytes normally only start moving once a Stage exists, because
|
|
5
|
+
* `Stage.loadFont()` is the only entry point. That serializes the network
|
|
6
|
+
* behind the whole synchronous boot (GL context, buffers, shaders, root node)
|
|
7
|
+
* and, for SDF, behind the texture manager's async init as well.
|
|
8
|
+
*
|
|
9
|
+
* Nothing about *fetching* a font needs a Stage: the SDF atlas is a JSON
|
|
10
|
+
* document plus an image, and a web font is a `FontFace`. This module starts
|
|
11
|
+
* that work as early as an app can call it — typically before the renderer is
|
|
12
|
+
* constructed — and parks the results. The font handlers pick them up from
|
|
13
|
+
* here when `loadFont()` eventually runs, so the round-trip overlaps CPU boot
|
|
14
|
+
* instead of following it.
|
|
15
|
+
*
|
|
16
|
+
* Everything here uses XHR rather than `fetch` to match the rest of the
|
|
17
|
+
* renderer (Chrome 38 / webOS 3 have no `fetch`).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { FontLoadOptions } from './TextRenderer.js';
|
|
21
|
+
import type { SdfFontData } from './SdfFontHandler.js';
|
|
22
|
+
import { fetchJson } from '../lib/utils.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A prefetched SDF font: the parsed atlas description and the atlas image,
|
|
26
|
+
* fetched concurrently. Either promise resolves to `null` if its request
|
|
27
|
+
* failed — the handler then falls back to its normal load path rather than
|
|
28
|
+
* inheriting the failure.
|
|
29
|
+
*/
|
|
30
|
+
export interface PrefetchedSdfFont {
|
|
31
|
+
data: Promise<SdfFontData | null>;
|
|
32
|
+
atlas: Promise<Blob | null>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const sdfPrefetch = new Map<string, PrefetchedSdfFont>();
|
|
36
|
+
const canvasPrefetch = new Map<string, Promise<FontFace[] | null>>();
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Options accepted by {@link prefetchFont}. Identical to the options passed to
|
|
40
|
+
* `Stage.loadFont()`, plus the optional SDF `type` discriminator that
|
|
41
|
+
* framework layers already carry on their font descriptors.
|
|
42
|
+
*/
|
|
43
|
+
export type FontPrefetchOptions = FontLoadOptions & {
|
|
44
|
+
type?: 'ssdf' | 'msdf' | 'canvas';
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Swallow a rejection into `null` so a prefetch that fails can never surface
|
|
49
|
+
* as an unhandled rejection. A prefetch is an optimization: when it fails the
|
|
50
|
+
* handler just does the work itself.
|
|
51
|
+
*/
|
|
52
|
+
const orNull = <T>(promise: Promise<T>): Promise<T | null> =>
|
|
53
|
+
promise.then(
|
|
54
|
+
(value) => value,
|
|
55
|
+
() => null,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Normalize an atlas description response into usable font data, or `null` if
|
|
60
|
+
* it is unusable.
|
|
61
|
+
*
|
|
62
|
+
* `fetchJson` resolves rather than rejects on a status of 0 (the file://
|
|
63
|
+
* allowance), so a failed request arrives here as `null`. Engines without
|
|
64
|
+
* `responseType = 'json'` support hand back the raw string instead of a parsed
|
|
65
|
+
* object — the SDF handler tolerates both, so this does too.
|
|
66
|
+
*/
|
|
67
|
+
const normalizeSdfData = (response: unknown): SdfFontData | null => {
|
|
68
|
+
let data = response;
|
|
69
|
+
|
|
70
|
+
if (typeof data === 'string') {
|
|
71
|
+
try {
|
|
72
|
+
data = JSON.parse(data);
|
|
73
|
+
} catch (e) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (data === null || typeof data !== 'object') {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Reject anything the handler would reject anyway, so a malformed prefetch
|
|
83
|
+
// falls back to a fresh fetch instead of failing the load outright.
|
|
84
|
+
return (data as SdfFontData).chars !== undefined
|
|
85
|
+
? (data as SdfFontData)
|
|
86
|
+
: null;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Start downloading a font before a renderer exists.
|
|
91
|
+
*
|
|
92
|
+
* Safe to call more than once for the same font — the first call wins and
|
|
93
|
+
* later ones are ignored. Never throws and never rejects; failures degrade to
|
|
94
|
+
* the handler's normal load path.
|
|
95
|
+
*
|
|
96
|
+
* The descriptor decides what is fetched: an `atlasDataUrl` means an SDF font
|
|
97
|
+
* (atlas JSON + atlas image, in parallel), otherwise a `fontUrl` means a web
|
|
98
|
+
* font (`FontFace.load()`).
|
|
99
|
+
*
|
|
100
|
+
* @param options - The same options later passed to `Stage.loadFont()`
|
|
101
|
+
*/
|
|
102
|
+
export const prefetchFont = (options: FontPrefetchOptions): void => {
|
|
103
|
+
const { fontFamily, fontUrl, atlasUrl, atlasDataUrl } = options;
|
|
104
|
+
const names = Array.isArray(fontFamily) ? fontFamily : [fontFamily];
|
|
105
|
+
const primary = names[0];
|
|
106
|
+
|
|
107
|
+
if (primary === undefined) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// SDF: atlas description and atlas image are independent requests. The
|
|
112
|
+
// handler fetches them sequentially (it needs the JSON before it knows the
|
|
113
|
+
// atlas is usable); here there is no such constraint, so overlap them.
|
|
114
|
+
if (atlasDataUrl !== undefined && options.type !== 'canvas') {
|
|
115
|
+
if (sdfPrefetch.has(primary) === true) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
sdfPrefetch.set(primary, {
|
|
120
|
+
data: orNull(fetchJson(atlasDataUrl, 'json')).then(normalizeSdfData),
|
|
121
|
+
atlas:
|
|
122
|
+
atlasUrl !== undefined
|
|
123
|
+
? orNull(fetchJson(atlasUrl, 'blob') as Promise<Blob>).then((blob) =>
|
|
124
|
+
blob instanceof Blob ? blob : null,
|
|
125
|
+
)
|
|
126
|
+
: Promise.resolve(null),
|
|
127
|
+
});
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Web font: `FontFace.load()` starts the download immediately. The face is
|
|
132
|
+
// not registered with the document here — that needs the platform, so the
|
|
133
|
+
// handler does it at attach time.
|
|
134
|
+
if (fontUrl !== undefined) {
|
|
135
|
+
if (canvasPrefetch.has(primary) === true) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (typeof FontFace === 'undefined') {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
canvasPrefetch.set(
|
|
144
|
+
primary,
|
|
145
|
+
orNull(
|
|
146
|
+
Promise.all(
|
|
147
|
+
names.map((name) => new FontFace(name, `url(${fontUrl})`).load()),
|
|
148
|
+
),
|
|
149
|
+
),
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Claim a prefetched SDF font, removing it from the pool.
|
|
156
|
+
*
|
|
157
|
+
* One-shot by design: a load that fails and retries must go back to the
|
|
158
|
+
* network rather than replaying a stale (possibly failed) prefetch.
|
|
159
|
+
*
|
|
160
|
+
* @param fontFamily - Primary font family name
|
|
161
|
+
*/
|
|
162
|
+
export const takeSdfPrefetch = (
|
|
163
|
+
fontFamily: string,
|
|
164
|
+
): PrefetchedSdfFont | undefined => {
|
|
165
|
+
const prefetched = sdfPrefetch.get(fontFamily);
|
|
166
|
+
if (prefetched !== undefined) {
|
|
167
|
+
sdfPrefetch.delete(fontFamily);
|
|
168
|
+
}
|
|
169
|
+
return prefetched;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Claim prefetched web font faces, removing them from the pool. One-shot for
|
|
174
|
+
* the same reason as {@link takeSdfPrefetch}.
|
|
175
|
+
*
|
|
176
|
+
* @param fontFamily - Primary font family name
|
|
177
|
+
*/
|
|
178
|
+
export const takeCanvasPrefetch = (
|
|
179
|
+
fontFamily: string,
|
|
180
|
+
): Promise<FontFace[] | null> | undefined => {
|
|
181
|
+
const prefetched = canvasPrefetch.get(fontFamily);
|
|
182
|
+
if (prefetched !== undefined) {
|
|
183
|
+
canvasPrefetch.delete(fontFamily);
|
|
184
|
+
}
|
|
185
|
+
return prefetched;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Drop every pending prefetch. Test/teardown helper — in-flight requests are
|
|
190
|
+
* not aborted, their results are simply no longer claimable.
|
|
191
|
+
*/
|
|
192
|
+
export const clearFontPrefetch = (): void => {
|
|
193
|
+
sdfPrefetch.clear();
|
|
194
|
+
canvasPrefetch.clear();
|
|
195
|
+
};
|
|
@@ -13,6 +13,7 @@ import { hasZeroWidthSpace } from './Utils.js';
|
|
|
13
13
|
import { normalizeFontMetrics } from './TextLayoutEngine.js';
|
|
14
14
|
import { isProductionEnvironment } from '../../utils.js';
|
|
15
15
|
import type { TextureError } from '../TextureError.js';
|
|
16
|
+
import { takeSdfPrefetch } from './FontPrefetch.js';
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* SDF Font Data structure matching msdf-bmfont-xml output
|
|
@@ -309,30 +310,44 @@ export const loadFont = (
|
|
|
309
310
|
options: FontLoadOptions,
|
|
310
311
|
): Promise<void> => {
|
|
311
312
|
const { fontFamily, atlasUrl, atlasDataUrl, metrics } = options;
|
|
313
|
+
// A single load may register the font under several names (the atlas is
|
|
314
|
+
// fetched once and shared). The first entry is the primary name used to key
|
|
315
|
+
// the in-flight promise and drive the fetch; the rest are aliases.
|
|
316
|
+
const names = Array.isArray(fontFamily) ? fontFamily : [fontFamily];
|
|
317
|
+
const primary = names[0]!;
|
|
318
|
+
|
|
319
|
+
// A prefetch started before the renderer existed, if any. Claimed here —
|
|
320
|
+
// ahead of the early returns — so that a font which turns out to need no
|
|
321
|
+
// load still releases the prefetched atlas blob rather than pinning it.
|
|
322
|
+
const prefetched = takeSdfPrefetch(primary);
|
|
323
|
+
|
|
312
324
|
// Early return if already loaded
|
|
313
|
-
if (fontCache.get(
|
|
325
|
+
if (fontCache.get(primary) !== undefined) {
|
|
314
326
|
return Promise.resolve();
|
|
315
327
|
}
|
|
316
328
|
|
|
317
329
|
// Early return if already loading
|
|
318
|
-
const existingPromise = fontLoadPromises.get(
|
|
330
|
+
const existingPromise = fontLoadPromises.get(primary);
|
|
319
331
|
if (existingPromise !== undefined) {
|
|
320
332
|
return existingPromise;
|
|
321
333
|
}
|
|
322
334
|
|
|
323
335
|
if (atlasDataUrl === undefined) {
|
|
324
336
|
return Promise.reject(
|
|
325
|
-
new Error(`Atlas data URL must be provided for SDF font: ${
|
|
337
|
+
new Error(`Atlas data URL must be provided for SDF font: ${primary}`),
|
|
326
338
|
);
|
|
327
339
|
}
|
|
328
340
|
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
//
|
|
332
|
-
//
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
341
|
+
// Ensure every name has a waiter list so nodes requesting an alias can park
|
|
342
|
+
// and be woken on load. Reuse existing lists — a previous load attempt for a
|
|
343
|
+
// name may have failed and left nodes parked there; overwriting the list
|
|
344
|
+
// would strand them, so a successful retry could never wake them. Lists are
|
|
345
|
+
// consumed (and deleted) on the next successful load.
|
|
346
|
+
for (let i = 0; i < names.length; i++) {
|
|
347
|
+
const name = names[i]!;
|
|
348
|
+
if (nodesWaitingForFont[name] === undefined) {
|
|
349
|
+
nodesWaitingForFont[name] = [];
|
|
350
|
+
}
|
|
336
351
|
}
|
|
337
352
|
// One attempt at fetching + decoding the JSON atlas description. A fresh
|
|
338
353
|
// XHR runs per attempt so a transient network/parse failure can recover.
|
|
@@ -371,7 +386,15 @@ export const loadFont = (
|
|
|
371
386
|
// success it processes + caches the font and wakes parked nodes. On failure
|
|
372
387
|
// it drops the dead atlas texture (createTexture caches by `src`, so the
|
|
373
388
|
// next attempt must evict it to build a fresh one) and rejects.
|
|
374
|
-
|
|
389
|
+
//
|
|
390
|
+
// `atlasBlob` is the prefetched atlas image when one was claimed. Passing it
|
|
391
|
+
// as `src` skips a second download; `key` is set to the atlas URL so the
|
|
392
|
+
// texture cache key is identical to the one the URL path produces — the two
|
|
393
|
+
// paths must not produce two cache entries for the same atlas.
|
|
394
|
+
const loadAtlas = (
|
|
395
|
+
fontData: SdfFontData,
|
|
396
|
+
atlasBlob: Blob | null,
|
|
397
|
+
): Promise<void> => {
|
|
375
398
|
if (!fontData || !fontData.chars) {
|
|
376
399
|
return Promise.reject(new Error('Invalid SDF font data format'));
|
|
377
400
|
}
|
|
@@ -386,21 +409,39 @@ export const loadFont = (
|
|
|
386
409
|
return new Promise<void>((resolve, reject) => {
|
|
387
410
|
// create new atlas texture using ImageTexture
|
|
388
411
|
const atlasTexture = stage.txManager.createTexture('ImageTexture', {
|
|
389
|
-
src: atlasUrl,
|
|
412
|
+
src: atlasBlob !== null ? atlasBlob : atlasUrl,
|
|
413
|
+
key: atlasUrl,
|
|
390
414
|
premultiplyAlpha: false,
|
|
391
415
|
});
|
|
392
416
|
|
|
393
|
-
|
|
417
|
+
// Register every name as a renderable owner so the shared atlas stays
|
|
418
|
+
// alive as long as any of its names is in use.
|
|
419
|
+
for (let i = 0; i < names.length; i++) {
|
|
420
|
+
atlasTexture.setRenderableOwner(names[i]!, true);
|
|
421
|
+
}
|
|
394
422
|
atlasTexture.preventCleanup = true; // Prevent automatic cleanup
|
|
395
423
|
|
|
396
424
|
const onLoaded = () => {
|
|
397
|
-
// Process and cache font data
|
|
398
|
-
processFontData(
|
|
425
|
+
// Process and cache font data under the primary name...
|
|
426
|
+
processFontData(primary, fontData, atlasTexture, metrics);
|
|
427
|
+
// ...then alias the remaining names onto the same cache entry so they
|
|
428
|
+
// resolve to the identical glyph/kerning/atlas data.
|
|
429
|
+
const cached = fontCache.get(primary)!;
|
|
430
|
+
for (let i = 1; i < names.length; i++) {
|
|
431
|
+
fontCache.set(names[i]!, cached);
|
|
432
|
+
}
|
|
399
433
|
|
|
400
|
-
|
|
401
|
-
|
|
434
|
+
// Wake every parked node across all names and clear their lists.
|
|
435
|
+
for (let i = 0; i < names.length; i++) {
|
|
436
|
+
const name = names[i]!;
|
|
437
|
+
const list = nodesWaitingForFont[name];
|
|
438
|
+
if (list !== undefined) {
|
|
439
|
+
for (let key in list) {
|
|
440
|
+
list[key]!.setUpdateType(UpdateType.Local);
|
|
441
|
+
}
|
|
442
|
+
delete nodesWaitingForFont[name];
|
|
443
|
+
}
|
|
402
444
|
}
|
|
403
|
-
delete nodesWaitingForFont[fontFamily];
|
|
404
445
|
resolve();
|
|
405
446
|
};
|
|
406
447
|
|
|
@@ -419,49 +460,78 @@ export const loadFont = (
|
|
|
419
460
|
atlasTexture.on('failed', (_target, error: TextureError) => {
|
|
420
461
|
// Drop the failed atlas so a retry builds a fresh texture rather than
|
|
421
462
|
// getting this dead instance back from the createTexture key-cache.
|
|
422
|
-
|
|
463
|
+
for (let i = 0; i < names.length; i++) {
|
|
464
|
+
atlasTexture.setRenderableOwner(names[i]!, false);
|
|
465
|
+
}
|
|
423
466
|
stage.txManager.removeTextureFromCache(atlasTexture);
|
|
424
467
|
reject(error);
|
|
425
468
|
});
|
|
426
469
|
});
|
|
427
470
|
};
|
|
428
471
|
|
|
429
|
-
// Initial attempt plus up to MAX_FONT_LOAD_RETRIES automatic reloads.
|
|
472
|
+
// Initial attempt plus up to MAX_FONT_LOAD_RETRIES automatic reloads. Only
|
|
473
|
+
// the first attempt uses the prefetch; a retry goes back to the network
|
|
474
|
+
// rather than replaying a payload that may be exactly what failed.
|
|
430
475
|
const loadPromise = (async (): Promise<void> => {
|
|
431
476
|
let lastError: unknown;
|
|
432
477
|
for (let attempt = 0; attempt <= MAX_FONT_LOAD_RETRIES; attempt++) {
|
|
433
478
|
try {
|
|
434
|
-
|
|
435
|
-
//
|
|
436
|
-
|
|
437
|
-
|
|
479
|
+
// Both halves of a prefetch are already in flight in parallel; the
|
|
480
|
+
// fallbacks below are the original sequential path.
|
|
481
|
+
const fontData =
|
|
482
|
+
(attempt === 0 && prefetched !== undefined
|
|
483
|
+
? await prefetched.data
|
|
484
|
+
: null) ?? (await fetchFontData());
|
|
485
|
+
|
|
486
|
+
const atlasBlob =
|
|
487
|
+
attempt === 0 && prefetched !== undefined
|
|
488
|
+
? await prefetched.atlas
|
|
489
|
+
: null;
|
|
490
|
+
|
|
491
|
+
await loadAtlas(fontData, atlasBlob);
|
|
492
|
+
// Success: clear the in-flight markers (the font now lives in
|
|
493
|
+
// fontCache) — parked nodes were already woken inside loadAtlas.
|
|
494
|
+
for (let i = 0; i < names.length; i++) {
|
|
495
|
+
fontLoadPromises.delete(names[i]!);
|
|
496
|
+
}
|
|
438
497
|
return;
|
|
439
498
|
} catch (error) {
|
|
440
499
|
lastError = error;
|
|
441
500
|
if (attempt < MAX_FONT_LOAD_RETRIES) {
|
|
442
501
|
console.warn(
|
|
443
|
-
`SDF font "${
|
|
444
|
-
|
|
445
|
-
}
|
|
502
|
+
`SDF font "${primary}" failed to load (attempt ${attempt + 1} of ${
|
|
503
|
+
MAX_FONT_LOAD_RETRIES + 1
|
|
504
|
+
}), retrying.`,
|
|
446
505
|
error,
|
|
447
506
|
);
|
|
448
507
|
}
|
|
449
508
|
}
|
|
450
509
|
}
|
|
451
510
|
|
|
452
|
-
// Every attempt failed. Clear the in-flight
|
|
511
|
+
// Every attempt failed. Clear the in-flight markers so the font can be
|
|
453
512
|
// requested again and drop any partial cache entry. nodesWaitingForFont
|
|
454
513
|
// is deliberately kept: nodes parked here must survive so a later
|
|
455
514
|
// loadFont() (which reuses the list) can still wake them if the font
|
|
456
515
|
// eventually loads. The list shrinks as nodes self-remove via
|
|
457
516
|
// stopWaitingForFont on destroy.
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
517
|
+
for (let i = 0; i < names.length; i++) {
|
|
518
|
+
fontLoadPromises.delete(names[i]!);
|
|
519
|
+
fontCache.delete(names[i]!);
|
|
520
|
+
}
|
|
521
|
+
console.error(`Failed to load SDF font: ${primary}`, lastError);
|
|
461
522
|
throw lastError;
|
|
462
523
|
})();
|
|
463
524
|
|
|
464
|
-
|
|
525
|
+
// Mark the load in flight under *every* name, not just the primary.
|
|
526
|
+
// `canRenderFont` treats a name with no cache entry and no in-flight promise
|
|
527
|
+
// as unrenderable, so keying only the primary made an alias unresolvable for
|
|
528
|
+
// the whole load: the stage would either fall through to the Canvas engine
|
|
529
|
+
// (permanently, with a substitute face) or, on an SDF-only engine list,
|
|
530
|
+
// throw "No compatible text renderer found". Registering every name also
|
|
531
|
+
// makes a later loadFont() for an alias dedupe onto this same load.
|
|
532
|
+
for (let i = 0; i < names.length; i++) {
|
|
533
|
+
fontLoadPromises.set(names[i]!, loadPromise);
|
|
534
|
+
}
|
|
465
535
|
return loadPromise;
|
|
466
536
|
};
|
|
467
537
|
|
|
@@ -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;
|