three-text 0.2.17 → 0.2.19
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 +8 -8
- package/dist/index.cjs +133 -105
- package/dist/index.d.ts +48 -43
- package/dist/index.js +133 -105
- package/dist/index.min.cjs +259 -251
- package/dist/index.min.js +319 -311
- package/dist/index.umd.js +133 -105
- package/dist/index.umd.min.js +295 -287
- package/dist/three/index.cjs +1 -0
- package/dist/three/index.d.ts +13 -1
- package/dist/three/index.js +1 -0
- package/dist/three/react.cjs +2 -1
- package/dist/three/react.d.ts +18 -12
- package/dist/three/react.js +2 -1
- package/dist/types/core/Text.d.ts +0 -1
- package/dist/types/core/geometry/Extruder.d.ts +1 -0
- package/dist/types/core/types.d.ts +10 -3
- package/dist/types/three/index.d.ts +6 -1
- package/dist/types/three/react.d.ts +1 -0
- package/package.json +14 -3
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* three-text v0.2.
|
|
2
|
+
* three-text v0.2.19
|
|
3
3
|
* Copyright (C) 2025 Countertype LLC
|
|
4
4
|
*
|
|
5
5
|
* This program is free software: you can redistribute it and/or modify
|
|
@@ -78,7 +78,9 @@ class PerformanceLogger {
|
|
|
78
78
|
// Find the metric in reverse order (most recent first)
|
|
79
79
|
for (let i = this.metrics.length - 1; i >= 0; i--) {
|
|
80
80
|
const metric = this.metrics[i];
|
|
81
|
-
if (metric.name === name &&
|
|
81
|
+
if (metric.name === name &&
|
|
82
|
+
metric.startTime === startTime &&
|
|
83
|
+
!metric.endTime) {
|
|
82
84
|
metric.endTime = endTime;
|
|
83
85
|
metric.duration = duration;
|
|
84
86
|
break;
|
|
@@ -466,7 +468,9 @@ class LineBreak {
|
|
|
466
468
|
const char = chars[i];
|
|
467
469
|
const nextChar = i < chars.length - 1 ? chars[i + 1] : null;
|
|
468
470
|
if (/\s/.test(char)) {
|
|
469
|
-
const width = widths
|
|
471
|
+
const width = widths
|
|
472
|
+
? (widths[i] ?? measureText(char))
|
|
473
|
+
: measureText(char);
|
|
470
474
|
items.push({
|
|
471
475
|
type: ItemType.GLUE,
|
|
472
476
|
width,
|
|
@@ -839,7 +843,9 @@ class LineBreak {
|
|
|
839
843
|
if (breaks.length === 0) {
|
|
840
844
|
// For first emergency attempt, use initialEmergencyStretch
|
|
841
845
|
// For subsequent iterations (short line detection), progressively increase
|
|
842
|
-
currentEmergencyStretch =
|
|
846
|
+
currentEmergencyStretch =
|
|
847
|
+
initialEmergencyStretch +
|
|
848
|
+
iteration * width * SHORT_LINE_EMERGENCY_STRETCH_INCREMENT;
|
|
843
849
|
breaks = LineBreak.findBreakpoints(currentItems, width, tolerance, looseness, true, currentEmergencyStretch, context);
|
|
844
850
|
}
|
|
845
851
|
// Last resort: allow higher badness (but not infinite)
|
|
@@ -1720,12 +1726,12 @@ class FontMetadataExtractor {
|
|
|
1720
1726
|
try {
|
|
1721
1727
|
if (gsubTableOffset) {
|
|
1722
1728
|
const gsubData = this.extractFeatureDataFromTable(view, gsubTableOffset, nameTableOffset);
|
|
1723
|
-
gsubData.features.forEach(f => features.add(f));
|
|
1729
|
+
gsubData.features.forEach((f) => features.add(f));
|
|
1724
1730
|
Object.assign(featureNames, gsubData.names);
|
|
1725
1731
|
}
|
|
1726
1732
|
if (gposTableOffset) {
|
|
1727
1733
|
const gposData = this.extractFeatureDataFromTable(view, gposTableOffset, nameTableOffset);
|
|
1728
|
-
gposData.features.forEach(f => features.add(f));
|
|
1734
|
+
gposData.features.forEach((f) => features.add(f));
|
|
1729
1735
|
Object.assign(featureNames, gposData.names);
|
|
1730
1736
|
}
|
|
1731
1737
|
}
|
|
@@ -2724,7 +2730,9 @@ class Tessellator {
|
|
|
2724
2730
|
tessContours = originalContours;
|
|
2725
2731
|
}
|
|
2726
2732
|
let extrusionContours = needsExtrusionContours
|
|
2727
|
-
?
|
|
2733
|
+
? needsWindingReversal
|
|
2734
|
+
? tessContours
|
|
2735
|
+
: (originalContours ?? this.pathsToContours(paths))
|
|
2728
2736
|
: [];
|
|
2729
2737
|
if (removeOverlaps) {
|
|
2730
2738
|
logger.log('Two-pass: boundary extraction then triangulation');
|
|
@@ -2746,24 +2754,6 @@ class Tessellator {
|
|
|
2746
2754
|
}
|
|
2747
2755
|
else {
|
|
2748
2756
|
logger.log(`Single-pass triangulation for ${isCFF ? 'CFF' : 'TTF'}`);
|
|
2749
|
-
// TTF contours may have inconsistent winding; check if we need normalization
|
|
2750
|
-
if (needsExtrusionContours && !isCFF) {
|
|
2751
|
-
const needsNormalization = this.needsWindingNormalization(extrusionContours);
|
|
2752
|
-
if (needsNormalization) {
|
|
2753
|
-
logger.log('Complex topology detected, running boundary pass for winding normalization');
|
|
2754
|
-
perfLogger.start('Tessellator.windingNormalization', {
|
|
2755
|
-
contourCount: extrusionContours.length
|
|
2756
|
-
});
|
|
2757
|
-
const boundaryResult = this.performTessellation(extrusionContours, 'boundary');
|
|
2758
|
-
perfLogger.end('Tessellator.windingNormalization');
|
|
2759
|
-
if (boundaryResult) {
|
|
2760
|
-
extrusionContours = this.boundaryToContours(boundaryResult);
|
|
2761
|
-
}
|
|
2762
|
-
}
|
|
2763
|
-
else {
|
|
2764
|
-
logger.log('Simple topology, skipping winding normalization');
|
|
2765
|
-
}
|
|
2766
|
-
}
|
|
2767
2757
|
}
|
|
2768
2758
|
perfLogger.start('Tessellator.triangulationPass', {
|
|
2769
2759
|
contourCount: tessContours.length
|
|
@@ -2775,7 +2765,10 @@ class Tessellator {
|
|
|
2775
2765
|
? 'libtess returned empty result from triangulation pass'
|
|
2776
2766
|
: 'libtess returned empty result from single-pass triangulation';
|
|
2777
2767
|
logger.warn(warning);
|
|
2778
|
-
return {
|
|
2768
|
+
return {
|
|
2769
|
+
triangles: { vertices: [], indices: [] },
|
|
2770
|
+
contours: extrusionContours
|
|
2771
|
+
};
|
|
2779
2772
|
}
|
|
2780
2773
|
return {
|
|
2781
2774
|
triangles: {
|
|
@@ -2945,28 +2938,58 @@ class Tessellator {
|
|
|
2945
2938
|
|
|
2946
2939
|
class Extruder {
|
|
2947
2940
|
constructor() { }
|
|
2941
|
+
packEdge(a, b) {
|
|
2942
|
+
const lo = a < b ? a : b;
|
|
2943
|
+
const hi = a < b ? b : a;
|
|
2944
|
+
return lo * 0x100000000 + hi;
|
|
2945
|
+
}
|
|
2948
2946
|
extrude(geometry, depth = 0, unitsPerEm) {
|
|
2949
2947
|
const points = geometry.triangles.vertices;
|
|
2950
2948
|
const triangleIndices = geometry.triangles.indices;
|
|
2951
2949
|
const numPoints = points.length / 2;
|
|
2952
|
-
// Count side
|
|
2953
|
-
let
|
|
2950
|
+
// Count boundary edges for side walls (4 vertices + 6 indices per edge)
|
|
2951
|
+
let boundaryEdges = [];
|
|
2954
2952
|
if (depth !== 0) {
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2953
|
+
const counts = new Map();
|
|
2954
|
+
const oriented = new Map();
|
|
2955
|
+
for (let i = 0; i < triangleIndices.length; i += 3) {
|
|
2956
|
+
const a = triangleIndices[i];
|
|
2957
|
+
const b = triangleIndices[i + 1];
|
|
2958
|
+
const c = triangleIndices[i + 2];
|
|
2959
|
+
const k0 = this.packEdge(a, b);
|
|
2960
|
+
const n0 = (counts.get(k0) ?? 0) + 1;
|
|
2961
|
+
counts.set(k0, n0);
|
|
2962
|
+
if (n0 === 1)
|
|
2963
|
+
oriented.set(k0, [a, b]);
|
|
2964
|
+
const k1 = this.packEdge(b, c);
|
|
2965
|
+
const n1 = (counts.get(k1) ?? 0) + 1;
|
|
2966
|
+
counts.set(k1, n1);
|
|
2967
|
+
if (n1 === 1)
|
|
2968
|
+
oriented.set(k1, [b, c]);
|
|
2969
|
+
const k2 = this.packEdge(c, a);
|
|
2970
|
+
const n2 = (counts.get(k2) ?? 0) + 1;
|
|
2971
|
+
counts.set(k2, n2);
|
|
2972
|
+
if (n2 === 1)
|
|
2973
|
+
oriented.set(k2, [c, a]);
|
|
2974
|
+
}
|
|
2975
|
+
boundaryEdges = [];
|
|
2976
|
+
for (const [key, count] of counts) {
|
|
2977
|
+
if (count !== 1)
|
|
2978
|
+
continue;
|
|
2979
|
+
const edge = oriented.get(key);
|
|
2980
|
+
if (edge)
|
|
2981
|
+
boundaryEdges.push(edge);
|
|
2960
2982
|
}
|
|
2961
2983
|
}
|
|
2962
|
-
const
|
|
2984
|
+
const sideEdgeCount = depth === 0 ? 0 : boundaryEdges.length;
|
|
2985
|
+
const sideVertexCount = depth === 0 ? 0 : sideEdgeCount * 4;
|
|
2963
2986
|
const baseVertexCount = depth === 0 ? numPoints : numPoints * 2;
|
|
2964
2987
|
const vertexCount = baseVertexCount + sideVertexCount;
|
|
2965
2988
|
const vertices = new Float32Array(vertexCount * 3);
|
|
2966
2989
|
const normals = new Float32Array(vertexCount * 3);
|
|
2967
2990
|
const indexCount = depth === 0
|
|
2968
2991
|
? triangleIndices.length
|
|
2969
|
-
: triangleIndices.length * 2 +
|
|
2992
|
+
: triangleIndices.length * 2 + sideEdgeCount * 6;
|
|
2970
2993
|
const indices = new Uint32Array(indexCount);
|
|
2971
2994
|
if (depth === 0) {
|
|
2972
2995
|
// Single-sided flat geometry at z=0
|
|
@@ -3022,60 +3045,62 @@ class Extruder {
|
|
|
3022
3045
|
// Side walls
|
|
3023
3046
|
let nextVertex = numPoints * 2;
|
|
3024
3047
|
let idxPos = triangleIndices.length * 2;
|
|
3025
|
-
for (
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3048
|
+
for (let e = 0; e < boundaryEdges.length; e++) {
|
|
3049
|
+
const [u, v] = boundaryEdges[e];
|
|
3050
|
+
const u2 = u * 2;
|
|
3051
|
+
const v2 = v * 2;
|
|
3052
|
+
const p0x = points[u2];
|
|
3053
|
+
const p0y = points[u2 + 1];
|
|
3054
|
+
const p1x = points[v2];
|
|
3055
|
+
const p1y = points[v2 + 1];
|
|
3056
|
+
// Perpendicular normal for this wall segment
|
|
3057
|
+
// Uses the edge direction from the cap triangulation so winding does not depend on contour direction
|
|
3058
|
+
const ex = p1x - p0x;
|
|
3059
|
+
const ey = p1y - p0y;
|
|
3060
|
+
const lenSq = ex * ex + ey * ey;
|
|
3061
|
+
let nx = 0;
|
|
3062
|
+
let ny = 0;
|
|
3063
|
+
if (lenSq > 0) {
|
|
3064
|
+
const invLen = 1 / Math.sqrt(lenSq);
|
|
3065
|
+
nx = ey * invLen;
|
|
3066
|
+
ny = -ex * invLen;
|
|
3067
|
+
}
|
|
3068
|
+
const baseVertex = nextVertex;
|
|
3069
|
+
const base = baseVertex * 3;
|
|
3070
|
+
// Wall quad: front edge at z=0, back edge at z=depth
|
|
3071
|
+
vertices[base] = p0x;
|
|
3072
|
+
vertices[base + 1] = p0y;
|
|
3073
|
+
vertices[base + 2] = 0;
|
|
3074
|
+
vertices[base + 3] = p1x;
|
|
3075
|
+
vertices[base + 4] = p1y;
|
|
3076
|
+
vertices[base + 5] = 0;
|
|
3077
|
+
vertices[base + 6] = p0x;
|
|
3078
|
+
vertices[base + 7] = p0y;
|
|
3079
|
+
vertices[base + 8] = backZ;
|
|
3080
|
+
vertices[base + 9] = p1x;
|
|
3081
|
+
vertices[base + 10] = p1y;
|
|
3082
|
+
vertices[base + 11] = backZ;
|
|
3083
|
+
// Wall normals point perpendicular to edge
|
|
3084
|
+
normals[base] = nx;
|
|
3085
|
+
normals[base + 1] = ny;
|
|
3086
|
+
normals[base + 2] = 0;
|
|
3087
|
+
normals[base + 3] = nx;
|
|
3088
|
+
normals[base + 4] = ny;
|
|
3089
|
+
normals[base + 5] = 0;
|
|
3090
|
+
normals[base + 6] = nx;
|
|
3091
|
+
normals[base + 7] = ny;
|
|
3092
|
+
normals[base + 8] = 0;
|
|
3093
|
+
normals[base + 9] = nx;
|
|
3094
|
+
normals[base + 10] = ny;
|
|
3095
|
+
normals[base + 11] = 0;
|
|
3096
|
+
// Two triangles per wall segment
|
|
3097
|
+
indices[idxPos++] = baseVertex;
|
|
3098
|
+
indices[idxPos++] = baseVertex + 1;
|
|
3099
|
+
indices[idxPos++] = baseVertex + 2;
|
|
3100
|
+
indices[idxPos++] = baseVertex + 1;
|
|
3101
|
+
indices[idxPos++] = baseVertex + 3;
|
|
3102
|
+
indices[idxPos++] = baseVertex + 2;
|
|
3103
|
+
nextVertex += 4;
|
|
3079
3104
|
}
|
|
3080
3105
|
return { vertices, normals, indices };
|
|
3081
3106
|
}
|
|
@@ -3400,9 +3425,7 @@ class PathOptimizer {
|
|
|
3400
3425
|
const v1LenSq = v1x * v1x + v1y * v1y;
|
|
3401
3426
|
const v2LenSq = v2x * v2x + v2y * v2y;
|
|
3402
3427
|
const minLenSq = this.config.minSegmentLength * this.config.minSegmentLength;
|
|
3403
|
-
if (angle > threshold ||
|
|
3404
|
-
v1LenSq < minLenSq ||
|
|
3405
|
-
v2LenSq < minLenSq) {
|
|
3428
|
+
if (angle > threshold || v1LenSq < minLenSq || v2LenSq < minLenSq) {
|
|
3406
3429
|
result.push(current);
|
|
3407
3430
|
}
|
|
3408
3431
|
else {
|
|
@@ -4164,7 +4187,7 @@ class GlyphGeometryBuilder {
|
|
|
4164
4187
|
const relativePositions = cluster.glyphs.map((g) => new Vec3(g.x ?? 0, g.y ?? 0, 0));
|
|
4165
4188
|
boundaryGroups = this.clusterer.cluster(clusterGlyphContours, relativePositions);
|
|
4166
4189
|
this.clusteringCache.set(cacheKey, {
|
|
4167
|
-
glyphIds: cluster.glyphs.map(g => g.g),
|
|
4190
|
+
glyphIds: cluster.glyphs.map((g) => g.g),
|
|
4168
4191
|
groups: boundaryGroups
|
|
4169
4192
|
});
|
|
4170
4193
|
}
|
|
@@ -4174,7 +4197,7 @@ class GlyphGeometryBuilder {
|
|
|
4174
4197
|
// Use glyph-level caching when separateGlyphs is set or when cluster contains colored text
|
|
4175
4198
|
const forceSeparate = separateGlyphs || clusterHasColoredGlyphs;
|
|
4176
4199
|
// Iterate over the geometric groups identified by BoundaryClusterer
|
|
4177
|
-
// logical groups (words) split into geometric sub-groups
|
|
4200
|
+
// logical groups (words) split into geometric sub-groups
|
|
4178
4201
|
for (const groupIndices of boundaryGroups) {
|
|
4179
4202
|
const isOverlappingGroup = groupIndices.length > 1;
|
|
4180
4203
|
const shouldCluster = isOverlappingGroup && !forceSeparate;
|
|
@@ -4554,7 +4577,8 @@ class TextShaper {
|
|
|
4554
4577
|
if (LineBreak.isCJOpeningPunctuation(currentChar)) {
|
|
4555
4578
|
shouldApply = false;
|
|
4556
4579
|
}
|
|
4557
|
-
if (LineBreak.isCJPunctuation(currentChar) &&
|
|
4580
|
+
if (LineBreak.isCJPunctuation(currentChar) &&
|
|
4581
|
+
LineBreak.isCJPunctuation(nextChar)) {
|
|
4558
4582
|
shouldApply = false;
|
|
4559
4583
|
}
|
|
4560
4584
|
if (shouldApply) {
|
|
@@ -5358,7 +5382,7 @@ class Text {
|
|
|
5358
5382
|
// Stringify with sorted keys for cache stability
|
|
5359
5383
|
static stableStringify(obj) {
|
|
5360
5384
|
const keys = Object.keys(obj).sort();
|
|
5361
|
-
const pairs = keys.map(k => `${k}:${obj[k]}`);
|
|
5385
|
+
const pairs = keys.map((k) => `${k}:${obj[k]}`);
|
|
5362
5386
|
return pairs.join(',');
|
|
5363
5387
|
}
|
|
5364
5388
|
constructor() {
|
|
@@ -5596,7 +5620,9 @@ class Text {
|
|
|
5596
5620
|
// to selectively use glyph-level caching (separate vertices) only for clusters containing
|
|
5597
5621
|
// colored text, while non-colored clusters can still use fast cluster-level merging
|
|
5598
5622
|
let coloredTextIndices;
|
|
5599
|
-
if (options.color &&
|
|
5623
|
+
if (options.color &&
|
|
5624
|
+
typeof options.color === 'object' &&
|
|
5625
|
+
!Array.isArray(options.color)) {
|
|
5600
5626
|
if (options.color.byText || options.color.byCharRange) {
|
|
5601
5627
|
// Build the set manually since glyphs don't exist yet
|
|
5602
5628
|
coloredTextIndices = new Set();
|
|
@@ -5701,7 +5727,9 @@ class Text {
|
|
|
5701
5727
|
const depthScale = this.loadedFont.upem / size;
|
|
5702
5728
|
const rawDepthInFontUnits = depth * depthScale;
|
|
5703
5729
|
const minExtrudeDepth = this.loadedFont.upem * 0.000025;
|
|
5704
|
-
const depthInFontUnits = rawDepthInFontUnits <= 0
|
|
5730
|
+
const depthInFontUnits = rawDepthInFontUnits <= 0
|
|
5731
|
+
? 0
|
|
5732
|
+
: Math.max(rawDepthInFontUnits, minExtrudeDepth);
|
|
5705
5733
|
if (!this.textLayout) {
|
|
5706
5734
|
this.textLayout = new TextLayout(this.loadedFont);
|
|
5707
5735
|
}
|
|
@@ -5873,10 +5901,12 @@ class Text {
|
|
|
5873
5901
|
planeBounds.max.z *= finalScale;
|
|
5874
5902
|
for (let i = 0; i < glyphInfoArray.length; i++) {
|
|
5875
5903
|
const glyphInfo = glyphInfoArray[i];
|
|
5876
|
-
glyphInfo.bounds.min.x =
|
|
5904
|
+
glyphInfo.bounds.min.x =
|
|
5905
|
+
glyphInfo.bounds.min.x * finalScale + offsetScaled;
|
|
5877
5906
|
glyphInfo.bounds.min.y *= finalScale;
|
|
5878
5907
|
glyphInfo.bounds.min.z *= finalScale;
|
|
5879
|
-
glyphInfo.bounds.max.x =
|
|
5908
|
+
glyphInfo.bounds.max.x =
|
|
5909
|
+
glyphInfo.bounds.max.x * finalScale + offsetScaled;
|
|
5880
5910
|
glyphInfo.bounds.max.y *= finalScale;
|
|
5881
5911
|
glyphInfo.bounds.max.z *= finalScale;
|
|
5882
5912
|
}
|
|
@@ -5939,13 +5969,11 @@ class Text {
|
|
|
5939
5969
|
static registerPattern(language, pattern) {
|
|
5940
5970
|
Text.patternCache.set(language, pattern);
|
|
5941
5971
|
}
|
|
5942
|
-
static clearFontCache() {
|
|
5943
|
-
Text.fontCache.clear();
|
|
5944
|
-
Text.fontCacheMemoryBytes = 0;
|
|
5945
|
-
}
|
|
5946
5972
|
static setMaxFontCacheMemoryMB(limitMB) {
|
|
5947
5973
|
Text.maxFontCacheMemoryBytes =
|
|
5948
|
-
limitMB === Infinity
|
|
5974
|
+
limitMB === Infinity
|
|
5975
|
+
? Infinity
|
|
5976
|
+
: Math.max(1, Math.floor(limitMB)) * 1024 * 1024;
|
|
5949
5977
|
Text.enforceFontCacheMemoryLimit();
|
|
5950
5978
|
}
|
|
5951
5979
|
getLoadedFont() {
|