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