three-text 0.2.10 → 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/README.md +3 -3
- package/dist/index.cjs +44 -25
- package/dist/index.js +44 -25
- package/dist/index.min.cjs +2 -2
- package/dist/index.min.js +2 -2
- package/dist/index.umd.js +44 -25
- 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.umd.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
|
|
@@ -260,7 +260,7 @@
|
|
|
260
260
|
// Non TeX default: emergency stretch for non-hyphenated text (10% of line width)
|
|
261
261
|
const DEFAULT_EMERGENCY_STRETCH_NO_HYPHEN = 0.1;
|
|
262
262
|
// Another non TeX default: Short line detection thresholds
|
|
263
|
-
const SHORT_LINE_WIDTH_THRESHOLD = 0.
|
|
263
|
+
const SHORT_LINE_WIDTH_THRESHOLD = 0.7; // Lines < 70% of width are problematic
|
|
264
264
|
const SHORT_LINE_EMERGENCY_STRETCH_INCREMENT = 0.1; // Add 10% per iteration
|
|
265
265
|
class LineBreak {
|
|
266
266
|
// Calculate badness according to TeX's formula (tex.web line 2337)
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
4072
|
-
|
|
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
|
-
|
|
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) {
|