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.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
|
|
@@ -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;
|
|
@@ -562,7 +562,7 @@ class LineBreak {
|
|
|
562
562
|
let useHyphenation = hyphenate;
|
|
563
563
|
if (useHyphenation &&
|
|
564
564
|
(!hyphenationPatterns || !hyphenationPatterns[language])) {
|
|
565
|
-
|
|
565
|
+
logger.warn(`Hyphenation patterns for ${language} not available`);
|
|
566
566
|
useHyphenation = false;
|
|
567
567
|
}
|
|
568
568
|
// Calculate initial emergency stretch (TeX default: 0)
|
|
@@ -1177,12 +1177,40 @@ class LineBreak {
|
|
|
1177
1177
|
}
|
|
1178
1178
|
}
|
|
1179
1179
|
|
|
1180
|
+
// Convert feature objects to HarfBuzz comma-separated format
|
|
1181
|
+
function convertFontFeaturesToString(features) {
|
|
1182
|
+
if (!features || Object.keys(features).length === 0) {
|
|
1183
|
+
return undefined;
|
|
1184
|
+
}
|
|
1185
|
+
const featureStrings = [];
|
|
1186
|
+
for (const [tag, value] of Object.entries(features)) {
|
|
1187
|
+
if (!/^[a-zA-Z0-9]{4}$/.test(tag)) {
|
|
1188
|
+
logger.warn(`Invalid OpenType feature tag: "${tag}". Tags must be exactly 4 alphanumeric characters.`);
|
|
1189
|
+
continue;
|
|
1190
|
+
}
|
|
1191
|
+
if (value === false || value === 0) {
|
|
1192
|
+
featureStrings.push(`${tag}=0`);
|
|
1193
|
+
}
|
|
1194
|
+
else if (value === true || value === 1) {
|
|
1195
|
+
featureStrings.push(tag);
|
|
1196
|
+
}
|
|
1197
|
+
else if (typeof value === 'number' && value > 1) {
|
|
1198
|
+
featureStrings.push(`${tag}=${Math.floor(value)}`);
|
|
1199
|
+
}
|
|
1200
|
+
else {
|
|
1201
|
+
logger.warn(`Invalid value for feature "${tag}": ${value}. Expected boolean or positive number.`);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
return featureStrings.length > 0 ? featureStrings.join(',') : undefined;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1180
1207
|
class TextMeasurer {
|
|
1181
1208
|
static measureTextWidth(loadedFont, text, letterSpacing = 0) {
|
|
1182
1209
|
const buffer = loadedFont.hb.createBuffer();
|
|
1183
1210
|
buffer.addText(text);
|
|
1184
1211
|
buffer.guessSegmentProperties();
|
|
1185
|
-
|
|
1212
|
+
const featuresString = convertFontFeaturesToString(loadedFont.fontFeatures);
|
|
1213
|
+
loadedFont.hb.shape(loadedFont.font, buffer, featuresString);
|
|
1186
1214
|
const glyphInfos = buffer.json(loadedFont.font);
|
|
1187
1215
|
const letterSpacingInFontUnits = letterSpacing * loadedFont.upem;
|
|
1188
1216
|
// Calculate total advance width with letter spacing
|
|
@@ -1283,6 +1311,17 @@ const FONT_SIGNATURE_OPEN_TYPE_CFF = 0x4f54544f; // 'OTTO'
|
|
|
1283
1311
|
const FONT_SIGNATURE_TRUE_TYPE_COLLECTION = 0x74746366; // 'ttcf'
|
|
1284
1312
|
const FONT_SIGNATURE_WOFF = 0x774f4646; // 'wOFF'
|
|
1285
1313
|
const FONT_SIGNATURE_WOFF2 = 0x774f4632; // 'wOF2'
|
|
1314
|
+
// Table Tags
|
|
1315
|
+
const TABLE_TAG_HEAD = 0x68656164; // 'head'
|
|
1316
|
+
const TABLE_TAG_HHEA = 0x68686561; // 'hhea'
|
|
1317
|
+
const TABLE_TAG_OS2 = 0x4f532f32; // 'OS/2'
|
|
1318
|
+
const TABLE_TAG_FVAR = 0x66766172; // 'fvar'
|
|
1319
|
+
const TABLE_TAG_STAT = 0x53544154; // 'STAT'
|
|
1320
|
+
const TABLE_TAG_NAME = 0x6e616d65; // 'name'
|
|
1321
|
+
const TABLE_TAG_CFF = 0x43464620; // 'CFF '
|
|
1322
|
+
const TABLE_TAG_CFF2 = 0x43464632; // 'CFF2'
|
|
1323
|
+
const TABLE_TAG_GSUB = 0x47535542; // 'GSUB'
|
|
1324
|
+
const TABLE_TAG_GPOS = 0x47504f53; // 'GPOS'
|
|
1286
1325
|
|
|
1287
1326
|
class FontMetadataExtractor {
|
|
1288
1327
|
static extractMetadata(fontBuffer) {
|
|
@@ -1299,7 +1338,6 @@ class FontMetadataExtractor {
|
|
|
1299
1338
|
if (!validSignatures.includes(sfntVersion)) {
|
|
1300
1339
|
throw new Error(`Invalid font format. Expected TrueType or OpenType, got signature: 0x${sfntVersion.toString(16)}`);
|
|
1301
1340
|
}
|
|
1302
|
-
const buffer = new Uint8Array(fontBuffer);
|
|
1303
1341
|
const numTables = view.getUint16(4); // OpenType header - number of tables is at offset 4
|
|
1304
1342
|
let isCFF = false;
|
|
1305
1343
|
let headTableOffset = 0;
|
|
@@ -1309,30 +1347,28 @@ class FontMetadataExtractor {
|
|
|
1309
1347
|
let nameTableOffset = 0;
|
|
1310
1348
|
let fvarTableOffset = 0;
|
|
1311
1349
|
for (let i = 0; i < numTables; i++) {
|
|
1312
|
-
const
|
|
1313
|
-
|
|
1350
|
+
const offset = 12 + i * 16;
|
|
1351
|
+
const tag = view.getUint32(offset);
|
|
1352
|
+
if (tag === TABLE_TAG_CFF || tag === TABLE_TAG_CFF2) {
|
|
1314
1353
|
isCFF = true;
|
|
1315
1354
|
}
|
|
1316
|
-
else if (tag ===
|
|
1317
|
-
|
|
1355
|
+
else if (tag === TABLE_TAG_HEAD) {
|
|
1356
|
+
headTableOffset = view.getUint32(offset + 8);
|
|
1318
1357
|
}
|
|
1319
|
-
if (tag ===
|
|
1320
|
-
|
|
1358
|
+
else if (tag === TABLE_TAG_HHEA) {
|
|
1359
|
+
hheaTableOffset = view.getUint32(offset + 8);
|
|
1321
1360
|
}
|
|
1322
|
-
if (tag ===
|
|
1323
|
-
|
|
1361
|
+
else if (tag === TABLE_TAG_OS2) {
|
|
1362
|
+
os2TableOffset = view.getUint32(offset + 8);
|
|
1324
1363
|
}
|
|
1325
|
-
if (tag ===
|
|
1326
|
-
|
|
1364
|
+
else if (tag === TABLE_TAG_FVAR) {
|
|
1365
|
+
fvarTableOffset = view.getUint32(offset + 8);
|
|
1327
1366
|
}
|
|
1328
|
-
if (tag ===
|
|
1329
|
-
|
|
1367
|
+
else if (tag === TABLE_TAG_STAT) {
|
|
1368
|
+
statTableOffset = view.getUint32(offset + 8);
|
|
1330
1369
|
}
|
|
1331
|
-
if (tag ===
|
|
1332
|
-
|
|
1333
|
-
}
|
|
1334
|
-
if (tag === 'name') {
|
|
1335
|
-
nameTableOffset = view.getUint32(12 + i * 16 + 8);
|
|
1370
|
+
else if (tag === TABLE_TAG_NAME) {
|
|
1371
|
+
nameTableOffset = view.getUint32(offset + 8);
|
|
1336
1372
|
}
|
|
1337
1373
|
}
|
|
1338
1374
|
const unitsPerEm = headTableOffset
|
|
@@ -1375,6 +1411,88 @@ class FontMetadataExtractor {
|
|
|
1375
1411
|
axisNames
|
|
1376
1412
|
};
|
|
1377
1413
|
}
|
|
1414
|
+
static extractFeatureTags(fontBuffer) {
|
|
1415
|
+
const view = new DataView(fontBuffer);
|
|
1416
|
+
const numTables = view.getUint16(4);
|
|
1417
|
+
let gsubTableOffset = 0;
|
|
1418
|
+
let gposTableOffset = 0;
|
|
1419
|
+
let nameTableOffset = 0;
|
|
1420
|
+
for (let i = 0; i < numTables; i++) {
|
|
1421
|
+
const offset = 12 + i * 16;
|
|
1422
|
+
const tag = view.getUint32(offset);
|
|
1423
|
+
if (tag === TABLE_TAG_GSUB) {
|
|
1424
|
+
gsubTableOffset = view.getUint32(offset + 8);
|
|
1425
|
+
}
|
|
1426
|
+
else if (tag === TABLE_TAG_GPOS) {
|
|
1427
|
+
gposTableOffset = view.getUint32(offset + 8);
|
|
1428
|
+
}
|
|
1429
|
+
else if (tag === TABLE_TAG_NAME) {
|
|
1430
|
+
nameTableOffset = view.getUint32(offset + 8);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
const features = new Set();
|
|
1434
|
+
const featureNames = {};
|
|
1435
|
+
try {
|
|
1436
|
+
if (gsubTableOffset) {
|
|
1437
|
+
const gsubData = this.extractFeatureDataFromTable(view, gsubTableOffset, nameTableOffset);
|
|
1438
|
+
gsubData.features.forEach(f => features.add(f));
|
|
1439
|
+
Object.assign(featureNames, gsubData.names);
|
|
1440
|
+
}
|
|
1441
|
+
if (gposTableOffset) {
|
|
1442
|
+
const gposData = this.extractFeatureDataFromTable(view, gposTableOffset, nameTableOffset);
|
|
1443
|
+
gposData.features.forEach(f => features.add(f));
|
|
1444
|
+
Object.assign(featureNames, gposData.names);
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
catch (e) {
|
|
1448
|
+
return undefined;
|
|
1449
|
+
}
|
|
1450
|
+
const featureArray = Array.from(features).sort();
|
|
1451
|
+
if (featureArray.length === 0)
|
|
1452
|
+
return undefined;
|
|
1453
|
+
return {
|
|
1454
|
+
tags: featureArray,
|
|
1455
|
+
names: Object.keys(featureNames).length > 0 ? featureNames : {}
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1458
|
+
static extractFeatureDataFromTable(view, tableOffset, nameTableOffset) {
|
|
1459
|
+
const featureListOffset = view.getUint16(tableOffset + 6);
|
|
1460
|
+
const featureListStart = tableOffset + featureListOffset;
|
|
1461
|
+
const featureCount = view.getUint16(featureListStart);
|
|
1462
|
+
const features = [];
|
|
1463
|
+
const names = {};
|
|
1464
|
+
for (let i = 0; i < featureCount; i++) {
|
|
1465
|
+
const recordOffset = featureListStart + 2 + i * 6;
|
|
1466
|
+
// Decode feature tag
|
|
1467
|
+
const tag = String.fromCharCode(view.getUint8(recordOffset), view.getUint8(recordOffset + 1), view.getUint8(recordOffset + 2), view.getUint8(recordOffset + 3));
|
|
1468
|
+
features.push(tag);
|
|
1469
|
+
// Extract feature name for stylistic sets and character variants
|
|
1470
|
+
if (/^(ss\d{2}|cv\d{2})$/.test(tag) && nameTableOffset) {
|
|
1471
|
+
const featureOffset = view.getUint16(recordOffset + 4);
|
|
1472
|
+
const featureTableStart = featureListStart + featureOffset;
|
|
1473
|
+
// Feature table structure:
|
|
1474
|
+
// uint16 FeatureParams offset
|
|
1475
|
+
// uint16 LookupCount
|
|
1476
|
+
// uint16[LookupCount] LookupListIndex
|
|
1477
|
+
const featureParamsOffset = view.getUint16(featureTableStart);
|
|
1478
|
+
// FeatureParams for ss features:
|
|
1479
|
+
// uint16 Version (should be 0)
|
|
1480
|
+
// uint16 UINameID
|
|
1481
|
+
if (featureParamsOffset !== 0) {
|
|
1482
|
+
const paramsStart = featureTableStart + featureParamsOffset;
|
|
1483
|
+
const version = view.getUint16(paramsStart);
|
|
1484
|
+
if (version === 0) {
|
|
1485
|
+
const nameID = view.getUint16(paramsStart + 2);
|
|
1486
|
+
const name = this.getNameFromNameTable(view, nameTableOffset, nameID);
|
|
1487
|
+
if (name) {
|
|
1488
|
+
names[tag] = name;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
return { features, names };
|
|
1495
|
+
}
|
|
1378
1496
|
static extractAxisNames(view, statOffset, nameOffset) {
|
|
1379
1497
|
try {
|
|
1380
1498
|
// STAT table structure
|
|
@@ -1585,7 +1703,7 @@ class WoffConverter {
|
|
|
1585
1703
|
const padding = (4 - (table.origLength % 4)) % 4;
|
|
1586
1704
|
sfntOffset += padding;
|
|
1587
1705
|
}
|
|
1588
|
-
|
|
1706
|
+
logger.log('WOFF font decompressed successfully');
|
|
1589
1707
|
return sfntData.buffer.slice(0, sfntOffset);
|
|
1590
1708
|
}
|
|
1591
1709
|
static async decompressZlib(compressedData) {
|
|
@@ -1614,7 +1732,7 @@ class FontLoader {
|
|
|
1614
1732
|
// Check if this is a WOFF font and decompress if needed
|
|
1615
1733
|
const format = WoffConverter.detectFormat(fontBuffer);
|
|
1616
1734
|
if (format === 'woff') {
|
|
1617
|
-
|
|
1735
|
+
logger.log('WOFF font detected, decompressing...');
|
|
1618
1736
|
fontBuffer = await WoffConverter.decompressWoff(fontBuffer);
|
|
1619
1737
|
}
|
|
1620
1738
|
else if (format === 'woff2') {
|
|
@@ -1652,6 +1770,7 @@ class FontLoader {
|
|
|
1652
1770
|
};
|
|
1653
1771
|
}
|
|
1654
1772
|
}
|
|
1773
|
+
const featureData = FontMetadataExtractor.extractFeatureTags(fontBuffer);
|
|
1655
1774
|
return {
|
|
1656
1775
|
hb,
|
|
1657
1776
|
fontBlob,
|
|
@@ -1662,11 +1781,13 @@ class FontLoader {
|
|
|
1662
1781
|
metrics,
|
|
1663
1782
|
fontVariations,
|
|
1664
1783
|
isVariable,
|
|
1665
|
-
variationAxes
|
|
1784
|
+
variationAxes,
|
|
1785
|
+
availableFeatures: featureData?.tags,
|
|
1786
|
+
featureNames: featureData?.names
|
|
1666
1787
|
};
|
|
1667
1788
|
}
|
|
1668
1789
|
catch (error) {
|
|
1669
|
-
|
|
1790
|
+
logger.error('Failed to load font:', error);
|
|
1670
1791
|
throw error;
|
|
1671
1792
|
}
|
|
1672
1793
|
finally {
|
|
@@ -1687,7 +1808,7 @@ class FontLoader {
|
|
|
1687
1808
|
}
|
|
1688
1809
|
}
|
|
1689
1810
|
catch (error) {
|
|
1690
|
-
|
|
1811
|
+
logger.error('Error destroying font resources:', error);
|
|
1691
1812
|
}
|
|
1692
1813
|
}
|
|
1693
1814
|
}
|
|
@@ -2089,7 +2210,7 @@ class Tessellator {
|
|
|
2089
2210
|
if (valid.length === 0) {
|
|
2090
2211
|
return { triangles: { vertices: [], indices: [] }, contours: [] };
|
|
2091
2212
|
}
|
|
2092
|
-
|
|
2213
|
+
logger.log(`Tessellator: removeOverlaps=${removeOverlaps}, processing ${valid.length} paths`);
|
|
2093
2214
|
return this.tessellate(valid, removeOverlaps, isCFF);
|
|
2094
2215
|
}
|
|
2095
2216
|
tessellate(paths, removeOverlaps, isCFF) {
|
|
@@ -2099,19 +2220,19 @@ class Tessellator {
|
|
|
2099
2220
|
: paths;
|
|
2100
2221
|
let contours = this.pathsToContours(normalizedPaths);
|
|
2101
2222
|
if (removeOverlaps) {
|
|
2102
|
-
|
|
2223
|
+
logger.log('Two-pass: boundary extraction then triangulation');
|
|
2103
2224
|
// Extract boundaries to remove overlaps
|
|
2104
2225
|
const boundaryResult = this.performTessellation(contours, 'boundary');
|
|
2105
2226
|
if (!boundaryResult) {
|
|
2106
|
-
|
|
2227
|
+
logger.warn('libtess returned empty result from boundary pass');
|
|
2107
2228
|
return { triangles: { vertices: [], indices: [] }, contours: [] };
|
|
2108
2229
|
}
|
|
2109
2230
|
// Convert boundary elements back to contours
|
|
2110
2231
|
contours = this.boundaryToContours(boundaryResult);
|
|
2111
|
-
|
|
2232
|
+
logger.log(`Boundary pass created ${contours.length} contours. Starting triangulation pass.`);
|
|
2112
2233
|
}
|
|
2113
2234
|
else {
|
|
2114
|
-
|
|
2235
|
+
logger.log(`Single-pass triangulation for ${isCFF ? 'CFF' : 'TTF'}`);
|
|
2115
2236
|
}
|
|
2116
2237
|
// Triangulate the contours
|
|
2117
2238
|
const triangleResult = this.performTessellation(contours, 'triangles');
|
|
@@ -2119,7 +2240,7 @@ class Tessellator {
|
|
|
2119
2240
|
const warning = removeOverlaps
|
|
2120
2241
|
? 'libtess returned empty result from triangulation pass'
|
|
2121
2242
|
: 'libtess returned empty result from single-pass triangulation';
|
|
2122
|
-
|
|
2243
|
+
logger.warn(warning);
|
|
2123
2244
|
return { triangles: { vertices: [], indices: [] }, contours };
|
|
2124
2245
|
}
|
|
2125
2246
|
return {
|
|
@@ -2174,7 +2295,7 @@ class Tessellator {
|
|
|
2174
2295
|
return idx;
|
|
2175
2296
|
});
|
|
2176
2297
|
tess.gluTessCallback(libtess_minExports.gluEnum.GLU_TESS_ERROR, (errno) => {
|
|
2177
|
-
|
|
2298
|
+
logger.warn(`libtess error: ${errno}`);
|
|
2178
2299
|
});
|
|
2179
2300
|
tess.gluTessNormal(0, 0, 1);
|
|
2180
2301
|
tess.gluTessBeginPolygon(null);
|
|
@@ -3196,7 +3317,7 @@ class DrawCallbackHandler {
|
|
|
3196
3317
|
}
|
|
3197
3318
|
}
|
|
3198
3319
|
catch (error) {
|
|
3199
|
-
|
|
3320
|
+
logger.warn('Error destroying draw callbacks:', error);
|
|
3200
3321
|
}
|
|
3201
3322
|
this.collector = undefined;
|
|
3202
3323
|
}
|
|
@@ -3486,7 +3607,8 @@ class TextShaper {
|
|
|
3486
3607
|
}
|
|
3487
3608
|
buffer.addText(lineInfo.text);
|
|
3488
3609
|
buffer.guessSegmentProperties();
|
|
3489
|
-
|
|
3610
|
+
const featuresString = convertFontFeaturesToString(this.loadedFont.fontFeatures);
|
|
3611
|
+
this.loadedFont.hb.shape(this.loadedFont.font, buffer, featuresString);
|
|
3490
3612
|
const glyphInfos = buffer.json(this.loadedFont.font);
|
|
3491
3613
|
buffer.destroy();
|
|
3492
3614
|
const clusters = [];
|
|
@@ -3565,12 +3687,10 @@ class TextShaper {
|
|
|
3565
3687
|
const stretchFactor = SPACE_STRETCH_RATIO;
|
|
3566
3688
|
const shrinkFactor = SPACE_SHRINK_RATIO;
|
|
3567
3689
|
if (lineInfo.adjustmentRatio > 0) {
|
|
3568
|
-
spaceAdjustment =
|
|
3569
|
-
lineInfo.adjustmentRatio * width * stretchFactor;
|
|
3690
|
+
spaceAdjustment = lineInfo.adjustmentRatio * width * stretchFactor;
|
|
3570
3691
|
}
|
|
3571
3692
|
else if (lineInfo.adjustmentRatio < 0) {
|
|
3572
|
-
spaceAdjustment =
|
|
3573
|
-
lineInfo.adjustmentRatio * width * shrinkFactor;
|
|
3693
|
+
spaceAdjustment = lineInfo.adjustmentRatio * width * shrinkFactor;
|
|
3574
3694
|
}
|
|
3575
3695
|
}
|
|
3576
3696
|
return spaceAdjustment;
|
|
@@ -4495,12 +4615,16 @@ class Text {
|
|
|
4495
4615
|
const baseFontKey = typeof options.font === 'string'
|
|
4496
4616
|
? options.font
|
|
4497
4617
|
: `buffer-${Text.generateFontContentHash(options.font)}`;
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4618
|
+
let fontKey = baseFontKey;
|
|
4619
|
+
if (options.fontVariations) {
|
|
4620
|
+
fontKey += `_var_${JSON.stringify(options.fontVariations)}`;
|
|
4621
|
+
}
|
|
4622
|
+
if (options.fontFeatures) {
|
|
4623
|
+
fontKey += `_feat_${JSON.stringify(options.fontFeatures)}`;
|
|
4624
|
+
}
|
|
4501
4625
|
let loadedFont = Text.fontCache.get(fontKey);
|
|
4502
4626
|
if (!loadedFont) {
|
|
4503
|
-
loadedFont = await Text.loadAndCacheFont(fontKey, options.font, options.fontVariations);
|
|
4627
|
+
loadedFont = await Text.loadAndCacheFont(fontKey, options.font, options.fontVariations, options.fontFeatures);
|
|
4504
4628
|
}
|
|
4505
4629
|
const text = new Text({ maxCacheSizeMB: options.maxCacheSizeMB });
|
|
4506
4630
|
text.setLoadedFont(loadedFont);
|
|
@@ -4514,12 +4638,11 @@ class Text {
|
|
|
4514
4638
|
measureTextWidth: (textString, letterSpacing) => text.measureTextWidth(textString, letterSpacing)
|
|
4515
4639
|
};
|
|
4516
4640
|
}
|
|
4517
|
-
static async loadAndCacheFont(fontKey, font, fontVariations) {
|
|
4641
|
+
static async loadAndCacheFont(fontKey, font, fontVariations, fontFeatures) {
|
|
4518
4642
|
const tempText = new Text();
|
|
4519
|
-
await tempText.loadFont(font, fontVariations);
|
|
4643
|
+
await tempText.loadFont(font, fontVariations, fontFeatures);
|
|
4520
4644
|
const loadedFont = tempText.getLoadedFont();
|
|
4521
4645
|
Text.fontCache.set(fontKey, loadedFont);
|
|
4522
|
-
// Don't destroy tempText - the cached font references its HarfBuzz objects
|
|
4523
4646
|
return loadedFont;
|
|
4524
4647
|
}
|
|
4525
4648
|
static generateFontContentHash(buffer) {
|
|
@@ -4538,10 +4661,13 @@ class Text {
|
|
|
4538
4661
|
const contentHash = Text.generateFontContentHash(loadedFont._buffer);
|
|
4539
4662
|
this.currentFontId = `font_${contentHash}`;
|
|
4540
4663
|
if (loadedFont.fontVariations) {
|
|
4541
|
-
this.currentFontId += `
|
|
4664
|
+
this.currentFontId += `_var_${JSON.stringify(loadedFont.fontVariations)}`;
|
|
4665
|
+
}
|
|
4666
|
+
if (loadedFont.fontFeatures) {
|
|
4667
|
+
this.currentFontId += `_feat_${JSON.stringify(loadedFont.fontFeatures)}`;
|
|
4542
4668
|
}
|
|
4543
4669
|
}
|
|
4544
|
-
async loadFont(fontSrc, fontVariations) {
|
|
4670
|
+
async loadFont(fontSrc, fontVariations, fontFeatures) {
|
|
4545
4671
|
perfLogger.start('Text.loadFont', {
|
|
4546
4672
|
fontSrc: typeof fontSrc === 'string' ? fontSrc : `buffer(${fontSrc.byteLength})`
|
|
4547
4673
|
});
|
|
@@ -4562,14 +4688,20 @@ class Text {
|
|
|
4562
4688
|
this.destroy();
|
|
4563
4689
|
}
|
|
4564
4690
|
this.loadedFont = await this.fontLoader.loadFont(fontBuffer, fontVariations);
|
|
4691
|
+
if (fontFeatures) {
|
|
4692
|
+
this.loadedFont.fontFeatures = fontFeatures;
|
|
4693
|
+
}
|
|
4565
4694
|
const contentHash = Text.generateFontContentHash(fontBuffer);
|
|
4566
4695
|
this.currentFontId = `font_${contentHash}`;
|
|
4567
4696
|
if (fontVariations) {
|
|
4568
|
-
this.currentFontId += `
|
|
4697
|
+
this.currentFontId += `_var_${JSON.stringify(fontVariations)}`;
|
|
4698
|
+
}
|
|
4699
|
+
if (fontFeatures) {
|
|
4700
|
+
this.currentFontId += `_feat_${JSON.stringify(fontFeatures)}`;
|
|
4569
4701
|
}
|
|
4570
4702
|
}
|
|
4571
4703
|
catch (error) {
|
|
4572
|
-
|
|
4704
|
+
logger.error('Failed to load font:', error);
|
|
4573
4705
|
throw error;
|
|
4574
4706
|
}
|
|
4575
4707
|
finally {
|
|
@@ -4644,7 +4776,7 @@ class Text {
|
|
|
4644
4776
|
};
|
|
4645
4777
|
}
|
|
4646
4778
|
catch (error) {
|
|
4647
|
-
|
|
4779
|
+
logger.warn(`Failed to load patterns for ${language}: ${error}`);
|
|
4648
4780
|
return {
|
|
4649
4781
|
...options,
|
|
4650
4782
|
layout: {
|
|
@@ -4908,7 +5040,7 @@ class Text {
|
|
|
4908
5040
|
Text.patternCache.set(language, pattern);
|
|
4909
5041
|
}
|
|
4910
5042
|
catch (error) {
|
|
4911
|
-
|
|
5043
|
+
logger.warn(`Failed to pre-load patterns for ${language}: ${error}`);
|
|
4912
5044
|
}
|
|
4913
5045
|
}
|
|
4914
5046
|
}));
|
|
@@ -4970,7 +5102,7 @@ class Text {
|
|
|
4970
5102
|
FontLoader.destroyFont(currentFont);
|
|
4971
5103
|
}
|
|
4972
5104
|
catch (error) {
|
|
4973
|
-
|
|
5105
|
+
logger.warn('Error destroying HarfBuzz objects:', error);
|
|
4974
5106
|
}
|
|
4975
5107
|
finally {
|
|
4976
5108
|
this.loadedFont = undefined;
|