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.cjs +43 -24
- package/dist/index.js +43 -24
- package/dist/index.min.cjs +2 -2
- package/dist/index.min.js +2 -2
- package/dist/index.umd.js +43 -24
- package/dist/index.umd.min.js +2 -2
- package/dist/types/core/cache/GlyphGeometryBuilder.d.ts +1 -1
- package/package.json +2 -1
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* three-text v0.2.
|
|
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
|
|
@@ -3772,7 +3772,7 @@ class GlyphGeometryBuilder {
|
|
|
3772
3772
|
this.fontId = fontId;
|
|
3773
3773
|
}
|
|
3774
3774
|
// Build instanced geometry from glyph contours
|
|
3775
|
-
buildInstancedGeometry(clustersByLine, depth, removeOverlaps, isCFF, separateGlyphs = false) {
|
|
3775
|
+
buildInstancedGeometry(clustersByLine, depth, removeOverlaps, isCFF, separateGlyphs = false, coloredTextIndices) {
|
|
3776
3776
|
perfLogger.start('GlyphGeometryBuilder.buildInstancedGeometry', {
|
|
3777
3777
|
lineCount: clustersByLine.length,
|
|
3778
3778
|
wordCount: clustersByLine.flat().length,
|
|
@@ -3798,7 +3798,13 @@ class GlyphGeometryBuilder {
|
|
|
3798
3798
|
// Step 2: Check for overlaps within the cluster
|
|
3799
3799
|
const relativePositions = cluster.glyphs.map((g) => new Vec3(g.x, g.y, 0));
|
|
3800
3800
|
const boundaryGroups = this.clusterer.cluster(clusterGlyphContours, relativePositions);
|
|
3801
|
-
const
|
|
3801
|
+
const clusterHasColoredGlyphs = coloredTextIndices &&
|
|
3802
|
+
cluster.glyphs.some((g) => coloredTextIndices.has(g.absoluteTextIndex));
|
|
3803
|
+
// Force glyph-level caching if:
|
|
3804
|
+
// - separateGlyphs flag is set (for shader attributes), OR
|
|
3805
|
+
// - cluster contains selectively colored text (needs separate vertex ranges per glyph)
|
|
3806
|
+
const forceSeparate = separateGlyphs || clusterHasColoredGlyphs;
|
|
3807
|
+
const hasOverlaps = forceSeparate
|
|
3802
3808
|
? false
|
|
3803
3809
|
: boundaryGroups.some((group) => group.length > 1);
|
|
3804
3810
|
if (hasOverlaps) {
|
|
@@ -4008,30 +4014,14 @@ class TextShaper {
|
|
|
4008
4014
|
perfLogger.start('TextShaper.shapeLines', {
|
|
4009
4015
|
lineCount: lineInfos.length
|
|
4010
4016
|
});
|
|
4011
|
-
// Calculate color boundaries once for the entire text before line processing
|
|
4012
|
-
const colorBoundaries = new Set();
|
|
4013
|
-
if (color &&
|
|
4014
|
-
typeof color === 'object' &&
|
|
4015
|
-
'byText' in color &&
|
|
4016
|
-
color.byText &&
|
|
4017
|
-
originalText) {
|
|
4018
|
-
for (const textToColor of Object.keys(color.byText)) {
|
|
4019
|
-
let index = 0;
|
|
4020
|
-
while ((index = originalText.indexOf(textToColor, index)) !== -1) {
|
|
4021
|
-
colorBoundaries.add(index);
|
|
4022
|
-
colorBoundaries.add(index + textToColor.length);
|
|
4023
|
-
index += textToColor.length;
|
|
4024
|
-
}
|
|
4025
|
-
}
|
|
4026
|
-
}
|
|
4027
4017
|
const clustersByLine = [];
|
|
4028
4018
|
lineInfos.forEach((lineInfo, lineIndex) => {
|
|
4029
|
-
const clusters = this.shapeLineIntoClusters(lineInfo, lineIndex, scaledLineHeight, letterSpacing, align, direction
|
|
4019
|
+
const clusters = this.shapeLineIntoClusters(lineInfo, lineIndex, scaledLineHeight, letterSpacing, align, direction);
|
|
4030
4020
|
clustersByLine.push(clusters);
|
|
4031
4021
|
});
|
|
4032
4022
|
return clustersByLine;
|
|
4033
4023
|
}
|
|
4034
|
-
shapeLineIntoClusters(lineInfo, lineIndex, scaledLineHeight, letterSpacing, align, direction
|
|
4024
|
+
shapeLineIntoClusters(lineInfo, lineIndex, scaledLineHeight, letterSpacing, align, direction) {
|
|
4035
4025
|
const buffer = this.loadedFont.hb.createBuffer();
|
|
4036
4026
|
if (direction === 'rtl') {
|
|
4037
4027
|
buffer.setDirection('rtl');
|
|
@@ -4064,8 +4054,9 @@ class TextShaper {
|
|
|
4064
4054
|
glyph.absoluteTextIndex = lineInfo.originalStart + glyph.cl;
|
|
4065
4055
|
}
|
|
4066
4056
|
glyph.lineIndex = lineIndex;
|
|
4067
|
-
|
|
4068
|
-
|
|
4057
|
+
// Cluster boundaries are based on whitespace only.
|
|
4058
|
+
// Coloring is applied later via vertex colors and must never affect shaping/kerning.
|
|
4059
|
+
if (isWhitespace) {
|
|
4069
4060
|
if (currentClusterGlyphs.length > 0) {
|
|
4070
4061
|
clusters.push({
|
|
4071
4062
|
text: currentClusterText,
|
|
@@ -5252,7 +5243,35 @@ class Text {
|
|
|
5252
5243
|
// Allow manual override via options.removeOverlaps
|
|
5253
5244
|
const shouldRemoveOverlaps = options.removeOverlaps ?? this.loadedFont.isVariable ?? false;
|
|
5254
5245
|
const clustersByLine = this.textShaper.shapeLines(layoutData.lines, layoutData.scaledLineHeight, layoutData.letterSpacing, layoutData.align, layoutData.direction, options.color, options.text);
|
|
5255
|
-
|
|
5246
|
+
// Pre-compute which character indices will be colored. This allows geometry building
|
|
5247
|
+
// to selectively use glyph-level caching (separate vertices) only for clusters containing
|
|
5248
|
+
// colored text, while non-colored clusters can still use fast cluster-level merging
|
|
5249
|
+
let coloredTextIndices;
|
|
5250
|
+
if (options.color && typeof options.color === 'object' && !Array.isArray(options.color)) {
|
|
5251
|
+
if (options.color.byText || options.color.byCharRange) {
|
|
5252
|
+
// Build the set manually since glyphs don't exist yet
|
|
5253
|
+
coloredTextIndices = new Set();
|
|
5254
|
+
if (options.color.byText) {
|
|
5255
|
+
for (const pattern of Object.keys(options.color.byText)) {
|
|
5256
|
+
let index = 0;
|
|
5257
|
+
while ((index = options.text.indexOf(pattern, index)) !== -1) {
|
|
5258
|
+
for (let i = index; i < index + pattern.length; i++) {
|
|
5259
|
+
coloredTextIndices.add(i);
|
|
5260
|
+
}
|
|
5261
|
+
index += pattern.length;
|
|
5262
|
+
}
|
|
5263
|
+
}
|
|
5264
|
+
}
|
|
5265
|
+
if (options.color.byCharRange) {
|
|
5266
|
+
for (const range of options.color.byCharRange) {
|
|
5267
|
+
for (let i = range.start; i < range.end; i++) {
|
|
5268
|
+
coloredTextIndices.add(i);
|
|
5269
|
+
}
|
|
5270
|
+
}
|
|
5271
|
+
}
|
|
5272
|
+
}
|
|
5273
|
+
}
|
|
5274
|
+
const shapedResult = this.geometryBuilder.buildInstancedGeometry(clustersByLine, layoutData.depth, shouldRemoveOverlaps, this.loadedFont.metrics.isCFF, options.separateGlyphsWithAttributes || false, coloredTextIndices);
|
|
5256
5275
|
const cacheStats = this.geometryBuilder.getCacheStats();
|
|
5257
5276
|
const result = this.finalizeGeometry(shapedResult.vertices, shapedResult.normals, shapedResult.indices, shapedResult.glyphInfos, shapedResult.planeBounds, options, cacheStats, options.text);
|
|
5258
5277
|
if (options.separateGlyphsWithAttributes) {
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* three-text v0.2.
|
|
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
|
|
@@ -3769,7 +3769,7 @@ class GlyphGeometryBuilder {
|
|
|
3769
3769
|
this.fontId = fontId;
|
|
3770
3770
|
}
|
|
3771
3771
|
// Build instanced geometry from glyph contours
|
|
3772
|
-
buildInstancedGeometry(clustersByLine, depth, removeOverlaps, isCFF, separateGlyphs = false) {
|
|
3772
|
+
buildInstancedGeometry(clustersByLine, depth, removeOverlaps, isCFF, separateGlyphs = false, coloredTextIndices) {
|
|
3773
3773
|
perfLogger.start('GlyphGeometryBuilder.buildInstancedGeometry', {
|
|
3774
3774
|
lineCount: clustersByLine.length,
|
|
3775
3775
|
wordCount: clustersByLine.flat().length,
|
|
@@ -3795,7 +3795,13 @@ class GlyphGeometryBuilder {
|
|
|
3795
3795
|
// Step 2: Check for overlaps within the cluster
|
|
3796
3796
|
const relativePositions = cluster.glyphs.map((g) => new Vec3(g.x, g.y, 0));
|
|
3797
3797
|
const boundaryGroups = this.clusterer.cluster(clusterGlyphContours, relativePositions);
|
|
3798
|
-
const
|
|
3798
|
+
const clusterHasColoredGlyphs = coloredTextIndices &&
|
|
3799
|
+
cluster.glyphs.some((g) => coloredTextIndices.has(g.absoluteTextIndex));
|
|
3800
|
+
// Force glyph-level caching if:
|
|
3801
|
+
// - separateGlyphs flag is set (for shader attributes), OR
|
|
3802
|
+
// - cluster contains selectively colored text (needs separate vertex ranges per glyph)
|
|
3803
|
+
const forceSeparate = separateGlyphs || clusterHasColoredGlyphs;
|
|
3804
|
+
const hasOverlaps = forceSeparate
|
|
3799
3805
|
? false
|
|
3800
3806
|
: boundaryGroups.some((group) => group.length > 1);
|
|
3801
3807
|
if (hasOverlaps) {
|
|
@@ -4005,30 +4011,14 @@ class TextShaper {
|
|
|
4005
4011
|
perfLogger.start('TextShaper.shapeLines', {
|
|
4006
4012
|
lineCount: lineInfos.length
|
|
4007
4013
|
});
|
|
4008
|
-
// Calculate color boundaries once for the entire text before line processing
|
|
4009
|
-
const colorBoundaries = new Set();
|
|
4010
|
-
if (color &&
|
|
4011
|
-
typeof color === 'object' &&
|
|
4012
|
-
'byText' in color &&
|
|
4013
|
-
color.byText &&
|
|
4014
|
-
originalText) {
|
|
4015
|
-
for (const textToColor of Object.keys(color.byText)) {
|
|
4016
|
-
let index = 0;
|
|
4017
|
-
while ((index = originalText.indexOf(textToColor, index)) !== -1) {
|
|
4018
|
-
colorBoundaries.add(index);
|
|
4019
|
-
colorBoundaries.add(index + textToColor.length);
|
|
4020
|
-
index += textToColor.length;
|
|
4021
|
-
}
|
|
4022
|
-
}
|
|
4023
|
-
}
|
|
4024
4014
|
const clustersByLine = [];
|
|
4025
4015
|
lineInfos.forEach((lineInfo, lineIndex) => {
|
|
4026
|
-
const clusters = this.shapeLineIntoClusters(lineInfo, lineIndex, scaledLineHeight, letterSpacing, align, direction
|
|
4016
|
+
const clusters = this.shapeLineIntoClusters(lineInfo, lineIndex, scaledLineHeight, letterSpacing, align, direction);
|
|
4027
4017
|
clustersByLine.push(clusters);
|
|
4028
4018
|
});
|
|
4029
4019
|
return clustersByLine;
|
|
4030
4020
|
}
|
|
4031
|
-
shapeLineIntoClusters(lineInfo, lineIndex, scaledLineHeight, letterSpacing, align, direction
|
|
4021
|
+
shapeLineIntoClusters(lineInfo, lineIndex, scaledLineHeight, letterSpacing, align, direction) {
|
|
4032
4022
|
const buffer = this.loadedFont.hb.createBuffer();
|
|
4033
4023
|
if (direction === 'rtl') {
|
|
4034
4024
|
buffer.setDirection('rtl');
|
|
@@ -4061,8 +4051,9 @@ class TextShaper {
|
|
|
4061
4051
|
glyph.absoluteTextIndex = lineInfo.originalStart + glyph.cl;
|
|
4062
4052
|
}
|
|
4063
4053
|
glyph.lineIndex = lineIndex;
|
|
4064
|
-
|
|
4065
|
-
|
|
4054
|
+
// Cluster boundaries are based on whitespace only.
|
|
4055
|
+
// Coloring is applied later via vertex colors and must never affect shaping/kerning.
|
|
4056
|
+
if (isWhitespace) {
|
|
4066
4057
|
if (currentClusterGlyphs.length > 0) {
|
|
4067
4058
|
clusters.push({
|
|
4068
4059
|
text: currentClusterText,
|
|
@@ -5249,7 +5240,35 @@ class Text {
|
|
|
5249
5240
|
// Allow manual override via options.removeOverlaps
|
|
5250
5241
|
const shouldRemoveOverlaps = options.removeOverlaps ?? this.loadedFont.isVariable ?? false;
|
|
5251
5242
|
const clustersByLine = this.textShaper.shapeLines(layoutData.lines, layoutData.scaledLineHeight, layoutData.letterSpacing, layoutData.align, layoutData.direction, options.color, options.text);
|
|
5252
|
-
|
|
5243
|
+
// Pre-compute which character indices will be colored. This allows geometry building
|
|
5244
|
+
// to selectively use glyph-level caching (separate vertices) only for clusters containing
|
|
5245
|
+
// colored text, while non-colored clusters can still use fast cluster-level merging
|
|
5246
|
+
let coloredTextIndices;
|
|
5247
|
+
if (options.color && typeof options.color === 'object' && !Array.isArray(options.color)) {
|
|
5248
|
+
if (options.color.byText || options.color.byCharRange) {
|
|
5249
|
+
// Build the set manually since glyphs don't exist yet
|
|
5250
|
+
coloredTextIndices = new Set();
|
|
5251
|
+
if (options.color.byText) {
|
|
5252
|
+
for (const pattern of Object.keys(options.color.byText)) {
|
|
5253
|
+
let index = 0;
|
|
5254
|
+
while ((index = options.text.indexOf(pattern, index)) !== -1) {
|
|
5255
|
+
for (let i = index; i < index + pattern.length; i++) {
|
|
5256
|
+
coloredTextIndices.add(i);
|
|
5257
|
+
}
|
|
5258
|
+
index += pattern.length;
|
|
5259
|
+
}
|
|
5260
|
+
}
|
|
5261
|
+
}
|
|
5262
|
+
if (options.color.byCharRange) {
|
|
5263
|
+
for (const range of options.color.byCharRange) {
|
|
5264
|
+
for (let i = range.start; i < range.end; i++) {
|
|
5265
|
+
coloredTextIndices.add(i);
|
|
5266
|
+
}
|
|
5267
|
+
}
|
|
5268
|
+
}
|
|
5269
|
+
}
|
|
5270
|
+
}
|
|
5271
|
+
const shapedResult = this.geometryBuilder.buildInstancedGeometry(clustersByLine, layoutData.depth, shouldRemoveOverlaps, this.loadedFont.metrics.isCFF, options.separateGlyphsWithAttributes || false, coloredTextIndices);
|
|
5253
5272
|
const cacheStats = this.geometryBuilder.getCacheStats();
|
|
5254
5273
|
const result = this.finalizeGeometry(shapedResult.vertices, shapedResult.normals, shapedResult.indices, shapedResult.glyphInfos, shapedResult.planeBounds, options, cacheStats, options.text);
|
|
5255
5274
|
if (options.separateGlyphsWithAttributes) {
|