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.js
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
|
|
@@ -193,7 +193,7 @@ var FitnessClass;
|
|
|
193
193
|
FitnessClass[FitnessClass["LOOSE"] = 2] = "LOOSE";
|
|
194
194
|
FitnessClass[FitnessClass["VERY_LOOSE"] = 3] = "VERY_LOOSE";
|
|
195
195
|
})(FitnessClass || (FitnessClass = {}));
|
|
196
|
-
// ActiveNodeList maintains all currently viable breakpoints as we scan through the text
|
|
196
|
+
// ActiveNodeList maintains all currently viable breakpoints as we scan through the text
|
|
197
197
|
// Each node represents a potential break with accumulated demerits (total "cost" from start)
|
|
198
198
|
//
|
|
199
199
|
// Demerits = cumulative penalty score from text start to this break, calculated as:
|
|
@@ -335,9 +335,9 @@ class LineBreak {
|
|
|
335
335
|
// Converts text into items (boxes, glues, penalties) for line breaking
|
|
336
336
|
// The measureText function should return widths that include any letter spacing
|
|
337
337
|
static itemizeText(text, measureText, // function to measure text width
|
|
338
|
-
hyphenate = false, language = 'en-us', availablePatterns, lefthyphenmin = DEFAULT_LEFT_HYPHEN_MIN, righthyphenmin = DEFAULT_RIGHT_HYPHEN_MIN, context) {
|
|
338
|
+
measureTextWidths, hyphenate = false, language = 'en-us', availablePatterns, lefthyphenmin = DEFAULT_LEFT_HYPHEN_MIN, righthyphenmin = DEFAULT_RIGHT_HYPHEN_MIN, context) {
|
|
339
339
|
const items = [];
|
|
340
|
-
items.push(...this.itemizeParagraph(text, measureText, hyphenate, language, availablePatterns, lefthyphenmin, righthyphenmin, context));
|
|
340
|
+
items.push(...this.itemizeParagraph(text, measureText, measureTextWidths, hyphenate, language, availablePatterns, lefthyphenmin, righthyphenmin, context));
|
|
341
341
|
// Final glue and penalty to end the paragraph
|
|
342
342
|
// Use infinite stretch to fill the last line
|
|
343
343
|
items.push({
|
|
@@ -442,9 +442,10 @@ class LineBreak {
|
|
|
442
442
|
return (this.isCJClosingPunctuation(char) || this.isCJOpeningPunctuation(char));
|
|
443
443
|
}
|
|
444
444
|
// CJK (Chinese/Japanese/Korean) character-level itemization with inter-character glue
|
|
445
|
-
static itemizeCJKText(text, measureText, context, startOffset = 0, glueParams) {
|
|
445
|
+
static itemizeCJKText(text, measureText, measureTextWidths, context, startOffset = 0, glueParams) {
|
|
446
446
|
const items = [];
|
|
447
447
|
const chars = Array.from(text);
|
|
448
|
+
const widths = measureTextWidths ? measureTextWidths(text) : null;
|
|
448
449
|
let textPosition = startOffset;
|
|
449
450
|
// Inter-character glue parameters
|
|
450
451
|
let glueWidth;
|
|
@@ -465,7 +466,7 @@ class LineBreak {
|
|
|
465
466
|
const char = chars[i];
|
|
466
467
|
const nextChar = i < chars.length - 1 ? chars[i + 1] : null;
|
|
467
468
|
if (/\s/.test(char)) {
|
|
468
|
-
const width = measureText(char);
|
|
469
|
+
const width = widths ? (widths[i] ?? measureText(char)) : measureText(char);
|
|
469
470
|
items.push({
|
|
470
471
|
type: ItemType.GLUE,
|
|
471
472
|
width,
|
|
@@ -479,7 +480,7 @@ class LineBreak {
|
|
|
479
480
|
}
|
|
480
481
|
items.push({
|
|
481
482
|
type: ItemType.BOX,
|
|
482
|
-
width: measureText(char),
|
|
483
|
+
width: widths ? (widths[i] ?? measureText(char)) : measureText(char),
|
|
483
484
|
text: char,
|
|
484
485
|
originIndex: textPosition
|
|
485
486
|
});
|
|
@@ -510,15 +511,21 @@ class LineBreak {
|
|
|
510
511
|
}
|
|
511
512
|
return items;
|
|
512
513
|
}
|
|
513
|
-
static itemizeParagraph(text, measureText, hyphenate, language, availablePatterns, lefthyphenmin, righthyphenmin, context) {
|
|
514
|
+
static itemizeParagraph(text, measureText, measureTextWidths, hyphenate, language, availablePatterns, lefthyphenmin, righthyphenmin, context) {
|
|
514
515
|
const items = [];
|
|
515
516
|
const chars = Array.from(text);
|
|
516
|
-
// Calculate CJK glue parameters once for consistency across all segments
|
|
517
|
-
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
517
|
+
// Calculate CJK glue parameters lazily and once for consistency across all segments
|
|
518
|
+
let cjkGlueParams;
|
|
519
|
+
const getCjkGlueParams = () => {
|
|
520
|
+
if (!cjkGlueParams) {
|
|
521
|
+
const baseCharWidth = measureText('字');
|
|
522
|
+
cjkGlueParams = {
|
|
523
|
+
width: 0,
|
|
524
|
+
stretch: baseCharWidth * 0.04,
|
|
525
|
+
shrink: baseCharWidth * 0.04
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
return cjkGlueParams;
|
|
522
529
|
};
|
|
523
530
|
let buffer = '';
|
|
524
531
|
let bufferStart = 0;
|
|
@@ -528,7 +535,7 @@ class LineBreak {
|
|
|
528
535
|
if (buffer.length === 0)
|
|
529
536
|
return;
|
|
530
537
|
if (bufferScript === 'cjk') {
|
|
531
|
-
const cjkItems = this.itemizeCJKText(buffer, measureText, context, bufferStart,
|
|
538
|
+
const cjkItems = this.itemizeCJKText(buffer, measureText, measureTextWidths, context, bufferStart, getCjkGlueParams());
|
|
532
539
|
items.push(...cjkItems);
|
|
533
540
|
}
|
|
534
541
|
else {
|
|
@@ -721,7 +728,7 @@ class LineBreak {
|
|
|
721
728
|
align: options.align || 'left',
|
|
722
729
|
hyphenate: options.hyphenate || false
|
|
723
730
|
});
|
|
724
|
-
const { text, width, align = 'left', direction = 'ltr', hyphenate = false, language = 'en-us', respectExistingBreaks = true, measureText, hyphenationPatterns, unitsPerEm, letterSpacing = 0, tolerance = DEFAULT_TOLERANCE, pretolerance = DEFAULT_PRETOLERANCE, emergencyStretch = DEFAULT_EMERGENCY_STRETCH, autoEmergencyStretch, lefthyphenmin = DEFAULT_LEFT_HYPHEN_MIN, righthyphenmin = DEFAULT_RIGHT_HYPHEN_MIN, linepenalty = DEFAULT_LINE_PENALTY, adjdemerits = DEFAULT_FITNESS_DIFF_DEMERITS, hyphenpenalty = DEFAULT_HYPHEN_PENALTY, exhyphenpenalty = DEFAULT_EX_HYPHEN_PENALTY, doublehyphendemerits = DEFAULT_DOUBLE_HYPHEN_DEMERITS, looseness = 0, disableShortLineDetection = false, shortLineThreshold = SHORT_LINE_WIDTH_THRESHOLD } = options;
|
|
731
|
+
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;
|
|
725
732
|
// Handle multiple paragraphs by processing each independently
|
|
726
733
|
if (respectExistingBreaks && text.includes('\n')) {
|
|
727
734
|
const paragraphs = text.split('\n');
|
|
@@ -784,9 +791,9 @@ class LineBreak {
|
|
|
784
791
|
exHyphenPenalty: exhyphenpenalty,
|
|
785
792
|
currentAlign: align,
|
|
786
793
|
unitsPerEm,
|
|
787
|
-
// measureText() includes trailing letter spacing after the final glyph of a token
|
|
794
|
+
// measureText() includes trailing letter spacing after the final glyph of a token
|
|
788
795
|
// Shaping applies letter spacing only between glyphs, so we subtract one
|
|
789
|
-
// trailing letterSpacingFU per line segment (see computeAdjustmentRatio/createLines)
|
|
796
|
+
// trailing letterSpacingFU per line segment (see computeAdjustmentRatio/createLines)
|
|
790
797
|
letterSpacingFU: unitsPerEm ? letterSpacing * unitsPerEm : 0
|
|
791
798
|
};
|
|
792
799
|
if (!width || width === Infinity) {
|
|
@@ -805,7 +812,7 @@ class LineBreak {
|
|
|
805
812
|
];
|
|
806
813
|
}
|
|
807
814
|
// Itemize without hyphenation first (TeX approach: only compute if needed)
|
|
808
|
-
const allItems = LineBreak.itemizeText(text, measureText, false, language, hyphenationPatterns, lefthyphenmin, righthyphenmin, context);
|
|
815
|
+
const allItems = LineBreak.itemizeText(text, measureText, measureTextWidths, false, language, hyphenationPatterns, lefthyphenmin, righthyphenmin, context);
|
|
809
816
|
if (allItems.length === 0) {
|
|
810
817
|
return [];
|
|
811
818
|
}
|
|
@@ -824,7 +831,7 @@ class LineBreak {
|
|
|
824
831
|
let breaks = LineBreak.findBreakpoints(currentItems, width, pretolerance, looseness, false, 0, context);
|
|
825
832
|
// Second pass: with hyphenation if first pass failed
|
|
826
833
|
if (breaks.length === 0 && useHyphenation) {
|
|
827
|
-
const itemsWithHyphenation = LineBreak.itemizeText(text, measureText, true, language, hyphenationPatterns, lefthyphenmin, righthyphenmin, context);
|
|
834
|
+
const itemsWithHyphenation = LineBreak.itemizeText(text, measureText, measureTextWidths, true, language, hyphenationPatterns, lefthyphenmin, righthyphenmin, context);
|
|
828
835
|
currentItems = itemsWithHyphenation;
|
|
829
836
|
breaks = LineBreak.findBreakpoints(currentItems, width, tolerance, looseness, false, 0, context);
|
|
830
837
|
}
|
|
@@ -1138,9 +1145,9 @@ class LineBreak {
|
|
|
1138
1145
|
? items[lineEnd].width
|
|
1139
1146
|
: items[lineEnd].preBreakWidth;
|
|
1140
1147
|
}
|
|
1141
|
-
// Correct for trailing letter spacing at the end of the line segment
|
|
1148
|
+
// Correct for trailing letter spacing at the end of the line segment
|
|
1142
1149
|
// Our token measurement includes letter spacing after the final glyph;
|
|
1143
|
-
// shaping does not add letter spacing after the final glyph in a line
|
|
1150
|
+
// shaping does not add letter spacing after the final glyph in a line
|
|
1144
1151
|
if (context?.letterSpacingFU && totalWidth !== 0) {
|
|
1145
1152
|
totalWidth -= context.letterSpacingFU;
|
|
1146
1153
|
}
|
|
@@ -1306,7 +1313,7 @@ class LineBreak {
|
|
|
1306
1313
|
}
|
|
1307
1314
|
}
|
|
1308
1315
|
const lineText = lineTextParts.join('');
|
|
1309
|
-
// Correct for trailing letter spacing at the end of the line
|
|
1316
|
+
// Correct for trailing letter spacing at the end of the line
|
|
1310
1317
|
if (context?.letterSpacingFU && naturalWidth !== 0) {
|
|
1311
1318
|
naturalWidth -= context.letterSpacingFU;
|
|
1312
1319
|
}
|
|
@@ -1363,7 +1370,7 @@ class LineBreak {
|
|
|
1363
1370
|
finalNaturalWidth += item.width;
|
|
1364
1371
|
}
|
|
1365
1372
|
const finalLineText = finalLineTextParts.join('');
|
|
1366
|
-
// Correct for trailing letter spacing at the end of the final line
|
|
1373
|
+
// Correct for trailing letter spacing at the end of the final line
|
|
1367
1374
|
if (context?.letterSpacingFU && finalNaturalWidth !== 0) {
|
|
1368
1375
|
finalNaturalWidth -= context.letterSpacingFU;
|
|
1369
1376
|
}
|
|
@@ -1400,12 +1407,21 @@ class LineBreak {
|
|
|
1400
1407
|
}
|
|
1401
1408
|
}
|
|
1402
1409
|
|
|
1410
|
+
// Memoize conversion per feature-object identity to avoid rebuilding the same
|
|
1411
|
+
// comma-separated string on every HarfBuzz shape call
|
|
1412
|
+
const featureStringCache = new WeakMap();
|
|
1403
1413
|
// Convert feature objects to HarfBuzz comma-separated format
|
|
1404
1414
|
function convertFontFeaturesToString(features) {
|
|
1405
1415
|
if (!features || Object.keys(features).length === 0) {
|
|
1406
1416
|
return undefined;
|
|
1407
1417
|
}
|
|
1418
|
+
const cached = featureStringCache.get(features);
|
|
1419
|
+
if (cached !== undefined) {
|
|
1420
|
+
return cached ?? undefined;
|
|
1421
|
+
}
|
|
1408
1422
|
const featureStrings = [];
|
|
1423
|
+
// Preserve insertion order of the input object
|
|
1424
|
+
// (The public API/tests expect this to be stable and predictable)
|
|
1409
1425
|
for (const [tag, value] of Object.entries(features)) {
|
|
1410
1426
|
if (!/^[a-zA-Z0-9]{4}$/.test(tag)) {
|
|
1411
1427
|
logger.warn(`Invalid OpenType feature tag: "${tag}". Tags must be exactly 4 alphanumeric characters.`);
|
|
@@ -1424,10 +1440,63 @@ function convertFontFeaturesToString(features) {
|
|
|
1424
1440
|
logger.warn(`Invalid value for feature "${tag}": ${value}. Expected boolean or positive number.`);
|
|
1425
1441
|
}
|
|
1426
1442
|
}
|
|
1427
|
-
|
|
1443
|
+
const result = featureStrings.length > 0 ? featureStrings.join(',') : undefined;
|
|
1444
|
+
featureStringCache.set(features, result ?? null);
|
|
1445
|
+
return result;
|
|
1428
1446
|
}
|
|
1429
1447
|
|
|
1430
1448
|
class TextMeasurer {
|
|
1449
|
+
// Shape once and return per-codepoint widths aligned with Array.from(text)
|
|
1450
|
+
// Groups glyph advances by HarfBuzz cluster (cl)
|
|
1451
|
+
// Includes trailing per-glyph letter spacing like measureTextWidth
|
|
1452
|
+
static measureTextWidths(loadedFont, text, letterSpacing = 0) {
|
|
1453
|
+
const chars = Array.from(text);
|
|
1454
|
+
if (chars.length === 0)
|
|
1455
|
+
return [];
|
|
1456
|
+
// HarfBuzz clusters are UTF-16 code unit indices
|
|
1457
|
+
const startToCharIndex = new Map();
|
|
1458
|
+
let codeUnitIndex = 0;
|
|
1459
|
+
for (let i = 0; i < chars.length; i++) {
|
|
1460
|
+
startToCharIndex.set(codeUnitIndex, i);
|
|
1461
|
+
codeUnitIndex += chars[i].length;
|
|
1462
|
+
}
|
|
1463
|
+
const widths = new Array(chars.length).fill(0);
|
|
1464
|
+
const buffer = loadedFont.hb.createBuffer();
|
|
1465
|
+
try {
|
|
1466
|
+
buffer.addText(text);
|
|
1467
|
+
buffer.guessSegmentProperties();
|
|
1468
|
+
const featuresString = convertFontFeaturesToString(loadedFont.fontFeatures);
|
|
1469
|
+
loadedFont.hb.shape(loadedFont.font, buffer, featuresString);
|
|
1470
|
+
const glyphInfos = buffer.json(loadedFont.font);
|
|
1471
|
+
const letterSpacingInFontUnits = letterSpacing * loadedFont.upem;
|
|
1472
|
+
for (let i = 0; i < glyphInfos.length; i++) {
|
|
1473
|
+
const glyph = glyphInfos[i];
|
|
1474
|
+
const cl = glyph.cl ?? 0;
|
|
1475
|
+
let charIndex = startToCharIndex.get(cl);
|
|
1476
|
+
// Fallback if cl lands mid-codepoint
|
|
1477
|
+
if (charIndex === undefined) {
|
|
1478
|
+
// Find the closest start <= cl
|
|
1479
|
+
for (let back = cl; back >= 0; back--) {
|
|
1480
|
+
const candidate = startToCharIndex.get(back);
|
|
1481
|
+
if (candidate !== undefined) {
|
|
1482
|
+
charIndex = candidate;
|
|
1483
|
+
break;
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
if (charIndex === undefined)
|
|
1488
|
+
continue;
|
|
1489
|
+
widths[charIndex] += glyph.ax;
|
|
1490
|
+
if (letterSpacingInFontUnits !== 0) {
|
|
1491
|
+
widths[charIndex] += letterSpacingInFontUnits;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
return widths;
|
|
1495
|
+
}
|
|
1496
|
+
finally {
|
|
1497
|
+
buffer.destroy();
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1431
1500
|
static measureTextWidth(loadedFont, text, letterSpacing = 0) {
|
|
1432
1501
|
const buffer = loadedFont.hb.createBuffer();
|
|
1433
1502
|
buffer.addText(text);
|
|
@@ -1484,7 +1553,8 @@ class TextLayout {
|
|
|
1484
1553
|
unitsPerEm: this.loadedFont.upem,
|
|
1485
1554
|
letterSpacing,
|
|
1486
1555
|
measureText: (textToMeasure) => TextMeasurer.measureTextWidth(this.loadedFont, textToMeasure, letterSpacing // Letter spacing included in width measurements
|
|
1487
|
-
)
|
|
1556
|
+
),
|
|
1557
|
+
measureTextWidths: (textToMeasure) => TextMeasurer.measureTextWidths(this.loadedFont, textToMeasure, letterSpacing)
|
|
1488
1558
|
});
|
|
1489
1559
|
}
|
|
1490
1560
|
else {
|
|
@@ -1506,6 +1576,15 @@ class TextLayout {
|
|
|
1506
1576
|
return { lines };
|
|
1507
1577
|
}
|
|
1508
1578
|
applyAlignment(vertices, options) {
|
|
1579
|
+
const { offset, adjustedBounds } = this.computeAlignmentOffset(options);
|
|
1580
|
+
if (offset !== 0) {
|
|
1581
|
+
for (let i = 0; i < vertices.length; i += 3) {
|
|
1582
|
+
vertices[i] += offset;
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
return { offset, adjustedBounds };
|
|
1586
|
+
}
|
|
1587
|
+
computeAlignmentOffset(options) {
|
|
1509
1588
|
const { width, align, planeBounds } = options;
|
|
1510
1589
|
let offset = 0;
|
|
1511
1590
|
const adjustedBounds = {
|
|
@@ -1517,17 +1596,13 @@ class TextLayout {
|
|
|
1517
1596
|
if (align === 'center') {
|
|
1518
1597
|
offset = (width - lineWidth) / 2 - planeBounds.min.x;
|
|
1519
1598
|
}
|
|
1520
|
-
else
|
|
1599
|
+
else {
|
|
1521
1600
|
offset = width - planeBounds.max.x;
|
|
1522
1601
|
}
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
}
|
|
1528
|
-
adjustedBounds.min.x += offset;
|
|
1529
|
-
adjustedBounds.max.x += offset;
|
|
1530
|
-
}
|
|
1602
|
+
}
|
|
1603
|
+
if (offset !== 0) {
|
|
1604
|
+
adjustedBounds.min.x += offset;
|
|
1605
|
+
adjustedBounds.max.x += offset;
|
|
1531
1606
|
}
|
|
1532
1607
|
return { offset, adjustedBounds };
|
|
1533
1608
|
}
|
|
@@ -2622,7 +2697,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
|
|
|
2622
2697
|
var libtess_minExports = libtess_min.exports;
|
|
2623
2698
|
|
|
2624
2699
|
class Tessellator {
|
|
2625
|
-
process(paths, removeOverlaps = true, isCFF = false) {
|
|
2700
|
+
process(paths, removeOverlaps = true, isCFF = false, needsExtrusionContours = true) {
|
|
2626
2701
|
if (paths.length === 0) {
|
|
2627
2702
|
return { triangles: { vertices: [], indices: [] }, contours: [] };
|
|
2628
2703
|
}
|
|
@@ -2631,66 +2706,124 @@ class Tessellator {
|
|
|
2631
2706
|
return { triangles: { vertices: [], indices: [] }, contours: [] };
|
|
2632
2707
|
}
|
|
2633
2708
|
logger.log(`Tessellator: removeOverlaps=${removeOverlaps}, processing ${valid.length} paths`);
|
|
2634
|
-
return this.tessellate(valid, removeOverlaps, isCFF);
|
|
2635
|
-
}
|
|
2636
|
-
tessellate(paths, removeOverlaps, isCFF) {
|
|
2637
|
-
//
|
|
2638
|
-
const
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2709
|
+
return this.tessellate(valid, removeOverlaps, isCFF, needsExtrusionContours);
|
|
2710
|
+
}
|
|
2711
|
+
tessellate(paths, removeOverlaps, isCFF, needsExtrusionContours) {
|
|
2712
|
+
// libtess expects CCW winding; TTF outer contours are CW
|
|
2713
|
+
const needsWindingReversal = !isCFF && !removeOverlaps;
|
|
2714
|
+
let originalContours;
|
|
2715
|
+
let tessContours;
|
|
2716
|
+
if (needsWindingReversal) {
|
|
2717
|
+
tessContours = this.pathsToContours(paths, true);
|
|
2718
|
+
if (removeOverlaps || needsExtrusionContours) {
|
|
2719
|
+
originalContours = this.pathsToContours(paths);
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
else {
|
|
2723
|
+
originalContours = this.pathsToContours(paths);
|
|
2724
|
+
tessContours = originalContours;
|
|
2725
|
+
}
|
|
2726
|
+
let extrusionContours = needsExtrusionContours
|
|
2727
|
+
? originalContours ?? this.pathsToContours(paths)
|
|
2728
|
+
: [];
|
|
2642
2729
|
if (removeOverlaps) {
|
|
2643
2730
|
logger.log('Two-pass: boundary extraction then triangulation');
|
|
2644
|
-
// Extract boundaries to remove overlaps
|
|
2645
2731
|
perfLogger.start('Tessellator.boundaryPass', {
|
|
2646
|
-
contourCount:
|
|
2732
|
+
contourCount: tessContours.length
|
|
2647
2733
|
});
|
|
2648
|
-
const boundaryResult = this.performTessellation(
|
|
2734
|
+
const boundaryResult = this.performTessellation(originalContours, 'boundary');
|
|
2649
2735
|
perfLogger.end('Tessellator.boundaryPass');
|
|
2650
2736
|
if (!boundaryResult) {
|
|
2651
2737
|
logger.warn('libtess returned empty result from boundary pass');
|
|
2652
2738
|
return { triangles: { vertices: [], indices: [] }, contours: [] };
|
|
2653
2739
|
}
|
|
2654
|
-
//
|
|
2655
|
-
|
|
2656
|
-
|
|
2740
|
+
// Boundary pass normalizes winding (outer CCW, holes CW)
|
|
2741
|
+
tessContours = this.boundaryToContours(boundaryResult);
|
|
2742
|
+
if (needsExtrusionContours) {
|
|
2743
|
+
extrusionContours = tessContours;
|
|
2744
|
+
}
|
|
2745
|
+
logger.log(`Boundary pass created ${tessContours.length} contours. Starting triangulation pass.`);
|
|
2657
2746
|
}
|
|
2658
2747
|
else {
|
|
2659
2748
|
logger.log(`Single-pass triangulation for ${isCFF ? 'CFF' : 'TTF'}`);
|
|
2749
|
+
// TTF contours may have inconsistent winding; check if we need normalization
|
|
2750
|
+
if (needsExtrusionContours && !isCFF) {
|
|
2751
|
+
const needsNormalization = this.needsWindingNormalization(extrusionContours);
|
|
2752
|
+
if (needsNormalization) {
|
|
2753
|
+
logger.log('Complex topology detected, running boundary pass for winding normalization');
|
|
2754
|
+
perfLogger.start('Tessellator.windingNormalization', {
|
|
2755
|
+
contourCount: extrusionContours.length
|
|
2756
|
+
});
|
|
2757
|
+
const boundaryResult = this.performTessellation(extrusionContours, 'boundary');
|
|
2758
|
+
perfLogger.end('Tessellator.windingNormalization');
|
|
2759
|
+
if (boundaryResult) {
|
|
2760
|
+
extrusionContours = this.boundaryToContours(boundaryResult);
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
else {
|
|
2764
|
+
logger.log('Simple topology, skipping winding normalization');
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2660
2767
|
}
|
|
2661
|
-
// Triangulate the contours
|
|
2662
2768
|
perfLogger.start('Tessellator.triangulationPass', {
|
|
2663
|
-
contourCount:
|
|
2769
|
+
contourCount: tessContours.length
|
|
2664
2770
|
});
|
|
2665
|
-
const triangleResult = this.performTessellation(
|
|
2771
|
+
const triangleResult = this.performTessellation(tessContours, 'triangles');
|
|
2666
2772
|
perfLogger.end('Tessellator.triangulationPass');
|
|
2667
2773
|
if (!triangleResult) {
|
|
2668
2774
|
const warning = removeOverlaps
|
|
2669
2775
|
? 'libtess returned empty result from triangulation pass'
|
|
2670
2776
|
: 'libtess returned empty result from single-pass triangulation';
|
|
2671
2777
|
logger.warn(warning);
|
|
2672
|
-
return { triangles: { vertices: [], indices: [] }, contours };
|
|
2778
|
+
return { triangles: { vertices: [], indices: [] }, contours: extrusionContours };
|
|
2673
2779
|
}
|
|
2674
2780
|
return {
|
|
2675
2781
|
triangles: {
|
|
2676
2782
|
vertices: triangleResult.vertices,
|
|
2677
2783
|
indices: triangleResult.indices || []
|
|
2678
2784
|
},
|
|
2679
|
-
contours
|
|
2785
|
+
contours: extrusionContours
|
|
2680
2786
|
};
|
|
2681
2787
|
}
|
|
2682
|
-
pathsToContours(paths) {
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2788
|
+
pathsToContours(paths, reversePoints = false) {
|
|
2789
|
+
const contours = new Array(paths.length);
|
|
2790
|
+
for (let p = 0; p < paths.length; p++) {
|
|
2791
|
+
const points = paths[p].points;
|
|
2792
|
+
const pointCount = points.length;
|
|
2793
|
+
// Clipper-style paths can be explicitly closed by repeating the first point at the end
|
|
2794
|
+
// Normalize to a single closing vertex for stable side wall generation
|
|
2795
|
+
const isClosed = pointCount > 1 &&
|
|
2796
|
+
points[0].x === points[pointCount - 1].x &&
|
|
2797
|
+
points[0].y === points[pointCount - 1].y;
|
|
2798
|
+
const end = isClosed ? pointCount - 1 : pointCount;
|
|
2799
|
+
// +1 to append a closing vertex
|
|
2800
|
+
const contour = new Array((end + 1) * 2);
|
|
2801
|
+
let i = 0;
|
|
2802
|
+
if (reversePoints) {
|
|
2803
|
+
for (let k = end - 1; k >= 0; k--) {
|
|
2804
|
+
const pt = points[k];
|
|
2805
|
+
contour[i++] = pt.x;
|
|
2806
|
+
contour[i++] = pt.y;
|
|
2807
|
+
}
|
|
2687
2808
|
}
|
|
2688
|
-
|
|
2689
|
-
|
|
2809
|
+
else {
|
|
2810
|
+
for (let k = 0; k < end; k++) {
|
|
2811
|
+
const pt = points[k];
|
|
2812
|
+
contour[i++] = pt.x;
|
|
2813
|
+
contour[i++] = pt.y;
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
// Some glyphs omit closePath, leaving gaps in extruded side walls
|
|
2817
|
+
if (i >= 2) {
|
|
2818
|
+
contour[i++] = contour[0];
|
|
2819
|
+
contour[i++] = contour[1];
|
|
2820
|
+
}
|
|
2821
|
+
contours[p] = contour;
|
|
2822
|
+
}
|
|
2823
|
+
return contours;
|
|
2690
2824
|
}
|
|
2691
2825
|
performTessellation(contours, mode) {
|
|
2692
2826
|
const tess = new libtess_minExports.GluTesselator();
|
|
2693
|
-
// Set winding rule to NON-ZERO
|
|
2694
2827
|
tess.gluTessProperty(libtess_minExports.gluEnum.GLU_TESS_WINDING_RULE, libtess_minExports.windingRule.GLU_TESS_WINDING_NONZERO);
|
|
2695
2828
|
const vertices = [];
|
|
2696
2829
|
const indices = [];
|
|
@@ -2713,7 +2846,7 @@ class Tessellator {
|
|
|
2713
2846
|
});
|
|
2714
2847
|
tess.gluTessCallback(libtess_minExports.gluEnum.GLU_TESS_END, () => {
|
|
2715
2848
|
if (currentContour.length > 0) {
|
|
2716
|
-
contourIndices.push(
|
|
2849
|
+
contourIndices.push(currentContour);
|
|
2717
2850
|
}
|
|
2718
2851
|
});
|
|
2719
2852
|
}
|
|
@@ -2758,7 +2891,6 @@ class Tessellator {
|
|
|
2758
2891
|
const vertIdx = idx * 2;
|
|
2759
2892
|
contour.push(boundaryResult.vertices[vertIdx], boundaryResult.vertices[vertIdx + 1]);
|
|
2760
2893
|
}
|
|
2761
|
-
// Ensure contour is closed for side wall generation
|
|
2762
2894
|
if (contour.length > 2) {
|
|
2763
2895
|
if (contour[0] !== contour[contour.length - 2] ||
|
|
2764
2896
|
contour[1] !== contour[contour.length - 1]) {
|
|
@@ -2769,11 +2901,45 @@ class Tessellator {
|
|
|
2769
2901
|
}
|
|
2770
2902
|
return contours;
|
|
2771
2903
|
}
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2904
|
+
// Check if contours need winding normalization via boundary pass
|
|
2905
|
+
// Returns false if topology is simple enough to skip the expensive pass
|
|
2906
|
+
needsWindingNormalization(contours) {
|
|
2907
|
+
if (contours.length === 0)
|
|
2908
|
+
return false;
|
|
2909
|
+
// Heuristic 1: Single contour never needs normalization
|
|
2910
|
+
if (contours.length === 1)
|
|
2911
|
+
return false;
|
|
2912
|
+
// Heuristic 2: All same winding = all outers, no holes
|
|
2913
|
+
// Compute signed areas
|
|
2914
|
+
let firstSign = null;
|
|
2915
|
+
for (const contour of contours) {
|
|
2916
|
+
const area = this.signedArea(contour);
|
|
2917
|
+
const sign = area >= 0 ? 1 : -1;
|
|
2918
|
+
if (firstSign === null) {
|
|
2919
|
+
firstSign = sign;
|
|
2920
|
+
}
|
|
2921
|
+
else if (sign !== firstSign) {
|
|
2922
|
+
// Mixed winding detected → might have holes or complex topology
|
|
2923
|
+
return true;
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
// All same winding → simple topology, no normalization needed
|
|
2927
|
+
return false;
|
|
2928
|
+
}
|
|
2929
|
+
// Compute signed area (CCW = positive, CW = negative)
|
|
2930
|
+
signedArea(contour) {
|
|
2931
|
+
let area = 0;
|
|
2932
|
+
const len = contour.length;
|
|
2933
|
+
if (len < 6)
|
|
2934
|
+
return 0; // Need at least 3 points
|
|
2935
|
+
for (let i = 0; i < len; i += 2) {
|
|
2936
|
+
const x1 = contour[i];
|
|
2937
|
+
const y1 = contour[i + 1];
|
|
2938
|
+
const x2 = contour[(i + 2) % len];
|
|
2939
|
+
const y2 = contour[(i + 3) % len];
|
|
2940
|
+
area += x1 * y2 - x2 * y1;
|
|
2941
|
+
}
|
|
2942
|
+
return area / 2;
|
|
2777
2943
|
}
|
|
2778
2944
|
}
|
|
2779
2945
|
|
|
@@ -2783,12 +2949,11 @@ class Extruder {
|
|
|
2783
2949
|
const points = geometry.triangles.vertices;
|
|
2784
2950
|
const triangleIndices = geometry.triangles.indices;
|
|
2785
2951
|
const numPoints = points.length / 2;
|
|
2786
|
-
// Count side-wall segments (
|
|
2952
|
+
// Count side-wall segments (4 vertices + 6 indices per segment)
|
|
2787
2953
|
let sideSegments = 0;
|
|
2788
2954
|
if (depth !== 0) {
|
|
2789
2955
|
for (const contour of geometry.contours) {
|
|
2790
|
-
//
|
|
2791
|
-
// Contours are expected to be closed (last point repeats first), so segments = (nPoints - 1)
|
|
2956
|
+
// Contours are closed (last point repeats first)
|
|
2792
2957
|
const contourPoints = contour.length / 2;
|
|
2793
2958
|
if (contourPoints >= 2)
|
|
2794
2959
|
sideSegments += contourPoints - 1;
|
|
@@ -2804,7 +2969,7 @@ class Extruder {
|
|
|
2804
2969
|
: triangleIndices.length * 2 + sideSegments * 6;
|
|
2805
2970
|
const indices = new Uint32Array(indexCount);
|
|
2806
2971
|
if (depth === 0) {
|
|
2807
|
-
//
|
|
2972
|
+
// Single-sided flat geometry at z=0
|
|
2808
2973
|
let vPos = 0;
|
|
2809
2974
|
for (let i = 0; i < points.length; i += 2) {
|
|
2810
2975
|
vertices[vPos] = points[i];
|
|
@@ -2815,42 +2980,44 @@ class Extruder {
|
|
|
2815
2980
|
normals[vPos + 2] = 1;
|
|
2816
2981
|
vPos += 3;
|
|
2817
2982
|
}
|
|
2983
|
+
// libtess outputs CCW, use as-is for +Z facing geometry
|
|
2818
2984
|
for (let i = 0; i < triangleIndices.length; i++) {
|
|
2819
2985
|
indices[i] = triangleIndices[i];
|
|
2820
2986
|
}
|
|
2821
2987
|
return { vertices, normals, indices };
|
|
2822
2988
|
}
|
|
2823
|
-
//
|
|
2989
|
+
// Extruded geometry: front at z=0, back at z=depth
|
|
2824
2990
|
const minBackOffset = unitsPerEm * 0.000025;
|
|
2825
2991
|
const backZ = depth <= minBackOffset ? minBackOffset : depth;
|
|
2826
|
-
//
|
|
2827
|
-
for (let p = 0, vi = 0; p < points.length; p += 2, vi++) {
|
|
2828
|
-
const base = vi * 3;
|
|
2829
|
-
vertices[base] = points[p];
|
|
2830
|
-
vertices[base + 1] = points[p + 1];
|
|
2831
|
-
vertices[base + 2] = 0;
|
|
2832
|
-
normals[base] = 0;
|
|
2833
|
-
normals[base + 1] = 0;
|
|
2834
|
-
normals[base + 2] = 1;
|
|
2835
|
-
}
|
|
2836
|
-
// Fill back vertices/normals (numPoints..2*numPoints-1)
|
|
2992
|
+
// Generate both caps in one pass
|
|
2837
2993
|
for (let p = 0, vi = 0; p < points.length; p += 2, vi++) {
|
|
2838
|
-
const
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2994
|
+
const x = points[p];
|
|
2995
|
+
const y = points[p + 1];
|
|
2996
|
+
// Cap at z=0
|
|
2997
|
+
const base0 = vi * 3;
|
|
2998
|
+
vertices[base0] = x;
|
|
2999
|
+
vertices[base0 + 1] = y;
|
|
3000
|
+
vertices[base0 + 2] = 0;
|
|
3001
|
+
normals[base0] = 0;
|
|
3002
|
+
normals[base0 + 1] = 0;
|
|
3003
|
+
normals[base0 + 2] = -1;
|
|
3004
|
+
// Cap at z=depth
|
|
3005
|
+
const baseD = (numPoints + vi) * 3;
|
|
3006
|
+
vertices[baseD] = x;
|
|
3007
|
+
vertices[baseD + 1] = y;
|
|
3008
|
+
vertices[baseD + 2] = backZ;
|
|
3009
|
+
normals[baseD] = 0;
|
|
3010
|
+
normals[baseD + 1] = 0;
|
|
3011
|
+
normals[baseD + 2] = 1;
|
|
3012
|
+
}
|
|
3013
|
+
// libtess outputs CCW triangles (viewed from +Z)
|
|
3014
|
+
// Z=0 cap faces -Z, reverse winding
|
|
2847
3015
|
for (let i = 0; i < triangleIndices.length; i++) {
|
|
2848
|
-
indices[i] = triangleIndices[i];
|
|
3016
|
+
indices[i] = triangleIndices[triangleIndices.length - 1 - i];
|
|
2849
3017
|
}
|
|
2850
|
-
//
|
|
3018
|
+
// Z=depth cap faces +Z, use original winding
|
|
2851
3019
|
for (let i = 0; i < triangleIndices.length; i++) {
|
|
2852
|
-
indices[triangleIndices.length + i] =
|
|
2853
|
-
triangleIndices[triangleIndices.length - 1 - i] + numPoints;
|
|
3020
|
+
indices[triangleIndices.length + i] = triangleIndices[i] + numPoints;
|
|
2854
3021
|
}
|
|
2855
3022
|
// Side walls
|
|
2856
3023
|
let nextVertex = numPoints * 2;
|
|
@@ -2861,7 +3028,7 @@ class Extruder {
|
|
|
2861
3028
|
const p0y = contour[i + 1];
|
|
2862
3029
|
const p1x = contour[i + 2];
|
|
2863
3030
|
const p1y = contour[i + 3];
|
|
2864
|
-
//
|
|
3031
|
+
// Perpendicular normal for this wall segment
|
|
2865
3032
|
const ex = p1x - p0x;
|
|
2866
3033
|
const ey = p1y - p0y;
|
|
2867
3034
|
const lenSq = ex * ex + ey * ey;
|
|
@@ -2874,7 +3041,7 @@ class Extruder {
|
|
|
2874
3041
|
}
|
|
2875
3042
|
const baseVertex = nextVertex;
|
|
2876
3043
|
const base = baseVertex * 3;
|
|
2877
|
-
//
|
|
3044
|
+
// Wall quad: front edge at z=0, back edge at z=depth
|
|
2878
3045
|
vertices[base] = p0x;
|
|
2879
3046
|
vertices[base + 1] = p0y;
|
|
2880
3047
|
vertices[base + 2] = 0;
|
|
@@ -2887,7 +3054,7 @@ class Extruder {
|
|
|
2887
3054
|
vertices[base + 9] = p1x;
|
|
2888
3055
|
vertices[base + 10] = p1y;
|
|
2889
3056
|
vertices[base + 11] = backZ;
|
|
2890
|
-
//
|
|
3057
|
+
// Wall normals point perpendicular to edge
|
|
2891
3058
|
normals[base] = nx;
|
|
2892
3059
|
normals[base + 1] = ny;
|
|
2893
3060
|
normals[base + 2] = 0;
|
|
@@ -2900,7 +3067,7 @@ class Extruder {
|
|
|
2900
3067
|
normals[base + 9] = nx;
|
|
2901
3068
|
normals[base + 10] = ny;
|
|
2902
3069
|
normals[base + 11] = 0;
|
|
2903
|
-
//
|
|
3070
|
+
// Two triangles per wall segment
|
|
2904
3071
|
indices[idxPos++] = baseVertex;
|
|
2905
3072
|
indices[idxPos++] = baseVertex + 1;
|
|
2906
3073
|
indices[idxPos++] = baseVertex + 2;
|
|
@@ -3135,21 +3302,23 @@ class PathOptimizer {
|
|
|
3135
3302
|
return path;
|
|
3136
3303
|
}
|
|
3137
3304
|
this.stats.originalPointCount += path.points.length;
|
|
3138
|
-
|
|
3305
|
+
// Most paths are already immutable after collection; avoid copying large point arrays
|
|
3306
|
+
// The optimizers below never mutate the input `points` array
|
|
3307
|
+
const points = path.points;
|
|
3139
3308
|
if (points.length < 5) {
|
|
3140
3309
|
return path;
|
|
3141
3310
|
}
|
|
3142
|
-
|
|
3143
|
-
if (
|
|
3311
|
+
let optimized = this.simplifyPathVW(points, this.config.areaThreshold);
|
|
3312
|
+
if (optimized.length < 3) {
|
|
3144
3313
|
return path;
|
|
3145
3314
|
}
|
|
3146
|
-
|
|
3147
|
-
if (
|
|
3315
|
+
optimized = this.removeColinearPoints(optimized, this.config.colinearThreshold);
|
|
3316
|
+
if (optimized.length < 3) {
|
|
3148
3317
|
return path;
|
|
3149
3318
|
}
|
|
3150
3319
|
return {
|
|
3151
3320
|
...path,
|
|
3152
|
-
points
|
|
3321
|
+
points: optimized
|
|
3153
3322
|
};
|
|
3154
3323
|
}
|
|
3155
3324
|
// Visvalingam-Whyatt algorithm
|
|
@@ -3603,7 +3772,7 @@ class GlyphContourCollector {
|
|
|
3603
3772
|
if (this.currentGlyphPaths.length > 0) {
|
|
3604
3773
|
this.collectedGlyphs.push({
|
|
3605
3774
|
glyphId: this.currentGlyphId,
|
|
3606
|
-
paths:
|
|
3775
|
+
paths: this.currentGlyphPaths,
|
|
3607
3776
|
bounds: {
|
|
3608
3777
|
min: {
|
|
3609
3778
|
x: this.currentGlyphBounds.min.x,
|
|
@@ -3655,11 +3824,10 @@ class GlyphContourCollector {
|
|
|
3655
3824
|
return;
|
|
3656
3825
|
}
|
|
3657
3826
|
const flattenedPoints = this.polygonizer.polygonizeQuadratic(start, control, end);
|
|
3658
|
-
for (const point of flattenedPoints) {
|
|
3659
|
-
this.updateBounds(point);
|
|
3660
|
-
}
|
|
3661
3827
|
for (let i = 0; i < flattenedPoints.length; i++) {
|
|
3662
|
-
|
|
3828
|
+
const pt = flattenedPoints[i];
|
|
3829
|
+
this.updateBounds(pt);
|
|
3830
|
+
this.currentPath.points.push(pt);
|
|
3663
3831
|
}
|
|
3664
3832
|
this.currentPoint = end;
|
|
3665
3833
|
}
|
|
@@ -3679,11 +3847,10 @@ class GlyphContourCollector {
|
|
|
3679
3847
|
return;
|
|
3680
3848
|
}
|
|
3681
3849
|
const flattenedPoints = this.polygonizer.polygonizeCubic(start, control1, control2, end);
|
|
3682
|
-
for (const point of flattenedPoints) {
|
|
3683
|
-
this.updateBounds(point);
|
|
3684
|
-
}
|
|
3685
3850
|
for (let i = 0; i < flattenedPoints.length; i++) {
|
|
3686
|
-
|
|
3851
|
+
const pt = flattenedPoints[i];
|
|
3852
|
+
this.updateBounds(pt);
|
|
3853
|
+
this.currentPath.points.push(pt);
|
|
3687
3854
|
}
|
|
3688
3855
|
this.currentPoint = end;
|
|
3689
3856
|
}
|
|
@@ -3873,6 +4040,7 @@ class GlyphGeometryBuilder {
|
|
|
3873
4040
|
constructor(cache, loadedFont) {
|
|
3874
4041
|
this.fontId = 'default';
|
|
3875
4042
|
this.cacheKeyPrefix = 'default';
|
|
4043
|
+
this.emptyGlyphs = new Set();
|
|
3876
4044
|
this.cache = cache;
|
|
3877
4045
|
this.loadedFont = loadedFont;
|
|
3878
4046
|
this.tessellator = new Tessellator();
|
|
@@ -3926,63 +4094,34 @@ class GlyphGeometryBuilder {
|
|
|
3926
4094
|
}
|
|
3927
4095
|
// Build instanced geometry from glyph contours
|
|
3928
4096
|
buildInstancedGeometry(clustersByLine, depth, removeOverlaps, isCFF, separateGlyphs = false, coloredTextIndices) {
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
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
|
-
nextSize *= 2;
|
|
3958
|
-
const next = new Uint32Array(nextSize);
|
|
3959
|
-
next.set(buffer);
|
|
3960
|
-
return next;
|
|
3961
|
-
};
|
|
3962
|
-
const appendGeometryToBuffers = (data, position, vertexOffset) => {
|
|
3963
|
-
const v = data.vertices;
|
|
3964
|
-
const n = data.normals;
|
|
3965
|
-
const idx = data.indices;
|
|
3966
|
-
// Grow buffers as needed
|
|
3967
|
-
vertexBuffer = ensureFloatCapacity(vertexBuffer, vertexPos + v.length);
|
|
3968
|
-
normalBuffer = ensureFloatCapacity(normalBuffer, normalPos + n.length);
|
|
3969
|
-
indexBuffer = ensureIndexCapacity(indexBuffer, indexPos + idx.length);
|
|
3970
|
-
// Vertices: translate by position
|
|
3971
|
-
const px = position.x;
|
|
3972
|
-
const py = position.y;
|
|
3973
|
-
const pz = position.z;
|
|
3974
|
-
for (let j = 0; j < v.length; j += 3) {
|
|
3975
|
-
vertexBuffer[vertexPos++] = v[j] + px;
|
|
3976
|
-
vertexBuffer[vertexPos++] = v[j + 1] + py;
|
|
3977
|
-
vertexBuffer[vertexPos++] = v[j + 2] + pz;
|
|
3978
|
-
}
|
|
3979
|
-
// Normals: straight copy
|
|
3980
|
-
normalBuffer.set(n, normalPos);
|
|
3981
|
-
normalPos += n.length;
|
|
3982
|
-
// Indices: copy with vertex offset
|
|
3983
|
-
for (let j = 0; j < idx.length; j++) {
|
|
3984
|
-
indexBuffer[indexPos++] = idx[j] + vertexOffset;
|
|
3985
|
-
}
|
|
4097
|
+
if (isLogEnabled) {
|
|
4098
|
+
let wordCount = 0;
|
|
4099
|
+
for (let i = 0; i < clustersByLine.length; i++) {
|
|
4100
|
+
wordCount += clustersByLine[i].length;
|
|
4101
|
+
}
|
|
4102
|
+
perfLogger.start('GlyphGeometryBuilder.buildInstancedGeometry', {
|
|
4103
|
+
lineCount: clustersByLine.length,
|
|
4104
|
+
wordCount,
|
|
4105
|
+
depth,
|
|
4106
|
+
removeOverlaps
|
|
4107
|
+
});
|
|
4108
|
+
}
|
|
4109
|
+
else {
|
|
4110
|
+
perfLogger.start('GlyphGeometryBuilder.buildInstancedGeometry');
|
|
4111
|
+
}
|
|
4112
|
+
const tasks = [];
|
|
4113
|
+
let totalVertexFloats = 0;
|
|
4114
|
+
let totalNormalFloats = 0;
|
|
4115
|
+
let totalIndexCount = 0;
|
|
4116
|
+
let vertexCursor = 0; // vertex offset (not float offset)
|
|
4117
|
+
const pushTask = (data, px, py, pz) => {
|
|
4118
|
+
const vertexStart = vertexCursor;
|
|
4119
|
+
tasks.push({ data, px, py, pz, vertexStart });
|
|
4120
|
+
totalVertexFloats += data.vertices.length;
|
|
4121
|
+
totalNormalFloats += data.normals.length;
|
|
4122
|
+
totalIndexCount += data.indices.length;
|
|
4123
|
+
vertexCursor += data.vertices.length / 3;
|
|
4124
|
+
return vertexStart;
|
|
3986
4125
|
};
|
|
3987
4126
|
const glyphInfos = [];
|
|
3988
4127
|
const planeBounds = {
|
|
@@ -3992,6 +4131,9 @@ class GlyphGeometryBuilder {
|
|
|
3992
4131
|
for (let lineIndex = 0; lineIndex < clustersByLine.length; lineIndex++) {
|
|
3993
4132
|
const line = clustersByLine[lineIndex];
|
|
3994
4133
|
for (const cluster of line) {
|
|
4134
|
+
const clusterX = cluster.position.x;
|
|
4135
|
+
const clusterY = cluster.position.y;
|
|
4136
|
+
const clusterZ = cluster.position.z;
|
|
3995
4137
|
const clusterGlyphContours = [];
|
|
3996
4138
|
for (const glyph of cluster.glyphs) {
|
|
3997
4139
|
clusterGlyphContours.push(this.getContoursForGlyph(glyph.g));
|
|
@@ -4032,7 +4174,7 @@ class GlyphGeometryBuilder {
|
|
|
4032
4174
|
// Use glyph-level caching when separateGlyphs is set or when cluster contains colored text
|
|
4033
4175
|
const forceSeparate = separateGlyphs || clusterHasColoredGlyphs;
|
|
4034
4176
|
// Iterate over the geometric groups identified by BoundaryClusterer
|
|
4035
|
-
// logical groups (words)
|
|
4177
|
+
// logical groups (words) split into geometric sub-groups (e.g. "aa", "XX", "bb")
|
|
4036
4178
|
for (const groupIndices of boundaryGroups) {
|
|
4037
4179
|
const isOverlappingGroup = groupIndices.length > 1;
|
|
4038
4180
|
const shouldCluster = isOverlappingGroup && !forceSeparate;
|
|
@@ -4064,16 +4206,19 @@ class GlyphGeometryBuilder {
|
|
|
4064
4206
|
// Calculate the absolute position of this sub-cluster based on its first glyph
|
|
4065
4207
|
// (since the cached geometry is relative to that first glyph)
|
|
4066
4208
|
const firstGlyphInGroup = subClusterGlyphs[0];
|
|
4067
|
-
const
|
|
4068
|
-
const
|
|
4069
|
-
|
|
4209
|
+
const groupPosX = clusterX + (firstGlyphInGroup.x ?? 0);
|
|
4210
|
+
const groupPosY = clusterY + (firstGlyphInGroup.y ?? 0);
|
|
4211
|
+
const groupPosZ = clusterZ;
|
|
4212
|
+
const vertexStart = pushTask(cachedCluster, groupPosX, groupPosY, groupPosZ);
|
|
4070
4213
|
const clusterVertexCount = cachedCluster.vertices.length / 3;
|
|
4071
4214
|
for (let i = 0; i < groupIndices.length; i++) {
|
|
4072
4215
|
const originalIndex = groupIndices[i];
|
|
4073
4216
|
const glyph = cluster.glyphs[originalIndex];
|
|
4074
4217
|
const glyphContours = clusterGlyphContours[originalIndex];
|
|
4075
|
-
const
|
|
4076
|
-
const
|
|
4218
|
+
const glyphPosX = clusterX + (glyph.x ?? 0);
|
|
4219
|
+
const glyphPosY = clusterY + (glyph.y ?? 0);
|
|
4220
|
+
const glyphPosZ = clusterZ;
|
|
4221
|
+
const glyphInfo = this.createGlyphInfo(glyph, vertexStart, clusterVertexCount, glyphPosX, glyphPosY, glyphPosZ, glyphContours, depth);
|
|
4077
4222
|
glyphInfos.push(glyphInfo);
|
|
4078
4223
|
this.updatePlaneBounds(glyphInfo.bounds, planeBounds);
|
|
4079
4224
|
}
|
|
@@ -4083,24 +4228,26 @@ class GlyphGeometryBuilder {
|
|
|
4083
4228
|
for (const i of groupIndices) {
|
|
4084
4229
|
const glyph = cluster.glyphs[i];
|
|
4085
4230
|
const glyphContours = clusterGlyphContours[i];
|
|
4086
|
-
const
|
|
4231
|
+
const glyphPosX = clusterX + (glyph.x ?? 0);
|
|
4232
|
+
const glyphPosY = clusterY + (glyph.y ?? 0);
|
|
4233
|
+
const glyphPosZ = clusterZ;
|
|
4087
4234
|
// Skip glyphs with no paths (spaces, zero-width characters, etc.)
|
|
4088
4235
|
if (glyphContours.paths.length === 0) {
|
|
4089
|
-
const glyphInfo = this.createGlyphInfo(glyph, 0, 0,
|
|
4236
|
+
const glyphInfo = this.createGlyphInfo(glyph, 0, 0, glyphPosX, glyphPosY, glyphPosZ, glyphContours, depth);
|
|
4090
4237
|
glyphInfos.push(glyphInfo);
|
|
4091
4238
|
continue;
|
|
4092
4239
|
}
|
|
4093
|
-
|
|
4240
|
+
const glyphCacheKey = getGlyphCacheKey(this.cacheKeyPrefix, glyph.g, depth, removeOverlaps);
|
|
4241
|
+
let cachedGlyph = this.cache.get(glyphCacheKey);
|
|
4094
4242
|
if (!cachedGlyph) {
|
|
4095
4243
|
cachedGlyph = this.tessellateGlyph(glyphContours, depth, removeOverlaps, isCFF);
|
|
4096
|
-
this.cache.set(
|
|
4244
|
+
this.cache.set(glyphCacheKey, cachedGlyph);
|
|
4097
4245
|
}
|
|
4098
4246
|
else {
|
|
4099
4247
|
cachedGlyph.useCount++;
|
|
4100
4248
|
}
|
|
4101
|
-
const
|
|
4102
|
-
|
|
4103
|
-
const glyphInfo = this.createGlyphInfo(glyph, vertexOffset, cachedGlyph.vertices.length / 3, glyphPosition, glyphContours, depth);
|
|
4249
|
+
const vertexStart = pushTask(cachedGlyph, glyphPosX, glyphPosY, glyphPosZ);
|
|
4250
|
+
const glyphInfo = this.createGlyphInfo(glyph, vertexStart, cachedGlyph.vertices.length / 3, glyphPosX, glyphPosY, glyphPosZ, glyphContours, depth);
|
|
4104
4251
|
glyphInfos.push(glyphInfo);
|
|
4105
4252
|
this.updatePlaneBounds(glyphInfo.bounds, planeBounds);
|
|
4106
4253
|
}
|
|
@@ -4108,10 +4255,33 @@ class GlyphGeometryBuilder {
|
|
|
4108
4255
|
}
|
|
4109
4256
|
}
|
|
4110
4257
|
}
|
|
4111
|
-
//
|
|
4112
|
-
const vertexArray =
|
|
4113
|
-
const normalArray =
|
|
4114
|
-
const indexArray =
|
|
4258
|
+
// Allocate exact-sized buffers and fill once
|
|
4259
|
+
const vertexArray = new Float32Array(totalVertexFloats);
|
|
4260
|
+
const normalArray = new Float32Array(totalNormalFloats);
|
|
4261
|
+
const indexArray = new Uint32Array(totalIndexCount);
|
|
4262
|
+
let vertexPos = 0; // float index (multiple of 3)
|
|
4263
|
+
let normalPos = 0; // float index (multiple of 3)
|
|
4264
|
+
let indexPos = 0; // index count
|
|
4265
|
+
for (let t = 0; t < tasks.length; t++) {
|
|
4266
|
+
const task = tasks[t];
|
|
4267
|
+
const v = task.data.vertices;
|
|
4268
|
+
const n = task.data.normals;
|
|
4269
|
+
const idx = task.data.indices;
|
|
4270
|
+
const px = task.px;
|
|
4271
|
+
const py = task.py;
|
|
4272
|
+
const pz = task.pz;
|
|
4273
|
+
for (let j = 0; j < v.length; j += 3) {
|
|
4274
|
+
vertexArray[vertexPos++] = v[j] + px;
|
|
4275
|
+
vertexArray[vertexPos++] = v[j + 1] + py;
|
|
4276
|
+
vertexArray[vertexPos++] = v[j + 2] + pz;
|
|
4277
|
+
}
|
|
4278
|
+
normalArray.set(n, normalPos);
|
|
4279
|
+
normalPos += n.length;
|
|
4280
|
+
const vertexStart = task.vertexStart;
|
|
4281
|
+
for (let j = 0; j < idx.length; j++) {
|
|
4282
|
+
indexArray[indexPos++] = idx[j] + vertexStart;
|
|
4283
|
+
}
|
|
4284
|
+
}
|
|
4115
4285
|
perfLogger.end('GlyphGeometryBuilder.buildInstancedGeometry');
|
|
4116
4286
|
return {
|
|
4117
4287
|
vertices: vertexArray,
|
|
@@ -4136,7 +4306,7 @@ class GlyphGeometryBuilder {
|
|
|
4136
4306
|
const roundedDepth = Math.round(depth * 1000) / 1000;
|
|
4137
4307
|
return `${this.cacheKeyPrefix}_${ids}_${roundedDepth}_${removeOverlaps}`;
|
|
4138
4308
|
}
|
|
4139
|
-
createGlyphInfo(glyph, vertexStart, vertexCount,
|
|
4309
|
+
createGlyphInfo(glyph, vertexStart, vertexCount, positionX, positionY, positionZ, contours, depth) {
|
|
4140
4310
|
return {
|
|
4141
4311
|
textIndex: glyph.absoluteTextIndex,
|
|
4142
4312
|
lineIndex: glyph.lineIndex,
|
|
@@ -4144,19 +4314,30 @@ class GlyphGeometryBuilder {
|
|
|
4144
4314
|
vertexCount,
|
|
4145
4315
|
bounds: {
|
|
4146
4316
|
min: {
|
|
4147
|
-
x: contours.bounds.min.x +
|
|
4148
|
-
y: contours.bounds.min.y +
|
|
4149
|
-
z:
|
|
4317
|
+
x: contours.bounds.min.x + positionX,
|
|
4318
|
+
y: contours.bounds.min.y + positionY,
|
|
4319
|
+
z: positionZ
|
|
4150
4320
|
},
|
|
4151
4321
|
max: {
|
|
4152
|
-
x: contours.bounds.max.x +
|
|
4153
|
-
y: contours.bounds.max.y +
|
|
4154
|
-
z:
|
|
4322
|
+
x: contours.bounds.max.x + positionX,
|
|
4323
|
+
y: contours.bounds.max.y + positionY,
|
|
4324
|
+
z: positionZ + depth
|
|
4155
4325
|
}
|
|
4156
4326
|
}
|
|
4157
4327
|
};
|
|
4158
4328
|
}
|
|
4159
4329
|
getContoursForGlyph(glyphId) {
|
|
4330
|
+
// Fast path: skip HarfBuzz draw for known-empty glyphs (spaces, zero-width, etc)
|
|
4331
|
+
if (this.emptyGlyphs.has(glyphId)) {
|
|
4332
|
+
return {
|
|
4333
|
+
glyphId,
|
|
4334
|
+
paths: [],
|
|
4335
|
+
bounds: {
|
|
4336
|
+
min: { x: 0, y: 0 },
|
|
4337
|
+
max: { x: 0, y: 0 }
|
|
4338
|
+
}
|
|
4339
|
+
};
|
|
4340
|
+
}
|
|
4160
4341
|
const key = `${this.cacheKeyPrefix}_${glyphId}`;
|
|
4161
4342
|
const cached = this.contourCache.get(key);
|
|
4162
4343
|
if (cached) {
|
|
@@ -4177,11 +4358,15 @@ class GlyphGeometryBuilder {
|
|
|
4177
4358
|
max: { x: 0, y: 0 }
|
|
4178
4359
|
}
|
|
4179
4360
|
};
|
|
4361
|
+
// Mark glyph as empty for future fast-path
|
|
4362
|
+
if (contours.paths.length === 0) {
|
|
4363
|
+
this.emptyGlyphs.add(glyphId);
|
|
4364
|
+
}
|
|
4180
4365
|
this.contourCache.set(key, contours);
|
|
4181
4366
|
return contours;
|
|
4182
4367
|
}
|
|
4183
4368
|
tessellateGlyphCluster(paths, depth, isCFF) {
|
|
4184
|
-
const processedGeometry = this.tessellator.process(paths, true, isCFF);
|
|
4369
|
+
const processedGeometry = this.tessellator.process(paths, true, isCFF, depth !== 0);
|
|
4185
4370
|
return this.extrudeAndPackage(processedGeometry, depth);
|
|
4186
4371
|
}
|
|
4187
4372
|
extrudeAndPackage(processedGeometry, depth) {
|
|
@@ -4229,7 +4414,7 @@ class GlyphGeometryBuilder {
|
|
|
4229
4414
|
glyphId: glyphContours.glyphId,
|
|
4230
4415
|
pathCount: glyphContours.paths.length
|
|
4231
4416
|
});
|
|
4232
|
-
const processedGeometry = this.tessellator.process(glyphContours.paths, removeOverlaps, isCFF);
|
|
4417
|
+
const processedGeometry = this.tessellator.process(glyphContours.paths, removeOverlaps, isCFF, depth !== 0);
|
|
4233
4418
|
perfLogger.end('GlyphGeometryBuilder.tessellateGlyph');
|
|
4234
4419
|
return this.extrudeAndPackage(processedGeometry, depth);
|
|
4235
4420
|
}
|
|
@@ -4299,8 +4484,11 @@ class TextShaper {
|
|
|
4299
4484
|
const clusters = [];
|
|
4300
4485
|
let currentClusterGlyphs = [];
|
|
4301
4486
|
let currentClusterText = '';
|
|
4302
|
-
let
|
|
4303
|
-
let
|
|
4487
|
+
let clusterStartX = 0;
|
|
4488
|
+
let clusterStartY = 0;
|
|
4489
|
+
let cursorX = lineInfo.xOffset;
|
|
4490
|
+
let cursorY = -lineIndex * scaledLineHeight;
|
|
4491
|
+
const cursorZ = 0;
|
|
4304
4492
|
// Apply letter spacing after each glyph to match width measurements used during line breaking
|
|
4305
4493
|
const letterSpacingFU = letterSpacing * this.loadedFont.upem;
|
|
4306
4494
|
const spaceAdjustment = this.calculateSpaceAdjustment(lineInfo, align, letterSpacing);
|
|
@@ -4325,31 +4513,31 @@ class TextShaper {
|
|
|
4325
4513
|
clusters.push({
|
|
4326
4514
|
text: currentClusterText,
|
|
4327
4515
|
glyphs: currentClusterGlyphs,
|
|
4328
|
-
position:
|
|
4516
|
+
position: new Vec3(clusterStartX, clusterStartY, cursorZ)
|
|
4329
4517
|
});
|
|
4330
4518
|
currentClusterGlyphs = [];
|
|
4331
4519
|
currentClusterText = '';
|
|
4332
4520
|
}
|
|
4333
4521
|
}
|
|
4334
|
-
const
|
|
4335
|
-
|
|
4336
|
-
.add(new Vec3(glyph.dx, glyph.dy, 0));
|
|
4522
|
+
const absoluteGlyphX = cursorX + glyph.dx;
|
|
4523
|
+
const absoluteGlyphY = cursorY + glyph.dy;
|
|
4337
4524
|
if (!isWhitespace) {
|
|
4338
4525
|
if (currentClusterGlyphs.length === 0) {
|
|
4339
|
-
|
|
4526
|
+
clusterStartX = absoluteGlyphX;
|
|
4527
|
+
clusterStartY = absoluteGlyphY;
|
|
4340
4528
|
}
|
|
4341
|
-
glyph.x =
|
|
4342
|
-
glyph.y =
|
|
4529
|
+
glyph.x = absoluteGlyphX - clusterStartX;
|
|
4530
|
+
glyph.y = absoluteGlyphY - clusterStartY;
|
|
4343
4531
|
currentClusterGlyphs.push(glyph);
|
|
4344
4532
|
currentClusterText += lineInfo.text[glyph.cl];
|
|
4345
4533
|
}
|
|
4346
|
-
|
|
4347
|
-
|
|
4534
|
+
cursorX += glyph.ax;
|
|
4535
|
+
cursorY += glyph.ay;
|
|
4348
4536
|
if (letterSpacingFU !== 0 && i < glyphInfos.length - 1) {
|
|
4349
|
-
|
|
4537
|
+
cursorX += letterSpacingFU;
|
|
4350
4538
|
}
|
|
4351
4539
|
if (isWhitespace) {
|
|
4352
|
-
|
|
4540
|
+
cursorX += spaceAdjustment;
|
|
4353
4541
|
}
|
|
4354
4542
|
// CJK glue adjustment (must match exactly where LineBreak adds glue)
|
|
4355
4543
|
if (cjkAdjustment !== 0 && i < glyphInfos.length - 1 && !isWhitespace) {
|
|
@@ -4370,7 +4558,7 @@ class TextShaper {
|
|
|
4370
4558
|
shouldApply = false;
|
|
4371
4559
|
}
|
|
4372
4560
|
if (shouldApply) {
|
|
4373
|
-
|
|
4561
|
+
cursorX += cjkAdjustment;
|
|
4374
4562
|
}
|
|
4375
4563
|
}
|
|
4376
4564
|
}
|
|
@@ -4379,7 +4567,7 @@ class TextShaper {
|
|
|
4379
4567
|
clusters.push({
|
|
4380
4568
|
text: currentClusterText,
|
|
4381
4569
|
glyphs: currentClusterGlyphs,
|
|
4382
|
-
position:
|
|
4570
|
+
position: new Vec3(clusterStartX, clusterStartY, cursorZ)
|
|
4383
4571
|
});
|
|
4384
4572
|
}
|
|
4385
4573
|
return clusters;
|
|
@@ -5206,9 +5394,8 @@ class Text {
|
|
|
5206
5394
|
const loadedFont = await Text.resolveFont(options);
|
|
5207
5395
|
const text = new Text();
|
|
5208
5396
|
text.setLoadedFont(loadedFont);
|
|
5209
|
-
//
|
|
5210
|
-
const
|
|
5211
|
-
const result = await text.createGeometry(geometryOptions);
|
|
5397
|
+
// Pass full options so createGeometry honors maxCacheSizeMB etc
|
|
5398
|
+
const result = await text.createGeometry(options);
|
|
5212
5399
|
// Recursive update function
|
|
5213
5400
|
const update = async (newOptions) => {
|
|
5214
5401
|
// Merge options - preserve font from original options if not provided
|
|
@@ -5230,8 +5417,7 @@ class Text {
|
|
|
5230
5417
|
}
|
|
5231
5418
|
// Update closure options for next time
|
|
5232
5419
|
options = mergedOptions;
|
|
5233
|
-
const
|
|
5234
|
-
const newResult = await text.createGeometry(currentGeometryOptions);
|
|
5420
|
+
const newResult = await text.createGeometry(options);
|
|
5235
5421
|
return {
|
|
5236
5422
|
...newResult,
|
|
5237
5423
|
getLoadedFont: () => text.getLoadedFont(),
|
|
@@ -5656,7 +5842,7 @@ class Text {
|
|
|
5656
5842
|
if (!this.textLayout) {
|
|
5657
5843
|
this.textLayout = new TextLayout(this.loadedFont);
|
|
5658
5844
|
}
|
|
5659
|
-
const alignmentResult = this.textLayout.
|
|
5845
|
+
const alignmentResult = this.textLayout.computeAlignmentOffset({
|
|
5660
5846
|
width,
|
|
5661
5847
|
align,
|
|
5662
5848
|
planeBounds
|
|
@@ -5665,9 +5851,19 @@ class Text {
|
|
|
5665
5851
|
planeBounds.min.x = alignmentResult.adjustedBounds.min.x;
|
|
5666
5852
|
planeBounds.max.x = alignmentResult.adjustedBounds.max.x;
|
|
5667
5853
|
const finalScale = size / this.loadedFont.upem;
|
|
5854
|
+
const offsetScaled = offset * finalScale;
|
|
5668
5855
|
// Scale vertices only (normals are unit vectors, don't scale)
|
|
5669
|
-
|
|
5670
|
-
|
|
5856
|
+
if (offsetScaled === 0) {
|
|
5857
|
+
for (let i = 0; i < vertices.length; i++) {
|
|
5858
|
+
vertices[i] *= finalScale;
|
|
5859
|
+
}
|
|
5860
|
+
}
|
|
5861
|
+
else {
|
|
5862
|
+
for (let i = 0; i < vertices.length; i += 3) {
|
|
5863
|
+
vertices[i] = vertices[i] * finalScale + offsetScaled;
|
|
5864
|
+
vertices[i + 1] *= finalScale;
|
|
5865
|
+
vertices[i + 2] *= finalScale;
|
|
5866
|
+
}
|
|
5671
5867
|
}
|
|
5672
5868
|
planeBounds.min.x *= finalScale;
|
|
5673
5869
|
planeBounds.min.y *= finalScale;
|
|
@@ -5677,14 +5873,10 @@ class Text {
|
|
|
5677
5873
|
planeBounds.max.z *= finalScale;
|
|
5678
5874
|
for (let i = 0; i < glyphInfoArray.length; i++) {
|
|
5679
5875
|
const glyphInfo = glyphInfoArray[i];
|
|
5680
|
-
|
|
5681
|
-
glyphInfo.bounds.min.x += offset;
|
|
5682
|
-
glyphInfo.bounds.max.x += offset;
|
|
5683
|
-
}
|
|
5684
|
-
glyphInfo.bounds.min.x *= finalScale;
|
|
5876
|
+
glyphInfo.bounds.min.x = glyphInfo.bounds.min.x * finalScale + offsetScaled;
|
|
5685
5877
|
glyphInfo.bounds.min.y *= finalScale;
|
|
5686
5878
|
glyphInfo.bounds.min.z *= finalScale;
|
|
5687
|
-
glyphInfo.bounds.max.x
|
|
5879
|
+
glyphInfo.bounds.max.x = glyphInfo.bounds.max.x * finalScale + offsetScaled;
|
|
5688
5880
|
glyphInfo.bounds.max.y *= finalScale;
|
|
5689
5881
|
glyphInfo.bounds.max.z *= finalScale;
|
|
5690
5882
|
}
|