three-text 0.2.12 → 0.2.13

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.12
2
+ * three-text v0.2.13
3
3
  * Copyright (C) 2025 Countertype LLC
4
4
  *
5
5
  * This program is free software: you can redistribute it and/or modify
@@ -2590,7 +2590,6 @@
2590
2590
  vertices.push(points[i], points[i + 1], 0);
2591
2591
  normals.push(0, 0, -1);
2592
2592
  }
2593
- // Add triangle indices
2594
2593
  for (let i = 0; i < triangleIndices.length; i++) {
2595
2594
  indices.push(baseIndex + triangleIndices[i]);
2596
2595
  }
@@ -2641,18 +2640,21 @@
2641
2640
  perfLogger.start('BoundaryClusterer.cluster', {
2642
2641
  glyphCount: glyphContoursList.length
2643
2642
  });
2644
- if (glyphContoursList.length === 0) {
2643
+ const n = glyphContoursList.length;
2644
+ if (n === 0) {
2645
2645
  perfLogger.end('BoundaryClusterer.cluster');
2646
2646
  return [];
2647
2647
  }
2648
+ if (n === 1) {
2649
+ perfLogger.end('BoundaryClusterer.cluster');
2650
+ return [[0]];
2651
+ }
2648
2652
  const result = this.clusterSweepLine(glyphContoursList, positions);
2649
2653
  perfLogger.end('BoundaryClusterer.cluster');
2650
2654
  return result;
2651
2655
  }
2652
2656
  clusterSweepLine(glyphContoursList, positions) {
2653
2657
  const n = glyphContoursList.length;
2654
- if (n <= 1)
2655
- return n === 0 ? [] : [[0]];
2656
2658
  const bounds = new Array(n);
2657
2659
  const events = new Array(2 * n);
2658
2660
  let eventIndex = 0;
@@ -2672,7 +2674,6 @@
2672
2674
  const py = find(y);
2673
2675
  if (px === py)
2674
2676
  return;
2675
- // Union by rank, attach smaller tree under larger tree
2676
2677
  if (rank[px] < rank[py]) {
2677
2678
  parent[px] = py;
2678
2679
  }
@@ -2688,8 +2689,6 @@
2688
2689
  for (const [, eventType, glyphIndex] of events) {
2689
2690
  if (eventType === 0) {
2690
2691
  const bounds1 = bounds[glyphIndex];
2691
- // Check y-overlap with all currently active glyphs
2692
- // (x-overlap is guaranteed by the sweep line)
2693
2692
  for (const activeIndex of active) {
2694
2693
  const bounds2 = bounds[activeIndex];
2695
2694
  if (bounds1.minY < bounds2.maxY + OVERLAP_EPSILON &&
@@ -2706,10 +2705,12 @@
2706
2705
  const clusters = new Map();
2707
2706
  for (let i = 0; i < n; i++) {
2708
2707
  const root = find(i);
2709
- if (!clusters.has(root)) {
2710
- clusters.set(root, []);
2708
+ let list = clusters.get(root);
2709
+ if (!list) {
2710
+ list = [];
2711
+ clusters.set(root, list);
2711
2712
  }
2712
- clusters.get(root).push(i);
2713
+ list.push(i);
2713
2714
  }
2714
2715
  return Array.from(clusters.values());
2715
2716
  }
@@ -3762,6 +3763,10 @@
3762
3763
  return size;
3763
3764
  }
3764
3765
  });
3766
+ this.clusteringCache = new LRUCache({
3767
+ maxEntries: 2000,
3768
+ calculateSize: () => 1
3769
+ });
3765
3770
  }
3766
3771
  getOptimizationStats() {
3767
3772
  return this.collector.getOptimizationStats();
@@ -3800,70 +3805,115 @@
3800
3805
  clusterGlyphContours.push(this.getContoursForGlyph(glyph.g));
3801
3806
  }
3802
3807
  // Step 2: Check for overlaps within the cluster
3803
- const relativePositions = cluster.glyphs.map((g) => new Vec3(g.x, g.y, 0));
3804
- const boundaryGroups = this.clusterer.cluster(clusterGlyphContours, relativePositions);
3808
+ let boundaryGroups;
3809
+ if (cluster.glyphs.length <= 1) {
3810
+ boundaryGroups = [[0]];
3811
+ }
3812
+ else {
3813
+ // Check clustering cache (same text + glyph IDs = same overlap groups)
3814
+ const cacheKey = cluster.text;
3815
+ const cached = this.clusteringCache.get(cacheKey);
3816
+ let isValid = false;
3817
+ if (cached && cached.glyphIds.length === cluster.glyphs.length) {
3818
+ isValid = true;
3819
+ for (let i = 0; i < cluster.glyphs.length; i++) {
3820
+ if (cached.glyphIds[i] !== cluster.glyphs[i].g) {
3821
+ isValid = false;
3822
+ break;
3823
+ }
3824
+ }
3825
+ }
3826
+ if (isValid && cached) {
3827
+ boundaryGroups = cached.groups;
3828
+ }
3829
+ else {
3830
+ const relativePositions = cluster.glyphs.map((g) => new Vec3(g.x, g.y, 0));
3831
+ boundaryGroups = this.clusterer.cluster(clusterGlyphContours, relativePositions);
3832
+ this.clusteringCache.set(cacheKey, {
3833
+ glyphIds: cluster.glyphs.map(g => g.g),
3834
+ groups: boundaryGroups
3835
+ });
3836
+ }
3837
+ }
3805
3838
  const clusterHasColoredGlyphs = coloredTextIndices &&
3806
3839
  cluster.glyphs.some((g) => coloredTextIndices.has(g.absoluteTextIndex));
3807
3840
  // Force glyph-level caching if:
3808
3841
  // - separateGlyphs flag is set (for shader attributes), OR
3809
3842
  // - cluster contains selectively colored text (needs separate vertex ranges per glyph)
3810
3843
  const forceSeparate = separateGlyphs || clusterHasColoredGlyphs;
3811
- const hasOverlaps = forceSeparate
3812
- ? false
3813
- : boundaryGroups.some((group) => group.length > 1);
3814
- if (hasOverlaps) {
3815
- // Cluster-level caching
3816
- const clusterKey = `${this.fontId}_${cluster.text}_${depth}_${removeOverlaps}`;
3817
- let cachedCluster = this.wordCache.get(clusterKey);
3818
- if (!cachedCluster) {
3819
- const clusterPaths = [];
3820
- for (let i = 0; i < clusterGlyphContours.length; i++) {
3821
- const glyphContours = clusterGlyphContours[i];
3822
- const glyph = cluster.glyphs[i];
3823
- for (const path of glyphContours.paths) {
3824
- clusterPaths.push({
3825
- ...path,
3826
- points: path.points.map((p) => new Vec2(p.x + (glyph.x ?? 0), p.y + (glyph.y ?? 0)))
3827
- });
3844
+ // Iterate over the geometric groups identified by BoundaryClusterer
3845
+ // logical groups (words) are now split into geometric sub-groups (e.g. "aa", "XX", "bb")
3846
+ for (const groupIndices of boundaryGroups) {
3847
+ const isOverlappingGroup = groupIndices.length > 1;
3848
+ const shouldCluster = isOverlappingGroup && !forceSeparate;
3849
+ if (shouldCluster) {
3850
+ // Cluster-level caching for this specific group of overlapping glyphs
3851
+ const subClusterGlyphs = groupIndices.map((i) => cluster.glyphs[i]);
3852
+ const clusterKey = this.getClusterKey(subClusterGlyphs, depth, removeOverlaps);
3853
+ let cachedCluster = this.wordCache.get(clusterKey);
3854
+ if (!cachedCluster) {
3855
+ const clusterPaths = [];
3856
+ const refX = subClusterGlyphs[0].x ?? 0;
3857
+ const refY = subClusterGlyphs[0].y ?? 0;
3858
+ for (let i = 0; i < groupIndices.length; i++) {
3859
+ const originalIndex = groupIndices[i];
3860
+ const glyphContours = clusterGlyphContours[originalIndex];
3861
+ const glyph = cluster.glyphs[originalIndex];
3862
+ // Position relative to the sub-cluster start
3863
+ const relX = (glyph.x ?? 0) - refX;
3864
+ const relY = (glyph.y ?? 0) - refY;
3865
+ for (const path of glyphContours.paths) {
3866
+ clusterPaths.push({
3867
+ ...path,
3868
+ points: path.points.map((p) => new Vec2(p.x + relX, p.y + relY))
3869
+ });
3870
+ }
3828
3871
  }
3872
+ cachedCluster = this.tessellateGlyphCluster(clusterPaths, depth, isCFF);
3873
+ this.wordCache.set(clusterKey, cachedCluster);
3829
3874
  }
3830
- cachedCluster = this.tessellateGlyphCluster(clusterPaths, depth, isCFF);
3831
- this.wordCache.set(clusterKey, cachedCluster);
3832
- }
3833
- const vertexOffset = vertices.length / 3;
3834
- this.appendGeometry(vertices, normals, indices, cachedCluster, cluster.position, vertexOffset);
3835
- const clusterVertexCount = cachedCluster.vertices.length / 3;
3836
- for (let i = 0; i < cluster.glyphs.length; i++) {
3837
- const glyph = cluster.glyphs[i];
3838
- const glyphContours = clusterGlyphContours[i];
3839
- const absoluteGlyphPosition = new Vec3(cluster.position.x + (glyph.x ?? 0), cluster.position.y + (glyph.y ?? 0), cluster.position.z);
3840
- const glyphInfo = this.createGlyphInfo(glyph, vertexOffset, clusterVertexCount, absoluteGlyphPosition, glyphContours, depth);
3841
- glyphInfos.push(glyphInfo);
3842
- this.updatePlaneBounds(glyphInfo.bounds, planeBounds);
3843
- }
3844
- }
3845
- else {
3846
- // Glyph-level caching
3847
- for (let i = 0; i < cluster.glyphs.length; i++) {
3848
- const glyph = cluster.glyphs[i];
3849
- const glyphContours = clusterGlyphContours[i];
3850
- const glyphPosition = new Vec3(cluster.position.x + (glyph.x ?? 0), cluster.position.y + (glyph.y ?? 0), cluster.position.z);
3851
- // Skip glyphs with no paths (spaces, zero-width characters, etc.)
3852
- if (glyphContours.paths.length === 0) {
3853
- const glyphInfo = this.createGlyphInfo(glyph, 0, 0, glyphPosition, glyphContours, depth);
3875
+ // Calculate the absolute position of this sub-cluster based on its first glyph
3876
+ // (since the cached geometry is relative to that first glyph)
3877
+ const firstGlyphInGroup = subClusterGlyphs[0];
3878
+ const groupPosition = new Vec3(cluster.position.x + (firstGlyphInGroup.x ?? 0), cluster.position.y + (firstGlyphInGroup.y ?? 0), cluster.position.z);
3879
+ const vertexOffset = vertices.length / 3;
3880
+ this.appendGeometry(vertices, normals, indices, cachedCluster, groupPosition, vertexOffset);
3881
+ const clusterVertexCount = cachedCluster.vertices.length / 3;
3882
+ // Register glyph infos for all glyphs in this sub-cluster
3883
+ // They all point to the same merged geometry
3884
+ for (let i = 0; i < groupIndices.length; i++) {
3885
+ const originalIndex = groupIndices[i];
3886
+ const glyph = cluster.glyphs[originalIndex];
3887
+ const glyphContours = clusterGlyphContours[originalIndex];
3888
+ const absoluteGlyphPosition = new Vec3(cluster.position.x + (glyph.x ?? 0), cluster.position.y + (glyph.y ?? 0), cluster.position.z);
3889
+ const glyphInfo = this.createGlyphInfo(glyph, vertexOffset, clusterVertexCount, absoluteGlyphPosition, glyphContours, depth);
3854
3890
  glyphInfos.push(glyphInfo);
3855
- continue;
3891
+ this.updatePlaneBounds(glyphInfo.bounds, planeBounds);
3856
3892
  }
3857
- let cachedGlyph = this.cache.get(this.fontId, glyph.g, depth, removeOverlaps);
3858
- if (!cachedGlyph) {
3859
- cachedGlyph = this.tessellateGlyph(glyphContours, depth, removeOverlaps, isCFF);
3860
- this.cache.set(this.fontId, glyph.g, depth, removeOverlaps, cachedGlyph);
3893
+ }
3894
+ else {
3895
+ // Glyph-level caching (standard path for isolated glyphs or when forced separate)
3896
+ for (const i of groupIndices) {
3897
+ const glyph = cluster.glyphs[i];
3898
+ const glyphContours = clusterGlyphContours[i];
3899
+ const glyphPosition = new Vec3(cluster.position.x + (glyph.x ?? 0), cluster.position.y + (glyph.y ?? 0), cluster.position.z);
3900
+ // Skip glyphs with no paths (spaces, zero-width characters, etc.)
3901
+ if (glyphContours.paths.length === 0) {
3902
+ const glyphInfo = this.createGlyphInfo(glyph, 0, 0, glyphPosition, glyphContours, depth);
3903
+ glyphInfos.push(glyphInfo);
3904
+ continue;
3905
+ }
3906
+ let cachedGlyph = this.cache.get(this.fontId, glyph.g, depth, removeOverlaps);
3907
+ if (!cachedGlyph) {
3908
+ cachedGlyph = this.tessellateGlyph(glyphContours, depth, removeOverlaps, isCFF);
3909
+ this.cache.set(this.fontId, glyph.g, depth, removeOverlaps, cachedGlyph);
3910
+ }
3911
+ const vertexOffset = vertices.length / 3;
3912
+ this.appendGeometry(vertices, normals, indices, cachedGlyph, glyphPosition, vertexOffset);
3913
+ const glyphInfo = this.createGlyphInfo(glyph, vertexOffset, cachedGlyph.vertices.length / 3, glyphPosition, glyphContours, depth);
3914
+ glyphInfos.push(glyphInfo);
3915
+ this.updatePlaneBounds(glyphInfo.bounds, planeBounds);
3861
3916
  }
3862
- const vertexOffset = vertices.length / 3;
3863
- this.appendGeometry(vertices, normals, indices, cachedGlyph, glyphPosition, vertexOffset);
3864
- const glyphInfo = this.createGlyphInfo(glyph, vertexOffset, cachedGlyph.vertices.length / 3, glyphPosition, glyphContours, depth);
3865
- glyphInfos.push(glyphInfo);
3866
- this.updatePlaneBounds(glyphInfo.bounds, planeBounds);
3867
3917
  }
3868
3918
  }
3869
3919
  }
@@ -3880,6 +3930,21 @@
3880
3930
  planeBounds
3881
3931
  };
3882
3932
  }
3933
+ getClusterKey(glyphs, depth, removeOverlaps) {
3934
+ if (glyphs.length === 0)
3935
+ return '';
3936
+ // Normalize positions relative to the first glyph in the cluster
3937
+ const refX = glyphs[0].x ?? 0;
3938
+ const refY = glyphs[0].y ?? 0;
3939
+ const parts = glyphs.map((g) => {
3940
+ const relX = (g.x ?? 0) - refX;
3941
+ const relY = (g.y ?? 0) - refY;
3942
+ return `${g.g}:${relX},${relY}`;
3943
+ });
3944
+ const ids = parts.join('|');
3945
+ const roundedDepth = Math.round(depth * 1000) / 1000;
3946
+ return `${this.fontId}_${ids}_${roundedDepth}_${removeOverlaps}`;
3947
+ }
3883
3948
  appendGeometry(vertices, normals, indices, data, position, offset) {
3884
3949
  for (let j = 0; j < data.vertices.length; j += 3) {
3885
3950
  vertices.push(data.vertices[j] + position.x, data.vertices[j + 1] + position.y, data.vertices[j + 2] + position.z);
@@ -4004,6 +4069,7 @@
4004
4069
  clearCache() {
4005
4070
  this.cache.clear();
4006
4071
  this.wordCache.clear();
4072
+ this.clusteringCache.clear();
4007
4073
  }
4008
4074
  }
4009
4075