three-text 0.2.12 → 0.2.14
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 +4 -4
- package/dist/index.cjs +904 -723
- package/dist/index.d.ts +88 -76
- package/dist/index.js +904 -724
- package/dist/index.min.cjs +861 -2
- package/dist/index.min.js +860 -2
- package/dist/index.umd.js +904 -723
- package/dist/index.umd.min.js +862 -2
- package/dist/three/react.cjs +44 -20
- package/dist/three/react.d.ts +36 -24
- package/dist/three/react.js +45 -21
- package/dist/types/core/Text.d.ts +14 -7
- package/dist/types/core/cache/GlyphGeometryBuilder.d.ts +14 -5
- package/dist/types/core/cache/sharedCaches.d.ts +12 -0
- package/dist/types/core/font/TableDirectory.d.ts +7 -0
- package/dist/types/core/font/constants.d.ts +0 -1
- package/dist/types/core/geometry/Extruder.d.ts +3 -6
- package/dist/types/core/layout/LineBreak.d.ts +2 -0
- package/dist/types/core/shaping/DrawCallbacks.d.ts +2 -0
- package/dist/types/core/shaping/TextShaper.d.ts +4 -1
- package/dist/types/core/types.d.ts +26 -0
- package/dist/types/index.d.ts +3 -3
- package/dist/types/three/ThreeText.d.ts +3 -3
- package/dist/types/utils/LRUCache.d.ts +2 -2
- package/dist/types/webgpu/index.d.ts +1 -2
- package/dist/webgpu/index.cjs +3 -4
- package/dist/webgpu/index.d.ts +1 -2
- package/dist/webgpu/index.js +3 -4
- package/package.json +1 -1
- package/dist/types/core/cache/GlyphCache.d.ts +0 -50
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* three-text v0.2.
|
|
2
|
+
* three-text v0.2.14
|
|
3
3
|
* Copyright (C) 2025 Countertype LLC
|
|
4
4
|
*
|
|
5
5
|
* This program is free software: you can redistribute it and/or modify
|
|
@@ -45,7 +45,9 @@ class PerformanceLogger {
|
|
|
45
45
|
if (!isLogEnabled)
|
|
46
46
|
return;
|
|
47
47
|
const startTime = performance.now();
|
|
48
|
-
|
|
48
|
+
// Generate unique key for nested timing support
|
|
49
|
+
const timerKey = `${name}_${startTime}`;
|
|
50
|
+
this.activeTimers.set(timerKey, startTime);
|
|
49
51
|
this.metrics.push({
|
|
50
52
|
name,
|
|
51
53
|
startTime,
|
|
@@ -57,17 +59,26 @@ class PerformanceLogger {
|
|
|
57
59
|
if (!isLogEnabled)
|
|
58
60
|
return null;
|
|
59
61
|
const endTime = performance.now();
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
// Find the most recent matching timer by scanning backwards
|
|
63
|
+
let timerKey;
|
|
64
|
+
let startTime;
|
|
65
|
+
for (const [key, time] of Array.from(this.activeTimers.entries()).reverse()) {
|
|
66
|
+
if (key.startsWith(`${name}_`)) {
|
|
67
|
+
timerKey = key;
|
|
68
|
+
startTime = time;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (startTime === undefined || !timerKey) {
|
|
62
73
|
logger.warn(`Performance timer "${name}" was not started`);
|
|
63
74
|
return null;
|
|
64
75
|
}
|
|
65
76
|
const duration = endTime - startTime;
|
|
66
|
-
this.activeTimers.delete(
|
|
77
|
+
this.activeTimers.delete(timerKey);
|
|
67
78
|
// Find the metric in reverse order (most recent first)
|
|
68
79
|
for (let i = this.metrics.length - 1; i >= 0; i--) {
|
|
69
80
|
const metric = this.metrics[i];
|
|
70
|
-
if (metric.name === name && !metric.endTime) {
|
|
81
|
+
if (metric.name === name && metric.startTime === startTime && !metric.endTime) {
|
|
71
82
|
metric.endTime = endTime;
|
|
72
83
|
metric.duration = duration;
|
|
73
84
|
break;
|
|
@@ -710,7 +721,7 @@ class LineBreak {
|
|
|
710
721
|
align: options.align || 'left',
|
|
711
722
|
hyphenate: options.hyphenate || false
|
|
712
723
|
});
|
|
713
|
-
const { text, width, align = 'left', direction = 'ltr', hyphenate = false, language = 'en-us', respectExistingBreaks = true, measureText, hyphenationPatterns, unitsPerEm, tolerance = DEFAULT_TOLERANCE, pretolerance = DEFAULT_PRETOLERANCE, emergencyStretch = DEFAULT_EMERGENCY_STRETCH, autoEmergencyStretch, lefthyphenmin = DEFAULT_LEFT_HYPHEN_MIN, righthyphenmin = DEFAULT_RIGHT_HYPHEN_MIN, linepenalty = DEFAULT_LINE_PENALTY, adjdemerits = DEFAULT_FITNESS_DIFF_DEMERITS, hyphenpenalty = DEFAULT_HYPHEN_PENALTY, exhyphenpenalty = DEFAULT_EX_HYPHEN_PENALTY, doublehyphendemerits = DEFAULT_DOUBLE_HYPHEN_DEMERITS, looseness = 0, disableShortLineDetection = false, shortLineThreshold = SHORT_LINE_WIDTH_THRESHOLD } = options;
|
|
724
|
+
const { text, width, align = 'left', direction = 'ltr', hyphenate = false, language = 'en-us', respectExistingBreaks = true, measureText, hyphenationPatterns, unitsPerEm, letterSpacing = 0, tolerance = DEFAULT_TOLERANCE, pretolerance = DEFAULT_PRETOLERANCE, emergencyStretch = DEFAULT_EMERGENCY_STRETCH, autoEmergencyStretch, lefthyphenmin = DEFAULT_LEFT_HYPHEN_MIN, righthyphenmin = DEFAULT_RIGHT_HYPHEN_MIN, linepenalty = DEFAULT_LINE_PENALTY, adjdemerits = DEFAULT_FITNESS_DIFF_DEMERITS, hyphenpenalty = DEFAULT_HYPHEN_PENALTY, exhyphenpenalty = DEFAULT_EX_HYPHEN_PENALTY, doublehyphendemerits = DEFAULT_DOUBLE_HYPHEN_DEMERITS, looseness = 0, disableShortLineDetection = false, shortLineThreshold = SHORT_LINE_WIDTH_THRESHOLD } = options;
|
|
714
725
|
// Handle multiple paragraphs by processing each independently
|
|
715
726
|
if (respectExistingBreaks && text.includes('\n')) {
|
|
716
727
|
const paragraphs = text.split('\n');
|
|
@@ -772,7 +783,11 @@ class LineBreak {
|
|
|
772
783
|
hyphenPenalty: hyphenpenalty,
|
|
773
784
|
exHyphenPenalty: exhyphenpenalty,
|
|
774
785
|
currentAlign: align,
|
|
775
|
-
unitsPerEm
|
|
786
|
+
unitsPerEm,
|
|
787
|
+
// measureText() includes trailing letter spacing after the final glyph of a token.
|
|
788
|
+
// Shaping applies letter spacing only between glyphs, so we subtract one
|
|
789
|
+
// trailing letterSpacingFU per line segment (see computeAdjustmentRatio/createLines).
|
|
790
|
+
letterSpacingFU: unitsPerEm ? letterSpacing * unitsPerEm : 0
|
|
776
791
|
};
|
|
777
792
|
if (!width || width === Infinity) {
|
|
778
793
|
const measuredWidth = measureText(text);
|
|
@@ -1080,7 +1095,7 @@ class LineBreak {
|
|
|
1080
1095
|
}
|
|
1081
1096
|
}
|
|
1082
1097
|
}
|
|
1083
|
-
static computeAdjustmentRatio(items, lineStart, lineEnd, _lineNumber, lineWidth, cumulativeWidths,
|
|
1098
|
+
static computeAdjustmentRatio(items, lineStart, lineEnd, _lineNumber, lineWidth, cumulativeWidths, context) {
|
|
1084
1099
|
let totalWidth = 0;
|
|
1085
1100
|
let totalStretch = 0;
|
|
1086
1101
|
let totalShrink = 0;
|
|
@@ -1123,6 +1138,12 @@ class LineBreak {
|
|
|
1123
1138
|
? items[lineEnd].width
|
|
1124
1139
|
: items[lineEnd].preBreakWidth;
|
|
1125
1140
|
}
|
|
1141
|
+
// Correct for trailing letter spacing at the end of the line segment.
|
|
1142
|
+
// Our token measurement includes letter spacing after the final glyph;
|
|
1143
|
+
// shaping does not add letter spacing after the final glyph in a line.
|
|
1144
|
+
if (context?.letterSpacingFU && totalWidth !== 0) {
|
|
1145
|
+
totalWidth -= context.letterSpacingFU;
|
|
1146
|
+
}
|
|
1126
1147
|
const adjustment = lineWidth - totalWidth;
|
|
1127
1148
|
let ratio;
|
|
1128
1149
|
if (adjustment > 0 && totalStretch > 0) {
|
|
@@ -1285,6 +1306,10 @@ class LineBreak {
|
|
|
1285
1306
|
}
|
|
1286
1307
|
}
|
|
1287
1308
|
const lineText = lineTextParts.join('');
|
|
1309
|
+
// Correct for trailing letter spacing at the end of the line.
|
|
1310
|
+
if (context?.letterSpacingFU && naturalWidth !== 0) {
|
|
1311
|
+
naturalWidth -= context.letterSpacingFU;
|
|
1312
|
+
}
|
|
1288
1313
|
let xOffset = 0;
|
|
1289
1314
|
let adjustmentRatio = 0;
|
|
1290
1315
|
let effectiveAlign = align;
|
|
@@ -1338,6 +1363,10 @@ class LineBreak {
|
|
|
1338
1363
|
finalNaturalWidth += item.width;
|
|
1339
1364
|
}
|
|
1340
1365
|
const finalLineText = finalLineTextParts.join('');
|
|
1366
|
+
// Correct for trailing letter spacing at the end of the final line.
|
|
1367
|
+
if (context?.letterSpacingFU && finalNaturalWidth !== 0) {
|
|
1368
|
+
finalNaturalWidth -= context.letterSpacingFU;
|
|
1369
|
+
}
|
|
1341
1370
|
let finalXOffset = 0;
|
|
1342
1371
|
let finalEffectiveAlign = align;
|
|
1343
1372
|
if (align === 'justify') {
|
|
@@ -1399,9 +1428,6 @@ function convertFontFeaturesToString(features) {
|
|
|
1399
1428
|
}
|
|
1400
1429
|
|
|
1401
1430
|
class TextMeasurer {
|
|
1402
|
-
// Measures text width including letter spacing
|
|
1403
|
-
// (letter spacing is added uniformly after each glyph during measurement,
|
|
1404
|
-
// so the widths given to the line-breaking algorithm already account for tracking)
|
|
1405
1431
|
static measureTextWidth(loadedFont, text, letterSpacing = 0) {
|
|
1406
1432
|
const buffer = loadedFont.hb.createBuffer();
|
|
1407
1433
|
buffer.addText(text);
|
|
@@ -1410,7 +1436,6 @@ class TextMeasurer {
|
|
|
1410
1436
|
loadedFont.hb.shape(loadedFont.font, buffer, featuresString);
|
|
1411
1437
|
const glyphInfos = buffer.json(loadedFont.font);
|
|
1412
1438
|
const letterSpacingInFontUnits = letterSpacing * loadedFont.upem;
|
|
1413
|
-
// Calculate total advance width with letter spacing
|
|
1414
1439
|
let totalWidth = 0;
|
|
1415
1440
|
glyphInfos.forEach((glyph) => {
|
|
1416
1441
|
totalWidth += glyph.ax;
|
|
@@ -1457,6 +1482,7 @@ class TextLayout {
|
|
|
1457
1482
|
disableShortLineDetection,
|
|
1458
1483
|
shortLineThreshold,
|
|
1459
1484
|
unitsPerEm: this.loadedFont.upem,
|
|
1485
|
+
letterSpacing,
|
|
1460
1486
|
measureText: (textToMeasure) => TextMeasurer.measureTextWidth(this.loadedFont, textToMeasure, letterSpacing // Letter spacing included in width measurements
|
|
1461
1487
|
)
|
|
1462
1488
|
});
|
|
@@ -1467,10 +1493,11 @@ class TextLayout {
|
|
|
1467
1493
|
lines = [];
|
|
1468
1494
|
let currentIndex = 0;
|
|
1469
1495
|
for (const line of linesArray) {
|
|
1496
|
+
const originalEnd = line.length === 0 ? currentIndex : currentIndex + line.length - 1;
|
|
1470
1497
|
lines.push({
|
|
1471
1498
|
text: line,
|
|
1472
1499
|
originalStart: currentIndex,
|
|
1473
|
-
originalEnd
|
|
1500
|
+
originalEnd,
|
|
1474
1501
|
xOffset: 0
|
|
1475
1502
|
});
|
|
1476
1503
|
currentIndex += line.length + 1;
|
|
@@ -1509,7 +1536,6 @@ class TextLayout {
|
|
|
1509
1536
|
// Font file signature constants
|
|
1510
1537
|
const FONT_SIGNATURE_TRUE_TYPE = 0x00010000;
|
|
1511
1538
|
const FONT_SIGNATURE_OPEN_TYPE_CFF = 0x4f54544f; // 'OTTO'
|
|
1512
|
-
const FONT_SIGNATURE_TRUE_TYPE_COLLECTION = 0x74746366; // 'ttcf'
|
|
1513
1539
|
const FONT_SIGNATURE_WOFF = 0x774f4646; // 'wOFF'
|
|
1514
1540
|
const FONT_SIGNATURE_WOFF2 = 0x774f4632; // 'wOF2'
|
|
1515
1541
|
// Table Tags
|
|
@@ -1524,6 +1550,28 @@ const TABLE_TAG_CFF2 = 0x43464632; // 'CFF2'
|
|
|
1524
1550
|
const TABLE_TAG_GSUB = 0x47535542; // 'GSUB'
|
|
1525
1551
|
const TABLE_TAG_GPOS = 0x47504f53; // 'GPOS'
|
|
1526
1552
|
|
|
1553
|
+
// Parses the SFNT table directory for TTF/OTF fonts
|
|
1554
|
+
// Assumes the DataView is positioned at the start of an sfnt font (offset 0)
|
|
1555
|
+
// Table records are 16 bytes each starting at byte offset 12
|
|
1556
|
+
function parseTableDirectory(view) {
|
|
1557
|
+
const numTables = view.getUint16(4);
|
|
1558
|
+
const tableRecordsStart = 12;
|
|
1559
|
+
const tables = new Map();
|
|
1560
|
+
for (let i = 0; i < numTables; i++) {
|
|
1561
|
+
const recordOffset = tableRecordsStart + i * 16;
|
|
1562
|
+
// Guard against corrupt buffers that report more tables than exist
|
|
1563
|
+
if (recordOffset + 16 > view.byteLength) {
|
|
1564
|
+
break;
|
|
1565
|
+
}
|
|
1566
|
+
const tag = view.getUint32(recordOffset);
|
|
1567
|
+
const checksum = view.getUint32(recordOffset + 4);
|
|
1568
|
+
const offset = view.getUint32(recordOffset + 8);
|
|
1569
|
+
const length = view.getUint32(recordOffset + 12);
|
|
1570
|
+
tables.set(tag, { tag, checksum, offset, length });
|
|
1571
|
+
}
|
|
1572
|
+
return tables;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1527
1575
|
class FontMetadataExtractor {
|
|
1528
1576
|
static extractMetadata(fontBuffer) {
|
|
1529
1577
|
if (!fontBuffer || fontBuffer.byteLength < 12) {
|
|
@@ -1533,45 +1581,19 @@ class FontMetadataExtractor {
|
|
|
1533
1581
|
const sfntVersion = view.getUint32(0);
|
|
1534
1582
|
const validSignatures = [
|
|
1535
1583
|
FONT_SIGNATURE_TRUE_TYPE,
|
|
1536
|
-
FONT_SIGNATURE_OPEN_TYPE_CFF
|
|
1537
|
-
FONT_SIGNATURE_TRUE_TYPE_COLLECTION
|
|
1584
|
+
FONT_SIGNATURE_OPEN_TYPE_CFF
|
|
1538
1585
|
];
|
|
1539
1586
|
if (!validSignatures.includes(sfntVersion)) {
|
|
1540
|
-
throw new Error(`Invalid font format. Expected
|
|
1541
|
-
}
|
|
1542
|
-
const
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
for (let i = 0; i < numTables; i++) {
|
|
1551
|
-
const offset = 12 + i * 16;
|
|
1552
|
-
const tag = view.getUint32(offset);
|
|
1553
|
-
if (tag === TABLE_TAG_CFF || tag === TABLE_TAG_CFF2) {
|
|
1554
|
-
isCFF = true;
|
|
1555
|
-
}
|
|
1556
|
-
else if (tag === TABLE_TAG_HEAD) {
|
|
1557
|
-
headTableOffset = view.getUint32(offset + 8);
|
|
1558
|
-
}
|
|
1559
|
-
else if (tag === TABLE_TAG_HHEA) {
|
|
1560
|
-
hheaTableOffset = view.getUint32(offset + 8);
|
|
1561
|
-
}
|
|
1562
|
-
else if (tag === TABLE_TAG_OS2) {
|
|
1563
|
-
os2TableOffset = view.getUint32(offset + 8);
|
|
1564
|
-
}
|
|
1565
|
-
else if (tag === TABLE_TAG_FVAR) {
|
|
1566
|
-
fvarTableOffset = view.getUint32(offset + 8);
|
|
1567
|
-
}
|
|
1568
|
-
else if (tag === TABLE_TAG_STAT) {
|
|
1569
|
-
statTableOffset = view.getUint32(offset + 8);
|
|
1570
|
-
}
|
|
1571
|
-
else if (tag === TABLE_TAG_NAME) {
|
|
1572
|
-
nameTableOffset = view.getUint32(offset + 8);
|
|
1573
|
-
}
|
|
1574
|
-
}
|
|
1587
|
+
throw new Error(`Invalid font format. Expected TTF/OTF (or WOFF), got signature: 0x${sfntVersion.toString(16)}`);
|
|
1588
|
+
}
|
|
1589
|
+
const tableDirectory = parseTableDirectory(view);
|
|
1590
|
+
const isCFF = tableDirectory.has(TABLE_TAG_CFF) || tableDirectory.has(TABLE_TAG_CFF2);
|
|
1591
|
+
const headTableOffset = tableDirectory.get(TABLE_TAG_HEAD)?.offset ?? 0;
|
|
1592
|
+
const hheaTableOffset = tableDirectory.get(TABLE_TAG_HHEA)?.offset ?? 0;
|
|
1593
|
+
const os2TableOffset = tableDirectory.get(TABLE_TAG_OS2)?.offset ?? 0;
|
|
1594
|
+
const fvarTableOffset = tableDirectory.get(TABLE_TAG_FVAR)?.offset ?? 0;
|
|
1595
|
+
const statTableOffset = tableDirectory.get(TABLE_TAG_STAT)?.offset ?? 0;
|
|
1596
|
+
const nameTableOffset = tableDirectory.get(TABLE_TAG_NAME)?.offset ?? 0;
|
|
1575
1597
|
const unitsPerEm = headTableOffset
|
|
1576
1598
|
? view.getUint16(headTableOffset + 18)
|
|
1577
1599
|
: 1000;
|
|
@@ -1614,23 +1636,10 @@ class FontMetadataExtractor {
|
|
|
1614
1636
|
}
|
|
1615
1637
|
static extractFeatureTags(fontBuffer) {
|
|
1616
1638
|
const view = new DataView(fontBuffer);
|
|
1617
|
-
const
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
for (let i = 0; i < numTables; i++) {
|
|
1622
|
-
const offset = 12 + i * 16;
|
|
1623
|
-
const tag = view.getUint32(offset);
|
|
1624
|
-
if (tag === TABLE_TAG_GSUB) {
|
|
1625
|
-
gsubTableOffset = view.getUint32(offset + 8);
|
|
1626
|
-
}
|
|
1627
|
-
else if (tag === TABLE_TAG_GPOS) {
|
|
1628
|
-
gposTableOffset = view.getUint32(offset + 8);
|
|
1629
|
-
}
|
|
1630
|
-
else if (tag === TABLE_TAG_NAME) {
|
|
1631
|
-
nameTableOffset = view.getUint32(offset + 8);
|
|
1632
|
-
}
|
|
1633
|
-
}
|
|
1639
|
+
const tableDirectory = parseTableDirectory(view);
|
|
1640
|
+
const gsubTableOffset = tableDirectory.get(TABLE_TAG_GSUB)?.offset ?? 0;
|
|
1641
|
+
const gposTableOffset = tableDirectory.get(TABLE_TAG_GPOS)?.offset ?? 0;
|
|
1642
|
+
const nameTableOffset = tableDirectory.get(TABLE_TAG_NAME)?.offset ?? 0;
|
|
1634
1643
|
const features = new Set();
|
|
1635
1644
|
const featureNames = {};
|
|
1636
1645
|
try {
|
|
@@ -1861,6 +1870,7 @@ class WoffConverter {
|
|
|
1861
1870
|
sfntView.setUint16(6, searchRange);
|
|
1862
1871
|
sfntView.setUint16(8, Math.floor(Math.log2(numTables)));
|
|
1863
1872
|
sfntView.setUint16(10, numTables * 16 - searchRange);
|
|
1873
|
+
// Read and decompress table directory
|
|
1864
1874
|
let sfntOffset = 12 + numTables * 16; // Start of table data
|
|
1865
1875
|
const tableDirectory = [];
|
|
1866
1876
|
// Read WOFF table directory
|
|
@@ -1943,11 +1953,10 @@ class FontLoader {
|
|
|
1943
1953
|
const sfntVersion = view.getUint32(0);
|
|
1944
1954
|
const validSignatures = [
|
|
1945
1955
|
FONT_SIGNATURE_TRUE_TYPE,
|
|
1946
|
-
FONT_SIGNATURE_OPEN_TYPE_CFF
|
|
1947
|
-
FONT_SIGNATURE_TRUE_TYPE_COLLECTION
|
|
1956
|
+
FONT_SIGNATURE_OPEN_TYPE_CFF
|
|
1948
1957
|
];
|
|
1949
1958
|
if (!validSignatures.includes(sfntVersion)) {
|
|
1950
|
-
throw new Error(`Invalid font format. Expected
|
|
1959
|
+
throw new Error(`Invalid font format. Expected TTF/OTF (or WOFF), got signature: 0x${sfntVersion.toString(16)}`);
|
|
1951
1960
|
}
|
|
1952
1961
|
const { hb, module } = await this.getHarfBuzzInstance();
|
|
1953
1962
|
try {
|
|
@@ -1984,7 +1993,8 @@ class FontLoader {
|
|
|
1984
1993
|
isVariable,
|
|
1985
1994
|
variationAxes,
|
|
1986
1995
|
availableFeatures: featureData?.tags,
|
|
1987
|
-
featureNames: featureData?.names
|
|
1996
|
+
featureNames: featureData?.names,
|
|
1997
|
+
_buffer: fontBuffer // For stable font ID generation
|
|
1988
1998
|
};
|
|
1989
1999
|
}
|
|
1990
2000
|
catch (error) {
|
|
@@ -2014,7 +2024,91 @@ class FontLoader {
|
|
|
2014
2024
|
}
|
|
2015
2025
|
}
|
|
2016
2026
|
|
|
2027
|
+
const SAFE_LANGUAGE_RE = /^[a-z]{2,3}(?:-[a-z0-9]{2,16})*$/i;
|
|
2028
|
+
// Built-in patterns shipped with three-text (matches files in src/hyphenation/*)
|
|
2029
|
+
const BUILTIN_PATTERN_LANGUAGES = new Set([
|
|
2030
|
+
'af',
|
|
2031
|
+
'as',
|
|
2032
|
+
'be',
|
|
2033
|
+
'bg',
|
|
2034
|
+
'bn',
|
|
2035
|
+
'ca',
|
|
2036
|
+
'cs',
|
|
2037
|
+
'cy',
|
|
2038
|
+
'da',
|
|
2039
|
+
'de-1996',
|
|
2040
|
+
'el-monoton',
|
|
2041
|
+
'el-polyton',
|
|
2042
|
+
'en-gb',
|
|
2043
|
+
'en-us',
|
|
2044
|
+
'eo',
|
|
2045
|
+
'es',
|
|
2046
|
+
'et',
|
|
2047
|
+
'eu',
|
|
2048
|
+
'fi',
|
|
2049
|
+
'fr',
|
|
2050
|
+
'fur',
|
|
2051
|
+
'ga',
|
|
2052
|
+
'gl',
|
|
2053
|
+
'gu',
|
|
2054
|
+
'hi',
|
|
2055
|
+
'hr',
|
|
2056
|
+
'hsb',
|
|
2057
|
+
'hu',
|
|
2058
|
+
'hy',
|
|
2059
|
+
'ia',
|
|
2060
|
+
'id',
|
|
2061
|
+
'is',
|
|
2062
|
+
'it',
|
|
2063
|
+
'ka',
|
|
2064
|
+
'kmr',
|
|
2065
|
+
'kn',
|
|
2066
|
+
'la',
|
|
2067
|
+
'lt',
|
|
2068
|
+
'lv',
|
|
2069
|
+
'mk',
|
|
2070
|
+
'ml',
|
|
2071
|
+
'mn-cyrl',
|
|
2072
|
+
'mr',
|
|
2073
|
+
'mul-ethi',
|
|
2074
|
+
'nb',
|
|
2075
|
+
'nl',
|
|
2076
|
+
'nn',
|
|
2077
|
+
'oc',
|
|
2078
|
+
'or',
|
|
2079
|
+
'pa',
|
|
2080
|
+
'pl',
|
|
2081
|
+
'pms',
|
|
2082
|
+
'pt',
|
|
2083
|
+
'rm',
|
|
2084
|
+
'ro',
|
|
2085
|
+
'ru',
|
|
2086
|
+
'sa',
|
|
2087
|
+
'sh-cyrl',
|
|
2088
|
+
'sh-latn',
|
|
2089
|
+
'sk',
|
|
2090
|
+
'sl',
|
|
2091
|
+
'sq',
|
|
2092
|
+
'sr-cyrl',
|
|
2093
|
+
'sv',
|
|
2094
|
+
'ta',
|
|
2095
|
+
'te',
|
|
2096
|
+
'th',
|
|
2097
|
+
'tk',
|
|
2098
|
+
'tr',
|
|
2099
|
+
'uk',
|
|
2100
|
+
'zh-latn-pinyin'
|
|
2101
|
+
]);
|
|
2017
2102
|
async function loadPattern(language, patternsPath) {
|
|
2103
|
+
if (!SAFE_LANGUAGE_RE.test(language)) {
|
|
2104
|
+
throw new Error(`Invalid hyphenation language code "${language}". Expected e.g. "en-us".`);
|
|
2105
|
+
}
|
|
2106
|
+
// When no patternsPath is provided, we only allow the built-in set shipped with
|
|
2107
|
+
// three-text to avoid accidental arbitrary imports / path traversal
|
|
2108
|
+
if (!patternsPath && !BUILTIN_PATTERN_LANGUAGES.has(language)) {
|
|
2109
|
+
throw new Error(`Unsupported hyphenation language "${language}". ` +
|
|
2110
|
+
`Use a built-in language (e.g. "en-us") or register patterns via Text.registerPattern("${language}", pattern).`);
|
|
2111
|
+
}
|
|
2018
2112
|
{
|
|
2019
2113
|
// In ESM build, use dynamic imports
|
|
2020
2114
|
try {
|
|
@@ -2203,108 +2297,233 @@ class Vec3 {
|
|
|
2203
2297
|
return this.x === v.x && this.y === v.y && this.z === v.z;
|
|
2204
2298
|
}
|
|
2205
2299
|
}
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
this.
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
this.
|
|
2214
|
-
|
|
2215
|
-
|
|
2300
|
+
|
|
2301
|
+
// Generic LRU (Least Recently Used) cache with optional memory-based eviction
|
|
2302
|
+
class LRUCache {
|
|
2303
|
+
constructor(options = {}) {
|
|
2304
|
+
this.cache = new Map();
|
|
2305
|
+
this.head = null;
|
|
2306
|
+
this.tail = null;
|
|
2307
|
+
this.stats = {
|
|
2308
|
+
hits: 0,
|
|
2309
|
+
misses: 0,
|
|
2310
|
+
evictions: 0,
|
|
2311
|
+
size: 0,
|
|
2312
|
+
memoryUsage: 0
|
|
2313
|
+
};
|
|
2314
|
+
this.options = {
|
|
2315
|
+
maxEntries: options.maxEntries ?? Infinity,
|
|
2316
|
+
maxMemoryBytes: options.maxMemoryBytes ?? Infinity,
|
|
2317
|
+
calculateSize: options.calculateSize ?? (() => 0),
|
|
2318
|
+
onEvict: options.onEvict
|
|
2319
|
+
};
|
|
2216
2320
|
}
|
|
2217
|
-
|
|
2218
|
-
this.
|
|
2219
|
-
|
|
2220
|
-
this.
|
|
2321
|
+
get(key) {
|
|
2322
|
+
const node = this.cache.get(key);
|
|
2323
|
+
if (node) {
|
|
2324
|
+
this.stats.hits++;
|
|
2325
|
+
this.moveToHead(node);
|
|
2326
|
+
return node.value;
|
|
2327
|
+
}
|
|
2328
|
+
else {
|
|
2329
|
+
this.stats.misses++;
|
|
2330
|
+
return undefined;
|
|
2221
2331
|
}
|
|
2222
|
-
return this;
|
|
2223
2332
|
}
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
this.max.x = this.max.y = this.max.z = -Infinity;
|
|
2227
|
-
return this;
|
|
2333
|
+
has(key) {
|
|
2334
|
+
return this.cache.has(key);
|
|
2228
2335
|
}
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2336
|
+
set(key, value) {
|
|
2337
|
+
// If key already exists, update it
|
|
2338
|
+
const existingNode = this.cache.get(key);
|
|
2339
|
+
if (existingNode) {
|
|
2340
|
+
const oldSize = this.options.calculateSize(existingNode.value);
|
|
2341
|
+
const newSize = this.options.calculateSize(value);
|
|
2342
|
+
this.stats.memoryUsage = this.stats.memoryUsage - oldSize + newSize;
|
|
2343
|
+
existingNode.value = value;
|
|
2344
|
+
this.moveToHead(existingNode);
|
|
2345
|
+
return;
|
|
2346
|
+
}
|
|
2347
|
+
const size = this.options.calculateSize(value);
|
|
2348
|
+
// Evict entries if we exceed limits
|
|
2349
|
+
this.evictIfNeeded(size);
|
|
2350
|
+
// Create new node
|
|
2351
|
+
const node = {
|
|
2352
|
+
key,
|
|
2353
|
+
value,
|
|
2354
|
+
prev: null,
|
|
2355
|
+
next: null
|
|
2356
|
+
};
|
|
2357
|
+
this.cache.set(key, node);
|
|
2358
|
+
this.addToHead(node);
|
|
2359
|
+
this.stats.size = this.cache.size;
|
|
2360
|
+
this.stats.memoryUsage += size;
|
|
2242
2361
|
}
|
|
2243
|
-
|
|
2244
|
-
this.
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
this.
|
|
2248
|
-
this.
|
|
2249
|
-
this.
|
|
2250
|
-
|
|
2362
|
+
delete(key) {
|
|
2363
|
+
const node = this.cache.get(key);
|
|
2364
|
+
if (!node)
|
|
2365
|
+
return false;
|
|
2366
|
+
const size = this.options.calculateSize(node.value);
|
|
2367
|
+
this.removeNode(node);
|
|
2368
|
+
this.cache.delete(key);
|
|
2369
|
+
this.stats.size = this.cache.size;
|
|
2370
|
+
this.stats.memoryUsage -= size;
|
|
2371
|
+
if (this.options.onEvict) {
|
|
2372
|
+
this.options.onEvict(key, node.value);
|
|
2373
|
+
}
|
|
2374
|
+
return true;
|
|
2251
2375
|
}
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
intersectsBox(box) {
|
|
2269
|
-
return (box.max.x >= this.min.x &&
|
|
2270
|
-
box.min.x <= this.max.x &&
|
|
2271
|
-
box.max.y >= this.min.y &&
|
|
2272
|
-
box.min.y <= this.max.y &&
|
|
2273
|
-
box.max.z >= this.min.z &&
|
|
2274
|
-
box.min.z <= this.max.z);
|
|
2275
|
-
}
|
|
2276
|
-
getCenter(target = new Vec3()) {
|
|
2277
|
-
return this.isEmpty()
|
|
2278
|
-
? target.set(0, 0, 0)
|
|
2279
|
-
: target.set((this.min.x + this.max.x) * 0.5, (this.min.y + this.max.y) * 0.5, (this.min.z + this.max.z) * 0.5);
|
|
2280
|
-
}
|
|
2281
|
-
getSize(target = new Vec3()) {
|
|
2282
|
-
return this.isEmpty()
|
|
2283
|
-
? target.set(0, 0, 0)
|
|
2284
|
-
: target.set(this.max.x - this.min.x, this.max.y - this.min.y, this.max.z - this.min.z);
|
|
2376
|
+
clear() {
|
|
2377
|
+
if (this.options.onEvict) {
|
|
2378
|
+
for (const [key, node] of this.cache) {
|
|
2379
|
+
this.options.onEvict(key, node.value);
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
this.cache.clear();
|
|
2383
|
+
this.head = null;
|
|
2384
|
+
this.tail = null;
|
|
2385
|
+
this.stats = {
|
|
2386
|
+
hits: 0,
|
|
2387
|
+
misses: 0,
|
|
2388
|
+
evictions: 0,
|
|
2389
|
+
size: 0,
|
|
2390
|
+
memoryUsage: 0
|
|
2391
|
+
};
|
|
2285
2392
|
}
|
|
2286
|
-
|
|
2287
|
-
|
|
2393
|
+
getStats() {
|
|
2394
|
+
const total = this.stats.hits + this.stats.misses;
|
|
2395
|
+
const hitRate = total > 0 ? (this.stats.hits / total) * 100 : 0;
|
|
2396
|
+
const memoryUsageMB = this.stats.memoryUsage / (1024 * 1024);
|
|
2397
|
+
return {
|
|
2398
|
+
...this.stats,
|
|
2399
|
+
hitRate,
|
|
2400
|
+
memoryUsageMB
|
|
2401
|
+
};
|
|
2288
2402
|
}
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
this.
|
|
2292
|
-
|
|
2403
|
+
keys() {
|
|
2404
|
+
const keys = [];
|
|
2405
|
+
let current = this.head;
|
|
2406
|
+
while (current) {
|
|
2407
|
+
keys.push(current.key);
|
|
2408
|
+
current = current.next;
|
|
2409
|
+
}
|
|
2410
|
+
return keys;
|
|
2293
2411
|
}
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
this.
|
|
2300
|
-
|
|
2301
|
-
|
|
2412
|
+
get size() {
|
|
2413
|
+
return this.cache.size;
|
|
2414
|
+
}
|
|
2415
|
+
evictIfNeeded(requiredSize) {
|
|
2416
|
+
// Evict by entry count
|
|
2417
|
+
while (this.cache.size >= this.options.maxEntries && this.tail) {
|
|
2418
|
+
this.evictTail();
|
|
2419
|
+
}
|
|
2420
|
+
// Evict by memory usage
|
|
2421
|
+
if (this.options.maxMemoryBytes < Infinity) {
|
|
2422
|
+
while (this.tail &&
|
|
2423
|
+
this.stats.memoryUsage + requiredSize > this.options.maxMemoryBytes) {
|
|
2424
|
+
this.evictTail();
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
evictTail() {
|
|
2429
|
+
if (!this.tail)
|
|
2430
|
+
return;
|
|
2431
|
+
const nodeToRemove = this.tail;
|
|
2432
|
+
const size = this.options.calculateSize(nodeToRemove.value);
|
|
2433
|
+
this.removeTail();
|
|
2434
|
+
this.cache.delete(nodeToRemove.key);
|
|
2435
|
+
this.stats.size = this.cache.size;
|
|
2436
|
+
this.stats.memoryUsage -= size;
|
|
2437
|
+
this.stats.evictions++;
|
|
2438
|
+
if (this.options.onEvict) {
|
|
2439
|
+
this.options.onEvict(nodeToRemove.key, nodeToRemove.value);
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
addToHead(node) {
|
|
2443
|
+
node.prev = null;
|
|
2444
|
+
node.next = null;
|
|
2445
|
+
if (!this.head) {
|
|
2446
|
+
this.head = this.tail = node;
|
|
2447
|
+
}
|
|
2448
|
+
else {
|
|
2449
|
+
node.next = this.head;
|
|
2450
|
+
this.head.prev = node;
|
|
2451
|
+
this.head = node;
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
removeNode(node) {
|
|
2455
|
+
if (node.prev) {
|
|
2456
|
+
node.prev.next = node.next;
|
|
2457
|
+
}
|
|
2458
|
+
else {
|
|
2459
|
+
this.head = node.next;
|
|
2460
|
+
}
|
|
2461
|
+
if (node.next) {
|
|
2462
|
+
node.next.prev = node.prev;
|
|
2463
|
+
}
|
|
2464
|
+
else {
|
|
2465
|
+
this.tail = node.prev;
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
removeTail() {
|
|
2469
|
+
if (this.tail) {
|
|
2470
|
+
this.removeNode(this.tail);
|
|
2471
|
+
}
|
|
2302
2472
|
}
|
|
2303
|
-
|
|
2304
|
-
|
|
2473
|
+
moveToHead(node) {
|
|
2474
|
+
if (node === this.head)
|
|
2475
|
+
return;
|
|
2476
|
+
this.removeNode(node);
|
|
2477
|
+
this.addToHead(node);
|
|
2305
2478
|
}
|
|
2306
2479
|
}
|
|
2307
2480
|
|
|
2481
|
+
const DEFAULT_CACHE_SIZE_MB = 250;
|
|
2482
|
+
function getGlyphCacheKey(fontId, glyphId, depth, removeOverlaps) {
|
|
2483
|
+
const roundedDepth = Math.round(depth * 1000) / 1000;
|
|
2484
|
+
return `${fontId}_${glyphId}_${roundedDepth}_${removeOverlaps}`;
|
|
2485
|
+
}
|
|
2486
|
+
function calculateGlyphMemoryUsage(glyph) {
|
|
2487
|
+
let size = 0;
|
|
2488
|
+
size += glyph.vertices.length * 4;
|
|
2489
|
+
size += glyph.normals.length * 4;
|
|
2490
|
+
size += glyph.indices.length * glyph.indices.BYTES_PER_ELEMENT;
|
|
2491
|
+
size += 24; // 2 Vec3s
|
|
2492
|
+
size += 256; // Object overhead
|
|
2493
|
+
return size;
|
|
2494
|
+
}
|
|
2495
|
+
const globalGlyphCache = new LRUCache({
|
|
2496
|
+
maxEntries: Infinity,
|
|
2497
|
+
maxMemoryBytes: DEFAULT_CACHE_SIZE_MB * 1024 * 1024,
|
|
2498
|
+
calculateSize: calculateGlyphMemoryUsage
|
|
2499
|
+
});
|
|
2500
|
+
function createGlyphCache(maxCacheSizeMB = DEFAULT_CACHE_SIZE_MB) {
|
|
2501
|
+
return new LRUCache({
|
|
2502
|
+
maxEntries: Infinity,
|
|
2503
|
+
maxMemoryBytes: maxCacheSizeMB * 1024 * 1024,
|
|
2504
|
+
calculateSize: calculateGlyphMemoryUsage
|
|
2505
|
+
});
|
|
2506
|
+
}
|
|
2507
|
+
// Shared across builder instances: contour extraction, word clustering, boundary grouping
|
|
2508
|
+
const globalContourCache = new LRUCache({
|
|
2509
|
+
maxEntries: 1000,
|
|
2510
|
+
calculateSize: (contours) => {
|
|
2511
|
+
let size = 0;
|
|
2512
|
+
for (const path of contours.paths) {
|
|
2513
|
+
size += path.points.length * 16; // Vec2 = 2 floats * 8 bytes
|
|
2514
|
+
}
|
|
2515
|
+
return size + 64; // bounds overhead
|
|
2516
|
+
}
|
|
2517
|
+
});
|
|
2518
|
+
const globalWordCache = new LRUCache({
|
|
2519
|
+
maxEntries: 1000,
|
|
2520
|
+
calculateSize: calculateGlyphMemoryUsage
|
|
2521
|
+
});
|
|
2522
|
+
const globalClusteringCache = new LRUCache({
|
|
2523
|
+
maxEntries: 2000,
|
|
2524
|
+
calculateSize: () => 1
|
|
2525
|
+
});
|
|
2526
|
+
|
|
2308
2527
|
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
|
|
2309
2528
|
|
|
2310
2529
|
function getDefaultExportFromCjs (x) {
|
|
@@ -2561,69 +2780,137 @@ class Tessellator {
|
|
|
2561
2780
|
class Extruder {
|
|
2562
2781
|
constructor() { }
|
|
2563
2782
|
extrude(geometry, depth = 0, unitsPerEm) {
|
|
2564
|
-
const
|
|
2565
|
-
const
|
|
2566
|
-
const
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
else {
|
|
2571
|
-
this.addFrontAndBackFaces(geometry.triangles, vertices, normals, indices, depth, unitsPerEm);
|
|
2783
|
+
const points = geometry.triangles.vertices;
|
|
2784
|
+
const triangleIndices = geometry.triangles.indices;
|
|
2785
|
+
const numPoints = points.length / 2;
|
|
2786
|
+
// Count side-wall segments (each segment emits 4 vertices + 6 indices)
|
|
2787
|
+
let sideSegments = 0;
|
|
2788
|
+
if (depth !== 0) {
|
|
2572
2789
|
for (const contour of geometry.contours) {
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
const
|
|
2581
|
-
const
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2790
|
+
// Each contour is a flat [x0,y0,x1,y1,...] array; side walls connect consecutive points
|
|
2791
|
+
// Contours are expected to be closed (last point repeats first), so segments = (nPoints - 1)
|
|
2792
|
+
const contourPoints = contour.length / 2;
|
|
2793
|
+
if (contourPoints >= 2)
|
|
2794
|
+
sideSegments += contourPoints - 1;
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
const sideVertexCount = depth === 0 ? 0 : sideSegments * 4;
|
|
2798
|
+
const baseVertexCount = depth === 0 ? numPoints : numPoints * 2;
|
|
2799
|
+
const vertexCount = baseVertexCount + sideVertexCount;
|
|
2800
|
+
const vertices = new Float32Array(vertexCount * 3);
|
|
2801
|
+
const normals = new Float32Array(vertexCount * 3);
|
|
2802
|
+
const indexCount = depth === 0
|
|
2803
|
+
? triangleIndices.length
|
|
2804
|
+
: triangleIndices.length * 2 + sideSegments * 6;
|
|
2805
|
+
const indices = new Uint32Array(indexCount);
|
|
2806
|
+
if (depth === 0) {
|
|
2807
|
+
// Flat faces only
|
|
2808
|
+
let vPos = 0;
|
|
2809
|
+
for (let i = 0; i < points.length; i += 2) {
|
|
2810
|
+
vertices[vPos] = points[i];
|
|
2811
|
+
vertices[vPos + 1] = points[i + 1];
|
|
2812
|
+
vertices[vPos + 2] = 0;
|
|
2813
|
+
normals[vPos] = 0;
|
|
2814
|
+
normals[vPos + 1] = 0;
|
|
2815
|
+
normals[vPos + 2] = -1;
|
|
2816
|
+
vPos += 3;
|
|
2817
|
+
}
|
|
2818
|
+
for (let i = 0; i < triangleIndices.length; i++) {
|
|
2819
|
+
indices[i] = triangleIndices[i];
|
|
2820
|
+
}
|
|
2821
|
+
return { vertices, normals, indices };
|
|
2822
|
+
}
|
|
2823
|
+
// Front/back faces
|
|
2600
2824
|
const minBackOffset = unitsPerEm * 0.000025;
|
|
2601
2825
|
const backZ = depth <= minBackOffset ? minBackOffset : depth;
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2826
|
+
// Fill front vertices/normals (0..numPoints-1)
|
|
2827
|
+
for (let p = 0, vi = 0; p < points.length; p += 2, vi++) {
|
|
2828
|
+
const base = vi * 3;
|
|
2829
|
+
vertices[base] = points[p];
|
|
2830
|
+
vertices[base + 1] = points[p + 1];
|
|
2831
|
+
vertices[base + 2] = 0;
|
|
2832
|
+
normals[base] = 0;
|
|
2833
|
+
normals[base + 1] = 0;
|
|
2834
|
+
normals[base + 2] = -1;
|
|
2835
|
+
}
|
|
2836
|
+
// Fill back vertices/normals (numPoints..2*numPoints-1)
|
|
2837
|
+
for (let p = 0, vi = 0; p < points.length; p += 2, vi++) {
|
|
2838
|
+
const base = (numPoints + vi) * 3;
|
|
2839
|
+
vertices[base] = points[p];
|
|
2840
|
+
vertices[base + 1] = points[p + 1];
|
|
2841
|
+
vertices[base + 2] = backZ;
|
|
2842
|
+
normals[base] = 0;
|
|
2843
|
+
normals[base + 1] = 0;
|
|
2844
|
+
normals[base + 2] = 1;
|
|
2845
|
+
}
|
|
2846
|
+
// Front indices
|
|
2607
2847
|
for (let i = 0; i < triangleIndices.length; i++) {
|
|
2608
|
-
indices
|
|
2609
|
-
}
|
|
2610
|
-
for (let i = triangleIndices.length - 1; i >= 0; i--) {
|
|
2611
|
-
indices.push(baseIndex + triangleIndices[i] + numPoints);
|
|
2848
|
+
indices[i] = triangleIndices[i];
|
|
2612
2849
|
}
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2850
|
+
// Back indices (reverse winding + offset)
|
|
2851
|
+
for (let i = 0; i < triangleIndices.length; i++) {
|
|
2852
|
+
indices[triangleIndices.length + i] =
|
|
2853
|
+
triangleIndices[triangleIndices.length - 1 - i] + numPoints;
|
|
2854
|
+
}
|
|
2855
|
+
// Side walls
|
|
2856
|
+
let nextVertex = numPoints * 2;
|
|
2857
|
+
let idxPos = triangleIndices.length * 2;
|
|
2858
|
+
for (const contour of geometry.contours) {
|
|
2859
|
+
for (let i = 0; i < contour.length - 2; i += 2) {
|
|
2860
|
+
const p0x = contour[i];
|
|
2861
|
+
const p0y = contour[i + 1];
|
|
2862
|
+
const p1x = contour[i + 2];
|
|
2863
|
+
const p1y = contour[i + 3];
|
|
2864
|
+
// Unit normal for the wall quad (per-edge)
|
|
2865
|
+
const ex = p1x - p0x;
|
|
2866
|
+
const ey = p1y - p0y;
|
|
2867
|
+
const lenSq = ex * ex + ey * ey;
|
|
2868
|
+
let nx = 0;
|
|
2869
|
+
let ny = 0;
|
|
2870
|
+
if (lenSq > 0) {
|
|
2871
|
+
const invLen = 1 / Math.sqrt(lenSq);
|
|
2872
|
+
nx = ey * invLen;
|
|
2873
|
+
ny = -ex * invLen;
|
|
2874
|
+
}
|
|
2875
|
+
const baseVertex = nextVertex;
|
|
2876
|
+
const base = baseVertex * 3;
|
|
2877
|
+
// 4 vertices (two at z=0, two at z=depth)
|
|
2878
|
+
vertices[base] = p0x;
|
|
2879
|
+
vertices[base + 1] = p0y;
|
|
2880
|
+
vertices[base + 2] = 0;
|
|
2881
|
+
vertices[base + 3] = p1x;
|
|
2882
|
+
vertices[base + 4] = p1y;
|
|
2883
|
+
vertices[base + 5] = 0;
|
|
2884
|
+
vertices[base + 6] = p0x;
|
|
2885
|
+
vertices[base + 7] = p0y;
|
|
2886
|
+
vertices[base + 8] = backZ;
|
|
2887
|
+
vertices[base + 9] = p1x;
|
|
2888
|
+
vertices[base + 10] = p1y;
|
|
2889
|
+
vertices[base + 11] = backZ;
|
|
2890
|
+
// Normals (same for all 4 wall vertices)
|
|
2891
|
+
normals[base] = nx;
|
|
2892
|
+
normals[base + 1] = ny;
|
|
2893
|
+
normals[base + 2] = 0;
|
|
2894
|
+
normals[base + 3] = nx;
|
|
2895
|
+
normals[base + 4] = ny;
|
|
2896
|
+
normals[base + 5] = 0;
|
|
2897
|
+
normals[base + 6] = nx;
|
|
2898
|
+
normals[base + 7] = ny;
|
|
2899
|
+
normals[base + 8] = 0;
|
|
2900
|
+
normals[base + 9] = nx;
|
|
2901
|
+
normals[base + 10] = ny;
|
|
2902
|
+
normals[base + 11] = 0;
|
|
2903
|
+
// Indices (two triangles)
|
|
2904
|
+
indices[idxPos++] = baseVertex;
|
|
2905
|
+
indices[idxPos++] = baseVertex + 1;
|
|
2906
|
+
indices[idxPos++] = baseVertex + 2;
|
|
2907
|
+
indices[idxPos++] = baseVertex + 1;
|
|
2908
|
+
indices[idxPos++] = baseVertex + 3;
|
|
2909
|
+
indices[idxPos++] = baseVertex + 2;
|
|
2910
|
+
nextVertex += 4;
|
|
2911
|
+
}
|
|
2626
2912
|
}
|
|
2913
|
+
return { vertices, normals, indices };
|
|
2627
2914
|
}
|
|
2628
2915
|
}
|
|
2629
2916
|
|
|
@@ -2634,18 +2921,21 @@ class BoundaryClusterer {
|
|
|
2634
2921
|
perfLogger.start('BoundaryClusterer.cluster', {
|
|
2635
2922
|
glyphCount: glyphContoursList.length
|
|
2636
2923
|
});
|
|
2637
|
-
|
|
2924
|
+
const n = glyphContoursList.length;
|
|
2925
|
+
if (n === 0) {
|
|
2638
2926
|
perfLogger.end('BoundaryClusterer.cluster');
|
|
2639
2927
|
return [];
|
|
2640
2928
|
}
|
|
2929
|
+
if (n === 1) {
|
|
2930
|
+
perfLogger.end('BoundaryClusterer.cluster');
|
|
2931
|
+
return [[0]];
|
|
2932
|
+
}
|
|
2641
2933
|
const result = this.clusterSweepLine(glyphContoursList, positions);
|
|
2642
2934
|
perfLogger.end('BoundaryClusterer.cluster');
|
|
2643
2935
|
return result;
|
|
2644
2936
|
}
|
|
2645
2937
|
clusterSweepLine(glyphContoursList, positions) {
|
|
2646
2938
|
const n = glyphContoursList.length;
|
|
2647
|
-
if (n <= 1)
|
|
2648
|
-
return n === 0 ? [] : [[0]];
|
|
2649
2939
|
const bounds = new Array(n);
|
|
2650
2940
|
const events = new Array(2 * n);
|
|
2651
2941
|
let eventIndex = 0;
|
|
@@ -2665,7 +2955,6 @@ class BoundaryClusterer {
|
|
|
2665
2955
|
const py = find(y);
|
|
2666
2956
|
if (px === py)
|
|
2667
2957
|
return;
|
|
2668
|
-
// Union by rank, attach smaller tree under larger tree
|
|
2669
2958
|
if (rank[px] < rank[py]) {
|
|
2670
2959
|
parent[px] = py;
|
|
2671
2960
|
}
|
|
@@ -2681,8 +2970,6 @@ class BoundaryClusterer {
|
|
|
2681
2970
|
for (const [, eventType, glyphIndex] of events) {
|
|
2682
2971
|
if (eventType === 0) {
|
|
2683
2972
|
const bounds1 = bounds[glyphIndex];
|
|
2684
|
-
// Check y-overlap with all currently active glyphs
|
|
2685
|
-
// (x-overlap is guaranteed by the sweep line)
|
|
2686
2973
|
for (const activeIndex of active) {
|
|
2687
2974
|
const bounds2 = bounds[activeIndex];
|
|
2688
2975
|
if (bounds1.minY < bounds2.maxY + OVERLAP_EPSILON &&
|
|
@@ -2699,10 +2986,12 @@ class BoundaryClusterer {
|
|
|
2699
2986
|
const clusters = new Map();
|
|
2700
2987
|
for (let i = 0; i < n; i++) {
|
|
2701
2988
|
const root = find(i);
|
|
2702
|
-
|
|
2703
|
-
|
|
2989
|
+
let list = clusters.get(root);
|
|
2990
|
+
if (!list) {
|
|
2991
|
+
list = [];
|
|
2992
|
+
clusters.set(root, list);
|
|
2704
2993
|
}
|
|
2705
|
-
|
|
2994
|
+
list.push(i);
|
|
2706
2995
|
}
|
|
2707
2996
|
return Array.from(clusters.values());
|
|
2708
2997
|
}
|
|
@@ -2934,13 +3223,17 @@ class PathOptimizer {
|
|
|
2934
3223
|
const prev = points[i - 1];
|
|
2935
3224
|
const current = points[i];
|
|
2936
3225
|
const next = points[i + 1];
|
|
2937
|
-
const
|
|
2938
|
-
const
|
|
2939
|
-
const
|
|
2940
|
-
const
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
3226
|
+
const v1x = current.x - prev.x;
|
|
3227
|
+
const v1y = current.y - prev.y;
|
|
3228
|
+
const v2x = next.x - current.x;
|
|
3229
|
+
const v2y = next.y - current.y;
|
|
3230
|
+
const angle = Math.abs(Math.atan2(v1x * v2y - v1y * v2x, v1x * v2x + v1y * v2y));
|
|
3231
|
+
const v1LenSq = v1x * v1x + v1y * v1y;
|
|
3232
|
+
const v2LenSq = v2x * v2x + v2y * v2y;
|
|
3233
|
+
const minLenSq = this.config.minSegmentLength * this.config.minSegmentLength;
|
|
3234
|
+
if (angle > threshold ||
|
|
3235
|
+
v1LenSq < minLenSq ||
|
|
3236
|
+
v2LenSq < minLenSq) {
|
|
2944
3237
|
result.push(current);
|
|
2945
3238
|
}
|
|
2946
3239
|
else {
|
|
@@ -3060,9 +3353,13 @@ class Polygonizer {
|
|
|
3060
3353
|
const angleTolerance = this.curveFidelityConfig.angleTolerance ??
|
|
3061
3354
|
DEFAULT_CURVE_FIDELITY.angleTolerance;
|
|
3062
3355
|
if (angleTolerance > 0) {
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3356
|
+
// Angle between segments (p1->p2) and (p2->p3).
|
|
3357
|
+
// Using atan2(cross, dot) avoids computing 2 separate atan2() values + wrap logic.
|
|
3358
|
+
const v1x = x2 - x1;
|
|
3359
|
+
const v1y = y2 - y1;
|
|
3360
|
+
const v2x = x3 - x2;
|
|
3361
|
+
const v2y = y3 - y2;
|
|
3362
|
+
const da = Math.abs(Math.atan2(v1x * v2y - v1y * v2x, v1x * v2x + v1y * v2y));
|
|
3066
3363
|
if (da < angleTolerance) {
|
|
3067
3364
|
this.addPoint(x2, y2, points);
|
|
3068
3365
|
return;
|
|
@@ -3157,9 +3454,12 @@ class Polygonizer {
|
|
|
3157
3454
|
const angleTolerance = this.curveFidelityConfig.angleTolerance ??
|
|
3158
3455
|
DEFAULT_CURVE_FIDELITY.angleTolerance;
|
|
3159
3456
|
if (angleTolerance > 0) {
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3457
|
+
// Angle between segments (p2->p3) and (p3->p4)
|
|
3458
|
+
const v1x = x3 - x2;
|
|
3459
|
+
const v1y = y3 - y2;
|
|
3460
|
+
const v2x = x4 - x3;
|
|
3461
|
+
const v2y = y4 - y3;
|
|
3462
|
+
const da1 = Math.abs(Math.atan2(v1x * v2y - v1y * v2x, v1x * v2x + v1y * v2y));
|
|
3163
3463
|
if (da1 < angleTolerance) {
|
|
3164
3464
|
this.addPoint(x2, y2, points);
|
|
3165
3465
|
this.addPoint(x3, y3, points);
|
|
@@ -3179,9 +3479,12 @@ class Polygonizer {
|
|
|
3179
3479
|
const angleTolerance = this.curveFidelityConfig.angleTolerance ??
|
|
3180
3480
|
DEFAULT_CURVE_FIDELITY.angleTolerance;
|
|
3181
3481
|
if (angleTolerance > 0) {
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3482
|
+
// Angle between segments (p1->p2) and (p2->p3)
|
|
3483
|
+
const v1x = x2 - x1;
|
|
3484
|
+
const v1y = y2 - y1;
|
|
3485
|
+
const v2x = x3 - x2;
|
|
3486
|
+
const v2y = y3 - y2;
|
|
3487
|
+
const da1 = Math.abs(Math.atan2(v1x * v2y - v1y * v2x, v1x * v2x + v1y * v2y));
|
|
3185
3488
|
if (da1 < angleTolerance) {
|
|
3186
3489
|
this.addPoint(x2, y2, points);
|
|
3187
3490
|
this.addPoint(x3, y3, points);
|
|
@@ -3201,12 +3504,18 @@ class Polygonizer {
|
|
|
3201
3504
|
const angleTolerance = this.curveFidelityConfig.angleTolerance ??
|
|
3202
3505
|
DEFAULT_CURVE_FIDELITY.angleTolerance;
|
|
3203
3506
|
if (angleTolerance > 0) {
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3507
|
+
// da1: angle between (p1->p2) and (p2->p3)
|
|
3508
|
+
const a1x = x2 - x1;
|
|
3509
|
+
const a1y = y2 - y1;
|
|
3510
|
+
const a2x = x3 - x2;
|
|
3511
|
+
const a2y = y3 - y2;
|
|
3512
|
+
const da1 = Math.abs(Math.atan2(a1x * a2y - a1y * a2x, a1x * a2x + a1y * a2y));
|
|
3513
|
+
// da2: angle between (p2->p3) and (p3->p4)
|
|
3514
|
+
const b1x = a2x;
|
|
3515
|
+
const b1y = a2y;
|
|
3516
|
+
const b2x = x4 - x3;
|
|
3517
|
+
const b2y = y4 - y3;
|
|
3518
|
+
const da2 = Math.abs(Math.atan2(b1x * b2y - b1y * b2x, b1x * b2x + b1y * b2y));
|
|
3210
3519
|
if (da1 + da2 < angleTolerance) {
|
|
3211
3520
|
this.addPoint(x2, y2, points);
|
|
3212
3521
|
this.addPoint(x3, y3, points);
|
|
@@ -3468,6 +3777,9 @@ class DrawCallbackHandler {
|
|
|
3468
3777
|
this.collector.updatePosition(dx, dy);
|
|
3469
3778
|
}
|
|
3470
3779
|
}
|
|
3780
|
+
setCollector(collector) {
|
|
3781
|
+
this.collector = collector;
|
|
3782
|
+
}
|
|
3471
3783
|
createDrawFuncs(font, collector) {
|
|
3472
3784
|
if (!font || !font.module || !font.hb) {
|
|
3473
3785
|
throw new Error('Invalid font object');
|
|
@@ -3544,229 +3856,73 @@ class DrawCallbackHandler {
|
|
|
3544
3856
|
this.collector = undefined;
|
|
3545
3857
|
}
|
|
3546
3858
|
}
|
|
3859
|
+
// Share a single DrawCallbackHandler per HarfBuzz module to avoid leaking
|
|
3860
|
+
// wasm function pointers when users create many Text instances
|
|
3861
|
+
const sharedDrawCallbackHandlers = new WeakMap();
|
|
3862
|
+
function getSharedDrawCallbackHandler(font) {
|
|
3863
|
+
const key = font.module;
|
|
3864
|
+
const existing = sharedDrawCallbackHandlers.get(key);
|
|
3865
|
+
if (existing)
|
|
3866
|
+
return existing;
|
|
3867
|
+
const handler = new DrawCallbackHandler();
|
|
3868
|
+
sharedDrawCallbackHandlers.set(key, handler);
|
|
3869
|
+
return handler;
|
|
3870
|
+
}
|
|
3547
3871
|
|
|
3548
|
-
// Generic LRU (Least Recently Used) cache with optional memory-based eviction
|
|
3549
|
-
class LRUCache {
|
|
3550
|
-
constructor(options = {}) {
|
|
3551
|
-
this.cache = new Map();
|
|
3552
|
-
this.head = null;
|
|
3553
|
-
this.tail = null;
|
|
3554
|
-
this.stats = {
|
|
3555
|
-
hits: 0,
|
|
3556
|
-
misses: 0,
|
|
3557
|
-
evictions: 0,
|
|
3558
|
-
size: 0,
|
|
3559
|
-
memoryUsage: 0
|
|
3560
|
-
};
|
|
3561
|
-
this.options = {
|
|
3562
|
-
maxEntries: options.maxEntries ?? Infinity,
|
|
3563
|
-
maxMemoryBytes: options.maxMemoryBytes ?? Infinity,
|
|
3564
|
-
calculateSize: options.calculateSize ?? (() => 0),
|
|
3565
|
-
onEvict: options.onEvict
|
|
3566
|
-
};
|
|
3567
|
-
}
|
|
3568
|
-
get(key) {
|
|
3569
|
-
const node = this.cache.get(key);
|
|
3570
|
-
if (node) {
|
|
3571
|
-
this.stats.hits++;
|
|
3572
|
-
this.moveToHead(node);
|
|
3573
|
-
return node.value;
|
|
3574
|
-
}
|
|
3575
|
-
else {
|
|
3576
|
-
this.stats.misses++;
|
|
3577
|
-
return undefined;
|
|
3578
|
-
}
|
|
3579
|
-
}
|
|
3580
|
-
has(key) {
|
|
3581
|
-
return this.cache.has(key);
|
|
3582
|
-
}
|
|
3583
|
-
set(key, value) {
|
|
3584
|
-
// If key already exists, update it
|
|
3585
|
-
const existingNode = this.cache.get(key);
|
|
3586
|
-
if (existingNode) {
|
|
3587
|
-
const oldSize = this.options.calculateSize(existingNode.value);
|
|
3588
|
-
const newSize = this.options.calculateSize(value);
|
|
3589
|
-
this.stats.memoryUsage = this.stats.memoryUsage - oldSize + newSize;
|
|
3590
|
-
existingNode.value = value;
|
|
3591
|
-
this.moveToHead(existingNode);
|
|
3592
|
-
return;
|
|
3593
|
-
}
|
|
3594
|
-
const size = this.options.calculateSize(value);
|
|
3595
|
-
// Evict entries if we exceed limits
|
|
3596
|
-
this.evictIfNeeded(size);
|
|
3597
|
-
// Create new node
|
|
3598
|
-
const node = {
|
|
3599
|
-
key,
|
|
3600
|
-
value,
|
|
3601
|
-
prev: null,
|
|
3602
|
-
next: null
|
|
3603
|
-
};
|
|
3604
|
-
this.cache.set(key, node);
|
|
3605
|
-
this.addToHead(node);
|
|
3606
|
-
this.stats.size = this.cache.size;
|
|
3607
|
-
this.stats.memoryUsage += size;
|
|
3608
|
-
}
|
|
3609
|
-
delete(key) {
|
|
3610
|
-
const node = this.cache.get(key);
|
|
3611
|
-
if (!node)
|
|
3612
|
-
return false;
|
|
3613
|
-
const size = this.options.calculateSize(node.value);
|
|
3614
|
-
this.removeNode(node);
|
|
3615
|
-
this.cache.delete(key);
|
|
3616
|
-
this.stats.size = this.cache.size;
|
|
3617
|
-
this.stats.memoryUsage -= size;
|
|
3618
|
-
if (this.options.onEvict) {
|
|
3619
|
-
this.options.onEvict(key, node.value);
|
|
3620
|
-
}
|
|
3621
|
-
return true;
|
|
3622
|
-
}
|
|
3623
|
-
clear() {
|
|
3624
|
-
if (this.options.onEvict) {
|
|
3625
|
-
for (const [key, node] of this.cache) {
|
|
3626
|
-
this.options.onEvict(key, node.value);
|
|
3627
|
-
}
|
|
3628
|
-
}
|
|
3629
|
-
this.cache.clear();
|
|
3630
|
-
this.head = null;
|
|
3631
|
-
this.tail = null;
|
|
3632
|
-
this.stats = {
|
|
3633
|
-
hits: 0,
|
|
3634
|
-
misses: 0,
|
|
3635
|
-
evictions: 0,
|
|
3636
|
-
size: 0,
|
|
3637
|
-
memoryUsage: 0
|
|
3638
|
-
};
|
|
3639
|
-
}
|
|
3640
|
-
getStats() {
|
|
3641
|
-
const total = this.stats.hits + this.stats.misses;
|
|
3642
|
-
const hitRate = total > 0 ? (this.stats.hits / total) * 100 : 0;
|
|
3643
|
-
const memoryUsageMB = this.stats.memoryUsage / (1024 * 1024);
|
|
3644
|
-
return {
|
|
3645
|
-
...this.stats,
|
|
3646
|
-
hitRate,
|
|
3647
|
-
memoryUsageMB
|
|
3648
|
-
};
|
|
3649
|
-
}
|
|
3650
|
-
keys() {
|
|
3651
|
-
const keys = [];
|
|
3652
|
-
let current = this.head;
|
|
3653
|
-
while (current) {
|
|
3654
|
-
keys.push(current.key);
|
|
3655
|
-
current = current.next;
|
|
3656
|
-
}
|
|
3657
|
-
return keys;
|
|
3658
|
-
}
|
|
3659
|
-
get size() {
|
|
3660
|
-
return this.cache.size;
|
|
3661
|
-
}
|
|
3662
|
-
evictIfNeeded(requiredSize) {
|
|
3663
|
-
// Evict by entry count
|
|
3664
|
-
while (this.cache.size >= this.options.maxEntries && this.tail) {
|
|
3665
|
-
this.evictTail();
|
|
3666
|
-
}
|
|
3667
|
-
// Evict by memory usage
|
|
3668
|
-
if (this.options.maxMemoryBytes < Infinity) {
|
|
3669
|
-
while (this.tail &&
|
|
3670
|
-
this.stats.memoryUsage + requiredSize > this.options.maxMemoryBytes) {
|
|
3671
|
-
this.evictTail();
|
|
3672
|
-
}
|
|
3673
|
-
}
|
|
3674
|
-
}
|
|
3675
|
-
evictTail() {
|
|
3676
|
-
if (!this.tail)
|
|
3677
|
-
return;
|
|
3678
|
-
const nodeToRemove = this.tail;
|
|
3679
|
-
const size = this.options.calculateSize(nodeToRemove.value);
|
|
3680
|
-
this.removeTail();
|
|
3681
|
-
this.cache.delete(nodeToRemove.key);
|
|
3682
|
-
this.stats.size = this.cache.size;
|
|
3683
|
-
this.stats.memoryUsage -= size;
|
|
3684
|
-
this.stats.evictions++;
|
|
3685
|
-
if (this.options.onEvict) {
|
|
3686
|
-
this.options.onEvict(nodeToRemove.key, nodeToRemove.value);
|
|
3687
|
-
}
|
|
3688
|
-
}
|
|
3689
|
-
addToHead(node) {
|
|
3690
|
-
if (!this.head) {
|
|
3691
|
-
this.head = this.tail = node;
|
|
3692
|
-
}
|
|
3693
|
-
else {
|
|
3694
|
-
node.next = this.head;
|
|
3695
|
-
this.head.prev = node;
|
|
3696
|
-
this.head = node;
|
|
3697
|
-
}
|
|
3698
|
-
}
|
|
3699
|
-
removeNode(node) {
|
|
3700
|
-
if (node.prev) {
|
|
3701
|
-
node.prev.next = node.next;
|
|
3702
|
-
}
|
|
3703
|
-
else {
|
|
3704
|
-
this.head = node.next;
|
|
3705
|
-
}
|
|
3706
|
-
if (node.next) {
|
|
3707
|
-
node.next.prev = node.prev;
|
|
3708
|
-
}
|
|
3709
|
-
else {
|
|
3710
|
-
this.tail = node.prev;
|
|
3711
|
-
}
|
|
3712
|
-
}
|
|
3713
|
-
removeTail() {
|
|
3714
|
-
if (this.tail) {
|
|
3715
|
-
this.removeNode(this.tail);
|
|
3716
|
-
}
|
|
3717
|
-
}
|
|
3718
|
-
moveToHead(node) {
|
|
3719
|
-
if (node === this.head)
|
|
3720
|
-
return;
|
|
3721
|
-
this.removeNode(node);
|
|
3722
|
-
this.addToHead(node);
|
|
3723
|
-
}
|
|
3724
|
-
}
|
|
3725
|
-
|
|
3726
|
-
const CONTOUR_CACHE_MAX_ENTRIES = 1000;
|
|
3727
|
-
const WORD_CACHE_MAX_ENTRIES = 1000;
|
|
3728
3872
|
class GlyphGeometryBuilder {
|
|
3729
3873
|
constructor(cache, loadedFont) {
|
|
3730
3874
|
this.fontId = 'default';
|
|
3875
|
+
this.cacheKeyPrefix = 'default';
|
|
3731
3876
|
this.cache = cache;
|
|
3732
3877
|
this.loadedFont = loadedFont;
|
|
3733
3878
|
this.tessellator = new Tessellator();
|
|
3734
3879
|
this.extruder = new Extruder();
|
|
3735
3880
|
this.clusterer = new BoundaryClusterer();
|
|
3736
3881
|
this.collector = new GlyphContourCollector();
|
|
3737
|
-
this.drawCallbacks =
|
|
3882
|
+
this.drawCallbacks = getSharedDrawCallbackHandler(this.loadedFont);
|
|
3738
3883
|
this.drawCallbacks.createDrawFuncs(this.loadedFont, this.collector);
|
|
3739
|
-
this.contourCache =
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
let size = 0;
|
|
3743
|
-
for (const path of contours.paths) {
|
|
3744
|
-
size += path.points.length * 16; // Vec2 = 2 floats * 8 bytes
|
|
3745
|
-
}
|
|
3746
|
-
return size + 64; // bounds overhead
|
|
3747
|
-
}
|
|
3748
|
-
});
|
|
3749
|
-
this.wordCache = new LRUCache({
|
|
3750
|
-
maxEntries: WORD_CACHE_MAX_ENTRIES,
|
|
3751
|
-
calculateSize: (data) => {
|
|
3752
|
-
let size = data.vertices.length * 4;
|
|
3753
|
-
size += data.normals.length * 4;
|
|
3754
|
-
size += data.indices.length * data.indices.BYTES_PER_ELEMENT;
|
|
3755
|
-
return size;
|
|
3756
|
-
}
|
|
3757
|
-
});
|
|
3884
|
+
this.contourCache = globalContourCache;
|
|
3885
|
+
this.wordCache = globalWordCache;
|
|
3886
|
+
this.clusteringCache = globalClusteringCache;
|
|
3758
3887
|
}
|
|
3759
3888
|
getOptimizationStats() {
|
|
3760
3889
|
return this.collector.getOptimizationStats();
|
|
3761
3890
|
}
|
|
3762
3891
|
setCurveFidelityConfig(config) {
|
|
3892
|
+
this.curveFidelityConfig = config;
|
|
3763
3893
|
this.collector.setCurveFidelityConfig(config);
|
|
3894
|
+
this.updateCacheKeyPrefix();
|
|
3764
3895
|
}
|
|
3765
3896
|
setGeometryOptimization(options) {
|
|
3897
|
+
this.geometryOptimizationOptions = options;
|
|
3766
3898
|
this.collector.setGeometryOptimization(options);
|
|
3899
|
+
this.updateCacheKeyPrefix();
|
|
3767
3900
|
}
|
|
3768
3901
|
setFontId(fontId) {
|
|
3769
3902
|
this.fontId = fontId;
|
|
3903
|
+
this.updateCacheKeyPrefix();
|
|
3904
|
+
}
|
|
3905
|
+
updateCacheKeyPrefix() {
|
|
3906
|
+
this.cacheKeyPrefix = `${this.fontId}__${this.getGeometryConfigSignature()}`;
|
|
3907
|
+
}
|
|
3908
|
+
getGeometryConfigSignature() {
|
|
3909
|
+
const distanceTolerance = this.curveFidelityConfig?.distanceTolerance ??
|
|
3910
|
+
DEFAULT_CURVE_FIDELITY.distanceTolerance;
|
|
3911
|
+
const angleTolerance = this.curveFidelityConfig?.angleTolerance ??
|
|
3912
|
+
DEFAULT_CURVE_FIDELITY.angleTolerance;
|
|
3913
|
+
const enabled = this.geometryOptimizationOptions?.enabled ??
|
|
3914
|
+
DEFAULT_OPTIMIZATION_CONFIG.enabled;
|
|
3915
|
+
const areaThreshold = this.geometryOptimizationOptions?.areaThreshold ??
|
|
3916
|
+
DEFAULT_OPTIMIZATION_CONFIG.areaThreshold;
|
|
3917
|
+
const colinearThreshold = this.geometryOptimizationOptions?.colinearThreshold ??
|
|
3918
|
+
DEFAULT_OPTIMIZATION_CONFIG.colinearThreshold;
|
|
3919
|
+
const minSegmentLength = this.geometryOptimizationOptions?.minSegmentLength ??
|
|
3920
|
+
DEFAULT_OPTIMIZATION_CONFIG.minSegmentLength;
|
|
3921
|
+
// Use fixed precision to keep cache keys stable and avoid float noise
|
|
3922
|
+
return [
|
|
3923
|
+
`cf:${distanceTolerance.toFixed(4)},${angleTolerance.toFixed(4)}`,
|
|
3924
|
+
`opt:${enabled ? 1 : 0},${areaThreshold.toFixed(4)},${colinearThreshold.toFixed(6)},${minSegmentLength.toFixed(4)}`
|
|
3925
|
+
].join('|');
|
|
3770
3926
|
}
|
|
3771
3927
|
// Build instanced geometry from glyph contours
|
|
3772
3928
|
buildInstancedGeometry(clustersByLine, depth, removeOverlaps, isCFF, separateGlyphs = false, coloredTextIndices) {
|
|
@@ -3776,9 +3932,58 @@ class GlyphGeometryBuilder {
|
|
|
3776
3932
|
depth,
|
|
3777
3933
|
removeOverlaps
|
|
3778
3934
|
});
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3935
|
+
// Growable typed arrays; slice to final size at end
|
|
3936
|
+
let vertexBuffer = new Float32Array(1024);
|
|
3937
|
+
let normalBuffer = new Float32Array(1024);
|
|
3938
|
+
let indexBuffer = new Uint32Array(1024);
|
|
3939
|
+
let vertexPos = 0; // float index (multiple of 3)
|
|
3940
|
+
let normalPos = 0; // float index (multiple of 3)
|
|
3941
|
+
let indexPos = 0; // index count
|
|
3942
|
+
const ensureFloatCapacity = (buffer, needed) => {
|
|
3943
|
+
if (needed <= buffer.length)
|
|
3944
|
+
return buffer;
|
|
3945
|
+
let nextSize = buffer.length;
|
|
3946
|
+
while (nextSize < needed)
|
|
3947
|
+
nextSize *= 2;
|
|
3948
|
+
const next = new Float32Array(nextSize);
|
|
3949
|
+
next.set(buffer);
|
|
3950
|
+
return next;
|
|
3951
|
+
};
|
|
3952
|
+
const ensureIndexCapacity = (buffer, needed) => {
|
|
3953
|
+
if (needed <= buffer.length)
|
|
3954
|
+
return buffer;
|
|
3955
|
+
let nextSize = buffer.length;
|
|
3956
|
+
while (nextSize < needed)
|
|
3957
|
+
nextSize *= 2;
|
|
3958
|
+
const next = new Uint32Array(nextSize);
|
|
3959
|
+
next.set(buffer);
|
|
3960
|
+
return next;
|
|
3961
|
+
};
|
|
3962
|
+
const appendGeometryToBuffers = (data, position, vertexOffset) => {
|
|
3963
|
+
const v = data.vertices;
|
|
3964
|
+
const n = data.normals;
|
|
3965
|
+
const idx = data.indices;
|
|
3966
|
+
// Grow buffers as needed
|
|
3967
|
+
vertexBuffer = ensureFloatCapacity(vertexBuffer, vertexPos + v.length);
|
|
3968
|
+
normalBuffer = ensureFloatCapacity(normalBuffer, normalPos + n.length);
|
|
3969
|
+
indexBuffer = ensureIndexCapacity(indexBuffer, indexPos + idx.length);
|
|
3970
|
+
// Vertices: translate by position
|
|
3971
|
+
const px = position.x;
|
|
3972
|
+
const py = position.y;
|
|
3973
|
+
const pz = position.z;
|
|
3974
|
+
for (let j = 0; j < v.length; j += 3) {
|
|
3975
|
+
vertexBuffer[vertexPos++] = v[j] + px;
|
|
3976
|
+
vertexBuffer[vertexPos++] = v[j + 1] + py;
|
|
3977
|
+
vertexBuffer[vertexPos++] = v[j + 2] + pz;
|
|
3978
|
+
}
|
|
3979
|
+
// Normals: straight copy
|
|
3980
|
+
normalBuffer.set(n, normalPos);
|
|
3981
|
+
normalPos += n.length;
|
|
3982
|
+
// Indices: copy with vertex offset
|
|
3983
|
+
for (let j = 0; j < idx.length; j++) {
|
|
3984
|
+
indexBuffer[indexPos++] = idx[j] + vertexOffset;
|
|
3985
|
+
}
|
|
3986
|
+
};
|
|
3782
3987
|
const glyphInfos = [];
|
|
3783
3988
|
const planeBounds = {
|
|
3784
3989
|
min: { x: Infinity, y: Infinity, z: 0 },
|
|
@@ -3787,83 +3992,126 @@ class GlyphGeometryBuilder {
|
|
|
3787
3992
|
for (let lineIndex = 0; lineIndex < clustersByLine.length; lineIndex++) {
|
|
3788
3993
|
const line = clustersByLine[lineIndex];
|
|
3789
3994
|
for (const cluster of line) {
|
|
3790
|
-
// Step 1: Get contours for all glyphs in the cluster
|
|
3791
3995
|
const clusterGlyphContours = [];
|
|
3792
3996
|
for (const glyph of cluster.glyphs) {
|
|
3793
3997
|
clusterGlyphContours.push(this.getContoursForGlyph(glyph.g));
|
|
3794
3998
|
}
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
if (!cachedCluster) {
|
|
3812
|
-
const clusterPaths = [];
|
|
3813
|
-
for (let i = 0; i < clusterGlyphContours.length; i++) {
|
|
3814
|
-
const glyphContours = clusterGlyphContours[i];
|
|
3815
|
-
const glyph = cluster.glyphs[i];
|
|
3816
|
-
for (const path of glyphContours.paths) {
|
|
3817
|
-
clusterPaths.push({
|
|
3818
|
-
...path,
|
|
3819
|
-
points: path.points.map((p) => new Vec2(p.x + (glyph.x ?? 0), p.y + (glyph.y ?? 0)))
|
|
3820
|
-
});
|
|
3999
|
+
let boundaryGroups;
|
|
4000
|
+
if (cluster.glyphs.length <= 1) {
|
|
4001
|
+
boundaryGroups = [[0]];
|
|
4002
|
+
}
|
|
4003
|
+
else {
|
|
4004
|
+
// Check clustering cache (same text + glyph IDs = same overlap groups)
|
|
4005
|
+
// Key must be font-specific; glyph ids/bounds differ between fonts
|
|
4006
|
+
const cacheKey = `${this.cacheKeyPrefix}_${cluster.text}`;
|
|
4007
|
+
const cached = this.clusteringCache.get(cacheKey);
|
|
4008
|
+
let isValid = false;
|
|
4009
|
+
if (cached && cached.glyphIds.length === cluster.glyphs.length) {
|
|
4010
|
+
isValid = true;
|
|
4011
|
+
for (let i = 0; i < cluster.glyphs.length; i++) {
|
|
4012
|
+
if (cached.glyphIds[i] !== cluster.glyphs[i].g) {
|
|
4013
|
+
isValid = false;
|
|
4014
|
+
break;
|
|
3821
4015
|
}
|
|
3822
4016
|
}
|
|
3823
|
-
cachedCluster = this.tessellateGlyphCluster(clusterPaths, depth, isCFF);
|
|
3824
|
-
this.wordCache.set(clusterKey, cachedCluster);
|
|
3825
4017
|
}
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
const
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
4018
|
+
if (isValid && cached) {
|
|
4019
|
+
boundaryGroups = cached.groups;
|
|
4020
|
+
}
|
|
4021
|
+
else {
|
|
4022
|
+
const relativePositions = cluster.glyphs.map((g) => new Vec3(g.x ?? 0, g.y ?? 0, 0));
|
|
4023
|
+
boundaryGroups = this.clusterer.cluster(clusterGlyphContours, relativePositions);
|
|
4024
|
+
this.clusteringCache.set(cacheKey, {
|
|
4025
|
+
glyphIds: cluster.glyphs.map(g => g.g),
|
|
4026
|
+
groups: boundaryGroups
|
|
4027
|
+
});
|
|
3836
4028
|
}
|
|
3837
4029
|
}
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
4030
|
+
const clusterHasColoredGlyphs = coloredTextIndices &&
|
|
4031
|
+
cluster.glyphs.some((g) => coloredTextIndices.has(g.absoluteTextIndex));
|
|
4032
|
+
// Use glyph-level caching when separateGlyphs is set or when cluster contains colored text
|
|
4033
|
+
const forceSeparate = separateGlyphs || clusterHasColoredGlyphs;
|
|
4034
|
+
// Iterate over the geometric groups identified by BoundaryClusterer
|
|
4035
|
+
// logical groups (words) are now split into geometric sub-groups (e.g. "aa", "XX", "bb")
|
|
4036
|
+
for (const groupIndices of boundaryGroups) {
|
|
4037
|
+
const isOverlappingGroup = groupIndices.length > 1;
|
|
4038
|
+
const shouldCluster = isOverlappingGroup && !forceSeparate;
|
|
4039
|
+
if (shouldCluster) {
|
|
4040
|
+
// Cluster-level caching for this specific group of overlapping glyphs
|
|
4041
|
+
const subClusterGlyphs = groupIndices.map((i) => cluster.glyphs[i]);
|
|
4042
|
+
const clusterKey = this.getClusterKey(subClusterGlyphs, depth, removeOverlaps);
|
|
4043
|
+
let cachedCluster = this.wordCache.get(clusterKey);
|
|
4044
|
+
if (!cachedCluster) {
|
|
4045
|
+
const clusterPaths = [];
|
|
4046
|
+
const refX = subClusterGlyphs[0].x ?? 0;
|
|
4047
|
+
const refY = subClusterGlyphs[0].y ?? 0;
|
|
4048
|
+
for (let i = 0; i < groupIndices.length; i++) {
|
|
4049
|
+
const originalIndex = groupIndices[i];
|
|
4050
|
+
const glyphContours = clusterGlyphContours[originalIndex];
|
|
4051
|
+
const glyph = cluster.glyphs[originalIndex];
|
|
4052
|
+
const relX = (glyph.x ?? 0) - refX;
|
|
4053
|
+
const relY = (glyph.y ?? 0) - refY;
|
|
4054
|
+
for (const path of glyphContours.paths) {
|
|
4055
|
+
clusterPaths.push({
|
|
4056
|
+
...path,
|
|
4057
|
+
points: path.points.map((p) => new Vec2(p.x + relX, p.y + relY))
|
|
4058
|
+
});
|
|
4059
|
+
}
|
|
4060
|
+
}
|
|
4061
|
+
cachedCluster = this.tessellateGlyphCluster(clusterPaths, depth, isCFF);
|
|
4062
|
+
this.wordCache.set(clusterKey, cachedCluster);
|
|
4063
|
+
}
|
|
4064
|
+
// Calculate the absolute position of this sub-cluster based on its first glyph
|
|
4065
|
+
// (since the cached geometry is relative to that first glyph)
|
|
4066
|
+
const firstGlyphInGroup = subClusterGlyphs[0];
|
|
4067
|
+
const groupPosition = new Vec3(cluster.position.x + (firstGlyphInGroup.x ?? 0), cluster.position.y + (firstGlyphInGroup.y ?? 0), cluster.position.z);
|
|
4068
|
+
const vertexOffset = vertexPos / 3;
|
|
4069
|
+
appendGeometryToBuffers(cachedCluster, groupPosition, vertexOffset);
|
|
4070
|
+
const clusterVertexCount = cachedCluster.vertices.length / 3;
|
|
4071
|
+
for (let i = 0; i < groupIndices.length; i++) {
|
|
4072
|
+
const originalIndex = groupIndices[i];
|
|
4073
|
+
const glyph = cluster.glyphs[originalIndex];
|
|
4074
|
+
const glyphContours = clusterGlyphContours[originalIndex];
|
|
4075
|
+
const absoluteGlyphPosition = new Vec3(cluster.position.x + (glyph.x ?? 0), cluster.position.y + (glyph.y ?? 0), cluster.position.z);
|
|
4076
|
+
const glyphInfo = this.createGlyphInfo(glyph, vertexOffset, clusterVertexCount, absoluteGlyphPosition, glyphContours, depth);
|
|
3847
4077
|
glyphInfos.push(glyphInfo);
|
|
3848
|
-
|
|
4078
|
+
this.updatePlaneBounds(glyphInfo.bounds, planeBounds);
|
|
3849
4079
|
}
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
4080
|
+
}
|
|
4081
|
+
else {
|
|
4082
|
+
// Glyph-level caching (standard path for isolated glyphs or when forced separate)
|
|
4083
|
+
for (const i of groupIndices) {
|
|
4084
|
+
const glyph = cluster.glyphs[i];
|
|
4085
|
+
const glyphContours = clusterGlyphContours[i];
|
|
4086
|
+
const glyphPosition = new Vec3(cluster.position.x + (glyph.x ?? 0), cluster.position.y + (glyph.y ?? 0), cluster.position.z);
|
|
4087
|
+
// Skip glyphs with no paths (spaces, zero-width characters, etc.)
|
|
4088
|
+
if (glyphContours.paths.length === 0) {
|
|
4089
|
+
const glyphInfo = this.createGlyphInfo(glyph, 0, 0, glyphPosition, glyphContours, depth);
|
|
4090
|
+
glyphInfos.push(glyphInfo);
|
|
4091
|
+
continue;
|
|
4092
|
+
}
|
|
4093
|
+
let cachedGlyph = this.cache.get(getGlyphCacheKey(this.cacheKeyPrefix, glyph.g, depth, removeOverlaps));
|
|
4094
|
+
if (!cachedGlyph) {
|
|
4095
|
+
cachedGlyph = this.tessellateGlyph(glyphContours, depth, removeOverlaps, isCFF);
|
|
4096
|
+
this.cache.set(getGlyphCacheKey(this.cacheKeyPrefix, glyph.g, depth, removeOverlaps), cachedGlyph);
|
|
4097
|
+
}
|
|
4098
|
+
else {
|
|
4099
|
+
cachedGlyph.useCount++;
|
|
4100
|
+
}
|
|
4101
|
+
const vertexOffset = vertexPos / 3;
|
|
4102
|
+
appendGeometryToBuffers(cachedGlyph, glyphPosition, vertexOffset);
|
|
4103
|
+
const glyphInfo = this.createGlyphInfo(glyph, vertexOffset, cachedGlyph.vertices.length / 3, glyphPosition, glyphContours, depth);
|
|
4104
|
+
glyphInfos.push(glyphInfo);
|
|
4105
|
+
this.updatePlaneBounds(glyphInfo.bounds, planeBounds);
|
|
3854
4106
|
}
|
|
3855
|
-
const vertexOffset = vertices.length / 3;
|
|
3856
|
-
this.appendGeometry(vertices, normals, indices, cachedGlyph, glyphPosition, vertexOffset);
|
|
3857
|
-
const glyphInfo = this.createGlyphInfo(glyph, vertexOffset, cachedGlyph.vertices.length / 3, glyphPosition, glyphContours, depth);
|
|
3858
|
-
glyphInfos.push(glyphInfo);
|
|
3859
|
-
this.updatePlaneBounds(glyphInfo.bounds, planeBounds);
|
|
3860
4107
|
}
|
|
3861
4108
|
}
|
|
3862
4109
|
}
|
|
3863
4110
|
}
|
|
3864
|
-
|
|
3865
|
-
const
|
|
3866
|
-
const
|
|
4111
|
+
// Slice to used lengths (avoid returning oversized buffers)
|
|
4112
|
+
const vertexArray = vertexBuffer.slice(0, vertexPos);
|
|
4113
|
+
const normalArray = normalBuffer.slice(0, normalPos);
|
|
4114
|
+
const indexArray = indexBuffer.slice(0, indexPos);
|
|
3867
4115
|
perfLogger.end('GlyphGeometryBuilder.buildInstancedGeometry');
|
|
3868
4116
|
return {
|
|
3869
4117
|
vertices: vertexArray,
|
|
@@ -3873,16 +4121,20 @@ class GlyphGeometryBuilder {
|
|
|
3873
4121
|
planeBounds
|
|
3874
4122
|
};
|
|
3875
4123
|
}
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
4124
|
+
getClusterKey(glyphs, depth, removeOverlaps) {
|
|
4125
|
+
if (glyphs.length === 0)
|
|
4126
|
+
return '';
|
|
4127
|
+
// Normalize positions relative to the first glyph in the cluster
|
|
4128
|
+
const refX = glyphs[0].x ?? 0;
|
|
4129
|
+
const refY = glyphs[0].y ?? 0;
|
|
4130
|
+
const parts = glyphs.map((g) => {
|
|
4131
|
+
const relX = (g.x ?? 0) - refX;
|
|
4132
|
+
const relY = (g.y ?? 0) - refY;
|
|
4133
|
+
return `${g.g}:${relX},${relY}`;
|
|
4134
|
+
});
|
|
4135
|
+
const ids = parts.join('|');
|
|
4136
|
+
const roundedDepth = Math.round(depth * 1000) / 1000;
|
|
4137
|
+
return `${this.cacheKeyPrefix}_${ids}_${roundedDepth}_${removeOverlaps}`;
|
|
3886
4138
|
}
|
|
3887
4139
|
createGlyphInfo(glyph, vertexStart, vertexCount, position, contours, depth) {
|
|
3888
4140
|
return {
|
|
@@ -3905,10 +4157,13 @@ class GlyphGeometryBuilder {
|
|
|
3905
4157
|
};
|
|
3906
4158
|
}
|
|
3907
4159
|
getContoursForGlyph(glyphId) {
|
|
3908
|
-
const
|
|
4160
|
+
const key = `${this.cacheKeyPrefix}_${glyphId}`;
|
|
4161
|
+
const cached = this.contourCache.get(key);
|
|
3909
4162
|
if (cached) {
|
|
3910
4163
|
return cached;
|
|
3911
4164
|
}
|
|
4165
|
+
// Rebind collector before draw operation
|
|
4166
|
+
this.drawCallbacks.setCollector(this.collector);
|
|
3912
4167
|
this.collector.reset();
|
|
3913
4168
|
this.collector.beginGlyph(glyphId, 0);
|
|
3914
4169
|
this.loadedFont.module.exports.hb_font_draw_glyph(this.loadedFont.font.ptr, glyphId, this.drawCallbacks.getDrawFuncsPtr(), 0);
|
|
@@ -3922,7 +4177,7 @@ class GlyphGeometryBuilder {
|
|
|
3922
4177
|
max: { x: 0, y: 0 }
|
|
3923
4178
|
}
|
|
3924
4179
|
};
|
|
3925
|
-
this.contourCache.set(
|
|
4180
|
+
this.contourCache.set(key, contours);
|
|
3926
4181
|
return contours;
|
|
3927
4182
|
}
|
|
3928
4183
|
tessellateGlyphCluster(paths, depth, isCFF) {
|
|
@@ -3959,13 +4214,11 @@ class GlyphGeometryBuilder {
|
|
|
3959
4214
|
}
|
|
3960
4215
|
const boundsMin = new Vec3(minX, minY, minZ);
|
|
3961
4216
|
const boundsMax = new Vec3(maxX, maxY, maxZ);
|
|
3962
|
-
const vertexCount = extrudedResult.vertices.length / 3;
|
|
3963
|
-
const IndexArray = vertexCount < 65536 ? Uint16Array : Uint32Array;
|
|
3964
4217
|
return {
|
|
3965
4218
|
geometry: processedGeometry,
|
|
3966
|
-
vertices:
|
|
3967
|
-
normals:
|
|
3968
|
-
indices:
|
|
4219
|
+
vertices: extrudedResult.vertices,
|
|
4220
|
+
normals: extrudedResult.normals,
|
|
4221
|
+
indices: extrudedResult.indices,
|
|
3969
4222
|
bounds: { min: boundsMin, max: boundsMax },
|
|
3970
4223
|
useCount: 1
|
|
3971
4224
|
};
|
|
@@ -3981,15 +4234,22 @@ class GlyphGeometryBuilder {
|
|
|
3981
4234
|
return this.extrudeAndPackage(processedGeometry, depth);
|
|
3982
4235
|
}
|
|
3983
4236
|
updatePlaneBounds(glyphBounds, planeBounds) {
|
|
3984
|
-
const
|
|
3985
|
-
const
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
4237
|
+
const pMin = planeBounds.min;
|
|
4238
|
+
const pMax = planeBounds.max;
|
|
4239
|
+
const gMin = glyphBounds.min;
|
|
4240
|
+
const gMax = glyphBounds.max;
|
|
4241
|
+
if (gMin.x < pMin.x)
|
|
4242
|
+
pMin.x = gMin.x;
|
|
4243
|
+
if (gMin.y < pMin.y)
|
|
4244
|
+
pMin.y = gMin.y;
|
|
4245
|
+
if (gMin.z < pMin.z)
|
|
4246
|
+
pMin.z = gMin.z;
|
|
4247
|
+
if (gMax.x > pMax.x)
|
|
4248
|
+
pMax.x = gMax.x;
|
|
4249
|
+
if (gMax.y > pMax.y)
|
|
4250
|
+
pMax.y = gMax.y;
|
|
4251
|
+
if (gMax.z > pMax.z)
|
|
4252
|
+
pMax.z = gMax.z;
|
|
3993
4253
|
}
|
|
3994
4254
|
getCacheStats() {
|
|
3995
4255
|
return this.cache.getStats();
|
|
@@ -3997,6 +4257,8 @@ class GlyphGeometryBuilder {
|
|
|
3997
4257
|
clearCache() {
|
|
3998
4258
|
this.cache.clear();
|
|
3999
4259
|
this.wordCache.clear();
|
|
4260
|
+
this.clusteringCache.clear();
|
|
4261
|
+
this.contourCache.clear();
|
|
4000
4262
|
}
|
|
4001
4263
|
}
|
|
4002
4264
|
|
|
@@ -4011,12 +4273,17 @@ class TextShaper {
|
|
|
4011
4273
|
perfLogger.start('TextShaper.shapeLines', {
|
|
4012
4274
|
lineCount: lineInfos.length
|
|
4013
4275
|
});
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4276
|
+
try {
|
|
4277
|
+
const clustersByLine = [];
|
|
4278
|
+
lineInfos.forEach((lineInfo, lineIndex) => {
|
|
4279
|
+
const clusters = this.shapeLineIntoClusters(lineInfo, lineIndex, scaledLineHeight, letterSpacing, align, direction);
|
|
4280
|
+
clustersByLine.push(clusters);
|
|
4281
|
+
});
|
|
4282
|
+
return clustersByLine;
|
|
4283
|
+
}
|
|
4284
|
+
finally {
|
|
4285
|
+
perfLogger.end('TextShaper.shapeLines');
|
|
4286
|
+
}
|
|
4020
4287
|
}
|
|
4021
4288
|
shapeLineIntoClusters(lineInfo, lineIndex, scaledLineHeight, letterSpacing, align, direction) {
|
|
4022
4289
|
const buffer = this.loadedFont.hb.createBuffer();
|
|
@@ -4164,162 +4431,6 @@ class TextShaper {
|
|
|
4164
4431
|
}
|
|
4165
4432
|
}
|
|
4166
4433
|
|
|
4167
|
-
const DEFAULT_CACHE_SIZE_MB = 250;
|
|
4168
|
-
class GlyphCache {
|
|
4169
|
-
constructor(maxCacheSizeMB) {
|
|
4170
|
-
this.cache = new Map();
|
|
4171
|
-
this.head = null;
|
|
4172
|
-
this.tail = null;
|
|
4173
|
-
this.stats = {
|
|
4174
|
-
hits: 0,
|
|
4175
|
-
misses: 0,
|
|
4176
|
-
totalGlyphs: 0,
|
|
4177
|
-
uniqueGlyphs: 0,
|
|
4178
|
-
cacheSize: 0,
|
|
4179
|
-
saved: 0,
|
|
4180
|
-
memoryUsage: 0
|
|
4181
|
-
};
|
|
4182
|
-
if (maxCacheSizeMB) {
|
|
4183
|
-
this.maxCacheSize = maxCacheSizeMB * 1024 * 1024;
|
|
4184
|
-
}
|
|
4185
|
-
}
|
|
4186
|
-
getCacheKey(fontId, glyphId, depth, removeOverlaps) {
|
|
4187
|
-
// Round depth to avoid floating point precision issues
|
|
4188
|
-
const roundedDepth = Math.round(depth * 1000) / 1000;
|
|
4189
|
-
return `${fontId}_${glyphId}_${roundedDepth}_${removeOverlaps}`;
|
|
4190
|
-
}
|
|
4191
|
-
has(fontId, glyphId, depth, removeOverlaps) {
|
|
4192
|
-
const key = this.getCacheKey(fontId, glyphId, depth, removeOverlaps);
|
|
4193
|
-
return this.cache.has(key);
|
|
4194
|
-
}
|
|
4195
|
-
get(fontId, glyphId, depth, removeOverlaps) {
|
|
4196
|
-
const key = this.getCacheKey(fontId, glyphId, depth, removeOverlaps);
|
|
4197
|
-
const node = this.cache.get(key);
|
|
4198
|
-
if (node) {
|
|
4199
|
-
this.stats.hits++;
|
|
4200
|
-
this.stats.saved++;
|
|
4201
|
-
node.data.useCount++;
|
|
4202
|
-
// Move to head (most recently used)
|
|
4203
|
-
this.moveToHead(node);
|
|
4204
|
-
this.stats.totalGlyphs++;
|
|
4205
|
-
return node.data;
|
|
4206
|
-
}
|
|
4207
|
-
else {
|
|
4208
|
-
this.stats.misses++;
|
|
4209
|
-
this.stats.totalGlyphs++;
|
|
4210
|
-
return undefined;
|
|
4211
|
-
}
|
|
4212
|
-
}
|
|
4213
|
-
set(fontId, glyphId, depth, removeOverlaps, glyph) {
|
|
4214
|
-
const key = this.getCacheKey(fontId, glyphId, depth, removeOverlaps);
|
|
4215
|
-
const memoryUsage = this.calculateMemoryUsage(glyph);
|
|
4216
|
-
// LRU eviction when memory limit exceeded
|
|
4217
|
-
if (this.maxCacheSize &&
|
|
4218
|
-
this.stats.memoryUsage + memoryUsage > this.maxCacheSize) {
|
|
4219
|
-
this.evictLRU(memoryUsage);
|
|
4220
|
-
}
|
|
4221
|
-
const node = {
|
|
4222
|
-
key,
|
|
4223
|
-
data: glyph,
|
|
4224
|
-
prev: null,
|
|
4225
|
-
next: null
|
|
4226
|
-
};
|
|
4227
|
-
this.cache.set(key, node);
|
|
4228
|
-
this.addToHead(node);
|
|
4229
|
-
this.stats.uniqueGlyphs = this.cache.size;
|
|
4230
|
-
this.stats.cacheSize++;
|
|
4231
|
-
this.stats.memoryUsage += memoryUsage;
|
|
4232
|
-
}
|
|
4233
|
-
calculateMemoryUsage(glyph) {
|
|
4234
|
-
let size = 0;
|
|
4235
|
-
// 3 floats per vertex * 4 bytes per float
|
|
4236
|
-
size += glyph.vertices.length * 4;
|
|
4237
|
-
// 3 floats per normal * 4 bytes per float
|
|
4238
|
-
size += glyph.normals.length * 4;
|
|
4239
|
-
// Indices (Uint16Array or Uint32Array)
|
|
4240
|
-
size += glyph.indices.length * glyph.indices.BYTES_PER_ELEMENT;
|
|
4241
|
-
// Bounds (2 Vec3s = 6 floats * 4 bytes)
|
|
4242
|
-
size += 24;
|
|
4243
|
-
// Object overhead
|
|
4244
|
-
size += 256;
|
|
4245
|
-
return size;
|
|
4246
|
-
}
|
|
4247
|
-
// LRU eviction
|
|
4248
|
-
evictLRU(requiredSpace) {
|
|
4249
|
-
let freedSpace = 0;
|
|
4250
|
-
while (this.tail && freedSpace < requiredSpace) {
|
|
4251
|
-
const memoryUsage = this.calculateMemoryUsage(this.tail.data);
|
|
4252
|
-
const nodeToRemove = this.tail;
|
|
4253
|
-
this.removeTail();
|
|
4254
|
-
this.cache.delete(nodeToRemove.key);
|
|
4255
|
-
this.stats.memoryUsage -= memoryUsage;
|
|
4256
|
-
this.stats.cacheSize--;
|
|
4257
|
-
freedSpace += memoryUsage;
|
|
4258
|
-
}
|
|
4259
|
-
}
|
|
4260
|
-
addToHead(node) {
|
|
4261
|
-
if (!this.head) {
|
|
4262
|
-
this.head = this.tail = node;
|
|
4263
|
-
}
|
|
4264
|
-
else {
|
|
4265
|
-
node.next = this.head;
|
|
4266
|
-
this.head.prev = node;
|
|
4267
|
-
this.head = node;
|
|
4268
|
-
}
|
|
4269
|
-
}
|
|
4270
|
-
removeNode(node) {
|
|
4271
|
-
if (node.prev) {
|
|
4272
|
-
node.prev.next = node.next;
|
|
4273
|
-
}
|
|
4274
|
-
else {
|
|
4275
|
-
this.head = node.next;
|
|
4276
|
-
}
|
|
4277
|
-
if (node.next) {
|
|
4278
|
-
node.next.prev = node.prev;
|
|
4279
|
-
}
|
|
4280
|
-
else {
|
|
4281
|
-
this.tail = node.prev;
|
|
4282
|
-
}
|
|
4283
|
-
}
|
|
4284
|
-
removeTail() {
|
|
4285
|
-
if (this.tail) {
|
|
4286
|
-
this.removeNode(this.tail);
|
|
4287
|
-
}
|
|
4288
|
-
}
|
|
4289
|
-
moveToHead(node) {
|
|
4290
|
-
if (node === this.head)
|
|
4291
|
-
return;
|
|
4292
|
-
this.removeNode(node);
|
|
4293
|
-
this.addToHead(node);
|
|
4294
|
-
}
|
|
4295
|
-
clear() {
|
|
4296
|
-
this.cache.clear();
|
|
4297
|
-
this.head = null;
|
|
4298
|
-
this.tail = null;
|
|
4299
|
-
this.stats = {
|
|
4300
|
-
hits: 0,
|
|
4301
|
-
misses: 0,
|
|
4302
|
-
totalGlyphs: 0,
|
|
4303
|
-
uniqueGlyphs: 0,
|
|
4304
|
-
cacheSize: 0,
|
|
4305
|
-
saved: 0,
|
|
4306
|
-
memoryUsage: 0
|
|
4307
|
-
};
|
|
4308
|
-
}
|
|
4309
|
-
getStats() {
|
|
4310
|
-
const hitRate = this.stats.totalGlyphs > 0
|
|
4311
|
-
? (this.stats.hits / this.stats.totalGlyphs) * 100
|
|
4312
|
-
: 0;
|
|
4313
|
-
this.stats.uniqueGlyphs = this.cache.size;
|
|
4314
|
-
return {
|
|
4315
|
-
...this.stats,
|
|
4316
|
-
hitRate,
|
|
4317
|
-
memoryUsageMB: this.stats.memoryUsage / (1024 * 1024)
|
|
4318
|
-
};
|
|
4319
|
-
}
|
|
4320
|
-
}
|
|
4321
|
-
const globalGlyphCache = new GlyphCache(DEFAULT_CACHE_SIZE_MB);
|
|
4322
|
-
|
|
4323
4434
|
var hb = {exports: {}};
|
|
4324
4435
|
|
|
4325
4436
|
var fs = {}; const readFileSync = () => { throw new Error('fs not available in browser'); };
|
|
@@ -5025,14 +5136,25 @@ class TextRangeQuery {
|
|
|
5025
5136
|
max: { x: 0, y: 0, z: 0 }
|
|
5026
5137
|
};
|
|
5027
5138
|
}
|
|
5028
|
-
|
|
5139
|
+
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
|
5140
|
+
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
|
5029
5141
|
for (const glyph of glyphs) {
|
|
5030
|
-
|
|
5031
|
-
|
|
5142
|
+
if (glyph.bounds.min.x < minX)
|
|
5143
|
+
minX = glyph.bounds.min.x;
|
|
5144
|
+
if (glyph.bounds.min.y < minY)
|
|
5145
|
+
minY = glyph.bounds.min.y;
|
|
5146
|
+
if (glyph.bounds.min.z < minZ)
|
|
5147
|
+
minZ = glyph.bounds.min.z;
|
|
5148
|
+
if (glyph.bounds.max.x > maxX)
|
|
5149
|
+
maxX = glyph.bounds.max.x;
|
|
5150
|
+
if (glyph.bounds.max.y > maxY)
|
|
5151
|
+
maxY = glyph.bounds.max.y;
|
|
5152
|
+
if (glyph.bounds.max.z > maxZ)
|
|
5153
|
+
maxZ = glyph.bounds.max.z;
|
|
5032
5154
|
}
|
|
5033
5155
|
return {
|
|
5034
|
-
min: { x:
|
|
5035
|
-
max: { x:
|
|
5156
|
+
min: { x: minX, y: minY, z: minZ },
|
|
5157
|
+
max: { x: maxX, y: maxY, z: maxZ }
|
|
5036
5158
|
};
|
|
5037
5159
|
}
|
|
5038
5160
|
}
|
|
@@ -5042,8 +5164,16 @@ class Text {
|
|
|
5042
5164
|
static { this.patternCache = new Map(); }
|
|
5043
5165
|
static { this.hbInitPromise = null; }
|
|
5044
5166
|
static { this.fontCache = new Map(); }
|
|
5167
|
+
static { this.fontCacheMemoryBytes = 0; }
|
|
5168
|
+
static { this.maxFontCacheMemoryBytes = Infinity; }
|
|
5045
5169
|
static { this.fontIdCounter = 0; }
|
|
5046
|
-
|
|
5170
|
+
// Stringify with sorted keys for cache stability
|
|
5171
|
+
static stableStringify(obj) {
|
|
5172
|
+
const keys = Object.keys(obj).sort();
|
|
5173
|
+
const pairs = keys.map(k => `${k}:${obj[k]}`);
|
|
5174
|
+
return pairs.join(',');
|
|
5175
|
+
}
|
|
5176
|
+
constructor() {
|
|
5047
5177
|
this.currentFontId = '';
|
|
5048
5178
|
if (!Text.hbInitPromise) {
|
|
5049
5179
|
Text.hbInitPromise = HarfBuzzLoader.getHarfBuzz();
|
|
@@ -5074,7 +5204,7 @@ class Text {
|
|
|
5074
5204
|
Text.hbInitPromise = HarfBuzzLoader.getHarfBuzz();
|
|
5075
5205
|
}
|
|
5076
5206
|
const loadedFont = await Text.resolveFont(options);
|
|
5077
|
-
const text = new Text(
|
|
5207
|
+
const text = new Text();
|
|
5078
5208
|
text.setLoadedFont(loadedFont);
|
|
5079
5209
|
// Initial creation
|
|
5080
5210
|
const { font, maxCacheSizeMB, ...geometryOptions } = options;
|
|
@@ -5126,10 +5256,10 @@ class Text {
|
|
|
5126
5256
|
: `buffer-${Text.generateFontContentHash(options.font)}`;
|
|
5127
5257
|
let fontKey = baseFontKey;
|
|
5128
5258
|
if (options.fontVariations) {
|
|
5129
|
-
fontKey += `_var_${
|
|
5259
|
+
fontKey += `_var_${Text.stableStringify(options.fontVariations)}`;
|
|
5130
5260
|
}
|
|
5131
5261
|
if (options.fontFeatures) {
|
|
5132
|
-
fontKey += `_feat_${
|
|
5262
|
+
fontKey += `_feat_${Text.stableStringify(options.fontFeatures)}`;
|
|
5133
5263
|
}
|
|
5134
5264
|
let loadedFont = Text.fontCache.get(fontKey);
|
|
5135
5265
|
if (!loadedFont) {
|
|
@@ -5142,17 +5272,53 @@ class Text {
|
|
|
5142
5272
|
await tempText.loadFont(font, fontVariations, fontFeatures);
|
|
5143
5273
|
const loadedFont = tempText.getLoadedFont();
|
|
5144
5274
|
Text.fontCache.set(fontKey, loadedFont);
|
|
5275
|
+
Text.trackFontCacheAdd(loadedFont);
|
|
5276
|
+
Text.enforceFontCacheMemoryLimit();
|
|
5145
5277
|
return loadedFont;
|
|
5146
5278
|
}
|
|
5279
|
+
static trackFontCacheAdd(loadedFont) {
|
|
5280
|
+
const size = loadedFont._buffer?.byteLength ?? 0;
|
|
5281
|
+
Text.fontCacheMemoryBytes += size;
|
|
5282
|
+
}
|
|
5283
|
+
static trackFontCacheRemove(fontKey) {
|
|
5284
|
+
const font = Text.fontCache.get(fontKey);
|
|
5285
|
+
if (!font)
|
|
5286
|
+
return;
|
|
5287
|
+
const size = font._buffer?.byteLength ?? 0;
|
|
5288
|
+
Text.fontCacheMemoryBytes -= size;
|
|
5289
|
+
if (Text.fontCacheMemoryBytes < 0)
|
|
5290
|
+
Text.fontCacheMemoryBytes = 0;
|
|
5291
|
+
}
|
|
5292
|
+
static enforceFontCacheMemoryLimit() {
|
|
5293
|
+
if (Text.maxFontCacheMemoryBytes === Infinity)
|
|
5294
|
+
return;
|
|
5295
|
+
while (Text.fontCacheMemoryBytes > Text.maxFontCacheMemoryBytes &&
|
|
5296
|
+
Text.fontCache.size > 0) {
|
|
5297
|
+
const firstKey = Text.fontCache.keys().next().value;
|
|
5298
|
+
if (firstKey === undefined)
|
|
5299
|
+
break;
|
|
5300
|
+
Text.trackFontCacheRemove(firstKey);
|
|
5301
|
+
Text.fontCache.delete(firstKey);
|
|
5302
|
+
}
|
|
5303
|
+
}
|
|
5147
5304
|
static generateFontContentHash(buffer) {
|
|
5148
5305
|
if (buffer) {
|
|
5149
|
-
//
|
|
5306
|
+
// FNV-1a hash sampling 32 points
|
|
5150
5307
|
const view = new Uint8Array(buffer);
|
|
5151
|
-
|
|
5308
|
+
let hash = 2166136261;
|
|
5309
|
+
const samplePoints = Math.min(32, view.length);
|
|
5310
|
+
const step = Math.floor(view.length / samplePoints);
|
|
5311
|
+
for (let i = 0; i < samplePoints; i++) {
|
|
5312
|
+
const index = i * step;
|
|
5313
|
+
hash ^= view[index];
|
|
5314
|
+
hash = Math.imul(hash, 16777619);
|
|
5315
|
+
}
|
|
5316
|
+
hash ^= view.length;
|
|
5317
|
+
hash = Math.imul(hash, 16777619);
|
|
5318
|
+
return (hash >>> 0).toString(36);
|
|
5152
5319
|
}
|
|
5153
5320
|
else {
|
|
5154
|
-
|
|
5155
|
-
return `${++Text.fontIdCounter}`;
|
|
5321
|
+
return `c${++Text.fontIdCounter}`;
|
|
5156
5322
|
}
|
|
5157
5323
|
}
|
|
5158
5324
|
setLoadedFont(loadedFont) {
|
|
@@ -5160,10 +5326,10 @@ class Text {
|
|
|
5160
5326
|
const contentHash = Text.generateFontContentHash(loadedFont._buffer);
|
|
5161
5327
|
this.currentFontId = `font_${contentHash}`;
|
|
5162
5328
|
if (loadedFont.fontVariations) {
|
|
5163
|
-
this.currentFontId += `_var_${
|
|
5329
|
+
this.currentFontId += `_var_${Text.stableStringify(loadedFont.fontVariations)}`;
|
|
5164
5330
|
}
|
|
5165
5331
|
if (loadedFont.fontFeatures) {
|
|
5166
|
-
this.currentFontId += `_feat_${
|
|
5332
|
+
this.currentFontId += `_feat_${Text.stableStringify(loadedFont.fontFeatures)}`;
|
|
5167
5333
|
}
|
|
5168
5334
|
}
|
|
5169
5335
|
async loadFont(fontSrc, fontVariations, fontFeatures) {
|
|
@@ -5193,10 +5359,10 @@ class Text {
|
|
|
5193
5359
|
const contentHash = Text.generateFontContentHash(fontBuffer);
|
|
5194
5360
|
this.currentFontId = `font_${contentHash}`;
|
|
5195
5361
|
if (fontVariations) {
|
|
5196
|
-
this.currentFontId += `_var_${
|
|
5362
|
+
this.currentFontId += `_var_${Text.stableStringify(fontVariations)}`;
|
|
5197
5363
|
}
|
|
5198
5364
|
if (fontFeatures) {
|
|
5199
|
-
this.currentFontId += `_feat_${
|
|
5365
|
+
this.currentFontId += `_feat_${Text.stableStringify(fontFeatures)}`;
|
|
5200
5366
|
}
|
|
5201
5367
|
}
|
|
5202
5368
|
catch (error) {
|
|
@@ -5224,7 +5390,7 @@ class Text {
|
|
|
5224
5390
|
this.updateFontVariations(options);
|
|
5225
5391
|
if (!this.geometryBuilder) {
|
|
5226
5392
|
const cache = options.maxCacheSizeMB
|
|
5227
|
-
?
|
|
5393
|
+
? createGlyphCache(options.maxCacheSizeMB)
|
|
5228
5394
|
: globalGlyphCache;
|
|
5229
5395
|
this.geometryBuilder = new GlyphGeometryBuilder(cache, this.loadedFont);
|
|
5230
5396
|
this.geometryBuilder.setFontId(this.currentFontId);
|
|
@@ -5327,8 +5493,8 @@ class Text {
|
|
|
5327
5493
|
}
|
|
5328
5494
|
updateFontVariations(options) {
|
|
5329
5495
|
if (options.fontVariations && this.loadedFont) {
|
|
5330
|
-
if (
|
|
5331
|
-
|
|
5496
|
+
if (Text.stableStringify(options.fontVariations) !==
|
|
5497
|
+
Text.stableStringify(this.loadedFont.fontVariations || {})) {
|
|
5332
5498
|
this.loadedFont.font.setVariations(options.fontVariations);
|
|
5333
5499
|
this.loadedFont.fontVariations = options.fontVariations;
|
|
5334
5500
|
}
|
|
@@ -5344,7 +5510,12 @@ class Text {
|
|
|
5344
5510
|
if (width !== undefined) {
|
|
5345
5511
|
widthInFontUnits = width * (this.loadedFont.upem / size);
|
|
5346
5512
|
}
|
|
5347
|
-
|
|
5513
|
+
// Keep depth behavior consistent with Extruder: extremely small non-zero depths
|
|
5514
|
+
// are clamped to a minimum back offset so the back face is not coplanar.
|
|
5515
|
+
const depthScale = this.loadedFont.upem / size;
|
|
5516
|
+
const rawDepthInFontUnits = depth * depthScale;
|
|
5517
|
+
const minExtrudeDepth = this.loadedFont.upem * 0.000025;
|
|
5518
|
+
const depthInFontUnits = rawDepthInFontUnits <= 0 ? 0 : Math.max(rawDepthInFontUnits, minExtrudeDepth);
|
|
5348
5519
|
if (!this.textLayout) {
|
|
5349
5520
|
this.textLayout = new TextLayout(this.loadedFont);
|
|
5350
5521
|
}
|
|
@@ -5576,6 +5747,15 @@ class Text {
|
|
|
5576
5747
|
static registerPattern(language, pattern) {
|
|
5577
5748
|
Text.patternCache.set(language, pattern);
|
|
5578
5749
|
}
|
|
5750
|
+
static clearFontCache() {
|
|
5751
|
+
Text.fontCache.clear();
|
|
5752
|
+
Text.fontCacheMemoryBytes = 0;
|
|
5753
|
+
}
|
|
5754
|
+
static setMaxFontCacheMemoryMB(limitMB) {
|
|
5755
|
+
Text.maxFontCacheMemoryBytes =
|
|
5756
|
+
limitMB === Infinity ? Infinity : Math.max(1, Math.floor(limitMB)) * 1024 * 1024;
|
|
5757
|
+
Text.enforceFontCacheMemoryLimit();
|
|
5758
|
+
}
|
|
5579
5759
|
getLoadedFont() {
|
|
5580
5760
|
return this.loadedFont;
|
|
5581
5761
|
}
|
|
@@ -5645,4 +5825,4 @@ class Text {
|
|
|
5645
5825
|
}
|
|
5646
5826
|
}
|
|
5647
5827
|
|
|
5648
|
-
export { DEFAULT_CURVE_FIDELITY, FontMetadataExtractor, Text, globalGlyphCache };
|
|
5828
|
+
export { DEFAULT_CURVE_FIDELITY, FontMetadataExtractor, Text, createGlyphCache, globalGlyphCache };
|