three-text 0.2.19 → 0.3.0
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 +6 -2
- package/dist/index.cjs +185 -332
- package/dist/index.d.ts +10 -38
- package/dist/index.js +185 -332
- package/dist/index.min.cjs +704 -723
- package/dist/index.min.js +705 -724
- package/dist/index.umd.js +185 -332
- package/dist/index.umd.min.js +710 -729
- package/dist/three/index.cjs +3 -1
- package/dist/three/index.d.ts +1 -12
- package/dist/three/index.js +3 -1
- package/dist/three/react.d.ts +5 -13
- package/dist/types/core/Text.d.ts +1 -4
- package/dist/types/core/cache/GlyphGeometryBuilder.d.ts +4 -7
- package/dist/types/core/cache/sharedCaches.d.ts +6 -7
- package/dist/types/core/geometry/Extruder.d.ts +0 -1
- package/dist/types/core/shaping/TextShaper.d.ts +1 -4
- package/dist/types/core/types.d.ts +5 -6
- package/dist/types/index.d.ts +1 -1
- package/dist/types/three/index.d.ts +1 -5
- package/dist/types/utils/Cache.d.ts +14 -0
- package/dist/types/webgl/index.d.ts +12 -0
- package/dist/types/webgpu/index.d.ts +10 -0
- package/dist/webgl/index.cjs +18 -0
- package/dist/webgl/index.d.ts +12 -0
- package/dist/webgl/index.js +18 -0
- package/dist/webgpu/index.cjs +80 -1
- package/dist/webgpu/index.d.ts +10 -0
- package/dist/webgpu/index.js +80 -1
- package/package.json +9 -4
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* three-text v0.
|
|
2
|
+
* three-text v0.3.0
|
|
3
3
|
* Copyright (C) 2025 Countertype LLC
|
|
4
4
|
*
|
|
5
5
|
* This program is free software: you can redistribute it and/or modify
|
|
@@ -2379,231 +2379,50 @@ class Vec3 {
|
|
|
2379
2379
|
}
|
|
2380
2380
|
}
|
|
2381
2381
|
|
|
2382
|
-
//
|
|
2383
|
-
class
|
|
2384
|
-
constructor(
|
|
2382
|
+
// Map-based cache with no eviction policy
|
|
2383
|
+
class Cache {
|
|
2384
|
+
constructor() {
|
|
2385
2385
|
this.cache = new Map();
|
|
2386
|
-
this.head = null;
|
|
2387
|
-
this.tail = null;
|
|
2388
|
-
this.stats = {
|
|
2389
|
-
hits: 0,
|
|
2390
|
-
misses: 0,
|
|
2391
|
-
evictions: 0,
|
|
2392
|
-
size: 0,
|
|
2393
|
-
memoryUsage: 0
|
|
2394
|
-
};
|
|
2395
|
-
this.options = {
|
|
2396
|
-
maxEntries: options.maxEntries ?? Infinity,
|
|
2397
|
-
maxMemoryBytes: options.maxMemoryBytes ?? Infinity,
|
|
2398
|
-
calculateSize: options.calculateSize ?? (() => 0),
|
|
2399
|
-
onEvict: options.onEvict
|
|
2400
|
-
};
|
|
2401
2386
|
}
|
|
2402
2387
|
get(key) {
|
|
2403
|
-
|
|
2404
|
-
if (node) {
|
|
2405
|
-
this.stats.hits++;
|
|
2406
|
-
this.moveToHead(node);
|
|
2407
|
-
return node.value;
|
|
2408
|
-
}
|
|
2409
|
-
else {
|
|
2410
|
-
this.stats.misses++;
|
|
2411
|
-
return undefined;
|
|
2412
|
-
}
|
|
2388
|
+
return this.cache.get(key);
|
|
2413
2389
|
}
|
|
2414
2390
|
has(key) {
|
|
2415
2391
|
return this.cache.has(key);
|
|
2416
2392
|
}
|
|
2417
2393
|
set(key, value) {
|
|
2418
|
-
|
|
2419
|
-
const existingNode = this.cache.get(key);
|
|
2420
|
-
if (existingNode) {
|
|
2421
|
-
const oldSize = this.options.calculateSize(existingNode.value);
|
|
2422
|
-
const newSize = this.options.calculateSize(value);
|
|
2423
|
-
this.stats.memoryUsage = this.stats.memoryUsage - oldSize + newSize;
|
|
2424
|
-
existingNode.value = value;
|
|
2425
|
-
this.moveToHead(existingNode);
|
|
2426
|
-
return;
|
|
2427
|
-
}
|
|
2428
|
-
const size = this.options.calculateSize(value);
|
|
2429
|
-
// Evict entries if we exceed limits
|
|
2430
|
-
this.evictIfNeeded(size);
|
|
2431
|
-
// Create new node
|
|
2432
|
-
const node = {
|
|
2433
|
-
key,
|
|
2434
|
-
value,
|
|
2435
|
-
prev: null,
|
|
2436
|
-
next: null
|
|
2437
|
-
};
|
|
2438
|
-
this.cache.set(key, node);
|
|
2439
|
-
this.addToHead(node);
|
|
2440
|
-
this.stats.size = this.cache.size;
|
|
2441
|
-
this.stats.memoryUsage += size;
|
|
2394
|
+
this.cache.set(key, value);
|
|
2442
2395
|
}
|
|
2443
2396
|
delete(key) {
|
|
2444
|
-
|
|
2445
|
-
if (!node)
|
|
2446
|
-
return false;
|
|
2447
|
-
const size = this.options.calculateSize(node.value);
|
|
2448
|
-
this.removeNode(node);
|
|
2449
|
-
this.cache.delete(key);
|
|
2450
|
-
this.stats.size = this.cache.size;
|
|
2451
|
-
this.stats.memoryUsage -= size;
|
|
2452
|
-
if (this.options.onEvict) {
|
|
2453
|
-
this.options.onEvict(key, node.value);
|
|
2454
|
-
}
|
|
2455
|
-
return true;
|
|
2397
|
+
return this.cache.delete(key);
|
|
2456
2398
|
}
|
|
2457
2399
|
clear() {
|
|
2458
|
-
if (this.options.onEvict) {
|
|
2459
|
-
for (const [key, node] of this.cache) {
|
|
2460
|
-
this.options.onEvict(key, node.value);
|
|
2461
|
-
}
|
|
2462
|
-
}
|
|
2463
2400
|
this.cache.clear();
|
|
2464
|
-
this.head = null;
|
|
2465
|
-
this.tail = null;
|
|
2466
|
-
this.stats = {
|
|
2467
|
-
hits: 0,
|
|
2468
|
-
misses: 0,
|
|
2469
|
-
evictions: 0,
|
|
2470
|
-
size: 0,
|
|
2471
|
-
memoryUsage: 0
|
|
2472
|
-
};
|
|
2473
|
-
}
|
|
2474
|
-
getStats() {
|
|
2475
|
-
const total = this.stats.hits + this.stats.misses;
|
|
2476
|
-
const hitRate = total > 0 ? (this.stats.hits / total) * 100 : 0;
|
|
2477
|
-
const memoryUsageMB = this.stats.memoryUsage / (1024 * 1024);
|
|
2478
|
-
return {
|
|
2479
|
-
...this.stats,
|
|
2480
|
-
hitRate,
|
|
2481
|
-
memoryUsageMB
|
|
2482
|
-
};
|
|
2483
|
-
}
|
|
2484
|
-
keys() {
|
|
2485
|
-
const keys = [];
|
|
2486
|
-
let current = this.head;
|
|
2487
|
-
while (current) {
|
|
2488
|
-
keys.push(current.key);
|
|
2489
|
-
current = current.next;
|
|
2490
|
-
}
|
|
2491
|
-
return keys;
|
|
2492
2401
|
}
|
|
2493
2402
|
get size() {
|
|
2494
2403
|
return this.cache.size;
|
|
2495
2404
|
}
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
while (this.cache.size >= this.options.maxEntries && this.tail) {
|
|
2499
|
-
this.evictTail();
|
|
2500
|
-
}
|
|
2501
|
-
// Evict by memory usage
|
|
2502
|
-
if (this.options.maxMemoryBytes < Infinity) {
|
|
2503
|
-
while (this.tail &&
|
|
2504
|
-
this.stats.memoryUsage + requiredSize > this.options.maxMemoryBytes) {
|
|
2505
|
-
this.evictTail();
|
|
2506
|
-
}
|
|
2507
|
-
}
|
|
2508
|
-
}
|
|
2509
|
-
evictTail() {
|
|
2510
|
-
if (!this.tail)
|
|
2511
|
-
return;
|
|
2512
|
-
const nodeToRemove = this.tail;
|
|
2513
|
-
const size = this.options.calculateSize(nodeToRemove.value);
|
|
2514
|
-
this.removeTail();
|
|
2515
|
-
this.cache.delete(nodeToRemove.key);
|
|
2516
|
-
this.stats.size = this.cache.size;
|
|
2517
|
-
this.stats.memoryUsage -= size;
|
|
2518
|
-
this.stats.evictions++;
|
|
2519
|
-
if (this.options.onEvict) {
|
|
2520
|
-
this.options.onEvict(nodeToRemove.key, nodeToRemove.value);
|
|
2521
|
-
}
|
|
2522
|
-
}
|
|
2523
|
-
addToHead(node) {
|
|
2524
|
-
node.prev = null;
|
|
2525
|
-
node.next = null;
|
|
2526
|
-
if (!this.head) {
|
|
2527
|
-
this.head = this.tail = node;
|
|
2528
|
-
}
|
|
2529
|
-
else {
|
|
2530
|
-
node.next = this.head;
|
|
2531
|
-
this.head.prev = node;
|
|
2532
|
-
this.head = node;
|
|
2533
|
-
}
|
|
2534
|
-
}
|
|
2535
|
-
removeNode(node) {
|
|
2536
|
-
if (node.prev) {
|
|
2537
|
-
node.prev.next = node.next;
|
|
2538
|
-
}
|
|
2539
|
-
else {
|
|
2540
|
-
this.head = node.next;
|
|
2541
|
-
}
|
|
2542
|
-
if (node.next) {
|
|
2543
|
-
node.next.prev = node.prev;
|
|
2544
|
-
}
|
|
2545
|
-
else {
|
|
2546
|
-
this.tail = node.prev;
|
|
2547
|
-
}
|
|
2548
|
-
}
|
|
2549
|
-
removeTail() {
|
|
2550
|
-
if (this.tail) {
|
|
2551
|
-
this.removeNode(this.tail);
|
|
2552
|
-
}
|
|
2405
|
+
keys() {
|
|
2406
|
+
return Array.from(this.cache.keys());
|
|
2553
2407
|
}
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
this.addToHead(node);
|
|
2408
|
+
getStats() {
|
|
2409
|
+
return {
|
|
2410
|
+
size: this.cache.size
|
|
2411
|
+
};
|
|
2559
2412
|
}
|
|
2560
2413
|
}
|
|
2561
2414
|
|
|
2562
|
-
const DEFAULT_CACHE_SIZE_MB = 250;
|
|
2563
2415
|
function getGlyphCacheKey(fontId, glyphId, depth, removeOverlaps) {
|
|
2564
2416
|
const roundedDepth = Math.round(depth * 1000) / 1000;
|
|
2565
2417
|
return `${fontId}_${glyphId}_${roundedDepth}_${removeOverlaps}`;
|
|
2566
2418
|
}
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
size += glyph.normals.length * 4;
|
|
2571
|
-
size += glyph.indices.length * glyph.indices.BYTES_PER_ELEMENT;
|
|
2572
|
-
size += 24; // 2 Vec3s
|
|
2573
|
-
size += 256; // Object overhead
|
|
2574
|
-
return size;
|
|
2419
|
+
const globalGlyphCache = new Cache();
|
|
2420
|
+
function createGlyphCache() {
|
|
2421
|
+
return new Cache();
|
|
2575
2422
|
}
|
|
2576
|
-
const
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
calculateSize: calculateGlyphMemoryUsage
|
|
2580
|
-
});
|
|
2581
|
-
function createGlyphCache(maxCacheSizeMB = DEFAULT_CACHE_SIZE_MB) {
|
|
2582
|
-
return new LRUCache({
|
|
2583
|
-
maxEntries: Infinity,
|
|
2584
|
-
maxMemoryBytes: maxCacheSizeMB * 1024 * 1024,
|
|
2585
|
-
calculateSize: calculateGlyphMemoryUsage
|
|
2586
|
-
});
|
|
2587
|
-
}
|
|
2588
|
-
// Shared across builder instances: contour extraction, word clustering, boundary grouping
|
|
2589
|
-
const globalContourCache = new LRUCache({
|
|
2590
|
-
maxEntries: 1000,
|
|
2591
|
-
calculateSize: (contours) => {
|
|
2592
|
-
let size = 0;
|
|
2593
|
-
for (const path of contours.paths) {
|
|
2594
|
-
size += path.points.length * 16; // Vec2 = 2 floats * 8 bytes
|
|
2595
|
-
}
|
|
2596
|
-
return size + 64; // bounds overhead
|
|
2597
|
-
}
|
|
2598
|
-
});
|
|
2599
|
-
const globalWordCache = new LRUCache({
|
|
2600
|
-
maxEntries: 1000,
|
|
2601
|
-
calculateSize: calculateGlyphMemoryUsage
|
|
2602
|
-
});
|
|
2603
|
-
const globalClusteringCache = new LRUCache({
|
|
2604
|
-
maxEntries: 2000,
|
|
2605
|
-
calculateSize: () => 1
|
|
2606
|
-
});
|
|
2423
|
+
const globalContourCache = new Cache();
|
|
2424
|
+
const globalWordCache = new Cache();
|
|
2425
|
+
const globalClusteringCache = new Cache();
|
|
2607
2426
|
|
|
2608
2427
|
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
|
|
2609
2428
|
|
|
@@ -2938,47 +2757,79 @@ class Tessellator {
|
|
|
2938
2757
|
|
|
2939
2758
|
class Extruder {
|
|
2940
2759
|
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
|
-
}
|
|
2946
2760
|
extrude(geometry, depth = 0, unitsPerEm) {
|
|
2947
2761
|
const points = geometry.triangles.vertices;
|
|
2948
2762
|
const triangleIndices = geometry.triangles.indices;
|
|
2949
2763
|
const numPoints = points.length / 2;
|
|
2950
|
-
//
|
|
2764
|
+
// Boundary edges are those that appear in exactly one triangle
|
|
2951
2765
|
let boundaryEdges = [];
|
|
2952
2766
|
if (depth !== 0) {
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2767
|
+
// Pack edge pair into integer key: (min << 16) | max
|
|
2768
|
+
// Fits glyph vertex indices comfortably, good hash distribution
|
|
2769
|
+
const edgeMap = new Map();
|
|
2770
|
+
const triLen = triangleIndices.length;
|
|
2771
|
+
for (let i = 0; i < triLen; i += 3) {
|
|
2956
2772
|
const a = triangleIndices[i];
|
|
2957
2773
|
const b = triangleIndices[i + 1];
|
|
2958
2774
|
const c = triangleIndices[i + 2];
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2775
|
+
let key, v0, v1;
|
|
2776
|
+
if (a < b) {
|
|
2777
|
+
key = (a << 16) | b;
|
|
2778
|
+
v0 = a;
|
|
2779
|
+
v1 = b;
|
|
2780
|
+
}
|
|
2781
|
+
else {
|
|
2782
|
+
key = (b << 16) | a;
|
|
2783
|
+
v0 = a;
|
|
2784
|
+
v1 = b;
|
|
2785
|
+
}
|
|
2786
|
+
let data = edgeMap.get(key);
|
|
2787
|
+
if (data) {
|
|
2788
|
+
data[2]++;
|
|
2789
|
+
}
|
|
2790
|
+
else {
|
|
2791
|
+
edgeMap.set(key, [v0, v1, 1]);
|
|
2792
|
+
}
|
|
2793
|
+
if (b < c) {
|
|
2794
|
+
key = (b << 16) | c;
|
|
2795
|
+
v0 = b;
|
|
2796
|
+
v1 = c;
|
|
2797
|
+
}
|
|
2798
|
+
else {
|
|
2799
|
+
key = (c << 16) | b;
|
|
2800
|
+
v0 = b;
|
|
2801
|
+
v1 = c;
|
|
2802
|
+
}
|
|
2803
|
+
data = edgeMap.get(key);
|
|
2804
|
+
if (data) {
|
|
2805
|
+
data[2]++;
|
|
2806
|
+
}
|
|
2807
|
+
else {
|
|
2808
|
+
edgeMap.set(key, [v0, v1, 1]);
|
|
2809
|
+
}
|
|
2810
|
+
if (c < a) {
|
|
2811
|
+
key = (c << 16) | a;
|
|
2812
|
+
v0 = c;
|
|
2813
|
+
v1 = a;
|
|
2814
|
+
}
|
|
2815
|
+
else {
|
|
2816
|
+
key = (a << 16) | c;
|
|
2817
|
+
v0 = c;
|
|
2818
|
+
v1 = a;
|
|
2819
|
+
}
|
|
2820
|
+
data = edgeMap.get(key);
|
|
2821
|
+
if (data) {
|
|
2822
|
+
data[2]++;
|
|
2823
|
+
}
|
|
2824
|
+
else {
|
|
2825
|
+
edgeMap.set(key, [v0, v1, 1]);
|
|
2826
|
+
}
|
|
2974
2827
|
}
|
|
2975
2828
|
boundaryEdges = [];
|
|
2976
|
-
for (const [
|
|
2977
|
-
if (count
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
if (edge)
|
|
2981
|
-
boundaryEdges.push(edge);
|
|
2829
|
+
for (const [v0, v1, count] of edgeMap.values()) {
|
|
2830
|
+
if (count === 1) {
|
|
2831
|
+
boundaryEdges.push([v0, v1]);
|
|
2832
|
+
}
|
|
2982
2833
|
}
|
|
2983
2834
|
}
|
|
2984
2835
|
const sideEdgeCount = depth === 0 ? 0 : boundaryEdges.length;
|
|
@@ -2992,7 +2843,6 @@ class Extruder {
|
|
|
2992
2843
|
: triangleIndices.length * 2 + sideEdgeCount * 6;
|
|
2993
2844
|
const indices = new Uint32Array(indexCount);
|
|
2994
2845
|
if (depth === 0) {
|
|
2995
|
-
// Single-sided flat geometry at z=0
|
|
2996
2846
|
let vPos = 0;
|
|
2997
2847
|
for (let i = 0; i < points.length; i += 2) {
|
|
2998
2848
|
vertices[vPos] = points[i];
|
|
@@ -3003,16 +2853,11 @@ class Extruder {
|
|
|
3003
2853
|
normals[vPos + 2] = 1;
|
|
3004
2854
|
vPos += 3;
|
|
3005
2855
|
}
|
|
3006
|
-
|
|
3007
|
-
for (let i = 0; i < triangleIndices.length; i++) {
|
|
3008
|
-
indices[i] = triangleIndices[i];
|
|
3009
|
-
}
|
|
2856
|
+
indices.set(triangleIndices);
|
|
3010
2857
|
return { vertices, normals, indices };
|
|
3011
2858
|
}
|
|
3012
|
-
// Extruded geometry: front at z=0, back at z=depth
|
|
3013
2859
|
const minBackOffset = unitsPerEm * 0.000025;
|
|
3014
2860
|
const backZ = depth <= minBackOffset ? minBackOffset : depth;
|
|
3015
|
-
// Generate both caps in one pass
|
|
3016
2861
|
for (let p = 0, vi = 0; p < points.length; p += 2, vi++) {
|
|
3017
2862
|
const x = points[p];
|
|
3018
2863
|
const y = points[p + 1];
|
|
@@ -3033,41 +2878,39 @@ class Extruder {
|
|
|
3033
2878
|
normals[baseD + 1] = 0;
|
|
3034
2879
|
normals[baseD + 2] = 1;
|
|
3035
2880
|
}
|
|
3036
|
-
//
|
|
3037
|
-
|
|
3038
|
-
for (let i = 0; i <
|
|
3039
|
-
indices[i] = triangleIndices[
|
|
2881
|
+
// Front cap faces -Z, reverse winding from libtess CCW output
|
|
2882
|
+
const triLen = triangleIndices.length;
|
|
2883
|
+
for (let i = 0; i < triLen; i++) {
|
|
2884
|
+
indices[i] = triangleIndices[triLen - 1 - i];
|
|
3040
2885
|
}
|
|
3041
|
-
//
|
|
3042
|
-
for (let i = 0; i <
|
|
3043
|
-
indices[
|
|
2886
|
+
// Back cap faces +Z, use original winding
|
|
2887
|
+
for (let i = 0; i < triLen; i++) {
|
|
2888
|
+
indices[triLen + i] = triangleIndices[i] + numPoints;
|
|
3044
2889
|
}
|
|
3045
|
-
// Side walls
|
|
3046
2890
|
let nextVertex = numPoints * 2;
|
|
3047
|
-
let idxPos =
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
const
|
|
3051
|
-
const
|
|
2891
|
+
let idxPos = triLen * 2;
|
|
2892
|
+
const numEdges = boundaryEdges.length;
|
|
2893
|
+
for (let e = 0; e < numEdges; e++) {
|
|
2894
|
+
const edge = boundaryEdges[e];
|
|
2895
|
+
const u = edge[0];
|
|
2896
|
+
const v = edge[1];
|
|
2897
|
+
const u2 = u << 1;
|
|
2898
|
+
const v2 = v << 1;
|
|
3052
2899
|
const p0x = points[u2];
|
|
3053
2900
|
const p0y = points[u2 + 1];
|
|
3054
2901
|
const p1x = points[v2];
|
|
3055
2902
|
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
2903
|
const ex = p1x - p0x;
|
|
3059
2904
|
const ey = p1y - p0y;
|
|
3060
2905
|
const lenSq = ex * ex + ey * ey;
|
|
3061
2906
|
let nx = 0;
|
|
3062
2907
|
let ny = 0;
|
|
3063
|
-
if (lenSq >
|
|
2908
|
+
if (lenSq > 1e-10) {
|
|
3064
2909
|
const invLen = 1 / Math.sqrt(lenSq);
|
|
3065
2910
|
nx = ey * invLen;
|
|
3066
2911
|
ny = -ex * invLen;
|
|
3067
2912
|
}
|
|
3068
|
-
const
|
|
3069
|
-
const base = baseVertex * 3;
|
|
3070
|
-
// Wall quad: front edge at z=0, back edge at z=depth
|
|
2913
|
+
const base = nextVertex * 3;
|
|
3071
2914
|
vertices[base] = p0x;
|
|
3072
2915
|
vertices[base + 1] = p0y;
|
|
3073
2916
|
vertices[base + 2] = 0;
|
|
@@ -3080,7 +2923,6 @@ class Extruder {
|
|
|
3080
2923
|
vertices[base + 9] = p1x;
|
|
3081
2924
|
vertices[base + 10] = p1y;
|
|
3082
2925
|
vertices[base + 11] = backZ;
|
|
3083
|
-
// Wall normals point perpendicular to edge
|
|
3084
2926
|
normals[base] = nx;
|
|
3085
2927
|
normals[base + 1] = ny;
|
|
3086
2928
|
normals[base + 2] = 0;
|
|
@@ -3093,13 +2935,14 @@ class Extruder {
|
|
|
3093
2935
|
normals[base + 9] = nx;
|
|
3094
2936
|
normals[base + 10] = ny;
|
|
3095
2937
|
normals[base + 11] = 0;
|
|
3096
|
-
|
|
3097
|
-
indices[idxPos
|
|
3098
|
-
indices[idxPos
|
|
3099
|
-
indices[idxPos
|
|
3100
|
-
indices[idxPos
|
|
3101
|
-
indices[idxPos
|
|
3102
|
-
indices[idxPos
|
|
2938
|
+
const baseVertex = nextVertex;
|
|
2939
|
+
indices[idxPos] = baseVertex;
|
|
2940
|
+
indices[idxPos + 1] = baseVertex + 1;
|
|
2941
|
+
indices[idxPos + 2] = baseVertex + 2;
|
|
2942
|
+
indices[idxPos + 3] = baseVertex + 1;
|
|
2943
|
+
indices[idxPos + 4] = baseVertex + 3;
|
|
2944
|
+
indices[idxPos + 5] = baseVertex + 2;
|
|
2945
|
+
idxPos += 6;
|
|
3103
2946
|
nextVertex += 4;
|
|
3104
2947
|
}
|
|
3105
2948
|
return { vertices, normals, indices };
|
|
@@ -4116,7 +3959,7 @@ class GlyphGeometryBuilder {
|
|
|
4116
3959
|
].join('|');
|
|
4117
3960
|
}
|
|
4118
3961
|
// Build instanced geometry from glyph contours
|
|
4119
|
-
buildInstancedGeometry(clustersByLine, depth, removeOverlaps, isCFF, separateGlyphs = false, coloredTextIndices) {
|
|
3962
|
+
buildInstancedGeometry(clustersByLine, depth, removeOverlaps, isCFF, scale, separateGlyphs = false, coloredTextIndices) {
|
|
4120
3963
|
if (isLogEnabled) {
|
|
4121
3964
|
let wordCount = 0;
|
|
4122
3965
|
for (let i = 0; i < clustersByLine.length; i++) {
|
|
@@ -4294,9 +4137,9 @@ class GlyphGeometryBuilder {
|
|
|
4294
4137
|
const py = task.py;
|
|
4295
4138
|
const pz = task.pz;
|
|
4296
4139
|
for (let j = 0; j < v.length; j += 3) {
|
|
4297
|
-
vertexArray[vertexPos++] = v[j] + px;
|
|
4298
|
-
vertexArray[vertexPos++] = v[j + 1] + py;
|
|
4299
|
-
vertexArray[vertexPos++] = v[j + 2] + pz;
|
|
4140
|
+
vertexArray[vertexPos++] = (v[j] + px) * scale;
|
|
4141
|
+
vertexArray[vertexPos++] = (v[j + 1] + py) * scale;
|
|
4142
|
+
vertexArray[vertexPos++] = (v[j + 2] + pz) * scale;
|
|
4300
4143
|
}
|
|
4301
4144
|
normalArray.set(n, normalPos);
|
|
4302
4145
|
normalPos += n.length;
|
|
@@ -4306,6 +4149,20 @@ class GlyphGeometryBuilder {
|
|
|
4306
4149
|
}
|
|
4307
4150
|
}
|
|
4308
4151
|
perfLogger.end('GlyphGeometryBuilder.buildInstancedGeometry');
|
|
4152
|
+
planeBounds.min.x *= scale;
|
|
4153
|
+
planeBounds.min.y *= scale;
|
|
4154
|
+
planeBounds.min.z *= scale;
|
|
4155
|
+
planeBounds.max.x *= scale;
|
|
4156
|
+
planeBounds.max.y *= scale;
|
|
4157
|
+
planeBounds.max.z *= scale;
|
|
4158
|
+
for (let i = 0; i < glyphInfos.length; i++) {
|
|
4159
|
+
glyphInfos[i].bounds.min.x *= scale;
|
|
4160
|
+
glyphInfos[i].bounds.min.y *= scale;
|
|
4161
|
+
glyphInfos[i].bounds.min.z *= scale;
|
|
4162
|
+
glyphInfos[i].bounds.max.x *= scale;
|
|
4163
|
+
glyphInfos[i].bounds.max.y *= scale;
|
|
4164
|
+
glyphInfos[i].bounds.max.z *= scale;
|
|
4165
|
+
}
|
|
4309
4166
|
return {
|
|
4310
4167
|
vertices: vertexArray,
|
|
4311
4168
|
normals: normalArray,
|
|
@@ -4317,7 +4174,6 @@ class GlyphGeometryBuilder {
|
|
|
4317
4174
|
getClusterKey(glyphs, depth, removeOverlaps) {
|
|
4318
4175
|
if (glyphs.length === 0)
|
|
4319
4176
|
return '';
|
|
4320
|
-
// Normalize positions relative to the first glyph in the cluster
|
|
4321
4177
|
const refX = glyphs[0].x ?? 0;
|
|
4322
4178
|
const refY = glyphs[0].y ?? 0;
|
|
4323
4179
|
const parts = glyphs.map((g) => {
|
|
@@ -5372,6 +5228,7 @@ class TextRangeQuery {
|
|
|
5372
5228
|
}
|
|
5373
5229
|
|
|
5374
5230
|
const DEFAULT_MAX_TEXT_LENGTH = 100000;
|
|
5231
|
+
const DEFAULT_FONT_SIZE = 72;
|
|
5375
5232
|
class Text {
|
|
5376
5233
|
static { this.patternCache = new Map(); }
|
|
5377
5234
|
static { this.hbInitPromise = null; }
|
|
@@ -5445,7 +5302,7 @@ class Text {
|
|
|
5445
5302
|
return {
|
|
5446
5303
|
...newResult,
|
|
5447
5304
|
getLoadedFont: () => text.getLoadedFont(),
|
|
5448
|
-
|
|
5305
|
+
getCacheSize: () => text.getCacheSize(),
|
|
5449
5306
|
clearCache: () => text.clearCache(),
|
|
5450
5307
|
measureTextWidth: (textString, letterSpacing) => text.measureTextWidth(textString, letterSpacing),
|
|
5451
5308
|
update
|
|
@@ -5454,7 +5311,7 @@ class Text {
|
|
|
5454
5311
|
return {
|
|
5455
5312
|
...result,
|
|
5456
5313
|
getLoadedFont: () => text.getLoadedFont(),
|
|
5457
|
-
|
|
5314
|
+
getCacheSize: () => text.getCacheSize(),
|
|
5458
5315
|
clearCache: () => text.clearCache(),
|
|
5459
5316
|
measureTextWidth: (textString, letterSpacing) => text.measureTextWidth(textString, letterSpacing),
|
|
5460
5317
|
update
|
|
@@ -5586,7 +5443,7 @@ class Text {
|
|
|
5586
5443
|
async createGeometry(options) {
|
|
5587
5444
|
perfLogger.start('Text.createGeometry', {
|
|
5588
5445
|
textLength: options.text.length,
|
|
5589
|
-
size: options.size ||
|
|
5446
|
+
size: options.size || DEFAULT_FONT_SIZE,
|
|
5590
5447
|
hasLayout: !!options.layout,
|
|
5591
5448
|
mode: 'cached'
|
|
5592
5449
|
});
|
|
@@ -5600,7 +5457,7 @@ class Text {
|
|
|
5600
5457
|
this.updateFontVariations(options);
|
|
5601
5458
|
if (!this.geometryBuilder) {
|
|
5602
5459
|
const cache = options.maxCacheSizeMB
|
|
5603
|
-
? createGlyphCache(
|
|
5460
|
+
? createGlyphCache()
|
|
5604
5461
|
: globalGlyphCache;
|
|
5605
5462
|
this.geometryBuilder = new GlyphGeometryBuilder(cache, this.loadedFont);
|
|
5606
5463
|
this.geometryBuilder.setFontId(this.currentFontId);
|
|
@@ -5646,10 +5503,9 @@ class Text {
|
|
|
5646
5503
|
}
|
|
5647
5504
|
}
|
|
5648
5505
|
}
|
|
5649
|
-
const shapedResult = this.geometryBuilder.buildInstancedGeometry(clustersByLine, layoutData.depth, shouldRemoveOverlaps, this.loadedFont.metrics.isCFF, options.
|
|
5650
|
-
const
|
|
5651
|
-
|
|
5652
|
-
if (options.separateGlyphsWithAttributes) {
|
|
5506
|
+
const shapedResult = this.geometryBuilder.buildInstancedGeometry(clustersByLine, layoutData.depth, shouldRemoveOverlaps, this.loadedFont.metrics.isCFF, layoutData.pixelsPerFontUnit, options.perGlyphAttributes ?? false, coloredTextIndices);
|
|
5507
|
+
const result = this.finalizeGeometry(shapedResult.vertices, shapedResult.normals, shapedResult.indices, shapedResult.glyphInfos, shapedResult.planeBounds, options, options.text);
|
|
5508
|
+
if (options.perGlyphAttributes) {
|
|
5653
5509
|
const glyphAttrs = this.createGlyphAttributes(result.vertices.length / 3, result.glyphs);
|
|
5654
5510
|
result.glyphAttributes = glyphAttrs;
|
|
5655
5511
|
}
|
|
@@ -5716,16 +5572,16 @@ class Text {
|
|
|
5716
5572
|
if (!this.loadedFont) {
|
|
5717
5573
|
throw new Error('Font not loaded. Use Text.create() with a font option');
|
|
5718
5574
|
}
|
|
5719
|
-
const { text, size =
|
|
5575
|
+
const { text, size = DEFAULT_FONT_SIZE, depth = 0, lineHeight = 1.0, letterSpacing = 0, layout = {} } = options;
|
|
5720
5576
|
const { width, direction = 'ltr', align = direction === 'rtl' ? 'right' : 'left', respectExistingBreaks = true, hyphenate = true, language = 'en-us', tolerance = DEFAULT_TOLERANCE, pretolerance = DEFAULT_PRETOLERANCE, emergencyStretch = DEFAULT_EMERGENCY_STRETCH, autoEmergencyStretch, hyphenationPatterns, lefthyphenmin, righthyphenmin, linepenalty, adjdemerits, hyphenpenalty, exhyphenpenalty, doublehyphendemerits, looseness, disableShortLineDetection, shortLineThreshold } = layout;
|
|
5577
|
+
const fontUnitsPerPixel = this.loadedFont.upem / size;
|
|
5721
5578
|
let widthInFontUnits;
|
|
5722
5579
|
if (width !== undefined) {
|
|
5723
|
-
widthInFontUnits = width *
|
|
5580
|
+
widthInFontUnits = width * fontUnitsPerPixel;
|
|
5724
5581
|
}
|
|
5725
5582
|
// Keep depth behavior consistent with Extruder: extremely small non-zero depths
|
|
5726
|
-
// are clamped to a minimum back offset
|
|
5727
|
-
const
|
|
5728
|
-
const rawDepthInFontUnits = depth * depthScale;
|
|
5583
|
+
// are clamped to a minimum back offset to prevent Z fighting
|
|
5584
|
+
const rawDepthInFontUnits = depth * fontUnitsPerPixel;
|
|
5729
5585
|
const minExtrudeDepth = this.loadedFont.upem * 0.000025;
|
|
5730
5586
|
const depthInFontUnits = rawDepthInFontUnits <= 0
|
|
5731
5587
|
? 0
|
|
@@ -5768,7 +5624,8 @@ class Text {
|
|
|
5768
5624
|
align,
|
|
5769
5625
|
direction,
|
|
5770
5626
|
depth: depthInFontUnits,
|
|
5771
|
-
size
|
|
5627
|
+
size,
|
|
5628
|
+
pixelsPerFontUnit: 1 / fontUnitsPerPixel
|
|
5772
5629
|
};
|
|
5773
5630
|
}
|
|
5774
5631
|
applyColorSystem(vertices, glyphInfoArray, color, originalText) {
|
|
@@ -5864,8 +5721,8 @@ class Text {
|
|
|
5864
5721
|
}
|
|
5865
5722
|
return { colors, coloredRanges };
|
|
5866
5723
|
}
|
|
5867
|
-
finalizeGeometry(vertices, normals, indices, glyphInfoArray, planeBounds, options,
|
|
5868
|
-
const { layout = {}
|
|
5724
|
+
finalizeGeometry(vertices, normals, indices, glyphInfoArray, planeBounds, options, originalText) {
|
|
5725
|
+
const { layout = {} } = options;
|
|
5869
5726
|
const { width, align = layout.direction === 'rtl' ? 'right' : 'left' } = layout;
|
|
5870
5727
|
if (!this.textLayout) {
|
|
5871
5728
|
this.textLayout = new TextLayout(this.loadedFont);
|
|
@@ -5878,37 +5735,14 @@ class Text {
|
|
|
5878
5735
|
const offset = alignmentResult.offset;
|
|
5879
5736
|
planeBounds.min.x = alignmentResult.adjustedBounds.min.x;
|
|
5880
5737
|
planeBounds.max.x = alignmentResult.adjustedBounds.max.x;
|
|
5881
|
-
|
|
5882
|
-
const offsetScaled = offset * finalScale;
|
|
5883
|
-
// Scale vertices only (normals are unit vectors, don't scale)
|
|
5884
|
-
if (offsetScaled === 0) {
|
|
5885
|
-
for (let i = 0; i < vertices.length; i++) {
|
|
5886
|
-
vertices[i] *= finalScale;
|
|
5887
|
-
}
|
|
5888
|
-
}
|
|
5889
|
-
else {
|
|
5738
|
+
if (offset !== 0) {
|
|
5890
5739
|
for (let i = 0; i < vertices.length; i += 3) {
|
|
5891
|
-
vertices[i]
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
planeBounds.min.y *= finalScale;
|
|
5898
|
-
planeBounds.min.z *= finalScale;
|
|
5899
|
-
planeBounds.max.x *= finalScale;
|
|
5900
|
-
planeBounds.max.y *= finalScale;
|
|
5901
|
-
planeBounds.max.z *= finalScale;
|
|
5902
|
-
for (let i = 0; i < glyphInfoArray.length; i++) {
|
|
5903
|
-
const glyphInfo = glyphInfoArray[i];
|
|
5904
|
-
glyphInfo.bounds.min.x =
|
|
5905
|
-
glyphInfo.bounds.min.x * finalScale + offsetScaled;
|
|
5906
|
-
glyphInfo.bounds.min.y *= finalScale;
|
|
5907
|
-
glyphInfo.bounds.min.z *= finalScale;
|
|
5908
|
-
glyphInfo.bounds.max.x =
|
|
5909
|
-
glyphInfo.bounds.max.x * finalScale + offsetScaled;
|
|
5910
|
-
glyphInfo.bounds.max.y *= finalScale;
|
|
5911
|
-
glyphInfo.bounds.max.z *= finalScale;
|
|
5740
|
+
vertices[i] += offset;
|
|
5741
|
+
}
|
|
5742
|
+
for (let i = 0; i < glyphInfoArray.length; i++) {
|
|
5743
|
+
glyphInfoArray[i].bounds.min.x += offset;
|
|
5744
|
+
glyphInfoArray[i].bounds.max.x += offset;
|
|
5745
|
+
}
|
|
5912
5746
|
}
|
|
5913
5747
|
let colors;
|
|
5914
5748
|
let coloredRanges;
|
|
@@ -5933,8 +5767,7 @@ class Text {
|
|
|
5933
5767
|
verticesGenerated,
|
|
5934
5768
|
pointsRemovedByVisvalingam: optimizationStats.pointsRemovedByVisvalingam,
|
|
5935
5769
|
pointsRemovedByColinear: optimizationStats.pointsRemovedByColinear,
|
|
5936
|
-
originalPointCount: optimizationStats.originalPointCount
|
|
5937
|
-
...(cacheStats || {})
|
|
5770
|
+
originalPointCount: optimizationStats.originalPointCount
|
|
5938
5771
|
},
|
|
5939
5772
|
query: (options) => {
|
|
5940
5773
|
if (!originalText) {
|
|
@@ -5985,11 +5818,11 @@ class Text {
|
|
|
5985
5818
|
}
|
|
5986
5819
|
return TextMeasurer.measureTextWidth(this.loadedFont, text, letterSpacing);
|
|
5987
5820
|
}
|
|
5988
|
-
|
|
5821
|
+
getCacheSize() {
|
|
5989
5822
|
if (this.geometryBuilder) {
|
|
5990
|
-
return this.geometryBuilder.getCacheStats();
|
|
5823
|
+
return this.geometryBuilder.getCacheStats().size;
|
|
5991
5824
|
}
|
|
5992
|
-
return
|
|
5825
|
+
return 0;
|
|
5993
5826
|
}
|
|
5994
5827
|
clearCache() {
|
|
5995
5828
|
if (this.geometryBuilder) {
|
|
@@ -6000,25 +5833,45 @@ class Text {
|
|
|
6000
5833
|
const glyphCenters = new Float32Array(vertexCount * 3);
|
|
6001
5834
|
const glyphIndices = new Float32Array(vertexCount);
|
|
6002
5835
|
const glyphLineIndices = new Float32Array(vertexCount);
|
|
6003
|
-
|
|
5836
|
+
const glyphProgress = new Float32Array(vertexCount);
|
|
5837
|
+
const glyphBaselineY = new Float32Array(vertexCount);
|
|
5838
|
+
let minX = Infinity;
|
|
5839
|
+
let maxX = -Infinity;
|
|
5840
|
+
for (let i = 0; i < glyphs.length; i++) {
|
|
5841
|
+
const cx = (glyphs[i].bounds.min.x + glyphs[i].bounds.max.x) / 2;
|
|
5842
|
+
if (cx < minX)
|
|
5843
|
+
minX = cx;
|
|
5844
|
+
if (cx > maxX)
|
|
5845
|
+
maxX = cx;
|
|
5846
|
+
}
|
|
5847
|
+
const range = maxX - minX;
|
|
5848
|
+
for (let index = 0; index < glyphs.length; index++) {
|
|
5849
|
+
const glyph = glyphs[index];
|
|
6004
5850
|
const centerX = (glyph.bounds.min.x + glyph.bounds.max.x) / 2;
|
|
6005
5851
|
const centerY = (glyph.bounds.min.y + glyph.bounds.max.y) / 2;
|
|
6006
5852
|
const centerZ = (glyph.bounds.min.z + glyph.bounds.max.z) / 2;
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
|
|
5853
|
+
const baselineY = glyph.bounds.min.y;
|
|
5854
|
+
const progress = range > 0 ? (centerX - minX) / range : 0;
|
|
5855
|
+
const start = glyph.vertexStart;
|
|
5856
|
+
const end = Math.min(start + glyph.vertexCount, vertexCount);
|
|
5857
|
+
if (end <= start)
|
|
5858
|
+
continue;
|
|
5859
|
+
glyphIndices.fill(index, start, end);
|
|
5860
|
+
glyphLineIndices.fill(glyph.lineIndex, start, end);
|
|
5861
|
+
glyphProgress.fill(progress, start, end);
|
|
5862
|
+
glyphBaselineY.fill(baselineY, start, end);
|
|
5863
|
+
for (let v = start * 3; v < end * 3; v += 3) {
|
|
5864
|
+
glyphCenters[v] = centerX;
|
|
5865
|
+
glyphCenters[v + 1] = centerY;
|
|
5866
|
+
glyphCenters[v + 2] = centerZ;
|
|
6016
5867
|
}
|
|
6017
|
-
}
|
|
5868
|
+
}
|
|
6018
5869
|
return {
|
|
6019
5870
|
glyphCenter: glyphCenters,
|
|
6020
5871
|
glyphIndex: glyphIndices,
|
|
6021
|
-
glyphLineIndex: glyphLineIndices
|
|
5872
|
+
glyphLineIndex: glyphLineIndices,
|
|
5873
|
+
glyphProgress,
|
|
5874
|
+
glyphBaselineY
|
|
6022
5875
|
};
|
|
6023
5876
|
}
|
|
6024
5877
|
resetHelpers() {
|