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/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://www.typescriptlang.org/)
|
|
5
5
|
[](https://www.gnu.org/licenses/agpl-3.0)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
High fidelity 3D mesh font geometry and text layout engine for the web
|
|
8
8
|
|
|
9
9
|

|
|
10
10
|
|
|
@@ -15,11 +15,7 @@ A high fidelity 3D font renderer and text layout engine for the web
|
|
|
15
15
|
> [!CAUTION]
|
|
16
16
|
> three-text is an alpha release and the API may break rapidly. This warning will last at least through the end of 2025. If API stability is important to you, consider pinning your version. Community feedback is encouraged; please open an issue if you have any suggestions or feedback, thank you
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
text: `three-text is a 3D font geometry and text layout library for the web. Its supports TTF, OTF, and WOFF font files. For layout, it uses Tex-based parameters for breaking text into paragraphs across multiple lines and supports CJK and RTL scripts. three-text caches the geometries it generates for low CPU overhead in languages with lots of repeating glyphs. Variable fonts are supported as static instances at a given axis coordinate, and can be animated by re-drawing each frame with new coordinates. The library has a framework-agnostic core that returns raw vertex data, with lightweight adapters for Three.js, React Three Fiber, p5.js, WebGL and WebGPU. Under the hood, three-text relies on HarfBuzz for text shaping, Knuth-Plass line breaking, Liang hyphenation, libtess by Eric Veach for tessellation, curve polygonization from Maxim Shemanarev's Anti-Grain Geometry, and Visvalingam-Whyatt line simplification`,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
**three-text** is a 3D font geometry and text layout library for the web. It supports TTF, OTF, and WOFF font files. For layout, it uses [TeX](https://en.wikipedia.org/wiki/TeX)-based parameters for breaking text into paragraphs across multiple lines and supports CJK and RTL scripts. three-text caches the geometries it generates for low CPU overhead in languages with lots of repeating glyphs. Variable fonts are supported as static instances at a given axis coordinate, and can be animated by re-drawing each frame with new coordinates
|
|
18
|
+
**three-text** is a 3D mesh font geometry and text layout library for the web. It supports TTF, OTF, and WOFF font files. For layout, it uses [TeX](https://en.wikipedia.org/wiki/TeX)-based parameters for breaking text into paragraphs across multiple lines and supports CJK and RTL scripts. three-text caches the geometries it generates for low CPU overhead in languages with lots of repeating glyphs. Variable fonts are supported as static instances at a given axis coordinate, and can be animated by re-drawing each frame with new coordinates
|
|
23
19
|
|
|
24
20
|
The library has a framework-agnostic core that returns raw vertex data, with lightweight adapters for [Three.js](https://threejs.org), [React Three Fiber](https://docs.pmnd.rs/react-three-fiber), [p5.js](https://p5js.org), [WebGL](https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API), and [WebGPU](https://developer.mozilla.org/en-US/docs/Web/API/WebGPU_API)
|
|
25
21
|
|
|
@@ -556,7 +552,7 @@ const text = await Text.create({
|
|
|
556
552
|
|
|
557
553
|
Values can be boolean (`true`/`false`) to enable or disable, or numeric for features accepting variant indices. Explicitly disabling a feature overrides the font's defaults
|
|
558
554
|
|
|
559
|
-
Common tags include [`liga`](https://learn.microsoft.com/en-us/typography/opentype/spec/features_ko#liga) (ligatures),
|
|
555
|
+
Common tags include [`liga`](https://learn.microsoft.com/en-us/typography/opentype/spec/features_ko#liga) (ligatures), [`calt`](https://learn.microsoft.com/en-us/typography/opentype/spec/features_ae#calt) (contextual alternates), [`tnum`](https://learn.microsoft.com/en-us/typography/opentype/spec/features_pt#tnum) (tabular numbers), sylistic alternates [`ss01`-`ss20`](https://learn.microsoft.com/en-us/typography/opentype/spec/features_pt#ss01--ss20) and character variants [`cv01`-`cv99`](https://learn.microsoft.com/en-us/typography/opentype/spec/features_ae#cv01--cv99). Feature availability depends on the font
|
|
560
556
|
|
|
561
557
|
### Per-glyph attributes
|
|
562
558
|
|
|
@@ -736,6 +732,10 @@ Initializes HarfBuzz WebAssembly. Called automatically by `create()`, but can be
|
|
|
736
732
|
|
|
737
733
|
Preloads hyphenation patterns for specified languages. Useful for avoiding async pattern loading during text rendering
|
|
738
734
|
|
|
735
|
+
##### `Text.setMaxFontCacheMemoryMB(limitMB: number): void`
|
|
736
|
+
|
|
737
|
+
Sets an upper bound for the font cache, measured by the raw font buffer size, eviction is FIFO by insertion order
|
|
738
|
+
|
|
739
739
|
#### Instance Methods
|
|
740
740
|
|
|
741
741
|
The following methods are available on instances created by `Text.create()`:
|
|
@@ -753,7 +753,7 @@ Below are the most important configuration interfaces. For a complete list of al
|
|
|
753
753
|
```typescript
|
|
754
754
|
interface TextOptions {
|
|
755
755
|
text: string; // Text content to render
|
|
756
|
-
font
|
|
756
|
+
font: string | ArrayBuffer; // Font file path or buffer (TTF, OTF, or WOFF)
|
|
757
757
|
size?: number; // Font size in scene units (default: 72)
|
|
758
758
|
depth?: number; // Extrusion depth (default: 0)
|
|
759
759
|
lineHeight?: number; // Line height multiplier (default: 1.0)
|
package/dist/index.cjs
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
|
|
@@ -81,7 +81,9 @@ class PerformanceLogger {
|
|
|
81
81
|
// Find the metric in reverse order (most recent first)
|
|
82
82
|
for (let i = this.metrics.length - 1; i >= 0; i--) {
|
|
83
83
|
const metric = this.metrics[i];
|
|
84
|
-
if (metric.name === name &&
|
|
84
|
+
if (metric.name === name &&
|
|
85
|
+
metric.startTime === startTime &&
|
|
86
|
+
!metric.endTime) {
|
|
85
87
|
metric.endTime = endTime;
|
|
86
88
|
metric.duration = duration;
|
|
87
89
|
break;
|
|
@@ -469,7 +471,9 @@ class LineBreak {
|
|
|
469
471
|
const char = chars[i];
|
|
470
472
|
const nextChar = i < chars.length - 1 ? chars[i + 1] : null;
|
|
471
473
|
if (/\s/.test(char)) {
|
|
472
|
-
const width = widths
|
|
474
|
+
const width = widths
|
|
475
|
+
? (widths[i] ?? measureText(char))
|
|
476
|
+
: measureText(char);
|
|
473
477
|
items.push({
|
|
474
478
|
type: ItemType.GLUE,
|
|
475
479
|
width,
|
|
@@ -842,7 +846,9 @@ class LineBreak {
|
|
|
842
846
|
if (breaks.length === 0) {
|
|
843
847
|
// For first emergency attempt, use initialEmergencyStretch
|
|
844
848
|
// For subsequent iterations (short line detection), progressively increase
|
|
845
|
-
currentEmergencyStretch =
|
|
849
|
+
currentEmergencyStretch =
|
|
850
|
+
initialEmergencyStretch +
|
|
851
|
+
iteration * width * SHORT_LINE_EMERGENCY_STRETCH_INCREMENT;
|
|
846
852
|
breaks = LineBreak.findBreakpoints(currentItems, width, tolerance, looseness, true, currentEmergencyStretch, context);
|
|
847
853
|
}
|
|
848
854
|
// Last resort: allow higher badness (but not infinite)
|
|
@@ -1723,12 +1729,12 @@ class FontMetadataExtractor {
|
|
|
1723
1729
|
try {
|
|
1724
1730
|
if (gsubTableOffset) {
|
|
1725
1731
|
const gsubData = this.extractFeatureDataFromTable(view, gsubTableOffset, nameTableOffset);
|
|
1726
|
-
gsubData.features.forEach(f => features.add(f));
|
|
1732
|
+
gsubData.features.forEach((f) => features.add(f));
|
|
1727
1733
|
Object.assign(featureNames, gsubData.names);
|
|
1728
1734
|
}
|
|
1729
1735
|
if (gposTableOffset) {
|
|
1730
1736
|
const gposData = this.extractFeatureDataFromTable(view, gposTableOffset, nameTableOffset);
|
|
1731
|
-
gposData.features.forEach(f => features.add(f));
|
|
1737
|
+
gposData.features.forEach((f) => features.add(f));
|
|
1732
1738
|
Object.assign(featureNames, gposData.names);
|
|
1733
1739
|
}
|
|
1734
1740
|
}
|
|
@@ -2727,7 +2733,9 @@ class Tessellator {
|
|
|
2727
2733
|
tessContours = originalContours;
|
|
2728
2734
|
}
|
|
2729
2735
|
let extrusionContours = needsExtrusionContours
|
|
2730
|
-
?
|
|
2736
|
+
? needsWindingReversal
|
|
2737
|
+
? tessContours
|
|
2738
|
+
: (originalContours ?? this.pathsToContours(paths))
|
|
2731
2739
|
: [];
|
|
2732
2740
|
if (removeOverlaps) {
|
|
2733
2741
|
logger.log('Two-pass: boundary extraction then triangulation');
|
|
@@ -2749,24 +2757,6 @@ class Tessellator {
|
|
|
2749
2757
|
}
|
|
2750
2758
|
else {
|
|
2751
2759
|
logger.log(`Single-pass triangulation for ${isCFF ? 'CFF' : 'TTF'}`);
|
|
2752
|
-
// TTF contours may have inconsistent winding; check if we need normalization
|
|
2753
|
-
if (needsExtrusionContours && !isCFF) {
|
|
2754
|
-
const needsNormalization = this.needsWindingNormalization(extrusionContours);
|
|
2755
|
-
if (needsNormalization) {
|
|
2756
|
-
logger.log('Complex topology detected, running boundary pass for winding normalization');
|
|
2757
|
-
perfLogger.start('Tessellator.windingNormalization', {
|
|
2758
|
-
contourCount: extrusionContours.length
|
|
2759
|
-
});
|
|
2760
|
-
const boundaryResult = this.performTessellation(extrusionContours, 'boundary');
|
|
2761
|
-
perfLogger.end('Tessellator.windingNormalization');
|
|
2762
|
-
if (boundaryResult) {
|
|
2763
|
-
extrusionContours = this.boundaryToContours(boundaryResult);
|
|
2764
|
-
}
|
|
2765
|
-
}
|
|
2766
|
-
else {
|
|
2767
|
-
logger.log('Simple topology, skipping winding normalization');
|
|
2768
|
-
}
|
|
2769
|
-
}
|
|
2770
2760
|
}
|
|
2771
2761
|
perfLogger.start('Tessellator.triangulationPass', {
|
|
2772
2762
|
contourCount: tessContours.length
|
|
@@ -2778,7 +2768,10 @@ class Tessellator {
|
|
|
2778
2768
|
? 'libtess returned empty result from triangulation pass'
|
|
2779
2769
|
: 'libtess returned empty result from single-pass triangulation';
|
|
2780
2770
|
logger.warn(warning);
|
|
2781
|
-
return {
|
|
2771
|
+
return {
|
|
2772
|
+
triangles: { vertices: [], indices: [] },
|
|
2773
|
+
contours: extrusionContours
|
|
2774
|
+
};
|
|
2782
2775
|
}
|
|
2783
2776
|
return {
|
|
2784
2777
|
triangles: {
|
|
@@ -2948,28 +2941,58 @@ class Tessellator {
|
|
|
2948
2941
|
|
|
2949
2942
|
class Extruder {
|
|
2950
2943
|
constructor() { }
|
|
2944
|
+
packEdge(a, b) {
|
|
2945
|
+
const lo = a < b ? a : b;
|
|
2946
|
+
const hi = a < b ? b : a;
|
|
2947
|
+
return lo * 0x100000000 + hi;
|
|
2948
|
+
}
|
|
2951
2949
|
extrude(geometry, depth = 0, unitsPerEm) {
|
|
2952
2950
|
const points = geometry.triangles.vertices;
|
|
2953
2951
|
const triangleIndices = geometry.triangles.indices;
|
|
2954
2952
|
const numPoints = points.length / 2;
|
|
2955
|
-
// Count side
|
|
2956
|
-
let
|
|
2953
|
+
// Count boundary edges for side walls (4 vertices + 6 indices per edge)
|
|
2954
|
+
let boundaryEdges = [];
|
|
2957
2955
|
if (depth !== 0) {
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2956
|
+
const counts = new Map();
|
|
2957
|
+
const oriented = new Map();
|
|
2958
|
+
for (let i = 0; i < triangleIndices.length; i += 3) {
|
|
2959
|
+
const a = triangleIndices[i];
|
|
2960
|
+
const b = triangleIndices[i + 1];
|
|
2961
|
+
const c = triangleIndices[i + 2];
|
|
2962
|
+
const k0 = this.packEdge(a, b);
|
|
2963
|
+
const n0 = (counts.get(k0) ?? 0) + 1;
|
|
2964
|
+
counts.set(k0, n0);
|
|
2965
|
+
if (n0 === 1)
|
|
2966
|
+
oriented.set(k0, [a, b]);
|
|
2967
|
+
const k1 = this.packEdge(b, c);
|
|
2968
|
+
const n1 = (counts.get(k1) ?? 0) + 1;
|
|
2969
|
+
counts.set(k1, n1);
|
|
2970
|
+
if (n1 === 1)
|
|
2971
|
+
oriented.set(k1, [b, c]);
|
|
2972
|
+
const k2 = this.packEdge(c, a);
|
|
2973
|
+
const n2 = (counts.get(k2) ?? 0) + 1;
|
|
2974
|
+
counts.set(k2, n2);
|
|
2975
|
+
if (n2 === 1)
|
|
2976
|
+
oriented.set(k2, [c, a]);
|
|
2977
|
+
}
|
|
2978
|
+
boundaryEdges = [];
|
|
2979
|
+
for (const [key, count] of counts) {
|
|
2980
|
+
if (count !== 1)
|
|
2981
|
+
continue;
|
|
2982
|
+
const edge = oriented.get(key);
|
|
2983
|
+
if (edge)
|
|
2984
|
+
boundaryEdges.push(edge);
|
|
2963
2985
|
}
|
|
2964
2986
|
}
|
|
2965
|
-
const
|
|
2987
|
+
const sideEdgeCount = depth === 0 ? 0 : boundaryEdges.length;
|
|
2988
|
+
const sideVertexCount = depth === 0 ? 0 : sideEdgeCount * 4;
|
|
2966
2989
|
const baseVertexCount = depth === 0 ? numPoints : numPoints * 2;
|
|
2967
2990
|
const vertexCount = baseVertexCount + sideVertexCount;
|
|
2968
2991
|
const vertices = new Float32Array(vertexCount * 3);
|
|
2969
2992
|
const normals = new Float32Array(vertexCount * 3);
|
|
2970
2993
|
const indexCount = depth === 0
|
|
2971
2994
|
? triangleIndices.length
|
|
2972
|
-
: triangleIndices.length * 2 +
|
|
2995
|
+
: triangleIndices.length * 2 + sideEdgeCount * 6;
|
|
2973
2996
|
const indices = new Uint32Array(indexCount);
|
|
2974
2997
|
if (depth === 0) {
|
|
2975
2998
|
// Single-sided flat geometry at z=0
|
|
@@ -3025,60 +3048,62 @@ class Extruder {
|
|
|
3025
3048
|
// Side walls
|
|
3026
3049
|
let nextVertex = numPoints * 2;
|
|
3027
3050
|
let idxPos = triangleIndices.length * 2;
|
|
3028
|
-
for (
|
|
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
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3051
|
+
for (let e = 0; e < boundaryEdges.length; e++) {
|
|
3052
|
+
const [u, v] = boundaryEdges[e];
|
|
3053
|
+
const u2 = u * 2;
|
|
3054
|
+
const v2 = v * 2;
|
|
3055
|
+
const p0x = points[u2];
|
|
3056
|
+
const p0y = points[u2 + 1];
|
|
3057
|
+
const p1x = points[v2];
|
|
3058
|
+
const p1y = points[v2 + 1];
|
|
3059
|
+
// Perpendicular normal for this wall segment
|
|
3060
|
+
// Uses the edge direction from the cap triangulation so winding does not depend on contour direction
|
|
3061
|
+
const ex = p1x - p0x;
|
|
3062
|
+
const ey = p1y - p0y;
|
|
3063
|
+
const lenSq = ex * ex + ey * ey;
|
|
3064
|
+
let nx = 0;
|
|
3065
|
+
let ny = 0;
|
|
3066
|
+
if (lenSq > 0) {
|
|
3067
|
+
const invLen = 1 / Math.sqrt(lenSq);
|
|
3068
|
+
nx = ey * invLen;
|
|
3069
|
+
ny = -ex * invLen;
|
|
3070
|
+
}
|
|
3071
|
+
const baseVertex = nextVertex;
|
|
3072
|
+
const base = baseVertex * 3;
|
|
3073
|
+
// Wall quad: front edge at z=0, back edge at z=depth
|
|
3074
|
+
vertices[base] = p0x;
|
|
3075
|
+
vertices[base + 1] = p0y;
|
|
3076
|
+
vertices[base + 2] = 0;
|
|
3077
|
+
vertices[base + 3] = p1x;
|
|
3078
|
+
vertices[base + 4] = p1y;
|
|
3079
|
+
vertices[base + 5] = 0;
|
|
3080
|
+
vertices[base + 6] = p0x;
|
|
3081
|
+
vertices[base + 7] = p0y;
|
|
3082
|
+
vertices[base + 8] = backZ;
|
|
3083
|
+
vertices[base + 9] = p1x;
|
|
3084
|
+
vertices[base + 10] = p1y;
|
|
3085
|
+
vertices[base + 11] = backZ;
|
|
3086
|
+
// Wall normals point perpendicular to edge
|
|
3087
|
+
normals[base] = nx;
|
|
3088
|
+
normals[base + 1] = ny;
|
|
3089
|
+
normals[base + 2] = 0;
|
|
3090
|
+
normals[base + 3] = nx;
|
|
3091
|
+
normals[base + 4] = ny;
|
|
3092
|
+
normals[base + 5] = 0;
|
|
3093
|
+
normals[base + 6] = nx;
|
|
3094
|
+
normals[base + 7] = ny;
|
|
3095
|
+
normals[base + 8] = 0;
|
|
3096
|
+
normals[base + 9] = nx;
|
|
3097
|
+
normals[base + 10] = ny;
|
|
3098
|
+
normals[base + 11] = 0;
|
|
3099
|
+
// Two triangles per wall segment
|
|
3100
|
+
indices[idxPos++] = baseVertex;
|
|
3101
|
+
indices[idxPos++] = baseVertex + 1;
|
|
3102
|
+
indices[idxPos++] = baseVertex + 2;
|
|
3103
|
+
indices[idxPos++] = baseVertex + 1;
|
|
3104
|
+
indices[idxPos++] = baseVertex + 3;
|
|
3105
|
+
indices[idxPos++] = baseVertex + 2;
|
|
3106
|
+
nextVertex += 4;
|
|
3082
3107
|
}
|
|
3083
3108
|
return { vertices, normals, indices };
|
|
3084
3109
|
}
|
|
@@ -3403,9 +3428,7 @@ class PathOptimizer {
|
|
|
3403
3428
|
const v1LenSq = v1x * v1x + v1y * v1y;
|
|
3404
3429
|
const v2LenSq = v2x * v2x + v2y * v2y;
|
|
3405
3430
|
const minLenSq = this.config.minSegmentLength * this.config.minSegmentLength;
|
|
3406
|
-
if (angle > threshold ||
|
|
3407
|
-
v1LenSq < minLenSq ||
|
|
3408
|
-
v2LenSq < minLenSq) {
|
|
3431
|
+
if (angle > threshold || v1LenSq < minLenSq || v2LenSq < minLenSq) {
|
|
3409
3432
|
result.push(current);
|
|
3410
3433
|
}
|
|
3411
3434
|
else {
|
|
@@ -4167,7 +4190,7 @@ class GlyphGeometryBuilder {
|
|
|
4167
4190
|
const relativePositions = cluster.glyphs.map((g) => new Vec3(g.x ?? 0, g.y ?? 0, 0));
|
|
4168
4191
|
boundaryGroups = this.clusterer.cluster(clusterGlyphContours, relativePositions);
|
|
4169
4192
|
this.clusteringCache.set(cacheKey, {
|
|
4170
|
-
glyphIds: cluster.glyphs.map(g => g.g),
|
|
4193
|
+
glyphIds: cluster.glyphs.map((g) => g.g),
|
|
4171
4194
|
groups: boundaryGroups
|
|
4172
4195
|
});
|
|
4173
4196
|
}
|
|
@@ -4177,7 +4200,7 @@ class GlyphGeometryBuilder {
|
|
|
4177
4200
|
// Use glyph-level caching when separateGlyphs is set or when cluster contains colored text
|
|
4178
4201
|
const forceSeparate = separateGlyphs || clusterHasColoredGlyphs;
|
|
4179
4202
|
// Iterate over the geometric groups identified by BoundaryClusterer
|
|
4180
|
-
// logical groups (words) split into geometric sub-groups
|
|
4203
|
+
// logical groups (words) split into geometric sub-groups
|
|
4181
4204
|
for (const groupIndices of boundaryGroups) {
|
|
4182
4205
|
const isOverlappingGroup = groupIndices.length > 1;
|
|
4183
4206
|
const shouldCluster = isOverlappingGroup && !forceSeparate;
|
|
@@ -4557,7 +4580,8 @@ class TextShaper {
|
|
|
4557
4580
|
if (LineBreak.isCJOpeningPunctuation(currentChar)) {
|
|
4558
4581
|
shouldApply = false;
|
|
4559
4582
|
}
|
|
4560
|
-
if (LineBreak.isCJPunctuation(currentChar) &&
|
|
4583
|
+
if (LineBreak.isCJPunctuation(currentChar) &&
|
|
4584
|
+
LineBreak.isCJPunctuation(nextChar)) {
|
|
4561
4585
|
shouldApply = false;
|
|
4562
4586
|
}
|
|
4563
4587
|
if (shouldApply) {
|
|
@@ -5361,7 +5385,7 @@ class Text {
|
|
|
5361
5385
|
// Stringify with sorted keys for cache stability
|
|
5362
5386
|
static stableStringify(obj) {
|
|
5363
5387
|
const keys = Object.keys(obj).sort();
|
|
5364
|
-
const pairs = keys.map(k => `${k}:${obj[k]}`);
|
|
5388
|
+
const pairs = keys.map((k) => `${k}:${obj[k]}`);
|
|
5365
5389
|
return pairs.join(',');
|
|
5366
5390
|
}
|
|
5367
5391
|
constructor() {
|
|
@@ -5599,7 +5623,9 @@ class Text {
|
|
|
5599
5623
|
// to selectively use glyph-level caching (separate vertices) only for clusters containing
|
|
5600
5624
|
// colored text, while non-colored clusters can still use fast cluster-level merging
|
|
5601
5625
|
let coloredTextIndices;
|
|
5602
|
-
if (options.color &&
|
|
5626
|
+
if (options.color &&
|
|
5627
|
+
typeof options.color === 'object' &&
|
|
5628
|
+
!Array.isArray(options.color)) {
|
|
5603
5629
|
if (options.color.byText || options.color.byCharRange) {
|
|
5604
5630
|
// Build the set manually since glyphs don't exist yet
|
|
5605
5631
|
coloredTextIndices = new Set();
|
|
@@ -5704,7 +5730,9 @@ class Text {
|
|
|
5704
5730
|
const depthScale = this.loadedFont.upem / size;
|
|
5705
5731
|
const rawDepthInFontUnits = depth * depthScale;
|
|
5706
5732
|
const minExtrudeDepth = this.loadedFont.upem * 0.000025;
|
|
5707
|
-
const depthInFontUnits = rawDepthInFontUnits <= 0
|
|
5733
|
+
const depthInFontUnits = rawDepthInFontUnits <= 0
|
|
5734
|
+
? 0
|
|
5735
|
+
: Math.max(rawDepthInFontUnits, minExtrudeDepth);
|
|
5708
5736
|
if (!this.textLayout) {
|
|
5709
5737
|
this.textLayout = new TextLayout(this.loadedFont);
|
|
5710
5738
|
}
|
|
@@ -5876,10 +5904,12 @@ class Text {
|
|
|
5876
5904
|
planeBounds.max.z *= finalScale;
|
|
5877
5905
|
for (let i = 0; i < glyphInfoArray.length; i++) {
|
|
5878
5906
|
const glyphInfo = glyphInfoArray[i];
|
|
5879
|
-
glyphInfo.bounds.min.x =
|
|
5907
|
+
glyphInfo.bounds.min.x =
|
|
5908
|
+
glyphInfo.bounds.min.x * finalScale + offsetScaled;
|
|
5880
5909
|
glyphInfo.bounds.min.y *= finalScale;
|
|
5881
5910
|
glyphInfo.bounds.min.z *= finalScale;
|
|
5882
|
-
glyphInfo.bounds.max.x =
|
|
5911
|
+
glyphInfo.bounds.max.x =
|
|
5912
|
+
glyphInfo.bounds.max.x * finalScale + offsetScaled;
|
|
5883
5913
|
glyphInfo.bounds.max.y *= finalScale;
|
|
5884
5914
|
glyphInfo.bounds.max.z *= finalScale;
|
|
5885
5915
|
}
|
|
@@ -5942,13 +5972,11 @@ class Text {
|
|
|
5942
5972
|
static registerPattern(language, pattern) {
|
|
5943
5973
|
Text.patternCache.set(language, pattern);
|
|
5944
5974
|
}
|
|
5945
|
-
static clearFontCache() {
|
|
5946
|
-
Text.fontCache.clear();
|
|
5947
|
-
Text.fontCacheMemoryBytes = 0;
|
|
5948
|
-
}
|
|
5949
5975
|
static setMaxFontCacheMemoryMB(limitMB) {
|
|
5950
5976
|
Text.maxFontCacheMemoryBytes =
|
|
5951
|
-
limitMB === Infinity
|
|
5977
|
+
limitMB === Infinity
|
|
5978
|
+
? Infinity
|
|
5979
|
+
: Math.max(1, Math.floor(limitMB)) * 1024 * 1024;
|
|
5952
5980
|
Text.enforceFontCacheMemoryLimit();
|
|
5953
5981
|
}
|
|
5954
5982
|
getLoadedFont() {
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,45 @@ interface HyphenationTrieNode {
|
|
|
5
5
|
};
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
interface LRUCacheOptions<K, V> {
|
|
9
|
+
maxEntries?: number;
|
|
10
|
+
maxMemoryBytes?: number;
|
|
11
|
+
calculateSize?: (value: V) => number;
|
|
12
|
+
onEvict?: (key: K, value: V) => void;
|
|
13
|
+
}
|
|
14
|
+
interface CacheStats {
|
|
15
|
+
hits: number;
|
|
16
|
+
misses: number;
|
|
17
|
+
evictions: number;
|
|
18
|
+
size: number;
|
|
19
|
+
memoryUsage: number;
|
|
20
|
+
}
|
|
21
|
+
declare class LRUCache<K, V> {
|
|
22
|
+
private cache;
|
|
23
|
+
private head;
|
|
24
|
+
private tail;
|
|
25
|
+
private stats;
|
|
26
|
+
private options;
|
|
27
|
+
constructor(options?: LRUCacheOptions<K, V>);
|
|
28
|
+
get(key: K): V | undefined;
|
|
29
|
+
has(key: K): boolean;
|
|
30
|
+
set(key: K, value: V): void;
|
|
31
|
+
delete(key: K): boolean;
|
|
32
|
+
clear(): void;
|
|
33
|
+
getStats(): CacheStats & {
|
|
34
|
+
hitRate: number;
|
|
35
|
+
memoryUsageMB: number;
|
|
36
|
+
};
|
|
37
|
+
keys(): K[];
|
|
38
|
+
get size(): number;
|
|
39
|
+
private evictIfNeeded;
|
|
40
|
+
private evictTail;
|
|
41
|
+
private addToHead;
|
|
42
|
+
private removeNode;
|
|
43
|
+
private removeTail;
|
|
44
|
+
private moveToHead;
|
|
45
|
+
}
|
|
46
|
+
|
|
8
47
|
interface BoundingBox {
|
|
9
48
|
min: {
|
|
10
49
|
x: number;
|
|
@@ -221,13 +260,19 @@ interface TextGeometryInfo {
|
|
|
221
260
|
pointsRemovedByVisvalingam: number;
|
|
222
261
|
pointsRemovedByColinear: number;
|
|
223
262
|
originalPointCount: number;
|
|
224
|
-
}
|
|
263
|
+
} & Partial<CacheStats & {
|
|
264
|
+
hitRate: number;
|
|
265
|
+
memoryUsageMB: number;
|
|
266
|
+
}>;
|
|
225
267
|
query(options: TextQueryOptions): TextRange[];
|
|
226
268
|
coloredRanges?: ColoredRange[];
|
|
227
269
|
}
|
|
228
270
|
interface TextHandle extends TextGeometryInfo {
|
|
229
271
|
getLoadedFont(): LoadedFont | undefined;
|
|
230
|
-
getCacheStatistics():
|
|
272
|
+
getCacheStatistics(): (CacheStats & {
|
|
273
|
+
hitRate: number;
|
|
274
|
+
memoryUsageMB: number;
|
|
275
|
+
}) | null;
|
|
231
276
|
clearCache(): void;
|
|
232
277
|
measureTextWidth(text: string, letterSpacing?: number): number;
|
|
233
278
|
update(options: Partial<TextOptions>): Promise<TextHandle>;
|
|
@@ -266,7 +311,7 @@ interface ColoredRange {
|
|
|
266
311
|
}
|
|
267
312
|
interface TextOptions {
|
|
268
313
|
text: string;
|
|
269
|
-
font
|
|
314
|
+
font: string | ArrayBuffer;
|
|
270
315
|
size?: number;
|
|
271
316
|
depth?: number;
|
|
272
317
|
lineHeight?: number;
|
|
@@ -367,7 +412,6 @@ declare class Text {
|
|
|
367
412
|
getFontMetrics(): FontMetrics;
|
|
368
413
|
static preloadPatterns(languages: string[], patternsPath?: string): Promise<void>;
|
|
369
414
|
static registerPattern(language: string, pattern: HyphenationTrieNode): void;
|
|
370
|
-
static clearFontCache(): void;
|
|
371
415
|
static setMaxFontCacheMemoryMB(limitMB: number): void;
|
|
372
416
|
getLoadedFont(): LoadedFont | undefined;
|
|
373
417
|
measureTextWidth(text: string, letterSpacing?: number): number;
|
|
@@ -436,45 +480,6 @@ declare class FontMetadataExtractor {
|
|
|
436
480
|
static getFontMetrics(metrics: ExtractedMetrics): FontMetrics;
|
|
437
481
|
}
|
|
438
482
|
|
|
439
|
-
interface LRUCacheOptions<K, V> {
|
|
440
|
-
maxEntries?: number;
|
|
441
|
-
maxMemoryBytes?: number;
|
|
442
|
-
calculateSize?: (value: V) => number;
|
|
443
|
-
onEvict?: (key: K, value: V) => void;
|
|
444
|
-
}
|
|
445
|
-
interface CacheStats {
|
|
446
|
-
hits: number;
|
|
447
|
-
misses: number;
|
|
448
|
-
evictions: number;
|
|
449
|
-
size: number;
|
|
450
|
-
memoryUsage: number;
|
|
451
|
-
}
|
|
452
|
-
declare class LRUCache<K, V> {
|
|
453
|
-
private cache;
|
|
454
|
-
private head;
|
|
455
|
-
private tail;
|
|
456
|
-
private stats;
|
|
457
|
-
private options;
|
|
458
|
-
constructor(options?: LRUCacheOptions<K, V>);
|
|
459
|
-
get(key: K): V | undefined;
|
|
460
|
-
has(key: K): boolean;
|
|
461
|
-
set(key: K, value: V): void;
|
|
462
|
-
delete(key: K): boolean;
|
|
463
|
-
clear(): void;
|
|
464
|
-
getStats(): CacheStats & {
|
|
465
|
-
hitRate: number;
|
|
466
|
-
memoryUsageMB: number;
|
|
467
|
-
};
|
|
468
|
-
keys(): K[];
|
|
469
|
-
get size(): number;
|
|
470
|
-
private evictIfNeeded;
|
|
471
|
-
private evictTail;
|
|
472
|
-
private addToHead;
|
|
473
|
-
private removeNode;
|
|
474
|
-
private removeTail;
|
|
475
|
-
private moveToHead;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
483
|
declare const globalGlyphCache: LRUCache<string, GlyphData>;
|
|
479
484
|
declare function createGlyphCache(maxCacheSizeMB?: number): LRUCache<string, GlyphData>;
|
|
480
485
|
|