three-text 0.2.11 → 0.2.12

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.11
2
+ * three-text v0.2.12
3
3
  * Copyright (C) 2025 Countertype LLC
4
4
  *
5
5
  * This program is free software: you can redistribute it and/or modify
@@ -3776,7 +3776,7 @@
3776
3776
  this.fontId = fontId;
3777
3777
  }
3778
3778
  // Build instanced geometry from glyph contours
3779
- buildInstancedGeometry(clustersByLine, depth, removeOverlaps, isCFF, separateGlyphs = false) {
3779
+ buildInstancedGeometry(clustersByLine, depth, removeOverlaps, isCFF, separateGlyphs = false, coloredTextIndices) {
3780
3780
  perfLogger.start('GlyphGeometryBuilder.buildInstancedGeometry', {
3781
3781
  lineCount: clustersByLine.length,
3782
3782
  wordCount: clustersByLine.flat().length,
@@ -3802,7 +3802,13 @@
3802
3802
  // Step 2: Check for overlaps within the cluster
3803
3803
  const relativePositions = cluster.glyphs.map((g) => new Vec3(g.x, g.y, 0));
3804
3804
  const boundaryGroups = this.clusterer.cluster(clusterGlyphContours, relativePositions);
3805
- const hasOverlaps = separateGlyphs
3805
+ const clusterHasColoredGlyphs = coloredTextIndices &&
3806
+ cluster.glyphs.some((g) => coloredTextIndices.has(g.absoluteTextIndex));
3807
+ // Force glyph-level caching if:
3808
+ // - separateGlyphs flag is set (for shader attributes), OR
3809
+ // - cluster contains selectively colored text (needs separate vertex ranges per glyph)
3810
+ const forceSeparate = separateGlyphs || clusterHasColoredGlyphs;
3811
+ const hasOverlaps = forceSeparate
3806
3812
  ? false
3807
3813
  : boundaryGroups.some((group) => group.length > 1);
3808
3814
  if (hasOverlaps) {
@@ -4012,30 +4018,14 @@
4012
4018
  perfLogger.start('TextShaper.shapeLines', {
4013
4019
  lineCount: lineInfos.length
4014
4020
  });
4015
- // Calculate color boundaries once for the entire text before line processing
4016
- const colorBoundaries = new Set();
4017
- if (color &&
4018
- typeof color === 'object' &&
4019
- 'byText' in color &&
4020
- color.byText &&
4021
- originalText) {
4022
- for (const textToColor of Object.keys(color.byText)) {
4023
- let index = 0;
4024
- while ((index = originalText.indexOf(textToColor, index)) !== -1) {
4025
- colorBoundaries.add(index);
4026
- colorBoundaries.add(index + textToColor.length);
4027
- index += textToColor.length;
4028
- }
4029
- }
4030
- }
4031
4021
  const clustersByLine = [];
4032
4022
  lineInfos.forEach((lineInfo, lineIndex) => {
4033
- const clusters = this.shapeLineIntoClusters(lineInfo, lineIndex, scaledLineHeight, letterSpacing, align, direction, colorBoundaries);
4023
+ const clusters = this.shapeLineIntoClusters(lineInfo, lineIndex, scaledLineHeight, letterSpacing, align, direction);
4034
4024
  clustersByLine.push(clusters);
4035
4025
  });
4036
4026
  return clustersByLine;
4037
4027
  }
4038
- shapeLineIntoClusters(lineInfo, lineIndex, scaledLineHeight, letterSpacing, align, direction, colorBoundaries) {
4028
+ shapeLineIntoClusters(lineInfo, lineIndex, scaledLineHeight, letterSpacing, align, direction) {
4039
4029
  const buffer = this.loadedFont.hb.createBuffer();
4040
4030
  if (direction === 'rtl') {
4041
4031
  buffer.setDirection('rtl');
@@ -4068,8 +4058,9 @@
4068
4058
  glyph.absoluteTextIndex = lineInfo.originalStart + glyph.cl;
4069
4059
  }
4070
4060
  glyph.lineIndex = lineIndex;
4071
- const isBoundary = colorBoundaries.has(glyph.absoluteTextIndex);
4072
- if (isWhitespace || isBoundary) {
4061
+ // Cluster boundaries are based on whitespace only.
4062
+ // Coloring is applied later via vertex colors and must never affect shaping/kerning.
4063
+ if (isWhitespace) {
4073
4064
  if (currentClusterGlyphs.length > 0) {
4074
4065
  clusters.push({
4075
4066
  text: currentClusterText,
@@ -5256,7 +5247,35 @@
5256
5247
  // Allow manual override via options.removeOverlaps
5257
5248
  const shouldRemoveOverlaps = options.removeOverlaps ?? this.loadedFont.isVariable ?? false;
5258
5249
  const clustersByLine = this.textShaper.shapeLines(layoutData.lines, layoutData.scaledLineHeight, layoutData.letterSpacing, layoutData.align, layoutData.direction, options.color, options.text);
5259
- const shapedResult = this.geometryBuilder.buildInstancedGeometry(clustersByLine, layoutData.depth, shouldRemoveOverlaps, this.loadedFont.metrics.isCFF, options.separateGlyphsWithAttributes || false);
5250
+ // Pre-compute which character indices will be colored. This allows geometry building
5251
+ // to selectively use glyph-level caching (separate vertices) only for clusters containing
5252
+ // colored text, while non-colored clusters can still use fast cluster-level merging
5253
+ let coloredTextIndices;
5254
+ if (options.color && typeof options.color === 'object' && !Array.isArray(options.color)) {
5255
+ if (options.color.byText || options.color.byCharRange) {
5256
+ // Build the set manually since glyphs don't exist yet
5257
+ coloredTextIndices = new Set();
5258
+ if (options.color.byText) {
5259
+ for (const pattern of Object.keys(options.color.byText)) {
5260
+ let index = 0;
5261
+ while ((index = options.text.indexOf(pattern, index)) !== -1) {
5262
+ for (let i = index; i < index + pattern.length; i++) {
5263
+ coloredTextIndices.add(i);
5264
+ }
5265
+ index += pattern.length;
5266
+ }
5267
+ }
5268
+ }
5269
+ if (options.color.byCharRange) {
5270
+ for (const range of options.color.byCharRange) {
5271
+ for (let i = range.start; i < range.end; i++) {
5272
+ coloredTextIndices.add(i);
5273
+ }
5274
+ }
5275
+ }
5276
+ }
5277
+ }
5278
+ const shapedResult = this.geometryBuilder.buildInstancedGeometry(clustersByLine, layoutData.depth, shouldRemoveOverlaps, this.loadedFont.metrics.isCFF, options.separateGlyphsWithAttributes || false, coloredTextIndices);
5260
5279
  const cacheStats = this.geometryBuilder.getCacheStats();
5261
5280
  const result = this.finalizeGeometry(shapedResult.vertices, shapedResult.normals, shapedResult.indices, shapedResult.glyphInfos, shapedResult.planeBounds, options, cacheStats, options.text);
5262
5281
  if (options.separateGlyphsWithAttributes) {