three-text 0.2.17 → 0.2.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![TypeScript](https://img.shields.io/badge/built%20with-TypeScript-007acc.svg)](https://www.typescriptlang.org/)
5
5
  [![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3_or_later-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)
6
6
 
7
- A high fidelity 3D font renderer and text layout engine for the web
7
+ High fidelity 3D mesh font geometry and text layout engine for the web
8
8
 
9
9
  ![Screenshot of three-text example file](https://countertype.com/assets/three-text/3D.png)
10
10
 
@@ -15,11 +15,7 @@ A high fidelity 3D font renderer and text layout engine for the web
15
15
  > [!CAUTION]
16
16
  > three-text is an alpha release and the API may break rapidly. This warning will last at least through the end of 2025. If API stability is important to you, consider pinning your version. Community feedback is encouraged; please open an issue if you have any suggestions or feedback, thank you
17
17
 
18
-
19
- text: `three-text is a 3D font geometry and text layout library for the web. Its supports TTF, OTF, and WOFF font files. For layout, it uses Tex-based parameters for breaking text into paragraphs across multiple lines and supports CJK and RTL scripts. three-text caches the geometries it generates for low CPU overhead in languages with lots of repeating glyphs. Variable fonts are supported as static instances at a given axis coordinate, and can be animated by re-drawing each frame with new coordinates. The library has a framework-agnostic core that returns raw vertex data, with lightweight adapters for Three.js, React Three Fiber, p5.js, WebGL and WebGPU. Under the hood, three-text relies on HarfBuzz for text shaping, Knuth-Plass line breaking, Liang hyphenation, libtess by Eric Veach for tessellation, curve polygonization from Maxim Shemanarev's Anti-Grain Geometry, and Visvalingam-Whyatt line simplification`,
20
-
21
-
22
- **three-text** is a 3D font geometry and text layout library for the web. It supports TTF, OTF, and WOFF font files. For layout, it uses [TeX](https://en.wikipedia.org/wiki/TeX)-based parameters for breaking text into paragraphs across multiple lines and supports CJK and RTL scripts. three-text caches the geometries it generates for low CPU overhead in languages with lots of repeating glyphs. Variable fonts are supported as static instances at a given axis coordinate, and can be animated by re-drawing each frame with new coordinates
18
+ **three-text** is a 3D mesh font geometry and text layout library for the web. It supports TTF, OTF, and WOFF font files. For layout, it uses [TeX](https://en.wikipedia.org/wiki/TeX)-based parameters for breaking text into paragraphs across multiple lines and supports CJK and RTL scripts. three-text caches the geometries it generates for low CPU overhead in languages with lots of repeating glyphs. Variable fonts are supported as static instances at a given axis coordinate, and can be animated by re-drawing each frame with new coordinates
23
19
 
24
20
  The library has a framework-agnostic core that returns raw vertex data, with lightweight adapters for [Three.js](https://threejs.org), [React Three Fiber](https://docs.pmnd.rs/react-three-fiber), [p5.js](https://p5js.org), [WebGL](https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API), and [WebGPU](https://developer.mozilla.org/en-US/docs/Web/API/WebGPU_API)
25
21
 
@@ -556,7 +552,7 @@ const text = await Text.create({
556
552
 
557
553
  Values can be boolean (`true`/`false`) to enable or disable, or numeric for features accepting variant indices. Explicitly disabling a feature overrides the font's defaults
558
554
 
559
- Common tags include [`liga`](https://learn.microsoft.com/en-us/typography/opentype/spec/features_ko#liga) (ligatures), [`kern`](https://learn.microsoft.com/en-us/typography/opentype/spec/features_ko#kern) (kerning), [`calt`](https://learn.microsoft.com/en-us/typography/opentype/spec/features_ae#calt) (contextual alternates), and [`smcp`](https://learn.microsoft.com/en-us/typography/opentype/spec/features_pt#smcp) (small capitals). Number styling uses [`lnum`](https://learn.microsoft.com/en-us/typography/opentype/spec/features_ko#lnum)/[`onum`](https://learn.microsoft.com/en-us/typography/opentype/spec/features_ko#onum)/[`tnum`](https://learn.microsoft.com/en-us/typography/opentype/spec/features_pt#tnum). Stylistic alternates are [`ss01`-`ss20`](https://learn.microsoft.com/en-us/typography/opentype/spec/features_pt#ss01--ss20) and [`cv01`-`cv99`](https://learn.microsoft.com/en-us/typography/opentype/spec/features_ae#cv01--cv99). Feature availability depends on the font
555
+ Common tags include [`liga`](https://learn.microsoft.com/en-us/typography/opentype/spec/features_ko#liga) (ligatures), [`calt`](https://learn.microsoft.com/en-us/typography/opentype/spec/features_ae#calt) (contextual alternates), [`tnum`](https://learn.microsoft.com/en-us/typography/opentype/spec/features_pt#tnum) (tabular numbers), sylistic alternates [`ss01`-`ss20`](https://learn.microsoft.com/en-us/typography/opentype/spec/features_pt#ss01--ss20) and character variants [`cv01`-`cv99`](https://learn.microsoft.com/en-us/typography/opentype/spec/features_ae#cv01--cv99). Feature availability depends on the font
560
556
 
561
557
  ### Per-glyph attributes
562
558
 
@@ -736,6 +732,10 @@ Initializes HarfBuzz WebAssembly. Called automatically by `create()`, but can be
736
732
 
737
733
  Preloads hyphenation patterns for specified languages. Useful for avoiding async pattern loading during text rendering
738
734
 
735
+ ##### `Text.setMaxFontCacheMemoryMB(limitMB: number): void`
736
+
737
+ Sets an upper bound for the font cache, measured by the raw font buffer size, eviction is FIFO by insertion order
738
+
739
739
  #### Instance Methods
740
740
 
741
741
  The following methods are available on instances created by `Text.create()`:
@@ -753,7 +753,7 @@ Below are the most important configuration interfaces. For a complete list of al
753
753
  ```typescript
754
754
  interface TextOptions {
755
755
  text: string; // Text content to render
756
- font?: string | ArrayBuffer; // Font file path or buffer (TTF, OTF, or WOFF)
756
+ font: string | ArrayBuffer; // Font file path or buffer (TTF, OTF, or WOFF)
757
757
  size?: number; // Font size in scene units (default: 72)
758
758
  depth?: number; // Extrusion depth (default: 0)
759
759
  lineHeight?: number; // Line height multiplier (default: 1.0)
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * three-text v0.2.17
2
+ * three-text v0.2.19
3
3
  * Copyright (C) 2025 Countertype LLC
4
4
  *
5
5
  * This program is free software: you can redistribute it and/or modify
@@ -81,7 +81,9 @@ class PerformanceLogger {
81
81
  // Find the metric in reverse order (most recent first)
82
82
  for (let i = this.metrics.length - 1; i >= 0; i--) {
83
83
  const metric = this.metrics[i];
84
- if (metric.name === name && metric.startTime === startTime && !metric.endTime) {
84
+ if (metric.name === name &&
85
+ metric.startTime === startTime &&
86
+ !metric.endTime) {
85
87
  metric.endTime = endTime;
86
88
  metric.duration = duration;
87
89
  break;
@@ -469,7 +471,9 @@ class LineBreak {
469
471
  const char = chars[i];
470
472
  const nextChar = i < chars.length - 1 ? chars[i + 1] : null;
471
473
  if (/\s/.test(char)) {
472
- const width = widths ? (widths[i] ?? measureText(char)) : measureText(char);
474
+ const width = widths
475
+ ? (widths[i] ?? measureText(char))
476
+ : measureText(char);
473
477
  items.push({
474
478
  type: ItemType.GLUE,
475
479
  width,
@@ -842,7 +846,9 @@ class LineBreak {
842
846
  if (breaks.length === 0) {
843
847
  // For first emergency attempt, use initialEmergencyStretch
844
848
  // For subsequent iterations (short line detection), progressively increase
845
- currentEmergencyStretch = initialEmergencyStretch + (iteration * width * SHORT_LINE_EMERGENCY_STRETCH_INCREMENT);
849
+ currentEmergencyStretch =
850
+ initialEmergencyStretch +
851
+ iteration * width * SHORT_LINE_EMERGENCY_STRETCH_INCREMENT;
846
852
  breaks = LineBreak.findBreakpoints(currentItems, width, tolerance, looseness, true, currentEmergencyStretch, context);
847
853
  }
848
854
  // Last resort: allow higher badness (but not infinite)
@@ -1723,12 +1729,12 @@ class FontMetadataExtractor {
1723
1729
  try {
1724
1730
  if (gsubTableOffset) {
1725
1731
  const gsubData = this.extractFeatureDataFromTable(view, gsubTableOffset, nameTableOffset);
1726
- gsubData.features.forEach(f => features.add(f));
1732
+ gsubData.features.forEach((f) => features.add(f));
1727
1733
  Object.assign(featureNames, gsubData.names);
1728
1734
  }
1729
1735
  if (gposTableOffset) {
1730
1736
  const gposData = this.extractFeatureDataFromTable(view, gposTableOffset, nameTableOffset);
1731
- gposData.features.forEach(f => features.add(f));
1737
+ gposData.features.forEach((f) => features.add(f));
1732
1738
  Object.assign(featureNames, gposData.names);
1733
1739
  }
1734
1740
  }
@@ -2727,7 +2733,9 @@ class Tessellator {
2727
2733
  tessContours = originalContours;
2728
2734
  }
2729
2735
  let extrusionContours = needsExtrusionContours
2730
- ? originalContours ?? this.pathsToContours(paths)
2736
+ ? needsWindingReversal
2737
+ ? tessContours
2738
+ : (originalContours ?? this.pathsToContours(paths))
2731
2739
  : [];
2732
2740
  if (removeOverlaps) {
2733
2741
  logger.log('Two-pass: boundary extraction then triangulation');
@@ -2749,24 +2757,6 @@ class Tessellator {
2749
2757
  }
2750
2758
  else {
2751
2759
  logger.log(`Single-pass triangulation for ${isCFF ? 'CFF' : 'TTF'}`);
2752
- // TTF contours may have inconsistent winding; check if we need normalization
2753
- if (needsExtrusionContours && !isCFF) {
2754
- const needsNormalization = this.needsWindingNormalization(extrusionContours);
2755
- if (needsNormalization) {
2756
- logger.log('Complex topology detected, running boundary pass for winding normalization');
2757
- perfLogger.start('Tessellator.windingNormalization', {
2758
- contourCount: extrusionContours.length
2759
- });
2760
- const boundaryResult = this.performTessellation(extrusionContours, 'boundary');
2761
- perfLogger.end('Tessellator.windingNormalization');
2762
- if (boundaryResult) {
2763
- extrusionContours = this.boundaryToContours(boundaryResult);
2764
- }
2765
- }
2766
- else {
2767
- logger.log('Simple topology, skipping winding normalization');
2768
- }
2769
- }
2770
2760
  }
2771
2761
  perfLogger.start('Tessellator.triangulationPass', {
2772
2762
  contourCount: tessContours.length
@@ -2778,7 +2768,10 @@ class Tessellator {
2778
2768
  ? 'libtess returned empty result from triangulation pass'
2779
2769
  : 'libtess returned empty result from single-pass triangulation';
2780
2770
  logger.warn(warning);
2781
- return { triangles: { vertices: [], indices: [] }, contours: extrusionContours };
2771
+ return {
2772
+ triangles: { vertices: [], indices: [] },
2773
+ contours: extrusionContours
2774
+ };
2782
2775
  }
2783
2776
  return {
2784
2777
  triangles: {
@@ -2948,28 +2941,58 @@ class Tessellator {
2948
2941
 
2949
2942
  class Extruder {
2950
2943
  constructor() { }
2944
+ packEdge(a, b) {
2945
+ const lo = a < b ? a : b;
2946
+ const hi = a < b ? b : a;
2947
+ return lo * 0x100000000 + hi;
2948
+ }
2951
2949
  extrude(geometry, depth = 0, unitsPerEm) {
2952
2950
  const points = geometry.triangles.vertices;
2953
2951
  const triangleIndices = geometry.triangles.indices;
2954
2952
  const numPoints = points.length / 2;
2955
- // Count side-wall segments (4 vertices + 6 indices per segment)
2956
- let sideSegments = 0;
2953
+ // Count boundary edges for side walls (4 vertices + 6 indices per edge)
2954
+ let boundaryEdges = [];
2957
2955
  if (depth !== 0) {
2958
- for (const contour of geometry.contours) {
2959
- // Contours are closed (last point repeats first)
2960
- const contourPoints = contour.length / 2;
2961
- if (contourPoints >= 2)
2962
- sideSegments += contourPoints - 1;
2956
+ const counts = new Map();
2957
+ const oriented = new Map();
2958
+ for (let i = 0; i < triangleIndices.length; i += 3) {
2959
+ const a = triangleIndices[i];
2960
+ const b = triangleIndices[i + 1];
2961
+ const c = triangleIndices[i + 2];
2962
+ const k0 = this.packEdge(a, b);
2963
+ const n0 = (counts.get(k0) ?? 0) + 1;
2964
+ counts.set(k0, n0);
2965
+ if (n0 === 1)
2966
+ oriented.set(k0, [a, b]);
2967
+ const k1 = this.packEdge(b, c);
2968
+ const n1 = (counts.get(k1) ?? 0) + 1;
2969
+ counts.set(k1, n1);
2970
+ if (n1 === 1)
2971
+ oriented.set(k1, [b, c]);
2972
+ const k2 = this.packEdge(c, a);
2973
+ const n2 = (counts.get(k2) ?? 0) + 1;
2974
+ counts.set(k2, n2);
2975
+ if (n2 === 1)
2976
+ oriented.set(k2, [c, a]);
2977
+ }
2978
+ boundaryEdges = [];
2979
+ for (const [key, count] of counts) {
2980
+ if (count !== 1)
2981
+ continue;
2982
+ const edge = oriented.get(key);
2983
+ if (edge)
2984
+ boundaryEdges.push(edge);
2963
2985
  }
2964
2986
  }
2965
- const sideVertexCount = depth === 0 ? 0 : sideSegments * 4;
2987
+ const sideEdgeCount = depth === 0 ? 0 : boundaryEdges.length;
2988
+ const sideVertexCount = depth === 0 ? 0 : sideEdgeCount * 4;
2966
2989
  const baseVertexCount = depth === 0 ? numPoints : numPoints * 2;
2967
2990
  const vertexCount = baseVertexCount + sideVertexCount;
2968
2991
  const vertices = new Float32Array(vertexCount * 3);
2969
2992
  const normals = new Float32Array(vertexCount * 3);
2970
2993
  const indexCount = depth === 0
2971
2994
  ? triangleIndices.length
2972
- : triangleIndices.length * 2 + sideSegments * 6;
2995
+ : triangleIndices.length * 2 + sideEdgeCount * 6;
2973
2996
  const indices = new Uint32Array(indexCount);
2974
2997
  if (depth === 0) {
2975
2998
  // Single-sided flat geometry at z=0
@@ -3025,60 +3048,62 @@ class Extruder {
3025
3048
  // Side walls
3026
3049
  let nextVertex = numPoints * 2;
3027
3050
  let idxPos = triangleIndices.length * 2;
3028
- for (const contour of geometry.contours) {
3029
- for (let i = 0; i < contour.length - 2; i += 2) {
3030
- const p0x = contour[i];
3031
- const p0y = contour[i + 1];
3032
- const p1x = contour[i + 2];
3033
- const p1y = contour[i + 3];
3034
- // Perpendicular normal for this wall segment
3035
- const ex = p1x - p0x;
3036
- const ey = p1y - p0y;
3037
- const lenSq = ex * ex + ey * ey;
3038
- let nx = 0;
3039
- let ny = 0;
3040
- if (lenSq > 0) {
3041
- const invLen = 1 / Math.sqrt(lenSq);
3042
- nx = ey * invLen;
3043
- ny = -ex * invLen;
3044
- }
3045
- const baseVertex = nextVertex;
3046
- const base = baseVertex * 3;
3047
- // Wall quad: front edge at z=0, back edge at z=depth
3048
- vertices[base] = p0x;
3049
- vertices[base + 1] = p0y;
3050
- vertices[base + 2] = 0;
3051
- vertices[base + 3] = p1x;
3052
- vertices[base + 4] = p1y;
3053
- vertices[base + 5] = 0;
3054
- vertices[base + 6] = p0x;
3055
- vertices[base + 7] = p0y;
3056
- vertices[base + 8] = backZ;
3057
- vertices[base + 9] = p1x;
3058
- vertices[base + 10] = p1y;
3059
- vertices[base + 11] = backZ;
3060
- // Wall normals point perpendicular to edge
3061
- normals[base] = nx;
3062
- normals[base + 1] = ny;
3063
- normals[base + 2] = 0;
3064
- normals[base + 3] = nx;
3065
- normals[base + 4] = ny;
3066
- normals[base + 5] = 0;
3067
- normals[base + 6] = nx;
3068
- normals[base + 7] = ny;
3069
- normals[base + 8] = 0;
3070
- normals[base + 9] = nx;
3071
- normals[base + 10] = ny;
3072
- normals[base + 11] = 0;
3073
- // Two triangles per wall segment
3074
- indices[idxPos++] = baseVertex;
3075
- indices[idxPos++] = baseVertex + 1;
3076
- indices[idxPos++] = baseVertex + 2;
3077
- indices[idxPos++] = baseVertex + 1;
3078
- indices[idxPos++] = baseVertex + 3;
3079
- indices[idxPos++] = baseVertex + 2;
3080
- nextVertex += 4;
3081
- }
3051
+ for (let e = 0; e < boundaryEdges.length; e++) {
3052
+ const [u, v] = boundaryEdges[e];
3053
+ const u2 = u * 2;
3054
+ const v2 = v * 2;
3055
+ const p0x = points[u2];
3056
+ const p0y = points[u2 + 1];
3057
+ const p1x = points[v2];
3058
+ const p1y = points[v2 + 1];
3059
+ // Perpendicular normal for this wall segment
3060
+ // Uses the edge direction from the cap triangulation so winding does not depend on contour direction
3061
+ const ex = p1x - p0x;
3062
+ const ey = p1y - p0y;
3063
+ const lenSq = ex * ex + ey * ey;
3064
+ let nx = 0;
3065
+ let ny = 0;
3066
+ if (lenSq > 0) {
3067
+ const invLen = 1 / Math.sqrt(lenSq);
3068
+ nx = ey * invLen;
3069
+ ny = -ex * invLen;
3070
+ }
3071
+ const baseVertex = nextVertex;
3072
+ const base = baseVertex * 3;
3073
+ // Wall quad: front edge at z=0, back edge at z=depth
3074
+ vertices[base] = p0x;
3075
+ vertices[base + 1] = p0y;
3076
+ vertices[base + 2] = 0;
3077
+ vertices[base + 3] = p1x;
3078
+ vertices[base + 4] = p1y;
3079
+ vertices[base + 5] = 0;
3080
+ vertices[base + 6] = p0x;
3081
+ vertices[base + 7] = p0y;
3082
+ vertices[base + 8] = backZ;
3083
+ vertices[base + 9] = p1x;
3084
+ vertices[base + 10] = p1y;
3085
+ vertices[base + 11] = backZ;
3086
+ // Wall normals point perpendicular to edge
3087
+ normals[base] = nx;
3088
+ normals[base + 1] = ny;
3089
+ normals[base + 2] = 0;
3090
+ normals[base + 3] = nx;
3091
+ normals[base + 4] = ny;
3092
+ normals[base + 5] = 0;
3093
+ normals[base + 6] = nx;
3094
+ normals[base + 7] = ny;
3095
+ normals[base + 8] = 0;
3096
+ normals[base + 9] = nx;
3097
+ normals[base + 10] = ny;
3098
+ normals[base + 11] = 0;
3099
+ // Two triangles per wall segment
3100
+ indices[idxPos++] = baseVertex;
3101
+ indices[idxPos++] = baseVertex + 1;
3102
+ indices[idxPos++] = baseVertex + 2;
3103
+ indices[idxPos++] = baseVertex + 1;
3104
+ indices[idxPos++] = baseVertex + 3;
3105
+ indices[idxPos++] = baseVertex + 2;
3106
+ nextVertex += 4;
3082
3107
  }
3083
3108
  return { vertices, normals, indices };
3084
3109
  }
@@ -3403,9 +3428,7 @@ class PathOptimizer {
3403
3428
  const v1LenSq = v1x * v1x + v1y * v1y;
3404
3429
  const v2LenSq = v2x * v2x + v2y * v2y;
3405
3430
  const minLenSq = this.config.minSegmentLength * this.config.minSegmentLength;
3406
- if (angle > threshold ||
3407
- v1LenSq < minLenSq ||
3408
- v2LenSq < minLenSq) {
3431
+ if (angle > threshold || v1LenSq < minLenSq || v2LenSq < minLenSq) {
3409
3432
  result.push(current);
3410
3433
  }
3411
3434
  else {
@@ -4167,7 +4190,7 @@ class GlyphGeometryBuilder {
4167
4190
  const relativePositions = cluster.glyphs.map((g) => new Vec3(g.x ?? 0, g.y ?? 0, 0));
4168
4191
  boundaryGroups = this.clusterer.cluster(clusterGlyphContours, relativePositions);
4169
4192
  this.clusteringCache.set(cacheKey, {
4170
- glyphIds: cluster.glyphs.map(g => g.g),
4193
+ glyphIds: cluster.glyphs.map((g) => g.g),
4171
4194
  groups: boundaryGroups
4172
4195
  });
4173
4196
  }
@@ -4177,7 +4200,7 @@ class GlyphGeometryBuilder {
4177
4200
  // Use glyph-level caching when separateGlyphs is set or when cluster contains colored text
4178
4201
  const forceSeparate = separateGlyphs || clusterHasColoredGlyphs;
4179
4202
  // Iterate over the geometric groups identified by BoundaryClusterer
4180
- // logical groups (words) split into geometric sub-groups (e.g. "aa", "XX", "bb")
4203
+ // logical groups (words) split into geometric sub-groups
4181
4204
  for (const groupIndices of boundaryGroups) {
4182
4205
  const isOverlappingGroup = groupIndices.length > 1;
4183
4206
  const shouldCluster = isOverlappingGroup && !forceSeparate;
@@ -4557,7 +4580,8 @@ class TextShaper {
4557
4580
  if (LineBreak.isCJOpeningPunctuation(currentChar)) {
4558
4581
  shouldApply = false;
4559
4582
  }
4560
- if (LineBreak.isCJPunctuation(currentChar) && LineBreak.isCJPunctuation(nextChar)) {
4583
+ if (LineBreak.isCJPunctuation(currentChar) &&
4584
+ LineBreak.isCJPunctuation(nextChar)) {
4561
4585
  shouldApply = false;
4562
4586
  }
4563
4587
  if (shouldApply) {
@@ -5361,7 +5385,7 @@ class Text {
5361
5385
  // Stringify with sorted keys for cache stability
5362
5386
  static stableStringify(obj) {
5363
5387
  const keys = Object.keys(obj).sort();
5364
- const pairs = keys.map(k => `${k}:${obj[k]}`);
5388
+ const pairs = keys.map((k) => `${k}:${obj[k]}`);
5365
5389
  return pairs.join(',');
5366
5390
  }
5367
5391
  constructor() {
@@ -5599,7 +5623,9 @@ class Text {
5599
5623
  // to selectively use glyph-level caching (separate vertices) only for clusters containing
5600
5624
  // colored text, while non-colored clusters can still use fast cluster-level merging
5601
5625
  let coloredTextIndices;
5602
- if (options.color && typeof options.color === 'object' && !Array.isArray(options.color)) {
5626
+ if (options.color &&
5627
+ typeof options.color === 'object' &&
5628
+ !Array.isArray(options.color)) {
5603
5629
  if (options.color.byText || options.color.byCharRange) {
5604
5630
  // Build the set manually since glyphs don't exist yet
5605
5631
  coloredTextIndices = new Set();
@@ -5704,7 +5730,9 @@ class Text {
5704
5730
  const depthScale = this.loadedFont.upem / size;
5705
5731
  const rawDepthInFontUnits = depth * depthScale;
5706
5732
  const minExtrudeDepth = this.loadedFont.upem * 0.000025;
5707
- const depthInFontUnits = rawDepthInFontUnits <= 0 ? 0 : Math.max(rawDepthInFontUnits, minExtrudeDepth);
5733
+ const depthInFontUnits = rawDepthInFontUnits <= 0
5734
+ ? 0
5735
+ : Math.max(rawDepthInFontUnits, minExtrudeDepth);
5708
5736
  if (!this.textLayout) {
5709
5737
  this.textLayout = new TextLayout(this.loadedFont);
5710
5738
  }
@@ -5876,10 +5904,12 @@ class Text {
5876
5904
  planeBounds.max.z *= finalScale;
5877
5905
  for (let i = 0; i < glyphInfoArray.length; i++) {
5878
5906
  const glyphInfo = glyphInfoArray[i];
5879
- glyphInfo.bounds.min.x = glyphInfo.bounds.min.x * finalScale + offsetScaled;
5907
+ glyphInfo.bounds.min.x =
5908
+ glyphInfo.bounds.min.x * finalScale + offsetScaled;
5880
5909
  glyphInfo.bounds.min.y *= finalScale;
5881
5910
  glyphInfo.bounds.min.z *= finalScale;
5882
- glyphInfo.bounds.max.x = glyphInfo.bounds.max.x * finalScale + offsetScaled;
5911
+ glyphInfo.bounds.max.x =
5912
+ glyphInfo.bounds.max.x * finalScale + offsetScaled;
5883
5913
  glyphInfo.bounds.max.y *= finalScale;
5884
5914
  glyphInfo.bounds.max.z *= finalScale;
5885
5915
  }
@@ -5942,13 +5972,11 @@ class Text {
5942
5972
  static registerPattern(language, pattern) {
5943
5973
  Text.patternCache.set(language, pattern);
5944
5974
  }
5945
- static clearFontCache() {
5946
- Text.fontCache.clear();
5947
- Text.fontCacheMemoryBytes = 0;
5948
- }
5949
5975
  static setMaxFontCacheMemoryMB(limitMB) {
5950
5976
  Text.maxFontCacheMemoryBytes =
5951
- limitMB === Infinity ? Infinity : Math.max(1, Math.floor(limitMB)) * 1024 * 1024;
5977
+ limitMB === Infinity
5978
+ ? Infinity
5979
+ : Math.max(1, Math.floor(limitMB)) * 1024 * 1024;
5952
5980
  Text.enforceFontCacheMemoryLimit();
5953
5981
  }
5954
5982
  getLoadedFont() {
package/dist/index.d.ts CHANGED
@@ -5,6 +5,45 @@ interface HyphenationTrieNode {
5
5
  };
6
6
  }
7
7
 
8
+ interface LRUCacheOptions<K, V> {
9
+ maxEntries?: number;
10
+ maxMemoryBytes?: number;
11
+ calculateSize?: (value: V) => number;
12
+ onEvict?: (key: K, value: V) => void;
13
+ }
14
+ interface CacheStats {
15
+ hits: number;
16
+ misses: number;
17
+ evictions: number;
18
+ size: number;
19
+ memoryUsage: number;
20
+ }
21
+ declare class LRUCache<K, V> {
22
+ private cache;
23
+ private head;
24
+ private tail;
25
+ private stats;
26
+ private options;
27
+ constructor(options?: LRUCacheOptions<K, V>);
28
+ get(key: K): V | undefined;
29
+ has(key: K): boolean;
30
+ set(key: K, value: V): void;
31
+ delete(key: K): boolean;
32
+ clear(): void;
33
+ getStats(): CacheStats & {
34
+ hitRate: number;
35
+ memoryUsageMB: number;
36
+ };
37
+ keys(): K[];
38
+ get size(): number;
39
+ private evictIfNeeded;
40
+ private evictTail;
41
+ private addToHead;
42
+ private removeNode;
43
+ private removeTail;
44
+ private moveToHead;
45
+ }
46
+
8
47
  interface BoundingBox {
9
48
  min: {
10
49
  x: number;
@@ -221,13 +260,19 @@ interface TextGeometryInfo {
221
260
  pointsRemovedByVisvalingam: number;
222
261
  pointsRemovedByColinear: number;
223
262
  originalPointCount: number;
224
- };
263
+ } & Partial<CacheStats & {
264
+ hitRate: number;
265
+ memoryUsageMB: number;
266
+ }>;
225
267
  query(options: TextQueryOptions): TextRange[];
226
268
  coloredRanges?: ColoredRange[];
227
269
  }
228
270
  interface TextHandle extends TextGeometryInfo {
229
271
  getLoadedFont(): LoadedFont | undefined;
230
- getCacheStatistics(): any;
272
+ getCacheStatistics(): (CacheStats & {
273
+ hitRate: number;
274
+ memoryUsageMB: number;
275
+ }) | null;
231
276
  clearCache(): void;
232
277
  measureTextWidth(text: string, letterSpacing?: number): number;
233
278
  update(options: Partial<TextOptions>): Promise<TextHandle>;
@@ -266,7 +311,7 @@ interface ColoredRange {
266
311
  }
267
312
  interface TextOptions {
268
313
  text: string;
269
- font?: string | ArrayBuffer;
314
+ font: string | ArrayBuffer;
270
315
  size?: number;
271
316
  depth?: number;
272
317
  lineHeight?: number;
@@ -367,7 +412,6 @@ declare class Text {
367
412
  getFontMetrics(): FontMetrics;
368
413
  static preloadPatterns(languages: string[], patternsPath?: string): Promise<void>;
369
414
  static registerPattern(language: string, pattern: HyphenationTrieNode): void;
370
- static clearFontCache(): void;
371
415
  static setMaxFontCacheMemoryMB(limitMB: number): void;
372
416
  getLoadedFont(): LoadedFont | undefined;
373
417
  measureTextWidth(text: string, letterSpacing?: number): number;
@@ -436,45 +480,6 @@ declare class FontMetadataExtractor {
436
480
  static getFontMetrics(metrics: ExtractedMetrics): FontMetrics;
437
481
  }
438
482
 
439
- interface LRUCacheOptions<K, V> {
440
- maxEntries?: number;
441
- maxMemoryBytes?: number;
442
- calculateSize?: (value: V) => number;
443
- onEvict?: (key: K, value: V) => void;
444
- }
445
- interface CacheStats {
446
- hits: number;
447
- misses: number;
448
- evictions: number;
449
- size: number;
450
- memoryUsage: number;
451
- }
452
- declare class LRUCache<K, V> {
453
- private cache;
454
- private head;
455
- private tail;
456
- private stats;
457
- private options;
458
- constructor(options?: LRUCacheOptions<K, V>);
459
- get(key: K): V | undefined;
460
- has(key: K): boolean;
461
- set(key: K, value: V): void;
462
- delete(key: K): boolean;
463
- clear(): void;
464
- getStats(): CacheStats & {
465
- hitRate: number;
466
- memoryUsageMB: number;
467
- };
468
- keys(): K[];
469
- get size(): number;
470
- private evictIfNeeded;
471
- private evictTail;
472
- private addToHead;
473
- private removeNode;
474
- private removeTail;
475
- private moveToHead;
476
- }
477
-
478
483
  declare const globalGlyphCache: LRUCache<string, GlyphData>;
479
484
  declare function createGlyphCache(maxCacheSizeMB?: number): LRUCache<string, GlyphData>;
480
485