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 CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * three-text v0.4.2
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 numPoints = points.length / 2;
2733
- // Boundary edges are those that appear in exactly one triangle
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
- // Pack edge pair into integer key: (min << 16) | max
2737
- // Fits glyph vertex indices comfortably, good hash distribution
2738
- const edgeMap = new Map();
2739
- const triLen = triangleIndices.length;
2740
- for (let i = 0; i < triLen; i += 3) {
2741
- const a = triangleIndices[i];
2742
- const b = triangleIndices[i + 1];
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
- boundaryEdges = [];
2798
- for (const [v0, v1, count] of edgeMap.values()) {
2799
- if (count === 1) {
2800
- boundaryEdges.push([v0, v1]);
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 sideEdgeCount = depth === 0 ? 0 : boundaryEdges.length;
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
- for (let i = 0; i < points.length; i += 2) {
2817
- vertices[vPos] = points[i];
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
- for (let p = 0, vi = 0; p < points.length; p += 2, vi++) {
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 = (numPoints + vi) * 3;
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
- const numEdges = boundaryEdges.length;
2862
- for (let e = 0; e < numEdges; e++) {
2863
- const edge = boundaryEdges[e];
2864
- const u = edge[0];
2865
- const v = edge[1];
2866
- const u2 = u << 1;
2867
- const v2 = v << 1;
2868
- const p0x = points[u2];
2869
- const p0y = points[u2 + 1];
2870
- const p1x = points[v2];
2871
- const p1y = points[v2 + 1];
2872
- const ex = p1x - p0x;
2873
- const ey = p1y - p0y;
2874
- const lenSq = ex * ex + ey * ey;
2875
- let nx = 0;
2876
- let ny = 0;
2877
- if (lenSq > 1e-10) {
2878
- const invLen = 1 / Math.sqrt(lenSq);
2879
- nx = ey * invLen;
2880
- ny = -ex * invLen;
2881
- }
2882
- const base = nextVertex * 3;
2883
- vertices[base] = p0x;
2884
- vertices[base + 1] = p0y;
2885
- vertices[base + 2] = 0;
2886
- vertices[base + 3] = p1x;
2887
- vertices[base + 4] = p1y;
2888
- vertices[base + 5] = 0;
2889
- vertices[base + 6] = p0x;
2890
- vertices[base + 7] = p0y;
2891
- vertices[base + 8] = backZ;
2892
- vertices[base + 9] = p1x;
2893
- vertices[base + 10] = p1y;
2894
- vertices[base + 11] = backZ;
2895
- normals[base] = nx;
2896
- normals[base + 1] = ny;
2897
- normals[base + 2] = 0;
2898
- normals[base + 3] = nx;
2899
- normals[base + 4] = ny;
2900
- normals[base + 5] = 0;
2901
- normals[base + 6] = nx;
2902
- normals[base + 7] = ny;
2903
- normals[base + 8] = 0;
2904
- normals[base + 9] = nx;
2905
- normals[base + 10] = ny;
2906
- normals[base + 11] = 0;
2907
- const baseVertex = nextVertex;
2908
- indices[idxPos] = baseVertex;
2909
- indices[idxPos + 1] = baseVertex + 1;
2910
- indices[idxPos + 2] = baseVertex + 2;
2911
- indices[idxPos + 3] = baseVertex + 1;
2912
- indices[idxPos + 4] = baseVertex + 3;
2913
- indices[idxPos + 5] = baseVertex + 2;
2914
- idxPos += 6;
2915
- nextVertex += 4;
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
- tasks.push({ data, px, py, pz, vertexStart });
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 relativePositions = cluster.glyphs.map((g) => new Vec3(g.x ?? 0, g.y ?? 0, 0));
4045
- boundaryGroups = this.clusterer.cluster(clusterGlyphContours, relativePositions);
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 clusterPaths = [];
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
- clusterPaths.push({
4113
- ...path,
4114
- points: path.points.map((p) => new Vec2(p.x + relX, p.y + relY))
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
- cachedCluster = this.tessellateGlyphCluster(clusterPaths, depth, isCFF);
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
- for (let j = 0; j < v.length; j += 3) {
4189
- vertexArray[vertexPos++] = (v[j] + px) * scale;
4190
- vertexArray[vertexPos++] = (v[j + 1] + py) * scale;
4191
- vertexArray[vertexPos++] = (v[j + 2] + pz) * scale;
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
- for (let j = 0; j < idx.length; j++) {
4197
- indexArray[indexPos++] = idx[j] + vertexStart;
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(paths, depth, isCFF) {
4297
- const processedGeometry = this.tessellator.process(paths, true, isCFF, depth !== 0);
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) {