three-text 0.4.1 → 0.4.3
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/dist/index.cjs +424 -184
- package/dist/index.js +424 -184
- package/dist/index.min.cjs +641 -599
- package/dist/index.min.js +641 -599
- package/dist/index.umd.js +424 -184
- package/dist/index.umd.min.js +589 -547
- package/dist/types/core/cache/GlyphGeometryBuilder.d.ts +3 -0
- package/dist/types/core/geometry/Tessellator.d.ts +4 -0
- package/dist/types/core/layout/LineBreak.d.ts +0 -1
- package/package.json +1 -1
- package/dist/types/core/layout/LineBreakGood.d.ts +0 -100
- package/dist/types/core/layout/LineBreakHashMap.d.ts +0 -103
- package/dist/types/core/layout/LineBreakOriginal.d.ts +0 -103
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* three-text v0.4.
|
|
2
|
+
* three-text v0.4.3
|
|
3
3
|
* Copyright (C) 2025 Countertype LLC
|
|
4
4
|
*
|
|
5
5
|
* This program is free software: you can redistribute it and/or modify
|
|
@@ -218,6 +218,9 @@ class ActiveNodeList {
|
|
|
218
218
|
existing.previous = node.previous;
|
|
219
219
|
existing.hyphenated = node.hyphenated;
|
|
220
220
|
existing.line = node.line;
|
|
221
|
+
existing.cumWidth = node.cumWidth;
|
|
222
|
+
existing.cumStretch = node.cumStretch;
|
|
223
|
+
existing.cumShrink = node.cumShrink;
|
|
221
224
|
return true;
|
|
222
225
|
}
|
|
223
226
|
return false;
|
|
@@ -294,27 +297,6 @@ class LineBreak {
|
|
|
294
297
|
return FitnessClass.LOOSE; // stretching 0.5-1.0
|
|
295
298
|
return FitnessClass.VERY_LOOSE; // stretching > 1.0
|
|
296
299
|
}
|
|
297
|
-
// Build prefix sums so we can quickly compute the width/stretch/shrink
|
|
298
|
-
// of any range [a, b] as cumulative[b] - cumulative[a]
|
|
299
|
-
static computeCumulativeWidths(items) {
|
|
300
|
-
const n = items.length + 1;
|
|
301
|
-
const width = new Float64Array(n);
|
|
302
|
-
const stretch = new Float64Array(n);
|
|
303
|
-
const shrink = new Float64Array(n);
|
|
304
|
-
for (let i = 0; i < items.length; i++) {
|
|
305
|
-
const item = items[i];
|
|
306
|
-
width[i + 1] = width[i] + item.width;
|
|
307
|
-
if (item.type === ItemType.GLUE) {
|
|
308
|
-
stretch[i + 1] = stretch[i] + item.stretch;
|
|
309
|
-
shrink[i + 1] = shrink[i] + item.shrink;
|
|
310
|
-
}
|
|
311
|
-
else {
|
|
312
|
-
stretch[i + 1] = stretch[i];
|
|
313
|
-
shrink[i + 1] = shrink[i];
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
return { width, stretch, shrink };
|
|
317
|
-
}
|
|
318
300
|
static findHyphenationPoints(word, language = 'en-us', availablePatterns, lefthyphenmin = DEFAULT_LEFT_HYPHEN_MIN, righthyphenmin = DEFAULT_RIGHT_HYPHEN_MIN) {
|
|
319
301
|
let patternTrie;
|
|
320
302
|
if (availablePatterns && availablePatterns[language]) {
|
|
@@ -734,9 +716,8 @@ class LineBreak {
|
|
|
734
716
|
// TeX: line_break inner loop (tex.web lines 16169-17256)
|
|
735
717
|
// Finds optimal breakpoints using Knuth-Plass algorithm
|
|
736
718
|
static lineBreak(items, lineWidth, threshold, emergencyStretch, context) {
|
|
737
|
-
const cumulative = this.computeCumulativeWidths(items);
|
|
738
719
|
const activeNodes = new ActiveNodeList();
|
|
739
|
-
// Start node
|
|
720
|
+
// Start node with zero cumulative width
|
|
740
721
|
activeNodes.insert({
|
|
741
722
|
position: 0,
|
|
742
723
|
line: 0,
|
|
@@ -745,8 +726,15 @@ class LineBreak {
|
|
|
745
726
|
previous: null,
|
|
746
727
|
hyphenated: false,
|
|
747
728
|
active: true,
|
|
748
|
-
activeIndex: 0
|
|
729
|
+
activeIndex: 0,
|
|
730
|
+
cumWidth: 0,
|
|
731
|
+
cumStretch: 0,
|
|
732
|
+
cumShrink: 0
|
|
749
733
|
});
|
|
734
|
+
// Cumulative width from paragraph start, representing items[0..i-1]
|
|
735
|
+
let cumWidth = 0;
|
|
736
|
+
let cumStretch = 0;
|
|
737
|
+
let cumShrink = 0;
|
|
750
738
|
// Process each item
|
|
751
739
|
for (let i = 0; i < items.length; i++) {
|
|
752
740
|
const item = items[i];
|
|
@@ -757,8 +745,22 @@ class LineBreak {
|
|
|
757
745
|
(item.type === ItemType.GLUE &&
|
|
758
746
|
i > 0 &&
|
|
759
747
|
items[i - 1].type === ItemType.BOX);
|
|
760
|
-
if (!isBreakpoint)
|
|
748
|
+
if (!isBreakpoint) {
|
|
749
|
+
// Accumulate width for non-breakpoint items
|
|
750
|
+
if (item.type === ItemType.BOX) {
|
|
751
|
+
cumWidth += item.width;
|
|
752
|
+
}
|
|
753
|
+
else if (item.type === ItemType.GLUE) {
|
|
754
|
+
const glue = item;
|
|
755
|
+
cumWidth += glue.width;
|
|
756
|
+
cumStretch += glue.stretch;
|
|
757
|
+
cumShrink += glue.shrink;
|
|
758
|
+
}
|
|
759
|
+
else if (item.type === ItemType.DISCRETIONARY) {
|
|
760
|
+
cumWidth += item.width;
|
|
761
|
+
}
|
|
761
762
|
continue;
|
|
763
|
+
}
|
|
762
764
|
// Get penalty and flagged status
|
|
763
765
|
let pi = 0;
|
|
764
766
|
let flagged = false;
|
|
@@ -780,18 +782,14 @@ class LineBreak {
|
|
|
780
782
|
const bestDemerits = [Infinity, Infinity, Infinity, Infinity];
|
|
781
783
|
// Nodes to deactivate
|
|
782
784
|
const toDeactivate = [];
|
|
783
|
-
// Current cumulative values at position i
|
|
784
|
-
const curWidth = cumulative.width[i];
|
|
785
|
-
const curStretch = cumulative.stretch[i];
|
|
786
|
-
const curShrink = cumulative.shrink[i];
|
|
787
785
|
// Try each active node as predecessor
|
|
788
786
|
const active = activeNodes.getActive();
|
|
789
787
|
for (let j = 0; j < active.length; j++) {
|
|
790
788
|
const a = active[j];
|
|
791
|
-
// Line width from a to i
|
|
792
|
-
const lineW =
|
|
793
|
-
const lineStretch =
|
|
794
|
-
const lineShrink =
|
|
789
|
+
// Line width from a to i
|
|
790
|
+
const lineW = cumWidth - a.cumWidth + breakWidth;
|
|
791
|
+
const lineStretch = cumStretch - a.cumStretch;
|
|
792
|
+
const lineShrink = cumShrink - a.cumShrink;
|
|
795
793
|
const shortfall = lineWidth - lineW;
|
|
796
794
|
let ratio;
|
|
797
795
|
if (shortfall > 0) {
|
|
@@ -847,7 +845,10 @@ class LineBreak {
|
|
|
847
845
|
previous: a,
|
|
848
846
|
hyphenated: flagged,
|
|
849
847
|
active: true,
|
|
850
|
-
activeIndex: -1
|
|
848
|
+
activeIndex: -1,
|
|
849
|
+
cumWidth: cumWidth,
|
|
850
|
+
cumStretch: cumStretch,
|
|
851
|
+
cumShrink: cumShrink
|
|
851
852
|
};
|
|
852
853
|
}
|
|
853
854
|
}
|
|
@@ -864,6 +865,19 @@ class LineBreak {
|
|
|
864
865
|
if (activeNodes.size() === 0 && pi !== EJECT_PENALTY) {
|
|
865
866
|
return null;
|
|
866
867
|
}
|
|
868
|
+
// Accumulate width after evaluating this breakpoint
|
|
869
|
+
if (item.type === ItemType.BOX) {
|
|
870
|
+
cumWidth += item.width;
|
|
871
|
+
}
|
|
872
|
+
else if (item.type === ItemType.GLUE) {
|
|
873
|
+
const glue = item;
|
|
874
|
+
cumWidth += glue.width;
|
|
875
|
+
cumStretch += glue.stretch;
|
|
876
|
+
cumShrink += glue.shrink;
|
|
877
|
+
}
|
|
878
|
+
else if (item.type === ItemType.DISCRETIONARY) {
|
|
879
|
+
cumWidth += item.width;
|
|
880
|
+
}
|
|
867
881
|
}
|
|
868
882
|
// Find best solution
|
|
869
883
|
let best = null;
|
|
@@ -2488,6 +2502,12 @@ class Tessellator {
|
|
|
2488
2502
|
logger.log(`Tessellator: removeOverlaps=${removeOverlaps}, processing ${valid.length} paths`);
|
|
2489
2503
|
return this.tessellate(valid, removeOverlaps, isCFF, needsExtrusionContours);
|
|
2490
2504
|
}
|
|
2505
|
+
processContours(contours, removeOverlaps = true, isCFF = false, needsExtrusionContours = true) {
|
|
2506
|
+
if (contours.length === 0) {
|
|
2507
|
+
return { triangles: { vertices: [], indices: [] }, contours: [] };
|
|
2508
|
+
}
|
|
2509
|
+
return this.tessellateContours(contours, removeOverlaps, isCFF, needsExtrusionContours);
|
|
2510
|
+
}
|
|
2491
2511
|
tessellate(paths, removeOverlaps, isCFF, needsExtrusionContours) {
|
|
2492
2512
|
// libtess expects CCW winding; TTF outer contours are CW
|
|
2493
2513
|
const needsWindingReversal = !isCFF && !removeOverlaps;
|
|
@@ -2552,6 +2572,68 @@ class Tessellator {
|
|
|
2552
2572
|
contours: extrusionContours
|
|
2553
2573
|
};
|
|
2554
2574
|
}
|
|
2575
|
+
tessellateContours(contours, removeOverlaps, isCFF, needsExtrusionContours) {
|
|
2576
|
+
const needsWindingReversal = !isCFF && !removeOverlaps;
|
|
2577
|
+
let originalContours;
|
|
2578
|
+
let tessContours;
|
|
2579
|
+
if (needsWindingReversal) {
|
|
2580
|
+
tessContours = this.reverseContours(contours);
|
|
2581
|
+
if (removeOverlaps || needsExtrusionContours) {
|
|
2582
|
+
originalContours = contours;
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
else {
|
|
2586
|
+
originalContours = contours;
|
|
2587
|
+
tessContours = contours;
|
|
2588
|
+
}
|
|
2589
|
+
let extrusionContours = needsExtrusionContours
|
|
2590
|
+
? needsWindingReversal
|
|
2591
|
+
? tessContours
|
|
2592
|
+
: (originalContours ?? contours)
|
|
2593
|
+
: [];
|
|
2594
|
+
if (removeOverlaps) {
|
|
2595
|
+
logger.log('Two-pass: boundary extraction then triangulation');
|
|
2596
|
+
perfLogger.start('Tessellator.boundaryPass', {
|
|
2597
|
+
contourCount: tessContours.length
|
|
2598
|
+
});
|
|
2599
|
+
const boundaryResult = this.performTessellation(originalContours, 'boundary');
|
|
2600
|
+
perfLogger.end('Tessellator.boundaryPass');
|
|
2601
|
+
if (!boundaryResult) {
|
|
2602
|
+
logger.warn('libtess returned empty result from boundary pass');
|
|
2603
|
+
return { triangles: { vertices: [], indices: [] }, contours: [] };
|
|
2604
|
+
}
|
|
2605
|
+
tessContours = this.boundaryToContours(boundaryResult);
|
|
2606
|
+
if (needsExtrusionContours) {
|
|
2607
|
+
extrusionContours = tessContours;
|
|
2608
|
+
}
|
|
2609
|
+
logger.log(`Boundary pass created ${tessContours.length} contours. Starting triangulation pass.`);
|
|
2610
|
+
}
|
|
2611
|
+
else {
|
|
2612
|
+
logger.log(`Single-pass triangulation for ${isCFF ? 'CFF' : 'TTF'}`);
|
|
2613
|
+
}
|
|
2614
|
+
perfLogger.start('Tessellator.triangulationPass', {
|
|
2615
|
+
contourCount: tessContours.length
|
|
2616
|
+
});
|
|
2617
|
+
const triangleResult = this.performTessellation(tessContours, 'triangles');
|
|
2618
|
+
perfLogger.end('Tessellator.triangulationPass');
|
|
2619
|
+
if (!triangleResult) {
|
|
2620
|
+
const warning = removeOverlaps
|
|
2621
|
+
? 'libtess returned empty result from triangulation pass'
|
|
2622
|
+
: 'libtess returned empty result from single-pass triangulation';
|
|
2623
|
+
logger.warn(warning);
|
|
2624
|
+
return {
|
|
2625
|
+
triangles: { vertices: [], indices: [] },
|
|
2626
|
+
contours: extrusionContours
|
|
2627
|
+
};
|
|
2628
|
+
}
|
|
2629
|
+
return {
|
|
2630
|
+
triangles: {
|
|
2631
|
+
vertices: triangleResult.vertices,
|
|
2632
|
+
indices: triangleResult.indices || []
|
|
2633
|
+
},
|
|
2634
|
+
contours: extrusionContours
|
|
2635
|
+
};
|
|
2636
|
+
}
|
|
2555
2637
|
pathsToContours(paths, reversePoints = false) {
|
|
2556
2638
|
const contours = new Array(paths.length);
|
|
2557
2639
|
for (let p = 0; p < paths.length; p++) {
|
|
@@ -2589,6 +2671,35 @@ class Tessellator {
|
|
|
2589
2671
|
}
|
|
2590
2672
|
return contours;
|
|
2591
2673
|
}
|
|
2674
|
+
reverseContours(contours) {
|
|
2675
|
+
const reversed = new Array(contours.length);
|
|
2676
|
+
for (let i = 0; i < contours.length; i++) {
|
|
2677
|
+
reversed[i] = this.reverseContour(contours[i]);
|
|
2678
|
+
}
|
|
2679
|
+
return reversed;
|
|
2680
|
+
}
|
|
2681
|
+
reverseContour(contour) {
|
|
2682
|
+
const len = contour.length;
|
|
2683
|
+
if (len === 0)
|
|
2684
|
+
return [];
|
|
2685
|
+
const isClosed = len >= 4 &&
|
|
2686
|
+
contour[0] === contour[len - 2] &&
|
|
2687
|
+
contour[1] === contour[len - 1];
|
|
2688
|
+
const end = isClosed ? len - 2 : len;
|
|
2689
|
+
if (end === 0)
|
|
2690
|
+
return [];
|
|
2691
|
+
const reversed = new Array(end + 2);
|
|
2692
|
+
let out = 0;
|
|
2693
|
+
for (let i = end - 2; i >= 0; i -= 2) {
|
|
2694
|
+
reversed[out++] = contour[i];
|
|
2695
|
+
reversed[out++] = contour[i + 1];
|
|
2696
|
+
}
|
|
2697
|
+
if (out >= 2) {
|
|
2698
|
+
reversed[out++] = reversed[0];
|
|
2699
|
+
reversed[out++] = reversed[1];
|
|
2700
|
+
}
|
|
2701
|
+
return reversed;
|
|
2702
|
+
}
|
|
2592
2703
|
performTessellation(contours, mode) {
|
|
2593
2704
|
const tess = new libtess_minExports.GluTesselator();
|
|
2594
2705
|
tess.gluTessProperty(libtess_minExports.gluEnum.GLU_TESS_WINDING_RULE, libtess_minExports.windingRule.GLU_TESS_WINDING_NONZERO);
|
|
@@ -2715,80 +2826,86 @@ class Extruder {
|
|
|
2715
2826
|
extrude(geometry, depth = 0, unitsPerEm) {
|
|
2716
2827
|
const points = geometry.triangles.vertices;
|
|
2717
2828
|
const triangleIndices = geometry.triangles.indices;
|
|
2718
|
-
const
|
|
2719
|
-
|
|
2829
|
+
const contours = geometry.contours;
|
|
2830
|
+
const pointLen = points.length;
|
|
2831
|
+
const numPoints = pointLen / 2;
|
|
2832
|
+
// Prefer contours for side walls; fall back to triangle edges
|
|
2720
2833
|
let boundaryEdges = [];
|
|
2834
|
+
let sideEdgeCount = 0;
|
|
2835
|
+
let useContours = false;
|
|
2721
2836
|
if (depth !== 0) {
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
const c = triangleIndices[i + 2];
|
|
2730
|
-
let key, v0, v1;
|
|
2731
|
-
if (a < b) {
|
|
2732
|
-
key = (a << 16) | b;
|
|
2733
|
-
v0 = a;
|
|
2734
|
-
v1 = b;
|
|
2735
|
-
}
|
|
2736
|
-
else {
|
|
2737
|
-
key = (b << 16) | a;
|
|
2738
|
-
v0 = a;
|
|
2739
|
-
v1 = b;
|
|
2740
|
-
}
|
|
2741
|
-
let data = edgeMap.get(key);
|
|
2742
|
-
if (data) {
|
|
2743
|
-
data[2]++;
|
|
2744
|
-
}
|
|
2745
|
-
else {
|
|
2746
|
-
edgeMap.set(key, [v0, v1, 1]);
|
|
2747
|
-
}
|
|
2748
|
-
if (b < c) {
|
|
2749
|
-
key = (b << 16) | c;
|
|
2750
|
-
v0 = b;
|
|
2751
|
-
v1 = c;
|
|
2752
|
-
}
|
|
2753
|
-
else {
|
|
2754
|
-
key = (c << 16) | b;
|
|
2755
|
-
v0 = b;
|
|
2756
|
-
v1 = c;
|
|
2757
|
-
}
|
|
2758
|
-
data = edgeMap.get(key);
|
|
2759
|
-
if (data) {
|
|
2760
|
-
data[2]++;
|
|
2761
|
-
}
|
|
2762
|
-
else {
|
|
2763
|
-
edgeMap.set(key, [v0, v1, 1]);
|
|
2764
|
-
}
|
|
2765
|
-
if (c < a) {
|
|
2766
|
-
key = (c << 16) | a;
|
|
2767
|
-
v0 = c;
|
|
2768
|
-
v1 = a;
|
|
2769
|
-
}
|
|
2770
|
-
else {
|
|
2771
|
-
key = (a << 16) | c;
|
|
2772
|
-
v0 = c;
|
|
2773
|
-
v1 = a;
|
|
2774
|
-
}
|
|
2775
|
-
data = edgeMap.get(key);
|
|
2776
|
-
if (data) {
|
|
2777
|
-
data[2]++;
|
|
2778
|
-
}
|
|
2779
|
-
else {
|
|
2780
|
-
edgeMap.set(key, [v0, v1, 1]);
|
|
2837
|
+
if (contours.length > 0) {
|
|
2838
|
+
useContours = true;
|
|
2839
|
+
for (const contour of contours) {
|
|
2840
|
+
const contourPointCount = contour.length >> 1;
|
|
2841
|
+
if (contourPointCount >= 2) {
|
|
2842
|
+
sideEdgeCount += contourPointCount - 1;
|
|
2843
|
+
}
|
|
2781
2844
|
}
|
|
2782
2845
|
}
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2846
|
+
else {
|
|
2847
|
+
// Use a directionless key (min/max) to detect shared edges
|
|
2848
|
+
// Store the directed edge (a->b) and mark as null when seen twice
|
|
2849
|
+
const edgeMap = new Map();
|
|
2850
|
+
const triLen = triangleIndices.length;
|
|
2851
|
+
for (let i = 0; i < triLen; i += 3) {
|
|
2852
|
+
const a = triangleIndices[i];
|
|
2853
|
+
const b = triangleIndices[i + 1];
|
|
2854
|
+
const c = triangleIndices[i + 2];
|
|
2855
|
+
let key, packed;
|
|
2856
|
+
if (a < b) {
|
|
2857
|
+
key = (a << 16) | b;
|
|
2858
|
+
}
|
|
2859
|
+
else {
|
|
2860
|
+
key = (b << 16) | a;
|
|
2861
|
+
}
|
|
2862
|
+
packed = (a << 16) | b;
|
|
2863
|
+
let data = edgeMap.get(key);
|
|
2864
|
+
if (data === undefined) {
|
|
2865
|
+
edgeMap.set(key, packed);
|
|
2866
|
+
}
|
|
2867
|
+
else if (data !== null) {
|
|
2868
|
+
edgeMap.set(key, null);
|
|
2869
|
+
}
|
|
2870
|
+
if (b < c) {
|
|
2871
|
+
key = (b << 16) | c;
|
|
2872
|
+
}
|
|
2873
|
+
else {
|
|
2874
|
+
key = (c << 16) | b;
|
|
2875
|
+
}
|
|
2876
|
+
packed = (b << 16) | c;
|
|
2877
|
+
data = edgeMap.get(key);
|
|
2878
|
+
if (data === undefined) {
|
|
2879
|
+
edgeMap.set(key, packed);
|
|
2880
|
+
}
|
|
2881
|
+
else if (data !== null) {
|
|
2882
|
+
edgeMap.set(key, null);
|
|
2883
|
+
}
|
|
2884
|
+
if (c < a) {
|
|
2885
|
+
key = (c << 16) | a;
|
|
2886
|
+
}
|
|
2887
|
+
else {
|
|
2888
|
+
key = (a << 16) | c;
|
|
2889
|
+
}
|
|
2890
|
+
packed = (c << 16) | a;
|
|
2891
|
+
data = edgeMap.get(key);
|
|
2892
|
+
if (data === undefined) {
|
|
2893
|
+
edgeMap.set(key, packed);
|
|
2894
|
+
}
|
|
2895
|
+
else if (data !== null) {
|
|
2896
|
+
edgeMap.set(key, null);
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
boundaryEdges = [];
|
|
2900
|
+
for (const packedEdge of edgeMap.values()) {
|
|
2901
|
+
if (packedEdge === null)
|
|
2902
|
+
continue;
|
|
2903
|
+
boundaryEdges.push(packedEdge >>> 16, packedEdge & 0xffff);
|
|
2787
2904
|
}
|
|
2905
|
+
sideEdgeCount = boundaryEdges.length >> 1;
|
|
2788
2906
|
}
|
|
2789
2907
|
}
|
|
2790
|
-
const
|
|
2791
|
-
const sideVertexCount = depth === 0 ? 0 : sideEdgeCount * 4;
|
|
2908
|
+
const sideVertexCount = sideEdgeCount * 4;
|
|
2792
2909
|
const baseVertexCount = depth === 0 ? numPoints : numPoints * 2;
|
|
2793
2910
|
const vertexCount = baseVertexCount + sideVertexCount;
|
|
2794
2911
|
const vertices = new Float32Array(vertexCount * 3);
|
|
@@ -2798,26 +2915,24 @@ class Extruder {
|
|
|
2798
2915
|
: triangleIndices.length * 2 + sideEdgeCount * 6;
|
|
2799
2916
|
const indices = new Uint32Array(indexCount);
|
|
2800
2917
|
if (depth === 0) {
|
|
2801
|
-
let vPos = 0;
|
|
2802
|
-
|
|
2803
|
-
vertices[vPos] = points[
|
|
2804
|
-
vertices[vPos + 1] = points[i + 1];
|
|
2918
|
+
for (let p = 0, vPos = 0; p < pointLen; p += 2, vPos += 3) {
|
|
2919
|
+
vertices[vPos] = points[p];
|
|
2920
|
+
vertices[vPos + 1] = points[p + 1];
|
|
2805
2921
|
vertices[vPos + 2] = 0;
|
|
2806
2922
|
normals[vPos] = 0;
|
|
2807
2923
|
normals[vPos + 1] = 0;
|
|
2808
2924
|
normals[vPos + 2] = 1;
|
|
2809
|
-
vPos += 3;
|
|
2810
2925
|
}
|
|
2811
2926
|
indices.set(triangleIndices);
|
|
2812
2927
|
return { vertices, normals, indices };
|
|
2813
2928
|
}
|
|
2814
2929
|
const minBackOffset = unitsPerEm * 0.000025;
|
|
2815
2930
|
const backZ = depth <= minBackOffset ? minBackOffset : depth;
|
|
2816
|
-
|
|
2931
|
+
const backOffset = numPoints * 3;
|
|
2932
|
+
for (let p = 0, vi = 0, base0 = 0; p < pointLen; p += 2, vi++, base0 += 3) {
|
|
2817
2933
|
const x = points[p];
|
|
2818
2934
|
const y = points[p + 1];
|
|
2819
2935
|
// Cap at z=0
|
|
2820
|
-
const base0 = vi * 3;
|
|
2821
2936
|
vertices[base0] = x;
|
|
2822
2937
|
vertices[base0 + 1] = y;
|
|
2823
2938
|
vertices[base0 + 2] = 0;
|
|
@@ -2825,7 +2940,7 @@ class Extruder {
|
|
|
2825
2940
|
normals[base0 + 1] = 0;
|
|
2826
2941
|
normals[base0 + 2] = -1;
|
|
2827
2942
|
// Cap at z=depth
|
|
2828
|
-
const baseD =
|
|
2943
|
+
const baseD = base0 + backOffset;
|
|
2829
2944
|
vertices[baseD] = x;
|
|
2830
2945
|
vertices[baseD + 1] = y;
|
|
2831
2946
|
vertices[baseD + 2] = backZ;
|
|
@@ -2844,61 +2959,119 @@ class Extruder {
|
|
|
2844
2959
|
}
|
|
2845
2960
|
let nextVertex = numPoints * 2;
|
|
2846
2961
|
let idxPos = triLen * 2;
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2962
|
+
if (useContours) {
|
|
2963
|
+
for (const contour of contours) {
|
|
2964
|
+
const contourLen = contour.length;
|
|
2965
|
+
if (contourLen < 4)
|
|
2966
|
+
continue;
|
|
2967
|
+
for (let i = 0; i < contourLen - 2; i += 2) {
|
|
2968
|
+
const p0x = contour[i];
|
|
2969
|
+
const p0y = contour[i + 1];
|
|
2970
|
+
const p1x = contour[i + 2];
|
|
2971
|
+
const p1y = contour[i + 3];
|
|
2972
|
+
const ex = p1x - p0x;
|
|
2973
|
+
const ey = p1y - p0y;
|
|
2974
|
+
const lenSq = ex * ex + ey * ey;
|
|
2975
|
+
let nx = 0;
|
|
2976
|
+
let ny = 0;
|
|
2977
|
+
if (lenSq > 1e-10) {
|
|
2978
|
+
const invLen = 1 / Math.sqrt(lenSq);
|
|
2979
|
+
nx = ey * invLen;
|
|
2980
|
+
ny = -ex * invLen;
|
|
2981
|
+
}
|
|
2982
|
+
const base = nextVertex * 3;
|
|
2983
|
+
vertices[base] = p0x;
|
|
2984
|
+
vertices[base + 1] = p0y;
|
|
2985
|
+
vertices[base + 2] = 0;
|
|
2986
|
+
vertices[base + 3] = p1x;
|
|
2987
|
+
vertices[base + 4] = p1y;
|
|
2988
|
+
vertices[base + 5] = 0;
|
|
2989
|
+
vertices[base + 6] = p0x;
|
|
2990
|
+
vertices[base + 7] = p0y;
|
|
2991
|
+
vertices[base + 8] = backZ;
|
|
2992
|
+
vertices[base + 9] = p1x;
|
|
2993
|
+
vertices[base + 10] = p1y;
|
|
2994
|
+
vertices[base + 11] = backZ;
|
|
2995
|
+
normals[base] = nx;
|
|
2996
|
+
normals[base + 1] = ny;
|
|
2997
|
+
normals[base + 2] = 0;
|
|
2998
|
+
normals[base + 3] = nx;
|
|
2999
|
+
normals[base + 4] = ny;
|
|
3000
|
+
normals[base + 5] = 0;
|
|
3001
|
+
normals[base + 6] = nx;
|
|
3002
|
+
normals[base + 7] = ny;
|
|
3003
|
+
normals[base + 8] = 0;
|
|
3004
|
+
normals[base + 9] = nx;
|
|
3005
|
+
normals[base + 10] = ny;
|
|
3006
|
+
normals[base + 11] = 0;
|
|
3007
|
+
const baseVertex = nextVertex;
|
|
3008
|
+
indices[idxPos] = baseVertex;
|
|
3009
|
+
indices[idxPos + 1] = baseVertex + 1;
|
|
3010
|
+
indices[idxPos + 2] = baseVertex + 2;
|
|
3011
|
+
indices[idxPos + 3] = baseVertex + 1;
|
|
3012
|
+
indices[idxPos + 4] = baseVertex + 3;
|
|
3013
|
+
indices[idxPos + 5] = baseVertex + 2;
|
|
3014
|
+
idxPos += 6;
|
|
3015
|
+
nextVertex += 4;
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
else {
|
|
3020
|
+
for (let e = 0; e < sideEdgeCount; e++) {
|
|
3021
|
+
const edgeIndex = e << 1;
|
|
3022
|
+
const u = boundaryEdges[edgeIndex];
|
|
3023
|
+
const v = boundaryEdges[edgeIndex + 1];
|
|
3024
|
+
const u2 = u << 1;
|
|
3025
|
+
const v2 = v << 1;
|
|
3026
|
+
const p0x = points[u2];
|
|
3027
|
+
const p0y = points[u2 + 1];
|
|
3028
|
+
const p1x = points[v2];
|
|
3029
|
+
const p1y = points[v2 + 1];
|
|
3030
|
+
const ex = p1x - p0x;
|
|
3031
|
+
const ey = p1y - p0y;
|
|
3032
|
+
const lenSq = ex * ex + ey * ey;
|
|
3033
|
+
let nx = 0;
|
|
3034
|
+
let ny = 0;
|
|
3035
|
+
if (lenSq > 1e-10) {
|
|
3036
|
+
const invLen = 1 / Math.sqrt(lenSq);
|
|
3037
|
+
nx = ey * invLen;
|
|
3038
|
+
ny = -ex * invLen;
|
|
3039
|
+
}
|
|
3040
|
+
const base = nextVertex * 3;
|
|
3041
|
+
vertices[base] = p0x;
|
|
3042
|
+
vertices[base + 1] = p0y;
|
|
3043
|
+
vertices[base + 2] = 0;
|
|
3044
|
+
vertices[base + 3] = p1x;
|
|
3045
|
+
vertices[base + 4] = p1y;
|
|
3046
|
+
vertices[base + 5] = 0;
|
|
3047
|
+
vertices[base + 6] = p0x;
|
|
3048
|
+
vertices[base + 7] = p0y;
|
|
3049
|
+
vertices[base + 8] = backZ;
|
|
3050
|
+
vertices[base + 9] = p1x;
|
|
3051
|
+
vertices[base + 10] = p1y;
|
|
3052
|
+
vertices[base + 11] = backZ;
|
|
3053
|
+
normals[base] = nx;
|
|
3054
|
+
normals[base + 1] = ny;
|
|
3055
|
+
normals[base + 2] = 0;
|
|
3056
|
+
normals[base + 3] = nx;
|
|
3057
|
+
normals[base + 4] = ny;
|
|
3058
|
+
normals[base + 5] = 0;
|
|
3059
|
+
normals[base + 6] = nx;
|
|
3060
|
+
normals[base + 7] = ny;
|
|
3061
|
+
normals[base + 8] = 0;
|
|
3062
|
+
normals[base + 9] = nx;
|
|
3063
|
+
normals[base + 10] = ny;
|
|
3064
|
+
normals[base + 11] = 0;
|
|
3065
|
+
const baseVertex = nextVertex;
|
|
3066
|
+
indices[idxPos] = baseVertex;
|
|
3067
|
+
indices[idxPos + 1] = baseVertex + 1;
|
|
3068
|
+
indices[idxPos + 2] = baseVertex + 2;
|
|
3069
|
+
indices[idxPos + 3] = baseVertex + 1;
|
|
3070
|
+
indices[idxPos + 4] = baseVertex + 3;
|
|
3071
|
+
indices[idxPos + 5] = baseVertex + 2;
|
|
3072
|
+
idxPos += 6;
|
|
3073
|
+
nextVertex += 4;
|
|
3074
|
+
}
|
|
2902
3075
|
}
|
|
2903
3076
|
return { vertices, normals, indices };
|
|
2904
3077
|
}
|
|
@@ -3885,6 +4058,9 @@ class GlyphGeometryBuilder {
|
|
|
3885
4058
|
this.fontId = 'default';
|
|
3886
4059
|
this.cacheKeyPrefix = 'default';
|
|
3887
4060
|
this.emptyGlyphs = new Set();
|
|
4061
|
+
this.clusterPositions = [];
|
|
4062
|
+
this.clusterContoursScratch = [];
|
|
4063
|
+
this.taskScratch = [];
|
|
3888
4064
|
this.cache = cache;
|
|
3889
4065
|
this.loadedFont = loadedFont;
|
|
3890
4066
|
this.tessellator = new Tessellator();
|
|
@@ -3970,14 +4146,28 @@ class GlyphGeometryBuilder {
|
|
|
3970
4146
|
else {
|
|
3971
4147
|
perfLogger.start('GlyphGeometryBuilder.buildInstancedGeometry');
|
|
3972
4148
|
}
|
|
3973
|
-
const tasks =
|
|
4149
|
+
const tasks = this.taskScratch;
|
|
4150
|
+
tasks.length = 0;
|
|
4151
|
+
let taskCount = 0;
|
|
3974
4152
|
let totalVertexFloats = 0;
|
|
3975
4153
|
let totalNormalFloats = 0;
|
|
3976
4154
|
let totalIndexCount = 0;
|
|
3977
4155
|
let vertexCursor = 0; // vertex offset (not float offset)
|
|
3978
4156
|
const pushTask = (data, px, py, pz) => {
|
|
3979
4157
|
const vertexStart = vertexCursor;
|
|
3980
|
-
|
|
4158
|
+
let task = tasks[taskCount];
|
|
4159
|
+
if (task) {
|
|
4160
|
+
task.data = data;
|
|
4161
|
+
task.px = px;
|
|
4162
|
+
task.py = py;
|
|
4163
|
+
task.pz = pz;
|
|
4164
|
+
task.vertexStart = vertexStart;
|
|
4165
|
+
}
|
|
4166
|
+
else {
|
|
4167
|
+
task = { data, px, py, pz, vertexStart };
|
|
4168
|
+
tasks[taskCount] = task;
|
|
4169
|
+
}
|
|
4170
|
+
taskCount++;
|
|
3981
4171
|
totalVertexFloats += data.vertices.length;
|
|
3982
4172
|
totalNormalFloats += data.normals.length;
|
|
3983
4173
|
totalIndexCount += data.indices.length;
|
|
@@ -4027,8 +4217,21 @@ class GlyphGeometryBuilder {
|
|
|
4027
4217
|
boundaryGroups = cached.groups;
|
|
4028
4218
|
}
|
|
4029
4219
|
else {
|
|
4030
|
-
const
|
|
4031
|
-
|
|
4220
|
+
const glyphCount = cluster.glyphs.length;
|
|
4221
|
+
if (this.clusterPositions.length < glyphCount) {
|
|
4222
|
+
for (let i = this.clusterPositions.length; i < glyphCount; i++) {
|
|
4223
|
+
this.clusterPositions.push(new Vec3(0, 0, 0));
|
|
4224
|
+
}
|
|
4225
|
+
}
|
|
4226
|
+
this.clusterPositions.length = glyphCount;
|
|
4227
|
+
for (let i = 0; i < glyphCount; i++) {
|
|
4228
|
+
const glyph = cluster.glyphs[i];
|
|
4229
|
+
const pos = this.clusterPositions[i];
|
|
4230
|
+
pos.x = glyph.x ?? 0;
|
|
4231
|
+
pos.y = glyph.y ?? 0;
|
|
4232
|
+
pos.z = 0;
|
|
4233
|
+
}
|
|
4234
|
+
boundaryGroups = this.clusterer.cluster(clusterGlyphContours, this.clusterPositions);
|
|
4032
4235
|
this.clusteringCache.set(cacheKey, {
|
|
4033
4236
|
glyphIds: cluster.glyphs.map((g) => g.g),
|
|
4034
4237
|
positions: cluster.glyphs.map((g) => ({
|
|
@@ -4085,7 +4288,8 @@ class GlyphGeometryBuilder {
|
|
|
4085
4288
|
const clusterKey = this.getClusterKey(subClusterGlyphs, depth, removeOverlaps);
|
|
4086
4289
|
let cachedCluster = this.wordCache.get(clusterKey);
|
|
4087
4290
|
if (!cachedCluster) {
|
|
4088
|
-
const
|
|
4291
|
+
const clusterContours = this.clusterContoursScratch;
|
|
4292
|
+
let contourIndex = 0;
|
|
4089
4293
|
const refX = subClusterGlyphs[0].x ?? 0;
|
|
4090
4294
|
const refY = subClusterGlyphs[0].y ?? 0;
|
|
4091
4295
|
for (let i = 0; i < groupIndices.length; i++) {
|
|
@@ -4095,13 +4299,38 @@ class GlyphGeometryBuilder {
|
|
|
4095
4299
|
const relX = (glyph.x ?? 0) - refX;
|
|
4096
4300
|
const relY = (glyph.y ?? 0) - refY;
|
|
4097
4301
|
for (const path of glyphContours.paths) {
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4302
|
+
const points = path.points;
|
|
4303
|
+
const pointCount = points.length;
|
|
4304
|
+
if (pointCount < 3)
|
|
4305
|
+
continue;
|
|
4306
|
+
const isClosed = pointCount > 1 &&
|
|
4307
|
+
points[0].x === points[pointCount - 1].x &&
|
|
4308
|
+
points[0].y === points[pointCount - 1].y;
|
|
4309
|
+
const end = isClosed ? pointCount - 1 : pointCount;
|
|
4310
|
+
const needed = (end + 1) * 2;
|
|
4311
|
+
let contour = clusterContours[contourIndex];
|
|
4312
|
+
if (!contour || contour.length < needed) {
|
|
4313
|
+
contour = new Array(needed);
|
|
4314
|
+
clusterContours[contourIndex] = contour;
|
|
4315
|
+
}
|
|
4316
|
+
else {
|
|
4317
|
+
contour.length = needed;
|
|
4318
|
+
}
|
|
4319
|
+
let out = 0;
|
|
4320
|
+
for (let k = 0; k < end; k++) {
|
|
4321
|
+
const pt = points[k];
|
|
4322
|
+
contour[out++] = pt.x + relX;
|
|
4323
|
+
contour[out++] = pt.y + relY;
|
|
4324
|
+
}
|
|
4325
|
+
if (out >= 2) {
|
|
4326
|
+
contour[out++] = contour[0];
|
|
4327
|
+
contour[out++] = contour[1];
|
|
4328
|
+
}
|
|
4329
|
+
contourIndex++;
|
|
4102
4330
|
}
|
|
4103
4331
|
}
|
|
4104
|
-
|
|
4332
|
+
clusterContours.length = contourIndex;
|
|
4333
|
+
cachedCluster = this.tessellateGlyphCluster(clusterContours, depth, isCFF);
|
|
4105
4334
|
this.wordCache.set(clusterKey, cachedCluster);
|
|
4106
4335
|
}
|
|
4107
4336
|
// Calculate the absolute position of this sub-cluster based on its first glyph
|
|
@@ -4156,6 +4385,7 @@ class GlyphGeometryBuilder {
|
|
|
4156
4385
|
}
|
|
4157
4386
|
}
|
|
4158
4387
|
}
|
|
4388
|
+
tasks.length = taskCount;
|
|
4159
4389
|
// Allocate exact-sized buffers and fill once
|
|
4160
4390
|
const vertexArray = new Float32Array(totalVertexFloats);
|
|
4161
4391
|
const normalArray = new Float32Array(totalNormalFloats);
|
|
@@ -4171,17 +4401,27 @@ class GlyphGeometryBuilder {
|
|
|
4171
4401
|
const px = task.px;
|
|
4172
4402
|
const py = task.py;
|
|
4173
4403
|
const pz = task.pz;
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4404
|
+
const offsetX = px * scale;
|
|
4405
|
+
const offsetY = py * scale;
|
|
4406
|
+
const offsetZ = pz * scale;
|
|
4407
|
+
const vLen = v.length;
|
|
4408
|
+
let outPos = vertexPos;
|
|
4409
|
+
for (let j = 0; j < vLen; j += 3) {
|
|
4410
|
+
vertexArray[outPos] = v[j] * scale + offsetX;
|
|
4411
|
+
vertexArray[outPos + 1] = v[j + 1] * scale + offsetY;
|
|
4412
|
+
vertexArray[outPos + 2] = v[j + 2] * scale + offsetZ;
|
|
4413
|
+
outPos += 3;
|
|
4414
|
+
}
|
|
4415
|
+
vertexPos = outPos;
|
|
4179
4416
|
normalArray.set(n, normalPos);
|
|
4180
4417
|
normalPos += n.length;
|
|
4181
4418
|
const vertexStart = task.vertexStart;
|
|
4182
|
-
|
|
4183
|
-
|
|
4419
|
+
const idxLen = idx.length;
|
|
4420
|
+
let outIndexPos = indexPos;
|
|
4421
|
+
for (let j = 0; j < idxLen; j++) {
|
|
4422
|
+
indexArray[outIndexPos++] = idx[j] + vertexStart;
|
|
4184
4423
|
}
|
|
4424
|
+
indexPos = outIndexPos;
|
|
4185
4425
|
}
|
|
4186
4426
|
perfLogger.end('GlyphGeometryBuilder.buildInstancedGeometry');
|
|
4187
4427
|
planeBounds.min.x *= scale;
|
|
@@ -4279,8 +4519,8 @@ class GlyphGeometryBuilder {
|
|
|
4279
4519
|
this.contourCache.set(key, contours);
|
|
4280
4520
|
return contours;
|
|
4281
4521
|
}
|
|
4282
|
-
tessellateGlyphCluster(
|
|
4283
|
-
const processedGeometry = this.tessellator.
|
|
4522
|
+
tessellateGlyphCluster(contours, depth, isCFF) {
|
|
4523
|
+
const processedGeometry = this.tessellator.processContours(contours, true, isCFF, depth !== 0);
|
|
4284
4524
|
return this.extrudeAndPackage(processedGeometry, depth);
|
|
4285
4525
|
}
|
|
4286
4526
|
extrudeAndPackage(processedGeometry, depth) {
|