three-text 0.4.2 → 0.4.4
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 +405 -151
- package/dist/index.d.ts +1 -0
- package/dist/index.js +405 -151
- package/dist/index.min.cjs +379 -334
- package/dist/index.min.js +468 -423
- package/dist/index.umd.js +405 -151
- package/dist/index.umd.min.js +483 -438
- package/dist/types/core/cache/GlyphGeometryBuilder.d.ts +3 -0
- package/dist/types/core/geometry/Tessellator.d.ts +4 -0
- package/dist/types/core/types.d.ts +1 -0
- package/package.json +1 -1
- package/dist/types/core/layout/LineBreakDelta.d.ts +0 -97
- 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.4
|
|
3
3
|
* Copyright (C) 2025 Countertype LLC
|
|
4
4
|
*
|
|
5
5
|
* This program is free software: you can redistribute it and/or modify
|
|
@@ -557,6 +557,31 @@ class LineBreak {
|
|
|
557
557
|
currentIndex += token.length;
|
|
558
558
|
}
|
|
559
559
|
else {
|
|
560
|
+
if (lineWidth && token.includes('-') && !token.includes('\u00AD')) {
|
|
561
|
+
const tokenWidth = measureText(token);
|
|
562
|
+
if (tokenWidth > lineWidth) {
|
|
563
|
+
// Break long hyphenated tokens into characters (break-all behavior)
|
|
564
|
+
const chars = Array.from(token);
|
|
565
|
+
for (let i = 0; i < chars.length; i++) {
|
|
566
|
+
items.push({
|
|
567
|
+
type: ItemType.BOX,
|
|
568
|
+
width: measureText(chars[i]),
|
|
569
|
+
text: chars[i],
|
|
570
|
+
originIndex: tokenStartIndex + i
|
|
571
|
+
});
|
|
572
|
+
if (i < chars.length - 1) {
|
|
573
|
+
items.push({
|
|
574
|
+
type: ItemType.PENALTY,
|
|
575
|
+
width: 0,
|
|
576
|
+
penalty: 5000,
|
|
577
|
+
originIndex: tokenStartIndex + i + 1
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
currentIndex += token.length;
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
560
585
|
const segments = token.split(/(-)/);
|
|
561
586
|
let segmentIndex = tokenStartIndex;
|
|
562
587
|
for (const segment of segments) {
|
|
@@ -2502,6 +2527,12 @@ class Tessellator {
|
|
|
2502
2527
|
logger.log(`Tessellator: removeOverlaps=${removeOverlaps}, processing ${valid.length} paths`);
|
|
2503
2528
|
return this.tessellate(valid, removeOverlaps, isCFF, needsExtrusionContours);
|
|
2504
2529
|
}
|
|
2530
|
+
processContours(contours, removeOverlaps = true, isCFF = false, needsExtrusionContours = true) {
|
|
2531
|
+
if (contours.length === 0) {
|
|
2532
|
+
return { triangles: { vertices: [], indices: [] }, contours: [] };
|
|
2533
|
+
}
|
|
2534
|
+
return this.tessellateContours(contours, removeOverlaps, isCFF, needsExtrusionContours);
|
|
2535
|
+
}
|
|
2505
2536
|
tessellate(paths, removeOverlaps, isCFF, needsExtrusionContours) {
|
|
2506
2537
|
// libtess expects CCW winding; TTF outer contours are CW
|
|
2507
2538
|
const needsWindingReversal = !isCFF && !removeOverlaps;
|
|
@@ -2563,7 +2594,71 @@ class Tessellator {
|
|
|
2563
2594
|
vertices: triangleResult.vertices,
|
|
2564
2595
|
indices: triangleResult.indices || []
|
|
2565
2596
|
},
|
|
2566
|
-
contours: extrusionContours
|
|
2597
|
+
contours: extrusionContours,
|
|
2598
|
+
contoursAreBoundary: removeOverlaps
|
|
2599
|
+
};
|
|
2600
|
+
}
|
|
2601
|
+
tessellateContours(contours, removeOverlaps, isCFF, needsExtrusionContours) {
|
|
2602
|
+
const needsWindingReversal = !isCFF && !removeOverlaps;
|
|
2603
|
+
let originalContours;
|
|
2604
|
+
let tessContours;
|
|
2605
|
+
if (needsWindingReversal) {
|
|
2606
|
+
tessContours = this.reverseContours(contours);
|
|
2607
|
+
if (removeOverlaps || needsExtrusionContours) {
|
|
2608
|
+
originalContours = contours;
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
else {
|
|
2612
|
+
originalContours = contours;
|
|
2613
|
+
tessContours = contours;
|
|
2614
|
+
}
|
|
2615
|
+
let extrusionContours = needsExtrusionContours
|
|
2616
|
+
? needsWindingReversal
|
|
2617
|
+
? tessContours
|
|
2618
|
+
: (originalContours ?? contours)
|
|
2619
|
+
: [];
|
|
2620
|
+
if (removeOverlaps) {
|
|
2621
|
+
logger.log('Two-pass: boundary extraction then triangulation');
|
|
2622
|
+
perfLogger.start('Tessellator.boundaryPass', {
|
|
2623
|
+
contourCount: tessContours.length
|
|
2624
|
+
});
|
|
2625
|
+
const boundaryResult = this.performTessellation(originalContours, 'boundary');
|
|
2626
|
+
perfLogger.end('Tessellator.boundaryPass');
|
|
2627
|
+
if (!boundaryResult) {
|
|
2628
|
+
logger.warn('libtess returned empty result from boundary pass');
|
|
2629
|
+
return { triangles: { vertices: [], indices: [] }, contours: [] };
|
|
2630
|
+
}
|
|
2631
|
+
tessContours = this.boundaryToContours(boundaryResult);
|
|
2632
|
+
if (needsExtrusionContours) {
|
|
2633
|
+
extrusionContours = tessContours;
|
|
2634
|
+
}
|
|
2635
|
+
logger.log(`Boundary pass created ${tessContours.length} contours. Starting triangulation pass.`);
|
|
2636
|
+
}
|
|
2637
|
+
else {
|
|
2638
|
+
logger.log(`Single-pass triangulation for ${isCFF ? 'CFF' : 'TTF'}`);
|
|
2639
|
+
}
|
|
2640
|
+
perfLogger.start('Tessellator.triangulationPass', {
|
|
2641
|
+
contourCount: tessContours.length
|
|
2642
|
+
});
|
|
2643
|
+
const triangleResult = this.performTessellation(tessContours, 'triangles');
|
|
2644
|
+
perfLogger.end('Tessellator.triangulationPass');
|
|
2645
|
+
if (!triangleResult) {
|
|
2646
|
+
const warning = removeOverlaps
|
|
2647
|
+
? 'libtess returned empty result from triangulation pass'
|
|
2648
|
+
: 'libtess returned empty result from single-pass triangulation';
|
|
2649
|
+
logger.warn(warning);
|
|
2650
|
+
return {
|
|
2651
|
+
triangles: { vertices: [], indices: [] },
|
|
2652
|
+
contours: extrusionContours
|
|
2653
|
+
};
|
|
2654
|
+
}
|
|
2655
|
+
return {
|
|
2656
|
+
triangles: {
|
|
2657
|
+
vertices: triangleResult.vertices,
|
|
2658
|
+
indices: triangleResult.indices || []
|
|
2659
|
+
},
|
|
2660
|
+
contours: extrusionContours,
|
|
2661
|
+
contoursAreBoundary: removeOverlaps
|
|
2567
2662
|
};
|
|
2568
2663
|
}
|
|
2569
2664
|
pathsToContours(paths, reversePoints = false) {
|
|
@@ -2603,6 +2698,35 @@ class Tessellator {
|
|
|
2603
2698
|
}
|
|
2604
2699
|
return contours;
|
|
2605
2700
|
}
|
|
2701
|
+
reverseContours(contours) {
|
|
2702
|
+
const reversed = new Array(contours.length);
|
|
2703
|
+
for (let i = 0; i < contours.length; i++) {
|
|
2704
|
+
reversed[i] = this.reverseContour(contours[i]);
|
|
2705
|
+
}
|
|
2706
|
+
return reversed;
|
|
2707
|
+
}
|
|
2708
|
+
reverseContour(contour) {
|
|
2709
|
+
const len = contour.length;
|
|
2710
|
+
if (len === 0)
|
|
2711
|
+
return [];
|
|
2712
|
+
const isClosed = len >= 4 &&
|
|
2713
|
+
contour[0] === contour[len - 2] &&
|
|
2714
|
+
contour[1] === contour[len - 1];
|
|
2715
|
+
const end = isClosed ? len - 2 : len;
|
|
2716
|
+
if (end === 0)
|
|
2717
|
+
return [];
|
|
2718
|
+
const reversed = new Array(end + 2);
|
|
2719
|
+
let out = 0;
|
|
2720
|
+
for (let i = end - 2; i >= 0; i -= 2) {
|
|
2721
|
+
reversed[out++] = contour[i];
|
|
2722
|
+
reversed[out++] = contour[i + 1];
|
|
2723
|
+
}
|
|
2724
|
+
if (out >= 2) {
|
|
2725
|
+
reversed[out++] = reversed[0];
|
|
2726
|
+
reversed[out++] = reversed[1];
|
|
2727
|
+
}
|
|
2728
|
+
return reversed;
|
|
2729
|
+
}
|
|
2606
2730
|
performTessellation(contours, mode) {
|
|
2607
2731
|
const tess = new libtess_minExports.GluTesselator();
|
|
2608
2732
|
tess.gluTessProperty(libtess_minExports.gluEnum.GLU_TESS_WINDING_RULE, libtess_minExports.windingRule.GLU_TESS_WINDING_NONZERO);
|
|
@@ -2729,80 +2853,87 @@ class Extruder {
|
|
|
2729
2853
|
extrude(geometry, depth = 0, unitsPerEm) {
|
|
2730
2854
|
const points = geometry.triangles.vertices;
|
|
2731
2855
|
const triangleIndices = geometry.triangles.indices;
|
|
2732
|
-
const
|
|
2733
|
-
|
|
2856
|
+
const contours = geometry.contours;
|
|
2857
|
+
const contoursAreBoundary = geometry.contoursAreBoundary === true;
|
|
2858
|
+
const pointLen = points.length;
|
|
2859
|
+
const numPoints = pointLen / 2;
|
|
2860
|
+
// Use boundary contours for side walls when available
|
|
2734
2861
|
let boundaryEdges = [];
|
|
2862
|
+
let sideEdgeCount = 0;
|
|
2863
|
+
let useContours = false;
|
|
2735
2864
|
if (depth !== 0) {
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
const c = triangleIndices[i + 2];
|
|
2744
|
-
let key, v0, v1;
|
|
2745
|
-
if (a < b) {
|
|
2746
|
-
key = (a << 16) | b;
|
|
2747
|
-
v0 = a;
|
|
2748
|
-
v1 = b;
|
|
2749
|
-
}
|
|
2750
|
-
else {
|
|
2751
|
-
key = (b << 16) | a;
|
|
2752
|
-
v0 = a;
|
|
2753
|
-
v1 = b;
|
|
2754
|
-
}
|
|
2755
|
-
let data = edgeMap.get(key);
|
|
2756
|
-
if (data) {
|
|
2757
|
-
data[2]++;
|
|
2758
|
-
}
|
|
2759
|
-
else {
|
|
2760
|
-
edgeMap.set(key, [v0, v1, 1]);
|
|
2761
|
-
}
|
|
2762
|
-
if (b < c) {
|
|
2763
|
-
key = (b << 16) | c;
|
|
2764
|
-
v0 = b;
|
|
2765
|
-
v1 = c;
|
|
2766
|
-
}
|
|
2767
|
-
else {
|
|
2768
|
-
key = (c << 16) | b;
|
|
2769
|
-
v0 = b;
|
|
2770
|
-
v1 = c;
|
|
2771
|
-
}
|
|
2772
|
-
data = edgeMap.get(key);
|
|
2773
|
-
if (data) {
|
|
2774
|
-
data[2]++;
|
|
2775
|
-
}
|
|
2776
|
-
else {
|
|
2777
|
-
edgeMap.set(key, [v0, v1, 1]);
|
|
2778
|
-
}
|
|
2779
|
-
if (c < a) {
|
|
2780
|
-
key = (c << 16) | a;
|
|
2781
|
-
v0 = c;
|
|
2782
|
-
v1 = a;
|
|
2783
|
-
}
|
|
2784
|
-
else {
|
|
2785
|
-
key = (a << 16) | c;
|
|
2786
|
-
v0 = c;
|
|
2787
|
-
v1 = a;
|
|
2788
|
-
}
|
|
2789
|
-
data = edgeMap.get(key);
|
|
2790
|
-
if (data) {
|
|
2791
|
-
data[2]++;
|
|
2792
|
-
}
|
|
2793
|
-
else {
|
|
2794
|
-
edgeMap.set(key, [v0, v1, 1]);
|
|
2865
|
+
if (contoursAreBoundary && contours.length > 0) {
|
|
2866
|
+
useContours = true;
|
|
2867
|
+
for (const contour of contours) {
|
|
2868
|
+
const contourPointCount = contour.length >> 1;
|
|
2869
|
+
if (contourPointCount >= 2) {
|
|
2870
|
+
sideEdgeCount += contourPointCount - 1;
|
|
2871
|
+
}
|
|
2795
2872
|
}
|
|
2796
2873
|
}
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2874
|
+
else {
|
|
2875
|
+
// Use a directionless key (min/max) to detect shared edges
|
|
2876
|
+
// Store the directed edge (a->b) and mark as null when seen twice
|
|
2877
|
+
const edgeMap = new Map();
|
|
2878
|
+
const triLen = triangleIndices.length;
|
|
2879
|
+
for (let i = 0; i < triLen; i += 3) {
|
|
2880
|
+
const a = triangleIndices[i];
|
|
2881
|
+
const b = triangleIndices[i + 1];
|
|
2882
|
+
const c = triangleIndices[i + 2];
|
|
2883
|
+
let key, packed;
|
|
2884
|
+
if (a < b) {
|
|
2885
|
+
key = (a << 16) | b;
|
|
2886
|
+
}
|
|
2887
|
+
else {
|
|
2888
|
+
key = (b << 16) | a;
|
|
2889
|
+
}
|
|
2890
|
+
packed = (a << 16) | b;
|
|
2891
|
+
let 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
|
+
if (b < c) {
|
|
2899
|
+
key = (b << 16) | c;
|
|
2900
|
+
}
|
|
2901
|
+
else {
|
|
2902
|
+
key = (c << 16) | b;
|
|
2903
|
+
}
|
|
2904
|
+
packed = (b << 16) | c;
|
|
2905
|
+
data = edgeMap.get(key);
|
|
2906
|
+
if (data === undefined) {
|
|
2907
|
+
edgeMap.set(key, packed);
|
|
2908
|
+
}
|
|
2909
|
+
else if (data !== null) {
|
|
2910
|
+
edgeMap.set(key, null);
|
|
2911
|
+
}
|
|
2912
|
+
if (c < a) {
|
|
2913
|
+
key = (c << 16) | a;
|
|
2914
|
+
}
|
|
2915
|
+
else {
|
|
2916
|
+
key = (a << 16) | c;
|
|
2917
|
+
}
|
|
2918
|
+
packed = (c << 16) | a;
|
|
2919
|
+
data = edgeMap.get(key);
|
|
2920
|
+
if (data === undefined) {
|
|
2921
|
+
edgeMap.set(key, packed);
|
|
2922
|
+
}
|
|
2923
|
+
else if (data !== null) {
|
|
2924
|
+
edgeMap.set(key, null);
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
boundaryEdges = [];
|
|
2928
|
+
for (const packedEdge of edgeMap.values()) {
|
|
2929
|
+
if (packedEdge === null)
|
|
2930
|
+
continue;
|
|
2931
|
+
boundaryEdges.push(packedEdge >>> 16, packedEdge & 0xffff);
|
|
2801
2932
|
}
|
|
2933
|
+
sideEdgeCount = boundaryEdges.length >> 1;
|
|
2802
2934
|
}
|
|
2803
2935
|
}
|
|
2804
|
-
const
|
|
2805
|
-
const sideVertexCount = depth === 0 ? 0 : sideEdgeCount * 4;
|
|
2936
|
+
const sideVertexCount = sideEdgeCount * 4;
|
|
2806
2937
|
const baseVertexCount = depth === 0 ? numPoints : numPoints * 2;
|
|
2807
2938
|
const vertexCount = baseVertexCount + sideVertexCount;
|
|
2808
2939
|
const vertices = new Float32Array(vertexCount * 3);
|
|
@@ -2812,26 +2943,24 @@ class Extruder {
|
|
|
2812
2943
|
: triangleIndices.length * 2 + sideEdgeCount * 6;
|
|
2813
2944
|
const indices = new Uint32Array(indexCount);
|
|
2814
2945
|
if (depth === 0) {
|
|
2815
|
-
let vPos = 0;
|
|
2816
|
-
|
|
2817
|
-
vertices[vPos] = points[
|
|
2818
|
-
vertices[vPos + 1] = points[i + 1];
|
|
2946
|
+
for (let p = 0, vPos = 0; p < pointLen; p += 2, vPos += 3) {
|
|
2947
|
+
vertices[vPos] = points[p];
|
|
2948
|
+
vertices[vPos + 1] = points[p + 1];
|
|
2819
2949
|
vertices[vPos + 2] = 0;
|
|
2820
2950
|
normals[vPos] = 0;
|
|
2821
2951
|
normals[vPos + 1] = 0;
|
|
2822
2952
|
normals[vPos + 2] = 1;
|
|
2823
|
-
vPos += 3;
|
|
2824
2953
|
}
|
|
2825
2954
|
indices.set(triangleIndices);
|
|
2826
2955
|
return { vertices, normals, indices };
|
|
2827
2956
|
}
|
|
2828
2957
|
const minBackOffset = unitsPerEm * 0.000025;
|
|
2829
2958
|
const backZ = depth <= minBackOffset ? minBackOffset : depth;
|
|
2830
|
-
|
|
2959
|
+
const backOffset = numPoints * 3;
|
|
2960
|
+
for (let p = 0, vi = 0, base0 = 0; p < pointLen; p += 2, vi++, base0 += 3) {
|
|
2831
2961
|
const x = points[p];
|
|
2832
2962
|
const y = points[p + 1];
|
|
2833
2963
|
// Cap at z=0
|
|
2834
|
-
const base0 = vi * 3;
|
|
2835
2964
|
vertices[base0] = x;
|
|
2836
2965
|
vertices[base0 + 1] = y;
|
|
2837
2966
|
vertices[base0 + 2] = 0;
|
|
@@ -2839,7 +2968,7 @@ class Extruder {
|
|
|
2839
2968
|
normals[base0 + 1] = 0;
|
|
2840
2969
|
normals[base0 + 2] = -1;
|
|
2841
2970
|
// Cap at z=depth
|
|
2842
|
-
const baseD =
|
|
2971
|
+
const baseD = base0 + backOffset;
|
|
2843
2972
|
vertices[baseD] = x;
|
|
2844
2973
|
vertices[baseD + 1] = y;
|
|
2845
2974
|
vertices[baseD + 2] = backZ;
|
|
@@ -2858,61 +2987,119 @@ class Extruder {
|
|
|
2858
2987
|
}
|
|
2859
2988
|
let nextVertex = numPoints * 2;
|
|
2860
2989
|
let idxPos = triLen * 2;
|
|
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
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2990
|
+
if (useContours) {
|
|
2991
|
+
for (const contour of contours) {
|
|
2992
|
+
const contourLen = contour.length;
|
|
2993
|
+
if (contourLen < 4)
|
|
2994
|
+
continue;
|
|
2995
|
+
for (let i = 0; i < contourLen - 2; i += 2) {
|
|
2996
|
+
const p0x = contour[i];
|
|
2997
|
+
const p0y = contour[i + 1];
|
|
2998
|
+
const p1x = contour[i + 2];
|
|
2999
|
+
const p1y = contour[i + 3];
|
|
3000
|
+
const ex = p1x - p0x;
|
|
3001
|
+
const ey = p1y - p0y;
|
|
3002
|
+
const lenSq = ex * ex + ey * ey;
|
|
3003
|
+
let nx = 0;
|
|
3004
|
+
let ny = 0;
|
|
3005
|
+
if (lenSq > 1e-10) {
|
|
3006
|
+
const invLen = 1 / Math.sqrt(lenSq);
|
|
3007
|
+
nx = ey * invLen;
|
|
3008
|
+
ny = -ex * invLen;
|
|
3009
|
+
}
|
|
3010
|
+
const base = nextVertex * 3;
|
|
3011
|
+
vertices[base] = p0x;
|
|
3012
|
+
vertices[base + 1] = p0y;
|
|
3013
|
+
vertices[base + 2] = 0;
|
|
3014
|
+
vertices[base + 3] = p1x;
|
|
3015
|
+
vertices[base + 4] = p1y;
|
|
3016
|
+
vertices[base + 5] = 0;
|
|
3017
|
+
vertices[base + 6] = p0x;
|
|
3018
|
+
vertices[base + 7] = p0y;
|
|
3019
|
+
vertices[base + 8] = backZ;
|
|
3020
|
+
vertices[base + 9] = p1x;
|
|
3021
|
+
vertices[base + 10] = p1y;
|
|
3022
|
+
vertices[base + 11] = backZ;
|
|
3023
|
+
normals[base] = nx;
|
|
3024
|
+
normals[base + 1] = ny;
|
|
3025
|
+
normals[base + 2] = 0;
|
|
3026
|
+
normals[base + 3] = nx;
|
|
3027
|
+
normals[base + 4] = ny;
|
|
3028
|
+
normals[base + 5] = 0;
|
|
3029
|
+
normals[base + 6] = nx;
|
|
3030
|
+
normals[base + 7] = ny;
|
|
3031
|
+
normals[base + 8] = 0;
|
|
3032
|
+
normals[base + 9] = nx;
|
|
3033
|
+
normals[base + 10] = ny;
|
|
3034
|
+
normals[base + 11] = 0;
|
|
3035
|
+
const baseVertex = nextVertex;
|
|
3036
|
+
indices[idxPos] = baseVertex;
|
|
3037
|
+
indices[idxPos + 1] = baseVertex + 1;
|
|
3038
|
+
indices[idxPos + 2] = baseVertex + 2;
|
|
3039
|
+
indices[idxPos + 3] = baseVertex + 1;
|
|
3040
|
+
indices[idxPos + 4] = baseVertex + 3;
|
|
3041
|
+
indices[idxPos + 5] = baseVertex + 2;
|
|
3042
|
+
idxPos += 6;
|
|
3043
|
+
nextVertex += 4;
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
else {
|
|
3048
|
+
for (let e = 0; e < sideEdgeCount; e++) {
|
|
3049
|
+
const edgeIndex = e << 1;
|
|
3050
|
+
const u = boundaryEdges[edgeIndex];
|
|
3051
|
+
const v = boundaryEdges[edgeIndex + 1];
|
|
3052
|
+
const u2 = u << 1;
|
|
3053
|
+
const v2 = v << 1;
|
|
3054
|
+
const p0x = points[u2];
|
|
3055
|
+
const p0y = points[u2 + 1];
|
|
3056
|
+
const p1x = points[v2];
|
|
3057
|
+
const p1y = points[v2 + 1];
|
|
3058
|
+
const ex = p1x - p0x;
|
|
3059
|
+
const ey = p1y - p0y;
|
|
3060
|
+
const lenSq = ex * ex + ey * ey;
|
|
3061
|
+
let nx = 0;
|
|
3062
|
+
let ny = 0;
|
|
3063
|
+
if (lenSq > 1e-10) {
|
|
3064
|
+
const invLen = 1 / Math.sqrt(lenSq);
|
|
3065
|
+
nx = ey * invLen;
|
|
3066
|
+
ny = -ex * invLen;
|
|
3067
|
+
}
|
|
3068
|
+
const base = nextVertex * 3;
|
|
3069
|
+
vertices[base] = p0x;
|
|
3070
|
+
vertices[base + 1] = p0y;
|
|
3071
|
+
vertices[base + 2] = 0;
|
|
3072
|
+
vertices[base + 3] = p1x;
|
|
3073
|
+
vertices[base + 4] = p1y;
|
|
3074
|
+
vertices[base + 5] = 0;
|
|
3075
|
+
vertices[base + 6] = p0x;
|
|
3076
|
+
vertices[base + 7] = p0y;
|
|
3077
|
+
vertices[base + 8] = backZ;
|
|
3078
|
+
vertices[base + 9] = p1x;
|
|
3079
|
+
vertices[base + 10] = p1y;
|
|
3080
|
+
vertices[base + 11] = backZ;
|
|
3081
|
+
normals[base] = nx;
|
|
3082
|
+
normals[base + 1] = ny;
|
|
3083
|
+
normals[base + 2] = 0;
|
|
3084
|
+
normals[base + 3] = nx;
|
|
3085
|
+
normals[base + 4] = ny;
|
|
3086
|
+
normals[base + 5] = 0;
|
|
3087
|
+
normals[base + 6] = nx;
|
|
3088
|
+
normals[base + 7] = ny;
|
|
3089
|
+
normals[base + 8] = 0;
|
|
3090
|
+
normals[base + 9] = nx;
|
|
3091
|
+
normals[base + 10] = ny;
|
|
3092
|
+
normals[base + 11] = 0;
|
|
3093
|
+
const baseVertex = nextVertex;
|
|
3094
|
+
indices[idxPos] = baseVertex;
|
|
3095
|
+
indices[idxPos + 1] = baseVertex + 1;
|
|
3096
|
+
indices[idxPos + 2] = baseVertex + 2;
|
|
3097
|
+
indices[idxPos + 3] = baseVertex + 1;
|
|
3098
|
+
indices[idxPos + 4] = baseVertex + 3;
|
|
3099
|
+
indices[idxPos + 5] = baseVertex + 2;
|
|
3100
|
+
idxPos += 6;
|
|
3101
|
+
nextVertex += 4;
|
|
3102
|
+
}
|
|
2916
3103
|
}
|
|
2917
3104
|
return { vertices, normals, indices };
|
|
2918
3105
|
}
|
|
@@ -3899,6 +4086,9 @@ class GlyphGeometryBuilder {
|
|
|
3899
4086
|
this.fontId = 'default';
|
|
3900
4087
|
this.cacheKeyPrefix = 'default';
|
|
3901
4088
|
this.emptyGlyphs = new Set();
|
|
4089
|
+
this.clusterPositions = [];
|
|
4090
|
+
this.clusterContoursScratch = [];
|
|
4091
|
+
this.taskScratch = [];
|
|
3902
4092
|
this.cache = cache;
|
|
3903
4093
|
this.loadedFont = loadedFont;
|
|
3904
4094
|
this.tessellator = new Tessellator();
|
|
@@ -3984,14 +4174,28 @@ class GlyphGeometryBuilder {
|
|
|
3984
4174
|
else {
|
|
3985
4175
|
perfLogger.start('GlyphGeometryBuilder.buildInstancedGeometry');
|
|
3986
4176
|
}
|
|
3987
|
-
const tasks =
|
|
4177
|
+
const tasks = this.taskScratch;
|
|
4178
|
+
tasks.length = 0;
|
|
4179
|
+
let taskCount = 0;
|
|
3988
4180
|
let totalVertexFloats = 0;
|
|
3989
4181
|
let totalNormalFloats = 0;
|
|
3990
4182
|
let totalIndexCount = 0;
|
|
3991
4183
|
let vertexCursor = 0; // vertex offset (not float offset)
|
|
3992
4184
|
const pushTask = (data, px, py, pz) => {
|
|
3993
4185
|
const vertexStart = vertexCursor;
|
|
3994
|
-
|
|
4186
|
+
let task = tasks[taskCount];
|
|
4187
|
+
if (task) {
|
|
4188
|
+
task.data = data;
|
|
4189
|
+
task.px = px;
|
|
4190
|
+
task.py = py;
|
|
4191
|
+
task.pz = pz;
|
|
4192
|
+
task.vertexStart = vertexStart;
|
|
4193
|
+
}
|
|
4194
|
+
else {
|
|
4195
|
+
task = { data, px, py, pz, vertexStart };
|
|
4196
|
+
tasks[taskCount] = task;
|
|
4197
|
+
}
|
|
4198
|
+
taskCount++;
|
|
3995
4199
|
totalVertexFloats += data.vertices.length;
|
|
3996
4200
|
totalNormalFloats += data.normals.length;
|
|
3997
4201
|
totalIndexCount += data.indices.length;
|
|
@@ -4041,8 +4245,21 @@ class GlyphGeometryBuilder {
|
|
|
4041
4245
|
boundaryGroups = cached.groups;
|
|
4042
4246
|
}
|
|
4043
4247
|
else {
|
|
4044
|
-
const
|
|
4045
|
-
|
|
4248
|
+
const glyphCount = cluster.glyphs.length;
|
|
4249
|
+
if (this.clusterPositions.length < glyphCount) {
|
|
4250
|
+
for (let i = this.clusterPositions.length; i < glyphCount; i++) {
|
|
4251
|
+
this.clusterPositions.push(new Vec3(0, 0, 0));
|
|
4252
|
+
}
|
|
4253
|
+
}
|
|
4254
|
+
this.clusterPositions.length = glyphCount;
|
|
4255
|
+
for (let i = 0; i < glyphCount; i++) {
|
|
4256
|
+
const glyph = cluster.glyphs[i];
|
|
4257
|
+
const pos = this.clusterPositions[i];
|
|
4258
|
+
pos.x = glyph.x ?? 0;
|
|
4259
|
+
pos.y = glyph.y ?? 0;
|
|
4260
|
+
pos.z = 0;
|
|
4261
|
+
}
|
|
4262
|
+
boundaryGroups = this.clusterer.cluster(clusterGlyphContours, this.clusterPositions);
|
|
4046
4263
|
this.clusteringCache.set(cacheKey, {
|
|
4047
4264
|
glyphIds: cluster.glyphs.map((g) => g.g),
|
|
4048
4265
|
positions: cluster.glyphs.map((g) => ({
|
|
@@ -4099,7 +4316,8 @@ class GlyphGeometryBuilder {
|
|
|
4099
4316
|
const clusterKey = this.getClusterKey(subClusterGlyphs, depth, removeOverlaps);
|
|
4100
4317
|
let cachedCluster = this.wordCache.get(clusterKey);
|
|
4101
4318
|
if (!cachedCluster) {
|
|
4102
|
-
const
|
|
4319
|
+
const clusterContours = this.clusterContoursScratch;
|
|
4320
|
+
let contourIndex = 0;
|
|
4103
4321
|
const refX = subClusterGlyphs[0].x ?? 0;
|
|
4104
4322
|
const refY = subClusterGlyphs[0].y ?? 0;
|
|
4105
4323
|
for (let i = 0; i < groupIndices.length; i++) {
|
|
@@ -4109,13 +4327,38 @@ class GlyphGeometryBuilder {
|
|
|
4109
4327
|
const relX = (glyph.x ?? 0) - refX;
|
|
4110
4328
|
const relY = (glyph.y ?? 0) - refY;
|
|
4111
4329
|
for (const path of glyphContours.paths) {
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4330
|
+
const points = path.points;
|
|
4331
|
+
const pointCount = points.length;
|
|
4332
|
+
if (pointCount < 3)
|
|
4333
|
+
continue;
|
|
4334
|
+
const isClosed = pointCount > 1 &&
|
|
4335
|
+
points[0].x === points[pointCount - 1].x &&
|
|
4336
|
+
points[0].y === points[pointCount - 1].y;
|
|
4337
|
+
const end = isClosed ? pointCount - 1 : pointCount;
|
|
4338
|
+
const needed = (end + 1) * 2;
|
|
4339
|
+
let contour = clusterContours[contourIndex];
|
|
4340
|
+
if (!contour || contour.length < needed) {
|
|
4341
|
+
contour = new Array(needed);
|
|
4342
|
+
clusterContours[contourIndex] = contour;
|
|
4343
|
+
}
|
|
4344
|
+
else {
|
|
4345
|
+
contour.length = needed;
|
|
4346
|
+
}
|
|
4347
|
+
let out = 0;
|
|
4348
|
+
for (let k = 0; k < end; k++) {
|
|
4349
|
+
const pt = points[k];
|
|
4350
|
+
contour[out++] = pt.x + relX;
|
|
4351
|
+
contour[out++] = pt.y + relY;
|
|
4352
|
+
}
|
|
4353
|
+
if (out >= 2) {
|
|
4354
|
+
contour[out++] = contour[0];
|
|
4355
|
+
contour[out++] = contour[1];
|
|
4356
|
+
}
|
|
4357
|
+
contourIndex++;
|
|
4116
4358
|
}
|
|
4117
4359
|
}
|
|
4118
|
-
|
|
4360
|
+
clusterContours.length = contourIndex;
|
|
4361
|
+
cachedCluster = this.tessellateGlyphCluster(clusterContours, depth, isCFF);
|
|
4119
4362
|
this.wordCache.set(clusterKey, cachedCluster);
|
|
4120
4363
|
}
|
|
4121
4364
|
// Calculate the absolute position of this sub-cluster based on its first glyph
|
|
@@ -4170,6 +4413,7 @@ class GlyphGeometryBuilder {
|
|
|
4170
4413
|
}
|
|
4171
4414
|
}
|
|
4172
4415
|
}
|
|
4416
|
+
tasks.length = taskCount;
|
|
4173
4417
|
// Allocate exact-sized buffers and fill once
|
|
4174
4418
|
const vertexArray = new Float32Array(totalVertexFloats);
|
|
4175
4419
|
const normalArray = new Float32Array(totalNormalFloats);
|
|
@@ -4185,17 +4429,27 @@ class GlyphGeometryBuilder {
|
|
|
4185
4429
|
const px = task.px;
|
|
4186
4430
|
const py = task.py;
|
|
4187
4431
|
const pz = task.pz;
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4432
|
+
const offsetX = px * scale;
|
|
4433
|
+
const offsetY = py * scale;
|
|
4434
|
+
const offsetZ = pz * scale;
|
|
4435
|
+
const vLen = v.length;
|
|
4436
|
+
let outPos = vertexPos;
|
|
4437
|
+
for (let j = 0; j < vLen; j += 3) {
|
|
4438
|
+
vertexArray[outPos] = v[j] * scale + offsetX;
|
|
4439
|
+
vertexArray[outPos + 1] = v[j + 1] * scale + offsetY;
|
|
4440
|
+
vertexArray[outPos + 2] = v[j + 2] * scale + offsetZ;
|
|
4441
|
+
outPos += 3;
|
|
4442
|
+
}
|
|
4443
|
+
vertexPos = outPos;
|
|
4193
4444
|
normalArray.set(n, normalPos);
|
|
4194
4445
|
normalPos += n.length;
|
|
4195
4446
|
const vertexStart = task.vertexStart;
|
|
4196
|
-
|
|
4197
|
-
|
|
4447
|
+
const idxLen = idx.length;
|
|
4448
|
+
let outIndexPos = indexPos;
|
|
4449
|
+
for (let j = 0; j < idxLen; j++) {
|
|
4450
|
+
indexArray[outIndexPos++] = idx[j] + vertexStart;
|
|
4198
4451
|
}
|
|
4452
|
+
indexPos = outIndexPos;
|
|
4199
4453
|
}
|
|
4200
4454
|
perfLogger.end('GlyphGeometryBuilder.buildInstancedGeometry');
|
|
4201
4455
|
planeBounds.min.x *= scale;
|
|
@@ -4293,8 +4547,8 @@ class GlyphGeometryBuilder {
|
|
|
4293
4547
|
this.contourCache.set(key, contours);
|
|
4294
4548
|
return contours;
|
|
4295
4549
|
}
|
|
4296
|
-
tessellateGlyphCluster(
|
|
4297
|
-
const processedGeometry = this.tessellator.
|
|
4550
|
+
tessellateGlyphCluster(contours, depth, isCFF) {
|
|
4551
|
+
const processedGeometry = this.tessellator.processContours(contours, true, isCFF, depth !== 0);
|
|
4298
4552
|
return this.extrudeAndPackage(processedGeometry, depth);
|
|
4299
4553
|
}
|
|
4300
4554
|
extrudeAndPackage(processedGeometry, depth) {
|