three-text 0.3.4 → 0.3.5

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
@@ -637,6 +637,8 @@ const text = await Text.create({
637
637
 
638
638
  Text matching occurs after layout processing, so patterns like "connection" will be found even if hyphenation splits them across lines. The `coloredRanges` property on the returned object contains the resolved color assignments for programmatic access to the colored parts of the geometry
639
639
 
640
+ When using selective coloring with `byText` or `byCharRange`, colored glyphs are kept geometrically separate from adjacent non-colored glyphs. This ensures accurate vertex coloring while still allowing overlap removal between glyphs of the same color status, e.g. two adjacent colored letters that overlap will still be properly merged
641
+
640
642
  ## API reference
641
643
 
642
644
  The library's full TypeScript definitions are the most complete source of truth for the API. The core data structures and configuration options can be found in `src/core/types.ts`
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * three-text v0.3.4
2
+ * three-text v0.3.5
3
3
  * Copyright (C) 2025 Countertype LLC
4
4
  *
5
5
  * This program is free software: you can redistribute it and/or modify
@@ -3902,15 +3902,20 @@ class GlyphGeometryBuilder {
3902
3902
  boundaryGroups = [[0]];
3903
3903
  }
3904
3904
  else {
3905
- // Check clustering cache (same text + glyph IDs = same overlap groups)
3905
+ // Check clustering cache (same text + glyph IDs + positions = same overlap groups)
3906
3906
  // Key must be font-specific; glyph ids/bounds differ between fonts
3907
+ // Positions must match since overlap detection depends on relative glyph placement
3907
3908
  const cacheKey = `${this.cacheKeyPrefix}_${cluster.text}`;
3908
3909
  const cached = this.clusteringCache.get(cacheKey);
3909
3910
  let isValid = false;
3910
3911
  if (cached && cached.glyphIds.length === cluster.glyphs.length) {
3911
3912
  isValid = true;
3912
3913
  for (let i = 0; i < cluster.glyphs.length; i++) {
3913
- if (cached.glyphIds[i] !== cluster.glyphs[i].g) {
3914
+ const glyph = cluster.glyphs[i];
3915
+ const cachedPos = cached.positions[i];
3916
+ if (cached.glyphIds[i] !== glyph.g ||
3917
+ cachedPos.x !== (glyph.x ?? 0) ||
3918
+ cachedPos.y !== (glyph.y ?? 0)) {
3914
3919
  isValid = false;
3915
3920
  break;
3916
3921
  }
@@ -3924,17 +3929,52 @@ class GlyphGeometryBuilder {
3924
3929
  boundaryGroups = this.clusterer.cluster(clusterGlyphContours, relativePositions);
3925
3930
  this.clusteringCache.set(cacheKey, {
3926
3931
  glyphIds: cluster.glyphs.map((g) => g.g),
3932
+ positions: cluster.glyphs.map((g) => ({
3933
+ x: g.x ?? 0,
3934
+ y: g.y ?? 0
3935
+ })),
3927
3936
  groups: boundaryGroups
3928
3937
  });
3929
3938
  }
3930
3939
  }
3931
- const clusterHasColoredGlyphs = coloredTextIndices &&
3932
- cluster.glyphs.some((g) => coloredTextIndices.has(g.absoluteTextIndex));
3933
- // Use glyph-level caching when separateGlyphs is set or when cluster contains colored text
3934
- const forceSeparate = separateGlyphs || clusterHasColoredGlyphs;
3940
+ // Only force separate tessellation when explicitly requested via separateGlyphs
3941
+ const forceSeparate = separateGlyphs;
3942
+ // Split boundary groups so colored and non-colored glyphs don't merge together
3943
+ // This preserves overlap removal within each color class while keeping
3944
+ // geometry separate for accurate vertex coloring
3945
+ let finalGroups = boundaryGroups;
3946
+ if (coloredTextIndices && coloredTextIndices.size > 0) {
3947
+ finalGroups = [];
3948
+ for (const group of boundaryGroups) {
3949
+ if (group.length <= 1) {
3950
+ finalGroups.push(group);
3951
+ }
3952
+ else {
3953
+ // Split group into colored and non-colored sub-groups
3954
+ const coloredIndices = [];
3955
+ const nonColoredIndices = [];
3956
+ for (const idx of group) {
3957
+ const glyph = cluster.glyphs[idx];
3958
+ if (coloredTextIndices.has(glyph.absoluteTextIndex)) {
3959
+ coloredIndices.push(idx);
3960
+ }
3961
+ else {
3962
+ nonColoredIndices.push(idx);
3963
+ }
3964
+ }
3965
+ // Add non-empty sub-groups
3966
+ if (coloredIndices.length > 0) {
3967
+ finalGroups.push(coloredIndices);
3968
+ }
3969
+ if (nonColoredIndices.length > 0) {
3970
+ finalGroups.push(nonColoredIndices);
3971
+ }
3972
+ }
3973
+ }
3974
+ }
3935
3975
  // Iterate over the geometric groups identified by BoundaryClusterer
3936
3976
  // logical groups (words) split into geometric sub-groups
3937
- for (const groupIndices of boundaryGroups) {
3977
+ for (const groupIndices of finalGroups) {
3938
3978
  const isOverlappingGroup = groupIndices.length > 1;
3939
3979
  const shouldCluster = isOverlappingGroup && !forceSeparate;
3940
3980
  if (shouldCluster) {
@@ -5595,6 +5635,7 @@ class Text {
5595
5635
  else {
5596
5636
  lineGroups.set(glyph.lineIndex, [glyph]);
5597
5637
  }
5638
+ // Color vertices owned by this glyph
5598
5639
  for (let v = 0; v < glyph.vertexCount; v++) {
5599
5640
  const vertexIndex = (glyph.vertexStart + v) * 3;
5600
5641
  if (vertexIndex >= 0 && vertexIndex < colors.length) {
@@ -5636,6 +5677,7 @@ class Text {
5636
5677
  else {
5637
5678
  lineGroups.set(glyph.lineIndex, [glyph]);
5638
5679
  }
5680
+ // Color vertices owned by this glyph
5639
5681
  for (let v = 0; v < glyph.vertexCount; v++) {
5640
5682
  const vertexIndex = (glyph.vertexStart + v) * 3;
5641
5683
  if (vertexIndex >= 0 && vertexIndex < colors.length) {
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * three-text v0.3.4
2
+ * three-text v0.3.5
3
3
  * Copyright (C) 2025 Countertype LLC
4
4
  *
5
5
  * This program is free software: you can redistribute it and/or modify
@@ -3899,15 +3899,20 @@ class GlyphGeometryBuilder {
3899
3899
  boundaryGroups = [[0]];
3900
3900
  }
3901
3901
  else {
3902
- // Check clustering cache (same text + glyph IDs = same overlap groups)
3902
+ // Check clustering cache (same text + glyph IDs + positions = same overlap groups)
3903
3903
  // Key must be font-specific; glyph ids/bounds differ between fonts
3904
+ // Positions must match since overlap detection depends on relative glyph placement
3904
3905
  const cacheKey = `${this.cacheKeyPrefix}_${cluster.text}`;
3905
3906
  const cached = this.clusteringCache.get(cacheKey);
3906
3907
  let isValid = false;
3907
3908
  if (cached && cached.glyphIds.length === cluster.glyphs.length) {
3908
3909
  isValid = true;
3909
3910
  for (let i = 0; i < cluster.glyphs.length; i++) {
3910
- if (cached.glyphIds[i] !== cluster.glyphs[i].g) {
3911
+ const glyph = cluster.glyphs[i];
3912
+ const cachedPos = cached.positions[i];
3913
+ if (cached.glyphIds[i] !== glyph.g ||
3914
+ cachedPos.x !== (glyph.x ?? 0) ||
3915
+ cachedPos.y !== (glyph.y ?? 0)) {
3911
3916
  isValid = false;
3912
3917
  break;
3913
3918
  }
@@ -3921,17 +3926,52 @@ class GlyphGeometryBuilder {
3921
3926
  boundaryGroups = this.clusterer.cluster(clusterGlyphContours, relativePositions);
3922
3927
  this.clusteringCache.set(cacheKey, {
3923
3928
  glyphIds: cluster.glyphs.map((g) => g.g),
3929
+ positions: cluster.glyphs.map((g) => ({
3930
+ x: g.x ?? 0,
3931
+ y: g.y ?? 0
3932
+ })),
3924
3933
  groups: boundaryGroups
3925
3934
  });
3926
3935
  }
3927
3936
  }
3928
- const clusterHasColoredGlyphs = coloredTextIndices &&
3929
- cluster.glyphs.some((g) => coloredTextIndices.has(g.absoluteTextIndex));
3930
- // Use glyph-level caching when separateGlyphs is set or when cluster contains colored text
3931
- const forceSeparate = separateGlyphs || clusterHasColoredGlyphs;
3937
+ // Only force separate tessellation when explicitly requested via separateGlyphs
3938
+ const forceSeparate = separateGlyphs;
3939
+ // Split boundary groups so colored and non-colored glyphs don't merge together
3940
+ // This preserves overlap removal within each color class while keeping
3941
+ // geometry separate for accurate vertex coloring
3942
+ let finalGroups = boundaryGroups;
3943
+ if (coloredTextIndices && coloredTextIndices.size > 0) {
3944
+ finalGroups = [];
3945
+ for (const group of boundaryGroups) {
3946
+ if (group.length <= 1) {
3947
+ finalGroups.push(group);
3948
+ }
3949
+ else {
3950
+ // Split group into colored and non-colored sub-groups
3951
+ const coloredIndices = [];
3952
+ const nonColoredIndices = [];
3953
+ for (const idx of group) {
3954
+ const glyph = cluster.glyphs[idx];
3955
+ if (coloredTextIndices.has(glyph.absoluteTextIndex)) {
3956
+ coloredIndices.push(idx);
3957
+ }
3958
+ else {
3959
+ nonColoredIndices.push(idx);
3960
+ }
3961
+ }
3962
+ // Add non-empty sub-groups
3963
+ if (coloredIndices.length > 0) {
3964
+ finalGroups.push(coloredIndices);
3965
+ }
3966
+ if (nonColoredIndices.length > 0) {
3967
+ finalGroups.push(nonColoredIndices);
3968
+ }
3969
+ }
3970
+ }
3971
+ }
3932
3972
  // Iterate over the geometric groups identified by BoundaryClusterer
3933
3973
  // logical groups (words) split into geometric sub-groups
3934
- for (const groupIndices of boundaryGroups) {
3974
+ for (const groupIndices of finalGroups) {
3935
3975
  const isOverlappingGroup = groupIndices.length > 1;
3936
3976
  const shouldCluster = isOverlappingGroup && !forceSeparate;
3937
3977
  if (shouldCluster) {
@@ -5592,6 +5632,7 @@ class Text {
5592
5632
  else {
5593
5633
  lineGroups.set(glyph.lineIndex, [glyph]);
5594
5634
  }
5635
+ // Color vertices owned by this glyph
5595
5636
  for (let v = 0; v < glyph.vertexCount; v++) {
5596
5637
  const vertexIndex = (glyph.vertexStart + v) * 3;
5597
5638
  if (vertexIndex >= 0 && vertexIndex < colors.length) {
@@ -5633,6 +5674,7 @@ class Text {
5633
5674
  else {
5634
5675
  lineGroups.set(glyph.lineIndex, [glyph]);
5635
5676
  }
5677
+ // Color vertices owned by this glyph
5636
5678
  for (let v = 0; v < glyph.vertexCount; v++) {
5637
5679
  const vertexIndex = (glyph.vertexStart + v) * 3;
5638
5680
  if (vertexIndex >= 0 && vertexIndex < colors.length) {