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.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
|
|
@@ -22,7 +22,7 @@ const isLogEnabled = (() => {
|
|
|
22
22
|
}
|
|
23
23
|
return false;
|
|
24
24
|
})();
|
|
25
|
-
class
|
|
25
|
+
class Logger {
|
|
26
26
|
warn(message, ...args) {
|
|
27
27
|
console.warn(message, ...args);
|
|
28
28
|
}
|
|
@@ -33,7 +33,7 @@ class DebugLogger {
|
|
|
33
33
|
isLogEnabled && console.log(message, ...args);
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
|
-
const
|
|
36
|
+
const logger = new Logger();
|
|
37
37
|
|
|
38
38
|
class PerformanceLogger {
|
|
39
39
|
constructor() {
|
|
@@ -59,7 +59,7 @@ class PerformanceLogger {
|
|
|
59
59
|
const endTime = performance.now();
|
|
60
60
|
const startTime = this.activeTimers.get(name);
|
|
61
61
|
if (startTime === undefined) {
|
|
62
|
-
|
|
62
|
+
logger.warn(`Performance timer "${name}" was not started`);
|
|
63
63
|
return null;
|
|
64
64
|
}
|
|
65
65
|
const duration = endTime - startTime;
|
|
@@ -122,6 +122,9 @@ class PerformanceLogger {
|
|
|
122
122
|
this.metrics.length = 0;
|
|
123
123
|
this.activeTimers.clear();
|
|
124
124
|
}
|
|
125
|
+
reset() {
|
|
126
|
+
this.clear();
|
|
127
|
+
}
|
|
125
128
|
time(name, fn, metadata) {
|
|
126
129
|
if (!isLogEnabled)
|
|
127
130
|
return fn();
|
|
@@ -321,6 +324,8 @@ class LineBreak {
|
|
|
321
324
|
const filteredPoints = hyphenPoints.filter((pos) => pos >= lefthyphenmin && word.length - pos >= righthyphenmin);
|
|
322
325
|
return filteredPoints;
|
|
323
326
|
}
|
|
327
|
+
// Converts text into items (boxes, glues, penalties) for line breaking.
|
|
328
|
+
// The measureText function should return widths that include any letter spacing.
|
|
324
329
|
static itemizeText(text, measureText, // function to measure text width
|
|
325
330
|
hyphenate = false, language = 'en-us', availablePatterns, lefthyphenmin = DEFAULT_LEFT_HYPHEN_MIN, righthyphenmin = DEFAULT_RIGHT_HYPHEN_MIN, context) {
|
|
326
331
|
const items = [];
|
|
@@ -562,7 +567,7 @@ class LineBreak {
|
|
|
562
567
|
let useHyphenation = hyphenate;
|
|
563
568
|
if (useHyphenation &&
|
|
564
569
|
(!hyphenationPatterns || !hyphenationPatterns[language])) {
|
|
565
|
-
|
|
570
|
+
logger.warn(`Hyphenation patterns for ${language} not available`);
|
|
566
571
|
useHyphenation = false;
|
|
567
572
|
}
|
|
568
573
|
// Calculate initial emergency stretch (TeX default: 0)
|
|
@@ -1177,12 +1182,43 @@ class LineBreak {
|
|
|
1177
1182
|
}
|
|
1178
1183
|
}
|
|
1179
1184
|
|
|
1185
|
+
// Convert feature objects to HarfBuzz comma-separated format
|
|
1186
|
+
function convertFontFeaturesToString(features) {
|
|
1187
|
+
if (!features || Object.keys(features).length === 0) {
|
|
1188
|
+
return undefined;
|
|
1189
|
+
}
|
|
1190
|
+
const featureStrings = [];
|
|
1191
|
+
for (const [tag, value] of Object.entries(features)) {
|
|
1192
|
+
if (!/^[a-zA-Z0-9]{4}$/.test(tag)) {
|
|
1193
|
+
logger.warn(`Invalid OpenType feature tag: "${tag}". Tags must be exactly 4 alphanumeric characters.`);
|
|
1194
|
+
continue;
|
|
1195
|
+
}
|
|
1196
|
+
if (value === false || value === 0) {
|
|
1197
|
+
featureStrings.push(`${tag}=0`);
|
|
1198
|
+
}
|
|
1199
|
+
else if (value === true || value === 1) {
|
|
1200
|
+
featureStrings.push(tag);
|
|
1201
|
+
}
|
|
1202
|
+
else if (typeof value === 'number' && value > 1) {
|
|
1203
|
+
featureStrings.push(`${tag}=${Math.floor(value)}`);
|
|
1204
|
+
}
|
|
1205
|
+
else {
|
|
1206
|
+
logger.warn(`Invalid value for feature "${tag}": ${value}. Expected boolean or positive number.`);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
return featureStrings.length > 0 ? featureStrings.join(',') : undefined;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1180
1212
|
class TextMeasurer {
|
|
1213
|
+
// Measures text width including letter spacing
|
|
1214
|
+
// Letter spacing is added uniformly after each glyph during measurement,
|
|
1215
|
+
// so the widths given to the line-breaking algorithm already account for tracking
|
|
1181
1216
|
static measureTextWidth(loadedFont, text, letterSpacing = 0) {
|
|
1182
1217
|
const buffer = loadedFont.hb.createBuffer();
|
|
1183
1218
|
buffer.addText(text);
|
|
1184
1219
|
buffer.guessSegmentProperties();
|
|
1185
|
-
|
|
1220
|
+
const featuresString = convertFontFeaturesToString(loadedFont.fontFeatures);
|
|
1221
|
+
loadedFont.hb.shape(loadedFont.font, buffer, featuresString);
|
|
1186
1222
|
const glyphInfos = buffer.json(loadedFont.font);
|
|
1187
1223
|
const letterSpacingInFontUnits = letterSpacing * loadedFont.upem;
|
|
1188
1224
|
// Calculate total advance width with letter spacing
|
|
@@ -1206,6 +1242,8 @@ class TextLayout {
|
|
|
1206
1242
|
const { text, width, align, direction, hyphenate, language, respectExistingBreaks, tolerance, pretolerance, emergencyStretch, autoEmergencyStretch, hyphenationPatterns, lefthyphenmin, righthyphenmin, linepenalty, adjdemerits, hyphenpenalty, exhyphenpenalty, doublehyphendemerits, looseness, disableSingleWordDetection, letterSpacing } = options;
|
|
1207
1243
|
let lines;
|
|
1208
1244
|
if (width) {
|
|
1245
|
+
// Line breaking uses a measureText function that already includes letterSpacing,
|
|
1246
|
+
// so widths passed into LineBreak.breakText account for tracking
|
|
1209
1247
|
lines = LineBreak.breakText({
|
|
1210
1248
|
text,
|
|
1211
1249
|
width,
|
|
@@ -1229,7 +1267,8 @@ class TextLayout {
|
|
|
1229
1267
|
looseness,
|
|
1230
1268
|
disableSingleWordDetection,
|
|
1231
1269
|
unitsPerEm: this.loadedFont.upem,
|
|
1232
|
-
measureText: (textToMeasure) => TextMeasurer.measureTextWidth(this.loadedFont, textToMeasure, letterSpacing
|
|
1270
|
+
measureText: (textToMeasure) => TextMeasurer.measureTextWidth(this.loadedFont, textToMeasure, letterSpacing // Letter spacing included in width measurements
|
|
1271
|
+
)
|
|
1233
1272
|
});
|
|
1234
1273
|
}
|
|
1235
1274
|
else {
|
|
@@ -1283,6 +1322,17 @@ const FONT_SIGNATURE_OPEN_TYPE_CFF = 0x4f54544f; // 'OTTO'
|
|
|
1283
1322
|
const FONT_SIGNATURE_TRUE_TYPE_COLLECTION = 0x74746366; // 'ttcf'
|
|
1284
1323
|
const FONT_SIGNATURE_WOFF = 0x774f4646; // 'wOFF'
|
|
1285
1324
|
const FONT_SIGNATURE_WOFF2 = 0x774f4632; // 'wOF2'
|
|
1325
|
+
// Table Tags
|
|
1326
|
+
const TABLE_TAG_HEAD = 0x68656164; // 'head'
|
|
1327
|
+
const TABLE_TAG_HHEA = 0x68686561; // 'hhea'
|
|
1328
|
+
const TABLE_TAG_OS2 = 0x4f532f32; // 'OS/2'
|
|
1329
|
+
const TABLE_TAG_FVAR = 0x66766172; // 'fvar'
|
|
1330
|
+
const TABLE_TAG_STAT = 0x53544154; // 'STAT'
|
|
1331
|
+
const TABLE_TAG_NAME = 0x6e616d65; // 'name'
|
|
1332
|
+
const TABLE_TAG_CFF = 0x43464620; // 'CFF '
|
|
1333
|
+
const TABLE_TAG_CFF2 = 0x43464632; // 'CFF2'
|
|
1334
|
+
const TABLE_TAG_GSUB = 0x47535542; // 'GSUB'
|
|
1335
|
+
const TABLE_TAG_GPOS = 0x47504f53; // 'GPOS'
|
|
1286
1336
|
|
|
1287
1337
|
class FontMetadataExtractor {
|
|
1288
1338
|
static extractMetadata(fontBuffer) {
|
|
@@ -1299,7 +1349,6 @@ class FontMetadataExtractor {
|
|
|
1299
1349
|
if (!validSignatures.includes(sfntVersion)) {
|
|
1300
1350
|
throw new Error(`Invalid font format. Expected TrueType or OpenType, got signature: 0x${sfntVersion.toString(16)}`);
|
|
1301
1351
|
}
|
|
1302
|
-
const buffer = new Uint8Array(fontBuffer);
|
|
1303
1352
|
const numTables = view.getUint16(4); // OpenType header - number of tables is at offset 4
|
|
1304
1353
|
let isCFF = false;
|
|
1305
1354
|
let headTableOffset = 0;
|
|
@@ -1309,30 +1358,28 @@ class FontMetadataExtractor {
|
|
|
1309
1358
|
let nameTableOffset = 0;
|
|
1310
1359
|
let fvarTableOffset = 0;
|
|
1311
1360
|
for (let i = 0; i < numTables; i++) {
|
|
1312
|
-
const
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
}
|
|
1316
|
-
else if (tag === 'CFF2') {
|
|
1361
|
+
const offset = 12 + i * 16;
|
|
1362
|
+
const tag = view.getUint32(offset);
|
|
1363
|
+
if (tag === TABLE_TAG_CFF || tag === TABLE_TAG_CFF2) {
|
|
1317
1364
|
isCFF = true;
|
|
1318
1365
|
}
|
|
1319
|
-
if (tag ===
|
|
1320
|
-
headTableOffset = view.getUint32(
|
|
1366
|
+
else if (tag === TABLE_TAG_HEAD) {
|
|
1367
|
+
headTableOffset = view.getUint32(offset + 8);
|
|
1321
1368
|
}
|
|
1322
|
-
if (tag ===
|
|
1323
|
-
hheaTableOffset = view.getUint32(
|
|
1369
|
+
else if (tag === TABLE_TAG_HHEA) {
|
|
1370
|
+
hheaTableOffset = view.getUint32(offset + 8);
|
|
1324
1371
|
}
|
|
1325
|
-
if (tag ===
|
|
1326
|
-
os2TableOffset = view.getUint32(
|
|
1372
|
+
else if (tag === TABLE_TAG_OS2) {
|
|
1373
|
+
os2TableOffset = view.getUint32(offset + 8);
|
|
1327
1374
|
}
|
|
1328
|
-
if (tag ===
|
|
1329
|
-
fvarTableOffset = view.getUint32(
|
|
1375
|
+
else if (tag === TABLE_TAG_FVAR) {
|
|
1376
|
+
fvarTableOffset = view.getUint32(offset + 8);
|
|
1330
1377
|
}
|
|
1331
|
-
if (tag ===
|
|
1332
|
-
statTableOffset = view.getUint32(
|
|
1378
|
+
else if (tag === TABLE_TAG_STAT) {
|
|
1379
|
+
statTableOffset = view.getUint32(offset + 8);
|
|
1333
1380
|
}
|
|
1334
|
-
if (tag ===
|
|
1335
|
-
nameTableOffset = view.getUint32(
|
|
1381
|
+
else if (tag === TABLE_TAG_NAME) {
|
|
1382
|
+
nameTableOffset = view.getUint32(offset + 8);
|
|
1336
1383
|
}
|
|
1337
1384
|
}
|
|
1338
1385
|
const unitsPerEm = headTableOffset
|
|
@@ -1375,6 +1422,88 @@ class FontMetadataExtractor {
|
|
|
1375
1422
|
axisNames
|
|
1376
1423
|
};
|
|
1377
1424
|
}
|
|
1425
|
+
static extractFeatureTags(fontBuffer) {
|
|
1426
|
+
const view = new DataView(fontBuffer);
|
|
1427
|
+
const numTables = view.getUint16(4);
|
|
1428
|
+
let gsubTableOffset = 0;
|
|
1429
|
+
let gposTableOffset = 0;
|
|
1430
|
+
let nameTableOffset = 0;
|
|
1431
|
+
for (let i = 0; i < numTables; i++) {
|
|
1432
|
+
const offset = 12 + i * 16;
|
|
1433
|
+
const tag = view.getUint32(offset);
|
|
1434
|
+
if (tag === TABLE_TAG_GSUB) {
|
|
1435
|
+
gsubTableOffset = view.getUint32(offset + 8);
|
|
1436
|
+
}
|
|
1437
|
+
else if (tag === TABLE_TAG_GPOS) {
|
|
1438
|
+
gposTableOffset = view.getUint32(offset + 8);
|
|
1439
|
+
}
|
|
1440
|
+
else if (tag === TABLE_TAG_NAME) {
|
|
1441
|
+
nameTableOffset = view.getUint32(offset + 8);
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
const features = new Set();
|
|
1445
|
+
const featureNames = {};
|
|
1446
|
+
try {
|
|
1447
|
+
if (gsubTableOffset) {
|
|
1448
|
+
const gsubData = this.extractFeatureDataFromTable(view, gsubTableOffset, nameTableOffset);
|
|
1449
|
+
gsubData.features.forEach(f => features.add(f));
|
|
1450
|
+
Object.assign(featureNames, gsubData.names);
|
|
1451
|
+
}
|
|
1452
|
+
if (gposTableOffset) {
|
|
1453
|
+
const gposData = this.extractFeatureDataFromTable(view, gposTableOffset, nameTableOffset);
|
|
1454
|
+
gposData.features.forEach(f => features.add(f));
|
|
1455
|
+
Object.assign(featureNames, gposData.names);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
catch (e) {
|
|
1459
|
+
return undefined;
|
|
1460
|
+
}
|
|
1461
|
+
const featureArray = Array.from(features).sort();
|
|
1462
|
+
if (featureArray.length === 0)
|
|
1463
|
+
return undefined;
|
|
1464
|
+
return {
|
|
1465
|
+
tags: featureArray,
|
|
1466
|
+
names: Object.keys(featureNames).length > 0 ? featureNames : {}
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
static extractFeatureDataFromTable(view, tableOffset, nameTableOffset) {
|
|
1470
|
+
const featureListOffset = view.getUint16(tableOffset + 6);
|
|
1471
|
+
const featureListStart = tableOffset + featureListOffset;
|
|
1472
|
+
const featureCount = view.getUint16(featureListStart);
|
|
1473
|
+
const features = [];
|
|
1474
|
+
const names = {};
|
|
1475
|
+
for (let i = 0; i < featureCount; i++) {
|
|
1476
|
+
const recordOffset = featureListStart + 2 + i * 6;
|
|
1477
|
+
// Decode feature tag
|
|
1478
|
+
const tag = String.fromCharCode(view.getUint8(recordOffset), view.getUint8(recordOffset + 1), view.getUint8(recordOffset + 2), view.getUint8(recordOffset + 3));
|
|
1479
|
+
features.push(tag);
|
|
1480
|
+
// Extract feature name for stylistic sets and character variants
|
|
1481
|
+
if (/^(ss\d{2}|cv\d{2})$/.test(tag) && nameTableOffset) {
|
|
1482
|
+
const featureOffset = view.getUint16(recordOffset + 4);
|
|
1483
|
+
const featureTableStart = featureListStart + featureOffset;
|
|
1484
|
+
// Feature table structure:
|
|
1485
|
+
// uint16 FeatureParams offset
|
|
1486
|
+
// uint16 LookupCount
|
|
1487
|
+
// uint16[LookupCount] LookupListIndex
|
|
1488
|
+
const featureParamsOffset = view.getUint16(featureTableStart);
|
|
1489
|
+
// FeatureParams for ss features:
|
|
1490
|
+
// uint16 Version (should be 0)
|
|
1491
|
+
// uint16 UINameID
|
|
1492
|
+
if (featureParamsOffset !== 0) {
|
|
1493
|
+
const paramsStart = featureTableStart + featureParamsOffset;
|
|
1494
|
+
const version = view.getUint16(paramsStart);
|
|
1495
|
+
if (version === 0) {
|
|
1496
|
+
const nameID = view.getUint16(paramsStart + 2);
|
|
1497
|
+
const name = this.getNameFromNameTable(view, nameTableOffset, nameID);
|
|
1498
|
+
if (name) {
|
|
1499
|
+
names[tag] = name;
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
return { features, names };
|
|
1506
|
+
}
|
|
1378
1507
|
static extractAxisNames(view, statOffset, nameOffset) {
|
|
1379
1508
|
try {
|
|
1380
1509
|
// STAT table structure
|
|
@@ -1585,7 +1714,7 @@ class WoffConverter {
|
|
|
1585
1714
|
const padding = (4 - (table.origLength % 4)) % 4;
|
|
1586
1715
|
sfntOffset += padding;
|
|
1587
1716
|
}
|
|
1588
|
-
|
|
1717
|
+
logger.log('WOFF font decompressed successfully');
|
|
1589
1718
|
return sfntData.buffer.slice(0, sfntOffset);
|
|
1590
1719
|
}
|
|
1591
1720
|
static async decompressZlib(compressedData) {
|
|
@@ -1614,7 +1743,7 @@ class FontLoader {
|
|
|
1614
1743
|
// Check if this is a WOFF font and decompress if needed
|
|
1615
1744
|
const format = WoffConverter.detectFormat(fontBuffer);
|
|
1616
1745
|
if (format === 'woff') {
|
|
1617
|
-
|
|
1746
|
+
logger.log('WOFF font detected, decompressing...');
|
|
1618
1747
|
fontBuffer = await WoffConverter.decompressWoff(fontBuffer);
|
|
1619
1748
|
}
|
|
1620
1749
|
else if (format === 'woff2') {
|
|
@@ -1652,6 +1781,7 @@ class FontLoader {
|
|
|
1652
1781
|
};
|
|
1653
1782
|
}
|
|
1654
1783
|
}
|
|
1784
|
+
const featureData = FontMetadataExtractor.extractFeatureTags(fontBuffer);
|
|
1655
1785
|
return {
|
|
1656
1786
|
hb,
|
|
1657
1787
|
fontBlob,
|
|
@@ -1662,11 +1792,13 @@ class FontLoader {
|
|
|
1662
1792
|
metrics,
|
|
1663
1793
|
fontVariations,
|
|
1664
1794
|
isVariable,
|
|
1665
|
-
variationAxes
|
|
1795
|
+
variationAxes,
|
|
1796
|
+
availableFeatures: featureData?.tags,
|
|
1797
|
+
featureNames: featureData?.names
|
|
1666
1798
|
};
|
|
1667
1799
|
}
|
|
1668
1800
|
catch (error) {
|
|
1669
|
-
|
|
1801
|
+
logger.error('Failed to load font:', error);
|
|
1670
1802
|
throw error;
|
|
1671
1803
|
}
|
|
1672
1804
|
finally {
|
|
@@ -1687,7 +1819,7 @@ class FontLoader {
|
|
|
1687
1819
|
}
|
|
1688
1820
|
}
|
|
1689
1821
|
catch (error) {
|
|
1690
|
-
|
|
1822
|
+
logger.error('Error destroying font resources:', error);
|
|
1691
1823
|
}
|
|
1692
1824
|
}
|
|
1693
1825
|
}
|
|
@@ -2089,7 +2221,7 @@ class Tessellator {
|
|
|
2089
2221
|
if (valid.length === 0) {
|
|
2090
2222
|
return { triangles: { vertices: [], indices: [] }, contours: [] };
|
|
2091
2223
|
}
|
|
2092
|
-
|
|
2224
|
+
logger.log(`Tessellator: removeOverlaps=${removeOverlaps}, processing ${valid.length} paths`);
|
|
2093
2225
|
return this.tessellate(valid, removeOverlaps, isCFF);
|
|
2094
2226
|
}
|
|
2095
2227
|
tessellate(paths, removeOverlaps, isCFF) {
|
|
@@ -2099,27 +2231,35 @@ class Tessellator {
|
|
|
2099
2231
|
: paths;
|
|
2100
2232
|
let contours = this.pathsToContours(normalizedPaths);
|
|
2101
2233
|
if (removeOverlaps) {
|
|
2102
|
-
|
|
2234
|
+
logger.log('Two-pass: boundary extraction then triangulation');
|
|
2103
2235
|
// Extract boundaries to remove overlaps
|
|
2236
|
+
perfLogger.start('Tessellator.boundaryPass', {
|
|
2237
|
+
contourCount: contours.length
|
|
2238
|
+
});
|
|
2104
2239
|
const boundaryResult = this.performTessellation(contours, 'boundary');
|
|
2240
|
+
perfLogger.end('Tessellator.boundaryPass');
|
|
2105
2241
|
if (!boundaryResult) {
|
|
2106
|
-
|
|
2242
|
+
logger.warn('libtess returned empty result from boundary pass');
|
|
2107
2243
|
return { triangles: { vertices: [], indices: [] }, contours: [] };
|
|
2108
2244
|
}
|
|
2109
2245
|
// Convert boundary elements back to contours
|
|
2110
2246
|
contours = this.boundaryToContours(boundaryResult);
|
|
2111
|
-
|
|
2247
|
+
logger.log(`Boundary pass created ${contours.length} contours. Starting triangulation pass.`);
|
|
2112
2248
|
}
|
|
2113
2249
|
else {
|
|
2114
|
-
|
|
2250
|
+
logger.log(`Single-pass triangulation for ${isCFF ? 'CFF' : 'TTF'}`);
|
|
2115
2251
|
}
|
|
2116
2252
|
// Triangulate the contours
|
|
2253
|
+
perfLogger.start('Tessellator.triangulationPass', {
|
|
2254
|
+
contourCount: contours.length
|
|
2255
|
+
});
|
|
2117
2256
|
const triangleResult = this.performTessellation(contours, 'triangles');
|
|
2257
|
+
perfLogger.end('Tessellator.triangulationPass');
|
|
2118
2258
|
if (!triangleResult) {
|
|
2119
2259
|
const warning = removeOverlaps
|
|
2120
2260
|
? 'libtess returned empty result from triangulation pass'
|
|
2121
2261
|
: 'libtess returned empty result from single-pass triangulation';
|
|
2122
|
-
|
|
2262
|
+
logger.warn(warning);
|
|
2123
2263
|
return { triangles: { vertices: [], indices: [] }, contours };
|
|
2124
2264
|
}
|
|
2125
2265
|
return {
|
|
@@ -2174,7 +2314,7 @@ class Tessellator {
|
|
|
2174
2314
|
return idx;
|
|
2175
2315
|
});
|
|
2176
2316
|
tess.gluTessCallback(libtess_minExports.gluEnum.GLU_TESS_ERROR, (errno) => {
|
|
2177
|
-
|
|
2317
|
+
logger.warn(`libtess error: ${errno}`);
|
|
2178
2318
|
});
|
|
2179
2319
|
tess.gluTessNormal(0, 0, 1);
|
|
2180
2320
|
tess.gluTessBeginPolygon(null);
|
|
@@ -2301,10 +2441,16 @@ const OVERLAP_EPSILON = 1e-3;
|
|
|
2301
2441
|
class BoundaryClusterer {
|
|
2302
2442
|
constructor() { }
|
|
2303
2443
|
cluster(glyphContoursList, positions) {
|
|
2444
|
+
perfLogger.start('BoundaryClusterer.cluster', {
|
|
2445
|
+
glyphCount: glyphContoursList.length
|
|
2446
|
+
});
|
|
2304
2447
|
if (glyphContoursList.length === 0) {
|
|
2448
|
+
perfLogger.end('BoundaryClusterer.cluster');
|
|
2305
2449
|
return [];
|
|
2306
2450
|
}
|
|
2307
|
-
|
|
2451
|
+
const result = this.clusterSweepLine(glyphContoursList, positions);
|
|
2452
|
+
perfLogger.end('BoundaryClusterer.cluster');
|
|
2453
|
+
return result;
|
|
2308
2454
|
}
|
|
2309
2455
|
clusterSweepLine(glyphContoursList, positions) {
|
|
2310
2456
|
const n = glyphContoursList.length;
|
|
@@ -2945,6 +3091,11 @@ class GlyphContourCollector {
|
|
|
2945
3091
|
this.currentGlyphBounds.max.set(-Infinity, -Infinity);
|
|
2946
3092
|
// Record position for this glyph
|
|
2947
3093
|
this.glyphPositions.push(this.currentPosition.clone());
|
|
3094
|
+
// Time polygonization + path optimization per glyph
|
|
3095
|
+
perfLogger.start('Glyph.polygonizeAndOptimize', {
|
|
3096
|
+
glyphId,
|
|
3097
|
+
textIndex
|
|
3098
|
+
});
|
|
2948
3099
|
}
|
|
2949
3100
|
finishGlyph() {
|
|
2950
3101
|
if (this.currentPath) {
|
|
@@ -2968,6 +3119,8 @@ class GlyphContourCollector {
|
|
|
2968
3119
|
// Track textIndex separately
|
|
2969
3120
|
this.glyphTextIndices.push(this.currentTextIndex);
|
|
2970
3121
|
}
|
|
3122
|
+
// Stop timing for this glyph (even if it ended up empty)
|
|
3123
|
+
perfLogger.end('Glyph.polygonizeAndOptimize');
|
|
2971
3124
|
this.currentGlyphPaths = [];
|
|
2972
3125
|
}
|
|
2973
3126
|
onMoveTo(x, y) {
|
|
@@ -3196,12 +3349,190 @@ class DrawCallbackHandler {
|
|
|
3196
3349
|
}
|
|
3197
3350
|
}
|
|
3198
3351
|
catch (error) {
|
|
3199
|
-
|
|
3352
|
+
logger.warn('Error destroying draw callbacks:', error);
|
|
3200
3353
|
}
|
|
3201
3354
|
this.collector = undefined;
|
|
3202
3355
|
}
|
|
3203
3356
|
}
|
|
3204
3357
|
|
|
3358
|
+
// Generic LRU (Least Recently Used) cache with optional memory-based eviction
|
|
3359
|
+
class LRUCache {
|
|
3360
|
+
constructor(options = {}) {
|
|
3361
|
+
this.cache = new Map();
|
|
3362
|
+
this.head = null;
|
|
3363
|
+
this.tail = null;
|
|
3364
|
+
this.stats = {
|
|
3365
|
+
hits: 0,
|
|
3366
|
+
misses: 0,
|
|
3367
|
+
evictions: 0,
|
|
3368
|
+
size: 0,
|
|
3369
|
+
memoryUsage: 0
|
|
3370
|
+
};
|
|
3371
|
+
this.options = {
|
|
3372
|
+
maxEntries: options.maxEntries ?? Infinity,
|
|
3373
|
+
maxMemoryBytes: options.maxMemoryBytes ?? Infinity,
|
|
3374
|
+
calculateSize: options.calculateSize ?? (() => 0),
|
|
3375
|
+
onEvict: options.onEvict
|
|
3376
|
+
};
|
|
3377
|
+
}
|
|
3378
|
+
get(key) {
|
|
3379
|
+
const node = this.cache.get(key);
|
|
3380
|
+
if (node) {
|
|
3381
|
+
this.stats.hits++;
|
|
3382
|
+
this.moveToHead(node);
|
|
3383
|
+
return node.value;
|
|
3384
|
+
}
|
|
3385
|
+
else {
|
|
3386
|
+
this.stats.misses++;
|
|
3387
|
+
return undefined;
|
|
3388
|
+
}
|
|
3389
|
+
}
|
|
3390
|
+
has(key) {
|
|
3391
|
+
return this.cache.has(key);
|
|
3392
|
+
}
|
|
3393
|
+
set(key, value) {
|
|
3394
|
+
// If key already exists, update it
|
|
3395
|
+
const existingNode = this.cache.get(key);
|
|
3396
|
+
if (existingNode) {
|
|
3397
|
+
const oldSize = this.options.calculateSize(existingNode.value);
|
|
3398
|
+
const newSize = this.options.calculateSize(value);
|
|
3399
|
+
this.stats.memoryUsage = this.stats.memoryUsage - oldSize + newSize;
|
|
3400
|
+
existingNode.value = value;
|
|
3401
|
+
this.moveToHead(existingNode);
|
|
3402
|
+
return;
|
|
3403
|
+
}
|
|
3404
|
+
const size = this.options.calculateSize(value);
|
|
3405
|
+
// Evict entries if we exceed limits
|
|
3406
|
+
this.evictIfNeeded(size);
|
|
3407
|
+
// Create new node
|
|
3408
|
+
const node = {
|
|
3409
|
+
key,
|
|
3410
|
+
value,
|
|
3411
|
+
prev: null,
|
|
3412
|
+
next: null
|
|
3413
|
+
};
|
|
3414
|
+
this.cache.set(key, node);
|
|
3415
|
+
this.addToHead(node);
|
|
3416
|
+
this.stats.size = this.cache.size;
|
|
3417
|
+
this.stats.memoryUsage += size;
|
|
3418
|
+
}
|
|
3419
|
+
delete(key) {
|
|
3420
|
+
const node = this.cache.get(key);
|
|
3421
|
+
if (!node)
|
|
3422
|
+
return false;
|
|
3423
|
+
const size = this.options.calculateSize(node.value);
|
|
3424
|
+
this.removeNode(node);
|
|
3425
|
+
this.cache.delete(key);
|
|
3426
|
+
this.stats.size = this.cache.size;
|
|
3427
|
+
this.stats.memoryUsage -= size;
|
|
3428
|
+
if (this.options.onEvict) {
|
|
3429
|
+
this.options.onEvict(key, node.value);
|
|
3430
|
+
}
|
|
3431
|
+
return true;
|
|
3432
|
+
}
|
|
3433
|
+
clear() {
|
|
3434
|
+
if (this.options.onEvict) {
|
|
3435
|
+
for (const [key, node] of this.cache) {
|
|
3436
|
+
this.options.onEvict(key, node.value);
|
|
3437
|
+
}
|
|
3438
|
+
}
|
|
3439
|
+
this.cache.clear();
|
|
3440
|
+
this.head = null;
|
|
3441
|
+
this.tail = null;
|
|
3442
|
+
this.stats = {
|
|
3443
|
+
hits: 0,
|
|
3444
|
+
misses: 0,
|
|
3445
|
+
evictions: 0,
|
|
3446
|
+
size: 0,
|
|
3447
|
+
memoryUsage: 0
|
|
3448
|
+
};
|
|
3449
|
+
}
|
|
3450
|
+
getStats() {
|
|
3451
|
+
const total = this.stats.hits + this.stats.misses;
|
|
3452
|
+
const hitRate = total > 0 ? (this.stats.hits / total) * 100 : 0;
|
|
3453
|
+
const memoryUsageMB = this.stats.memoryUsage / (1024 * 1024);
|
|
3454
|
+
return {
|
|
3455
|
+
...this.stats,
|
|
3456
|
+
hitRate,
|
|
3457
|
+
memoryUsageMB
|
|
3458
|
+
};
|
|
3459
|
+
}
|
|
3460
|
+
keys() {
|
|
3461
|
+
const keys = [];
|
|
3462
|
+
let current = this.head;
|
|
3463
|
+
while (current) {
|
|
3464
|
+
keys.push(current.key);
|
|
3465
|
+
current = current.next;
|
|
3466
|
+
}
|
|
3467
|
+
return keys;
|
|
3468
|
+
}
|
|
3469
|
+
get size() {
|
|
3470
|
+
return this.cache.size;
|
|
3471
|
+
}
|
|
3472
|
+
evictIfNeeded(requiredSize) {
|
|
3473
|
+
// Evict by entry count
|
|
3474
|
+
while (this.cache.size >= this.options.maxEntries && this.tail) {
|
|
3475
|
+
this.evictTail();
|
|
3476
|
+
}
|
|
3477
|
+
// Evict by memory usage
|
|
3478
|
+
if (this.options.maxMemoryBytes < Infinity) {
|
|
3479
|
+
while (this.tail &&
|
|
3480
|
+
this.stats.memoryUsage + requiredSize > this.options.maxMemoryBytes) {
|
|
3481
|
+
this.evictTail();
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3484
|
+
}
|
|
3485
|
+
evictTail() {
|
|
3486
|
+
if (!this.tail)
|
|
3487
|
+
return;
|
|
3488
|
+
const nodeToRemove = this.tail;
|
|
3489
|
+
const size = this.options.calculateSize(nodeToRemove.value);
|
|
3490
|
+
this.removeTail();
|
|
3491
|
+
this.cache.delete(nodeToRemove.key);
|
|
3492
|
+
this.stats.size = this.cache.size;
|
|
3493
|
+
this.stats.memoryUsage -= size;
|
|
3494
|
+
this.stats.evictions++;
|
|
3495
|
+
if (this.options.onEvict) {
|
|
3496
|
+
this.options.onEvict(nodeToRemove.key, nodeToRemove.value);
|
|
3497
|
+
}
|
|
3498
|
+
}
|
|
3499
|
+
addToHead(node) {
|
|
3500
|
+
if (!this.head) {
|
|
3501
|
+
this.head = this.tail = node;
|
|
3502
|
+
}
|
|
3503
|
+
else {
|
|
3504
|
+
node.next = this.head;
|
|
3505
|
+
this.head.prev = node;
|
|
3506
|
+
this.head = node;
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
removeNode(node) {
|
|
3510
|
+
if (node.prev) {
|
|
3511
|
+
node.prev.next = node.next;
|
|
3512
|
+
}
|
|
3513
|
+
else {
|
|
3514
|
+
this.head = node.next;
|
|
3515
|
+
}
|
|
3516
|
+
if (node.next) {
|
|
3517
|
+
node.next.prev = node.prev;
|
|
3518
|
+
}
|
|
3519
|
+
else {
|
|
3520
|
+
this.tail = node.prev;
|
|
3521
|
+
}
|
|
3522
|
+
}
|
|
3523
|
+
removeTail() {
|
|
3524
|
+
if (this.tail) {
|
|
3525
|
+
this.removeNode(this.tail);
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
moveToHead(node) {
|
|
3529
|
+
if (node === this.head)
|
|
3530
|
+
return;
|
|
3531
|
+
this.removeNode(node);
|
|
3532
|
+
this.addToHead(node);
|
|
3533
|
+
}
|
|
3534
|
+
}
|
|
3535
|
+
|
|
3205
3536
|
class GlyphGeometryBuilder {
|
|
3206
3537
|
constructor(cache, loadedFont) {
|
|
3207
3538
|
this.fontId = 'default';
|
|
@@ -3214,6 +3545,16 @@ class GlyphGeometryBuilder {
|
|
|
3214
3545
|
this.collector = new GlyphContourCollector();
|
|
3215
3546
|
this.drawCallbacks = new DrawCallbackHandler();
|
|
3216
3547
|
this.drawCallbacks.createDrawFuncs(this.loadedFont, this.collector);
|
|
3548
|
+
this.contourCache = new LRUCache({
|
|
3549
|
+
maxEntries: 1000,
|
|
3550
|
+
calculateSize: (contours) => {
|
|
3551
|
+
let size = 0;
|
|
3552
|
+
for (const path of contours.paths) {
|
|
3553
|
+
size += path.points.length * 16; // Vec2 = 2 floats * 8 bytes
|
|
3554
|
+
}
|
|
3555
|
+
return size + 64; // bounds overhead
|
|
3556
|
+
}
|
|
3557
|
+
});
|
|
3217
3558
|
}
|
|
3218
3559
|
getOptimizationStats() {
|
|
3219
3560
|
return this.collector.getOptimizationStats();
|
|
@@ -3358,30 +3699,37 @@ class GlyphGeometryBuilder {
|
|
|
3358
3699
|
};
|
|
3359
3700
|
}
|
|
3360
3701
|
getContoursForGlyph(glyphId) {
|
|
3702
|
+
const cached = this.contourCache.get(glyphId);
|
|
3703
|
+
if (cached) {
|
|
3704
|
+
return cached;
|
|
3705
|
+
}
|
|
3361
3706
|
this.collector.reset();
|
|
3362
3707
|
this.collector.beginGlyph(glyphId, 0);
|
|
3363
3708
|
this.loadedFont.module.exports.hb_font_draw_glyph(this.loadedFont.font.ptr, glyphId, this.drawCallbacks.getDrawFuncsPtr(), 0);
|
|
3364
3709
|
this.collector.finishGlyph();
|
|
3365
3710
|
const collected = this.collector.getCollectedGlyphs()[0];
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
}
|
|
3377
|
-
return collected;
|
|
3711
|
+
const contours = collected || {
|
|
3712
|
+
glyphId,
|
|
3713
|
+
paths: [],
|
|
3714
|
+
bounds: {
|
|
3715
|
+
min: { x: 0, y: 0 },
|
|
3716
|
+
max: { x: 0, y: 0 }
|
|
3717
|
+
}
|
|
3718
|
+
};
|
|
3719
|
+
this.contourCache.set(glyphId, contours);
|
|
3720
|
+
return contours;
|
|
3378
3721
|
}
|
|
3379
3722
|
tessellateGlyphCluster(paths, depth, isCFF) {
|
|
3380
3723
|
const processedGeometry = this.tessellator.process(paths, true, isCFF);
|
|
3381
3724
|
return this.extrudeAndPackage(processedGeometry, depth);
|
|
3382
3725
|
}
|
|
3383
3726
|
extrudeAndPackage(processedGeometry, depth) {
|
|
3727
|
+
perfLogger.start('Extruder.extrude', {
|
|
3728
|
+
depth,
|
|
3729
|
+
upem: this.loadedFont.upem
|
|
3730
|
+
});
|
|
3384
3731
|
const extrudedResult = this.extruder.extrude(processedGeometry, depth, this.loadedFont.upem);
|
|
3732
|
+
perfLogger.end('Extruder.extrude');
|
|
3385
3733
|
// Compute bounding box from vertices
|
|
3386
3734
|
const vertices = extrudedResult.vertices;
|
|
3387
3735
|
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
|
@@ -3423,6 +3771,7 @@ class GlyphGeometryBuilder {
|
|
|
3423
3771
|
pathCount: glyphContours.paths.length
|
|
3424
3772
|
});
|
|
3425
3773
|
const processedGeometry = this.tessellator.process(glyphContours.paths, removeOverlaps, isCFF);
|
|
3774
|
+
perfLogger.end('GlyphGeometryBuilder.tessellateGlyph');
|
|
3426
3775
|
return this.extrudeAndPackage(processedGeometry, depth);
|
|
3427
3776
|
}
|
|
3428
3777
|
updatePlaneBounds(glyphBounds, planeBounds) {
|
|
@@ -3486,7 +3835,8 @@ class TextShaper {
|
|
|
3486
3835
|
}
|
|
3487
3836
|
buffer.addText(lineInfo.text);
|
|
3488
3837
|
buffer.guessSegmentProperties();
|
|
3489
|
-
|
|
3838
|
+
const featuresString = convertFontFeaturesToString(this.loadedFont.fontFeatures);
|
|
3839
|
+
this.loadedFont.hb.shape(this.loadedFont.font, buffer, featuresString);
|
|
3490
3840
|
const glyphInfos = buffer.json(this.loadedFont.font);
|
|
3491
3841
|
buffer.destroy();
|
|
3492
3842
|
const clusters = [];
|
|
@@ -3494,6 +3844,7 @@ class TextShaper {
|
|
|
3494
3844
|
let currentClusterText = '';
|
|
3495
3845
|
let clusterStartPosition = new Vec3();
|
|
3496
3846
|
let cursor = new Vec3(lineInfo.xOffset, -lineIndex * scaledLineHeight, 0);
|
|
3847
|
+
// Apply letter spacing between glyphs (must match what was used in width measurements)
|
|
3497
3848
|
const letterSpacingFU = letterSpacing * this.loadedFont.upem;
|
|
3498
3849
|
const spaceAdjustment = this.calculateSpaceAdjustment(lineInfo, align, letterSpacing);
|
|
3499
3850
|
for (let i = 0; i < glyphInfos.length; i++) {
|
|
@@ -3565,12 +3916,10 @@ class TextShaper {
|
|
|
3565
3916
|
const stretchFactor = SPACE_STRETCH_RATIO;
|
|
3566
3917
|
const shrinkFactor = SPACE_SHRINK_RATIO;
|
|
3567
3918
|
if (lineInfo.adjustmentRatio > 0) {
|
|
3568
|
-
spaceAdjustment =
|
|
3569
|
-
lineInfo.adjustmentRatio * width * stretchFactor;
|
|
3919
|
+
spaceAdjustment = lineInfo.adjustmentRatio * width * stretchFactor;
|
|
3570
3920
|
}
|
|
3571
3921
|
else if (lineInfo.adjustmentRatio < 0) {
|
|
3572
|
-
spaceAdjustment =
|
|
3573
|
-
lineInfo.adjustmentRatio * width * shrinkFactor;
|
|
3922
|
+
spaceAdjustment = lineInfo.adjustmentRatio * width * shrinkFactor;
|
|
3574
3923
|
}
|
|
3575
3924
|
}
|
|
3576
3925
|
return spaceAdjustment;
|
|
@@ -4495,12 +4844,16 @@ class Text {
|
|
|
4495
4844
|
const baseFontKey = typeof options.font === 'string'
|
|
4496
4845
|
? options.font
|
|
4497
4846
|
: `buffer-${Text.generateFontContentHash(options.font)}`;
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4847
|
+
let fontKey = baseFontKey;
|
|
4848
|
+
if (options.fontVariations) {
|
|
4849
|
+
fontKey += `_var_${JSON.stringify(options.fontVariations)}`;
|
|
4850
|
+
}
|
|
4851
|
+
if (options.fontFeatures) {
|
|
4852
|
+
fontKey += `_feat_${JSON.stringify(options.fontFeatures)}`;
|
|
4853
|
+
}
|
|
4501
4854
|
let loadedFont = Text.fontCache.get(fontKey);
|
|
4502
4855
|
if (!loadedFont) {
|
|
4503
|
-
loadedFont = await Text.loadAndCacheFont(fontKey, options.font, options.fontVariations);
|
|
4856
|
+
loadedFont = await Text.loadAndCacheFont(fontKey, options.font, options.fontVariations, options.fontFeatures);
|
|
4504
4857
|
}
|
|
4505
4858
|
const text = new Text({ maxCacheSizeMB: options.maxCacheSizeMB });
|
|
4506
4859
|
text.setLoadedFont(loadedFont);
|
|
@@ -4514,12 +4867,11 @@ class Text {
|
|
|
4514
4867
|
measureTextWidth: (textString, letterSpacing) => text.measureTextWidth(textString, letterSpacing)
|
|
4515
4868
|
};
|
|
4516
4869
|
}
|
|
4517
|
-
static async loadAndCacheFont(fontKey, font, fontVariations) {
|
|
4870
|
+
static async loadAndCacheFont(fontKey, font, fontVariations, fontFeatures) {
|
|
4518
4871
|
const tempText = new Text();
|
|
4519
|
-
await tempText.loadFont(font, fontVariations);
|
|
4872
|
+
await tempText.loadFont(font, fontVariations, fontFeatures);
|
|
4520
4873
|
const loadedFont = tempText.getLoadedFont();
|
|
4521
4874
|
Text.fontCache.set(fontKey, loadedFont);
|
|
4522
|
-
// Don't destroy tempText - the cached font references its HarfBuzz objects
|
|
4523
4875
|
return loadedFont;
|
|
4524
4876
|
}
|
|
4525
4877
|
static generateFontContentHash(buffer) {
|
|
@@ -4538,10 +4890,13 @@ class Text {
|
|
|
4538
4890
|
const contentHash = Text.generateFontContentHash(loadedFont._buffer);
|
|
4539
4891
|
this.currentFontId = `font_${contentHash}`;
|
|
4540
4892
|
if (loadedFont.fontVariations) {
|
|
4541
|
-
this.currentFontId += `
|
|
4893
|
+
this.currentFontId += `_var_${JSON.stringify(loadedFont.fontVariations)}`;
|
|
4894
|
+
}
|
|
4895
|
+
if (loadedFont.fontFeatures) {
|
|
4896
|
+
this.currentFontId += `_feat_${JSON.stringify(loadedFont.fontFeatures)}`;
|
|
4542
4897
|
}
|
|
4543
4898
|
}
|
|
4544
|
-
async loadFont(fontSrc, fontVariations) {
|
|
4899
|
+
async loadFont(fontSrc, fontVariations, fontFeatures) {
|
|
4545
4900
|
perfLogger.start('Text.loadFont', {
|
|
4546
4901
|
fontSrc: typeof fontSrc === 'string' ? fontSrc : `buffer(${fontSrc.byteLength})`
|
|
4547
4902
|
});
|
|
@@ -4562,14 +4917,20 @@ class Text {
|
|
|
4562
4917
|
this.destroy();
|
|
4563
4918
|
}
|
|
4564
4919
|
this.loadedFont = await this.fontLoader.loadFont(fontBuffer, fontVariations);
|
|
4920
|
+
if (fontFeatures) {
|
|
4921
|
+
this.loadedFont.fontFeatures = fontFeatures;
|
|
4922
|
+
}
|
|
4565
4923
|
const contentHash = Text.generateFontContentHash(fontBuffer);
|
|
4566
4924
|
this.currentFontId = `font_${contentHash}`;
|
|
4567
4925
|
if (fontVariations) {
|
|
4568
|
-
this.currentFontId += `
|
|
4926
|
+
this.currentFontId += `_var_${JSON.stringify(fontVariations)}`;
|
|
4927
|
+
}
|
|
4928
|
+
if (fontFeatures) {
|
|
4929
|
+
this.currentFontId += `_feat_${JSON.stringify(fontFeatures)}`;
|
|
4569
4930
|
}
|
|
4570
4931
|
}
|
|
4571
4932
|
catch (error) {
|
|
4572
|
-
|
|
4933
|
+
logger.error('Failed to load font:', error);
|
|
4573
4934
|
throw error;
|
|
4574
4935
|
}
|
|
4575
4936
|
finally {
|
|
@@ -4644,7 +5005,7 @@ class Text {
|
|
|
4644
5005
|
};
|
|
4645
5006
|
}
|
|
4646
5007
|
catch (error) {
|
|
4647
|
-
|
|
5008
|
+
logger.warn(`Failed to load patterns for ${language}: ${error}`);
|
|
4648
5009
|
return {
|
|
4649
5010
|
...options,
|
|
4650
5011
|
layout: {
|
|
@@ -4908,7 +5269,7 @@ class Text {
|
|
|
4908
5269
|
Text.patternCache.set(language, pattern);
|
|
4909
5270
|
}
|
|
4910
5271
|
catch (error) {
|
|
4911
|
-
|
|
5272
|
+
logger.warn(`Failed to pre-load patterns for ${language}: ${error}`);
|
|
4912
5273
|
}
|
|
4913
5274
|
}
|
|
4914
5275
|
}));
|
|
@@ -4970,7 +5331,7 @@ class Text {
|
|
|
4970
5331
|
FontLoader.destroyFont(currentFont);
|
|
4971
5332
|
}
|
|
4972
5333
|
catch (error) {
|
|
4973
|
-
|
|
5334
|
+
logger.warn('Error destroying HarfBuzz objects:', error);
|
|
4974
5335
|
}
|
|
4975
5336
|
finally {
|
|
4976
5337
|
this.loadedFont = undefined;
|