three-text 0.5.1 → 0.5.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/README.md +15 -16
- package/dist/index.cjs +66 -20
- package/dist/index.d.ts +6 -0
- package/dist/index.js +66 -20
- package/dist/index.min.cjs +310 -307
- package/dist/index.min.js +265 -262
- package/dist/index.umd.js +68 -21
- package/dist/index.umd.min.js +268 -265
- package/dist/three/index.cjs +2 -1
- package/dist/three/index.d.ts +1 -0
- package/dist/three/index.js +2 -1
- package/dist/three/react.cjs +35 -17
- package/dist/three/react.d.ts +6 -0
- package/dist/three/react.js +35 -17
- package/dist/types/core/Text.d.ts +6 -0
- package/dist/types/three/index.d.ts +1 -0
- package/dist/types/vector/core.d.ts +27 -0
- package/dist/types/vector/index.d.ts +30 -22
- package/dist/types/vector/loopBlinnTSL.d.ts +12 -1
- package/dist/types/vector/react.d.ts +4 -6
- package/dist/vector/all.cjs +21 -0
- package/dist/vector/all.d.ts +134 -0
- package/dist/vector/all.js +2 -0
- package/dist/vector/core.cjs +1310 -0
- package/dist/vector/core.d.ts +112 -0
- package/dist/vector/core.js +1306 -0
- package/dist/vector/index.cjs +43 -1119
- package/dist/vector/index.d.ts +338 -13
- package/dist/vector/index.js +29 -1112
- package/dist/vector/loopBlinnTSL.cjs +229 -0
- package/dist/vector/loopBlinnTSL.d.ts +69 -0
- package/dist/vector/loopBlinnTSL.js +207 -0
- package/dist/vector/react.cjs +45 -60
- package/dist/vector/react.d.ts +9 -3
- package/dist/vector/react.js +46 -42
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -71,10 +71,10 @@ three-text has a framework-agnostic core that processes fonts and generates geom
|
|
|
71
71
|
- **`three-text/mesh/webgpu`** - WebGPU mesh buffer utility
|
|
72
72
|
- **`three-text/mesh/p5`** - p5.js adapter
|
|
73
73
|
- **`three-text/core`** - Framework-agnostic core (returns raw arrays)
|
|
74
|
-
- **`three-text/vector`** - Vector rendering (Loop-Blinn
|
|
74
|
+
- **`three-text/vector`** - Vector rendering (Loop-Blinn curve eval + Kokojima stencil fill), `Text.create()` returns a `THREE.Group` ready for `scene.add()`
|
|
75
75
|
- **`three-text/vector/react`** - React Three Fiber component for vector text
|
|
76
|
-
- **`three-text/vector/webgl`** -
|
|
77
|
-
- **`three-text/vector/webgpu`** - WebGPU vector renderer
|
|
76
|
+
- **`three-text/vector/webgl`** - Raw WebGL2 vector renderer (no Three.js dependency)
|
|
77
|
+
- **`three-text/vector/webgpu`** - Raw WebGPU vector renderer (no Three.js dependency)
|
|
78
78
|
- **`three-text/webgl`** - Deprecated, use `three-text/mesh/webgl`
|
|
79
79
|
- **`three-text/webgpu`** - Deprecated, use `three-text/mesh/webgpu`
|
|
80
80
|
- **`three-text/p5`** - Deprecated, use `three-text/mesh/p5`
|
|
@@ -121,20 +121,18 @@ Resolution-independent outlines via Loop-Blinn stencil passes (see [Vector rende
|
|
|
121
121
|
|
|
122
122
|
```javascript
|
|
123
123
|
import { Text } from 'three-text/vector';
|
|
124
|
-
import { woff2Decode } from 'woff-lib/woff2/decode';
|
|
125
124
|
|
|
126
125
|
Text.setHarfBuzzPath('/hb/hb.wasm');
|
|
127
|
-
Text.enableWoff2(woff2Decode);
|
|
128
126
|
const result = await Text.create({
|
|
129
127
|
text: 'Hello Vector',
|
|
130
128
|
font: '/fonts/Font.woff2',
|
|
131
|
-
size: 72
|
|
129
|
+
size: 72,
|
|
130
|
+
color: '#ffffff'
|
|
132
131
|
});
|
|
133
|
-
|
|
134
|
-
const vectorData = result.geometryData;
|
|
132
|
+
scene.add(result.group);
|
|
135
133
|
```
|
|
136
134
|
|
|
137
|
-
|
|
135
|
+
`Text.create()` handles stencil setup, render ordering, and geometry centering internally. Pass `positionNode` and `colorNode` for TSL animation/styling. For raw WebGL2 or WebGPU without Three.js, see [Vector rendering](#vector-rendering)
|
|
138
136
|
|
|
139
137
|
#### Mesh + vector in one scene
|
|
140
138
|
|
|
@@ -142,7 +140,7 @@ Alias one import to avoid the name collision between `Text` components. The entr
|
|
|
142
140
|
|
|
143
141
|
```javascript
|
|
144
142
|
import { Text as MeshText } from 'three-text';
|
|
145
|
-
import { Text as VectorText
|
|
143
|
+
import { Text as VectorText } from 'three-text/vector';
|
|
146
144
|
|
|
147
145
|
MeshText.setHarfBuzzPath('/hb/hb.wasm');
|
|
148
146
|
|
|
@@ -156,10 +154,10 @@ scene.add(new THREE.Mesh(heading.geometry, material));
|
|
|
156
154
|
const caption = await VectorText.create({
|
|
157
155
|
text: 'Caption text',
|
|
158
156
|
font: '/fonts/Font.woff2',
|
|
159
|
-
size: 24
|
|
157
|
+
size: 24,
|
|
158
|
+
color: '#ffffff'
|
|
160
159
|
});
|
|
161
|
-
|
|
162
|
-
scene.add(interiorMesh, curveMesh, fillMesh);
|
|
160
|
+
scene.add(caption.group);
|
|
163
161
|
```
|
|
164
162
|
|
|
165
163
|
#### React Three Fiber — mesh
|
|
@@ -477,8 +475,9 @@ three-text/
|
|
|
477
475
|
│ │ ├── react.tsx # React component export
|
|
478
476
|
│ │ └── ThreeText.tsx # React Three Fiber component
|
|
479
477
|
│ ├── vector/ # Vector rendering (Loop-Blinn)
|
|
480
|
-
│ │ ├── index.ts #
|
|
481
|
-
│ │ ├──
|
|
478
|
+
│ │ ├── index.ts # Main entry point (wraps core + Three.js integration)
|
|
479
|
+
│ │ ├── core.ts # Three.js-free layout engine (used by raw WebGL/WebGPU)
|
|
480
|
+
│ │ ├── loopBlinnTSL.ts # TSL stencil materials and mesh construction
|
|
482
481
|
│ │ ├── LoopBlinnGeometry.ts # Fan triangulation + curve extraction
|
|
483
482
|
│ │ ├── GlyphVectorGeometryBuilder.ts # Outline collection and geometry packing
|
|
484
483
|
│ │ ├── GlyphOutlineCollector.ts # Collects draw callbacks for vector path
|
|
@@ -537,7 +536,7 @@ The multi-stage geometry approach (curve polygonization followed by cleanup, the
|
|
|
537
536
|
|
|
538
537
|
The vector pipeline (`three-text/vector`) renders glyphs directly from their mathematical outlines without tessellation or curve flattening. Text stays sharp at any zoom level and the geometry footprint is small -- just the control points of each curve
|
|
539
538
|
|
|
540
|
-
Curves use the [Loop-Blinn](https://www.microsoft.com/en-us/research/wp-content/uploads/2005/01/p1000-loop.pdf) technique: each quadratic curve is rendered as a triangle whose fragment shader evaluates `u² - v` to resolve inside/outside, with screen-space derivatives producing a signed distance that feeds alpha-to-coverage for smooth MSAA edges. Glyph interiors use [Kokojima et al.](https://dl.acm.org/doi/10.1145/1179849.1179997) stencil filling
|
|
539
|
+
Curves use the [Loop-Blinn](https://www.microsoft.com/en-us/research/wp-content/uploads/2005/01/p1000-loop.pdf) technique: each quadratic curve is rendered as a triangle whose fragment shader evaluates `u² - v` to resolve inside/outside, with screen-space derivatives producing a signed distance that feeds alpha-to-coverage for smooth MSAA edges. Glyph interiors use [Kokojima et al.](https://dl.acm.org/doi/10.1145/1179849.1179997) stencil filling with a nonzero winding rule (`INCR_WRAP`/`DECR_WRAP`), which correctly handles overlapping contours in variable fonts and connected scripts
|
|
541
540
|
|
|
542
541
|
An alternative for this sort of resolution-independent rendering is [Slug](https://github.com/EricLengyel/Slug) by Eric Lengyel, which casts rays against all curves per fragment to compute winding numbers. Loop-Blinn was chosen here because it integrates with hardware MSAA and alpha-to-coverage directly, without the overhead of adaptive supersampling that Slug requires for comparable antialiasing
|
|
543
542
|
|
package/dist/index.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*!
|
|
2
2
|
* @license
|
|
3
|
-
* three-text v0.5.
|
|
3
|
+
* three-text v0.5.2
|
|
4
4
|
* Copyright © 2025-2026 Jeremy Tribby, Countertype LLC
|
|
5
5
|
* SPDX-License-Identifier: MIT
|
|
6
6
|
*/
|
|
@@ -3263,6 +3263,8 @@ class Text {
|
|
|
3263
3263
|
static { this.patternCache = new Map(); }
|
|
3264
3264
|
static { this.hbInitPromise = null; }
|
|
3265
3265
|
static { this.fontCache = new Map(); }
|
|
3266
|
+
static { this.fontLoadPromises = new Map(); }
|
|
3267
|
+
static { this.fontRefCounts = new Map(); }
|
|
3266
3268
|
static { this.fontCacheMemoryBytes = 0; }
|
|
3267
3269
|
static { this.maxFontCacheMemoryBytes = Infinity; }
|
|
3268
3270
|
static { this.fontIdCounter = 0; }
|
|
@@ -3307,9 +3309,9 @@ class Text {
|
|
|
3307
3309
|
if (!Text.hbInitPromise) {
|
|
3308
3310
|
Text.hbInitPromise = HarfBuzzLoader.getHarfBuzz();
|
|
3309
3311
|
}
|
|
3310
|
-
const loadedFont = await Text.resolveFont(options);
|
|
3312
|
+
const { loadedFont, fontKey } = await Text.resolveFont(options);
|
|
3311
3313
|
const text = new Text();
|
|
3312
|
-
text.setLoadedFont(loadedFont);
|
|
3314
|
+
text.setLoadedFont(loadedFont, fontKey);
|
|
3313
3315
|
const result = await text.createLayout(options);
|
|
3314
3316
|
const update = async (newOptions) => {
|
|
3315
3317
|
const mergedOptions = { ...options };
|
|
@@ -3322,8 +3324,8 @@ class Text {
|
|
|
3322
3324
|
if (newOptions.font !== undefined ||
|
|
3323
3325
|
newOptions.fontVariations !== undefined ||
|
|
3324
3326
|
newOptions.fontFeatures !== undefined) {
|
|
3325
|
-
const newLoadedFont = await Text.resolveFont(mergedOptions);
|
|
3326
|
-
text.setLoadedFont(newLoadedFont);
|
|
3327
|
+
const { loadedFont: newLoadedFont, fontKey: newFontKey } = await Text.resolveFont(mergedOptions);
|
|
3328
|
+
text.setLoadedFont(newLoadedFont, newFontKey);
|
|
3327
3329
|
text.resetHelpers();
|
|
3328
3330
|
}
|
|
3329
3331
|
options = mergedOptions;
|
|
@@ -3344,6 +3346,22 @@ class Text {
|
|
|
3344
3346
|
dispose: () => text.destroy()
|
|
3345
3347
|
};
|
|
3346
3348
|
}
|
|
3349
|
+
static retainFont(fontKey) {
|
|
3350
|
+
Text.fontRefCounts.set(fontKey, (Text.fontRefCounts.get(fontKey) ?? 0) + 1);
|
|
3351
|
+
}
|
|
3352
|
+
static releaseFont(fontKey, loadedFont) {
|
|
3353
|
+
const nextCount = (Text.fontRefCounts.get(fontKey) ?? 0) - 1;
|
|
3354
|
+
if (nextCount > 0) {
|
|
3355
|
+
Text.fontRefCounts.set(fontKey, nextCount);
|
|
3356
|
+
return;
|
|
3357
|
+
}
|
|
3358
|
+
Text.fontRefCounts.delete(fontKey);
|
|
3359
|
+
// Cached fonts stay alive while present in the cache. If a font has been
|
|
3360
|
+
// evicted, destroy it once the last live handle releases it.
|
|
3361
|
+
if (!Text.fontCache.has(fontKey)) {
|
|
3362
|
+
FontLoader.destroyFont(loadedFont);
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3347
3365
|
static async resolveFont(options) {
|
|
3348
3366
|
const baseFontKey = typeof options.font === 'string'
|
|
3349
3367
|
? options.font
|
|
@@ -3357,9 +3375,17 @@ class Text {
|
|
|
3357
3375
|
}
|
|
3358
3376
|
let loadedFont = Text.fontCache.get(fontKey);
|
|
3359
3377
|
if (!loadedFont) {
|
|
3360
|
-
|
|
3378
|
+
let loadPromise = Text.fontLoadPromises.get(fontKey);
|
|
3379
|
+
if (!loadPromise) {
|
|
3380
|
+
loadPromise = Text.loadAndCacheFont(fontKey, options.font, options.fontVariations, options.fontFeatures).finally(() => {
|
|
3381
|
+
Text.fontLoadPromises.delete(fontKey);
|
|
3382
|
+
});
|
|
3383
|
+
Text.fontLoadPromises.set(fontKey, loadPromise);
|
|
3384
|
+
}
|
|
3385
|
+
loadedFont = await loadPromise;
|
|
3361
3386
|
}
|
|
3362
|
-
|
|
3387
|
+
Text.retainFont(fontKey);
|
|
3388
|
+
return { loadedFont, fontKey };
|
|
3363
3389
|
}
|
|
3364
3390
|
static async loadAndCacheFont(fontKey, font, fontVariations, fontFeatures) {
|
|
3365
3391
|
const tempText = new Text();
|
|
@@ -3391,8 +3417,12 @@ class Text {
|
|
|
3391
3417
|
const firstKey = Text.fontCache.keys().next().value;
|
|
3392
3418
|
if (firstKey === undefined)
|
|
3393
3419
|
break;
|
|
3420
|
+
const font = Text.fontCache.get(firstKey);
|
|
3394
3421
|
Text.trackFontCacheRemove(firstKey);
|
|
3395
3422
|
Text.fontCache.delete(firstKey);
|
|
3423
|
+
if ((Text.fontRefCounts.get(firstKey) ?? 0) <= 0 && font) {
|
|
3424
|
+
FontLoader.destroyFont(font);
|
|
3425
|
+
}
|
|
3396
3426
|
}
|
|
3397
3427
|
}
|
|
3398
3428
|
static generateFontContentHash(buffer) {
|
|
@@ -3414,8 +3444,12 @@ class Text {
|
|
|
3414
3444
|
return `c${++Text.fontIdCounter}`;
|
|
3415
3445
|
}
|
|
3416
3446
|
}
|
|
3417
|
-
setLoadedFont(loadedFont) {
|
|
3447
|
+
setLoadedFont(loadedFont, fontKey) {
|
|
3448
|
+
if (this.loadedFont && this.loadedFont !== loadedFont) {
|
|
3449
|
+
this.releaseCurrentFont();
|
|
3450
|
+
}
|
|
3418
3451
|
this.loadedFont = loadedFont;
|
|
3452
|
+
this.currentFontCacheKey = fontKey;
|
|
3419
3453
|
const contentHash = Text.generateFontContentHash(loadedFont._buffer);
|
|
3420
3454
|
this.currentFontId = `font_${contentHash}`;
|
|
3421
3455
|
if (loadedFont.fontVariations) {
|
|
@@ -3425,6 +3459,29 @@ class Text {
|
|
|
3425
3459
|
this.currentFontId += `_feat_${Text.stableStringify(loadedFont.fontFeatures)}`;
|
|
3426
3460
|
}
|
|
3427
3461
|
}
|
|
3462
|
+
releaseCurrentFont() {
|
|
3463
|
+
if (!this.loadedFont)
|
|
3464
|
+
return;
|
|
3465
|
+
const currentFont = this.loadedFont;
|
|
3466
|
+
const currentFontKey = this.currentFontCacheKey;
|
|
3467
|
+
try {
|
|
3468
|
+
if (currentFontKey) {
|
|
3469
|
+
Text.releaseFont(currentFontKey, currentFont);
|
|
3470
|
+
}
|
|
3471
|
+
else {
|
|
3472
|
+
FontLoader.destroyFont(currentFont);
|
|
3473
|
+
}
|
|
3474
|
+
}
|
|
3475
|
+
catch (error) {
|
|
3476
|
+
logger.warn('Error destroying HarfBuzz objects:', error);
|
|
3477
|
+
}
|
|
3478
|
+
finally {
|
|
3479
|
+
this.loadedFont = undefined;
|
|
3480
|
+
this.currentFontCacheKey = undefined;
|
|
3481
|
+
this.textLayout = undefined;
|
|
3482
|
+
this.textShaper = undefined;
|
|
3483
|
+
}
|
|
3484
|
+
}
|
|
3428
3485
|
async loadFont(fontSrc, fontVariations, fontFeatures) {
|
|
3429
3486
|
perfLogger.start('Text.loadFont', {
|
|
3430
3487
|
fontSrc: typeof fontSrc === 'string' ? fontSrc : `buffer(${fontSrc.byteLength})`
|
|
@@ -3644,18 +3701,7 @@ class Text {
|
|
|
3644
3701
|
if (!this.loadedFont) {
|
|
3645
3702
|
return;
|
|
3646
3703
|
}
|
|
3647
|
-
|
|
3648
|
-
try {
|
|
3649
|
-
FontLoader.destroyFont(currentFont);
|
|
3650
|
-
}
|
|
3651
|
-
catch (error) {
|
|
3652
|
-
logger.warn('Error destroying HarfBuzz objects:', error);
|
|
3653
|
-
}
|
|
3654
|
-
finally {
|
|
3655
|
-
this.loadedFont = undefined;
|
|
3656
|
-
this.textLayout = undefined;
|
|
3657
|
-
this.textShaper = undefined;
|
|
3658
|
-
}
|
|
3704
|
+
this.releaseCurrentFont();
|
|
3659
3705
|
}
|
|
3660
3706
|
}
|
|
3661
3707
|
|
package/dist/index.d.ts
CHANGED
|
@@ -457,6 +457,8 @@ declare class Text {
|
|
|
457
457
|
private static patternCache;
|
|
458
458
|
private static hbInitPromise;
|
|
459
459
|
private static fontCache;
|
|
460
|
+
private static fontLoadPromises;
|
|
461
|
+
private static fontRefCounts;
|
|
460
462
|
private static fontCacheMemoryBytes;
|
|
461
463
|
private static maxFontCacheMemoryBytes;
|
|
462
464
|
private static fontIdCounter;
|
|
@@ -465,6 +467,7 @@ declare class Text {
|
|
|
465
467
|
private fontLoader;
|
|
466
468
|
private loadedFont?;
|
|
467
469
|
private currentFontId;
|
|
470
|
+
private currentFontCacheKey?;
|
|
468
471
|
private textShaper?;
|
|
469
472
|
private textLayout?;
|
|
470
473
|
private constructor();
|
|
@@ -472,6 +475,8 @@ declare class Text {
|
|
|
472
475
|
static setHarfBuzzBuffer(wasmBuffer: ArrayBuffer): void;
|
|
473
476
|
static init(): Promise<HarfBuzzInstance>;
|
|
474
477
|
static create(options: TextOptions): Promise<TextLayoutHandle>;
|
|
478
|
+
private static retainFont;
|
|
479
|
+
private static releaseFont;
|
|
475
480
|
private static resolveFont;
|
|
476
481
|
private static loadAndCacheFont;
|
|
477
482
|
private static trackFontCacheAdd;
|
|
@@ -479,6 +484,7 @@ declare class Text {
|
|
|
479
484
|
private static enforceFontCacheMemoryLimit;
|
|
480
485
|
private static generateFontContentHash;
|
|
481
486
|
private setLoadedFont;
|
|
487
|
+
private releaseCurrentFont;
|
|
482
488
|
private loadFont;
|
|
483
489
|
private createLayout;
|
|
484
490
|
private prepareHyphenation;
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*!
|
|
2
2
|
* @license
|
|
3
|
-
* three-text v0.5.
|
|
3
|
+
* three-text v0.5.2
|
|
4
4
|
* Copyright © 2025-2026 Jeremy Tribby, Countertype LLC
|
|
5
5
|
* SPDX-License-Identifier: MIT
|
|
6
6
|
*/
|
|
@@ -3260,6 +3260,8 @@ class Text {
|
|
|
3260
3260
|
static { this.patternCache = new Map(); }
|
|
3261
3261
|
static { this.hbInitPromise = null; }
|
|
3262
3262
|
static { this.fontCache = new Map(); }
|
|
3263
|
+
static { this.fontLoadPromises = new Map(); }
|
|
3264
|
+
static { this.fontRefCounts = new Map(); }
|
|
3263
3265
|
static { this.fontCacheMemoryBytes = 0; }
|
|
3264
3266
|
static { this.maxFontCacheMemoryBytes = Infinity; }
|
|
3265
3267
|
static { this.fontIdCounter = 0; }
|
|
@@ -3304,9 +3306,9 @@ class Text {
|
|
|
3304
3306
|
if (!Text.hbInitPromise) {
|
|
3305
3307
|
Text.hbInitPromise = HarfBuzzLoader.getHarfBuzz();
|
|
3306
3308
|
}
|
|
3307
|
-
const loadedFont = await Text.resolveFont(options);
|
|
3309
|
+
const { loadedFont, fontKey } = await Text.resolveFont(options);
|
|
3308
3310
|
const text = new Text();
|
|
3309
|
-
text.setLoadedFont(loadedFont);
|
|
3311
|
+
text.setLoadedFont(loadedFont, fontKey);
|
|
3310
3312
|
const result = await text.createLayout(options);
|
|
3311
3313
|
const update = async (newOptions) => {
|
|
3312
3314
|
const mergedOptions = { ...options };
|
|
@@ -3319,8 +3321,8 @@ class Text {
|
|
|
3319
3321
|
if (newOptions.font !== undefined ||
|
|
3320
3322
|
newOptions.fontVariations !== undefined ||
|
|
3321
3323
|
newOptions.fontFeatures !== undefined) {
|
|
3322
|
-
const newLoadedFont = await Text.resolveFont(mergedOptions);
|
|
3323
|
-
text.setLoadedFont(newLoadedFont);
|
|
3324
|
+
const { loadedFont: newLoadedFont, fontKey: newFontKey } = await Text.resolveFont(mergedOptions);
|
|
3325
|
+
text.setLoadedFont(newLoadedFont, newFontKey);
|
|
3324
3326
|
text.resetHelpers();
|
|
3325
3327
|
}
|
|
3326
3328
|
options = mergedOptions;
|
|
@@ -3341,6 +3343,22 @@ class Text {
|
|
|
3341
3343
|
dispose: () => text.destroy()
|
|
3342
3344
|
};
|
|
3343
3345
|
}
|
|
3346
|
+
static retainFont(fontKey) {
|
|
3347
|
+
Text.fontRefCounts.set(fontKey, (Text.fontRefCounts.get(fontKey) ?? 0) + 1);
|
|
3348
|
+
}
|
|
3349
|
+
static releaseFont(fontKey, loadedFont) {
|
|
3350
|
+
const nextCount = (Text.fontRefCounts.get(fontKey) ?? 0) - 1;
|
|
3351
|
+
if (nextCount > 0) {
|
|
3352
|
+
Text.fontRefCounts.set(fontKey, nextCount);
|
|
3353
|
+
return;
|
|
3354
|
+
}
|
|
3355
|
+
Text.fontRefCounts.delete(fontKey);
|
|
3356
|
+
// Cached fonts stay alive while present in the cache. If a font has been
|
|
3357
|
+
// evicted, destroy it once the last live handle releases it.
|
|
3358
|
+
if (!Text.fontCache.has(fontKey)) {
|
|
3359
|
+
FontLoader.destroyFont(loadedFont);
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
3344
3362
|
static async resolveFont(options) {
|
|
3345
3363
|
const baseFontKey = typeof options.font === 'string'
|
|
3346
3364
|
? options.font
|
|
@@ -3354,9 +3372,17 @@ class Text {
|
|
|
3354
3372
|
}
|
|
3355
3373
|
let loadedFont = Text.fontCache.get(fontKey);
|
|
3356
3374
|
if (!loadedFont) {
|
|
3357
|
-
|
|
3375
|
+
let loadPromise = Text.fontLoadPromises.get(fontKey);
|
|
3376
|
+
if (!loadPromise) {
|
|
3377
|
+
loadPromise = Text.loadAndCacheFont(fontKey, options.font, options.fontVariations, options.fontFeatures).finally(() => {
|
|
3378
|
+
Text.fontLoadPromises.delete(fontKey);
|
|
3379
|
+
});
|
|
3380
|
+
Text.fontLoadPromises.set(fontKey, loadPromise);
|
|
3381
|
+
}
|
|
3382
|
+
loadedFont = await loadPromise;
|
|
3358
3383
|
}
|
|
3359
|
-
|
|
3384
|
+
Text.retainFont(fontKey);
|
|
3385
|
+
return { loadedFont, fontKey };
|
|
3360
3386
|
}
|
|
3361
3387
|
static async loadAndCacheFont(fontKey, font, fontVariations, fontFeatures) {
|
|
3362
3388
|
const tempText = new Text();
|
|
@@ -3388,8 +3414,12 @@ class Text {
|
|
|
3388
3414
|
const firstKey = Text.fontCache.keys().next().value;
|
|
3389
3415
|
if (firstKey === undefined)
|
|
3390
3416
|
break;
|
|
3417
|
+
const font = Text.fontCache.get(firstKey);
|
|
3391
3418
|
Text.trackFontCacheRemove(firstKey);
|
|
3392
3419
|
Text.fontCache.delete(firstKey);
|
|
3420
|
+
if ((Text.fontRefCounts.get(firstKey) ?? 0) <= 0 && font) {
|
|
3421
|
+
FontLoader.destroyFont(font);
|
|
3422
|
+
}
|
|
3393
3423
|
}
|
|
3394
3424
|
}
|
|
3395
3425
|
static generateFontContentHash(buffer) {
|
|
@@ -3411,8 +3441,12 @@ class Text {
|
|
|
3411
3441
|
return `c${++Text.fontIdCounter}`;
|
|
3412
3442
|
}
|
|
3413
3443
|
}
|
|
3414
|
-
setLoadedFont(loadedFont) {
|
|
3444
|
+
setLoadedFont(loadedFont, fontKey) {
|
|
3445
|
+
if (this.loadedFont && this.loadedFont !== loadedFont) {
|
|
3446
|
+
this.releaseCurrentFont();
|
|
3447
|
+
}
|
|
3415
3448
|
this.loadedFont = loadedFont;
|
|
3449
|
+
this.currentFontCacheKey = fontKey;
|
|
3416
3450
|
const contentHash = Text.generateFontContentHash(loadedFont._buffer);
|
|
3417
3451
|
this.currentFontId = `font_${contentHash}`;
|
|
3418
3452
|
if (loadedFont.fontVariations) {
|
|
@@ -3422,6 +3456,29 @@ class Text {
|
|
|
3422
3456
|
this.currentFontId += `_feat_${Text.stableStringify(loadedFont.fontFeatures)}`;
|
|
3423
3457
|
}
|
|
3424
3458
|
}
|
|
3459
|
+
releaseCurrentFont() {
|
|
3460
|
+
if (!this.loadedFont)
|
|
3461
|
+
return;
|
|
3462
|
+
const currentFont = this.loadedFont;
|
|
3463
|
+
const currentFontKey = this.currentFontCacheKey;
|
|
3464
|
+
try {
|
|
3465
|
+
if (currentFontKey) {
|
|
3466
|
+
Text.releaseFont(currentFontKey, currentFont);
|
|
3467
|
+
}
|
|
3468
|
+
else {
|
|
3469
|
+
FontLoader.destroyFont(currentFont);
|
|
3470
|
+
}
|
|
3471
|
+
}
|
|
3472
|
+
catch (error) {
|
|
3473
|
+
logger.warn('Error destroying HarfBuzz objects:', error);
|
|
3474
|
+
}
|
|
3475
|
+
finally {
|
|
3476
|
+
this.loadedFont = undefined;
|
|
3477
|
+
this.currentFontCacheKey = undefined;
|
|
3478
|
+
this.textLayout = undefined;
|
|
3479
|
+
this.textShaper = undefined;
|
|
3480
|
+
}
|
|
3481
|
+
}
|
|
3425
3482
|
async loadFont(fontSrc, fontVariations, fontFeatures) {
|
|
3426
3483
|
perfLogger.start('Text.loadFont', {
|
|
3427
3484
|
fontSrc: typeof fontSrc === 'string' ? fontSrc : `buffer(${fontSrc.byteLength})`
|
|
@@ -3641,18 +3698,7 @@ class Text {
|
|
|
3641
3698
|
if (!this.loadedFont) {
|
|
3642
3699
|
return;
|
|
3643
3700
|
}
|
|
3644
|
-
|
|
3645
|
-
try {
|
|
3646
|
-
FontLoader.destroyFont(currentFont);
|
|
3647
|
-
}
|
|
3648
|
-
catch (error) {
|
|
3649
|
-
logger.warn('Error destroying HarfBuzz objects:', error);
|
|
3650
|
-
}
|
|
3651
|
-
finally {
|
|
3652
|
-
this.loadedFont = undefined;
|
|
3653
|
-
this.textLayout = undefined;
|
|
3654
|
-
this.textShaper = undefined;
|
|
3655
|
-
}
|
|
3701
|
+
this.releaseCurrentFont();
|
|
3656
3702
|
}
|
|
3657
3703
|
}
|
|
3658
3704
|
|