three-text 0.2.4 → 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 +47 -24
- package/dist/index.cjs +193 -63
- package/dist/index.d.ts +18 -1
- package/dist/index.js +193 -63
- package/dist/index.min.cjs +2 -2
- package/dist/index.min.js +2 -2
- package/dist/index.umd.js +193 -63
- 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,22 +1182,47 @@
|
|
|
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
|
|
1194
1222
|
let totalWidth = 0;
|
|
1195
|
-
glyphInfos.forEach((glyph
|
|
1223
|
+
glyphInfos.forEach((glyph) => {
|
|
1196
1224
|
totalWidth += glyph.ax;
|
|
1197
|
-
|
|
1198
|
-
const isLastChar = index === glyphInfos.length - 1;
|
|
1199
|
-
const isSingleSpace = text === ' ' || text === ' ' || /^\s+$/.test(text);
|
|
1200
|
-
if (letterSpacingInFontUnits !== 0 && (!isLastChar || isSingleSpace)) {
|
|
1225
|
+
if (letterSpacingInFontUnits !== 0) {
|
|
1201
1226
|
totalWidth += letterSpacingInFontUnits;
|
|
1202
1227
|
}
|
|
1203
1228
|
});
|
|
@@ -1252,7 +1277,7 @@
|
|
|
1252
1277
|
originalEnd: currentIndex + line.length - 1,
|
|
1253
1278
|
xOffset: 0
|
|
1254
1279
|
});
|
|
1255
|
-
currentIndex += line.length + 1;
|
|
1280
|
+
currentIndex += line.length + 1;
|
|
1256
1281
|
}
|
|
1257
1282
|
}
|
|
1258
1283
|
return { lines };
|
|
@@ -1273,7 +1298,7 @@
|
|
|
1273
1298
|
offset = width - planeBounds.max.x;
|
|
1274
1299
|
}
|
|
1275
1300
|
if (offset !== 0) {
|
|
1276
|
-
// Translate vertices
|
|
1301
|
+
// Translate vertices
|
|
1277
1302
|
for (let i = 0; i < vertices.length; i += 3) {
|
|
1278
1303
|
vertices[i] += offset;
|
|
1279
1304
|
}
|
|
@@ -1291,6 +1316,17 @@
|
|
|
1291
1316
|
const FONT_SIGNATURE_TRUE_TYPE_COLLECTION = 0x74746366; // 'ttcf'
|
|
1292
1317
|
const FONT_SIGNATURE_WOFF = 0x774f4646; // 'wOFF'
|
|
1293
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'
|
|
1294
1330
|
|
|
1295
1331
|
class FontMetadataExtractor {
|
|
1296
1332
|
static extractMetadata(fontBuffer) {
|
|
@@ -1307,7 +1343,6 @@
|
|
|
1307
1343
|
if (!validSignatures.includes(sfntVersion)) {
|
|
1308
1344
|
throw new Error(`Invalid font format. Expected TrueType or OpenType, got signature: 0x${sfntVersion.toString(16)}`);
|
|
1309
1345
|
}
|
|
1310
|
-
const buffer = new Uint8Array(fontBuffer);
|
|
1311
1346
|
const numTables = view.getUint16(4); // OpenType header - number of tables is at offset 4
|
|
1312
1347
|
let isCFF = false;
|
|
1313
1348
|
let headTableOffset = 0;
|
|
@@ -1317,30 +1352,28 @@
|
|
|
1317
1352
|
let nameTableOffset = 0;
|
|
1318
1353
|
let fvarTableOffset = 0;
|
|
1319
1354
|
for (let i = 0; i < numTables; i++) {
|
|
1320
|
-
const
|
|
1321
|
-
|
|
1355
|
+
const offset = 12 + i * 16;
|
|
1356
|
+
const tag = view.getUint32(offset);
|
|
1357
|
+
if (tag === TABLE_TAG_CFF || tag === TABLE_TAG_CFF2) {
|
|
1322
1358
|
isCFF = true;
|
|
1323
1359
|
}
|
|
1324
|
-
else if (tag ===
|
|
1325
|
-
|
|
1360
|
+
else if (tag === TABLE_TAG_HEAD) {
|
|
1361
|
+
headTableOffset = view.getUint32(offset + 8);
|
|
1326
1362
|
}
|
|
1327
|
-
if (tag ===
|
|
1328
|
-
|
|
1363
|
+
else if (tag === TABLE_TAG_HHEA) {
|
|
1364
|
+
hheaTableOffset = view.getUint32(offset + 8);
|
|
1329
1365
|
}
|
|
1330
|
-
if (tag ===
|
|
1331
|
-
|
|
1366
|
+
else if (tag === TABLE_TAG_OS2) {
|
|
1367
|
+
os2TableOffset = view.getUint32(offset + 8);
|
|
1332
1368
|
}
|
|
1333
|
-
if (tag ===
|
|
1334
|
-
|
|
1369
|
+
else if (tag === TABLE_TAG_FVAR) {
|
|
1370
|
+
fvarTableOffset = view.getUint32(offset + 8);
|
|
1335
1371
|
}
|
|
1336
|
-
if (tag ===
|
|
1337
|
-
|
|
1372
|
+
else if (tag === TABLE_TAG_STAT) {
|
|
1373
|
+
statTableOffset = view.getUint32(offset + 8);
|
|
1338
1374
|
}
|
|
1339
|
-
if (tag ===
|
|
1340
|
-
|
|
1341
|
-
}
|
|
1342
|
-
if (tag === 'name') {
|
|
1343
|
-
nameTableOffset = view.getUint32(12 + i * 16 + 8);
|
|
1375
|
+
else if (tag === TABLE_TAG_NAME) {
|
|
1376
|
+
nameTableOffset = view.getUint32(offset + 8);
|
|
1344
1377
|
}
|
|
1345
1378
|
}
|
|
1346
1379
|
const unitsPerEm = headTableOffset
|
|
@@ -1383,6 +1416,88 @@
|
|
|
1383
1416
|
axisNames
|
|
1384
1417
|
};
|
|
1385
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
|
+
}
|
|
1386
1501
|
static extractAxisNames(view, statOffset, nameOffset) {
|
|
1387
1502
|
try {
|
|
1388
1503
|
// STAT table structure
|
|
@@ -1593,7 +1708,7 @@
|
|
|
1593
1708
|
const padding = (4 - (table.origLength % 4)) % 4;
|
|
1594
1709
|
sfntOffset += padding;
|
|
1595
1710
|
}
|
|
1596
|
-
|
|
1711
|
+
logger.log('WOFF font decompressed successfully');
|
|
1597
1712
|
return sfntData.buffer.slice(0, sfntOffset);
|
|
1598
1713
|
}
|
|
1599
1714
|
static async decompressZlib(compressedData) {
|
|
@@ -1622,7 +1737,7 @@
|
|
|
1622
1737
|
// Check if this is a WOFF font and decompress if needed
|
|
1623
1738
|
const format = WoffConverter.detectFormat(fontBuffer);
|
|
1624
1739
|
if (format === 'woff') {
|
|
1625
|
-
|
|
1740
|
+
logger.log('WOFF font detected, decompressing...');
|
|
1626
1741
|
fontBuffer = await WoffConverter.decompressWoff(fontBuffer);
|
|
1627
1742
|
}
|
|
1628
1743
|
else if (format === 'woff2') {
|
|
@@ -1660,6 +1775,7 @@
|
|
|
1660
1775
|
};
|
|
1661
1776
|
}
|
|
1662
1777
|
}
|
|
1778
|
+
const featureData = FontMetadataExtractor.extractFeatureTags(fontBuffer);
|
|
1663
1779
|
return {
|
|
1664
1780
|
hb,
|
|
1665
1781
|
fontBlob,
|
|
@@ -1670,11 +1786,13 @@
|
|
|
1670
1786
|
metrics,
|
|
1671
1787
|
fontVariations,
|
|
1672
1788
|
isVariable,
|
|
1673
|
-
variationAxes
|
|
1789
|
+
variationAxes,
|
|
1790
|
+
availableFeatures: featureData?.tags,
|
|
1791
|
+
featureNames: featureData?.names
|
|
1674
1792
|
};
|
|
1675
1793
|
}
|
|
1676
1794
|
catch (error) {
|
|
1677
|
-
|
|
1795
|
+
logger.error('Failed to load font:', error);
|
|
1678
1796
|
throw error;
|
|
1679
1797
|
}
|
|
1680
1798
|
finally {
|
|
@@ -1695,7 +1813,7 @@
|
|
|
1695
1813
|
}
|
|
1696
1814
|
}
|
|
1697
1815
|
catch (error) {
|
|
1698
|
-
|
|
1816
|
+
logger.error('Error destroying font resources:', error);
|
|
1699
1817
|
}
|
|
1700
1818
|
}
|
|
1701
1819
|
}
|
|
@@ -2099,7 +2217,7 @@
|
|
|
2099
2217
|
if (valid.length === 0) {
|
|
2100
2218
|
return { triangles: { vertices: [], indices: [] }, contours: [] };
|
|
2101
2219
|
}
|
|
2102
|
-
|
|
2220
|
+
logger.log(`Tessellator: removeOverlaps=${removeOverlaps}, processing ${valid.length} paths`);
|
|
2103
2221
|
return this.tessellate(valid, removeOverlaps, isCFF);
|
|
2104
2222
|
}
|
|
2105
2223
|
tessellate(paths, removeOverlaps, isCFF) {
|
|
@@ -2109,19 +2227,19 @@
|
|
|
2109
2227
|
: paths;
|
|
2110
2228
|
let contours = this.pathsToContours(normalizedPaths);
|
|
2111
2229
|
if (removeOverlaps) {
|
|
2112
|
-
|
|
2230
|
+
logger.log('Two-pass: boundary extraction then triangulation');
|
|
2113
2231
|
// Extract boundaries to remove overlaps
|
|
2114
2232
|
const boundaryResult = this.performTessellation(contours, 'boundary');
|
|
2115
2233
|
if (!boundaryResult) {
|
|
2116
|
-
|
|
2234
|
+
logger.warn('libtess returned empty result from boundary pass');
|
|
2117
2235
|
return { triangles: { vertices: [], indices: [] }, contours: [] };
|
|
2118
2236
|
}
|
|
2119
2237
|
// Convert boundary elements back to contours
|
|
2120
2238
|
contours = this.boundaryToContours(boundaryResult);
|
|
2121
|
-
|
|
2239
|
+
logger.log(`Boundary pass created ${contours.length} contours. Starting triangulation pass.`);
|
|
2122
2240
|
}
|
|
2123
2241
|
else {
|
|
2124
|
-
|
|
2242
|
+
logger.log(`Single-pass triangulation for ${isCFF ? 'CFF' : 'TTF'}`);
|
|
2125
2243
|
}
|
|
2126
2244
|
// Triangulate the contours
|
|
2127
2245
|
const triangleResult = this.performTessellation(contours, 'triangles');
|
|
@@ -2129,7 +2247,7 @@
|
|
|
2129
2247
|
const warning = removeOverlaps
|
|
2130
2248
|
? 'libtess returned empty result from triangulation pass'
|
|
2131
2249
|
: 'libtess returned empty result from single-pass triangulation';
|
|
2132
|
-
|
|
2250
|
+
logger.warn(warning);
|
|
2133
2251
|
return { triangles: { vertices: [], indices: [] }, contours };
|
|
2134
2252
|
}
|
|
2135
2253
|
return {
|
|
@@ -2184,7 +2302,7 @@
|
|
|
2184
2302
|
return idx;
|
|
2185
2303
|
});
|
|
2186
2304
|
tess.gluTessCallback(libtess_minExports.gluEnum.GLU_TESS_ERROR, (errno) => {
|
|
2187
|
-
|
|
2305
|
+
logger.warn(`libtess error: ${errno}`);
|
|
2188
2306
|
});
|
|
2189
2307
|
tess.gluTessNormal(0, 0, 1);
|
|
2190
2308
|
tess.gluTessBeginPolygon(null);
|
|
@@ -3206,7 +3324,7 @@
|
|
|
3206
3324
|
}
|
|
3207
3325
|
}
|
|
3208
3326
|
catch (error) {
|
|
3209
|
-
|
|
3327
|
+
logger.warn('Error destroying draw callbacks:', error);
|
|
3210
3328
|
}
|
|
3211
3329
|
this.collector = undefined;
|
|
3212
3330
|
}
|
|
@@ -3496,7 +3614,8 @@
|
|
|
3496
3614
|
}
|
|
3497
3615
|
buffer.addText(lineInfo.text);
|
|
3498
3616
|
buffer.guessSegmentProperties();
|
|
3499
|
-
|
|
3617
|
+
const featuresString = convertFontFeaturesToString(this.loadedFont.fontFeatures);
|
|
3618
|
+
this.loadedFont.hb.shape(this.loadedFont.font, buffer, featuresString);
|
|
3500
3619
|
const glyphInfos = buffer.json(this.loadedFont.font);
|
|
3501
3620
|
buffer.destroy();
|
|
3502
3621
|
const clusters = [];
|
|
@@ -3571,15 +3690,14 @@
|
|
|
3571
3690
|
naturalSpaceWidth = TextMeasurer.measureTextWidth(this.loadedFont, ' ', letterSpacing);
|
|
3572
3691
|
this.cachedSpaceWidth.set(letterSpacing, naturalSpaceWidth);
|
|
3573
3692
|
}
|
|
3693
|
+
const width = naturalSpaceWidth;
|
|
3574
3694
|
const stretchFactor = SPACE_STRETCH_RATIO;
|
|
3575
3695
|
const shrinkFactor = SPACE_SHRINK_RATIO;
|
|
3576
3696
|
if (lineInfo.adjustmentRatio > 0) {
|
|
3577
|
-
spaceAdjustment =
|
|
3578
|
-
lineInfo.adjustmentRatio * naturalSpaceWidth * stretchFactor;
|
|
3697
|
+
spaceAdjustment = lineInfo.adjustmentRatio * width * stretchFactor;
|
|
3579
3698
|
}
|
|
3580
3699
|
else if (lineInfo.adjustmentRatio < 0) {
|
|
3581
|
-
spaceAdjustment =
|
|
3582
|
-
lineInfo.adjustmentRatio * naturalSpaceWidth * shrinkFactor;
|
|
3700
|
+
spaceAdjustment = lineInfo.adjustmentRatio * width * shrinkFactor;
|
|
3583
3701
|
}
|
|
3584
3702
|
}
|
|
3585
3703
|
return spaceAdjustment;
|
|
@@ -4504,12 +4622,16 @@
|
|
|
4504
4622
|
const baseFontKey = typeof options.font === 'string'
|
|
4505
4623
|
? options.font
|
|
4506
4624
|
: `buffer-${Text.generateFontContentHash(options.font)}`;
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
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
|
+
}
|
|
4510
4632
|
let loadedFont = Text.fontCache.get(fontKey);
|
|
4511
4633
|
if (!loadedFont) {
|
|
4512
|
-
loadedFont = await Text.loadAndCacheFont(fontKey, options.font, options.fontVariations);
|
|
4634
|
+
loadedFont = await Text.loadAndCacheFont(fontKey, options.font, options.fontVariations, options.fontFeatures);
|
|
4513
4635
|
}
|
|
4514
4636
|
const text = new Text({ maxCacheSizeMB: options.maxCacheSizeMB });
|
|
4515
4637
|
text.setLoadedFont(loadedFont);
|
|
@@ -4523,12 +4645,11 @@
|
|
|
4523
4645
|
measureTextWidth: (textString, letterSpacing) => text.measureTextWidth(textString, letterSpacing)
|
|
4524
4646
|
};
|
|
4525
4647
|
}
|
|
4526
|
-
static async loadAndCacheFont(fontKey, font, fontVariations) {
|
|
4648
|
+
static async loadAndCacheFont(fontKey, font, fontVariations, fontFeatures) {
|
|
4527
4649
|
const tempText = new Text();
|
|
4528
|
-
await tempText.loadFont(font, fontVariations);
|
|
4650
|
+
await tempText.loadFont(font, fontVariations, fontFeatures);
|
|
4529
4651
|
const loadedFont = tempText.getLoadedFont();
|
|
4530
4652
|
Text.fontCache.set(fontKey, loadedFont);
|
|
4531
|
-
// Don't destroy tempText - the cached font references its HarfBuzz objects
|
|
4532
4653
|
return loadedFont;
|
|
4533
4654
|
}
|
|
4534
4655
|
static generateFontContentHash(buffer) {
|
|
@@ -4547,10 +4668,13 @@
|
|
|
4547
4668
|
const contentHash = Text.generateFontContentHash(loadedFont._buffer);
|
|
4548
4669
|
this.currentFontId = `font_${contentHash}`;
|
|
4549
4670
|
if (loadedFont.fontVariations) {
|
|
4550
|
-
this.currentFontId += `
|
|
4671
|
+
this.currentFontId += `_var_${JSON.stringify(loadedFont.fontVariations)}`;
|
|
4672
|
+
}
|
|
4673
|
+
if (loadedFont.fontFeatures) {
|
|
4674
|
+
this.currentFontId += `_feat_${JSON.stringify(loadedFont.fontFeatures)}`;
|
|
4551
4675
|
}
|
|
4552
4676
|
}
|
|
4553
|
-
async loadFont(fontSrc, fontVariations) {
|
|
4677
|
+
async loadFont(fontSrc, fontVariations, fontFeatures) {
|
|
4554
4678
|
perfLogger.start('Text.loadFont', {
|
|
4555
4679
|
fontSrc: typeof fontSrc === 'string' ? fontSrc : `buffer(${fontSrc.byteLength})`
|
|
4556
4680
|
});
|
|
@@ -4571,14 +4695,20 @@
|
|
|
4571
4695
|
this.destroy();
|
|
4572
4696
|
}
|
|
4573
4697
|
this.loadedFont = await this.fontLoader.loadFont(fontBuffer, fontVariations);
|
|
4698
|
+
if (fontFeatures) {
|
|
4699
|
+
this.loadedFont.fontFeatures = fontFeatures;
|
|
4700
|
+
}
|
|
4574
4701
|
const contentHash = Text.generateFontContentHash(fontBuffer);
|
|
4575
4702
|
this.currentFontId = `font_${contentHash}`;
|
|
4576
4703
|
if (fontVariations) {
|
|
4577
|
-
this.currentFontId += `
|
|
4704
|
+
this.currentFontId += `_var_${JSON.stringify(fontVariations)}`;
|
|
4705
|
+
}
|
|
4706
|
+
if (fontFeatures) {
|
|
4707
|
+
this.currentFontId += `_feat_${JSON.stringify(fontFeatures)}`;
|
|
4578
4708
|
}
|
|
4579
4709
|
}
|
|
4580
4710
|
catch (error) {
|
|
4581
|
-
|
|
4711
|
+
logger.error('Failed to load font:', error);
|
|
4582
4712
|
throw error;
|
|
4583
4713
|
}
|
|
4584
4714
|
finally {
|
|
@@ -4653,7 +4783,7 @@
|
|
|
4653
4783
|
};
|
|
4654
4784
|
}
|
|
4655
4785
|
catch (error) {
|
|
4656
|
-
|
|
4786
|
+
logger.warn(`Failed to load patterns for ${language}: ${error}`);
|
|
4657
4787
|
return {
|
|
4658
4788
|
...options,
|
|
4659
4789
|
layout: {
|
|
@@ -4917,7 +5047,7 @@
|
|
|
4917
5047
|
Text.patternCache.set(language, pattern);
|
|
4918
5048
|
}
|
|
4919
5049
|
catch (error) {
|
|
4920
|
-
|
|
5050
|
+
logger.warn(`Failed to pre-load patterns for ${language}: ${error}`);
|
|
4921
5051
|
}
|
|
4922
5052
|
}
|
|
4923
5053
|
}));
|
|
@@ -4979,7 +5109,7 @@
|
|
|
4979
5109
|
FontLoader.destroyFont(currentFont);
|
|
4980
5110
|
}
|
|
4981
5111
|
catch (error) {
|
|
4982
|
-
|
|
5112
|
+
logger.warn('Error destroying HarfBuzz objects:', error);
|
|
4983
5113
|
}
|
|
4984
5114
|
finally {
|
|
4985
5115
|
this.loadedFont = undefined;
|