three-text 0.2.5 → 0.2.6
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 +46 -23
- package/dist/index.cjs +188 -56
- package/dist/index.d.ts +18 -1
- package/dist/index.js +188 -56
- package/dist/index.min.cjs +2 -2
- package/dist/index.min.js +2 -2
- package/dist/index.umd.js +188 -56
- 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/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/{DebugLogger.d.ts → Logger.d.ts} +2 -2
- package/package.json +1 -1
package/dist/index.umd.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* three-text v0.2.
|
|
2
|
+
* three-text v0.2.6
|
|
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;
|
|
@@ -567,7 +567,7 @@
|
|
|
567
567
|
let useHyphenation = hyphenate;
|
|
568
568
|
if (useHyphenation &&
|
|
569
569
|
(!hyphenationPatterns || !hyphenationPatterns[language])) {
|
|
570
|
-
|
|
570
|
+
logger.warn(`Hyphenation patterns for ${language} not available`);
|
|
571
571
|
useHyphenation = false;
|
|
572
572
|
}
|
|
573
573
|
// Calculate initial emergency stretch (TeX default: 0)
|
|
@@ -1182,12 +1182,40 @@
|
|
|
1182
1182
|
}
|
|
1183
1183
|
}
|
|
1184
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
|
+
|
|
1185
1212
|
class TextMeasurer {
|
|
1186
1213
|
static measureTextWidth(loadedFont, text, letterSpacing = 0) {
|
|
1187
1214
|
const buffer = loadedFont.hb.createBuffer();
|
|
1188
1215
|
buffer.addText(text);
|
|
1189
1216
|
buffer.guessSegmentProperties();
|
|
1190
|
-
|
|
1217
|
+
const featuresString = convertFontFeaturesToString(loadedFont.fontFeatures);
|
|
1218
|
+
loadedFont.hb.shape(loadedFont.font, buffer, featuresString);
|
|
1191
1219
|
const glyphInfos = buffer.json(loadedFont.font);
|
|
1192
1220
|
const letterSpacingInFontUnits = letterSpacing * loadedFont.upem;
|
|
1193
1221
|
// Calculate total advance width with letter spacing
|
|
@@ -1288,6 +1316,17 @@
|
|
|
1288
1316
|
const FONT_SIGNATURE_TRUE_TYPE_COLLECTION = 0x74746366; // 'ttcf'
|
|
1289
1317
|
const FONT_SIGNATURE_WOFF = 0x774f4646; // 'wOFF'
|
|
1290
1318
|
const FONT_SIGNATURE_WOFF2 = 0x774f4632; // 'wOF2'
|
|
1319
|
+
// Table Tags
|
|
1320
|
+
const TABLE_TAG_HEAD = 0x68656164; // 'head'
|
|
1321
|
+
const TABLE_TAG_HHEA = 0x68686561; // 'hhea'
|
|
1322
|
+
const TABLE_TAG_OS2 = 0x4f532f32; // 'OS/2'
|
|
1323
|
+
const TABLE_TAG_FVAR = 0x66766172; // 'fvar'
|
|
1324
|
+
const TABLE_TAG_STAT = 0x53544154; // 'STAT'
|
|
1325
|
+
const TABLE_TAG_NAME = 0x6e616d65; // 'name'
|
|
1326
|
+
const TABLE_TAG_CFF = 0x43464620; // 'CFF '
|
|
1327
|
+
const TABLE_TAG_CFF2 = 0x43464632; // 'CFF2'
|
|
1328
|
+
const TABLE_TAG_GSUB = 0x47535542; // 'GSUB'
|
|
1329
|
+
const TABLE_TAG_GPOS = 0x47504f53; // 'GPOS'
|
|
1291
1330
|
|
|
1292
1331
|
class FontMetadataExtractor {
|
|
1293
1332
|
static extractMetadata(fontBuffer) {
|
|
@@ -1304,7 +1343,6 @@
|
|
|
1304
1343
|
if (!validSignatures.includes(sfntVersion)) {
|
|
1305
1344
|
throw new Error(`Invalid font format. Expected TrueType or OpenType, got signature: 0x${sfntVersion.toString(16)}`);
|
|
1306
1345
|
}
|
|
1307
|
-
const buffer = new Uint8Array(fontBuffer);
|
|
1308
1346
|
const numTables = view.getUint16(4); // OpenType header - number of tables is at offset 4
|
|
1309
1347
|
let isCFF = false;
|
|
1310
1348
|
let headTableOffset = 0;
|
|
@@ -1314,30 +1352,28 @@
|
|
|
1314
1352
|
let nameTableOffset = 0;
|
|
1315
1353
|
let fvarTableOffset = 0;
|
|
1316
1354
|
for (let i = 0; i < numTables; i++) {
|
|
1317
|
-
const
|
|
1318
|
-
|
|
1355
|
+
const offset = 12 + i * 16;
|
|
1356
|
+
const tag = view.getUint32(offset);
|
|
1357
|
+
if (tag === TABLE_TAG_CFF || tag === TABLE_TAG_CFF2) {
|
|
1319
1358
|
isCFF = true;
|
|
1320
1359
|
}
|
|
1321
|
-
else if (tag ===
|
|
1322
|
-
|
|
1360
|
+
else if (tag === TABLE_TAG_HEAD) {
|
|
1361
|
+
headTableOffset = view.getUint32(offset + 8);
|
|
1323
1362
|
}
|
|
1324
|
-
if (tag ===
|
|
1325
|
-
|
|
1363
|
+
else if (tag === TABLE_TAG_HHEA) {
|
|
1364
|
+
hheaTableOffset = view.getUint32(offset + 8);
|
|
1326
1365
|
}
|
|
1327
|
-
if (tag ===
|
|
1328
|
-
|
|
1366
|
+
else if (tag === TABLE_TAG_OS2) {
|
|
1367
|
+
os2TableOffset = view.getUint32(offset + 8);
|
|
1329
1368
|
}
|
|
1330
|
-
if (tag ===
|
|
1331
|
-
|
|
1369
|
+
else if (tag === TABLE_TAG_FVAR) {
|
|
1370
|
+
fvarTableOffset = view.getUint32(offset + 8);
|
|
1332
1371
|
}
|
|
1333
|
-
if (tag ===
|
|
1334
|
-
|
|
1372
|
+
else if (tag === TABLE_TAG_STAT) {
|
|
1373
|
+
statTableOffset = view.getUint32(offset + 8);
|
|
1335
1374
|
}
|
|
1336
|
-
if (tag ===
|
|
1337
|
-
|
|
1338
|
-
}
|
|
1339
|
-
if (tag === 'name') {
|
|
1340
|
-
nameTableOffset = view.getUint32(12 + i * 16 + 8);
|
|
1375
|
+
else if (tag === TABLE_TAG_NAME) {
|
|
1376
|
+
nameTableOffset = view.getUint32(offset + 8);
|
|
1341
1377
|
}
|
|
1342
1378
|
}
|
|
1343
1379
|
const unitsPerEm = headTableOffset
|
|
@@ -1380,6 +1416,88 @@
|
|
|
1380
1416
|
axisNames
|
|
1381
1417
|
};
|
|
1382
1418
|
}
|
|
1419
|
+
static extractFeatureTags(fontBuffer) {
|
|
1420
|
+
const view = new DataView(fontBuffer);
|
|
1421
|
+
const numTables = view.getUint16(4);
|
|
1422
|
+
let gsubTableOffset = 0;
|
|
1423
|
+
let gposTableOffset = 0;
|
|
1424
|
+
let nameTableOffset = 0;
|
|
1425
|
+
for (let i = 0; i < numTables; i++) {
|
|
1426
|
+
const offset = 12 + i * 16;
|
|
1427
|
+
const tag = view.getUint32(offset);
|
|
1428
|
+
if (tag === TABLE_TAG_GSUB) {
|
|
1429
|
+
gsubTableOffset = view.getUint32(offset + 8);
|
|
1430
|
+
}
|
|
1431
|
+
else if (tag === TABLE_TAG_GPOS) {
|
|
1432
|
+
gposTableOffset = view.getUint32(offset + 8);
|
|
1433
|
+
}
|
|
1434
|
+
else if (tag === TABLE_TAG_NAME) {
|
|
1435
|
+
nameTableOffset = view.getUint32(offset + 8);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
const features = new Set();
|
|
1439
|
+
const featureNames = {};
|
|
1440
|
+
try {
|
|
1441
|
+
if (gsubTableOffset) {
|
|
1442
|
+
const gsubData = this.extractFeatureDataFromTable(view, gsubTableOffset, nameTableOffset);
|
|
1443
|
+
gsubData.features.forEach(f => features.add(f));
|
|
1444
|
+
Object.assign(featureNames, gsubData.names);
|
|
1445
|
+
}
|
|
1446
|
+
if (gposTableOffset) {
|
|
1447
|
+
const gposData = this.extractFeatureDataFromTable(view, gposTableOffset, nameTableOffset);
|
|
1448
|
+
gposData.features.forEach(f => features.add(f));
|
|
1449
|
+
Object.assign(featureNames, gposData.names);
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
catch (e) {
|
|
1453
|
+
return undefined;
|
|
1454
|
+
}
|
|
1455
|
+
const featureArray = Array.from(features).sort();
|
|
1456
|
+
if (featureArray.length === 0)
|
|
1457
|
+
return undefined;
|
|
1458
|
+
return {
|
|
1459
|
+
tags: featureArray,
|
|
1460
|
+
names: Object.keys(featureNames).length > 0 ? featureNames : {}
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1463
|
+
static extractFeatureDataFromTable(view, tableOffset, nameTableOffset) {
|
|
1464
|
+
const featureListOffset = view.getUint16(tableOffset + 6);
|
|
1465
|
+
const featureListStart = tableOffset + featureListOffset;
|
|
1466
|
+
const featureCount = view.getUint16(featureListStart);
|
|
1467
|
+
const features = [];
|
|
1468
|
+
const names = {};
|
|
1469
|
+
for (let i = 0; i < featureCount; i++) {
|
|
1470
|
+
const recordOffset = featureListStart + 2 + i * 6;
|
|
1471
|
+
// Decode feature tag
|
|
1472
|
+
const tag = String.fromCharCode(view.getUint8(recordOffset), view.getUint8(recordOffset + 1), view.getUint8(recordOffset + 2), view.getUint8(recordOffset + 3));
|
|
1473
|
+
features.push(tag);
|
|
1474
|
+
// Extract feature name for stylistic sets and character variants
|
|
1475
|
+
if (/^(ss\d{2}|cv\d{2})$/.test(tag) && nameTableOffset) {
|
|
1476
|
+
const featureOffset = view.getUint16(recordOffset + 4);
|
|
1477
|
+
const featureTableStart = featureListStart + featureOffset;
|
|
1478
|
+
// Feature table structure:
|
|
1479
|
+
// uint16 FeatureParams offset
|
|
1480
|
+
// uint16 LookupCount
|
|
1481
|
+
// uint16[LookupCount] LookupListIndex
|
|
1482
|
+
const featureParamsOffset = view.getUint16(featureTableStart);
|
|
1483
|
+
// FeatureParams for ss features:
|
|
1484
|
+
// uint16 Version (should be 0)
|
|
1485
|
+
// uint16 UINameID
|
|
1486
|
+
if (featureParamsOffset !== 0) {
|
|
1487
|
+
const paramsStart = featureTableStart + featureParamsOffset;
|
|
1488
|
+
const version = view.getUint16(paramsStart);
|
|
1489
|
+
if (version === 0) {
|
|
1490
|
+
const nameID = view.getUint16(paramsStart + 2);
|
|
1491
|
+
const name = this.getNameFromNameTable(view, nameTableOffset, nameID);
|
|
1492
|
+
if (name) {
|
|
1493
|
+
names[tag] = name;
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
return { features, names };
|
|
1500
|
+
}
|
|
1383
1501
|
static extractAxisNames(view, statOffset, nameOffset) {
|
|
1384
1502
|
try {
|
|
1385
1503
|
// STAT table structure
|
|
@@ -1590,7 +1708,7 @@
|
|
|
1590
1708
|
const padding = (4 - (table.origLength % 4)) % 4;
|
|
1591
1709
|
sfntOffset += padding;
|
|
1592
1710
|
}
|
|
1593
|
-
|
|
1711
|
+
logger.log('WOFF font decompressed successfully');
|
|
1594
1712
|
return sfntData.buffer.slice(0, sfntOffset);
|
|
1595
1713
|
}
|
|
1596
1714
|
static async decompressZlib(compressedData) {
|
|
@@ -1619,7 +1737,7 @@
|
|
|
1619
1737
|
// Check if this is a WOFF font and decompress if needed
|
|
1620
1738
|
const format = WoffConverter.detectFormat(fontBuffer);
|
|
1621
1739
|
if (format === 'woff') {
|
|
1622
|
-
|
|
1740
|
+
logger.log('WOFF font detected, decompressing...');
|
|
1623
1741
|
fontBuffer = await WoffConverter.decompressWoff(fontBuffer);
|
|
1624
1742
|
}
|
|
1625
1743
|
else if (format === 'woff2') {
|
|
@@ -1657,6 +1775,7 @@
|
|
|
1657
1775
|
};
|
|
1658
1776
|
}
|
|
1659
1777
|
}
|
|
1778
|
+
const featureData = FontMetadataExtractor.extractFeatureTags(fontBuffer);
|
|
1660
1779
|
return {
|
|
1661
1780
|
hb,
|
|
1662
1781
|
fontBlob,
|
|
@@ -1667,11 +1786,13 @@
|
|
|
1667
1786
|
metrics,
|
|
1668
1787
|
fontVariations,
|
|
1669
1788
|
isVariable,
|
|
1670
|
-
variationAxes
|
|
1789
|
+
variationAxes,
|
|
1790
|
+
availableFeatures: featureData?.tags,
|
|
1791
|
+
featureNames: featureData?.names
|
|
1671
1792
|
};
|
|
1672
1793
|
}
|
|
1673
1794
|
catch (error) {
|
|
1674
|
-
|
|
1795
|
+
logger.error('Failed to load font:', error);
|
|
1675
1796
|
throw error;
|
|
1676
1797
|
}
|
|
1677
1798
|
finally {
|
|
@@ -1692,7 +1813,7 @@
|
|
|
1692
1813
|
}
|
|
1693
1814
|
}
|
|
1694
1815
|
catch (error) {
|
|
1695
|
-
|
|
1816
|
+
logger.error('Error destroying font resources:', error);
|
|
1696
1817
|
}
|
|
1697
1818
|
}
|
|
1698
1819
|
}
|
|
@@ -2096,7 +2217,7 @@
|
|
|
2096
2217
|
if (valid.length === 0) {
|
|
2097
2218
|
return { triangles: { vertices: [], indices: [] }, contours: [] };
|
|
2098
2219
|
}
|
|
2099
|
-
|
|
2220
|
+
logger.log(`Tessellator: removeOverlaps=${removeOverlaps}, processing ${valid.length} paths`);
|
|
2100
2221
|
return this.tessellate(valid, removeOverlaps, isCFF);
|
|
2101
2222
|
}
|
|
2102
2223
|
tessellate(paths, removeOverlaps, isCFF) {
|
|
@@ -2106,19 +2227,19 @@
|
|
|
2106
2227
|
: paths;
|
|
2107
2228
|
let contours = this.pathsToContours(normalizedPaths);
|
|
2108
2229
|
if (removeOverlaps) {
|
|
2109
|
-
|
|
2230
|
+
logger.log('Two-pass: boundary extraction then triangulation');
|
|
2110
2231
|
// Extract boundaries to remove overlaps
|
|
2111
2232
|
const boundaryResult = this.performTessellation(contours, 'boundary');
|
|
2112
2233
|
if (!boundaryResult) {
|
|
2113
|
-
|
|
2234
|
+
logger.warn('libtess returned empty result from boundary pass');
|
|
2114
2235
|
return { triangles: { vertices: [], indices: [] }, contours: [] };
|
|
2115
2236
|
}
|
|
2116
2237
|
// Convert boundary elements back to contours
|
|
2117
2238
|
contours = this.boundaryToContours(boundaryResult);
|
|
2118
|
-
|
|
2239
|
+
logger.log(`Boundary pass created ${contours.length} contours. Starting triangulation pass.`);
|
|
2119
2240
|
}
|
|
2120
2241
|
else {
|
|
2121
|
-
|
|
2242
|
+
logger.log(`Single-pass triangulation for ${isCFF ? 'CFF' : 'TTF'}`);
|
|
2122
2243
|
}
|
|
2123
2244
|
// Triangulate the contours
|
|
2124
2245
|
const triangleResult = this.performTessellation(contours, 'triangles');
|
|
@@ -2126,7 +2247,7 @@
|
|
|
2126
2247
|
const warning = removeOverlaps
|
|
2127
2248
|
? 'libtess returned empty result from triangulation pass'
|
|
2128
2249
|
: 'libtess returned empty result from single-pass triangulation';
|
|
2129
|
-
|
|
2250
|
+
logger.warn(warning);
|
|
2130
2251
|
return { triangles: { vertices: [], indices: [] }, contours };
|
|
2131
2252
|
}
|
|
2132
2253
|
return {
|
|
@@ -2181,7 +2302,7 @@
|
|
|
2181
2302
|
return idx;
|
|
2182
2303
|
});
|
|
2183
2304
|
tess.gluTessCallback(libtess_minExports.gluEnum.GLU_TESS_ERROR, (errno) => {
|
|
2184
|
-
|
|
2305
|
+
logger.warn(`libtess error: ${errno}`);
|
|
2185
2306
|
});
|
|
2186
2307
|
tess.gluTessNormal(0, 0, 1);
|
|
2187
2308
|
tess.gluTessBeginPolygon(null);
|
|
@@ -3203,7 +3324,7 @@
|
|
|
3203
3324
|
}
|
|
3204
3325
|
}
|
|
3205
3326
|
catch (error) {
|
|
3206
|
-
|
|
3327
|
+
logger.warn('Error destroying draw callbacks:', error);
|
|
3207
3328
|
}
|
|
3208
3329
|
this.collector = undefined;
|
|
3209
3330
|
}
|
|
@@ -3493,7 +3614,8 @@
|
|
|
3493
3614
|
}
|
|
3494
3615
|
buffer.addText(lineInfo.text);
|
|
3495
3616
|
buffer.guessSegmentProperties();
|
|
3496
|
-
|
|
3617
|
+
const featuresString = convertFontFeaturesToString(this.loadedFont.fontFeatures);
|
|
3618
|
+
this.loadedFont.hb.shape(this.loadedFont.font, buffer, featuresString);
|
|
3497
3619
|
const glyphInfos = buffer.json(this.loadedFont.font);
|
|
3498
3620
|
buffer.destroy();
|
|
3499
3621
|
const clusters = [];
|
|
@@ -3572,12 +3694,10 @@
|
|
|
3572
3694
|
const stretchFactor = SPACE_STRETCH_RATIO;
|
|
3573
3695
|
const shrinkFactor = SPACE_SHRINK_RATIO;
|
|
3574
3696
|
if (lineInfo.adjustmentRatio > 0) {
|
|
3575
|
-
spaceAdjustment =
|
|
3576
|
-
lineInfo.adjustmentRatio * width * stretchFactor;
|
|
3697
|
+
spaceAdjustment = lineInfo.adjustmentRatio * width * stretchFactor;
|
|
3577
3698
|
}
|
|
3578
3699
|
else if (lineInfo.adjustmentRatio < 0) {
|
|
3579
|
-
spaceAdjustment =
|
|
3580
|
-
lineInfo.adjustmentRatio * width * shrinkFactor;
|
|
3700
|
+
spaceAdjustment = lineInfo.adjustmentRatio * width * shrinkFactor;
|
|
3581
3701
|
}
|
|
3582
3702
|
}
|
|
3583
3703
|
return spaceAdjustment;
|
|
@@ -4502,12 +4622,16 @@
|
|
|
4502
4622
|
const baseFontKey = typeof options.font === 'string'
|
|
4503
4623
|
? options.font
|
|
4504
4624
|
: `buffer-${Text.generateFontContentHash(options.font)}`;
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4625
|
+
let fontKey = baseFontKey;
|
|
4626
|
+
if (options.fontVariations) {
|
|
4627
|
+
fontKey += `_var_${JSON.stringify(options.fontVariations)}`;
|
|
4628
|
+
}
|
|
4629
|
+
if (options.fontFeatures) {
|
|
4630
|
+
fontKey += `_feat_${JSON.stringify(options.fontFeatures)}`;
|
|
4631
|
+
}
|
|
4508
4632
|
let loadedFont = Text.fontCache.get(fontKey);
|
|
4509
4633
|
if (!loadedFont) {
|
|
4510
|
-
loadedFont = await Text.loadAndCacheFont(fontKey, options.font, options.fontVariations);
|
|
4634
|
+
loadedFont = await Text.loadAndCacheFont(fontKey, options.font, options.fontVariations, options.fontFeatures);
|
|
4511
4635
|
}
|
|
4512
4636
|
const text = new Text({ maxCacheSizeMB: options.maxCacheSizeMB });
|
|
4513
4637
|
text.setLoadedFont(loadedFont);
|
|
@@ -4521,12 +4645,11 @@
|
|
|
4521
4645
|
measureTextWidth: (textString, letterSpacing) => text.measureTextWidth(textString, letterSpacing)
|
|
4522
4646
|
};
|
|
4523
4647
|
}
|
|
4524
|
-
static async loadAndCacheFont(fontKey, font, fontVariations) {
|
|
4648
|
+
static async loadAndCacheFont(fontKey, font, fontVariations, fontFeatures) {
|
|
4525
4649
|
const tempText = new Text();
|
|
4526
|
-
await tempText.loadFont(font, fontVariations);
|
|
4650
|
+
await tempText.loadFont(font, fontVariations, fontFeatures);
|
|
4527
4651
|
const loadedFont = tempText.getLoadedFont();
|
|
4528
4652
|
Text.fontCache.set(fontKey, loadedFont);
|
|
4529
|
-
// Don't destroy tempText - the cached font references its HarfBuzz objects
|
|
4530
4653
|
return loadedFont;
|
|
4531
4654
|
}
|
|
4532
4655
|
static generateFontContentHash(buffer) {
|
|
@@ -4545,10 +4668,13 @@
|
|
|
4545
4668
|
const contentHash = Text.generateFontContentHash(loadedFont._buffer);
|
|
4546
4669
|
this.currentFontId = `font_${contentHash}`;
|
|
4547
4670
|
if (loadedFont.fontVariations) {
|
|
4548
|
-
this.currentFontId += `
|
|
4671
|
+
this.currentFontId += `_var_${JSON.stringify(loadedFont.fontVariations)}`;
|
|
4672
|
+
}
|
|
4673
|
+
if (loadedFont.fontFeatures) {
|
|
4674
|
+
this.currentFontId += `_feat_${JSON.stringify(loadedFont.fontFeatures)}`;
|
|
4549
4675
|
}
|
|
4550
4676
|
}
|
|
4551
|
-
async loadFont(fontSrc, fontVariations) {
|
|
4677
|
+
async loadFont(fontSrc, fontVariations, fontFeatures) {
|
|
4552
4678
|
perfLogger.start('Text.loadFont', {
|
|
4553
4679
|
fontSrc: typeof fontSrc === 'string' ? fontSrc : `buffer(${fontSrc.byteLength})`
|
|
4554
4680
|
});
|
|
@@ -4569,14 +4695,20 @@
|
|
|
4569
4695
|
this.destroy();
|
|
4570
4696
|
}
|
|
4571
4697
|
this.loadedFont = await this.fontLoader.loadFont(fontBuffer, fontVariations);
|
|
4698
|
+
if (fontFeatures) {
|
|
4699
|
+
this.loadedFont.fontFeatures = fontFeatures;
|
|
4700
|
+
}
|
|
4572
4701
|
const contentHash = Text.generateFontContentHash(fontBuffer);
|
|
4573
4702
|
this.currentFontId = `font_${contentHash}`;
|
|
4574
4703
|
if (fontVariations) {
|
|
4575
|
-
this.currentFontId += `
|
|
4704
|
+
this.currentFontId += `_var_${JSON.stringify(fontVariations)}`;
|
|
4705
|
+
}
|
|
4706
|
+
if (fontFeatures) {
|
|
4707
|
+
this.currentFontId += `_feat_${JSON.stringify(fontFeatures)}`;
|
|
4576
4708
|
}
|
|
4577
4709
|
}
|
|
4578
4710
|
catch (error) {
|
|
4579
|
-
|
|
4711
|
+
logger.error('Failed to load font:', error);
|
|
4580
4712
|
throw error;
|
|
4581
4713
|
}
|
|
4582
4714
|
finally {
|
|
@@ -4651,7 +4783,7 @@
|
|
|
4651
4783
|
};
|
|
4652
4784
|
}
|
|
4653
4785
|
catch (error) {
|
|
4654
|
-
|
|
4786
|
+
logger.warn(`Failed to load patterns for ${language}: ${error}`);
|
|
4655
4787
|
return {
|
|
4656
4788
|
...options,
|
|
4657
4789
|
layout: {
|
|
@@ -4915,7 +5047,7 @@
|
|
|
4915
5047
|
Text.patternCache.set(language, pattern);
|
|
4916
5048
|
}
|
|
4917
5049
|
catch (error) {
|
|
4918
|
-
|
|
5050
|
+
logger.warn(`Failed to pre-load patterns for ${language}: ${error}`);
|
|
4919
5051
|
}
|
|
4920
5052
|
}
|
|
4921
5053
|
}));
|
|
@@ -4977,7 +5109,7 @@
|
|
|
4977
5109
|
FontLoader.destroyFont(currentFont);
|
|
4978
5110
|
}
|
|
4979
5111
|
catch (error) {
|
|
4980
|
-
|
|
5112
|
+
logger.warn('Error destroying HarfBuzz objects:', error);
|
|
4981
5113
|
}
|
|
4982
5114
|
finally {
|
|
4983
5115
|
this.loadedFont = undefined;
|