three-text 0.2.12 → 0.2.13
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 +1 -1
- package/dist/index.cjs +130 -64
- package/dist/index.js +130 -64
- package/dist/index.min.cjs +2 -2
- package/dist/index.min.js +2 -2
- package/dist/index.umd.js +130 -64
- package/dist/index.umd.min.js +2 -2
- package/dist/types/core/cache/GlyphGeometryBuilder.d.ts +2 -0
- package/package.json +1 -1
package/dist/index.umd.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* three-text v0.2.
|
|
2
|
+
* three-text v0.2.13
|
|
3
3
|
* Copyright (C) 2025 Countertype LLC
|
|
4
4
|
*
|
|
5
5
|
* This program is free software: you can redistribute it and/or modify
|
|
@@ -2590,7 +2590,6 @@
|
|
|
2590
2590
|
vertices.push(points[i], points[i + 1], 0);
|
|
2591
2591
|
normals.push(0, 0, -1);
|
|
2592
2592
|
}
|
|
2593
|
-
// Add triangle indices
|
|
2594
2593
|
for (let i = 0; i < triangleIndices.length; i++) {
|
|
2595
2594
|
indices.push(baseIndex + triangleIndices[i]);
|
|
2596
2595
|
}
|
|
@@ -2641,18 +2640,21 @@
|
|
|
2641
2640
|
perfLogger.start('BoundaryClusterer.cluster', {
|
|
2642
2641
|
glyphCount: glyphContoursList.length
|
|
2643
2642
|
});
|
|
2644
|
-
|
|
2643
|
+
const n = glyphContoursList.length;
|
|
2644
|
+
if (n === 0) {
|
|
2645
2645
|
perfLogger.end('BoundaryClusterer.cluster');
|
|
2646
2646
|
return [];
|
|
2647
2647
|
}
|
|
2648
|
+
if (n === 1) {
|
|
2649
|
+
perfLogger.end('BoundaryClusterer.cluster');
|
|
2650
|
+
return [[0]];
|
|
2651
|
+
}
|
|
2648
2652
|
const result = this.clusterSweepLine(glyphContoursList, positions);
|
|
2649
2653
|
perfLogger.end('BoundaryClusterer.cluster');
|
|
2650
2654
|
return result;
|
|
2651
2655
|
}
|
|
2652
2656
|
clusterSweepLine(glyphContoursList, positions) {
|
|
2653
2657
|
const n = glyphContoursList.length;
|
|
2654
|
-
if (n <= 1)
|
|
2655
|
-
return n === 0 ? [] : [[0]];
|
|
2656
2658
|
const bounds = new Array(n);
|
|
2657
2659
|
const events = new Array(2 * n);
|
|
2658
2660
|
let eventIndex = 0;
|
|
@@ -2672,7 +2674,6 @@
|
|
|
2672
2674
|
const py = find(y);
|
|
2673
2675
|
if (px === py)
|
|
2674
2676
|
return;
|
|
2675
|
-
// Union by rank, attach smaller tree under larger tree
|
|
2676
2677
|
if (rank[px] < rank[py]) {
|
|
2677
2678
|
parent[px] = py;
|
|
2678
2679
|
}
|
|
@@ -2688,8 +2689,6 @@
|
|
|
2688
2689
|
for (const [, eventType, glyphIndex] of events) {
|
|
2689
2690
|
if (eventType === 0) {
|
|
2690
2691
|
const bounds1 = bounds[glyphIndex];
|
|
2691
|
-
// Check y-overlap with all currently active glyphs
|
|
2692
|
-
// (x-overlap is guaranteed by the sweep line)
|
|
2693
2692
|
for (const activeIndex of active) {
|
|
2694
2693
|
const bounds2 = bounds[activeIndex];
|
|
2695
2694
|
if (bounds1.minY < bounds2.maxY + OVERLAP_EPSILON &&
|
|
@@ -2706,10 +2705,12 @@
|
|
|
2706
2705
|
const clusters = new Map();
|
|
2707
2706
|
for (let i = 0; i < n; i++) {
|
|
2708
2707
|
const root = find(i);
|
|
2709
|
-
|
|
2710
|
-
|
|
2708
|
+
let list = clusters.get(root);
|
|
2709
|
+
if (!list) {
|
|
2710
|
+
list = [];
|
|
2711
|
+
clusters.set(root, list);
|
|
2711
2712
|
}
|
|
2712
|
-
|
|
2713
|
+
list.push(i);
|
|
2713
2714
|
}
|
|
2714
2715
|
return Array.from(clusters.values());
|
|
2715
2716
|
}
|
|
@@ -3762,6 +3763,10 @@
|
|
|
3762
3763
|
return size;
|
|
3763
3764
|
}
|
|
3764
3765
|
});
|
|
3766
|
+
this.clusteringCache = new LRUCache({
|
|
3767
|
+
maxEntries: 2000,
|
|
3768
|
+
calculateSize: () => 1
|
|
3769
|
+
});
|
|
3765
3770
|
}
|
|
3766
3771
|
getOptimizationStats() {
|
|
3767
3772
|
return this.collector.getOptimizationStats();
|
|
@@ -3800,70 +3805,115 @@
|
|
|
3800
3805
|
clusterGlyphContours.push(this.getContoursForGlyph(glyph.g));
|
|
3801
3806
|
}
|
|
3802
3807
|
// Step 2: Check for overlaps within the cluster
|
|
3803
|
-
|
|
3804
|
-
|
|
3808
|
+
let boundaryGroups;
|
|
3809
|
+
if (cluster.glyphs.length <= 1) {
|
|
3810
|
+
boundaryGroups = [[0]];
|
|
3811
|
+
}
|
|
3812
|
+
else {
|
|
3813
|
+
// Check clustering cache (same text + glyph IDs = same overlap groups)
|
|
3814
|
+
const cacheKey = cluster.text;
|
|
3815
|
+
const cached = this.clusteringCache.get(cacheKey);
|
|
3816
|
+
let isValid = false;
|
|
3817
|
+
if (cached && cached.glyphIds.length === cluster.glyphs.length) {
|
|
3818
|
+
isValid = true;
|
|
3819
|
+
for (let i = 0; i < cluster.glyphs.length; i++) {
|
|
3820
|
+
if (cached.glyphIds[i] !== cluster.glyphs[i].g) {
|
|
3821
|
+
isValid = false;
|
|
3822
|
+
break;
|
|
3823
|
+
}
|
|
3824
|
+
}
|
|
3825
|
+
}
|
|
3826
|
+
if (isValid && cached) {
|
|
3827
|
+
boundaryGroups = cached.groups;
|
|
3828
|
+
}
|
|
3829
|
+
else {
|
|
3830
|
+
const relativePositions = cluster.glyphs.map((g) => new Vec3(g.x, g.y, 0));
|
|
3831
|
+
boundaryGroups = this.clusterer.cluster(clusterGlyphContours, relativePositions);
|
|
3832
|
+
this.clusteringCache.set(cacheKey, {
|
|
3833
|
+
glyphIds: cluster.glyphs.map(g => g.g),
|
|
3834
|
+
groups: boundaryGroups
|
|
3835
|
+
});
|
|
3836
|
+
}
|
|
3837
|
+
}
|
|
3805
3838
|
const clusterHasColoredGlyphs = coloredTextIndices &&
|
|
3806
3839
|
cluster.glyphs.some((g) => coloredTextIndices.has(g.absoluteTextIndex));
|
|
3807
3840
|
// Force glyph-level caching if:
|
|
3808
3841
|
// - separateGlyphs flag is set (for shader attributes), OR
|
|
3809
3842
|
// - cluster contains selectively colored text (needs separate vertex ranges per glyph)
|
|
3810
3843
|
const forceSeparate = separateGlyphs || clusterHasColoredGlyphs;
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
const
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
const
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3844
|
+
// Iterate over the geometric groups identified by BoundaryClusterer
|
|
3845
|
+
// logical groups (words) are now split into geometric sub-groups (e.g. "aa", "XX", "bb")
|
|
3846
|
+
for (const groupIndices of boundaryGroups) {
|
|
3847
|
+
const isOverlappingGroup = groupIndices.length > 1;
|
|
3848
|
+
const shouldCluster = isOverlappingGroup && !forceSeparate;
|
|
3849
|
+
if (shouldCluster) {
|
|
3850
|
+
// Cluster-level caching for this specific group of overlapping glyphs
|
|
3851
|
+
const subClusterGlyphs = groupIndices.map((i) => cluster.glyphs[i]);
|
|
3852
|
+
const clusterKey = this.getClusterKey(subClusterGlyphs, depth, removeOverlaps);
|
|
3853
|
+
let cachedCluster = this.wordCache.get(clusterKey);
|
|
3854
|
+
if (!cachedCluster) {
|
|
3855
|
+
const clusterPaths = [];
|
|
3856
|
+
const refX = subClusterGlyphs[0].x ?? 0;
|
|
3857
|
+
const refY = subClusterGlyphs[0].y ?? 0;
|
|
3858
|
+
for (let i = 0; i < groupIndices.length; i++) {
|
|
3859
|
+
const originalIndex = groupIndices[i];
|
|
3860
|
+
const glyphContours = clusterGlyphContours[originalIndex];
|
|
3861
|
+
const glyph = cluster.glyphs[originalIndex];
|
|
3862
|
+
// Position relative to the sub-cluster start
|
|
3863
|
+
const relX = (glyph.x ?? 0) - refX;
|
|
3864
|
+
const relY = (glyph.y ?? 0) - refY;
|
|
3865
|
+
for (const path of glyphContours.paths) {
|
|
3866
|
+
clusterPaths.push({
|
|
3867
|
+
...path,
|
|
3868
|
+
points: path.points.map((p) => new Vec2(p.x + relX, p.y + relY))
|
|
3869
|
+
});
|
|
3870
|
+
}
|
|
3828
3871
|
}
|
|
3872
|
+
cachedCluster = this.tessellateGlyphCluster(clusterPaths, depth, isCFF);
|
|
3873
|
+
this.wordCache.set(clusterKey, cachedCluster);
|
|
3829
3874
|
}
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
else {
|
|
3846
|
-
// Glyph-level caching
|
|
3847
|
-
for (let i = 0; i < cluster.glyphs.length; i++) {
|
|
3848
|
-
const glyph = cluster.glyphs[i];
|
|
3849
|
-
const glyphContours = clusterGlyphContours[i];
|
|
3850
|
-
const glyphPosition = new Vec3(cluster.position.x + (glyph.x ?? 0), cluster.position.y + (glyph.y ?? 0), cluster.position.z);
|
|
3851
|
-
// Skip glyphs with no paths (spaces, zero-width characters, etc.)
|
|
3852
|
-
if (glyphContours.paths.length === 0) {
|
|
3853
|
-
const glyphInfo = this.createGlyphInfo(glyph, 0, 0, glyphPosition, glyphContours, depth);
|
|
3875
|
+
// Calculate the absolute position of this sub-cluster based on its first glyph
|
|
3876
|
+
// (since the cached geometry is relative to that first glyph)
|
|
3877
|
+
const firstGlyphInGroup = subClusterGlyphs[0];
|
|
3878
|
+
const groupPosition = new Vec3(cluster.position.x + (firstGlyphInGroup.x ?? 0), cluster.position.y + (firstGlyphInGroup.y ?? 0), cluster.position.z);
|
|
3879
|
+
const vertexOffset = vertices.length / 3;
|
|
3880
|
+
this.appendGeometry(vertices, normals, indices, cachedCluster, groupPosition, vertexOffset);
|
|
3881
|
+
const clusterVertexCount = cachedCluster.vertices.length / 3;
|
|
3882
|
+
// Register glyph infos for all glyphs in this sub-cluster
|
|
3883
|
+
// They all point to the same merged geometry
|
|
3884
|
+
for (let i = 0; i < groupIndices.length; i++) {
|
|
3885
|
+
const originalIndex = groupIndices[i];
|
|
3886
|
+
const glyph = cluster.glyphs[originalIndex];
|
|
3887
|
+
const glyphContours = clusterGlyphContours[originalIndex];
|
|
3888
|
+
const absoluteGlyphPosition = new Vec3(cluster.position.x + (glyph.x ?? 0), cluster.position.y + (glyph.y ?? 0), cluster.position.z);
|
|
3889
|
+
const glyphInfo = this.createGlyphInfo(glyph, vertexOffset, clusterVertexCount, absoluteGlyphPosition, glyphContours, depth);
|
|
3854
3890
|
glyphInfos.push(glyphInfo);
|
|
3855
|
-
|
|
3891
|
+
this.updatePlaneBounds(glyphInfo.bounds, planeBounds);
|
|
3856
3892
|
}
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3893
|
+
}
|
|
3894
|
+
else {
|
|
3895
|
+
// Glyph-level caching (standard path for isolated glyphs or when forced separate)
|
|
3896
|
+
for (const i of groupIndices) {
|
|
3897
|
+
const glyph = cluster.glyphs[i];
|
|
3898
|
+
const glyphContours = clusterGlyphContours[i];
|
|
3899
|
+
const glyphPosition = new Vec3(cluster.position.x + (glyph.x ?? 0), cluster.position.y + (glyph.y ?? 0), cluster.position.z);
|
|
3900
|
+
// Skip glyphs with no paths (spaces, zero-width characters, etc.)
|
|
3901
|
+
if (glyphContours.paths.length === 0) {
|
|
3902
|
+
const glyphInfo = this.createGlyphInfo(glyph, 0, 0, glyphPosition, glyphContours, depth);
|
|
3903
|
+
glyphInfos.push(glyphInfo);
|
|
3904
|
+
continue;
|
|
3905
|
+
}
|
|
3906
|
+
let cachedGlyph = this.cache.get(this.fontId, glyph.g, depth, removeOverlaps);
|
|
3907
|
+
if (!cachedGlyph) {
|
|
3908
|
+
cachedGlyph = this.tessellateGlyph(glyphContours, depth, removeOverlaps, isCFF);
|
|
3909
|
+
this.cache.set(this.fontId, glyph.g, depth, removeOverlaps, cachedGlyph);
|
|
3910
|
+
}
|
|
3911
|
+
const vertexOffset = vertices.length / 3;
|
|
3912
|
+
this.appendGeometry(vertices, normals, indices, cachedGlyph, glyphPosition, vertexOffset);
|
|
3913
|
+
const glyphInfo = this.createGlyphInfo(glyph, vertexOffset, cachedGlyph.vertices.length / 3, glyphPosition, glyphContours, depth);
|
|
3914
|
+
glyphInfos.push(glyphInfo);
|
|
3915
|
+
this.updatePlaneBounds(glyphInfo.bounds, planeBounds);
|
|
3861
3916
|
}
|
|
3862
|
-
const vertexOffset = vertices.length / 3;
|
|
3863
|
-
this.appendGeometry(vertices, normals, indices, cachedGlyph, glyphPosition, vertexOffset);
|
|
3864
|
-
const glyphInfo = this.createGlyphInfo(glyph, vertexOffset, cachedGlyph.vertices.length / 3, glyphPosition, glyphContours, depth);
|
|
3865
|
-
glyphInfos.push(glyphInfo);
|
|
3866
|
-
this.updatePlaneBounds(glyphInfo.bounds, planeBounds);
|
|
3867
3917
|
}
|
|
3868
3918
|
}
|
|
3869
3919
|
}
|
|
@@ -3880,6 +3930,21 @@
|
|
|
3880
3930
|
planeBounds
|
|
3881
3931
|
};
|
|
3882
3932
|
}
|
|
3933
|
+
getClusterKey(glyphs, depth, removeOverlaps) {
|
|
3934
|
+
if (glyphs.length === 0)
|
|
3935
|
+
return '';
|
|
3936
|
+
// Normalize positions relative to the first glyph in the cluster
|
|
3937
|
+
const refX = glyphs[0].x ?? 0;
|
|
3938
|
+
const refY = glyphs[0].y ?? 0;
|
|
3939
|
+
const parts = glyphs.map((g) => {
|
|
3940
|
+
const relX = (g.x ?? 0) - refX;
|
|
3941
|
+
const relY = (g.y ?? 0) - refY;
|
|
3942
|
+
return `${g.g}:${relX},${relY}`;
|
|
3943
|
+
});
|
|
3944
|
+
const ids = parts.join('|');
|
|
3945
|
+
const roundedDepth = Math.round(depth * 1000) / 1000;
|
|
3946
|
+
return `${this.fontId}_${ids}_${roundedDepth}_${removeOverlaps}`;
|
|
3947
|
+
}
|
|
3883
3948
|
appendGeometry(vertices, normals, indices, data, position, offset) {
|
|
3884
3949
|
for (let j = 0; j < data.vertices.length; j += 3) {
|
|
3885
3950
|
vertices.push(data.vertices[j] + position.x, data.vertices[j + 1] + position.y, data.vertices[j + 2] + position.z);
|
|
@@ -4004,6 +4069,7 @@
|
|
|
4004
4069
|
clearCache() {
|
|
4005
4070
|
this.cache.clear();
|
|
4006
4071
|
this.wordCache.clear();
|
|
4072
|
+
this.clusteringCache.clear();
|
|
4007
4073
|
}
|
|
4008
4074
|
}
|
|
4009
4075
|
|