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