three-text 0.2.17 → 0.2.18

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.umd.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * three-text v0.2.17
2
+ * three-text v0.2.18
3
3
  * Copyright (C) 2025 Countertype LLC
4
4
  *
5
5
  * This program is free software: you can redistribute it and/or modify
@@ -2731,7 +2731,9 @@
2731
2731
  tessContours = originalContours;
2732
2732
  }
2733
2733
  let extrusionContours = needsExtrusionContours
2734
- ? originalContours ?? this.pathsToContours(paths)
2734
+ ? needsWindingReversal
2735
+ ? tessContours
2736
+ : originalContours ?? this.pathsToContours(paths)
2735
2737
  : [];
2736
2738
  if (removeOverlaps) {
2737
2739
  logger.log('Two-pass: boundary extraction then triangulation');
@@ -2753,24 +2755,6 @@
2753
2755
  }
2754
2756
  else {
2755
2757
  logger.log(`Single-pass triangulation for ${isCFF ? 'CFF' : 'TTF'}`);
2756
- // TTF contours may have inconsistent winding; check if we need normalization
2757
- if (needsExtrusionContours && !isCFF) {
2758
- const needsNormalization = this.needsWindingNormalization(extrusionContours);
2759
- if (needsNormalization) {
2760
- logger.log('Complex topology detected, running boundary pass for winding normalization');
2761
- perfLogger.start('Tessellator.windingNormalization', {
2762
- contourCount: extrusionContours.length
2763
- });
2764
- const boundaryResult = this.performTessellation(extrusionContours, 'boundary');
2765
- perfLogger.end('Tessellator.windingNormalization');
2766
- if (boundaryResult) {
2767
- extrusionContours = this.boundaryToContours(boundaryResult);
2768
- }
2769
- }
2770
- else {
2771
- logger.log('Simple topology, skipping winding normalization');
2772
- }
2773
- }
2774
2758
  }
2775
2759
  perfLogger.start('Tessellator.triangulationPass', {
2776
2760
  contourCount: tessContours.length
@@ -2952,28 +2936,58 @@
2952
2936
 
2953
2937
  class Extruder {
2954
2938
  constructor() { }
2939
+ packEdge(a, b) {
2940
+ const lo = a < b ? a : b;
2941
+ const hi = a < b ? b : a;
2942
+ return lo * 0x100000000 + hi;
2943
+ }
2955
2944
  extrude(geometry, depth = 0, unitsPerEm) {
2956
2945
  const points = geometry.triangles.vertices;
2957
2946
  const triangleIndices = geometry.triangles.indices;
2958
2947
  const numPoints = points.length / 2;
2959
- // Count side-wall segments (4 vertices + 6 indices per segment)
2960
- let sideSegments = 0;
2948
+ // Count boundary edges for side walls (4 vertices + 6 indices per edge)
2949
+ let boundaryEdges = [];
2961
2950
  if (depth !== 0) {
2962
- for (const contour of geometry.contours) {
2963
- // Contours are closed (last point repeats first)
2964
- const contourPoints = contour.length / 2;
2965
- if (contourPoints >= 2)
2966
- sideSegments += contourPoints - 1;
2951
+ const counts = new Map();
2952
+ const oriented = new Map();
2953
+ for (let i = 0; i < triangleIndices.length; i += 3) {
2954
+ const a = triangleIndices[i];
2955
+ const b = triangleIndices[i + 1];
2956
+ const c = triangleIndices[i + 2];
2957
+ const k0 = this.packEdge(a, b);
2958
+ const n0 = (counts.get(k0) ?? 0) + 1;
2959
+ counts.set(k0, n0);
2960
+ if (n0 === 1)
2961
+ oriented.set(k0, [a, b]);
2962
+ const k1 = this.packEdge(b, c);
2963
+ const n1 = (counts.get(k1) ?? 0) + 1;
2964
+ counts.set(k1, n1);
2965
+ if (n1 === 1)
2966
+ oriented.set(k1, [b, c]);
2967
+ const k2 = this.packEdge(c, a);
2968
+ const n2 = (counts.get(k2) ?? 0) + 1;
2969
+ counts.set(k2, n2);
2970
+ if (n2 === 1)
2971
+ oriented.set(k2, [c, a]);
2972
+ }
2973
+ boundaryEdges = [];
2974
+ for (const [key, count] of counts) {
2975
+ if (count !== 1)
2976
+ continue;
2977
+ const edge = oriented.get(key);
2978
+ if (edge)
2979
+ boundaryEdges.push(edge);
2967
2980
  }
2968
2981
  }
2969
- const sideVertexCount = depth === 0 ? 0 : sideSegments * 4;
2982
+ const sideEdgeCount = depth === 0 ? 0 : boundaryEdges.length;
2983
+ const sideVertexCount = depth === 0 ? 0 : sideEdgeCount * 4;
2970
2984
  const baseVertexCount = depth === 0 ? numPoints : numPoints * 2;
2971
2985
  const vertexCount = baseVertexCount + sideVertexCount;
2972
2986
  const vertices = new Float32Array(vertexCount * 3);
2973
2987
  const normals = new Float32Array(vertexCount * 3);
2974
2988
  const indexCount = depth === 0
2975
2989
  ? triangleIndices.length
2976
- : triangleIndices.length * 2 + sideSegments * 6;
2990
+ : triangleIndices.length * 2 + sideEdgeCount * 6;
2977
2991
  const indices = new Uint32Array(indexCount);
2978
2992
  if (depth === 0) {
2979
2993
  // Single-sided flat geometry at z=0
@@ -3029,60 +3043,62 @@
3029
3043
  // Side walls
3030
3044
  let nextVertex = numPoints * 2;
3031
3045
  let idxPos = triangleIndices.length * 2;
3032
- for (const contour of geometry.contours) {
3033
- for (let i = 0; i < contour.length - 2; i += 2) {
3034
- const p0x = contour[i];
3035
- const p0y = contour[i + 1];
3036
- const p1x = contour[i + 2];
3037
- const p1y = contour[i + 3];
3038
- // Perpendicular normal for this wall segment
3039
- const ex = p1x - p0x;
3040
- const ey = p1y - p0y;
3041
- const lenSq = ex * ex + ey * ey;
3042
- let nx = 0;
3043
- let ny = 0;
3044
- if (lenSq > 0) {
3045
- const invLen = 1 / Math.sqrt(lenSq);
3046
- nx = ey * invLen;
3047
- ny = -ex * invLen;
3048
- }
3049
- const baseVertex = nextVertex;
3050
- const base = baseVertex * 3;
3051
- // Wall quad: front edge at z=0, back edge at z=depth
3052
- vertices[base] = p0x;
3053
- vertices[base + 1] = p0y;
3054
- vertices[base + 2] = 0;
3055
- vertices[base + 3] = p1x;
3056
- vertices[base + 4] = p1y;
3057
- vertices[base + 5] = 0;
3058
- vertices[base + 6] = p0x;
3059
- vertices[base + 7] = p0y;
3060
- vertices[base + 8] = backZ;
3061
- vertices[base + 9] = p1x;
3062
- vertices[base + 10] = p1y;
3063
- vertices[base + 11] = backZ;
3064
- // Wall normals point perpendicular to edge
3065
- normals[base] = nx;
3066
- normals[base + 1] = ny;
3067
- normals[base + 2] = 0;
3068
- normals[base + 3] = nx;
3069
- normals[base + 4] = ny;
3070
- normals[base + 5] = 0;
3071
- normals[base + 6] = nx;
3072
- normals[base + 7] = ny;
3073
- normals[base + 8] = 0;
3074
- normals[base + 9] = nx;
3075
- normals[base + 10] = ny;
3076
- normals[base + 11] = 0;
3077
- // Two triangles per wall segment
3078
- indices[idxPos++] = baseVertex;
3079
- indices[idxPos++] = baseVertex + 1;
3080
- indices[idxPos++] = baseVertex + 2;
3081
- indices[idxPos++] = baseVertex + 1;
3082
- indices[idxPos++] = baseVertex + 3;
3083
- indices[idxPos++] = baseVertex + 2;
3084
- nextVertex += 4;
3085
- }
3046
+ for (let e = 0; e < boundaryEdges.length; e++) {
3047
+ const [u, v] = boundaryEdges[e];
3048
+ const u2 = u * 2;
3049
+ const v2 = v * 2;
3050
+ const p0x = points[u2];
3051
+ const p0y = points[u2 + 1];
3052
+ const p1x = points[v2];
3053
+ const p1y = points[v2 + 1];
3054
+ // Perpendicular normal for this wall segment
3055
+ // Uses the edge direction from the cap triangulation so winding does not depend on contour direction
3056
+ const ex = p1x - p0x;
3057
+ const ey = p1y - p0y;
3058
+ const lenSq = ex * ex + ey * ey;
3059
+ let nx = 0;
3060
+ let ny = 0;
3061
+ if (lenSq > 0) {
3062
+ const invLen = 1 / Math.sqrt(lenSq);
3063
+ nx = ey * invLen;
3064
+ ny = -ex * invLen;
3065
+ }
3066
+ const baseVertex = nextVertex;
3067
+ const base = baseVertex * 3;
3068
+ // Wall quad: front edge at z=0, back edge at z=depth
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
+ // Wall normals point perpendicular to edge
3082
+ normals[base] = nx;
3083
+ normals[base + 1] = ny;
3084
+ normals[base + 2] = 0;
3085
+ normals[base + 3] = nx;
3086
+ normals[base + 4] = ny;
3087
+ normals[base + 5] = 0;
3088
+ normals[base + 6] = nx;
3089
+ normals[base + 7] = ny;
3090
+ normals[base + 8] = 0;
3091
+ normals[base + 9] = nx;
3092
+ normals[base + 10] = ny;
3093
+ normals[base + 11] = 0;
3094
+ // Two triangles per wall segment
3095
+ indices[idxPos++] = baseVertex;
3096
+ indices[idxPos++] = baseVertex + 1;
3097
+ indices[idxPos++] = baseVertex + 2;
3098
+ indices[idxPos++] = baseVertex + 1;
3099
+ indices[idxPos++] = baseVertex + 3;
3100
+ indices[idxPos++] = baseVertex + 2;
3101
+ nextVertex += 4;
3086
3102
  }
3087
3103
  return { vertices, normals, indices };
3088
3104
  }
@@ -4181,7 +4197,7 @@
4181
4197
  // Use glyph-level caching when separateGlyphs is set or when cluster contains colored text
4182
4198
  const forceSeparate = separateGlyphs || clusterHasColoredGlyphs;
4183
4199
  // Iterate over the geometric groups identified by BoundaryClusterer
4184
- // logical groups (words) split into geometric sub-groups (e.g. "aa", "XX", "bb")
4200
+ // logical groups (words) split into geometric sub-groups
4185
4201
  for (const groupIndices of boundaryGroups) {
4186
4202
  const isOverlappingGroup = groupIndices.length > 1;
4187
4203
  const shouldCluster = isOverlappingGroup && !forceSeparate;