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