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.umd.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
|
|
@@ -83,7 +83,9 @@
|
|
|
83
83
|
// Find the metric in reverse order (most recent first)
|
|
84
84
|
for (let i = this.metrics.length - 1; i >= 0; i--) {
|
|
85
85
|
const metric = this.metrics[i];
|
|
86
|
-
if (metric.name === name &&
|
|
86
|
+
if (metric.name === name &&
|
|
87
|
+
metric.startTime === startTime &&
|
|
88
|
+
!metric.endTime) {
|
|
87
89
|
metric.endTime = endTime;
|
|
88
90
|
metric.duration = duration;
|
|
89
91
|
break;
|
|
@@ -471,7 +473,9 @@
|
|
|
471
473
|
const char = chars[i];
|
|
472
474
|
const nextChar = i < chars.length - 1 ? chars[i + 1] : null;
|
|
473
475
|
if (/\s/.test(char)) {
|
|
474
|
-
const width = widths
|
|
476
|
+
const width = widths
|
|
477
|
+
? (widths[i] ?? measureText(char))
|
|
478
|
+
: measureText(char);
|
|
475
479
|
items.push({
|
|
476
480
|
type: ItemType.GLUE,
|
|
477
481
|
width,
|
|
@@ -844,7 +848,9 @@
|
|
|
844
848
|
if (breaks.length === 0) {
|
|
845
849
|
// For first emergency attempt, use initialEmergencyStretch
|
|
846
850
|
// For subsequent iterations (short line detection), progressively increase
|
|
847
|
-
currentEmergencyStretch =
|
|
851
|
+
currentEmergencyStretch =
|
|
852
|
+
initialEmergencyStretch +
|
|
853
|
+
iteration * width * SHORT_LINE_EMERGENCY_STRETCH_INCREMENT;
|
|
848
854
|
breaks = LineBreak.findBreakpoints(currentItems, width, tolerance, looseness, true, currentEmergencyStretch, context);
|
|
849
855
|
}
|
|
850
856
|
// Last resort: allow higher badness (but not infinite)
|
|
@@ -1725,12 +1731,12 @@
|
|
|
1725
1731
|
try {
|
|
1726
1732
|
if (gsubTableOffset) {
|
|
1727
1733
|
const gsubData = this.extractFeatureDataFromTable(view, gsubTableOffset, nameTableOffset);
|
|
1728
|
-
gsubData.features.forEach(f => features.add(f));
|
|
1734
|
+
gsubData.features.forEach((f) => features.add(f));
|
|
1729
1735
|
Object.assign(featureNames, gsubData.names);
|
|
1730
1736
|
}
|
|
1731
1737
|
if (gposTableOffset) {
|
|
1732
1738
|
const gposData = this.extractFeatureDataFromTable(view, gposTableOffset, nameTableOffset);
|
|
1733
|
-
gposData.features.forEach(f => features.add(f));
|
|
1739
|
+
gposData.features.forEach((f) => features.add(f));
|
|
1734
1740
|
Object.assign(featureNames, gposData.names);
|
|
1735
1741
|
}
|
|
1736
1742
|
}
|
|
@@ -2731,7 +2737,9 @@
|
|
|
2731
2737
|
tessContours = originalContours;
|
|
2732
2738
|
}
|
|
2733
2739
|
let extrusionContours = needsExtrusionContours
|
|
2734
|
-
?
|
|
2740
|
+
? needsWindingReversal
|
|
2741
|
+
? tessContours
|
|
2742
|
+
: (originalContours ?? this.pathsToContours(paths))
|
|
2735
2743
|
: [];
|
|
2736
2744
|
if (removeOverlaps) {
|
|
2737
2745
|
logger.log('Two-pass: boundary extraction then triangulation');
|
|
@@ -2753,24 +2761,6 @@
|
|
|
2753
2761
|
}
|
|
2754
2762
|
else {
|
|
2755
2763
|
logger.log(`Single-pass triangulation for ${isCFF ? 'CFF' : 'TTF'}`);
|
|
2756
|
-
// TTF contours may have inconsistent winding; check if we need normalization
|
|
2757
|
-
if (needsExtrusionContours && !isCFF) {
|
|
2758
|
-
const needsNormalization = this.needsWindingNormalization(extrusionContours);
|
|
2759
|
-
if (needsNormalization) {
|
|
2760
|
-
logger.log('Complex topology detected, running boundary pass for winding normalization');
|
|
2761
|
-
perfLogger.start('Tessellator.windingNormalization', {
|
|
2762
|
-
contourCount: extrusionContours.length
|
|
2763
|
-
});
|
|
2764
|
-
const boundaryResult = this.performTessellation(extrusionContours, 'boundary');
|
|
2765
|
-
perfLogger.end('Tessellator.windingNormalization');
|
|
2766
|
-
if (boundaryResult) {
|
|
2767
|
-
extrusionContours = this.boundaryToContours(boundaryResult);
|
|
2768
|
-
}
|
|
2769
|
-
}
|
|
2770
|
-
else {
|
|
2771
|
-
logger.log('Simple topology, skipping winding normalization');
|
|
2772
|
-
}
|
|
2773
|
-
}
|
|
2774
2764
|
}
|
|
2775
2765
|
perfLogger.start('Tessellator.triangulationPass', {
|
|
2776
2766
|
contourCount: tessContours.length
|
|
@@ -2782,7 +2772,10 @@
|
|
|
2782
2772
|
? 'libtess returned empty result from triangulation pass'
|
|
2783
2773
|
: 'libtess returned empty result from single-pass triangulation';
|
|
2784
2774
|
logger.warn(warning);
|
|
2785
|
-
return {
|
|
2775
|
+
return {
|
|
2776
|
+
triangles: { vertices: [], indices: [] },
|
|
2777
|
+
contours: extrusionContours
|
|
2778
|
+
};
|
|
2786
2779
|
}
|
|
2787
2780
|
return {
|
|
2788
2781
|
triangles: {
|
|
@@ -2952,28 +2945,58 @@
|
|
|
2952
2945
|
|
|
2953
2946
|
class Extruder {
|
|
2954
2947
|
constructor() { }
|
|
2948
|
+
packEdge(a, b) {
|
|
2949
|
+
const lo = a < b ? a : b;
|
|
2950
|
+
const hi = a < b ? b : a;
|
|
2951
|
+
return lo * 0x100000000 + hi;
|
|
2952
|
+
}
|
|
2955
2953
|
extrude(geometry, depth = 0, unitsPerEm) {
|
|
2956
2954
|
const points = geometry.triangles.vertices;
|
|
2957
2955
|
const triangleIndices = geometry.triangles.indices;
|
|
2958
2956
|
const numPoints = points.length / 2;
|
|
2959
|
-
// Count side
|
|
2960
|
-
let
|
|
2957
|
+
// Count boundary edges for side walls (4 vertices + 6 indices per edge)
|
|
2958
|
+
let boundaryEdges = [];
|
|
2961
2959
|
if (depth !== 0) {
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2960
|
+
const counts = new Map();
|
|
2961
|
+
const oriented = new Map();
|
|
2962
|
+
for (let i = 0; i < triangleIndices.length; i += 3) {
|
|
2963
|
+
const a = triangleIndices[i];
|
|
2964
|
+
const b = triangleIndices[i + 1];
|
|
2965
|
+
const c = triangleIndices[i + 2];
|
|
2966
|
+
const k0 = this.packEdge(a, b);
|
|
2967
|
+
const n0 = (counts.get(k0) ?? 0) + 1;
|
|
2968
|
+
counts.set(k0, n0);
|
|
2969
|
+
if (n0 === 1)
|
|
2970
|
+
oriented.set(k0, [a, b]);
|
|
2971
|
+
const k1 = this.packEdge(b, c);
|
|
2972
|
+
const n1 = (counts.get(k1) ?? 0) + 1;
|
|
2973
|
+
counts.set(k1, n1);
|
|
2974
|
+
if (n1 === 1)
|
|
2975
|
+
oriented.set(k1, [b, c]);
|
|
2976
|
+
const k2 = this.packEdge(c, a);
|
|
2977
|
+
const n2 = (counts.get(k2) ?? 0) + 1;
|
|
2978
|
+
counts.set(k2, n2);
|
|
2979
|
+
if (n2 === 1)
|
|
2980
|
+
oriented.set(k2, [c, a]);
|
|
2981
|
+
}
|
|
2982
|
+
boundaryEdges = [];
|
|
2983
|
+
for (const [key, count] of counts) {
|
|
2984
|
+
if (count !== 1)
|
|
2985
|
+
continue;
|
|
2986
|
+
const edge = oriented.get(key);
|
|
2987
|
+
if (edge)
|
|
2988
|
+
boundaryEdges.push(edge);
|
|
2967
2989
|
}
|
|
2968
2990
|
}
|
|
2969
|
-
const
|
|
2991
|
+
const sideEdgeCount = depth === 0 ? 0 : boundaryEdges.length;
|
|
2992
|
+
const sideVertexCount = depth === 0 ? 0 : sideEdgeCount * 4;
|
|
2970
2993
|
const baseVertexCount = depth === 0 ? numPoints : numPoints * 2;
|
|
2971
2994
|
const vertexCount = baseVertexCount + sideVertexCount;
|
|
2972
2995
|
const vertices = new Float32Array(vertexCount * 3);
|
|
2973
2996
|
const normals = new Float32Array(vertexCount * 3);
|
|
2974
2997
|
const indexCount = depth === 0
|
|
2975
2998
|
? triangleIndices.length
|
|
2976
|
-
: triangleIndices.length * 2 +
|
|
2999
|
+
: triangleIndices.length * 2 + sideEdgeCount * 6;
|
|
2977
3000
|
const indices = new Uint32Array(indexCount);
|
|
2978
3001
|
if (depth === 0) {
|
|
2979
3002
|
// Single-sided flat geometry at z=0
|
|
@@ -3029,60 +3052,62 @@
|
|
|
3029
3052
|
// Side walls
|
|
3030
3053
|
let nextVertex = numPoints * 2;
|
|
3031
3054
|
let idxPos = triangleIndices.length * 2;
|
|
3032
|
-
for (
|
|
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
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3055
|
+
for (let e = 0; e < boundaryEdges.length; e++) {
|
|
3056
|
+
const [u, v] = boundaryEdges[e];
|
|
3057
|
+
const u2 = u * 2;
|
|
3058
|
+
const v2 = v * 2;
|
|
3059
|
+
const p0x = points[u2];
|
|
3060
|
+
const p0y = points[u2 + 1];
|
|
3061
|
+
const p1x = points[v2];
|
|
3062
|
+
const p1y = points[v2 + 1];
|
|
3063
|
+
// Perpendicular normal for this wall segment
|
|
3064
|
+
// Uses the edge direction from the cap triangulation so winding does not depend on contour direction
|
|
3065
|
+
const ex = p1x - p0x;
|
|
3066
|
+
const ey = p1y - p0y;
|
|
3067
|
+
const lenSq = ex * ex + ey * ey;
|
|
3068
|
+
let nx = 0;
|
|
3069
|
+
let ny = 0;
|
|
3070
|
+
if (lenSq > 0) {
|
|
3071
|
+
const invLen = 1 / Math.sqrt(lenSq);
|
|
3072
|
+
nx = ey * invLen;
|
|
3073
|
+
ny = -ex * invLen;
|
|
3074
|
+
}
|
|
3075
|
+
const baseVertex = nextVertex;
|
|
3076
|
+
const base = baseVertex * 3;
|
|
3077
|
+
// Wall quad: front edge at z=0, back edge at z=depth
|
|
3078
|
+
vertices[base] = p0x;
|
|
3079
|
+
vertices[base + 1] = p0y;
|
|
3080
|
+
vertices[base + 2] = 0;
|
|
3081
|
+
vertices[base + 3] = p1x;
|
|
3082
|
+
vertices[base + 4] = p1y;
|
|
3083
|
+
vertices[base + 5] = 0;
|
|
3084
|
+
vertices[base + 6] = p0x;
|
|
3085
|
+
vertices[base + 7] = p0y;
|
|
3086
|
+
vertices[base + 8] = backZ;
|
|
3087
|
+
vertices[base + 9] = p1x;
|
|
3088
|
+
vertices[base + 10] = p1y;
|
|
3089
|
+
vertices[base + 11] = backZ;
|
|
3090
|
+
// Wall normals point perpendicular to edge
|
|
3091
|
+
normals[base] = nx;
|
|
3092
|
+
normals[base + 1] = ny;
|
|
3093
|
+
normals[base + 2] = 0;
|
|
3094
|
+
normals[base + 3] = nx;
|
|
3095
|
+
normals[base + 4] = ny;
|
|
3096
|
+
normals[base + 5] = 0;
|
|
3097
|
+
normals[base + 6] = nx;
|
|
3098
|
+
normals[base + 7] = ny;
|
|
3099
|
+
normals[base + 8] = 0;
|
|
3100
|
+
normals[base + 9] = nx;
|
|
3101
|
+
normals[base + 10] = ny;
|
|
3102
|
+
normals[base + 11] = 0;
|
|
3103
|
+
// Two triangles per wall segment
|
|
3104
|
+
indices[idxPos++] = baseVertex;
|
|
3105
|
+
indices[idxPos++] = baseVertex + 1;
|
|
3106
|
+
indices[idxPos++] = baseVertex + 2;
|
|
3107
|
+
indices[idxPos++] = baseVertex + 1;
|
|
3108
|
+
indices[idxPos++] = baseVertex + 3;
|
|
3109
|
+
indices[idxPos++] = baseVertex + 2;
|
|
3110
|
+
nextVertex += 4;
|
|
3086
3111
|
}
|
|
3087
3112
|
return { vertices, normals, indices };
|
|
3088
3113
|
}
|
|
@@ -3407,9 +3432,7 @@
|
|
|
3407
3432
|
const v1LenSq = v1x * v1x + v1y * v1y;
|
|
3408
3433
|
const v2LenSq = v2x * v2x + v2y * v2y;
|
|
3409
3434
|
const minLenSq = this.config.minSegmentLength * this.config.minSegmentLength;
|
|
3410
|
-
if (angle > threshold ||
|
|
3411
|
-
v1LenSq < minLenSq ||
|
|
3412
|
-
v2LenSq < minLenSq) {
|
|
3435
|
+
if (angle > threshold || v1LenSq < minLenSq || v2LenSq < minLenSq) {
|
|
3413
3436
|
result.push(current);
|
|
3414
3437
|
}
|
|
3415
3438
|
else {
|
|
@@ -4171,7 +4194,7 @@
|
|
|
4171
4194
|
const relativePositions = cluster.glyphs.map((g) => new Vec3(g.x ?? 0, g.y ?? 0, 0));
|
|
4172
4195
|
boundaryGroups = this.clusterer.cluster(clusterGlyphContours, relativePositions);
|
|
4173
4196
|
this.clusteringCache.set(cacheKey, {
|
|
4174
|
-
glyphIds: cluster.glyphs.map(g => g.g),
|
|
4197
|
+
glyphIds: cluster.glyphs.map((g) => g.g),
|
|
4175
4198
|
groups: boundaryGroups
|
|
4176
4199
|
});
|
|
4177
4200
|
}
|
|
@@ -4181,7 +4204,7 @@
|
|
|
4181
4204
|
// Use glyph-level caching when separateGlyphs is set or when cluster contains colored text
|
|
4182
4205
|
const forceSeparate = separateGlyphs || clusterHasColoredGlyphs;
|
|
4183
4206
|
// Iterate over the geometric groups identified by BoundaryClusterer
|
|
4184
|
-
// logical groups (words) split into geometric sub-groups
|
|
4207
|
+
// logical groups (words) split into geometric sub-groups
|
|
4185
4208
|
for (const groupIndices of boundaryGroups) {
|
|
4186
4209
|
const isOverlappingGroup = groupIndices.length > 1;
|
|
4187
4210
|
const shouldCluster = isOverlappingGroup && !forceSeparate;
|
|
@@ -4561,7 +4584,8 @@
|
|
|
4561
4584
|
if (LineBreak.isCJOpeningPunctuation(currentChar)) {
|
|
4562
4585
|
shouldApply = false;
|
|
4563
4586
|
}
|
|
4564
|
-
if (LineBreak.isCJPunctuation(currentChar) &&
|
|
4587
|
+
if (LineBreak.isCJPunctuation(currentChar) &&
|
|
4588
|
+
LineBreak.isCJPunctuation(nextChar)) {
|
|
4565
4589
|
shouldApply = false;
|
|
4566
4590
|
}
|
|
4567
4591
|
if (shouldApply) {
|
|
@@ -5365,7 +5389,7 @@
|
|
|
5365
5389
|
// Stringify with sorted keys for cache stability
|
|
5366
5390
|
static stableStringify(obj) {
|
|
5367
5391
|
const keys = Object.keys(obj).sort();
|
|
5368
|
-
const pairs = keys.map(k => `${k}:${obj[k]}`);
|
|
5392
|
+
const pairs = keys.map((k) => `${k}:${obj[k]}`);
|
|
5369
5393
|
return pairs.join(',');
|
|
5370
5394
|
}
|
|
5371
5395
|
constructor() {
|
|
@@ -5603,7 +5627,9 @@
|
|
|
5603
5627
|
// to selectively use glyph-level caching (separate vertices) only for clusters containing
|
|
5604
5628
|
// colored text, while non-colored clusters can still use fast cluster-level merging
|
|
5605
5629
|
let coloredTextIndices;
|
|
5606
|
-
if (options.color &&
|
|
5630
|
+
if (options.color &&
|
|
5631
|
+
typeof options.color === 'object' &&
|
|
5632
|
+
!Array.isArray(options.color)) {
|
|
5607
5633
|
if (options.color.byText || options.color.byCharRange) {
|
|
5608
5634
|
// Build the set manually since glyphs don't exist yet
|
|
5609
5635
|
coloredTextIndices = new Set();
|
|
@@ -5708,7 +5734,9 @@
|
|
|
5708
5734
|
const depthScale = this.loadedFont.upem / size;
|
|
5709
5735
|
const rawDepthInFontUnits = depth * depthScale;
|
|
5710
5736
|
const minExtrudeDepth = this.loadedFont.upem * 0.000025;
|
|
5711
|
-
const depthInFontUnits = rawDepthInFontUnits <= 0
|
|
5737
|
+
const depthInFontUnits = rawDepthInFontUnits <= 0
|
|
5738
|
+
? 0
|
|
5739
|
+
: Math.max(rawDepthInFontUnits, minExtrudeDepth);
|
|
5712
5740
|
if (!this.textLayout) {
|
|
5713
5741
|
this.textLayout = new TextLayout(this.loadedFont);
|
|
5714
5742
|
}
|
|
@@ -5880,10 +5908,12 @@
|
|
|
5880
5908
|
planeBounds.max.z *= finalScale;
|
|
5881
5909
|
for (let i = 0; i < glyphInfoArray.length; i++) {
|
|
5882
5910
|
const glyphInfo = glyphInfoArray[i];
|
|
5883
|
-
glyphInfo.bounds.min.x =
|
|
5911
|
+
glyphInfo.bounds.min.x =
|
|
5912
|
+
glyphInfo.bounds.min.x * finalScale + offsetScaled;
|
|
5884
5913
|
glyphInfo.bounds.min.y *= finalScale;
|
|
5885
5914
|
glyphInfo.bounds.min.z *= finalScale;
|
|
5886
|
-
glyphInfo.bounds.max.x =
|
|
5915
|
+
glyphInfo.bounds.max.x =
|
|
5916
|
+
glyphInfo.bounds.max.x * finalScale + offsetScaled;
|
|
5887
5917
|
glyphInfo.bounds.max.y *= finalScale;
|
|
5888
5918
|
glyphInfo.bounds.max.z *= finalScale;
|
|
5889
5919
|
}
|
|
@@ -5946,13 +5976,11 @@
|
|
|
5946
5976
|
static registerPattern(language, pattern) {
|
|
5947
5977
|
Text.patternCache.set(language, pattern);
|
|
5948
5978
|
}
|
|
5949
|
-
static clearFontCache() {
|
|
5950
|
-
Text.fontCache.clear();
|
|
5951
|
-
Text.fontCacheMemoryBytes = 0;
|
|
5952
|
-
}
|
|
5953
5979
|
static setMaxFontCacheMemoryMB(limitMB) {
|
|
5954
5980
|
Text.maxFontCacheMemoryBytes =
|
|
5955
|
-
limitMB === Infinity
|
|
5981
|
+
limitMB === Infinity
|
|
5982
|
+
? Infinity
|
|
5983
|
+
: Math.max(1, Math.floor(limitMB)) * 1024 * 1024;
|
|
5956
5984
|
Text.enforceFontCacheMemoryLimit();
|
|
5957
5985
|
}
|
|
5958
5986
|
getLoadedFont() {
|