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.d.ts
CHANGED
|
@@ -62,10 +62,17 @@ interface LoadedFont {
|
|
|
62
62
|
fontVariations?: {
|
|
63
63
|
[key: string]: number;
|
|
64
64
|
};
|
|
65
|
+
fontFeatures?: {
|
|
66
|
+
[tag: string]: boolean | number;
|
|
67
|
+
};
|
|
65
68
|
isVariable?: boolean;
|
|
66
69
|
variationAxes?: {
|
|
67
70
|
[key: string]: VariationAxis;
|
|
68
71
|
};
|
|
72
|
+
availableFeatures?: string[];
|
|
73
|
+
featureNames?: {
|
|
74
|
+
[tag: string]: string;
|
|
75
|
+
};
|
|
69
76
|
_buffer?: ArrayBuffer;
|
|
70
77
|
}
|
|
71
78
|
interface HarfBuzzModule {
|
|
@@ -84,7 +91,7 @@ interface HarfBuzzAPI {
|
|
|
84
91
|
createFace: (blob: HarfBuzzBlob, index: number) => HarfBuzzFace;
|
|
85
92
|
createFont: (face: HarfBuzzFace) => HarfBuzzFont;
|
|
86
93
|
createBuffer: () => HarfBuzzBuffer;
|
|
87
|
-
shape: (font: HarfBuzzFont, buffer: HarfBuzzBuffer) => void;
|
|
94
|
+
shape: (font: HarfBuzzFont, buffer: HarfBuzzBuffer, features?: string) => void;
|
|
88
95
|
}
|
|
89
96
|
interface HarfBuzzBlob {
|
|
90
97
|
destroy: () => void;
|
|
@@ -263,6 +270,9 @@ interface TextOptions {
|
|
|
263
270
|
fontVariations?: {
|
|
264
271
|
[key: string]: number;
|
|
265
272
|
};
|
|
273
|
+
fontFeatures?: {
|
|
274
|
+
[tag: string]: boolean | number;
|
|
275
|
+
};
|
|
266
276
|
maxTextLength?: number;
|
|
267
277
|
removeOverlaps?: boolean;
|
|
268
278
|
curveFidelity?: CurveFidelityConfig;
|
|
@@ -436,6 +446,13 @@ declare const DEFAULT_CURVE_FIDELITY: CurveFidelityConfig;
|
|
|
436
446
|
|
|
437
447
|
declare class FontMetadataExtractor {
|
|
438
448
|
static extractMetadata(fontBuffer: ArrayBuffer): ExtractedMetrics;
|
|
449
|
+
static extractFeatureTags(fontBuffer: ArrayBuffer): {
|
|
450
|
+
tags: string[];
|
|
451
|
+
names: {
|
|
452
|
+
[tag: string]: string;
|
|
453
|
+
};
|
|
454
|
+
} | undefined;
|
|
455
|
+
private static extractFeatureDataFromTable;
|
|
439
456
|
private static extractAxisNames;
|
|
440
457
|
private static getNameFromNameTable;
|
|
441
458
|
static getVerticalMetrics(metrics: ExtractedMetrics): VerticalMetrics;
|
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,22 +1177,47 @@ 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
|
|
1189
1217
|
let totalWidth = 0;
|
|
1190
|
-
glyphInfos.forEach((glyph
|
|
1218
|
+
glyphInfos.forEach((glyph) => {
|
|
1191
1219
|
totalWidth += glyph.ax;
|
|
1192
|
-
|
|
1193
|
-
const isLastChar = index === glyphInfos.length - 1;
|
|
1194
|
-
const isSingleSpace = text === ' ' || text === ' ' || /^\s+$/.test(text);
|
|
1195
|
-
if (letterSpacingInFontUnits !== 0 && (!isLastChar || isSingleSpace)) {
|
|
1220
|
+
if (letterSpacingInFontUnits !== 0) {
|
|
1196
1221
|
totalWidth += letterSpacingInFontUnits;
|
|
1197
1222
|
}
|
|
1198
1223
|
});
|
|
@@ -1247,7 +1272,7 @@ class TextLayout {
|
|
|
1247
1272
|
originalEnd: currentIndex + line.length - 1,
|
|
1248
1273
|
xOffset: 0
|
|
1249
1274
|
});
|
|
1250
|
-
currentIndex += line.length + 1;
|
|
1275
|
+
currentIndex += line.length + 1;
|
|
1251
1276
|
}
|
|
1252
1277
|
}
|
|
1253
1278
|
return { lines };
|
|
@@ -1268,7 +1293,7 @@ class TextLayout {
|
|
|
1268
1293
|
offset = width - planeBounds.max.x;
|
|
1269
1294
|
}
|
|
1270
1295
|
if (offset !== 0) {
|
|
1271
|
-
// Translate vertices
|
|
1296
|
+
// Translate vertices
|
|
1272
1297
|
for (let i = 0; i < vertices.length; i += 3) {
|
|
1273
1298
|
vertices[i] += offset;
|
|
1274
1299
|
}
|
|
@@ -1286,6 +1311,17 @@ const FONT_SIGNATURE_OPEN_TYPE_CFF = 0x4f54544f; // 'OTTO'
|
|
|
1286
1311
|
const FONT_SIGNATURE_TRUE_TYPE_COLLECTION = 0x74746366; // 'ttcf'
|
|
1287
1312
|
const FONT_SIGNATURE_WOFF = 0x774f4646; // 'wOFF'
|
|
1288
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'
|
|
1289
1325
|
|
|
1290
1326
|
class FontMetadataExtractor {
|
|
1291
1327
|
static extractMetadata(fontBuffer) {
|
|
@@ -1302,7 +1338,6 @@ class FontMetadataExtractor {
|
|
|
1302
1338
|
if (!validSignatures.includes(sfntVersion)) {
|
|
1303
1339
|
throw new Error(`Invalid font format. Expected TrueType or OpenType, got signature: 0x${sfntVersion.toString(16)}`);
|
|
1304
1340
|
}
|
|
1305
|
-
const buffer = new Uint8Array(fontBuffer);
|
|
1306
1341
|
const numTables = view.getUint16(4); // OpenType header - number of tables is at offset 4
|
|
1307
1342
|
let isCFF = false;
|
|
1308
1343
|
let headTableOffset = 0;
|
|
@@ -1312,30 +1347,28 @@ class FontMetadataExtractor {
|
|
|
1312
1347
|
let nameTableOffset = 0;
|
|
1313
1348
|
let fvarTableOffset = 0;
|
|
1314
1349
|
for (let i = 0; i < numTables; i++) {
|
|
1315
|
-
const
|
|
1316
|
-
|
|
1350
|
+
const offset = 12 + i * 16;
|
|
1351
|
+
const tag = view.getUint32(offset);
|
|
1352
|
+
if (tag === TABLE_TAG_CFF || tag === TABLE_TAG_CFF2) {
|
|
1317
1353
|
isCFF = true;
|
|
1318
1354
|
}
|
|
1319
|
-
else if (tag ===
|
|
1320
|
-
|
|
1355
|
+
else if (tag === TABLE_TAG_HEAD) {
|
|
1356
|
+
headTableOffset = view.getUint32(offset + 8);
|
|
1321
1357
|
}
|
|
1322
|
-
if (tag ===
|
|
1323
|
-
|
|
1358
|
+
else if (tag === TABLE_TAG_HHEA) {
|
|
1359
|
+
hheaTableOffset = view.getUint32(offset + 8);
|
|
1324
1360
|
}
|
|
1325
|
-
if (tag ===
|
|
1326
|
-
|
|
1361
|
+
else if (tag === TABLE_TAG_OS2) {
|
|
1362
|
+
os2TableOffset = view.getUint32(offset + 8);
|
|
1327
1363
|
}
|
|
1328
|
-
if (tag ===
|
|
1329
|
-
|
|
1364
|
+
else if (tag === TABLE_TAG_FVAR) {
|
|
1365
|
+
fvarTableOffset = view.getUint32(offset + 8);
|
|
1330
1366
|
}
|
|
1331
|
-
if (tag ===
|
|
1332
|
-
|
|
1367
|
+
else if (tag === TABLE_TAG_STAT) {
|
|
1368
|
+
statTableOffset = view.getUint32(offset + 8);
|
|
1333
1369
|
}
|
|
1334
|
-
if (tag ===
|
|
1335
|
-
|
|
1336
|
-
}
|
|
1337
|
-
if (tag === 'name') {
|
|
1338
|
-
nameTableOffset = view.getUint32(12 + i * 16 + 8);
|
|
1370
|
+
else if (tag === TABLE_TAG_NAME) {
|
|
1371
|
+
nameTableOffset = view.getUint32(offset + 8);
|
|
1339
1372
|
}
|
|
1340
1373
|
}
|
|
1341
1374
|
const unitsPerEm = headTableOffset
|
|
@@ -1378,6 +1411,88 @@ class FontMetadataExtractor {
|
|
|
1378
1411
|
axisNames
|
|
1379
1412
|
};
|
|
1380
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
|
+
}
|
|
1381
1496
|
static extractAxisNames(view, statOffset, nameOffset) {
|
|
1382
1497
|
try {
|
|
1383
1498
|
// STAT table structure
|
|
@@ -1588,7 +1703,7 @@ class WoffConverter {
|
|
|
1588
1703
|
const padding = (4 - (table.origLength % 4)) % 4;
|
|
1589
1704
|
sfntOffset += padding;
|
|
1590
1705
|
}
|
|
1591
|
-
|
|
1706
|
+
logger.log('WOFF font decompressed successfully');
|
|
1592
1707
|
return sfntData.buffer.slice(0, sfntOffset);
|
|
1593
1708
|
}
|
|
1594
1709
|
static async decompressZlib(compressedData) {
|
|
@@ -1617,7 +1732,7 @@ class FontLoader {
|
|
|
1617
1732
|
// Check if this is a WOFF font and decompress if needed
|
|
1618
1733
|
const format = WoffConverter.detectFormat(fontBuffer);
|
|
1619
1734
|
if (format === 'woff') {
|
|
1620
|
-
|
|
1735
|
+
logger.log('WOFF font detected, decompressing...');
|
|
1621
1736
|
fontBuffer = await WoffConverter.decompressWoff(fontBuffer);
|
|
1622
1737
|
}
|
|
1623
1738
|
else if (format === 'woff2') {
|
|
@@ -1655,6 +1770,7 @@ class FontLoader {
|
|
|
1655
1770
|
};
|
|
1656
1771
|
}
|
|
1657
1772
|
}
|
|
1773
|
+
const featureData = FontMetadataExtractor.extractFeatureTags(fontBuffer);
|
|
1658
1774
|
return {
|
|
1659
1775
|
hb,
|
|
1660
1776
|
fontBlob,
|
|
@@ -1665,11 +1781,13 @@ class FontLoader {
|
|
|
1665
1781
|
metrics,
|
|
1666
1782
|
fontVariations,
|
|
1667
1783
|
isVariable,
|
|
1668
|
-
variationAxes
|
|
1784
|
+
variationAxes,
|
|
1785
|
+
availableFeatures: featureData?.tags,
|
|
1786
|
+
featureNames: featureData?.names
|
|
1669
1787
|
};
|
|
1670
1788
|
}
|
|
1671
1789
|
catch (error) {
|
|
1672
|
-
|
|
1790
|
+
logger.error('Failed to load font:', error);
|
|
1673
1791
|
throw error;
|
|
1674
1792
|
}
|
|
1675
1793
|
finally {
|
|
@@ -1690,7 +1808,7 @@ class FontLoader {
|
|
|
1690
1808
|
}
|
|
1691
1809
|
}
|
|
1692
1810
|
catch (error) {
|
|
1693
|
-
|
|
1811
|
+
logger.error('Error destroying font resources:', error);
|
|
1694
1812
|
}
|
|
1695
1813
|
}
|
|
1696
1814
|
}
|
|
@@ -2092,7 +2210,7 @@ class Tessellator {
|
|
|
2092
2210
|
if (valid.length === 0) {
|
|
2093
2211
|
return { triangles: { vertices: [], indices: [] }, contours: [] };
|
|
2094
2212
|
}
|
|
2095
|
-
|
|
2213
|
+
logger.log(`Tessellator: removeOverlaps=${removeOverlaps}, processing ${valid.length} paths`);
|
|
2096
2214
|
return this.tessellate(valid, removeOverlaps, isCFF);
|
|
2097
2215
|
}
|
|
2098
2216
|
tessellate(paths, removeOverlaps, isCFF) {
|
|
@@ -2102,19 +2220,19 @@ class Tessellator {
|
|
|
2102
2220
|
: paths;
|
|
2103
2221
|
let contours = this.pathsToContours(normalizedPaths);
|
|
2104
2222
|
if (removeOverlaps) {
|
|
2105
|
-
|
|
2223
|
+
logger.log('Two-pass: boundary extraction then triangulation');
|
|
2106
2224
|
// Extract boundaries to remove overlaps
|
|
2107
2225
|
const boundaryResult = this.performTessellation(contours, 'boundary');
|
|
2108
2226
|
if (!boundaryResult) {
|
|
2109
|
-
|
|
2227
|
+
logger.warn('libtess returned empty result from boundary pass');
|
|
2110
2228
|
return { triangles: { vertices: [], indices: [] }, contours: [] };
|
|
2111
2229
|
}
|
|
2112
2230
|
// Convert boundary elements back to contours
|
|
2113
2231
|
contours = this.boundaryToContours(boundaryResult);
|
|
2114
|
-
|
|
2232
|
+
logger.log(`Boundary pass created ${contours.length} contours. Starting triangulation pass.`);
|
|
2115
2233
|
}
|
|
2116
2234
|
else {
|
|
2117
|
-
|
|
2235
|
+
logger.log(`Single-pass triangulation for ${isCFF ? 'CFF' : 'TTF'}`);
|
|
2118
2236
|
}
|
|
2119
2237
|
// Triangulate the contours
|
|
2120
2238
|
const triangleResult = this.performTessellation(contours, 'triangles');
|
|
@@ -2122,7 +2240,7 @@ class Tessellator {
|
|
|
2122
2240
|
const warning = removeOverlaps
|
|
2123
2241
|
? 'libtess returned empty result from triangulation pass'
|
|
2124
2242
|
: 'libtess returned empty result from single-pass triangulation';
|
|
2125
|
-
|
|
2243
|
+
logger.warn(warning);
|
|
2126
2244
|
return { triangles: { vertices: [], indices: [] }, contours };
|
|
2127
2245
|
}
|
|
2128
2246
|
return {
|
|
@@ -2177,7 +2295,7 @@ class Tessellator {
|
|
|
2177
2295
|
return idx;
|
|
2178
2296
|
});
|
|
2179
2297
|
tess.gluTessCallback(libtess_minExports.gluEnum.GLU_TESS_ERROR, (errno) => {
|
|
2180
|
-
|
|
2298
|
+
logger.warn(`libtess error: ${errno}`);
|
|
2181
2299
|
});
|
|
2182
2300
|
tess.gluTessNormal(0, 0, 1);
|
|
2183
2301
|
tess.gluTessBeginPolygon(null);
|
|
@@ -3199,7 +3317,7 @@ class DrawCallbackHandler {
|
|
|
3199
3317
|
}
|
|
3200
3318
|
}
|
|
3201
3319
|
catch (error) {
|
|
3202
|
-
|
|
3320
|
+
logger.warn('Error destroying draw callbacks:', error);
|
|
3203
3321
|
}
|
|
3204
3322
|
this.collector = undefined;
|
|
3205
3323
|
}
|
|
@@ -3489,7 +3607,8 @@ class TextShaper {
|
|
|
3489
3607
|
}
|
|
3490
3608
|
buffer.addText(lineInfo.text);
|
|
3491
3609
|
buffer.guessSegmentProperties();
|
|
3492
|
-
|
|
3610
|
+
const featuresString = convertFontFeaturesToString(this.loadedFont.fontFeatures);
|
|
3611
|
+
this.loadedFont.hb.shape(this.loadedFont.font, buffer, featuresString);
|
|
3493
3612
|
const glyphInfos = buffer.json(this.loadedFont.font);
|
|
3494
3613
|
buffer.destroy();
|
|
3495
3614
|
const clusters = [];
|
|
@@ -3564,15 +3683,14 @@ class TextShaper {
|
|
|
3564
3683
|
naturalSpaceWidth = TextMeasurer.measureTextWidth(this.loadedFont, ' ', letterSpacing);
|
|
3565
3684
|
this.cachedSpaceWidth.set(letterSpacing, naturalSpaceWidth);
|
|
3566
3685
|
}
|
|
3686
|
+
const width = naturalSpaceWidth;
|
|
3567
3687
|
const stretchFactor = SPACE_STRETCH_RATIO;
|
|
3568
3688
|
const shrinkFactor = SPACE_SHRINK_RATIO;
|
|
3569
3689
|
if (lineInfo.adjustmentRatio > 0) {
|
|
3570
|
-
spaceAdjustment =
|
|
3571
|
-
lineInfo.adjustmentRatio * naturalSpaceWidth * stretchFactor;
|
|
3690
|
+
spaceAdjustment = lineInfo.adjustmentRatio * width * stretchFactor;
|
|
3572
3691
|
}
|
|
3573
3692
|
else if (lineInfo.adjustmentRatio < 0) {
|
|
3574
|
-
spaceAdjustment =
|
|
3575
|
-
lineInfo.adjustmentRatio * naturalSpaceWidth * shrinkFactor;
|
|
3693
|
+
spaceAdjustment = lineInfo.adjustmentRatio * width * shrinkFactor;
|
|
3576
3694
|
}
|
|
3577
3695
|
}
|
|
3578
3696
|
return spaceAdjustment;
|
|
@@ -4497,12 +4615,16 @@ class Text {
|
|
|
4497
4615
|
const baseFontKey = typeof options.font === 'string'
|
|
4498
4616
|
? options.font
|
|
4499
4617
|
: `buffer-${Text.generateFontContentHash(options.font)}`;
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
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
|
+
}
|
|
4503
4625
|
let loadedFont = Text.fontCache.get(fontKey);
|
|
4504
4626
|
if (!loadedFont) {
|
|
4505
|
-
loadedFont = await Text.loadAndCacheFont(fontKey, options.font, options.fontVariations);
|
|
4627
|
+
loadedFont = await Text.loadAndCacheFont(fontKey, options.font, options.fontVariations, options.fontFeatures);
|
|
4506
4628
|
}
|
|
4507
4629
|
const text = new Text({ maxCacheSizeMB: options.maxCacheSizeMB });
|
|
4508
4630
|
text.setLoadedFont(loadedFont);
|
|
@@ -4516,12 +4638,11 @@ class Text {
|
|
|
4516
4638
|
measureTextWidth: (textString, letterSpacing) => text.measureTextWidth(textString, letterSpacing)
|
|
4517
4639
|
};
|
|
4518
4640
|
}
|
|
4519
|
-
static async loadAndCacheFont(fontKey, font, fontVariations) {
|
|
4641
|
+
static async loadAndCacheFont(fontKey, font, fontVariations, fontFeatures) {
|
|
4520
4642
|
const tempText = new Text();
|
|
4521
|
-
await tempText.loadFont(font, fontVariations);
|
|
4643
|
+
await tempText.loadFont(font, fontVariations, fontFeatures);
|
|
4522
4644
|
const loadedFont = tempText.getLoadedFont();
|
|
4523
4645
|
Text.fontCache.set(fontKey, loadedFont);
|
|
4524
|
-
// Don't destroy tempText - the cached font references its HarfBuzz objects
|
|
4525
4646
|
return loadedFont;
|
|
4526
4647
|
}
|
|
4527
4648
|
static generateFontContentHash(buffer) {
|
|
@@ -4540,10 +4661,13 @@ class Text {
|
|
|
4540
4661
|
const contentHash = Text.generateFontContentHash(loadedFont._buffer);
|
|
4541
4662
|
this.currentFontId = `font_${contentHash}`;
|
|
4542
4663
|
if (loadedFont.fontVariations) {
|
|
4543
|
-
this.currentFontId += `
|
|
4664
|
+
this.currentFontId += `_var_${JSON.stringify(loadedFont.fontVariations)}`;
|
|
4665
|
+
}
|
|
4666
|
+
if (loadedFont.fontFeatures) {
|
|
4667
|
+
this.currentFontId += `_feat_${JSON.stringify(loadedFont.fontFeatures)}`;
|
|
4544
4668
|
}
|
|
4545
4669
|
}
|
|
4546
|
-
async loadFont(fontSrc, fontVariations) {
|
|
4670
|
+
async loadFont(fontSrc, fontVariations, fontFeatures) {
|
|
4547
4671
|
perfLogger.start('Text.loadFont', {
|
|
4548
4672
|
fontSrc: typeof fontSrc === 'string' ? fontSrc : `buffer(${fontSrc.byteLength})`
|
|
4549
4673
|
});
|
|
@@ -4564,14 +4688,20 @@ class Text {
|
|
|
4564
4688
|
this.destroy();
|
|
4565
4689
|
}
|
|
4566
4690
|
this.loadedFont = await this.fontLoader.loadFont(fontBuffer, fontVariations);
|
|
4691
|
+
if (fontFeatures) {
|
|
4692
|
+
this.loadedFont.fontFeatures = fontFeatures;
|
|
4693
|
+
}
|
|
4567
4694
|
const contentHash = Text.generateFontContentHash(fontBuffer);
|
|
4568
4695
|
this.currentFontId = `font_${contentHash}`;
|
|
4569
4696
|
if (fontVariations) {
|
|
4570
|
-
this.currentFontId += `
|
|
4697
|
+
this.currentFontId += `_var_${JSON.stringify(fontVariations)}`;
|
|
4698
|
+
}
|
|
4699
|
+
if (fontFeatures) {
|
|
4700
|
+
this.currentFontId += `_feat_${JSON.stringify(fontFeatures)}`;
|
|
4571
4701
|
}
|
|
4572
4702
|
}
|
|
4573
4703
|
catch (error) {
|
|
4574
|
-
|
|
4704
|
+
logger.error('Failed to load font:', error);
|
|
4575
4705
|
throw error;
|
|
4576
4706
|
}
|
|
4577
4707
|
finally {
|
|
@@ -4646,7 +4776,7 @@ class Text {
|
|
|
4646
4776
|
};
|
|
4647
4777
|
}
|
|
4648
4778
|
catch (error) {
|
|
4649
|
-
|
|
4779
|
+
logger.warn(`Failed to load patterns for ${language}: ${error}`);
|
|
4650
4780
|
return {
|
|
4651
4781
|
...options,
|
|
4652
4782
|
layout: {
|
|
@@ -4910,7 +5040,7 @@ class Text {
|
|
|
4910
5040
|
Text.patternCache.set(language, pattern);
|
|
4911
5041
|
}
|
|
4912
5042
|
catch (error) {
|
|
4913
|
-
|
|
5043
|
+
logger.warn(`Failed to pre-load patterns for ${language}: ${error}`);
|
|
4914
5044
|
}
|
|
4915
5045
|
}
|
|
4916
5046
|
}));
|
|
@@ -4972,7 +5102,7 @@ class Text {
|
|
|
4972
5102
|
FontLoader.destroyFont(currentFont);
|
|
4973
5103
|
}
|
|
4974
5104
|
catch (error) {
|
|
4975
|
-
|
|
5105
|
+
logger.warn('Error destroying HarfBuzz objects:', error);
|
|
4976
5106
|
}
|
|
4977
5107
|
finally {
|
|
4978
5108
|
this.loadedFont = undefined;
|