three-text 0.2.5 → 0.2.7
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 +67 -27
- package/dist/index.cjs +431 -70
- package/dist/index.d.ts +18 -1
- package/dist/index.js +431 -70
- package/dist/index.min.cjs +2 -2
- package/dist/index.min.js +2 -2
- package/dist/index.umd.js +431 -70
- package/dist/index.umd.min.js +2 -2
- package/dist/p5/index.cjs +4 -8
- package/dist/p5/index.js +4 -8
- package/dist/three/react.d.ts +11 -1
- package/dist/types/core/cache/GlyphGeometryBuilder.d.ts +1 -0
- package/dist/types/core/font/FontMetadata.d.ts +7 -0
- package/dist/types/core/font/constants.d.ts +10 -0
- package/dist/types/core/shaping/fontFeatures.d.ts +3 -0
- package/dist/types/core/types.d.ts +11 -1
- package/dist/types/utils/LRUCache.d.ts +38 -0
- package/dist/types/utils/{DebugLogger.d.ts → Logger.d.ts} +2 -2
- package/dist/types/utils/PerformanceLogger.d.ts +1 -0
- package/package.json +2 -1
package/dist/index.umd.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* three-text v0.2.
|
|
2
|
+
* three-text v0.2.7
|
|
3
3
|
* Copyright (C) 2025 Countertype LLC
|
|
4
4
|
*
|
|
5
5
|
* This program is free software: you can redistribute it and/or modify
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
}
|
|
28
28
|
return false;
|
|
29
29
|
})();
|
|
30
|
-
class
|
|
30
|
+
class Logger {
|
|
31
31
|
warn(message, ...args) {
|
|
32
32
|
console.warn(message, ...args);
|
|
33
33
|
}
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
isLogEnabled && console.log(message, ...args);
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
-
const
|
|
41
|
+
const logger = new Logger();
|
|
42
42
|
|
|
43
43
|
class PerformanceLogger {
|
|
44
44
|
constructor() {
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
const endTime = performance.now();
|
|
65
65
|
const startTime = this.activeTimers.get(name);
|
|
66
66
|
if (startTime === undefined) {
|
|
67
|
-
|
|
67
|
+
logger.warn(`Performance timer "${name}" was not started`);
|
|
68
68
|
return null;
|
|
69
69
|
}
|
|
70
70
|
const duration = endTime - startTime;
|
|
@@ -127,6 +127,9 @@
|
|
|
127
127
|
this.metrics.length = 0;
|
|
128
128
|
this.activeTimers.clear();
|
|
129
129
|
}
|
|
130
|
+
reset() {
|
|
131
|
+
this.clear();
|
|
132
|
+
}
|
|
130
133
|
time(name, fn, metadata) {
|
|
131
134
|
if (!isLogEnabled)
|
|
132
135
|
return fn();
|
|
@@ -326,6 +329,8 @@
|
|
|
326
329
|
const filteredPoints = hyphenPoints.filter((pos) => pos >= lefthyphenmin && word.length - pos >= righthyphenmin);
|
|
327
330
|
return filteredPoints;
|
|
328
331
|
}
|
|
332
|
+
// Converts text into items (boxes, glues, penalties) for line breaking.
|
|
333
|
+
// The measureText function should return widths that include any letter spacing.
|
|
329
334
|
static itemizeText(text, measureText, // function to measure text width
|
|
330
335
|
hyphenate = false, language = 'en-us', availablePatterns, lefthyphenmin = DEFAULT_LEFT_HYPHEN_MIN, righthyphenmin = DEFAULT_RIGHT_HYPHEN_MIN, context) {
|
|
331
336
|
const items = [];
|
|
@@ -567,7 +572,7 @@
|
|
|
567
572
|
let useHyphenation = hyphenate;
|
|
568
573
|
if (useHyphenation &&
|
|
569
574
|
(!hyphenationPatterns || !hyphenationPatterns[language])) {
|
|
570
|
-
|
|
575
|
+
logger.warn(`Hyphenation patterns for ${language} not available`);
|
|
571
576
|
useHyphenation = false;
|
|
572
577
|
}
|
|
573
578
|
// Calculate initial emergency stretch (TeX default: 0)
|
|
@@ -1182,12 +1187,43 @@
|
|
|
1182
1187
|
}
|
|
1183
1188
|
}
|
|
1184
1189
|
|
|
1190
|
+
// Convert feature objects to HarfBuzz comma-separated format
|
|
1191
|
+
function convertFontFeaturesToString(features) {
|
|
1192
|
+
if (!features || Object.keys(features).length === 0) {
|
|
1193
|
+
return undefined;
|
|
1194
|
+
}
|
|
1195
|
+
const featureStrings = [];
|
|
1196
|
+
for (const [tag, value] of Object.entries(features)) {
|
|
1197
|
+
if (!/^[a-zA-Z0-9]{4}$/.test(tag)) {
|
|
1198
|
+
logger.warn(`Invalid OpenType feature tag: "${tag}". Tags must be exactly 4 alphanumeric characters.`);
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
1201
|
+
if (value === false || value === 0) {
|
|
1202
|
+
featureStrings.push(`${tag}=0`);
|
|
1203
|
+
}
|
|
1204
|
+
else if (value === true || value === 1) {
|
|
1205
|
+
featureStrings.push(tag);
|
|
1206
|
+
}
|
|
1207
|
+
else if (typeof value === 'number' && value > 1) {
|
|
1208
|
+
featureStrings.push(`${tag}=${Math.floor(value)}`);
|
|
1209
|
+
}
|
|
1210
|
+
else {
|
|
1211
|
+
logger.warn(`Invalid value for feature "${tag}": ${value}. Expected boolean or positive number.`);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
return featureStrings.length > 0 ? featureStrings.join(',') : undefined;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1185
1217
|
class TextMeasurer {
|
|
1218
|
+
// Measures text width including letter spacing
|
|
1219
|
+
// Letter spacing is added uniformly after each glyph during measurement,
|
|
1220
|
+
// so the widths given to the line-breaking algorithm already account for tracking
|
|
1186
1221
|
static measureTextWidth(loadedFont, text, letterSpacing = 0) {
|
|
1187
1222
|
const buffer = loadedFont.hb.createBuffer();
|
|
1188
1223
|
buffer.addText(text);
|
|
1189
1224
|
buffer.guessSegmentProperties();
|
|
1190
|
-
|
|
1225
|
+
const featuresString = convertFontFeaturesToString(loadedFont.fontFeatures);
|
|
1226
|
+
loadedFont.hb.shape(loadedFont.font, buffer, featuresString);
|
|
1191
1227
|
const glyphInfos = buffer.json(loadedFont.font);
|
|
1192
1228
|
const letterSpacingInFontUnits = letterSpacing * loadedFont.upem;
|
|
1193
1229
|
// Calculate total advance width with letter spacing
|
|
@@ -1211,6 +1247,8 @@
|
|
|
1211
1247
|
const { text, width, align, direction, hyphenate, language, respectExistingBreaks, tolerance, pretolerance, emergencyStretch, autoEmergencyStretch, hyphenationPatterns, lefthyphenmin, righthyphenmin, linepenalty, adjdemerits, hyphenpenalty, exhyphenpenalty, doublehyphendemerits, looseness, disableSingleWordDetection, letterSpacing } = options;
|
|
1212
1248
|
let lines;
|
|
1213
1249
|
if (width) {
|
|
1250
|
+
// Line breaking uses a measureText function that already includes letterSpacing,
|
|
1251
|
+
// so widths passed into LineBreak.breakText account for tracking
|
|
1214
1252
|
lines = LineBreak.breakText({
|
|
1215
1253
|
text,
|
|
1216
1254
|
width,
|
|
@@ -1234,7 +1272,8 @@
|
|
|
1234
1272
|
looseness,
|
|
1235
1273
|
disableSingleWordDetection,
|
|
1236
1274
|
unitsPerEm: this.loadedFont.upem,
|
|
1237
|
-
measureText: (textToMeasure) => TextMeasurer.measureTextWidth(this.loadedFont, textToMeasure, letterSpacing
|
|
1275
|
+
measureText: (textToMeasure) => TextMeasurer.measureTextWidth(this.loadedFont, textToMeasure, letterSpacing // Letter spacing included in width measurements
|
|
1276
|
+
)
|
|
1238
1277
|
});
|
|
1239
1278
|
}
|
|
1240
1279
|
else {
|
|
@@ -1288,6 +1327,17 @@
|
|
|
1288
1327
|
const FONT_SIGNATURE_TRUE_TYPE_COLLECTION = 0x74746366; // 'ttcf'
|
|
1289
1328
|
const FONT_SIGNATURE_WOFF = 0x774f4646; // 'wOFF'
|
|
1290
1329
|
const FONT_SIGNATURE_WOFF2 = 0x774f4632; // 'wOF2'
|
|
1330
|
+
// Table Tags
|
|
1331
|
+
const TABLE_TAG_HEAD = 0x68656164; // 'head'
|
|
1332
|
+
const TABLE_TAG_HHEA = 0x68686561; // 'hhea'
|
|
1333
|
+
const TABLE_TAG_OS2 = 0x4f532f32; // 'OS/2'
|
|
1334
|
+
const TABLE_TAG_FVAR = 0x66766172; // 'fvar'
|
|
1335
|
+
const TABLE_TAG_STAT = 0x53544154; // 'STAT'
|
|
1336
|
+
const TABLE_TAG_NAME = 0x6e616d65; // 'name'
|
|
1337
|
+
const TABLE_TAG_CFF = 0x43464620; // 'CFF '
|
|
1338
|
+
const TABLE_TAG_CFF2 = 0x43464632; // 'CFF2'
|
|
1339
|
+
const TABLE_TAG_GSUB = 0x47535542; // 'GSUB'
|
|
1340
|
+
const TABLE_TAG_GPOS = 0x47504f53; // 'GPOS'
|
|
1291
1341
|
|
|
1292
1342
|
class FontMetadataExtractor {
|
|
1293
1343
|
static extractMetadata(fontBuffer) {
|
|
@@ -1304,7 +1354,6 @@
|
|
|
1304
1354
|
if (!validSignatures.includes(sfntVersion)) {
|
|
1305
1355
|
throw new Error(`Invalid font format. Expected TrueType or OpenType, got signature: 0x${sfntVersion.toString(16)}`);
|
|
1306
1356
|
}
|
|
1307
|
-
const buffer = new Uint8Array(fontBuffer);
|
|
1308
1357
|
const numTables = view.getUint16(4); // OpenType header - number of tables is at offset 4
|
|
1309
1358
|
let isCFF = false;
|
|
1310
1359
|
let headTableOffset = 0;
|
|
@@ -1314,30 +1363,28 @@
|
|
|
1314
1363
|
let nameTableOffset = 0;
|
|
1315
1364
|
let fvarTableOffset = 0;
|
|
1316
1365
|
for (let i = 0; i < numTables; i++) {
|
|
1317
|
-
const
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
}
|
|
1321
|
-
else if (tag === 'CFF2') {
|
|
1366
|
+
const offset = 12 + i * 16;
|
|
1367
|
+
const tag = view.getUint32(offset);
|
|
1368
|
+
if (tag === TABLE_TAG_CFF || tag === TABLE_TAG_CFF2) {
|
|
1322
1369
|
isCFF = true;
|
|
1323
1370
|
}
|
|
1324
|
-
if (tag ===
|
|
1325
|
-
headTableOffset = view.getUint32(
|
|
1371
|
+
else if (tag === TABLE_TAG_HEAD) {
|
|
1372
|
+
headTableOffset = view.getUint32(offset + 8);
|
|
1326
1373
|
}
|
|
1327
|
-
if (tag ===
|
|
1328
|
-
hheaTableOffset = view.getUint32(
|
|
1374
|
+
else if (tag === TABLE_TAG_HHEA) {
|
|
1375
|
+
hheaTableOffset = view.getUint32(offset + 8);
|
|
1329
1376
|
}
|
|
1330
|
-
if (tag ===
|
|
1331
|
-
os2TableOffset = view.getUint32(
|
|
1377
|
+
else if (tag === TABLE_TAG_OS2) {
|
|
1378
|
+
os2TableOffset = view.getUint32(offset + 8);
|
|
1332
1379
|
}
|
|
1333
|
-
if (tag ===
|
|
1334
|
-
fvarTableOffset = view.getUint32(
|
|
1380
|
+
else if (tag === TABLE_TAG_FVAR) {
|
|
1381
|
+
fvarTableOffset = view.getUint32(offset + 8);
|
|
1335
1382
|
}
|
|
1336
|
-
if (tag ===
|
|
1337
|
-
statTableOffset = view.getUint32(
|
|
1383
|
+
else if (tag === TABLE_TAG_STAT) {
|
|
1384
|
+
statTableOffset = view.getUint32(offset + 8);
|
|
1338
1385
|
}
|
|
1339
|
-
if (tag ===
|
|
1340
|
-
nameTableOffset = view.getUint32(
|
|
1386
|
+
else if (tag === TABLE_TAG_NAME) {
|
|
1387
|
+
nameTableOffset = view.getUint32(offset + 8);
|
|
1341
1388
|
}
|
|
1342
1389
|
}
|
|
1343
1390
|
const unitsPerEm = headTableOffset
|
|
@@ -1380,6 +1427,88 @@
|
|
|
1380
1427
|
axisNames
|
|
1381
1428
|
};
|
|
1382
1429
|
}
|
|
1430
|
+
static extractFeatureTags(fontBuffer) {
|
|
1431
|
+
const view = new DataView(fontBuffer);
|
|
1432
|
+
const numTables = view.getUint16(4);
|
|
1433
|
+
let gsubTableOffset = 0;
|
|
1434
|
+
let gposTableOffset = 0;
|
|
1435
|
+
let nameTableOffset = 0;
|
|
1436
|
+
for (let i = 0; i < numTables; i++) {
|
|
1437
|
+
const offset = 12 + i * 16;
|
|
1438
|
+
const tag = view.getUint32(offset);
|
|
1439
|
+
if (tag === TABLE_TAG_GSUB) {
|
|
1440
|
+
gsubTableOffset = view.getUint32(offset + 8);
|
|
1441
|
+
}
|
|
1442
|
+
else if (tag === TABLE_TAG_GPOS) {
|
|
1443
|
+
gposTableOffset = view.getUint32(offset + 8);
|
|
1444
|
+
}
|
|
1445
|
+
else if (tag === TABLE_TAG_NAME) {
|
|
1446
|
+
nameTableOffset = view.getUint32(offset + 8);
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
const features = new Set();
|
|
1450
|
+
const featureNames = {};
|
|
1451
|
+
try {
|
|
1452
|
+
if (gsubTableOffset) {
|
|
1453
|
+
const gsubData = this.extractFeatureDataFromTable(view, gsubTableOffset, nameTableOffset);
|
|
1454
|
+
gsubData.features.forEach(f => features.add(f));
|
|
1455
|
+
Object.assign(featureNames, gsubData.names);
|
|
1456
|
+
}
|
|
1457
|
+
if (gposTableOffset) {
|
|
1458
|
+
const gposData = this.extractFeatureDataFromTable(view, gposTableOffset, nameTableOffset);
|
|
1459
|
+
gposData.features.forEach(f => features.add(f));
|
|
1460
|
+
Object.assign(featureNames, gposData.names);
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
catch (e) {
|
|
1464
|
+
return undefined;
|
|
1465
|
+
}
|
|
1466
|
+
const featureArray = Array.from(features).sort();
|
|
1467
|
+
if (featureArray.length === 0)
|
|
1468
|
+
return undefined;
|
|
1469
|
+
return {
|
|
1470
|
+
tags: featureArray,
|
|
1471
|
+
names: Object.keys(featureNames).length > 0 ? featureNames : {}
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1474
|
+
static extractFeatureDataFromTable(view, tableOffset, nameTableOffset) {
|
|
1475
|
+
const featureListOffset = view.getUint16(tableOffset + 6);
|
|
1476
|
+
const featureListStart = tableOffset + featureListOffset;
|
|
1477
|
+
const featureCount = view.getUint16(featureListStart);
|
|
1478
|
+
const features = [];
|
|
1479
|
+
const names = {};
|
|
1480
|
+
for (let i = 0; i < featureCount; i++) {
|
|
1481
|
+
const recordOffset = featureListStart + 2 + i * 6;
|
|
1482
|
+
// Decode feature tag
|
|
1483
|
+
const tag = String.fromCharCode(view.getUint8(recordOffset), view.getUint8(recordOffset + 1), view.getUint8(recordOffset + 2), view.getUint8(recordOffset + 3));
|
|
1484
|
+
features.push(tag);
|
|
1485
|
+
// Extract feature name for stylistic sets and character variants
|
|
1486
|
+
if (/^(ss\d{2}|cv\d{2})$/.test(tag) && nameTableOffset) {
|
|
1487
|
+
const featureOffset = view.getUint16(recordOffset + 4);
|
|
1488
|
+
const featureTableStart = featureListStart + featureOffset;
|
|
1489
|
+
// Feature table structure:
|
|
1490
|
+
// uint16 FeatureParams offset
|
|
1491
|
+
// uint16 LookupCount
|
|
1492
|
+
// uint16[LookupCount] LookupListIndex
|
|
1493
|
+
const featureParamsOffset = view.getUint16(featureTableStart);
|
|
1494
|
+
// FeatureParams for ss features:
|
|
1495
|
+
// uint16 Version (should be 0)
|
|
1496
|
+
// uint16 UINameID
|
|
1497
|
+
if (featureParamsOffset !== 0) {
|
|
1498
|
+
const paramsStart = featureTableStart + featureParamsOffset;
|
|
1499
|
+
const version = view.getUint16(paramsStart);
|
|
1500
|
+
if (version === 0) {
|
|
1501
|
+
const nameID = view.getUint16(paramsStart + 2);
|
|
1502
|
+
const name = this.getNameFromNameTable(view, nameTableOffset, nameID);
|
|
1503
|
+
if (name) {
|
|
1504
|
+
names[tag] = name;
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
return { features, names };
|
|
1511
|
+
}
|
|
1383
1512
|
static extractAxisNames(view, statOffset, nameOffset) {
|
|
1384
1513
|
try {
|
|
1385
1514
|
// STAT table structure
|
|
@@ -1590,7 +1719,7 @@
|
|
|
1590
1719
|
const padding = (4 - (table.origLength % 4)) % 4;
|
|
1591
1720
|
sfntOffset += padding;
|
|
1592
1721
|
}
|
|
1593
|
-
|
|
1722
|
+
logger.log('WOFF font decompressed successfully');
|
|
1594
1723
|
return sfntData.buffer.slice(0, sfntOffset);
|
|
1595
1724
|
}
|
|
1596
1725
|
static async decompressZlib(compressedData) {
|
|
@@ -1619,7 +1748,7 @@
|
|
|
1619
1748
|
// Check if this is a WOFF font and decompress if needed
|
|
1620
1749
|
const format = WoffConverter.detectFormat(fontBuffer);
|
|
1621
1750
|
if (format === 'woff') {
|
|
1622
|
-
|
|
1751
|
+
logger.log('WOFF font detected, decompressing...');
|
|
1623
1752
|
fontBuffer = await WoffConverter.decompressWoff(fontBuffer);
|
|
1624
1753
|
}
|
|
1625
1754
|
else if (format === 'woff2') {
|
|
@@ -1657,6 +1786,7 @@
|
|
|
1657
1786
|
};
|
|
1658
1787
|
}
|
|
1659
1788
|
}
|
|
1789
|
+
const featureData = FontMetadataExtractor.extractFeatureTags(fontBuffer);
|
|
1660
1790
|
return {
|
|
1661
1791
|
hb,
|
|
1662
1792
|
fontBlob,
|
|
@@ -1667,11 +1797,13 @@
|
|
|
1667
1797
|
metrics,
|
|
1668
1798
|
fontVariations,
|
|
1669
1799
|
isVariable,
|
|
1670
|
-
variationAxes
|
|
1800
|
+
variationAxes,
|
|
1801
|
+
availableFeatures: featureData?.tags,
|
|
1802
|
+
featureNames: featureData?.names
|
|
1671
1803
|
};
|
|
1672
1804
|
}
|
|
1673
1805
|
catch (error) {
|
|
1674
|
-
|
|
1806
|
+
logger.error('Failed to load font:', error);
|
|
1675
1807
|
throw error;
|
|
1676
1808
|
}
|
|
1677
1809
|
finally {
|
|
@@ -1692,7 +1824,7 @@
|
|
|
1692
1824
|
}
|
|
1693
1825
|
}
|
|
1694
1826
|
catch (error) {
|
|
1695
|
-
|
|
1827
|
+
logger.error('Error destroying font resources:', error);
|
|
1696
1828
|
}
|
|
1697
1829
|
}
|
|
1698
1830
|
}
|
|
@@ -2096,7 +2228,7 @@
|
|
|
2096
2228
|
if (valid.length === 0) {
|
|
2097
2229
|
return { triangles: { vertices: [], indices: [] }, contours: [] };
|
|
2098
2230
|
}
|
|
2099
|
-
|
|
2231
|
+
logger.log(`Tessellator: removeOverlaps=${removeOverlaps}, processing ${valid.length} paths`);
|
|
2100
2232
|
return this.tessellate(valid, removeOverlaps, isCFF);
|
|
2101
2233
|
}
|
|
2102
2234
|
tessellate(paths, removeOverlaps, isCFF) {
|
|
@@ -2106,27 +2238,35 @@
|
|
|
2106
2238
|
: paths;
|
|
2107
2239
|
let contours = this.pathsToContours(normalizedPaths);
|
|
2108
2240
|
if (removeOverlaps) {
|
|
2109
|
-
|
|
2241
|
+
logger.log('Two-pass: boundary extraction then triangulation');
|
|
2110
2242
|
// Extract boundaries to remove overlaps
|
|
2243
|
+
perfLogger.start('Tessellator.boundaryPass', {
|
|
2244
|
+
contourCount: contours.length
|
|
2245
|
+
});
|
|
2111
2246
|
const boundaryResult = this.performTessellation(contours, 'boundary');
|
|
2247
|
+
perfLogger.end('Tessellator.boundaryPass');
|
|
2112
2248
|
if (!boundaryResult) {
|
|
2113
|
-
|
|
2249
|
+
logger.warn('libtess returned empty result from boundary pass');
|
|
2114
2250
|
return { triangles: { vertices: [], indices: [] }, contours: [] };
|
|
2115
2251
|
}
|
|
2116
2252
|
// Convert boundary elements back to contours
|
|
2117
2253
|
contours = this.boundaryToContours(boundaryResult);
|
|
2118
|
-
|
|
2254
|
+
logger.log(`Boundary pass created ${contours.length} contours. Starting triangulation pass.`);
|
|
2119
2255
|
}
|
|
2120
2256
|
else {
|
|
2121
|
-
|
|
2257
|
+
logger.log(`Single-pass triangulation for ${isCFF ? 'CFF' : 'TTF'}`);
|
|
2122
2258
|
}
|
|
2123
2259
|
// Triangulate the contours
|
|
2260
|
+
perfLogger.start('Tessellator.triangulationPass', {
|
|
2261
|
+
contourCount: contours.length
|
|
2262
|
+
});
|
|
2124
2263
|
const triangleResult = this.performTessellation(contours, 'triangles');
|
|
2264
|
+
perfLogger.end('Tessellator.triangulationPass');
|
|
2125
2265
|
if (!triangleResult) {
|
|
2126
2266
|
const warning = removeOverlaps
|
|
2127
2267
|
? 'libtess returned empty result from triangulation pass'
|
|
2128
2268
|
: 'libtess returned empty result from single-pass triangulation';
|
|
2129
|
-
|
|
2269
|
+
logger.warn(warning);
|
|
2130
2270
|
return { triangles: { vertices: [], indices: [] }, contours };
|
|
2131
2271
|
}
|
|
2132
2272
|
return {
|
|
@@ -2181,7 +2321,7 @@
|
|
|
2181
2321
|
return idx;
|
|
2182
2322
|
});
|
|
2183
2323
|
tess.gluTessCallback(libtess_minExports.gluEnum.GLU_TESS_ERROR, (errno) => {
|
|
2184
|
-
|
|
2324
|
+
logger.warn(`libtess error: ${errno}`);
|
|
2185
2325
|
});
|
|
2186
2326
|
tess.gluTessNormal(0, 0, 1);
|
|
2187
2327
|
tess.gluTessBeginPolygon(null);
|
|
@@ -2308,10 +2448,16 @@
|
|
|
2308
2448
|
class BoundaryClusterer {
|
|
2309
2449
|
constructor() { }
|
|
2310
2450
|
cluster(glyphContoursList, positions) {
|
|
2451
|
+
perfLogger.start('BoundaryClusterer.cluster', {
|
|
2452
|
+
glyphCount: glyphContoursList.length
|
|
2453
|
+
});
|
|
2311
2454
|
if (glyphContoursList.length === 0) {
|
|
2455
|
+
perfLogger.end('BoundaryClusterer.cluster');
|
|
2312
2456
|
return [];
|
|
2313
2457
|
}
|
|
2314
|
-
|
|
2458
|
+
const result = this.clusterSweepLine(glyphContoursList, positions);
|
|
2459
|
+
perfLogger.end('BoundaryClusterer.cluster');
|
|
2460
|
+
return result;
|
|
2315
2461
|
}
|
|
2316
2462
|
clusterSweepLine(glyphContoursList, positions) {
|
|
2317
2463
|
const n = glyphContoursList.length;
|
|
@@ -2952,6 +3098,11 @@
|
|
|
2952
3098
|
this.currentGlyphBounds.max.set(-Infinity, -Infinity);
|
|
2953
3099
|
// Record position for this glyph
|
|
2954
3100
|
this.glyphPositions.push(this.currentPosition.clone());
|
|
3101
|
+
// Time polygonization + path optimization per glyph
|
|
3102
|
+
perfLogger.start('Glyph.polygonizeAndOptimize', {
|
|
3103
|
+
glyphId,
|
|
3104
|
+
textIndex
|
|
3105
|
+
});
|
|
2955
3106
|
}
|
|
2956
3107
|
finishGlyph() {
|
|
2957
3108
|
if (this.currentPath) {
|
|
@@ -2975,6 +3126,8 @@
|
|
|
2975
3126
|
// Track textIndex separately
|
|
2976
3127
|
this.glyphTextIndices.push(this.currentTextIndex);
|
|
2977
3128
|
}
|
|
3129
|
+
// Stop timing for this glyph (even if it ended up empty)
|
|
3130
|
+
perfLogger.end('Glyph.polygonizeAndOptimize');
|
|
2978
3131
|
this.currentGlyphPaths = [];
|
|
2979
3132
|
}
|
|
2980
3133
|
onMoveTo(x, y) {
|
|
@@ -3203,12 +3356,190 @@
|
|
|
3203
3356
|
}
|
|
3204
3357
|
}
|
|
3205
3358
|
catch (error) {
|
|
3206
|
-
|
|
3359
|
+
logger.warn('Error destroying draw callbacks:', error);
|
|
3207
3360
|
}
|
|
3208
3361
|
this.collector = undefined;
|
|
3209
3362
|
}
|
|
3210
3363
|
}
|
|
3211
3364
|
|
|
3365
|
+
// Generic LRU (Least Recently Used) cache with optional memory-based eviction
|
|
3366
|
+
class LRUCache {
|
|
3367
|
+
constructor(options = {}) {
|
|
3368
|
+
this.cache = new Map();
|
|
3369
|
+
this.head = null;
|
|
3370
|
+
this.tail = null;
|
|
3371
|
+
this.stats = {
|
|
3372
|
+
hits: 0,
|
|
3373
|
+
misses: 0,
|
|
3374
|
+
evictions: 0,
|
|
3375
|
+
size: 0,
|
|
3376
|
+
memoryUsage: 0
|
|
3377
|
+
};
|
|
3378
|
+
this.options = {
|
|
3379
|
+
maxEntries: options.maxEntries ?? Infinity,
|
|
3380
|
+
maxMemoryBytes: options.maxMemoryBytes ?? Infinity,
|
|
3381
|
+
calculateSize: options.calculateSize ?? (() => 0),
|
|
3382
|
+
onEvict: options.onEvict
|
|
3383
|
+
};
|
|
3384
|
+
}
|
|
3385
|
+
get(key) {
|
|
3386
|
+
const node = this.cache.get(key);
|
|
3387
|
+
if (node) {
|
|
3388
|
+
this.stats.hits++;
|
|
3389
|
+
this.moveToHead(node);
|
|
3390
|
+
return node.value;
|
|
3391
|
+
}
|
|
3392
|
+
else {
|
|
3393
|
+
this.stats.misses++;
|
|
3394
|
+
return undefined;
|
|
3395
|
+
}
|
|
3396
|
+
}
|
|
3397
|
+
has(key) {
|
|
3398
|
+
return this.cache.has(key);
|
|
3399
|
+
}
|
|
3400
|
+
set(key, value) {
|
|
3401
|
+
// If key already exists, update it
|
|
3402
|
+
const existingNode = this.cache.get(key);
|
|
3403
|
+
if (existingNode) {
|
|
3404
|
+
const oldSize = this.options.calculateSize(existingNode.value);
|
|
3405
|
+
const newSize = this.options.calculateSize(value);
|
|
3406
|
+
this.stats.memoryUsage = this.stats.memoryUsage - oldSize + newSize;
|
|
3407
|
+
existingNode.value = value;
|
|
3408
|
+
this.moveToHead(existingNode);
|
|
3409
|
+
return;
|
|
3410
|
+
}
|
|
3411
|
+
const size = this.options.calculateSize(value);
|
|
3412
|
+
// Evict entries if we exceed limits
|
|
3413
|
+
this.evictIfNeeded(size);
|
|
3414
|
+
// Create new node
|
|
3415
|
+
const node = {
|
|
3416
|
+
key,
|
|
3417
|
+
value,
|
|
3418
|
+
prev: null,
|
|
3419
|
+
next: null
|
|
3420
|
+
};
|
|
3421
|
+
this.cache.set(key, node);
|
|
3422
|
+
this.addToHead(node);
|
|
3423
|
+
this.stats.size = this.cache.size;
|
|
3424
|
+
this.stats.memoryUsage += size;
|
|
3425
|
+
}
|
|
3426
|
+
delete(key) {
|
|
3427
|
+
const node = this.cache.get(key);
|
|
3428
|
+
if (!node)
|
|
3429
|
+
return false;
|
|
3430
|
+
const size = this.options.calculateSize(node.value);
|
|
3431
|
+
this.removeNode(node);
|
|
3432
|
+
this.cache.delete(key);
|
|
3433
|
+
this.stats.size = this.cache.size;
|
|
3434
|
+
this.stats.memoryUsage -= size;
|
|
3435
|
+
if (this.options.onEvict) {
|
|
3436
|
+
this.options.onEvict(key, node.value);
|
|
3437
|
+
}
|
|
3438
|
+
return true;
|
|
3439
|
+
}
|
|
3440
|
+
clear() {
|
|
3441
|
+
if (this.options.onEvict) {
|
|
3442
|
+
for (const [key, node] of this.cache) {
|
|
3443
|
+
this.options.onEvict(key, node.value);
|
|
3444
|
+
}
|
|
3445
|
+
}
|
|
3446
|
+
this.cache.clear();
|
|
3447
|
+
this.head = null;
|
|
3448
|
+
this.tail = null;
|
|
3449
|
+
this.stats = {
|
|
3450
|
+
hits: 0,
|
|
3451
|
+
misses: 0,
|
|
3452
|
+
evictions: 0,
|
|
3453
|
+
size: 0,
|
|
3454
|
+
memoryUsage: 0
|
|
3455
|
+
};
|
|
3456
|
+
}
|
|
3457
|
+
getStats() {
|
|
3458
|
+
const total = this.stats.hits + this.stats.misses;
|
|
3459
|
+
const hitRate = total > 0 ? (this.stats.hits / total) * 100 : 0;
|
|
3460
|
+
const memoryUsageMB = this.stats.memoryUsage / (1024 * 1024);
|
|
3461
|
+
return {
|
|
3462
|
+
...this.stats,
|
|
3463
|
+
hitRate,
|
|
3464
|
+
memoryUsageMB
|
|
3465
|
+
};
|
|
3466
|
+
}
|
|
3467
|
+
keys() {
|
|
3468
|
+
const keys = [];
|
|
3469
|
+
let current = this.head;
|
|
3470
|
+
while (current) {
|
|
3471
|
+
keys.push(current.key);
|
|
3472
|
+
current = current.next;
|
|
3473
|
+
}
|
|
3474
|
+
return keys;
|
|
3475
|
+
}
|
|
3476
|
+
get size() {
|
|
3477
|
+
return this.cache.size;
|
|
3478
|
+
}
|
|
3479
|
+
evictIfNeeded(requiredSize) {
|
|
3480
|
+
// Evict by entry count
|
|
3481
|
+
while (this.cache.size >= this.options.maxEntries && this.tail) {
|
|
3482
|
+
this.evictTail();
|
|
3483
|
+
}
|
|
3484
|
+
// Evict by memory usage
|
|
3485
|
+
if (this.options.maxMemoryBytes < Infinity) {
|
|
3486
|
+
while (this.tail &&
|
|
3487
|
+
this.stats.memoryUsage + requiredSize > this.options.maxMemoryBytes) {
|
|
3488
|
+
this.evictTail();
|
|
3489
|
+
}
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
3492
|
+
evictTail() {
|
|
3493
|
+
if (!this.tail)
|
|
3494
|
+
return;
|
|
3495
|
+
const nodeToRemove = this.tail;
|
|
3496
|
+
const size = this.options.calculateSize(nodeToRemove.value);
|
|
3497
|
+
this.removeTail();
|
|
3498
|
+
this.cache.delete(nodeToRemove.key);
|
|
3499
|
+
this.stats.size = this.cache.size;
|
|
3500
|
+
this.stats.memoryUsage -= size;
|
|
3501
|
+
this.stats.evictions++;
|
|
3502
|
+
if (this.options.onEvict) {
|
|
3503
|
+
this.options.onEvict(nodeToRemove.key, nodeToRemove.value);
|
|
3504
|
+
}
|
|
3505
|
+
}
|
|
3506
|
+
addToHead(node) {
|
|
3507
|
+
if (!this.head) {
|
|
3508
|
+
this.head = this.tail = node;
|
|
3509
|
+
}
|
|
3510
|
+
else {
|
|
3511
|
+
node.next = this.head;
|
|
3512
|
+
this.head.prev = node;
|
|
3513
|
+
this.head = node;
|
|
3514
|
+
}
|
|
3515
|
+
}
|
|
3516
|
+
removeNode(node) {
|
|
3517
|
+
if (node.prev) {
|
|
3518
|
+
node.prev.next = node.next;
|
|
3519
|
+
}
|
|
3520
|
+
else {
|
|
3521
|
+
this.head = node.next;
|
|
3522
|
+
}
|
|
3523
|
+
if (node.next) {
|
|
3524
|
+
node.next.prev = node.prev;
|
|
3525
|
+
}
|
|
3526
|
+
else {
|
|
3527
|
+
this.tail = node.prev;
|
|
3528
|
+
}
|
|
3529
|
+
}
|
|
3530
|
+
removeTail() {
|
|
3531
|
+
if (this.tail) {
|
|
3532
|
+
this.removeNode(this.tail);
|
|
3533
|
+
}
|
|
3534
|
+
}
|
|
3535
|
+
moveToHead(node) {
|
|
3536
|
+
if (node === this.head)
|
|
3537
|
+
return;
|
|
3538
|
+
this.removeNode(node);
|
|
3539
|
+
this.addToHead(node);
|
|
3540
|
+
}
|
|
3541
|
+
}
|
|
3542
|
+
|
|
3212
3543
|
class GlyphGeometryBuilder {
|
|
3213
3544
|
constructor(cache, loadedFont) {
|
|
3214
3545
|
this.fontId = 'default';
|
|
@@ -3221,6 +3552,16 @@
|
|
|
3221
3552
|
this.collector = new GlyphContourCollector();
|
|
3222
3553
|
this.drawCallbacks = new DrawCallbackHandler();
|
|
3223
3554
|
this.drawCallbacks.createDrawFuncs(this.loadedFont, this.collector);
|
|
3555
|
+
this.contourCache = new LRUCache({
|
|
3556
|
+
maxEntries: 1000,
|
|
3557
|
+
calculateSize: (contours) => {
|
|
3558
|
+
let size = 0;
|
|
3559
|
+
for (const path of contours.paths) {
|
|
3560
|
+
size += path.points.length * 16; // Vec2 = 2 floats * 8 bytes
|
|
3561
|
+
}
|
|
3562
|
+
return size + 64; // bounds overhead
|
|
3563
|
+
}
|
|
3564
|
+
});
|
|
3224
3565
|
}
|
|
3225
3566
|
getOptimizationStats() {
|
|
3226
3567
|
return this.collector.getOptimizationStats();
|
|
@@ -3365,30 +3706,37 @@
|
|
|
3365
3706
|
};
|
|
3366
3707
|
}
|
|
3367
3708
|
getContoursForGlyph(glyphId) {
|
|
3709
|
+
const cached = this.contourCache.get(glyphId);
|
|
3710
|
+
if (cached) {
|
|
3711
|
+
return cached;
|
|
3712
|
+
}
|
|
3368
3713
|
this.collector.reset();
|
|
3369
3714
|
this.collector.beginGlyph(glyphId, 0);
|
|
3370
3715
|
this.loadedFont.module.exports.hb_font_draw_glyph(this.loadedFont.font.ptr, glyphId, this.drawCallbacks.getDrawFuncsPtr(), 0);
|
|
3371
3716
|
this.collector.finishGlyph();
|
|
3372
3717
|
const collected = this.collector.getCollectedGlyphs()[0];
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
}
|
|
3384
|
-
return collected;
|
|
3718
|
+
const contours = collected || {
|
|
3719
|
+
glyphId,
|
|
3720
|
+
paths: [],
|
|
3721
|
+
bounds: {
|
|
3722
|
+
min: { x: 0, y: 0 },
|
|
3723
|
+
max: { x: 0, y: 0 }
|
|
3724
|
+
}
|
|
3725
|
+
};
|
|
3726
|
+
this.contourCache.set(glyphId, contours);
|
|
3727
|
+
return contours;
|
|
3385
3728
|
}
|
|
3386
3729
|
tessellateGlyphCluster(paths, depth, isCFF) {
|
|
3387
3730
|
const processedGeometry = this.tessellator.process(paths, true, isCFF);
|
|
3388
3731
|
return this.extrudeAndPackage(processedGeometry, depth);
|
|
3389
3732
|
}
|
|
3390
3733
|
extrudeAndPackage(processedGeometry, depth) {
|
|
3734
|
+
perfLogger.start('Extruder.extrude', {
|
|
3735
|
+
depth,
|
|
3736
|
+
upem: this.loadedFont.upem
|
|
3737
|
+
});
|
|
3391
3738
|
const extrudedResult = this.extruder.extrude(processedGeometry, depth, this.loadedFont.upem);
|
|
3739
|
+
perfLogger.end('Extruder.extrude');
|
|
3392
3740
|
// Compute bounding box from vertices
|
|
3393
3741
|
const vertices = extrudedResult.vertices;
|
|
3394
3742
|
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
|
@@ -3430,6 +3778,7 @@
|
|
|
3430
3778
|
pathCount: glyphContours.paths.length
|
|
3431
3779
|
});
|
|
3432
3780
|
const processedGeometry = this.tessellator.process(glyphContours.paths, removeOverlaps, isCFF);
|
|
3781
|
+
perfLogger.end('GlyphGeometryBuilder.tessellateGlyph');
|
|
3433
3782
|
return this.extrudeAndPackage(processedGeometry, depth);
|
|
3434
3783
|
}
|
|
3435
3784
|
updatePlaneBounds(glyphBounds, planeBounds) {
|
|
@@ -3493,7 +3842,8 @@
|
|
|
3493
3842
|
}
|
|
3494
3843
|
buffer.addText(lineInfo.text);
|
|
3495
3844
|
buffer.guessSegmentProperties();
|
|
3496
|
-
|
|
3845
|
+
const featuresString = convertFontFeaturesToString(this.loadedFont.fontFeatures);
|
|
3846
|
+
this.loadedFont.hb.shape(this.loadedFont.font, buffer, featuresString);
|
|
3497
3847
|
const glyphInfos = buffer.json(this.loadedFont.font);
|
|
3498
3848
|
buffer.destroy();
|
|
3499
3849
|
const clusters = [];
|
|
@@ -3501,6 +3851,7 @@
|
|
|
3501
3851
|
let currentClusterText = '';
|
|
3502
3852
|
let clusterStartPosition = new Vec3();
|
|
3503
3853
|
let cursor = new Vec3(lineInfo.xOffset, -lineIndex * scaledLineHeight, 0);
|
|
3854
|
+
// Apply letter spacing between glyphs (must match what was used in width measurements)
|
|
3504
3855
|
const letterSpacingFU = letterSpacing * this.loadedFont.upem;
|
|
3505
3856
|
const spaceAdjustment = this.calculateSpaceAdjustment(lineInfo, align, letterSpacing);
|
|
3506
3857
|
for (let i = 0; i < glyphInfos.length; i++) {
|
|
@@ -3572,12 +3923,10 @@
|
|
|
3572
3923
|
const stretchFactor = SPACE_STRETCH_RATIO;
|
|
3573
3924
|
const shrinkFactor = SPACE_SHRINK_RATIO;
|
|
3574
3925
|
if (lineInfo.adjustmentRatio > 0) {
|
|
3575
|
-
spaceAdjustment =
|
|
3576
|
-
lineInfo.adjustmentRatio * width * stretchFactor;
|
|
3926
|
+
spaceAdjustment = lineInfo.adjustmentRatio * width * stretchFactor;
|
|
3577
3927
|
}
|
|
3578
3928
|
else if (lineInfo.adjustmentRatio < 0) {
|
|
3579
|
-
spaceAdjustment =
|
|
3580
|
-
lineInfo.adjustmentRatio * width * shrinkFactor;
|
|
3929
|
+
spaceAdjustment = lineInfo.adjustmentRatio * width * shrinkFactor;
|
|
3581
3930
|
}
|
|
3582
3931
|
}
|
|
3583
3932
|
return spaceAdjustment;
|
|
@@ -4502,12 +4851,16 @@
|
|
|
4502
4851
|
const baseFontKey = typeof options.font === 'string'
|
|
4503
4852
|
? options.font
|
|
4504
4853
|
: `buffer-${Text.generateFontContentHash(options.font)}`;
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4854
|
+
let fontKey = baseFontKey;
|
|
4855
|
+
if (options.fontVariations) {
|
|
4856
|
+
fontKey += `_var_${JSON.stringify(options.fontVariations)}`;
|
|
4857
|
+
}
|
|
4858
|
+
if (options.fontFeatures) {
|
|
4859
|
+
fontKey += `_feat_${JSON.stringify(options.fontFeatures)}`;
|
|
4860
|
+
}
|
|
4508
4861
|
let loadedFont = Text.fontCache.get(fontKey);
|
|
4509
4862
|
if (!loadedFont) {
|
|
4510
|
-
loadedFont = await Text.loadAndCacheFont(fontKey, options.font, options.fontVariations);
|
|
4863
|
+
loadedFont = await Text.loadAndCacheFont(fontKey, options.font, options.fontVariations, options.fontFeatures);
|
|
4511
4864
|
}
|
|
4512
4865
|
const text = new Text({ maxCacheSizeMB: options.maxCacheSizeMB });
|
|
4513
4866
|
text.setLoadedFont(loadedFont);
|
|
@@ -4521,12 +4874,11 @@
|
|
|
4521
4874
|
measureTextWidth: (textString, letterSpacing) => text.measureTextWidth(textString, letterSpacing)
|
|
4522
4875
|
};
|
|
4523
4876
|
}
|
|
4524
|
-
static async loadAndCacheFont(fontKey, font, fontVariations) {
|
|
4877
|
+
static async loadAndCacheFont(fontKey, font, fontVariations, fontFeatures) {
|
|
4525
4878
|
const tempText = new Text();
|
|
4526
|
-
await tempText.loadFont(font, fontVariations);
|
|
4879
|
+
await tempText.loadFont(font, fontVariations, fontFeatures);
|
|
4527
4880
|
const loadedFont = tempText.getLoadedFont();
|
|
4528
4881
|
Text.fontCache.set(fontKey, loadedFont);
|
|
4529
|
-
// Don't destroy tempText - the cached font references its HarfBuzz objects
|
|
4530
4882
|
return loadedFont;
|
|
4531
4883
|
}
|
|
4532
4884
|
static generateFontContentHash(buffer) {
|
|
@@ -4545,10 +4897,13 @@
|
|
|
4545
4897
|
const contentHash = Text.generateFontContentHash(loadedFont._buffer);
|
|
4546
4898
|
this.currentFontId = `font_${contentHash}`;
|
|
4547
4899
|
if (loadedFont.fontVariations) {
|
|
4548
|
-
this.currentFontId += `
|
|
4900
|
+
this.currentFontId += `_var_${JSON.stringify(loadedFont.fontVariations)}`;
|
|
4901
|
+
}
|
|
4902
|
+
if (loadedFont.fontFeatures) {
|
|
4903
|
+
this.currentFontId += `_feat_${JSON.stringify(loadedFont.fontFeatures)}`;
|
|
4549
4904
|
}
|
|
4550
4905
|
}
|
|
4551
|
-
async loadFont(fontSrc, fontVariations) {
|
|
4906
|
+
async loadFont(fontSrc, fontVariations, fontFeatures) {
|
|
4552
4907
|
perfLogger.start('Text.loadFont', {
|
|
4553
4908
|
fontSrc: typeof fontSrc === 'string' ? fontSrc : `buffer(${fontSrc.byteLength})`
|
|
4554
4909
|
});
|
|
@@ -4569,14 +4924,20 @@
|
|
|
4569
4924
|
this.destroy();
|
|
4570
4925
|
}
|
|
4571
4926
|
this.loadedFont = await this.fontLoader.loadFont(fontBuffer, fontVariations);
|
|
4927
|
+
if (fontFeatures) {
|
|
4928
|
+
this.loadedFont.fontFeatures = fontFeatures;
|
|
4929
|
+
}
|
|
4572
4930
|
const contentHash = Text.generateFontContentHash(fontBuffer);
|
|
4573
4931
|
this.currentFontId = `font_${contentHash}`;
|
|
4574
4932
|
if (fontVariations) {
|
|
4575
|
-
this.currentFontId += `
|
|
4933
|
+
this.currentFontId += `_var_${JSON.stringify(fontVariations)}`;
|
|
4934
|
+
}
|
|
4935
|
+
if (fontFeatures) {
|
|
4936
|
+
this.currentFontId += `_feat_${JSON.stringify(fontFeatures)}`;
|
|
4576
4937
|
}
|
|
4577
4938
|
}
|
|
4578
4939
|
catch (error) {
|
|
4579
|
-
|
|
4940
|
+
logger.error('Failed to load font:', error);
|
|
4580
4941
|
throw error;
|
|
4581
4942
|
}
|
|
4582
4943
|
finally {
|
|
@@ -4651,7 +5012,7 @@
|
|
|
4651
5012
|
};
|
|
4652
5013
|
}
|
|
4653
5014
|
catch (error) {
|
|
4654
|
-
|
|
5015
|
+
logger.warn(`Failed to load patterns for ${language}: ${error}`);
|
|
4655
5016
|
return {
|
|
4656
5017
|
...options,
|
|
4657
5018
|
layout: {
|
|
@@ -4915,7 +5276,7 @@
|
|
|
4915
5276
|
Text.patternCache.set(language, pattern);
|
|
4916
5277
|
}
|
|
4917
5278
|
catch (error) {
|
|
4918
|
-
|
|
5279
|
+
logger.warn(`Failed to pre-load patterns for ${language}: ${error}`);
|
|
4919
5280
|
}
|
|
4920
5281
|
}
|
|
4921
5282
|
}));
|
|
@@ -4977,7 +5338,7 @@
|
|
|
4977
5338
|
FontLoader.destroyFont(currentFont);
|
|
4978
5339
|
}
|
|
4979
5340
|
catch (error) {
|
|
4980
|
-
|
|
5341
|
+
logger.warn('Error destroying HarfBuzz objects:', error);
|
|
4981
5342
|
}
|
|
4982
5343
|
finally {
|
|
4983
5344
|
this.loadedFont = undefined;
|