three-text 0.2.18 → 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.18
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
  }
@@ -2729,7 +2735,7 @@ class Tessellator {
2729
2735
  let extrusionContours = needsExtrusionContours
2730
2736
  ? needsWindingReversal
2731
2737
  ? tessContours
2732
- : originalContours ?? this.pathsToContours(paths)
2738
+ : (originalContours ?? this.pathsToContours(paths))
2733
2739
  : [];
2734
2740
  if (removeOverlaps) {
2735
2741
  logger.log('Two-pass: boundary extraction then triangulation');
@@ -2762,7 +2768,10 @@ class Tessellator {
2762
2768
  ? 'libtess returned empty result from triangulation pass'
2763
2769
  : 'libtess returned empty result from single-pass triangulation';
2764
2770
  logger.warn(warning);
2765
- return { triangles: { vertices: [], indices: [] }, contours: extrusionContours };
2771
+ return {
2772
+ triangles: { vertices: [], indices: [] },
2773
+ contours: extrusionContours
2774
+ };
2766
2775
  }
2767
2776
  return {
2768
2777
  triangles: {
@@ -3419,9 +3428,7 @@ class PathOptimizer {
3419
3428
  const v1LenSq = v1x * v1x + v1y * v1y;
3420
3429
  const v2LenSq = v2x * v2x + v2y * v2y;
3421
3430
  const minLenSq = this.config.minSegmentLength * this.config.minSegmentLength;
3422
- if (angle > threshold ||
3423
- v1LenSq < minLenSq ||
3424
- v2LenSq < minLenSq) {
3431
+ if (angle > threshold || v1LenSq < minLenSq || v2LenSq < minLenSq) {
3425
3432
  result.push(current);
3426
3433
  }
3427
3434
  else {
@@ -4183,7 +4190,7 @@ class GlyphGeometryBuilder {
4183
4190
  const relativePositions = cluster.glyphs.map((g) => new Vec3(g.x ?? 0, g.y ?? 0, 0));
4184
4191
  boundaryGroups = this.clusterer.cluster(clusterGlyphContours, relativePositions);
4185
4192
  this.clusteringCache.set(cacheKey, {
4186
- glyphIds: cluster.glyphs.map(g => g.g),
4193
+ glyphIds: cluster.glyphs.map((g) => g.g),
4187
4194
  groups: boundaryGroups
4188
4195
  });
4189
4196
  }
@@ -4573,7 +4580,8 @@ class TextShaper {
4573
4580
  if (LineBreak.isCJOpeningPunctuation(currentChar)) {
4574
4581
  shouldApply = false;
4575
4582
  }
4576
- if (LineBreak.isCJPunctuation(currentChar) && LineBreak.isCJPunctuation(nextChar)) {
4583
+ if (LineBreak.isCJPunctuation(currentChar) &&
4584
+ LineBreak.isCJPunctuation(nextChar)) {
4577
4585
  shouldApply = false;
4578
4586
  }
4579
4587
  if (shouldApply) {
@@ -5377,7 +5385,7 @@ class Text {
5377
5385
  // Stringify with sorted keys for cache stability
5378
5386
  static stableStringify(obj) {
5379
5387
  const keys = Object.keys(obj).sort();
5380
- const pairs = keys.map(k => `${k}:${obj[k]}`);
5388
+ const pairs = keys.map((k) => `${k}:${obj[k]}`);
5381
5389
  return pairs.join(',');
5382
5390
  }
5383
5391
  constructor() {
@@ -5615,7 +5623,9 @@ class Text {
5615
5623
  // to selectively use glyph-level caching (separate vertices) only for clusters containing
5616
5624
  // colored text, while non-colored clusters can still use fast cluster-level merging
5617
5625
  let coloredTextIndices;
5618
- 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)) {
5619
5629
  if (options.color.byText || options.color.byCharRange) {
5620
5630
  // Build the set manually since glyphs don't exist yet
5621
5631
  coloredTextIndices = new Set();
@@ -5720,7 +5730,9 @@ class Text {
5720
5730
  const depthScale = this.loadedFont.upem / size;
5721
5731
  const rawDepthInFontUnits = depth * depthScale;
5722
5732
  const minExtrudeDepth = this.loadedFont.upem * 0.000025;
5723
- const depthInFontUnits = rawDepthInFontUnits <= 0 ? 0 : Math.max(rawDepthInFontUnits, minExtrudeDepth);
5733
+ const depthInFontUnits = rawDepthInFontUnits <= 0
5734
+ ? 0
5735
+ : Math.max(rawDepthInFontUnits, minExtrudeDepth);
5724
5736
  if (!this.textLayout) {
5725
5737
  this.textLayout = new TextLayout(this.loadedFont);
5726
5738
  }
@@ -5892,10 +5904,12 @@ class Text {
5892
5904
  planeBounds.max.z *= finalScale;
5893
5905
  for (let i = 0; i < glyphInfoArray.length; i++) {
5894
5906
  const glyphInfo = glyphInfoArray[i];
5895
- glyphInfo.bounds.min.x = glyphInfo.bounds.min.x * finalScale + offsetScaled;
5907
+ glyphInfo.bounds.min.x =
5908
+ glyphInfo.bounds.min.x * finalScale + offsetScaled;
5896
5909
  glyphInfo.bounds.min.y *= finalScale;
5897
5910
  glyphInfo.bounds.min.z *= finalScale;
5898
- glyphInfo.bounds.max.x = glyphInfo.bounds.max.x * finalScale + offsetScaled;
5911
+ glyphInfo.bounds.max.x =
5912
+ glyphInfo.bounds.max.x * finalScale + offsetScaled;
5899
5913
  glyphInfo.bounds.max.y *= finalScale;
5900
5914
  glyphInfo.bounds.max.z *= finalScale;
5901
5915
  }
@@ -5958,13 +5972,11 @@ class Text {
5958
5972
  static registerPattern(language, pattern) {
5959
5973
  Text.patternCache.set(language, pattern);
5960
5974
  }
5961
- static clearFontCache() {
5962
- Text.fontCache.clear();
5963
- Text.fontCacheMemoryBytes = 0;
5964
- }
5965
5975
  static setMaxFontCacheMemoryMB(limitMB) {
5966
5976
  Text.maxFontCacheMemoryBytes =
5967
- 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;
5968
5980
  Text.enforceFontCacheMemoryLimit();
5969
5981
  }
5970
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
 
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * three-text v0.2.18
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
@@ -78,7 +78,9 @@ class PerformanceLogger {
78
78
  // Find the metric in reverse order (most recent first)
79
79
  for (let i = this.metrics.length - 1; i >= 0; i--) {
80
80
  const metric = this.metrics[i];
81
- if (metric.name === name && metric.startTime === startTime && !metric.endTime) {
81
+ if (metric.name === name &&
82
+ metric.startTime === startTime &&
83
+ !metric.endTime) {
82
84
  metric.endTime = endTime;
83
85
  metric.duration = duration;
84
86
  break;
@@ -466,7 +468,9 @@ class LineBreak {
466
468
  const char = chars[i];
467
469
  const nextChar = i < chars.length - 1 ? chars[i + 1] : null;
468
470
  if (/\s/.test(char)) {
469
- const width = widths ? (widths[i] ?? measureText(char)) : measureText(char);
471
+ const width = widths
472
+ ? (widths[i] ?? measureText(char))
473
+ : measureText(char);
470
474
  items.push({
471
475
  type: ItemType.GLUE,
472
476
  width,
@@ -839,7 +843,9 @@ class LineBreak {
839
843
  if (breaks.length === 0) {
840
844
  // For first emergency attempt, use initialEmergencyStretch
841
845
  // For subsequent iterations (short line detection), progressively increase
842
- currentEmergencyStretch = initialEmergencyStretch + (iteration * width * SHORT_LINE_EMERGENCY_STRETCH_INCREMENT);
846
+ currentEmergencyStretch =
847
+ initialEmergencyStretch +
848
+ iteration * width * SHORT_LINE_EMERGENCY_STRETCH_INCREMENT;
843
849
  breaks = LineBreak.findBreakpoints(currentItems, width, tolerance, looseness, true, currentEmergencyStretch, context);
844
850
  }
845
851
  // Last resort: allow higher badness (but not infinite)
@@ -1720,12 +1726,12 @@ class FontMetadataExtractor {
1720
1726
  try {
1721
1727
  if (gsubTableOffset) {
1722
1728
  const gsubData = this.extractFeatureDataFromTable(view, gsubTableOffset, nameTableOffset);
1723
- gsubData.features.forEach(f => features.add(f));
1729
+ gsubData.features.forEach((f) => features.add(f));
1724
1730
  Object.assign(featureNames, gsubData.names);
1725
1731
  }
1726
1732
  if (gposTableOffset) {
1727
1733
  const gposData = this.extractFeatureDataFromTable(view, gposTableOffset, nameTableOffset);
1728
- gposData.features.forEach(f => features.add(f));
1734
+ gposData.features.forEach((f) => features.add(f));
1729
1735
  Object.assign(featureNames, gposData.names);
1730
1736
  }
1731
1737
  }
@@ -2726,7 +2732,7 @@ class Tessellator {
2726
2732
  let extrusionContours = needsExtrusionContours
2727
2733
  ? needsWindingReversal
2728
2734
  ? tessContours
2729
- : originalContours ?? this.pathsToContours(paths)
2735
+ : (originalContours ?? this.pathsToContours(paths))
2730
2736
  : [];
2731
2737
  if (removeOverlaps) {
2732
2738
  logger.log('Two-pass: boundary extraction then triangulation');
@@ -2759,7 +2765,10 @@ class Tessellator {
2759
2765
  ? 'libtess returned empty result from triangulation pass'
2760
2766
  : 'libtess returned empty result from single-pass triangulation';
2761
2767
  logger.warn(warning);
2762
- return { triangles: { vertices: [], indices: [] }, contours: extrusionContours };
2768
+ return {
2769
+ triangles: { vertices: [], indices: [] },
2770
+ contours: extrusionContours
2771
+ };
2763
2772
  }
2764
2773
  return {
2765
2774
  triangles: {
@@ -3416,9 +3425,7 @@ class PathOptimizer {
3416
3425
  const v1LenSq = v1x * v1x + v1y * v1y;
3417
3426
  const v2LenSq = v2x * v2x + v2y * v2y;
3418
3427
  const minLenSq = this.config.minSegmentLength * this.config.minSegmentLength;
3419
- if (angle > threshold ||
3420
- v1LenSq < minLenSq ||
3421
- v2LenSq < minLenSq) {
3428
+ if (angle > threshold || v1LenSq < minLenSq || v2LenSq < minLenSq) {
3422
3429
  result.push(current);
3423
3430
  }
3424
3431
  else {
@@ -4180,7 +4187,7 @@ class GlyphGeometryBuilder {
4180
4187
  const relativePositions = cluster.glyphs.map((g) => new Vec3(g.x ?? 0, g.y ?? 0, 0));
4181
4188
  boundaryGroups = this.clusterer.cluster(clusterGlyphContours, relativePositions);
4182
4189
  this.clusteringCache.set(cacheKey, {
4183
- glyphIds: cluster.glyphs.map(g => g.g),
4190
+ glyphIds: cluster.glyphs.map((g) => g.g),
4184
4191
  groups: boundaryGroups
4185
4192
  });
4186
4193
  }
@@ -4570,7 +4577,8 @@ class TextShaper {
4570
4577
  if (LineBreak.isCJOpeningPunctuation(currentChar)) {
4571
4578
  shouldApply = false;
4572
4579
  }
4573
- if (LineBreak.isCJPunctuation(currentChar) && LineBreak.isCJPunctuation(nextChar)) {
4580
+ if (LineBreak.isCJPunctuation(currentChar) &&
4581
+ LineBreak.isCJPunctuation(nextChar)) {
4574
4582
  shouldApply = false;
4575
4583
  }
4576
4584
  if (shouldApply) {
@@ -5374,7 +5382,7 @@ class Text {
5374
5382
  // Stringify with sorted keys for cache stability
5375
5383
  static stableStringify(obj) {
5376
5384
  const keys = Object.keys(obj).sort();
5377
- const pairs = keys.map(k => `${k}:${obj[k]}`);
5385
+ const pairs = keys.map((k) => `${k}:${obj[k]}`);
5378
5386
  return pairs.join(',');
5379
5387
  }
5380
5388
  constructor() {
@@ -5612,7 +5620,9 @@ class Text {
5612
5620
  // to selectively use glyph-level caching (separate vertices) only for clusters containing
5613
5621
  // colored text, while non-colored clusters can still use fast cluster-level merging
5614
5622
  let coloredTextIndices;
5615
- if (options.color && typeof options.color === 'object' && !Array.isArray(options.color)) {
5623
+ if (options.color &&
5624
+ typeof options.color === 'object' &&
5625
+ !Array.isArray(options.color)) {
5616
5626
  if (options.color.byText || options.color.byCharRange) {
5617
5627
  // Build the set manually since glyphs don't exist yet
5618
5628
  coloredTextIndices = new Set();
@@ -5717,7 +5727,9 @@ class Text {
5717
5727
  const depthScale = this.loadedFont.upem / size;
5718
5728
  const rawDepthInFontUnits = depth * depthScale;
5719
5729
  const minExtrudeDepth = this.loadedFont.upem * 0.000025;
5720
- const depthInFontUnits = rawDepthInFontUnits <= 0 ? 0 : Math.max(rawDepthInFontUnits, minExtrudeDepth);
5730
+ const depthInFontUnits = rawDepthInFontUnits <= 0
5731
+ ? 0
5732
+ : Math.max(rawDepthInFontUnits, minExtrudeDepth);
5721
5733
  if (!this.textLayout) {
5722
5734
  this.textLayout = new TextLayout(this.loadedFont);
5723
5735
  }
@@ -5889,10 +5901,12 @@ class Text {
5889
5901
  planeBounds.max.z *= finalScale;
5890
5902
  for (let i = 0; i < glyphInfoArray.length; i++) {
5891
5903
  const glyphInfo = glyphInfoArray[i];
5892
- glyphInfo.bounds.min.x = glyphInfo.bounds.min.x * finalScale + offsetScaled;
5904
+ glyphInfo.bounds.min.x =
5905
+ glyphInfo.bounds.min.x * finalScale + offsetScaled;
5893
5906
  glyphInfo.bounds.min.y *= finalScale;
5894
5907
  glyphInfo.bounds.min.z *= finalScale;
5895
- glyphInfo.bounds.max.x = glyphInfo.bounds.max.x * finalScale + offsetScaled;
5908
+ glyphInfo.bounds.max.x =
5909
+ glyphInfo.bounds.max.x * finalScale + offsetScaled;
5896
5910
  glyphInfo.bounds.max.y *= finalScale;
5897
5911
  glyphInfo.bounds.max.z *= finalScale;
5898
5912
  }
@@ -5955,13 +5969,11 @@ class Text {
5955
5969
  static registerPattern(language, pattern) {
5956
5970
  Text.patternCache.set(language, pattern);
5957
5971
  }
5958
- static clearFontCache() {
5959
- Text.fontCache.clear();
5960
- Text.fontCacheMemoryBytes = 0;
5961
- }
5962
5972
  static setMaxFontCacheMemoryMB(limitMB) {
5963
5973
  Text.maxFontCacheMemoryBytes =
5964
- limitMB === Infinity ? Infinity : Math.max(1, Math.floor(limitMB)) * 1024 * 1024;
5974
+ limitMB === Infinity
5975
+ ? Infinity
5976
+ : Math.max(1, Math.floor(limitMB)) * 1024 * 1024;
5965
5977
  Text.enforceFontCacheMemoryLimit();
5966
5978
  }
5967
5979
  getLoadedFont() {
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * three-text v0.2.18
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
@@ -886,10 +886,10 @@ w=e.colors,m=e.fc}const v=this.Ar.bn()
886
886
  return{vertices:t,normals:e,indices:i,colors:w,glyphs:s,planeBounds:n,stats:{dc:i.length/3,yc:t.length/3,Ls:v.Ls,Ms:v.Ms,Is:v.Is,...o||{}},query(t){if(!h)throw Error("Original text not available for querying")
887
887
  return new gt(h,s).xa(t)},fc:m,glyphAttributes:void 0}}Ce(){if(!this.le)throw Error("Font not loaded. Call loadFont() first")
888
888
  return B.Ce(this.le.p)}static async wc(t,e){await Promise.all(t.map(async t=>{if(!xt.Ma.has(t))try{const s=await i(t,e)
889
- xt.Ma.set(t,s)}catch(e){l.warn(`Failed to pre-load patterns for ${t}: ${e}`)}}))}static mc(t,e){xt.Ma.set(t,e)}static vc(){xt.Ga.clear(),xt.Oa=0}static gc(t){xt.Fa=t===1/0?1/0:1048576*Math.max(1,Math.floor(t)),xt.Va()}getLoadedFont(){return this.le}measureTextWidth(t,e=0){if(!this.le)throw Error("Font not loaded. Call loadFont() first")
889
+ xt.Ma.set(t,s)}catch(e){l.warn(`Failed to pre-load patterns for ${t}: ${e}`)}}))}static mc(t,e){xt.Ma.set(t,e)}static vc(t){xt.Fa=t===1/0?1/0:1048576*Math.max(1,Math.floor(t)),xt.Va()}getLoadedFont(){return this.le}measureTextWidth(t,e=0){if(!this.le)throw Error("Font not loaded. Call loadFont() first")
890
890
  return G.measureTextWidth(this.le,t,e)}getCacheStatistics(){return this.Ar?this.Ar.Er():null}clearCache(){this.Ar&&this.Ar.clearCache()}hc(t,e){const i=new Float32Array(3*t),s=new Float32Array(t),n=new Float32Array(t)
891
891
  return e.forEach((e,r)=>{const o=(e.bounds.min.x+e.bounds.max.x)/2,h=(e.bounds.min.y+e.bounds.max.y)/2,a=(e.bounds.min.z+e.bounds.max.z)/2
892
892
  for(let c=0;e.gr>c;c++){const l=e.nr+c
893
- t>l&&(i[3*l]=o,i[3*l+1]=h,i[3*l+2]=a,s[l]=r,n[l]=e.vr)}}),{xc:i,dn:s,Sc:n}}ja(){this.Ar=void 0,this.tc=void 0,this.lc=void 0}destroy(){if(!this.le)return
893
+ t>l&&(i[3*l]=o,i[3*l+1]=h,i[3*l+2]=a,s[l]=r,n[l]=e.vr)}}),{gc:i,dn:s,xc:n}}ja(){this.Ar=void 0,this.tc=void 0,this.lc=void 0}destroy(){if(!this.le)return
894
894
  const t=this.le
895
895
  try{$.Ze(t)}catch(t){l.warn("Error destroying HarfBuzz objects:",t)}finally{this.le=void 0,this.lc=void 0,this.tc=void 0}}}exports.DEFAULT_CURVE_FIDELITY=it,exports.FontMetadataExtractor=B,exports.Text=xt,exports.createGlyphCache=r,exports.globalGlyphCache=z
package/dist/index.min.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * three-text v0.2.18
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
@@ -885,10 +885,10 @@ w=e.colors,v=e.fc}const m=this.Ar.bn()
885
885
  return{vertices:t,normals:e,indices:i,colors:w,glyphs:s,planeBounds:n,stats:{dc:i.length/3,yc:t.length/3,Ls:m.Ls,Ms:m.Ms,Is:m.Is,...o||{}},query(t){if(!h)throw Error("Original text not available for querying")
886
886
  return new mt(h,s).xa(t)},fc:v,glyphAttributes:void 0}}Ce(){if(!this.le)throw Error("Font not loaded. Call loadFont() first")
887
887
  return C.Ce(this.le.p)}static async wc(t,e){await Promise.all(t.map(async t=>{if(!gt.Ma.has(t))try{const s=await i(t,e)
888
- gt.Ma.set(t,s)}catch(e){c.warn(`Failed to pre-load patterns for ${t}: ${e}`)}}))}static vc(t,e){gt.Ma.set(t,e)}static mc(){gt.Ga.clear(),gt.Oa=0}static gc(t){gt.Fa=t===1/0?1/0:1048576*Math.max(1,Math.floor(t)),gt.Va()}getLoadedFont(){return this.le}measureTextWidth(t,e=0){if(!this.le)throw Error("Font not loaded. Call loadFont() first")
888
+ gt.Ma.set(t,s)}catch(e){c.warn(`Failed to pre-load patterns for ${t}: ${e}`)}}))}static vc(t,e){gt.Ma.set(t,e)}static mc(t){gt.Fa=t===1/0?1/0:1048576*Math.max(1,Math.floor(t)),gt.Va()}getLoadedFont(){return this.le}measureTextWidth(t,e=0){if(!this.le)throw Error("Font not loaded. Call loadFont() first")
889
889
  return I.measureTextWidth(this.le,t,e)}getCacheStatistics(){return this.Ar?this.Ar._r():null}clearCache(){this.Ar&&this.Ar.clearCache()}hc(t,e){const i=new Float32Array(3*t),s=new Float32Array(t),n=new Float32Array(t)
890
890
  return e.forEach((e,r)=>{const o=(e.bounds.min.x+e.bounds.max.x)/2,h=(e.bounds.min.y+e.bounds.max.y)/2,a=(e.bounds.min.z+e.bounds.max.z)/2
891
891
  for(let c=0;e.gr>c;c++){const l=e.nr+c
892
- t>l&&(i[3*l]=o,i[3*l+1]=h,i[3*l+2]=a,s[l]=r,n[l]=e.mr)}}),{xc:i,dn:s,Sc:n}}za(){this.Ar=void 0,this.tc=void 0,this.lc=void 0}destroy(){if(!this.le)return
892
+ t>l&&(i[3*l]=o,i[3*l+1]=h,i[3*l+2]=a,s[l]=r,n[l]=e.mr)}}),{gc:i,dn:s,xc:n}}za(){this.Ar=void 0,this.tc=void 0,this.lc=void 0}destroy(){if(!this.le)return
893
893
  const t=this.le
894
894
  try{$.Ze(t)}catch(t){c.warn("Error destroying HarfBuzz objects:",t)}finally{this.le=void 0,this.lc=void 0,this.tc=void 0}}}export{et as DEFAULT_CURVE_FIDELITY,C as FontMetadataExtractor,gt as Text,r as createGlyphCache,z as globalGlyphCache}
package/dist/index.umd.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * three-text v0.2.18
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
@@ -83,7 +83,9 @@
83
83
  // Find the metric in reverse order (most recent first)
84
84
  for (let i = this.metrics.length - 1; i >= 0; i--) {
85
85
  const metric = this.metrics[i];
86
- if (metric.name === name && metric.startTime === startTime && !metric.endTime) {
86
+ if (metric.name === name &&
87
+ metric.startTime === startTime &&
88
+ !metric.endTime) {
87
89
  metric.endTime = endTime;
88
90
  metric.duration = duration;
89
91
  break;
@@ -471,7 +473,9 @@
471
473
  const char = chars[i];
472
474
  const nextChar = i < chars.length - 1 ? chars[i + 1] : null;
473
475
  if (/\s/.test(char)) {
474
- const width = widths ? (widths[i] ?? measureText(char)) : measureText(char);
476
+ const width = widths
477
+ ? (widths[i] ?? measureText(char))
478
+ : measureText(char);
475
479
  items.push({
476
480
  type: ItemType.GLUE,
477
481
  width,
@@ -844,7 +848,9 @@
844
848
  if (breaks.length === 0) {
845
849
  // For first emergency attempt, use initialEmergencyStretch
846
850
  // For subsequent iterations (short line detection), progressively increase
847
- currentEmergencyStretch = initialEmergencyStretch + (iteration * width * SHORT_LINE_EMERGENCY_STRETCH_INCREMENT);
851
+ currentEmergencyStretch =
852
+ initialEmergencyStretch +
853
+ iteration * width * SHORT_LINE_EMERGENCY_STRETCH_INCREMENT;
848
854
  breaks = LineBreak.findBreakpoints(currentItems, width, tolerance, looseness, true, currentEmergencyStretch, context);
849
855
  }
850
856
  // Last resort: allow higher badness (but not infinite)
@@ -1725,12 +1731,12 @@
1725
1731
  try {
1726
1732
  if (gsubTableOffset) {
1727
1733
  const gsubData = this.extractFeatureDataFromTable(view, gsubTableOffset, nameTableOffset);
1728
- gsubData.features.forEach(f => features.add(f));
1734
+ gsubData.features.forEach((f) => features.add(f));
1729
1735
  Object.assign(featureNames, gsubData.names);
1730
1736
  }
1731
1737
  if (gposTableOffset) {
1732
1738
  const gposData = this.extractFeatureDataFromTable(view, gposTableOffset, nameTableOffset);
1733
- gposData.features.forEach(f => features.add(f));
1739
+ gposData.features.forEach((f) => features.add(f));
1734
1740
  Object.assign(featureNames, gposData.names);
1735
1741
  }
1736
1742
  }
@@ -2733,7 +2739,7 @@
2733
2739
  let extrusionContours = needsExtrusionContours
2734
2740
  ? needsWindingReversal
2735
2741
  ? tessContours
2736
- : originalContours ?? this.pathsToContours(paths)
2742
+ : (originalContours ?? this.pathsToContours(paths))
2737
2743
  : [];
2738
2744
  if (removeOverlaps) {
2739
2745
  logger.log('Two-pass: boundary extraction then triangulation');
@@ -2766,7 +2772,10 @@
2766
2772
  ? 'libtess returned empty result from triangulation pass'
2767
2773
  : 'libtess returned empty result from single-pass triangulation';
2768
2774
  logger.warn(warning);
2769
- return { triangles: { vertices: [], indices: [] }, contours: extrusionContours };
2775
+ return {
2776
+ triangles: { vertices: [], indices: [] },
2777
+ contours: extrusionContours
2778
+ };
2770
2779
  }
2771
2780
  return {
2772
2781
  triangles: {
@@ -3423,9 +3432,7 @@
3423
3432
  const v1LenSq = v1x * v1x + v1y * v1y;
3424
3433
  const v2LenSq = v2x * v2x + v2y * v2y;
3425
3434
  const minLenSq = this.config.minSegmentLength * this.config.minSegmentLength;
3426
- if (angle > threshold ||
3427
- v1LenSq < minLenSq ||
3428
- v2LenSq < minLenSq) {
3435
+ if (angle > threshold || v1LenSq < minLenSq || v2LenSq < minLenSq) {
3429
3436
  result.push(current);
3430
3437
  }
3431
3438
  else {
@@ -4187,7 +4194,7 @@
4187
4194
  const relativePositions = cluster.glyphs.map((g) => new Vec3(g.x ?? 0, g.y ?? 0, 0));
4188
4195
  boundaryGroups = this.clusterer.cluster(clusterGlyphContours, relativePositions);
4189
4196
  this.clusteringCache.set(cacheKey, {
4190
- glyphIds: cluster.glyphs.map(g => g.g),
4197
+ glyphIds: cluster.glyphs.map((g) => g.g),
4191
4198
  groups: boundaryGroups
4192
4199
  });
4193
4200
  }
@@ -4577,7 +4584,8 @@
4577
4584
  if (LineBreak.isCJOpeningPunctuation(currentChar)) {
4578
4585
  shouldApply = false;
4579
4586
  }
4580
- if (LineBreak.isCJPunctuation(currentChar) && LineBreak.isCJPunctuation(nextChar)) {
4587
+ if (LineBreak.isCJPunctuation(currentChar) &&
4588
+ LineBreak.isCJPunctuation(nextChar)) {
4581
4589
  shouldApply = false;
4582
4590
  }
4583
4591
  if (shouldApply) {
@@ -5381,7 +5389,7 @@
5381
5389
  // Stringify with sorted keys for cache stability
5382
5390
  static stableStringify(obj) {
5383
5391
  const keys = Object.keys(obj).sort();
5384
- const pairs = keys.map(k => `${k}:${obj[k]}`);
5392
+ const pairs = keys.map((k) => `${k}:${obj[k]}`);
5385
5393
  return pairs.join(',');
5386
5394
  }
5387
5395
  constructor() {
@@ -5619,7 +5627,9 @@
5619
5627
  // to selectively use glyph-level caching (separate vertices) only for clusters containing
5620
5628
  // colored text, while non-colored clusters can still use fast cluster-level merging
5621
5629
  let coloredTextIndices;
5622
- if (options.color && typeof options.color === 'object' && !Array.isArray(options.color)) {
5630
+ if (options.color &&
5631
+ typeof options.color === 'object' &&
5632
+ !Array.isArray(options.color)) {
5623
5633
  if (options.color.byText || options.color.byCharRange) {
5624
5634
  // Build the set manually since glyphs don't exist yet
5625
5635
  coloredTextIndices = new Set();
@@ -5724,7 +5734,9 @@
5724
5734
  const depthScale = this.loadedFont.upem / size;
5725
5735
  const rawDepthInFontUnits = depth * depthScale;
5726
5736
  const minExtrudeDepth = this.loadedFont.upem * 0.000025;
5727
- const depthInFontUnits = rawDepthInFontUnits <= 0 ? 0 : Math.max(rawDepthInFontUnits, minExtrudeDepth);
5737
+ const depthInFontUnits = rawDepthInFontUnits <= 0
5738
+ ? 0
5739
+ : Math.max(rawDepthInFontUnits, minExtrudeDepth);
5728
5740
  if (!this.textLayout) {
5729
5741
  this.textLayout = new TextLayout(this.loadedFont);
5730
5742
  }
@@ -5896,10 +5908,12 @@
5896
5908
  planeBounds.max.z *= finalScale;
5897
5909
  for (let i = 0; i < glyphInfoArray.length; i++) {
5898
5910
  const glyphInfo = glyphInfoArray[i];
5899
- glyphInfo.bounds.min.x = glyphInfo.bounds.min.x * finalScale + offsetScaled;
5911
+ glyphInfo.bounds.min.x =
5912
+ glyphInfo.bounds.min.x * finalScale + offsetScaled;
5900
5913
  glyphInfo.bounds.min.y *= finalScale;
5901
5914
  glyphInfo.bounds.min.z *= finalScale;
5902
- glyphInfo.bounds.max.x = glyphInfo.bounds.max.x * finalScale + offsetScaled;
5915
+ glyphInfo.bounds.max.x =
5916
+ glyphInfo.bounds.max.x * finalScale + offsetScaled;
5903
5917
  glyphInfo.bounds.max.y *= finalScale;
5904
5918
  glyphInfo.bounds.max.z *= finalScale;
5905
5919
  }
@@ -5962,13 +5976,11 @@
5962
5976
  static registerPattern(language, pattern) {
5963
5977
  Text.patternCache.set(language, pattern);
5964
5978
  }
5965
- static clearFontCache() {
5966
- Text.fontCache.clear();
5967
- Text.fontCacheMemoryBytes = 0;
5968
- }
5969
5979
  static setMaxFontCacheMemoryMB(limitMB) {
5970
5980
  Text.maxFontCacheMemoryBytes =
5971
- limitMB === Infinity ? Infinity : Math.max(1, Math.floor(limitMB)) * 1024 * 1024;
5981
+ limitMB === Infinity
5982
+ ? Infinity
5983
+ : Math.max(1, Math.floor(limitMB)) * 1024 * 1024;
5972
5984
  Text.enforceFontCacheMemoryLimit();
5973
5985
  }
5974
5986
  getLoadedFont() {
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * three-text v0.2.18
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
@@ -887,10 +887,10 @@ w=e.colors,v=e.fc}const g=this.Ar.Sn()
887
887
  return{vertices:t,normals:e,indices:i,colors:w,glyphs:s,planeBounds:n,stats:{dc:i.length/3,yc:t.length/3,Ms:g.Ms,Ls:g.Ls,Is:g.Is,...o||{}},query(t){if(!h)throw Error("Original text not available for querying")
888
888
  return new mt(h,s).xa(t)},fc:v,glyphAttributes:void 0}}Ce(){if(!this.le)throw Error("Font not loaded. Call loadFont() first")
889
889
  return B.Ce(this.le.p)}static async wc(t,e){await Promise.all(t.map(async t=>{if(!xt.La.has(t))try{const i=await s(t,e)
890
- xt.La.set(t,i)}catch(e){l.warn(`Failed to pre-load patterns for ${t}: ${e}`)}}))}static vc(t,e){xt.La.set(t,e)}static gc(){xt.Ga.clear(),xt.Oa=0}static mc(t){xt.Fa=t===1/0?1/0:1048576*Math.max(1,Math.floor(t)),xt.Va()}getLoadedFont(){return this.le}measureTextWidth(t,e=0){if(!this.le)throw Error("Font not loaded. Call loadFont() first")
890
+ xt.La.set(t,i)}catch(e){l.warn(`Failed to pre-load patterns for ${t}: ${e}`)}}))}static vc(t,e){xt.La.set(t,e)}static gc(t){xt.Fa=t===1/0?1/0:1048576*Math.max(1,Math.floor(t)),xt.Va()}getLoadedFont(){return this.le}measureTextWidth(t,e=0){if(!this.le)throw Error("Font not loaded. Call loadFont() first")
891
891
  return G.measureTextWidth(this.le,t,e)}getCacheStatistics(){return this.Ar?this.Ar.Er():null}clearCache(){this.Ar&&this.Ar.clearCache()}hc(t,e){const i=new Float32Array(3*t),s=new Float32Array(t),n=new Float32Array(t)
892
892
  return e.forEach((e,r)=>{const o=(e.bounds.min.x+e.bounds.max.x)/2,h=(e.bounds.min.y+e.bounds.max.y)/2,a=(e.bounds.min.z+e.bounds.max.z)/2
893
893
  for(let c=0;e.mr>c;c++){const l=e.nr+c
894
- t>l&&(i[3*l]=o,i[3*l+1]=h,i[3*l+2]=a,s[l]=r,n[l]=e.gr)}}),{xc:i,dn:s,bc:n}}za(){this.Ar=void 0,this.tc=void 0,this.lc=void 0}destroy(){if(!this.le)return
894
+ t>l&&(i[3*l]=o,i[3*l+1]=h,i[3*l+2]=a,s[l]=r,n[l]=e.gr)}}),{mc:i,dn:s,xc:n}}za(){this.Ar=void 0,this.tc=void 0,this.lc=void 0}destroy(){if(!this.le)return
895
895
  const t=this.le
896
- try{$.Ze(t)}catch(t){l.warn("Error destroying HarfBuzz objects:",t)}finally{this.le=void 0,this.lc=void 0,this.tc=void 0}}}t.DEFAULT_CURVE_FIDELITY=it,t.FontMetadataExtractor=B,t.Text=xt,t.Sc=o,t._c=W})
896
+ try{$.Ze(t)}catch(t){l.warn("Error destroying HarfBuzz objects:",t)}finally{this.le=void 0,this.lc=void 0,this.tc=void 0}}}t.DEFAULT_CURVE_FIDELITY=it,t.FontMetadataExtractor=B,t.Text=xt,t.bc=o,t.Sc=W})
@@ -47,6 +47,7 @@ class Text {
47
47
  static { this.init = Text$1.Text.init; }
48
48
  static { this.registerPattern = Text$1.Text.registerPattern; }
49
49
  static { this.preloadPatterns = Text$1.Text.preloadPatterns; }
50
+ static { this.setMaxFontCacheMemoryMB = Text$1.Text.setMaxFontCacheMemoryMB; }
50
51
  // Main API - wraps core result in BufferGeometry
51
52
  static async create(options) {
52
53
  const coreResult = await Text$1.Text.create(options);
@@ -10,10 +10,21 @@ interface HyphenationTrieNode {
10
10
  };
11
11
  }
12
12
 
13
+ interface CacheStats {
14
+ hits: number;
15
+ misses: number;
16
+ evictions: number;
17
+ size: number;
18
+ memoryUsage: number;
19
+ }
20
+
13
21
  interface ThreeTextGeometryInfo extends Omit<TextGeometryInfo, 'vertices' | 'normals' | 'indices' | 'colors' | 'glyphAttributes'> {
14
22
  geometry: BufferGeometry;
15
23
  getLoadedFont(): LoadedFont | undefined;
16
- getCacheStatistics(): any;
24
+ getCacheStatistics(): (CacheStats & {
25
+ hitRate: number;
26
+ memoryUsageMB: number;
27
+ }) | null;
17
28
  clearCache(): void;
18
29
  measureTextWidth(text: string, letterSpacing?: number): number;
19
30
  update(options: Partial<TextOptions>): Promise<ThreeTextGeometryInfo>;
@@ -24,6 +35,7 @@ declare class Text {
24
35
  static init: typeof Text$1.init;
25
36
  static registerPattern: typeof Text$1.registerPattern;
26
37
  static preloadPatterns: typeof Text$1.preloadPatterns;
38
+ static setMaxFontCacheMemoryMB: typeof Text$1.setMaxFontCacheMemoryMB;
27
39
  static create(options: TextOptions): Promise<ThreeTextGeometryInfo>;
28
40
  }
29
41
 
@@ -45,6 +45,7 @@ class Text {
45
45
  static { this.init = Text$1.init; }
46
46
  static { this.registerPattern = Text$1.registerPattern; }
47
47
  static { this.preloadPatterns = Text$1.preloadPatterns; }
48
+ static { this.setMaxFontCacheMemoryMB = Text$1.setMaxFontCacheMemoryMB; }
48
49
  // Main API - wraps core result in BufferGeometry
49
50
  static async create(options) {
50
51
  const coreResult = await Text$1.create(options);
@@ -3,7 +3,7 @@
3
3
  var jsxRuntime = require('react/jsx-runtime');
4
4
  var react = require('react');
5
5
  var THREE = require('three');
6
- var index = require('./index');
6
+ var index = require('./index.cjs');
7
7
 
8
8
  function _interopNamespaceDefault(e) {
9
9
  var n = Object.create(null);
@@ -142,6 +142,7 @@ const Text = Object.assign(Text$1, {
142
142
  init: index.Text.init,
143
143
  registerPattern: index.Text.registerPattern,
144
144
  preloadPatterns: index.Text.preloadPatterns,
145
+ setMaxFontCacheMemoryMB: index.Text.setMaxFontCacheMemoryMB,
145
146
  create: index.Text.create
146
147
  });
147
148
 
@@ -9,6 +9,14 @@ interface HyphenationTrieNode {
9
9
  };
10
10
  }
11
11
 
12
+ interface CacheStats {
13
+ hits: number;
14
+ misses: number;
15
+ evictions: number;
16
+ size: number;
17
+ memoryUsage: number;
18
+ }
19
+
12
20
  interface BoundingBox {
13
21
  min: {
14
22
  x: number;
@@ -183,13 +191,19 @@ interface TextGeometryInfo {
183
191
  pointsRemovedByVisvalingam: number;
184
192
  pointsRemovedByColinear: number;
185
193
  originalPointCount: number;
186
- };
194
+ } & Partial<CacheStats & {
195
+ hitRate: number;
196
+ memoryUsageMB: number;
197
+ }>;
187
198
  query(options: TextQueryOptions): TextRange[];
188
199
  coloredRanges?: ColoredRange[];
189
200
  }
190
201
  interface TextHandle extends TextGeometryInfo {
191
202
  getLoadedFont(): LoadedFont | undefined;
192
- getCacheStatistics(): any;
203
+ getCacheStatistics(): (CacheStats & {
204
+ hitRate: number;
205
+ memoryUsageMB: number;
206
+ }) | null;
193
207
  clearCache(): void;
194
208
  measureTextWidth(text: string, letterSpacing?: number): number;
195
209
  update(options: Partial<TextOptions>): Promise<TextHandle>;
@@ -228,7 +242,7 @@ interface ColoredRange {
228
242
  }
229
243
  interface TextOptions {
230
244
  text: string;
231
- font?: string | ArrayBuffer;
245
+ font: string | ArrayBuffer;
232
246
  size?: number;
233
247
  depth?: number;
234
248
  lineHeight?: number;
@@ -329,7 +343,6 @@ declare class Text$1 {
329
343
  getFontMetrics(): FontMetrics;
330
344
  static preloadPatterns(languages: string[], patternsPath?: string): Promise<void>;
331
345
  static registerPattern(language: string, pattern: HyphenationTrieNode): void;
332
- static clearFontCache(): void;
333
346
  static setMaxFontCacheMemoryMB(limitMB: number): void;
334
347
  getLoadedFont(): LoadedFont | undefined;
335
348
  measureTextWidth(text: string, letterSpacing?: number): number;
@@ -343,14 +356,6 @@ declare class Text$1 {
343
356
  destroy(): void;
344
357
  }
345
358
 
346
- interface CacheStats {
347
- hits: number;
348
- misses: number;
349
- evictions: number;
350
- size: number;
351
- memoryUsage: number;
352
- }
353
-
354
359
  interface ThreeTextProps extends Omit<TextOptions$1, 'text'> {
355
360
  children: string;
356
361
  font: string | ArrayBuffer;
@@ -369,6 +374,7 @@ declare const Text: react.ForwardRefExoticComponent<ThreeTextProps & react.RefAt
369
374
  init: typeof Text$1.init;
370
375
  registerPattern: typeof Text$1.registerPattern;
371
376
  preloadPatterns: typeof Text$1.preloadPatterns;
377
+ setMaxFontCacheMemoryMB: typeof Text$1.setMaxFontCacheMemoryMB;
372
378
  create: typeof Text$2.create;
373
379
  };
374
380
 
@@ -1,7 +1,7 @@
1
1
  import { jsx } from 'react/jsx-runtime';
2
2
  import { forwardRef, useState, useRef, useMemo, useEffect } from 'react';
3
3
  import * as THREE from 'three';
4
- import { Text as Text$2 } from './index';
4
+ import { Text as Text$2 } from './index.js';
5
5
 
6
6
  function deepEqual(a, b) {
7
7
  if (a === b)
@@ -121,6 +121,7 @@ const Text = Object.assign(Text$1, {
121
121
  init: Text$2.init,
122
122
  registerPattern: Text$2.registerPattern,
123
123
  preloadPatterns: Text$2.preloadPatterns,
124
+ setMaxFontCacheMemoryMB: Text$2.setMaxFontCacheMemoryMB,
124
125
  create: Text$2.create
125
126
  });
126
127
 
@@ -43,7 +43,6 @@ export declare class Text {
43
43
  getFontMetrics(): FontMetrics;
44
44
  static preloadPatterns(languages: string[], patternsPath?: string): Promise<void>;
45
45
  static registerPattern(language: string, pattern: HyphenationTrieNode): void;
46
- static clearFontCache(): void;
47
46
  static setMaxFontCacheMemoryMB(limitMB: number): void;
48
47
  getLoadedFont(): LoadedFont | undefined;
49
48
  measureTextWidth(text: string, letterSpacing?: number): number;
@@ -1,4 +1,5 @@
1
1
  import type { HyphenationTrieNode } from '../hyphenation';
2
+ import type { CacheStats } from '../utils/LRUCache';
2
3
  import type { Vec2, Vec3, BoundingBox } from './vectors';
3
4
  export type { HyphenationTrieNode };
4
5
  export interface Path {
@@ -249,13 +250,19 @@ export interface TextGeometryInfo {
249
250
  pointsRemovedByVisvalingam: number;
250
251
  pointsRemovedByColinear: number;
251
252
  originalPointCount: number;
252
- };
253
+ } & Partial<CacheStats & {
254
+ hitRate: number;
255
+ memoryUsageMB: number;
256
+ }>;
253
257
  query(options: TextQueryOptions): TextRange[];
254
258
  coloredRanges?: ColoredRange[];
255
259
  }
256
260
  export interface TextHandle extends TextGeometryInfo {
257
261
  getLoadedFont(): LoadedFont | undefined;
258
- getCacheStatistics(): any;
262
+ getCacheStatistics(): (CacheStats & {
263
+ hitRate: number;
264
+ memoryUsageMB: number;
265
+ }) | null;
259
266
  clearCache(): void;
260
267
  measureTextWidth(text: string, letterSpacing?: number): number;
261
268
  update(options: Partial<TextOptions>): Promise<TextHandle>;
@@ -294,7 +301,7 @@ export interface ColoredRange {
294
301
  }
295
302
  export interface TextOptions {
296
303
  text: string;
297
- font?: string | ArrayBuffer;
304
+ font: string | ArrayBuffer;
298
305
  size?: number;
299
306
  depth?: number;
300
307
  lineHeight?: number;
@@ -2,10 +2,14 @@ import { BufferGeometry } from 'three';
2
2
  import { Text as TextCore } from '../core/Text';
3
3
  import type { TextOptions, TextGeometryInfo as CoreTextGeometryInfo, LoadedFont } from '../core/types';
4
4
  import type { HyphenationTrieNode } from '../hyphenation';
5
+ import type { CacheStats } from '../utils/LRUCache';
5
6
  export interface ThreeTextGeometryInfo extends Omit<CoreTextGeometryInfo, 'vertices' | 'normals' | 'indices' | 'colors' | 'glyphAttributes'> {
6
7
  geometry: BufferGeometry;
7
8
  getLoadedFont(): LoadedFont | undefined;
8
- getCacheStatistics(): any;
9
+ getCacheStatistics(): (CacheStats & {
10
+ hitRate: number;
11
+ memoryUsageMB: number;
12
+ }) | null;
9
13
  clearCache(): void;
10
14
  measureTextWidth(text: string, letterSpacing?: number): number;
11
15
  update(options: Partial<TextOptions>): Promise<ThreeTextGeometryInfo>;
@@ -16,6 +20,7 @@ export declare class Text {
16
20
  static init: typeof TextCore.init;
17
21
  static registerPattern: typeof TextCore.registerPattern;
18
22
  static preloadPatterns: typeof TextCore.preloadPatterns;
23
+ static setMaxFontCacheMemoryMB: typeof TextCore.setMaxFontCacheMemoryMB;
19
24
  static create(options: TextOptions): Promise<ThreeTextGeometryInfo>;
20
25
  }
21
26
  export type { TextOptions, ThreeTextGeometryInfo as TextGeometryInfo, LoadedFont };
@@ -6,5 +6,6 @@ export declare const Text: import("react").ForwardRefExoticComponent<import("./T
6
6
  init: typeof import("..").Text.init;
7
7
  registerPattern: typeof import("..").Text.registerPattern;
8
8
  preloadPatterns: typeof import("..").Text.preloadPatterns;
9
+ setMaxFontCacheMemoryMB: typeof import("..").Text.setMaxFontCacheMemoryMB;
9
10
  create: typeof ThreeTextCore.create;
10
11
  };
package/package.json CHANGED
@@ -1,37 +1,47 @@
1
1
  {
2
2
  "name": "three-text",
3
- "version": "0.2.18",
4
- "description": "3D font rendering and text layout engine for the web",
3
+ "version": "0.2.19",
4
+ "description": "3D mesh font geometry and text layout engine for the web",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",
7
7
  "type": "module",
8
8
  "types": "dist/index.d.ts",
9
9
  "exports": {
10
10
  ".": {
11
+ "types": "./dist/index.d.ts",
11
12
  "import": "./dist/index.js",
12
13
  "require": "./dist/index.cjs"
13
14
  },
14
15
  "./three": {
16
+ "types": "./dist/three/index.d.ts",
15
17
  "import": "./dist/three/index.js",
16
18
  "require": "./dist/three/index.cjs"
17
19
  },
18
20
  "./three/react": {
21
+ "types": "./dist/three/react.d.ts",
19
22
  "import": "./dist/three/react.js",
20
23
  "require": "./dist/three/react.cjs"
21
24
  },
22
25
  "./webgl": {
26
+ "types": "./dist/webgl/index.d.ts",
23
27
  "import": "./dist/webgl/index.js",
24
28
  "require": "./dist/webgl/index.cjs"
25
29
  },
26
30
  "./webgpu": {
31
+ "types": "./dist/webgpu/index.d.ts",
27
32
  "import": "./dist/webgpu/index.js",
28
33
  "require": "./dist/webgpu/index.cjs"
29
34
  },
30
35
  "./p5": {
36
+ "types": "./dist/p5/index.d.ts",
31
37
  "import": "./dist/p5/index.js",
32
38
  "require": "./dist/p5/index.cjs"
33
39
  },
34
- "./patterns/*": "./dist/patterns/*"
40
+ "./patterns/*": {
41
+ "types": "./dist/patterns/*.d.ts",
42
+ "import": "./dist/patterns/*.js",
43
+ "require": "./dist/patterns/*.cjs"
44
+ }
35
45
  },
36
46
  "files": [
37
47
  "dist",
@@ -95,6 +105,7 @@
95
105
  "libtess": "^1.2.2"
96
106
  },
97
107
  "devDependencies": {
108
+ "@webgpu/types": "^0.1.64",
98
109
  "@react-three/fiber": ">=8.0.0",
99
110
  "@rollup/plugin-commonjs": "^25.0.0",
100
111
  "@rollup/plugin-node-resolve": "^15.0.0",