three-text 0.2.15 → 0.2.17
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 +21 -4
- package/dist/index.cjs +422 -230
- package/dist/index.js +422 -230
- package/dist/index.min.cjs +632 -618
- package/dist/index.min.js +624 -610
- package/dist/index.umd.js +422 -230
- package/dist/index.umd.min.js +632 -618
- package/dist/types/core/cache/GlyphGeometryBuilder.d.ts +1 -0
- package/dist/types/core/geometry/Tessellator.d.ts +3 -2
- package/dist/types/core/layout/LineBreak.d.ts +2 -1
- package/dist/types/core/layout/TextLayout.d.ts +15 -0
- package/dist/types/core/shaping/TextMeasurer.d.ts +1 -0
- package/dist/types/webgpu/index.d.ts +1 -0
- package/dist/webgpu/index.cjs +4 -2
- package/dist/webgpu/index.d.ts +1 -0
- package/dist/webgpu/index.js +4 -2
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* three-text v0.2.
|
|
2
|
+
* three-text v0.2.17
|
|
3
3
|
* Copyright (C) 2025 Countertype LLC
|
|
4
4
|
*
|
|
5
5
|
* This program is free software: you can redistribute it and/or modify
|
|
@@ -196,7 +196,7 @@ var FitnessClass;
|
|
|
196
196
|
FitnessClass[FitnessClass["LOOSE"] = 2] = "LOOSE";
|
|
197
197
|
FitnessClass[FitnessClass["VERY_LOOSE"] = 3] = "VERY_LOOSE";
|
|
198
198
|
})(FitnessClass || (FitnessClass = {}));
|
|
199
|
-
// ActiveNodeList maintains all currently viable breakpoints as we scan through the text
|
|
199
|
+
// ActiveNodeList maintains all currently viable breakpoints as we scan through the text
|
|
200
200
|
// Each node represents a potential break with accumulated demerits (total "cost" from start)
|
|
201
201
|
//
|
|
202
202
|
// Demerits = cumulative penalty score from text start to this break, calculated as:
|
|
@@ -338,9 +338,9 @@ class LineBreak {
|
|
|
338
338
|
// Converts text into items (boxes, glues, penalties) for line breaking
|
|
339
339
|
// The measureText function should return widths that include any letter spacing
|
|
340
340
|
static itemizeText(text, measureText, // function to measure text width
|
|
341
|
-
hyphenate = false, language = 'en-us', availablePatterns, lefthyphenmin = DEFAULT_LEFT_HYPHEN_MIN, righthyphenmin = DEFAULT_RIGHT_HYPHEN_MIN, context) {
|
|
341
|
+
measureTextWidths, hyphenate = false, language = 'en-us', availablePatterns, lefthyphenmin = DEFAULT_LEFT_HYPHEN_MIN, righthyphenmin = DEFAULT_RIGHT_HYPHEN_MIN, context) {
|
|
342
342
|
const items = [];
|
|
343
|
-
items.push(...this.itemizeParagraph(text, measureText, hyphenate, language, availablePatterns, lefthyphenmin, righthyphenmin, context));
|
|
343
|
+
items.push(...this.itemizeParagraph(text, measureText, measureTextWidths, hyphenate, language, availablePatterns, lefthyphenmin, righthyphenmin, context));
|
|
344
344
|
// Final glue and penalty to end the paragraph
|
|
345
345
|
// Use infinite stretch to fill the last line
|
|
346
346
|
items.push({
|
|
@@ -445,9 +445,10 @@ class LineBreak {
|
|
|
445
445
|
return (this.isCJClosingPunctuation(char) || this.isCJOpeningPunctuation(char));
|
|
446
446
|
}
|
|
447
447
|
// CJK (Chinese/Japanese/Korean) character-level itemization with inter-character glue
|
|
448
|
-
static itemizeCJKText(text, measureText, context, startOffset = 0, glueParams) {
|
|
448
|
+
static itemizeCJKText(text, measureText, measureTextWidths, context, startOffset = 0, glueParams) {
|
|
449
449
|
const items = [];
|
|
450
450
|
const chars = Array.from(text);
|
|
451
|
+
const widths = measureTextWidths ? measureTextWidths(text) : null;
|
|
451
452
|
let textPosition = startOffset;
|
|
452
453
|
// Inter-character glue parameters
|
|
453
454
|
let glueWidth;
|
|
@@ -468,7 +469,7 @@ class LineBreak {
|
|
|
468
469
|
const char = chars[i];
|
|
469
470
|
const nextChar = i < chars.length - 1 ? chars[i + 1] : null;
|
|
470
471
|
if (/\s/.test(char)) {
|
|
471
|
-
const width = measureText(char);
|
|
472
|
+
const width = widths ? (widths[i] ?? measureText(char)) : measureText(char);
|
|
472
473
|
items.push({
|
|
473
474
|
type: ItemType.GLUE,
|
|
474
475
|
width,
|
|
@@ -482,7 +483,7 @@ class LineBreak {
|
|
|
482
483
|
}
|
|
483
484
|
items.push({
|
|
484
485
|
type: ItemType.BOX,
|
|
485
|
-
width: measureText(char),
|
|
486
|
+
width: widths ? (widths[i] ?? measureText(char)) : measureText(char),
|
|
486
487
|
text: char,
|
|
487
488
|
originIndex: textPosition
|
|
488
489
|
});
|
|
@@ -513,15 +514,21 @@ class LineBreak {
|
|
|
513
514
|
}
|
|
514
515
|
return items;
|
|
515
516
|
}
|
|
516
|
-
static itemizeParagraph(text, measureText, hyphenate, language, availablePatterns, lefthyphenmin, righthyphenmin, context) {
|
|
517
|
+
static itemizeParagraph(text, measureText, measureTextWidths, hyphenate, language, availablePatterns, lefthyphenmin, righthyphenmin, context) {
|
|
517
518
|
const items = [];
|
|
518
519
|
const chars = Array.from(text);
|
|
519
|
-
// Calculate CJK glue parameters once for consistency across all segments
|
|
520
|
-
|
|
521
|
-
const
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
520
|
+
// Calculate CJK glue parameters lazily and once for consistency across all segments
|
|
521
|
+
let cjkGlueParams;
|
|
522
|
+
const getCjkGlueParams = () => {
|
|
523
|
+
if (!cjkGlueParams) {
|
|
524
|
+
const baseCharWidth = measureText('字');
|
|
525
|
+
cjkGlueParams = {
|
|
526
|
+
width: 0,
|
|
527
|
+
stretch: baseCharWidth * 0.04,
|
|
528
|
+
shrink: baseCharWidth * 0.04
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
return cjkGlueParams;
|
|
525
532
|
};
|
|
526
533
|
let buffer = '';
|
|
527
534
|
let bufferStart = 0;
|
|
@@ -531,7 +538,7 @@ class LineBreak {
|
|
|
531
538
|
if (buffer.length === 0)
|
|
532
539
|
return;
|
|
533
540
|
if (bufferScript === 'cjk') {
|
|
534
|
-
const cjkItems = this.itemizeCJKText(buffer, measureText, context, bufferStart,
|
|
541
|
+
const cjkItems = this.itemizeCJKText(buffer, measureText, measureTextWidths, context, bufferStart, getCjkGlueParams());
|
|
535
542
|
items.push(...cjkItems);
|
|
536
543
|
}
|
|
537
544
|
else {
|
|
@@ -724,7 +731,7 @@ class LineBreak {
|
|
|
724
731
|
align: options.align || 'left',
|
|
725
732
|
hyphenate: options.hyphenate || false
|
|
726
733
|
});
|
|
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;
|
|
734
|
+
const { text, width, align = 'left', direction = 'ltr', hyphenate = false, language = 'en-us', respectExistingBreaks = true, measureText, measureTextWidths, 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;
|
|
728
735
|
// Handle multiple paragraphs by processing each independently
|
|
729
736
|
if (respectExistingBreaks && text.includes('\n')) {
|
|
730
737
|
const paragraphs = text.split('\n');
|
|
@@ -787,9 +794,9 @@ class LineBreak {
|
|
|
787
794
|
exHyphenPenalty: exhyphenpenalty,
|
|
788
795
|
currentAlign: align,
|
|
789
796
|
unitsPerEm,
|
|
790
|
-
// measureText() includes trailing letter spacing after the final glyph of a token
|
|
797
|
+
// measureText() includes trailing letter spacing after the final glyph of a token
|
|
791
798
|
// Shaping applies letter spacing only between glyphs, so we subtract one
|
|
792
|
-
// trailing letterSpacingFU per line segment (see computeAdjustmentRatio/createLines)
|
|
799
|
+
// trailing letterSpacingFU per line segment (see computeAdjustmentRatio/createLines)
|
|
793
800
|
letterSpacingFU: unitsPerEm ? letterSpacing * unitsPerEm : 0
|
|
794
801
|
};
|
|
795
802
|
if (!width || width === Infinity) {
|
|
@@ -808,7 +815,7 @@ class LineBreak {
|
|
|
808
815
|
];
|
|
809
816
|
}
|
|
810
817
|
// Itemize without hyphenation first (TeX approach: only compute if needed)
|
|
811
|
-
const allItems = LineBreak.itemizeText(text, measureText, false, language, hyphenationPatterns, lefthyphenmin, righthyphenmin, context);
|
|
818
|
+
const allItems = LineBreak.itemizeText(text, measureText, measureTextWidths, false, language, hyphenationPatterns, lefthyphenmin, righthyphenmin, context);
|
|
812
819
|
if (allItems.length === 0) {
|
|
813
820
|
return [];
|
|
814
821
|
}
|
|
@@ -827,7 +834,7 @@ class LineBreak {
|
|
|
827
834
|
let breaks = LineBreak.findBreakpoints(currentItems, width, pretolerance, looseness, false, 0, context);
|
|
828
835
|
// Second pass: with hyphenation if first pass failed
|
|
829
836
|
if (breaks.length === 0 && useHyphenation) {
|
|
830
|
-
const itemsWithHyphenation = LineBreak.itemizeText(text, measureText, true, language, hyphenationPatterns, lefthyphenmin, righthyphenmin, context);
|
|
837
|
+
const itemsWithHyphenation = LineBreak.itemizeText(text, measureText, measureTextWidths, true, language, hyphenationPatterns, lefthyphenmin, righthyphenmin, context);
|
|
831
838
|
currentItems = itemsWithHyphenation;
|
|
832
839
|
breaks = LineBreak.findBreakpoints(currentItems, width, tolerance, looseness, false, 0, context);
|
|
833
840
|
}
|
|
@@ -1141,9 +1148,9 @@ class LineBreak {
|
|
|
1141
1148
|
? items[lineEnd].width
|
|
1142
1149
|
: items[lineEnd].preBreakWidth;
|
|
1143
1150
|
}
|
|
1144
|
-
// Correct for trailing letter spacing at the end of the line segment
|
|
1151
|
+
// Correct for trailing letter spacing at the end of the line segment
|
|
1145
1152
|
// Our token measurement includes letter spacing after the final glyph;
|
|
1146
|
-
// shaping does not add letter spacing after the final glyph in a line
|
|
1153
|
+
// shaping does not add letter spacing after the final glyph in a line
|
|
1147
1154
|
if (context?.letterSpacingFU && totalWidth !== 0) {
|
|
1148
1155
|
totalWidth -= context.letterSpacingFU;
|
|
1149
1156
|
}
|
|
@@ -1309,7 +1316,7 @@ class LineBreak {
|
|
|
1309
1316
|
}
|
|
1310
1317
|
}
|
|
1311
1318
|
const lineText = lineTextParts.join('');
|
|
1312
|
-
// Correct for trailing letter spacing at the end of the line
|
|
1319
|
+
// Correct for trailing letter spacing at the end of the line
|
|
1313
1320
|
if (context?.letterSpacingFU && naturalWidth !== 0) {
|
|
1314
1321
|
naturalWidth -= context.letterSpacingFU;
|
|
1315
1322
|
}
|
|
@@ -1366,7 +1373,7 @@ class LineBreak {
|
|
|
1366
1373
|
finalNaturalWidth += item.width;
|
|
1367
1374
|
}
|
|
1368
1375
|
const finalLineText = finalLineTextParts.join('');
|
|
1369
|
-
// Correct for trailing letter spacing at the end of the final line
|
|
1376
|
+
// Correct for trailing letter spacing at the end of the final line
|
|
1370
1377
|
if (context?.letterSpacingFU && finalNaturalWidth !== 0) {
|
|
1371
1378
|
finalNaturalWidth -= context.letterSpacingFU;
|
|
1372
1379
|
}
|
|
@@ -1403,12 +1410,21 @@ class LineBreak {
|
|
|
1403
1410
|
}
|
|
1404
1411
|
}
|
|
1405
1412
|
|
|
1413
|
+
// Memoize conversion per feature-object identity to avoid rebuilding the same
|
|
1414
|
+
// comma-separated string on every HarfBuzz shape call
|
|
1415
|
+
const featureStringCache = new WeakMap();
|
|
1406
1416
|
// Convert feature objects to HarfBuzz comma-separated format
|
|
1407
1417
|
function convertFontFeaturesToString(features) {
|
|
1408
1418
|
if (!features || Object.keys(features).length === 0) {
|
|
1409
1419
|
return undefined;
|
|
1410
1420
|
}
|
|
1421
|
+
const cached = featureStringCache.get(features);
|
|
1422
|
+
if (cached !== undefined) {
|
|
1423
|
+
return cached ?? undefined;
|
|
1424
|
+
}
|
|
1411
1425
|
const featureStrings = [];
|
|
1426
|
+
// Preserve insertion order of the input object
|
|
1427
|
+
// (The public API/tests expect this to be stable and predictable)
|
|
1412
1428
|
for (const [tag, value] of Object.entries(features)) {
|
|
1413
1429
|
if (!/^[a-zA-Z0-9]{4}$/.test(tag)) {
|
|
1414
1430
|
logger.warn(`Invalid OpenType feature tag: "${tag}". Tags must be exactly 4 alphanumeric characters.`);
|
|
@@ -1427,10 +1443,63 @@ function convertFontFeaturesToString(features) {
|
|
|
1427
1443
|
logger.warn(`Invalid value for feature "${tag}": ${value}. Expected boolean or positive number.`);
|
|
1428
1444
|
}
|
|
1429
1445
|
}
|
|
1430
|
-
|
|
1446
|
+
const result = featureStrings.length > 0 ? featureStrings.join(',') : undefined;
|
|
1447
|
+
featureStringCache.set(features, result ?? null);
|
|
1448
|
+
return result;
|
|
1431
1449
|
}
|
|
1432
1450
|
|
|
1433
1451
|
class TextMeasurer {
|
|
1452
|
+
// Shape once and return per-codepoint widths aligned with Array.from(text)
|
|
1453
|
+
// Groups glyph advances by HarfBuzz cluster (cl)
|
|
1454
|
+
// Includes trailing per-glyph letter spacing like measureTextWidth
|
|
1455
|
+
static measureTextWidths(loadedFont, text, letterSpacing = 0) {
|
|
1456
|
+
const chars = Array.from(text);
|
|
1457
|
+
if (chars.length === 0)
|
|
1458
|
+
return [];
|
|
1459
|
+
// HarfBuzz clusters are UTF-16 code unit indices
|
|
1460
|
+
const startToCharIndex = new Map();
|
|
1461
|
+
let codeUnitIndex = 0;
|
|
1462
|
+
for (let i = 0; i < chars.length; i++) {
|
|
1463
|
+
startToCharIndex.set(codeUnitIndex, i);
|
|
1464
|
+
codeUnitIndex += chars[i].length;
|
|
1465
|
+
}
|
|
1466
|
+
const widths = new Array(chars.length).fill(0);
|
|
1467
|
+
const buffer = loadedFont.hb.createBuffer();
|
|
1468
|
+
try {
|
|
1469
|
+
buffer.addText(text);
|
|
1470
|
+
buffer.guessSegmentProperties();
|
|
1471
|
+
const featuresString = convertFontFeaturesToString(loadedFont.fontFeatures);
|
|
1472
|
+
loadedFont.hb.shape(loadedFont.font, buffer, featuresString);
|
|
1473
|
+
const glyphInfos = buffer.json(loadedFont.font);
|
|
1474
|
+
const letterSpacingInFontUnits = letterSpacing * loadedFont.upem;
|
|
1475
|
+
for (let i = 0; i < glyphInfos.length; i++) {
|
|
1476
|
+
const glyph = glyphInfos[i];
|
|
1477
|
+
const cl = glyph.cl ?? 0;
|
|
1478
|
+
let charIndex = startToCharIndex.get(cl);
|
|
1479
|
+
// Fallback if cl lands mid-codepoint
|
|
1480
|
+
if (charIndex === undefined) {
|
|
1481
|
+
// Find the closest start <= cl
|
|
1482
|
+
for (let back = cl; back >= 0; back--) {
|
|
1483
|
+
const candidate = startToCharIndex.get(back);
|
|
1484
|
+
if (candidate !== undefined) {
|
|
1485
|
+
charIndex = candidate;
|
|
1486
|
+
break;
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
if (charIndex === undefined)
|
|
1491
|
+
continue;
|
|
1492
|
+
widths[charIndex] += glyph.ax;
|
|
1493
|
+
if (letterSpacingInFontUnits !== 0) {
|
|
1494
|
+
widths[charIndex] += letterSpacingInFontUnits;
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
return widths;
|
|
1498
|
+
}
|
|
1499
|
+
finally {
|
|
1500
|
+
buffer.destroy();
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1434
1503
|
static measureTextWidth(loadedFont, text, letterSpacing = 0) {
|
|
1435
1504
|
const buffer = loadedFont.hb.createBuffer();
|
|
1436
1505
|
buffer.addText(text);
|
|
@@ -1487,7 +1556,8 @@ class TextLayout {
|
|
|
1487
1556
|
unitsPerEm: this.loadedFont.upem,
|
|
1488
1557
|
letterSpacing,
|
|
1489
1558
|
measureText: (textToMeasure) => TextMeasurer.measureTextWidth(this.loadedFont, textToMeasure, letterSpacing // Letter spacing included in width measurements
|
|
1490
|
-
)
|
|
1559
|
+
),
|
|
1560
|
+
measureTextWidths: (textToMeasure) => TextMeasurer.measureTextWidths(this.loadedFont, textToMeasure, letterSpacing)
|
|
1491
1561
|
});
|
|
1492
1562
|
}
|
|
1493
1563
|
else {
|
|
@@ -1509,6 +1579,15 @@ class TextLayout {
|
|
|
1509
1579
|
return { lines };
|
|
1510
1580
|
}
|
|
1511
1581
|
applyAlignment(vertices, options) {
|
|
1582
|
+
const { offset, adjustedBounds } = this.computeAlignmentOffset(options);
|
|
1583
|
+
if (offset !== 0) {
|
|
1584
|
+
for (let i = 0; i < vertices.length; i += 3) {
|
|
1585
|
+
vertices[i] += offset;
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
return { offset, adjustedBounds };
|
|
1589
|
+
}
|
|
1590
|
+
computeAlignmentOffset(options) {
|
|
1512
1591
|
const { width, align, planeBounds } = options;
|
|
1513
1592
|
let offset = 0;
|
|
1514
1593
|
const adjustedBounds = {
|
|
@@ -1520,17 +1599,13 @@ class TextLayout {
|
|
|
1520
1599
|
if (align === 'center') {
|
|
1521
1600
|
offset = (width - lineWidth) / 2 - planeBounds.min.x;
|
|
1522
1601
|
}
|
|
1523
|
-
else
|
|
1602
|
+
else {
|
|
1524
1603
|
offset = width - planeBounds.max.x;
|
|
1525
1604
|
}
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
}
|
|
1531
|
-
adjustedBounds.min.x += offset;
|
|
1532
|
-
adjustedBounds.max.x += offset;
|
|
1533
|
-
}
|
|
1605
|
+
}
|
|
1606
|
+
if (offset !== 0) {
|
|
1607
|
+
adjustedBounds.min.x += offset;
|
|
1608
|
+
adjustedBounds.max.x += offset;
|
|
1534
1609
|
}
|
|
1535
1610
|
return { offset, adjustedBounds };
|
|
1536
1611
|
}
|
|
@@ -2625,7 +2700,7 @@ var n;function t(a,b){return a.b===b.b&&a.a===b.a}function u(a,b){return a.b<b.b
|
|
|
2625
2700
|
var libtess_minExports = libtess_min.exports;
|
|
2626
2701
|
|
|
2627
2702
|
class Tessellator {
|
|
2628
|
-
process(paths, removeOverlaps = true, isCFF = false) {
|
|
2703
|
+
process(paths, removeOverlaps = true, isCFF = false, needsExtrusionContours = true) {
|
|
2629
2704
|
if (paths.length === 0) {
|
|
2630
2705
|
return { triangles: { vertices: [], indices: [] }, contours: [] };
|
|
2631
2706
|
}
|
|
@@ -2634,66 +2709,124 @@ class Tessellator {
|
|
|
2634
2709
|
return { triangles: { vertices: [], indices: [] }, contours: [] };
|
|
2635
2710
|
}
|
|
2636
2711
|
logger.log(`Tessellator: removeOverlaps=${removeOverlaps}, processing ${valid.length} paths`);
|
|
2637
|
-
return this.tessellate(valid, removeOverlaps, isCFF);
|
|
2638
|
-
}
|
|
2639
|
-
tessellate(paths, removeOverlaps, isCFF) {
|
|
2640
|
-
//
|
|
2641
|
-
const
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2712
|
+
return this.tessellate(valid, removeOverlaps, isCFF, needsExtrusionContours);
|
|
2713
|
+
}
|
|
2714
|
+
tessellate(paths, removeOverlaps, isCFF, needsExtrusionContours) {
|
|
2715
|
+
// libtess expects CCW winding; TTF outer contours are CW
|
|
2716
|
+
const needsWindingReversal = !isCFF && !removeOverlaps;
|
|
2717
|
+
let originalContours;
|
|
2718
|
+
let tessContours;
|
|
2719
|
+
if (needsWindingReversal) {
|
|
2720
|
+
tessContours = this.pathsToContours(paths, true);
|
|
2721
|
+
if (removeOverlaps || needsExtrusionContours) {
|
|
2722
|
+
originalContours = this.pathsToContours(paths);
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
else {
|
|
2726
|
+
originalContours = this.pathsToContours(paths);
|
|
2727
|
+
tessContours = originalContours;
|
|
2728
|
+
}
|
|
2729
|
+
let extrusionContours = needsExtrusionContours
|
|
2730
|
+
? originalContours ?? this.pathsToContours(paths)
|
|
2731
|
+
: [];
|
|
2645
2732
|
if (removeOverlaps) {
|
|
2646
2733
|
logger.log('Two-pass: boundary extraction then triangulation');
|
|
2647
|
-
// Extract boundaries to remove overlaps
|
|
2648
2734
|
perfLogger.start('Tessellator.boundaryPass', {
|
|
2649
|
-
contourCount:
|
|
2735
|
+
contourCount: tessContours.length
|
|
2650
2736
|
});
|
|
2651
|
-
const boundaryResult = this.performTessellation(
|
|
2737
|
+
const boundaryResult = this.performTessellation(originalContours, 'boundary');
|
|
2652
2738
|
perfLogger.end('Tessellator.boundaryPass');
|
|
2653
2739
|
if (!boundaryResult) {
|
|
2654
2740
|
logger.warn('libtess returned empty result from boundary pass');
|
|
2655
2741
|
return { triangles: { vertices: [], indices: [] }, contours: [] };
|
|
2656
2742
|
}
|
|
2657
|
-
//
|
|
2658
|
-
|
|
2659
|
-
|
|
2743
|
+
// Boundary pass normalizes winding (outer CCW, holes CW)
|
|
2744
|
+
tessContours = this.boundaryToContours(boundaryResult);
|
|
2745
|
+
if (needsExtrusionContours) {
|
|
2746
|
+
extrusionContours = tessContours;
|
|
2747
|
+
}
|
|
2748
|
+
logger.log(`Boundary pass created ${tessContours.length} contours. Starting triangulation pass.`);
|
|
2660
2749
|
}
|
|
2661
2750
|
else {
|
|
2662
2751
|
logger.log(`Single-pass triangulation for ${isCFF ? 'CFF' : 'TTF'}`);
|
|
2752
|
+
// TTF contours may have inconsistent winding; check if we need normalization
|
|
2753
|
+
if (needsExtrusionContours && !isCFF) {
|
|
2754
|
+
const needsNormalization = this.needsWindingNormalization(extrusionContours);
|
|
2755
|
+
if (needsNormalization) {
|
|
2756
|
+
logger.log('Complex topology detected, running boundary pass for winding normalization');
|
|
2757
|
+
perfLogger.start('Tessellator.windingNormalization', {
|
|
2758
|
+
contourCount: extrusionContours.length
|
|
2759
|
+
});
|
|
2760
|
+
const boundaryResult = this.performTessellation(extrusionContours, 'boundary');
|
|
2761
|
+
perfLogger.end('Tessellator.windingNormalization');
|
|
2762
|
+
if (boundaryResult) {
|
|
2763
|
+
extrusionContours = this.boundaryToContours(boundaryResult);
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2766
|
+
else {
|
|
2767
|
+
logger.log('Simple topology, skipping winding normalization');
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2663
2770
|
}
|
|
2664
|
-
// Triangulate the contours
|
|
2665
2771
|
perfLogger.start('Tessellator.triangulationPass', {
|
|
2666
|
-
contourCount:
|
|
2772
|
+
contourCount: tessContours.length
|
|
2667
2773
|
});
|
|
2668
|
-
const triangleResult = this.performTessellation(
|
|
2774
|
+
const triangleResult = this.performTessellation(tessContours, 'triangles');
|
|
2669
2775
|
perfLogger.end('Tessellator.triangulationPass');
|
|
2670
2776
|
if (!triangleResult) {
|
|
2671
2777
|
const warning = removeOverlaps
|
|
2672
2778
|
? 'libtess returned empty result from triangulation pass'
|
|
2673
2779
|
: 'libtess returned empty result from single-pass triangulation';
|
|
2674
2780
|
logger.warn(warning);
|
|
2675
|
-
return { triangles: { vertices: [], indices: [] }, contours };
|
|
2781
|
+
return { triangles: { vertices: [], indices: [] }, contours: extrusionContours };
|
|
2676
2782
|
}
|
|
2677
2783
|
return {
|
|
2678
2784
|
triangles: {
|
|
2679
2785
|
vertices: triangleResult.vertices,
|
|
2680
2786
|
indices: triangleResult.indices || []
|
|
2681
2787
|
},
|
|
2682
|
-
contours
|
|
2788
|
+
contours: extrusionContours
|
|
2683
2789
|
};
|
|
2684
2790
|
}
|
|
2685
|
-
pathsToContours(paths) {
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2791
|
+
pathsToContours(paths, reversePoints = false) {
|
|
2792
|
+
const contours = new Array(paths.length);
|
|
2793
|
+
for (let p = 0; p < paths.length; p++) {
|
|
2794
|
+
const points = paths[p].points;
|
|
2795
|
+
const pointCount = points.length;
|
|
2796
|
+
// Clipper-style paths can be explicitly closed by repeating the first point at the end
|
|
2797
|
+
// Normalize to a single closing vertex for stable side wall generation
|
|
2798
|
+
const isClosed = pointCount > 1 &&
|
|
2799
|
+
points[0].x === points[pointCount - 1].x &&
|
|
2800
|
+
points[0].y === points[pointCount - 1].y;
|
|
2801
|
+
const end = isClosed ? pointCount - 1 : pointCount;
|
|
2802
|
+
// +1 to append a closing vertex
|
|
2803
|
+
const contour = new Array((end + 1) * 2);
|
|
2804
|
+
let i = 0;
|
|
2805
|
+
if (reversePoints) {
|
|
2806
|
+
for (let k = end - 1; k >= 0; k--) {
|
|
2807
|
+
const pt = points[k];
|
|
2808
|
+
contour[i++] = pt.x;
|
|
2809
|
+
contour[i++] = pt.y;
|
|
2810
|
+
}
|
|
2690
2811
|
}
|
|
2691
|
-
|
|
2692
|
-
|
|
2812
|
+
else {
|
|
2813
|
+
for (let k = 0; k < end; k++) {
|
|
2814
|
+
const pt = points[k];
|
|
2815
|
+
contour[i++] = pt.x;
|
|
2816
|
+
contour[i++] = pt.y;
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
// Some glyphs omit closePath, leaving gaps in extruded side walls
|
|
2820
|
+
if (i >= 2) {
|
|
2821
|
+
contour[i++] = contour[0];
|
|
2822
|
+
contour[i++] = contour[1];
|
|
2823
|
+
}
|
|
2824
|
+
contours[p] = contour;
|
|
2825
|
+
}
|
|
2826
|
+
return contours;
|
|
2693
2827
|
}
|
|
2694
2828
|
performTessellation(contours, mode) {
|
|
2695
2829
|
const tess = new libtess_minExports.GluTesselator();
|
|
2696
|
-
// Set winding rule to NON-ZERO
|
|
2697
2830
|
tess.gluTessProperty(libtess_minExports.gluEnum.GLU_TESS_WINDING_RULE, libtess_minExports.windingRule.GLU_TESS_WINDING_NONZERO);
|
|
2698
2831
|
const vertices = [];
|
|
2699
2832
|
const indices = [];
|
|
@@ -2716,7 +2849,7 @@ class Tessellator {
|
|
|
2716
2849
|
});
|
|
2717
2850
|
tess.gluTessCallback(libtess_minExports.gluEnum.GLU_TESS_END, () => {
|
|
2718
2851
|
if (currentContour.length > 0) {
|
|
2719
|
-
contourIndices.push(
|
|
2852
|
+
contourIndices.push(currentContour);
|
|
2720
2853
|
}
|
|
2721
2854
|
});
|
|
2722
2855
|
}
|
|
@@ -2761,7 +2894,6 @@ class Tessellator {
|
|
|
2761
2894
|
const vertIdx = idx * 2;
|
|
2762
2895
|
contour.push(boundaryResult.vertices[vertIdx], boundaryResult.vertices[vertIdx + 1]);
|
|
2763
2896
|
}
|
|
2764
|
-
// Ensure contour is closed for side wall generation
|
|
2765
2897
|
if (contour.length > 2) {
|
|
2766
2898
|
if (contour[0] !== contour[contour.length - 2] ||
|
|
2767
2899
|
contour[1] !== contour[contour.length - 1]) {
|
|
@@ -2772,11 +2904,45 @@ class Tessellator {
|
|
|
2772
2904
|
}
|
|
2773
2905
|
return contours;
|
|
2774
2906
|
}
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2907
|
+
// Check if contours need winding normalization via boundary pass
|
|
2908
|
+
// Returns false if topology is simple enough to skip the expensive pass
|
|
2909
|
+
needsWindingNormalization(contours) {
|
|
2910
|
+
if (contours.length === 0)
|
|
2911
|
+
return false;
|
|
2912
|
+
// Heuristic 1: Single contour never needs normalization
|
|
2913
|
+
if (contours.length === 1)
|
|
2914
|
+
return false;
|
|
2915
|
+
// Heuristic 2: All same winding = all outers, no holes
|
|
2916
|
+
// Compute signed areas
|
|
2917
|
+
let firstSign = null;
|
|
2918
|
+
for (const contour of contours) {
|
|
2919
|
+
const area = this.signedArea(contour);
|
|
2920
|
+
const sign = area >= 0 ? 1 : -1;
|
|
2921
|
+
if (firstSign === null) {
|
|
2922
|
+
firstSign = sign;
|
|
2923
|
+
}
|
|
2924
|
+
else if (sign !== firstSign) {
|
|
2925
|
+
// Mixed winding detected → might have holes or complex topology
|
|
2926
|
+
return true;
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
// All same winding → simple topology, no normalization needed
|
|
2930
|
+
return false;
|
|
2931
|
+
}
|
|
2932
|
+
// Compute signed area (CCW = positive, CW = negative)
|
|
2933
|
+
signedArea(contour) {
|
|
2934
|
+
let area = 0;
|
|
2935
|
+
const len = contour.length;
|
|
2936
|
+
if (len < 6)
|
|
2937
|
+
return 0; // Need at least 3 points
|
|
2938
|
+
for (let i = 0; i < len; i += 2) {
|
|
2939
|
+
const x1 = contour[i];
|
|
2940
|
+
const y1 = contour[i + 1];
|
|
2941
|
+
const x2 = contour[(i + 2) % len];
|
|
2942
|
+
const y2 = contour[(i + 3) % len];
|
|
2943
|
+
area += x1 * y2 - x2 * y1;
|
|
2944
|
+
}
|
|
2945
|
+
return area / 2;
|
|
2780
2946
|
}
|
|
2781
2947
|
}
|
|
2782
2948
|
|
|
@@ -2786,12 +2952,11 @@ class Extruder {
|
|
|
2786
2952
|
const points = geometry.triangles.vertices;
|
|
2787
2953
|
const triangleIndices = geometry.triangles.indices;
|
|
2788
2954
|
const numPoints = points.length / 2;
|
|
2789
|
-
// Count side-wall segments (
|
|
2955
|
+
// Count side-wall segments (4 vertices + 6 indices per segment)
|
|
2790
2956
|
let sideSegments = 0;
|
|
2791
2957
|
if (depth !== 0) {
|
|
2792
2958
|
for (const contour of geometry.contours) {
|
|
2793
|
-
//
|
|
2794
|
-
// Contours are expected to be closed (last point repeats first), so segments = (nPoints - 1)
|
|
2959
|
+
// Contours are closed (last point repeats first)
|
|
2795
2960
|
const contourPoints = contour.length / 2;
|
|
2796
2961
|
if (contourPoints >= 2)
|
|
2797
2962
|
sideSegments += contourPoints - 1;
|
|
@@ -2807,7 +2972,7 @@ class Extruder {
|
|
|
2807
2972
|
: triangleIndices.length * 2 + sideSegments * 6;
|
|
2808
2973
|
const indices = new Uint32Array(indexCount);
|
|
2809
2974
|
if (depth === 0) {
|
|
2810
|
-
//
|
|
2975
|
+
// Single-sided flat geometry at z=0
|
|
2811
2976
|
let vPos = 0;
|
|
2812
2977
|
for (let i = 0; i < points.length; i += 2) {
|
|
2813
2978
|
vertices[vPos] = points[i];
|
|
@@ -2818,42 +2983,44 @@ class Extruder {
|
|
|
2818
2983
|
normals[vPos + 2] = 1;
|
|
2819
2984
|
vPos += 3;
|
|
2820
2985
|
}
|
|
2986
|
+
// libtess outputs CCW, use as-is for +Z facing geometry
|
|
2821
2987
|
for (let i = 0; i < triangleIndices.length; i++) {
|
|
2822
2988
|
indices[i] = triangleIndices[i];
|
|
2823
2989
|
}
|
|
2824
2990
|
return { vertices, normals, indices };
|
|
2825
2991
|
}
|
|
2826
|
-
//
|
|
2992
|
+
// Extruded geometry: front at z=0, back at z=depth
|
|
2827
2993
|
const minBackOffset = unitsPerEm * 0.000025;
|
|
2828
2994
|
const backZ = depth <= minBackOffset ? minBackOffset : depth;
|
|
2829
|
-
//
|
|
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)
|
|
2995
|
+
// Generate both caps in one pass
|
|
2840
2996
|
for (let p = 0, vi = 0; p < points.length; p += 2, vi++) {
|
|
2841
|
-
const
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2997
|
+
const x = points[p];
|
|
2998
|
+
const y = points[p + 1];
|
|
2999
|
+
// Cap at z=0
|
|
3000
|
+
const base0 = vi * 3;
|
|
3001
|
+
vertices[base0] = x;
|
|
3002
|
+
vertices[base0 + 1] = y;
|
|
3003
|
+
vertices[base0 + 2] = 0;
|
|
3004
|
+
normals[base0] = 0;
|
|
3005
|
+
normals[base0 + 1] = 0;
|
|
3006
|
+
normals[base0 + 2] = -1;
|
|
3007
|
+
// Cap at z=depth
|
|
3008
|
+
const baseD = (numPoints + vi) * 3;
|
|
3009
|
+
vertices[baseD] = x;
|
|
3010
|
+
vertices[baseD + 1] = y;
|
|
3011
|
+
vertices[baseD + 2] = backZ;
|
|
3012
|
+
normals[baseD] = 0;
|
|
3013
|
+
normals[baseD + 1] = 0;
|
|
3014
|
+
normals[baseD + 2] = 1;
|
|
3015
|
+
}
|
|
3016
|
+
// libtess outputs CCW triangles (viewed from +Z)
|
|
3017
|
+
// Z=0 cap faces -Z, reverse winding
|
|
2850
3018
|
for (let i = 0; i < triangleIndices.length; i++) {
|
|
2851
|
-
indices[i] = triangleIndices[i];
|
|
3019
|
+
indices[i] = triangleIndices[triangleIndices.length - 1 - i];
|
|
2852
3020
|
}
|
|
2853
|
-
//
|
|
3021
|
+
// Z=depth cap faces +Z, use original winding
|
|
2854
3022
|
for (let i = 0; i < triangleIndices.length; i++) {
|
|
2855
|
-
indices[triangleIndices.length + i] =
|
|
2856
|
-
triangleIndices[triangleIndices.length - 1 - i] + numPoints;
|
|
3023
|
+
indices[triangleIndices.length + i] = triangleIndices[i] + numPoints;
|
|
2857
3024
|
}
|
|
2858
3025
|
// Side walls
|
|
2859
3026
|
let nextVertex = numPoints * 2;
|
|
@@ -2864,7 +3031,7 @@ class Extruder {
|
|
|
2864
3031
|
const p0y = contour[i + 1];
|
|
2865
3032
|
const p1x = contour[i + 2];
|
|
2866
3033
|
const p1y = contour[i + 3];
|
|
2867
|
-
//
|
|
3034
|
+
// Perpendicular normal for this wall segment
|
|
2868
3035
|
const ex = p1x - p0x;
|
|
2869
3036
|
const ey = p1y - p0y;
|
|
2870
3037
|
const lenSq = ex * ex + ey * ey;
|
|
@@ -2877,7 +3044,7 @@ class Extruder {
|
|
|
2877
3044
|
}
|
|
2878
3045
|
const baseVertex = nextVertex;
|
|
2879
3046
|
const base = baseVertex * 3;
|
|
2880
|
-
//
|
|
3047
|
+
// Wall quad: front edge at z=0, back edge at z=depth
|
|
2881
3048
|
vertices[base] = p0x;
|
|
2882
3049
|
vertices[base + 1] = p0y;
|
|
2883
3050
|
vertices[base + 2] = 0;
|
|
@@ -2890,7 +3057,7 @@ class Extruder {
|
|
|
2890
3057
|
vertices[base + 9] = p1x;
|
|
2891
3058
|
vertices[base + 10] = p1y;
|
|
2892
3059
|
vertices[base + 11] = backZ;
|
|
2893
|
-
//
|
|
3060
|
+
// Wall normals point perpendicular to edge
|
|
2894
3061
|
normals[base] = nx;
|
|
2895
3062
|
normals[base + 1] = ny;
|
|
2896
3063
|
normals[base + 2] = 0;
|
|
@@ -2903,7 +3070,7 @@ class Extruder {
|
|
|
2903
3070
|
normals[base + 9] = nx;
|
|
2904
3071
|
normals[base + 10] = ny;
|
|
2905
3072
|
normals[base + 11] = 0;
|
|
2906
|
-
//
|
|
3073
|
+
// Two triangles per wall segment
|
|
2907
3074
|
indices[idxPos++] = baseVertex;
|
|
2908
3075
|
indices[idxPos++] = baseVertex + 1;
|
|
2909
3076
|
indices[idxPos++] = baseVertex + 2;
|
|
@@ -3138,21 +3305,23 @@ class PathOptimizer {
|
|
|
3138
3305
|
return path;
|
|
3139
3306
|
}
|
|
3140
3307
|
this.stats.originalPointCount += path.points.length;
|
|
3141
|
-
|
|
3308
|
+
// Most paths are already immutable after collection; avoid copying large point arrays
|
|
3309
|
+
// The optimizers below never mutate the input `points` array
|
|
3310
|
+
const points = path.points;
|
|
3142
3311
|
if (points.length < 5) {
|
|
3143
3312
|
return path;
|
|
3144
3313
|
}
|
|
3145
|
-
|
|
3146
|
-
if (
|
|
3314
|
+
let optimized = this.simplifyPathVW(points, this.config.areaThreshold);
|
|
3315
|
+
if (optimized.length < 3) {
|
|
3147
3316
|
return path;
|
|
3148
3317
|
}
|
|
3149
|
-
|
|
3150
|
-
if (
|
|
3318
|
+
optimized = this.removeColinearPoints(optimized, this.config.colinearThreshold);
|
|
3319
|
+
if (optimized.length < 3) {
|
|
3151
3320
|
return path;
|
|
3152
3321
|
}
|
|
3153
3322
|
return {
|
|
3154
3323
|
...path,
|
|
3155
|
-
points
|
|
3324
|
+
points: optimized
|
|
3156
3325
|
};
|
|
3157
3326
|
}
|
|
3158
3327
|
// Visvalingam-Whyatt algorithm
|
|
@@ -3606,7 +3775,7 @@ class GlyphContourCollector {
|
|
|
3606
3775
|
if (this.currentGlyphPaths.length > 0) {
|
|
3607
3776
|
this.collectedGlyphs.push({
|
|
3608
3777
|
glyphId: this.currentGlyphId,
|
|
3609
|
-
paths:
|
|
3778
|
+
paths: this.currentGlyphPaths,
|
|
3610
3779
|
bounds: {
|
|
3611
3780
|
min: {
|
|
3612
3781
|
x: this.currentGlyphBounds.min.x,
|
|
@@ -3658,11 +3827,10 @@ class GlyphContourCollector {
|
|
|
3658
3827
|
return;
|
|
3659
3828
|
}
|
|
3660
3829
|
const flattenedPoints = this.polygonizer.polygonizeQuadratic(start, control, end);
|
|
3661
|
-
for (const point of flattenedPoints) {
|
|
3662
|
-
this.updateBounds(point);
|
|
3663
|
-
}
|
|
3664
3830
|
for (let i = 0; i < flattenedPoints.length; i++) {
|
|
3665
|
-
|
|
3831
|
+
const pt = flattenedPoints[i];
|
|
3832
|
+
this.updateBounds(pt);
|
|
3833
|
+
this.currentPath.points.push(pt);
|
|
3666
3834
|
}
|
|
3667
3835
|
this.currentPoint = end;
|
|
3668
3836
|
}
|
|
@@ -3682,11 +3850,10 @@ class GlyphContourCollector {
|
|
|
3682
3850
|
return;
|
|
3683
3851
|
}
|
|
3684
3852
|
const flattenedPoints = this.polygonizer.polygonizeCubic(start, control1, control2, end);
|
|
3685
|
-
for (const point of flattenedPoints) {
|
|
3686
|
-
this.updateBounds(point);
|
|
3687
|
-
}
|
|
3688
3853
|
for (let i = 0; i < flattenedPoints.length; i++) {
|
|
3689
|
-
|
|
3854
|
+
const pt = flattenedPoints[i];
|
|
3855
|
+
this.updateBounds(pt);
|
|
3856
|
+
this.currentPath.points.push(pt);
|
|
3690
3857
|
}
|
|
3691
3858
|
this.currentPoint = end;
|
|
3692
3859
|
}
|
|
@@ -3876,6 +4043,7 @@ class GlyphGeometryBuilder {
|
|
|
3876
4043
|
constructor(cache, loadedFont) {
|
|
3877
4044
|
this.fontId = 'default';
|
|
3878
4045
|
this.cacheKeyPrefix = 'default';
|
|
4046
|
+
this.emptyGlyphs = new Set();
|
|
3879
4047
|
this.cache = cache;
|
|
3880
4048
|
this.loadedFont = loadedFont;
|
|
3881
4049
|
this.tessellator = new Tessellator();
|
|
@@ -3929,63 +4097,34 @@ class GlyphGeometryBuilder {
|
|
|
3929
4097
|
}
|
|
3930
4098
|
// Build instanced geometry from glyph contours
|
|
3931
4099
|
buildInstancedGeometry(clustersByLine, depth, removeOverlaps, isCFF, separateGlyphs = false, coloredTextIndices) {
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
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
|
-
}
|
|
4100
|
+
if (isLogEnabled) {
|
|
4101
|
+
let wordCount = 0;
|
|
4102
|
+
for (let i = 0; i < clustersByLine.length; i++) {
|
|
4103
|
+
wordCount += clustersByLine[i].length;
|
|
4104
|
+
}
|
|
4105
|
+
perfLogger.start('GlyphGeometryBuilder.buildInstancedGeometry', {
|
|
4106
|
+
lineCount: clustersByLine.length,
|
|
4107
|
+
wordCount,
|
|
4108
|
+
depth,
|
|
4109
|
+
removeOverlaps
|
|
4110
|
+
});
|
|
4111
|
+
}
|
|
4112
|
+
else {
|
|
4113
|
+
perfLogger.start('GlyphGeometryBuilder.buildInstancedGeometry');
|
|
4114
|
+
}
|
|
4115
|
+
const tasks = [];
|
|
4116
|
+
let totalVertexFloats = 0;
|
|
4117
|
+
let totalNormalFloats = 0;
|
|
4118
|
+
let totalIndexCount = 0;
|
|
4119
|
+
let vertexCursor = 0; // vertex offset (not float offset)
|
|
4120
|
+
const pushTask = (data, px, py, pz) => {
|
|
4121
|
+
const vertexStart = vertexCursor;
|
|
4122
|
+
tasks.push({ data, px, py, pz, vertexStart });
|
|
4123
|
+
totalVertexFloats += data.vertices.length;
|
|
4124
|
+
totalNormalFloats += data.normals.length;
|
|
4125
|
+
totalIndexCount += data.indices.length;
|
|
4126
|
+
vertexCursor += data.vertices.length / 3;
|
|
4127
|
+
return vertexStart;
|
|
3989
4128
|
};
|
|
3990
4129
|
const glyphInfos = [];
|
|
3991
4130
|
const planeBounds = {
|
|
@@ -3995,6 +4134,9 @@ class GlyphGeometryBuilder {
|
|
|
3995
4134
|
for (let lineIndex = 0; lineIndex < clustersByLine.length; lineIndex++) {
|
|
3996
4135
|
const line = clustersByLine[lineIndex];
|
|
3997
4136
|
for (const cluster of line) {
|
|
4137
|
+
const clusterX = cluster.position.x;
|
|
4138
|
+
const clusterY = cluster.position.y;
|
|
4139
|
+
const clusterZ = cluster.position.z;
|
|
3998
4140
|
const clusterGlyphContours = [];
|
|
3999
4141
|
for (const glyph of cluster.glyphs) {
|
|
4000
4142
|
clusterGlyphContours.push(this.getContoursForGlyph(glyph.g));
|
|
@@ -4035,7 +4177,7 @@ class GlyphGeometryBuilder {
|
|
|
4035
4177
|
// Use glyph-level caching when separateGlyphs is set or when cluster contains colored text
|
|
4036
4178
|
const forceSeparate = separateGlyphs || clusterHasColoredGlyphs;
|
|
4037
4179
|
// Iterate over the geometric groups identified by BoundaryClusterer
|
|
4038
|
-
// logical groups (words)
|
|
4180
|
+
// logical groups (words) split into geometric sub-groups (e.g. "aa", "XX", "bb")
|
|
4039
4181
|
for (const groupIndices of boundaryGroups) {
|
|
4040
4182
|
const isOverlappingGroup = groupIndices.length > 1;
|
|
4041
4183
|
const shouldCluster = isOverlappingGroup && !forceSeparate;
|
|
@@ -4067,16 +4209,19 @@ class GlyphGeometryBuilder {
|
|
|
4067
4209
|
// Calculate the absolute position of this sub-cluster based on its first glyph
|
|
4068
4210
|
// (since the cached geometry is relative to that first glyph)
|
|
4069
4211
|
const firstGlyphInGroup = subClusterGlyphs[0];
|
|
4070
|
-
const
|
|
4071
|
-
const
|
|
4072
|
-
|
|
4212
|
+
const groupPosX = clusterX + (firstGlyphInGroup.x ?? 0);
|
|
4213
|
+
const groupPosY = clusterY + (firstGlyphInGroup.y ?? 0);
|
|
4214
|
+
const groupPosZ = clusterZ;
|
|
4215
|
+
const vertexStart = pushTask(cachedCluster, groupPosX, groupPosY, groupPosZ);
|
|
4073
4216
|
const clusterVertexCount = cachedCluster.vertices.length / 3;
|
|
4074
4217
|
for (let i = 0; i < groupIndices.length; i++) {
|
|
4075
4218
|
const originalIndex = groupIndices[i];
|
|
4076
4219
|
const glyph = cluster.glyphs[originalIndex];
|
|
4077
4220
|
const glyphContours = clusterGlyphContours[originalIndex];
|
|
4078
|
-
const
|
|
4079
|
-
const
|
|
4221
|
+
const glyphPosX = clusterX + (glyph.x ?? 0);
|
|
4222
|
+
const glyphPosY = clusterY + (glyph.y ?? 0);
|
|
4223
|
+
const glyphPosZ = clusterZ;
|
|
4224
|
+
const glyphInfo = this.createGlyphInfo(glyph, vertexStart, clusterVertexCount, glyphPosX, glyphPosY, glyphPosZ, glyphContours, depth);
|
|
4080
4225
|
glyphInfos.push(glyphInfo);
|
|
4081
4226
|
this.updatePlaneBounds(glyphInfo.bounds, planeBounds);
|
|
4082
4227
|
}
|
|
@@ -4086,24 +4231,26 @@ class GlyphGeometryBuilder {
|
|
|
4086
4231
|
for (const i of groupIndices) {
|
|
4087
4232
|
const glyph = cluster.glyphs[i];
|
|
4088
4233
|
const glyphContours = clusterGlyphContours[i];
|
|
4089
|
-
const
|
|
4234
|
+
const glyphPosX = clusterX + (glyph.x ?? 0);
|
|
4235
|
+
const glyphPosY = clusterY + (glyph.y ?? 0);
|
|
4236
|
+
const glyphPosZ = clusterZ;
|
|
4090
4237
|
// Skip glyphs with no paths (spaces, zero-width characters, etc.)
|
|
4091
4238
|
if (glyphContours.paths.length === 0) {
|
|
4092
|
-
const glyphInfo = this.createGlyphInfo(glyph, 0, 0,
|
|
4239
|
+
const glyphInfo = this.createGlyphInfo(glyph, 0, 0, glyphPosX, glyphPosY, glyphPosZ, glyphContours, depth);
|
|
4093
4240
|
glyphInfos.push(glyphInfo);
|
|
4094
4241
|
continue;
|
|
4095
4242
|
}
|
|
4096
|
-
|
|
4243
|
+
const glyphCacheKey = getGlyphCacheKey(this.cacheKeyPrefix, glyph.g, depth, removeOverlaps);
|
|
4244
|
+
let cachedGlyph = this.cache.get(glyphCacheKey);
|
|
4097
4245
|
if (!cachedGlyph) {
|
|
4098
4246
|
cachedGlyph = this.tessellateGlyph(glyphContours, depth, removeOverlaps, isCFF);
|
|
4099
|
-
this.cache.set(
|
|
4247
|
+
this.cache.set(glyphCacheKey, cachedGlyph);
|
|
4100
4248
|
}
|
|
4101
4249
|
else {
|
|
4102
4250
|
cachedGlyph.useCount++;
|
|
4103
4251
|
}
|
|
4104
|
-
const
|
|
4105
|
-
|
|
4106
|
-
const glyphInfo = this.createGlyphInfo(glyph, vertexOffset, cachedGlyph.vertices.length / 3, glyphPosition, glyphContours, depth);
|
|
4252
|
+
const vertexStart = pushTask(cachedGlyph, glyphPosX, glyphPosY, glyphPosZ);
|
|
4253
|
+
const glyphInfo = this.createGlyphInfo(glyph, vertexStart, cachedGlyph.vertices.length / 3, glyphPosX, glyphPosY, glyphPosZ, glyphContours, depth);
|
|
4107
4254
|
glyphInfos.push(glyphInfo);
|
|
4108
4255
|
this.updatePlaneBounds(glyphInfo.bounds, planeBounds);
|
|
4109
4256
|
}
|
|
@@ -4111,10 +4258,33 @@ class GlyphGeometryBuilder {
|
|
|
4111
4258
|
}
|
|
4112
4259
|
}
|
|
4113
4260
|
}
|
|
4114
|
-
//
|
|
4115
|
-
const vertexArray =
|
|
4116
|
-
const normalArray =
|
|
4117
|
-
const indexArray =
|
|
4261
|
+
// Allocate exact-sized buffers and fill once
|
|
4262
|
+
const vertexArray = new Float32Array(totalVertexFloats);
|
|
4263
|
+
const normalArray = new Float32Array(totalNormalFloats);
|
|
4264
|
+
const indexArray = new Uint32Array(totalIndexCount);
|
|
4265
|
+
let vertexPos = 0; // float index (multiple of 3)
|
|
4266
|
+
let normalPos = 0; // float index (multiple of 3)
|
|
4267
|
+
let indexPos = 0; // index count
|
|
4268
|
+
for (let t = 0; t < tasks.length; t++) {
|
|
4269
|
+
const task = tasks[t];
|
|
4270
|
+
const v = task.data.vertices;
|
|
4271
|
+
const n = task.data.normals;
|
|
4272
|
+
const idx = task.data.indices;
|
|
4273
|
+
const px = task.px;
|
|
4274
|
+
const py = task.py;
|
|
4275
|
+
const pz = task.pz;
|
|
4276
|
+
for (let j = 0; j < v.length; j += 3) {
|
|
4277
|
+
vertexArray[vertexPos++] = v[j] + px;
|
|
4278
|
+
vertexArray[vertexPos++] = v[j + 1] + py;
|
|
4279
|
+
vertexArray[vertexPos++] = v[j + 2] + pz;
|
|
4280
|
+
}
|
|
4281
|
+
normalArray.set(n, normalPos);
|
|
4282
|
+
normalPos += n.length;
|
|
4283
|
+
const vertexStart = task.vertexStart;
|
|
4284
|
+
for (let j = 0; j < idx.length; j++) {
|
|
4285
|
+
indexArray[indexPos++] = idx[j] + vertexStart;
|
|
4286
|
+
}
|
|
4287
|
+
}
|
|
4118
4288
|
perfLogger.end('GlyphGeometryBuilder.buildInstancedGeometry');
|
|
4119
4289
|
return {
|
|
4120
4290
|
vertices: vertexArray,
|
|
@@ -4139,7 +4309,7 @@ class GlyphGeometryBuilder {
|
|
|
4139
4309
|
const roundedDepth = Math.round(depth * 1000) / 1000;
|
|
4140
4310
|
return `${this.cacheKeyPrefix}_${ids}_${roundedDepth}_${removeOverlaps}`;
|
|
4141
4311
|
}
|
|
4142
|
-
createGlyphInfo(glyph, vertexStart, vertexCount,
|
|
4312
|
+
createGlyphInfo(glyph, vertexStart, vertexCount, positionX, positionY, positionZ, contours, depth) {
|
|
4143
4313
|
return {
|
|
4144
4314
|
textIndex: glyph.absoluteTextIndex,
|
|
4145
4315
|
lineIndex: glyph.lineIndex,
|
|
@@ -4147,19 +4317,30 @@ class GlyphGeometryBuilder {
|
|
|
4147
4317
|
vertexCount,
|
|
4148
4318
|
bounds: {
|
|
4149
4319
|
min: {
|
|
4150
|
-
x: contours.bounds.min.x +
|
|
4151
|
-
y: contours.bounds.min.y +
|
|
4152
|
-
z:
|
|
4320
|
+
x: contours.bounds.min.x + positionX,
|
|
4321
|
+
y: contours.bounds.min.y + positionY,
|
|
4322
|
+
z: positionZ
|
|
4153
4323
|
},
|
|
4154
4324
|
max: {
|
|
4155
|
-
x: contours.bounds.max.x +
|
|
4156
|
-
y: contours.bounds.max.y +
|
|
4157
|
-
z:
|
|
4325
|
+
x: contours.bounds.max.x + positionX,
|
|
4326
|
+
y: contours.bounds.max.y + positionY,
|
|
4327
|
+
z: positionZ + depth
|
|
4158
4328
|
}
|
|
4159
4329
|
}
|
|
4160
4330
|
};
|
|
4161
4331
|
}
|
|
4162
4332
|
getContoursForGlyph(glyphId) {
|
|
4333
|
+
// Fast path: skip HarfBuzz draw for known-empty glyphs (spaces, zero-width, etc)
|
|
4334
|
+
if (this.emptyGlyphs.has(glyphId)) {
|
|
4335
|
+
return {
|
|
4336
|
+
glyphId,
|
|
4337
|
+
paths: [],
|
|
4338
|
+
bounds: {
|
|
4339
|
+
min: { x: 0, y: 0 },
|
|
4340
|
+
max: { x: 0, y: 0 }
|
|
4341
|
+
}
|
|
4342
|
+
};
|
|
4343
|
+
}
|
|
4163
4344
|
const key = `${this.cacheKeyPrefix}_${glyphId}`;
|
|
4164
4345
|
const cached = this.contourCache.get(key);
|
|
4165
4346
|
if (cached) {
|
|
@@ -4180,11 +4361,15 @@ class GlyphGeometryBuilder {
|
|
|
4180
4361
|
max: { x: 0, y: 0 }
|
|
4181
4362
|
}
|
|
4182
4363
|
};
|
|
4364
|
+
// Mark glyph as empty for future fast-path
|
|
4365
|
+
if (contours.paths.length === 0) {
|
|
4366
|
+
this.emptyGlyphs.add(glyphId);
|
|
4367
|
+
}
|
|
4183
4368
|
this.contourCache.set(key, contours);
|
|
4184
4369
|
return contours;
|
|
4185
4370
|
}
|
|
4186
4371
|
tessellateGlyphCluster(paths, depth, isCFF) {
|
|
4187
|
-
const processedGeometry = this.tessellator.process(paths, true, isCFF);
|
|
4372
|
+
const processedGeometry = this.tessellator.process(paths, true, isCFF, depth !== 0);
|
|
4188
4373
|
return this.extrudeAndPackage(processedGeometry, depth);
|
|
4189
4374
|
}
|
|
4190
4375
|
extrudeAndPackage(processedGeometry, depth) {
|
|
@@ -4232,7 +4417,7 @@ class GlyphGeometryBuilder {
|
|
|
4232
4417
|
glyphId: glyphContours.glyphId,
|
|
4233
4418
|
pathCount: glyphContours.paths.length
|
|
4234
4419
|
});
|
|
4235
|
-
const processedGeometry = this.tessellator.process(glyphContours.paths, removeOverlaps, isCFF);
|
|
4420
|
+
const processedGeometry = this.tessellator.process(glyphContours.paths, removeOverlaps, isCFF, depth !== 0);
|
|
4236
4421
|
perfLogger.end('GlyphGeometryBuilder.tessellateGlyph');
|
|
4237
4422
|
return this.extrudeAndPackage(processedGeometry, depth);
|
|
4238
4423
|
}
|
|
@@ -4302,8 +4487,11 @@ class TextShaper {
|
|
|
4302
4487
|
const clusters = [];
|
|
4303
4488
|
let currentClusterGlyphs = [];
|
|
4304
4489
|
let currentClusterText = '';
|
|
4305
|
-
let
|
|
4306
|
-
let
|
|
4490
|
+
let clusterStartX = 0;
|
|
4491
|
+
let clusterStartY = 0;
|
|
4492
|
+
let cursorX = lineInfo.xOffset;
|
|
4493
|
+
let cursorY = -lineIndex * scaledLineHeight;
|
|
4494
|
+
const cursorZ = 0;
|
|
4307
4495
|
// Apply letter spacing after each glyph to match width measurements used during line breaking
|
|
4308
4496
|
const letterSpacingFU = letterSpacing * this.loadedFont.upem;
|
|
4309
4497
|
const spaceAdjustment = this.calculateSpaceAdjustment(lineInfo, align, letterSpacing);
|
|
@@ -4328,31 +4516,31 @@ class TextShaper {
|
|
|
4328
4516
|
clusters.push({
|
|
4329
4517
|
text: currentClusterText,
|
|
4330
4518
|
glyphs: currentClusterGlyphs,
|
|
4331
|
-
position:
|
|
4519
|
+
position: new Vec3(clusterStartX, clusterStartY, cursorZ)
|
|
4332
4520
|
});
|
|
4333
4521
|
currentClusterGlyphs = [];
|
|
4334
4522
|
currentClusterText = '';
|
|
4335
4523
|
}
|
|
4336
4524
|
}
|
|
4337
|
-
const
|
|
4338
|
-
|
|
4339
|
-
.add(new Vec3(glyph.dx, glyph.dy, 0));
|
|
4525
|
+
const absoluteGlyphX = cursorX + glyph.dx;
|
|
4526
|
+
const absoluteGlyphY = cursorY + glyph.dy;
|
|
4340
4527
|
if (!isWhitespace) {
|
|
4341
4528
|
if (currentClusterGlyphs.length === 0) {
|
|
4342
|
-
|
|
4529
|
+
clusterStartX = absoluteGlyphX;
|
|
4530
|
+
clusterStartY = absoluteGlyphY;
|
|
4343
4531
|
}
|
|
4344
|
-
glyph.x =
|
|
4345
|
-
glyph.y =
|
|
4532
|
+
glyph.x = absoluteGlyphX - clusterStartX;
|
|
4533
|
+
glyph.y = absoluteGlyphY - clusterStartY;
|
|
4346
4534
|
currentClusterGlyphs.push(glyph);
|
|
4347
4535
|
currentClusterText += lineInfo.text[glyph.cl];
|
|
4348
4536
|
}
|
|
4349
|
-
|
|
4350
|
-
|
|
4537
|
+
cursorX += glyph.ax;
|
|
4538
|
+
cursorY += glyph.ay;
|
|
4351
4539
|
if (letterSpacingFU !== 0 && i < glyphInfos.length - 1) {
|
|
4352
|
-
|
|
4540
|
+
cursorX += letterSpacingFU;
|
|
4353
4541
|
}
|
|
4354
4542
|
if (isWhitespace) {
|
|
4355
|
-
|
|
4543
|
+
cursorX += spaceAdjustment;
|
|
4356
4544
|
}
|
|
4357
4545
|
// CJK glue adjustment (must match exactly where LineBreak adds glue)
|
|
4358
4546
|
if (cjkAdjustment !== 0 && i < glyphInfos.length - 1 && !isWhitespace) {
|
|
@@ -4373,7 +4561,7 @@ class TextShaper {
|
|
|
4373
4561
|
shouldApply = false;
|
|
4374
4562
|
}
|
|
4375
4563
|
if (shouldApply) {
|
|
4376
|
-
|
|
4564
|
+
cursorX += cjkAdjustment;
|
|
4377
4565
|
}
|
|
4378
4566
|
}
|
|
4379
4567
|
}
|
|
@@ -4382,7 +4570,7 @@ class TextShaper {
|
|
|
4382
4570
|
clusters.push({
|
|
4383
4571
|
text: currentClusterText,
|
|
4384
4572
|
glyphs: currentClusterGlyphs,
|
|
4385
|
-
position:
|
|
4573
|
+
position: new Vec3(clusterStartX, clusterStartY, cursorZ)
|
|
4386
4574
|
});
|
|
4387
4575
|
}
|
|
4388
4576
|
return clusters;
|
|
@@ -5209,9 +5397,8 @@ class Text {
|
|
|
5209
5397
|
const loadedFont = await Text.resolveFont(options);
|
|
5210
5398
|
const text = new Text();
|
|
5211
5399
|
text.setLoadedFont(loadedFont);
|
|
5212
|
-
//
|
|
5213
|
-
const
|
|
5214
|
-
const result = await text.createGeometry(geometryOptions);
|
|
5400
|
+
// Pass full options so createGeometry honors maxCacheSizeMB etc
|
|
5401
|
+
const result = await text.createGeometry(options);
|
|
5215
5402
|
// Recursive update function
|
|
5216
5403
|
const update = async (newOptions) => {
|
|
5217
5404
|
// Merge options - preserve font from original options if not provided
|
|
@@ -5233,8 +5420,7 @@ class Text {
|
|
|
5233
5420
|
}
|
|
5234
5421
|
// Update closure options for next time
|
|
5235
5422
|
options = mergedOptions;
|
|
5236
|
-
const
|
|
5237
|
-
const newResult = await text.createGeometry(currentGeometryOptions);
|
|
5423
|
+
const newResult = await text.createGeometry(options);
|
|
5238
5424
|
return {
|
|
5239
5425
|
...newResult,
|
|
5240
5426
|
getLoadedFont: () => text.getLoadedFont(),
|
|
@@ -5659,7 +5845,7 @@ class Text {
|
|
|
5659
5845
|
if (!this.textLayout) {
|
|
5660
5846
|
this.textLayout = new TextLayout(this.loadedFont);
|
|
5661
5847
|
}
|
|
5662
|
-
const alignmentResult = this.textLayout.
|
|
5848
|
+
const alignmentResult = this.textLayout.computeAlignmentOffset({
|
|
5663
5849
|
width,
|
|
5664
5850
|
align,
|
|
5665
5851
|
planeBounds
|
|
@@ -5668,9 +5854,19 @@ class Text {
|
|
|
5668
5854
|
planeBounds.min.x = alignmentResult.adjustedBounds.min.x;
|
|
5669
5855
|
planeBounds.max.x = alignmentResult.adjustedBounds.max.x;
|
|
5670
5856
|
const finalScale = size / this.loadedFont.upem;
|
|
5857
|
+
const offsetScaled = offset * finalScale;
|
|
5671
5858
|
// Scale vertices only (normals are unit vectors, don't scale)
|
|
5672
|
-
|
|
5673
|
-
|
|
5859
|
+
if (offsetScaled === 0) {
|
|
5860
|
+
for (let i = 0; i < vertices.length; i++) {
|
|
5861
|
+
vertices[i] *= finalScale;
|
|
5862
|
+
}
|
|
5863
|
+
}
|
|
5864
|
+
else {
|
|
5865
|
+
for (let i = 0; i < vertices.length; i += 3) {
|
|
5866
|
+
vertices[i] = vertices[i] * finalScale + offsetScaled;
|
|
5867
|
+
vertices[i + 1] *= finalScale;
|
|
5868
|
+
vertices[i + 2] *= finalScale;
|
|
5869
|
+
}
|
|
5674
5870
|
}
|
|
5675
5871
|
planeBounds.min.x *= finalScale;
|
|
5676
5872
|
planeBounds.min.y *= finalScale;
|
|
@@ -5680,14 +5876,10 @@ class Text {
|
|
|
5680
5876
|
planeBounds.max.z *= finalScale;
|
|
5681
5877
|
for (let i = 0; i < glyphInfoArray.length; i++) {
|
|
5682
5878
|
const glyphInfo = glyphInfoArray[i];
|
|
5683
|
-
|
|
5684
|
-
glyphInfo.bounds.min.x += offset;
|
|
5685
|
-
glyphInfo.bounds.max.x += offset;
|
|
5686
|
-
}
|
|
5687
|
-
glyphInfo.bounds.min.x *= finalScale;
|
|
5879
|
+
glyphInfo.bounds.min.x = glyphInfo.bounds.min.x * finalScale + offsetScaled;
|
|
5688
5880
|
glyphInfo.bounds.min.y *= finalScale;
|
|
5689
5881
|
glyphInfo.bounds.min.z *= finalScale;
|
|
5690
|
-
glyphInfo.bounds.max.x
|
|
5882
|
+
glyphInfo.bounds.max.x = glyphInfo.bounds.max.x * finalScale + offsetScaled;
|
|
5691
5883
|
glyphInfo.bounds.max.y *= finalScale;
|
|
5692
5884
|
glyphInfo.bounds.max.z *= finalScale;
|
|
5693
5885
|
}
|