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.cjs
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
|
|
@@ -2382,231 +2382,50 @@ class Vec3 {
|
|
|
2382
2382
|
}
|
|
2383
2383
|
}
|
|
2384
2384
|
|
|
2385
|
-
//
|
|
2386
|
-
class
|
|
2387
|
-
constructor(
|
|
2385
|
+
// Map-based cache with no eviction policy
|
|
2386
|
+
class Cache {
|
|
2387
|
+
constructor() {
|
|
2388
2388
|
this.cache = new Map();
|
|
2389
|
-
this.head = null;
|
|
2390
|
-
this.tail = null;
|
|
2391
|
-
this.stats = {
|
|
2392
|
-
hits: 0,
|
|
2393
|
-
misses: 0,
|
|
2394
|
-
evictions: 0,
|
|
2395
|
-
size: 0,
|
|
2396
|
-
memoryUsage: 0
|
|
2397
|
-
};
|
|
2398
|
-
this.options = {
|
|
2399
|
-
maxEntries: options.maxEntries ?? Infinity,
|
|
2400
|
-
maxMemoryBytes: options.maxMemoryBytes ?? Infinity,
|
|
2401
|
-
calculateSize: options.calculateSize ?? (() => 0),
|
|
2402
|
-
onEvict: options.onEvict
|
|
2403
|
-
};
|
|
2404
2389
|
}
|
|
2405
2390
|
get(key) {
|
|
2406
|
-
|
|
2407
|
-
if (node) {
|
|
2408
|
-
this.stats.hits++;
|
|
2409
|
-
this.moveToHead(node);
|
|
2410
|
-
return node.value;
|
|
2411
|
-
}
|
|
2412
|
-
else {
|
|
2413
|
-
this.stats.misses++;
|
|
2414
|
-
return undefined;
|
|
2415
|
-
}
|
|
2391
|
+
return this.cache.get(key);
|
|
2416
2392
|
}
|
|
2417
2393
|
has(key) {
|
|
2418
2394
|
return this.cache.has(key);
|
|
2419
2395
|
}
|
|
2420
2396
|
set(key, value) {
|
|
2421
|
-
|
|
2422
|
-
const existingNode = this.cache.get(key);
|
|
2423
|
-
if (existingNode) {
|
|
2424
|
-
const oldSize = this.options.calculateSize(existingNode.value);
|
|
2425
|
-
const newSize = this.options.calculateSize(value);
|
|
2426
|
-
this.stats.memoryUsage = this.stats.memoryUsage - oldSize + newSize;
|
|
2427
|
-
existingNode.value = value;
|
|
2428
|
-
this.moveToHead(existingNode);
|
|
2429
|
-
return;
|
|
2430
|
-
}
|
|
2431
|
-
const size = this.options.calculateSize(value);
|
|
2432
|
-
// Evict entries if we exceed limits
|
|
2433
|
-
this.evictIfNeeded(size);
|
|
2434
|
-
// Create new node
|
|
2435
|
-
const node = {
|
|
2436
|
-
key,
|
|
2437
|
-
value,
|
|
2438
|
-
prev: null,
|
|
2439
|
-
next: null
|
|
2440
|
-
};
|
|
2441
|
-
this.cache.set(key, node);
|
|
2442
|
-
this.addToHead(node);
|
|
2443
|
-
this.stats.size = this.cache.size;
|
|
2444
|
-
this.stats.memoryUsage += size;
|
|
2397
|
+
this.cache.set(key, value);
|
|
2445
2398
|
}
|
|
2446
2399
|
delete(key) {
|
|
2447
|
-
|
|
2448
|
-
if (!node)
|
|
2449
|
-
return false;
|
|
2450
|
-
const size = this.options.calculateSize(node.value);
|
|
2451
|
-
this.removeNode(node);
|
|
2452
|
-
this.cache.delete(key);
|
|
2453
|
-
this.stats.size = this.cache.size;
|
|
2454
|
-
this.stats.memoryUsage -= size;
|
|
2455
|
-
if (this.options.onEvict) {
|
|
2456
|
-
this.options.onEvict(key, node.value);
|
|
2457
|
-
}
|
|
2458
|
-
return true;
|
|
2400
|
+
return this.cache.delete(key);
|
|
2459
2401
|
}
|
|
2460
2402
|
clear() {
|
|
2461
|
-
if (this.options.onEvict) {
|
|
2462
|
-
for (const [key, node] of this.cache) {
|
|
2463
|
-
this.options.onEvict(key, node.value);
|
|
2464
|
-
}
|
|
2465
|
-
}
|
|
2466
2403
|
this.cache.clear();
|
|
2467
|
-
this.head = null;
|
|
2468
|
-
this.tail = null;
|
|
2469
|
-
this.stats = {
|
|
2470
|
-
hits: 0,
|
|
2471
|
-
misses: 0,
|
|
2472
|
-
evictions: 0,
|
|
2473
|
-
size: 0,
|
|
2474
|
-
memoryUsage: 0
|
|
2475
|
-
};
|
|
2476
|
-
}
|
|
2477
|
-
getStats() {
|
|
2478
|
-
const total = this.stats.hits + this.stats.misses;
|
|
2479
|
-
const hitRate = total > 0 ? (this.stats.hits / total) * 100 : 0;
|
|
2480
|
-
const memoryUsageMB = this.stats.memoryUsage / (1024 * 1024);
|
|
2481
|
-
return {
|
|
2482
|
-
...this.stats,
|
|
2483
|
-
hitRate,
|
|
2484
|
-
memoryUsageMB
|
|
2485
|
-
};
|
|
2486
|
-
}
|
|
2487
|
-
keys() {
|
|
2488
|
-
const keys = [];
|
|
2489
|
-
let current = this.head;
|
|
2490
|
-
while (current) {
|
|
2491
|
-
keys.push(current.key);
|
|
2492
|
-
current = current.next;
|
|
2493
|
-
}
|
|
2494
|
-
return keys;
|
|
2495
2404
|
}
|
|
2496
2405
|
get size() {
|
|
2497
2406
|
return this.cache.size;
|
|
2498
2407
|
}
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
while (this.cache.size >= this.options.maxEntries && this.tail) {
|
|
2502
|
-
this.evictTail();
|
|
2503
|
-
}
|
|
2504
|
-
// Evict by memory usage
|
|
2505
|
-
if (this.options.maxMemoryBytes < Infinity) {
|
|
2506
|
-
while (this.tail &&
|
|
2507
|
-
this.stats.memoryUsage + requiredSize > this.options.maxMemoryBytes) {
|
|
2508
|
-
this.evictTail();
|
|
2509
|
-
}
|
|
2510
|
-
}
|
|
2511
|
-
}
|
|
2512
|
-
evictTail() {
|
|
2513
|
-
if (!this.tail)
|
|
2514
|
-
return;
|
|
2515
|
-
const nodeToRemove = this.tail;
|
|
2516
|
-
const size = this.options.calculateSize(nodeToRemove.value);
|
|
2517
|
-
this.removeTail();
|
|
2518
|
-
this.cache.delete(nodeToRemove.key);
|
|
2519
|
-
this.stats.size = this.cache.size;
|
|
2520
|
-
this.stats.memoryUsage -= size;
|
|
2521
|
-
this.stats.evictions++;
|
|
2522
|
-
if (this.options.onEvict) {
|
|
2523
|
-
this.options.onEvict(nodeToRemove.key, nodeToRemove.value);
|
|
2524
|
-
}
|
|
2525
|
-
}
|
|
2526
|
-
addToHead(node) {
|
|
2527
|
-
node.prev = null;
|
|
2528
|
-
node.next = null;
|
|
2529
|
-
if (!this.head) {
|
|
2530
|
-
this.head = this.tail = node;
|
|
2531
|
-
}
|
|
2532
|
-
else {
|
|
2533
|
-
node.next = this.head;
|
|
2534
|
-
this.head.prev = node;
|
|
2535
|
-
this.head = node;
|
|
2536
|
-
}
|
|
2537
|
-
}
|
|
2538
|
-
removeNode(node) {
|
|
2539
|
-
if (node.prev) {
|
|
2540
|
-
node.prev.next = node.next;
|
|
2541
|
-
}
|
|
2542
|
-
else {
|
|
2543
|
-
this.head = node.next;
|
|
2544
|
-
}
|
|
2545
|
-
if (node.next) {
|
|
2546
|
-
node.next.prev = node.prev;
|
|
2547
|
-
}
|
|
2548
|
-
else {
|
|
2549
|
-
this.tail = node.prev;
|
|
2550
|
-
}
|
|
2551
|
-
}
|
|
2552
|
-
removeTail() {
|
|
2553
|
-
if (this.tail) {
|
|
2554
|
-
this.removeNode(this.tail);
|
|
2555
|
-
}
|
|
2408
|
+
keys() {
|
|
2409
|
+
return Array.from(this.cache.keys());
|
|
2556
2410
|
}
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
this.addToHead(node);
|
|
2411
|
+
getStats() {
|
|
2412
|
+
return {
|
|
2413
|
+
size: this.cache.size
|
|
2414
|
+
};
|
|
2562
2415
|
}
|
|
2563
2416
|
}
|
|
2564
2417
|
|
|
2565
|
-
const DEFAULT_CACHE_SIZE_MB = 250;
|
|
2566
2418
|
function getGlyphCacheKey(fontId, glyphId, depth, removeOverlaps) {
|
|
2567
2419
|
const roundedDepth = Math.round(depth * 1000) / 1000;
|
|
2568
2420
|
return `${fontId}_${glyphId}_${roundedDepth}_${removeOverlaps}`;
|
|
2569
2421
|
}
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
size += glyph.normals.length * 4;
|
|
2574
|
-
size += glyph.indices.length * glyph.indices.BYTES_PER_ELEMENT;
|
|
2575
|
-
size += 24; // 2 Vec3s
|
|
2576
|
-
size += 256; // Object overhead
|
|
2577
|
-
return size;
|
|
2422
|
+
const globalGlyphCache = new Cache();
|
|
2423
|
+
function createGlyphCache() {
|
|
2424
|
+
return new Cache();
|
|
2578
2425
|
}
|
|
2579
|
-
const
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
calculateSize: calculateGlyphMemoryUsage
|
|
2583
|
-
});
|
|
2584
|
-
function createGlyphCache(maxCacheSizeMB = DEFAULT_CACHE_SIZE_MB) {
|
|
2585
|
-
return new LRUCache({
|
|
2586
|
-
maxEntries: Infinity,
|
|
2587
|
-
maxMemoryBytes: maxCacheSizeMB * 1024 * 1024,
|
|
2588
|
-
calculateSize: calculateGlyphMemoryUsage
|
|
2589
|
-
});
|
|
2590
|
-
}
|
|
2591
|
-
// Shared across builder instances: contour extraction, word clustering, boundary grouping
|
|
2592
|
-
const globalContourCache = new LRUCache({
|
|
2593
|
-
maxEntries: 1000,
|
|
2594
|
-
calculateSize: (contours) => {
|
|
2595
|
-
let size = 0;
|
|
2596
|
-
for (const path of contours.paths) {
|
|
2597
|
-
size += path.points.length * 16; // Vec2 = 2 floats * 8 bytes
|
|
2598
|
-
}
|
|
2599
|
-
return size + 64; // bounds overhead
|
|
2600
|
-
}
|
|
2601
|
-
});
|
|
2602
|
-
const globalWordCache = new LRUCache({
|
|
2603
|
-
maxEntries: 1000,
|
|
2604
|
-
calculateSize: calculateGlyphMemoryUsage
|
|
2605
|
-
});
|
|
2606
|
-
const globalClusteringCache = new LRUCache({
|
|
2607
|
-
maxEntries: 2000,
|
|
2608
|
-
calculateSize: () => 1
|
|
2609
|
-
});
|
|
2426
|
+
const globalContourCache = new Cache();
|
|
2427
|
+
const globalWordCache = new Cache();
|
|
2428
|
+
const globalClusteringCache = new Cache();
|
|
2610
2429
|
|
|
2611
2430
|
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
|
|
2612
2431
|
|
|
@@ -2941,47 +2760,79 @@ class Tessellator {
|
|
|
2941
2760
|
|
|
2942
2761
|
class Extruder {
|
|
2943
2762
|
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
|
-
}
|
|
2949
2763
|
extrude(geometry, depth = 0, unitsPerEm) {
|
|
2950
2764
|
const points = geometry.triangles.vertices;
|
|
2951
2765
|
const triangleIndices = geometry.triangles.indices;
|
|
2952
2766
|
const numPoints = points.length / 2;
|
|
2953
|
-
//
|
|
2767
|
+
// Boundary edges are those that appear in exactly one triangle
|
|
2954
2768
|
let boundaryEdges = [];
|
|
2955
2769
|
if (depth !== 0) {
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2770
|
+
// Pack edge pair into integer key: (min << 16) | max
|
|
2771
|
+
// Fits glyph vertex indices comfortably, good hash distribution
|
|
2772
|
+
const edgeMap = new Map();
|
|
2773
|
+
const triLen = triangleIndices.length;
|
|
2774
|
+
for (let i = 0; i < triLen; i += 3) {
|
|
2959
2775
|
const a = triangleIndices[i];
|
|
2960
2776
|
const b = triangleIndices[i + 1];
|
|
2961
2777
|
const c = triangleIndices[i + 2];
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2778
|
+
let key, v0, v1;
|
|
2779
|
+
if (a < b) {
|
|
2780
|
+
key = (a << 16) | b;
|
|
2781
|
+
v0 = a;
|
|
2782
|
+
v1 = b;
|
|
2783
|
+
}
|
|
2784
|
+
else {
|
|
2785
|
+
key = (b << 16) | a;
|
|
2786
|
+
v0 = a;
|
|
2787
|
+
v1 = b;
|
|
2788
|
+
}
|
|
2789
|
+
let data = edgeMap.get(key);
|
|
2790
|
+
if (data) {
|
|
2791
|
+
data[2]++;
|
|
2792
|
+
}
|
|
2793
|
+
else {
|
|
2794
|
+
edgeMap.set(key, [v0, v1, 1]);
|
|
2795
|
+
}
|
|
2796
|
+
if (b < c) {
|
|
2797
|
+
key = (b << 16) | c;
|
|
2798
|
+
v0 = b;
|
|
2799
|
+
v1 = c;
|
|
2800
|
+
}
|
|
2801
|
+
else {
|
|
2802
|
+
key = (c << 16) | b;
|
|
2803
|
+
v0 = b;
|
|
2804
|
+
v1 = c;
|
|
2805
|
+
}
|
|
2806
|
+
data = edgeMap.get(key);
|
|
2807
|
+
if (data) {
|
|
2808
|
+
data[2]++;
|
|
2809
|
+
}
|
|
2810
|
+
else {
|
|
2811
|
+
edgeMap.set(key, [v0, v1, 1]);
|
|
2812
|
+
}
|
|
2813
|
+
if (c < a) {
|
|
2814
|
+
key = (c << 16) | a;
|
|
2815
|
+
v0 = c;
|
|
2816
|
+
v1 = a;
|
|
2817
|
+
}
|
|
2818
|
+
else {
|
|
2819
|
+
key = (a << 16) | c;
|
|
2820
|
+
v0 = c;
|
|
2821
|
+
v1 = a;
|
|
2822
|
+
}
|
|
2823
|
+
data = edgeMap.get(key);
|
|
2824
|
+
if (data) {
|
|
2825
|
+
data[2]++;
|
|
2826
|
+
}
|
|
2827
|
+
else {
|
|
2828
|
+
edgeMap.set(key, [v0, v1, 1]);
|
|
2829
|
+
}
|
|
2977
2830
|
}
|
|
2978
2831
|
boundaryEdges = [];
|
|
2979
|
-
for (const [
|
|
2980
|
-
if (count
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
if (edge)
|
|
2984
|
-
boundaryEdges.push(edge);
|
|
2832
|
+
for (const [v0, v1, count] of edgeMap.values()) {
|
|
2833
|
+
if (count === 1) {
|
|
2834
|
+
boundaryEdges.push([v0, v1]);
|
|
2835
|
+
}
|
|
2985
2836
|
}
|
|
2986
2837
|
}
|
|
2987
2838
|
const sideEdgeCount = depth === 0 ? 0 : boundaryEdges.length;
|
|
@@ -2995,7 +2846,6 @@ class Extruder {
|
|
|
2995
2846
|
: triangleIndices.length * 2 + sideEdgeCount * 6;
|
|
2996
2847
|
const indices = new Uint32Array(indexCount);
|
|
2997
2848
|
if (depth === 0) {
|
|
2998
|
-
// Single-sided flat geometry at z=0
|
|
2999
2849
|
let vPos = 0;
|
|
3000
2850
|
for (let i = 0; i < points.length; i += 2) {
|
|
3001
2851
|
vertices[vPos] = points[i];
|
|
@@ -3006,16 +2856,11 @@ class Extruder {
|
|
|
3006
2856
|
normals[vPos + 2] = 1;
|
|
3007
2857
|
vPos += 3;
|
|
3008
2858
|
}
|
|
3009
|
-
|
|
3010
|
-
for (let i = 0; i < triangleIndices.length; i++) {
|
|
3011
|
-
indices[i] = triangleIndices[i];
|
|
3012
|
-
}
|
|
2859
|
+
indices.set(triangleIndices);
|
|
3013
2860
|
return { vertices, normals, indices };
|
|
3014
2861
|
}
|
|
3015
|
-
// Extruded geometry: front at z=0, back at z=depth
|
|
3016
2862
|
const minBackOffset = unitsPerEm * 0.000025;
|
|
3017
2863
|
const backZ = depth <= minBackOffset ? minBackOffset : depth;
|
|
3018
|
-
// Generate both caps in one pass
|
|
3019
2864
|
for (let p = 0, vi = 0; p < points.length; p += 2, vi++) {
|
|
3020
2865
|
const x = points[p];
|
|
3021
2866
|
const y = points[p + 1];
|
|
@@ -3036,41 +2881,39 @@ class Extruder {
|
|
|
3036
2881
|
normals[baseD + 1] = 0;
|
|
3037
2882
|
normals[baseD + 2] = 1;
|
|
3038
2883
|
}
|
|
3039
|
-
//
|
|
3040
|
-
|
|
3041
|
-
for (let i = 0; i <
|
|
3042
|
-
indices[i] = triangleIndices[
|
|
2884
|
+
// Front cap faces -Z, reverse winding from libtess CCW output
|
|
2885
|
+
const triLen = triangleIndices.length;
|
|
2886
|
+
for (let i = 0; i < triLen; i++) {
|
|
2887
|
+
indices[i] = triangleIndices[triLen - 1 - i];
|
|
3043
2888
|
}
|
|
3044
|
-
//
|
|
3045
|
-
for (let i = 0; i <
|
|
3046
|
-
indices[
|
|
2889
|
+
// Back cap faces +Z, use original winding
|
|
2890
|
+
for (let i = 0; i < triLen; i++) {
|
|
2891
|
+
indices[triLen + i] = triangleIndices[i] + numPoints;
|
|
3047
2892
|
}
|
|
3048
|
-
// Side walls
|
|
3049
2893
|
let nextVertex = numPoints * 2;
|
|
3050
|
-
let idxPos =
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
const
|
|
3054
|
-
const
|
|
2894
|
+
let idxPos = triLen * 2;
|
|
2895
|
+
const numEdges = boundaryEdges.length;
|
|
2896
|
+
for (let e = 0; e < numEdges; e++) {
|
|
2897
|
+
const edge = boundaryEdges[e];
|
|
2898
|
+
const u = edge[0];
|
|
2899
|
+
const v = edge[1];
|
|
2900
|
+
const u2 = u << 1;
|
|
2901
|
+
const v2 = v << 1;
|
|
3055
2902
|
const p0x = points[u2];
|
|
3056
2903
|
const p0y = points[u2 + 1];
|
|
3057
2904
|
const p1x = points[v2];
|
|
3058
2905
|
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
2906
|
const ex = p1x - p0x;
|
|
3062
2907
|
const ey = p1y - p0y;
|
|
3063
2908
|
const lenSq = ex * ex + ey * ey;
|
|
3064
2909
|
let nx = 0;
|
|
3065
2910
|
let ny = 0;
|
|
3066
|
-
if (lenSq >
|
|
2911
|
+
if (lenSq > 1e-10) {
|
|
3067
2912
|
const invLen = 1 / Math.sqrt(lenSq);
|
|
3068
2913
|
nx = ey * invLen;
|
|
3069
2914
|
ny = -ex * invLen;
|
|
3070
2915
|
}
|
|
3071
|
-
const
|
|
3072
|
-
const base = baseVertex * 3;
|
|
3073
|
-
// Wall quad: front edge at z=0, back edge at z=depth
|
|
2916
|
+
const base = nextVertex * 3;
|
|
3074
2917
|
vertices[base] = p0x;
|
|
3075
2918
|
vertices[base + 1] = p0y;
|
|
3076
2919
|
vertices[base + 2] = 0;
|
|
@@ -3083,7 +2926,6 @@ class Extruder {
|
|
|
3083
2926
|
vertices[base + 9] = p1x;
|
|
3084
2927
|
vertices[base + 10] = p1y;
|
|
3085
2928
|
vertices[base + 11] = backZ;
|
|
3086
|
-
// Wall normals point perpendicular to edge
|
|
3087
2929
|
normals[base] = nx;
|
|
3088
2930
|
normals[base + 1] = ny;
|
|
3089
2931
|
normals[base + 2] = 0;
|
|
@@ -3096,13 +2938,14 @@ class Extruder {
|
|
|
3096
2938
|
normals[base + 9] = nx;
|
|
3097
2939
|
normals[base + 10] = ny;
|
|
3098
2940
|
normals[base + 11] = 0;
|
|
3099
|
-
|
|
3100
|
-
indices[idxPos
|
|
3101
|
-
indices[idxPos
|
|
3102
|
-
indices[idxPos
|
|
3103
|
-
indices[idxPos
|
|
3104
|
-
indices[idxPos
|
|
3105
|
-
indices[idxPos
|
|
2941
|
+
const baseVertex = nextVertex;
|
|
2942
|
+
indices[idxPos] = baseVertex;
|
|
2943
|
+
indices[idxPos + 1] = baseVertex + 1;
|
|
2944
|
+
indices[idxPos + 2] = baseVertex + 2;
|
|
2945
|
+
indices[idxPos + 3] = baseVertex + 1;
|
|
2946
|
+
indices[idxPos + 4] = baseVertex + 3;
|
|
2947
|
+
indices[idxPos + 5] = baseVertex + 2;
|
|
2948
|
+
idxPos += 6;
|
|
3106
2949
|
nextVertex += 4;
|
|
3107
2950
|
}
|
|
3108
2951
|
return { vertices, normals, indices };
|
|
@@ -4119,7 +3962,7 @@ class GlyphGeometryBuilder {
|
|
|
4119
3962
|
].join('|');
|
|
4120
3963
|
}
|
|
4121
3964
|
// Build instanced geometry from glyph contours
|
|
4122
|
-
buildInstancedGeometry(clustersByLine, depth, removeOverlaps, isCFF, separateGlyphs = false, coloredTextIndices) {
|
|
3965
|
+
buildInstancedGeometry(clustersByLine, depth, removeOverlaps, isCFF, scale, separateGlyphs = false, coloredTextIndices) {
|
|
4123
3966
|
if (isLogEnabled) {
|
|
4124
3967
|
let wordCount = 0;
|
|
4125
3968
|
for (let i = 0; i < clustersByLine.length; i++) {
|
|
@@ -4297,9 +4140,9 @@ class GlyphGeometryBuilder {
|
|
|
4297
4140
|
const py = task.py;
|
|
4298
4141
|
const pz = task.pz;
|
|
4299
4142
|
for (let j = 0; j < v.length; j += 3) {
|
|
4300
|
-
vertexArray[vertexPos++] = v[j] + px;
|
|
4301
|
-
vertexArray[vertexPos++] = v[j + 1] + py;
|
|
4302
|
-
vertexArray[vertexPos++] = v[j + 2] + pz;
|
|
4143
|
+
vertexArray[vertexPos++] = (v[j] + px) * scale;
|
|
4144
|
+
vertexArray[vertexPos++] = (v[j + 1] + py) * scale;
|
|
4145
|
+
vertexArray[vertexPos++] = (v[j + 2] + pz) * scale;
|
|
4303
4146
|
}
|
|
4304
4147
|
normalArray.set(n, normalPos);
|
|
4305
4148
|
normalPos += n.length;
|
|
@@ -4309,6 +4152,20 @@ class GlyphGeometryBuilder {
|
|
|
4309
4152
|
}
|
|
4310
4153
|
}
|
|
4311
4154
|
perfLogger.end('GlyphGeometryBuilder.buildInstancedGeometry');
|
|
4155
|
+
planeBounds.min.x *= scale;
|
|
4156
|
+
planeBounds.min.y *= scale;
|
|
4157
|
+
planeBounds.min.z *= scale;
|
|
4158
|
+
planeBounds.max.x *= scale;
|
|
4159
|
+
planeBounds.max.y *= scale;
|
|
4160
|
+
planeBounds.max.z *= scale;
|
|
4161
|
+
for (let i = 0; i < glyphInfos.length; i++) {
|
|
4162
|
+
glyphInfos[i].bounds.min.x *= scale;
|
|
4163
|
+
glyphInfos[i].bounds.min.y *= scale;
|
|
4164
|
+
glyphInfos[i].bounds.min.z *= scale;
|
|
4165
|
+
glyphInfos[i].bounds.max.x *= scale;
|
|
4166
|
+
glyphInfos[i].bounds.max.y *= scale;
|
|
4167
|
+
glyphInfos[i].bounds.max.z *= scale;
|
|
4168
|
+
}
|
|
4312
4169
|
return {
|
|
4313
4170
|
vertices: vertexArray,
|
|
4314
4171
|
normals: normalArray,
|
|
@@ -4320,7 +4177,6 @@ class GlyphGeometryBuilder {
|
|
|
4320
4177
|
getClusterKey(glyphs, depth, removeOverlaps) {
|
|
4321
4178
|
if (glyphs.length === 0)
|
|
4322
4179
|
return '';
|
|
4323
|
-
// Normalize positions relative to the first glyph in the cluster
|
|
4324
4180
|
const refX = glyphs[0].x ?? 0;
|
|
4325
4181
|
const refY = glyphs[0].y ?? 0;
|
|
4326
4182
|
const parts = glyphs.map((g) => {
|
|
@@ -5375,6 +5231,7 @@ class TextRangeQuery {
|
|
|
5375
5231
|
}
|
|
5376
5232
|
|
|
5377
5233
|
const DEFAULT_MAX_TEXT_LENGTH = 100000;
|
|
5234
|
+
const DEFAULT_FONT_SIZE = 72;
|
|
5378
5235
|
class Text {
|
|
5379
5236
|
static { this.patternCache = new Map(); }
|
|
5380
5237
|
static { this.hbInitPromise = null; }
|
|
@@ -5448,7 +5305,7 @@ class Text {
|
|
|
5448
5305
|
return {
|
|
5449
5306
|
...newResult,
|
|
5450
5307
|
getLoadedFont: () => text.getLoadedFont(),
|
|
5451
|
-
|
|
5308
|
+
getCacheSize: () => text.getCacheSize(),
|
|
5452
5309
|
clearCache: () => text.clearCache(),
|
|
5453
5310
|
measureTextWidth: (textString, letterSpacing) => text.measureTextWidth(textString, letterSpacing),
|
|
5454
5311
|
update
|
|
@@ -5457,7 +5314,7 @@ class Text {
|
|
|
5457
5314
|
return {
|
|
5458
5315
|
...result,
|
|
5459
5316
|
getLoadedFont: () => text.getLoadedFont(),
|
|
5460
|
-
|
|
5317
|
+
getCacheSize: () => text.getCacheSize(),
|
|
5461
5318
|
clearCache: () => text.clearCache(),
|
|
5462
5319
|
measureTextWidth: (textString, letterSpacing) => text.measureTextWidth(textString, letterSpacing),
|
|
5463
5320
|
update
|
|
@@ -5589,7 +5446,7 @@ class Text {
|
|
|
5589
5446
|
async createGeometry(options) {
|
|
5590
5447
|
perfLogger.start('Text.createGeometry', {
|
|
5591
5448
|
textLength: options.text.length,
|
|
5592
|
-
size: options.size ||
|
|
5449
|
+
size: options.size || DEFAULT_FONT_SIZE,
|
|
5593
5450
|
hasLayout: !!options.layout,
|
|
5594
5451
|
mode: 'cached'
|
|
5595
5452
|
});
|
|
@@ -5603,7 +5460,7 @@ class Text {
|
|
|
5603
5460
|
this.updateFontVariations(options);
|
|
5604
5461
|
if (!this.geometryBuilder) {
|
|
5605
5462
|
const cache = options.maxCacheSizeMB
|
|
5606
|
-
? createGlyphCache(
|
|
5463
|
+
? createGlyphCache()
|
|
5607
5464
|
: globalGlyphCache;
|
|
5608
5465
|
this.geometryBuilder = new GlyphGeometryBuilder(cache, this.loadedFont);
|
|
5609
5466
|
this.geometryBuilder.setFontId(this.currentFontId);
|
|
@@ -5649,10 +5506,9 @@ class Text {
|
|
|
5649
5506
|
}
|
|
5650
5507
|
}
|
|
5651
5508
|
}
|
|
5652
|
-
const shapedResult = this.geometryBuilder.buildInstancedGeometry(clustersByLine, layoutData.depth, shouldRemoveOverlaps, this.loadedFont.metrics.isCFF, options.
|
|
5653
|
-
const
|
|
5654
|
-
|
|
5655
|
-
if (options.separateGlyphsWithAttributes) {
|
|
5509
|
+
const shapedResult = this.geometryBuilder.buildInstancedGeometry(clustersByLine, layoutData.depth, shouldRemoveOverlaps, this.loadedFont.metrics.isCFF, layoutData.pixelsPerFontUnit, options.perGlyphAttributes ?? false, coloredTextIndices);
|
|
5510
|
+
const result = this.finalizeGeometry(shapedResult.vertices, shapedResult.normals, shapedResult.indices, shapedResult.glyphInfos, shapedResult.planeBounds, options, options.text);
|
|
5511
|
+
if (options.perGlyphAttributes) {
|
|
5656
5512
|
const glyphAttrs = this.createGlyphAttributes(result.vertices.length / 3, result.glyphs);
|
|
5657
5513
|
result.glyphAttributes = glyphAttrs;
|
|
5658
5514
|
}
|
|
@@ -5719,16 +5575,16 @@ class Text {
|
|
|
5719
5575
|
if (!this.loadedFont) {
|
|
5720
5576
|
throw new Error('Font not loaded. Use Text.create() with a font option');
|
|
5721
5577
|
}
|
|
5722
|
-
const { text, size =
|
|
5578
|
+
const { text, size = DEFAULT_FONT_SIZE, depth = 0, lineHeight = 1.0, letterSpacing = 0, layout = {} } = options;
|
|
5723
5579
|
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;
|
|
5580
|
+
const fontUnitsPerPixel = this.loadedFont.upem / size;
|
|
5724
5581
|
let widthInFontUnits;
|
|
5725
5582
|
if (width !== undefined) {
|
|
5726
|
-
widthInFontUnits = width *
|
|
5583
|
+
widthInFontUnits = width * fontUnitsPerPixel;
|
|
5727
5584
|
}
|
|
5728
5585
|
// Keep depth behavior consistent with Extruder: extremely small non-zero depths
|
|
5729
|
-
// are clamped to a minimum back offset
|
|
5730
|
-
const
|
|
5731
|
-
const rawDepthInFontUnits = depth * depthScale;
|
|
5586
|
+
// are clamped to a minimum back offset to prevent Z fighting
|
|
5587
|
+
const rawDepthInFontUnits = depth * fontUnitsPerPixel;
|
|
5732
5588
|
const minExtrudeDepth = this.loadedFont.upem * 0.000025;
|
|
5733
5589
|
const depthInFontUnits = rawDepthInFontUnits <= 0
|
|
5734
5590
|
? 0
|
|
@@ -5771,7 +5627,8 @@ class Text {
|
|
|
5771
5627
|
align,
|
|
5772
5628
|
direction,
|
|
5773
5629
|
depth: depthInFontUnits,
|
|
5774
|
-
size
|
|
5630
|
+
size,
|
|
5631
|
+
pixelsPerFontUnit: 1 / fontUnitsPerPixel
|
|
5775
5632
|
};
|
|
5776
5633
|
}
|
|
5777
5634
|
applyColorSystem(vertices, glyphInfoArray, color, originalText) {
|
|
@@ -5867,8 +5724,8 @@ class Text {
|
|
|
5867
5724
|
}
|
|
5868
5725
|
return { colors, coloredRanges };
|
|
5869
5726
|
}
|
|
5870
|
-
finalizeGeometry(vertices, normals, indices, glyphInfoArray, planeBounds, options,
|
|
5871
|
-
const { layout = {}
|
|
5727
|
+
finalizeGeometry(vertices, normals, indices, glyphInfoArray, planeBounds, options, originalText) {
|
|
5728
|
+
const { layout = {} } = options;
|
|
5872
5729
|
const { width, align = layout.direction === 'rtl' ? 'right' : 'left' } = layout;
|
|
5873
5730
|
if (!this.textLayout) {
|
|
5874
5731
|
this.textLayout = new TextLayout(this.loadedFont);
|
|
@@ -5881,37 +5738,14 @@ class Text {
|
|
|
5881
5738
|
const offset = alignmentResult.offset;
|
|
5882
5739
|
planeBounds.min.x = alignmentResult.adjustedBounds.min.x;
|
|
5883
5740
|
planeBounds.max.x = alignmentResult.adjustedBounds.max.x;
|
|
5884
|
-
|
|
5885
|
-
const offsetScaled = offset * finalScale;
|
|
5886
|
-
// Scale vertices only (normals are unit vectors, don't scale)
|
|
5887
|
-
if (offsetScaled === 0) {
|
|
5888
|
-
for (let i = 0; i < vertices.length; i++) {
|
|
5889
|
-
vertices[i] *= finalScale;
|
|
5890
|
-
}
|
|
5891
|
-
}
|
|
5892
|
-
else {
|
|
5741
|
+
if (offset !== 0) {
|
|
5893
5742
|
for (let i = 0; i < vertices.length; i += 3) {
|
|
5894
|
-
vertices[i]
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
planeBounds.min.y *= finalScale;
|
|
5901
|
-
planeBounds.min.z *= finalScale;
|
|
5902
|
-
planeBounds.max.x *= finalScale;
|
|
5903
|
-
planeBounds.max.y *= finalScale;
|
|
5904
|
-
planeBounds.max.z *= finalScale;
|
|
5905
|
-
for (let i = 0; i < glyphInfoArray.length; i++) {
|
|
5906
|
-
const glyphInfo = glyphInfoArray[i];
|
|
5907
|
-
glyphInfo.bounds.min.x =
|
|
5908
|
-
glyphInfo.bounds.min.x * finalScale + offsetScaled;
|
|
5909
|
-
glyphInfo.bounds.min.y *= finalScale;
|
|
5910
|
-
glyphInfo.bounds.min.z *= finalScale;
|
|
5911
|
-
glyphInfo.bounds.max.x =
|
|
5912
|
-
glyphInfo.bounds.max.x * finalScale + offsetScaled;
|
|
5913
|
-
glyphInfo.bounds.max.y *= finalScale;
|
|
5914
|
-
glyphInfo.bounds.max.z *= finalScale;
|
|
5743
|
+
vertices[i] += offset;
|
|
5744
|
+
}
|
|
5745
|
+
for (let i = 0; i < glyphInfoArray.length; i++) {
|
|
5746
|
+
glyphInfoArray[i].bounds.min.x += offset;
|
|
5747
|
+
glyphInfoArray[i].bounds.max.x += offset;
|
|
5748
|
+
}
|
|
5915
5749
|
}
|
|
5916
5750
|
let colors;
|
|
5917
5751
|
let coloredRanges;
|
|
@@ -5936,8 +5770,7 @@ class Text {
|
|
|
5936
5770
|
verticesGenerated,
|
|
5937
5771
|
pointsRemovedByVisvalingam: optimizationStats.pointsRemovedByVisvalingam,
|
|
5938
5772
|
pointsRemovedByColinear: optimizationStats.pointsRemovedByColinear,
|
|
5939
|
-
originalPointCount: optimizationStats.originalPointCount
|
|
5940
|
-
...(cacheStats || {})
|
|
5773
|
+
originalPointCount: optimizationStats.originalPointCount
|
|
5941
5774
|
},
|
|
5942
5775
|
query: (options) => {
|
|
5943
5776
|
if (!originalText) {
|
|
@@ -5988,11 +5821,11 @@ class Text {
|
|
|
5988
5821
|
}
|
|
5989
5822
|
return TextMeasurer.measureTextWidth(this.loadedFont, text, letterSpacing);
|
|
5990
5823
|
}
|
|
5991
|
-
|
|
5824
|
+
getCacheSize() {
|
|
5992
5825
|
if (this.geometryBuilder) {
|
|
5993
|
-
return this.geometryBuilder.getCacheStats();
|
|
5826
|
+
return this.geometryBuilder.getCacheStats().size;
|
|
5994
5827
|
}
|
|
5995
|
-
return
|
|
5828
|
+
return 0;
|
|
5996
5829
|
}
|
|
5997
5830
|
clearCache() {
|
|
5998
5831
|
if (this.geometryBuilder) {
|
|
@@ -6003,25 +5836,45 @@ class Text {
|
|
|
6003
5836
|
const glyphCenters = new Float32Array(vertexCount * 3);
|
|
6004
5837
|
const glyphIndices = new Float32Array(vertexCount);
|
|
6005
5838
|
const glyphLineIndices = new Float32Array(vertexCount);
|
|
6006
|
-
|
|
5839
|
+
const glyphProgress = new Float32Array(vertexCount);
|
|
5840
|
+
const glyphBaselineY = new Float32Array(vertexCount);
|
|
5841
|
+
let minX = Infinity;
|
|
5842
|
+
let maxX = -Infinity;
|
|
5843
|
+
for (let i = 0; i < glyphs.length; i++) {
|
|
5844
|
+
const cx = (glyphs[i].bounds.min.x + glyphs[i].bounds.max.x) / 2;
|
|
5845
|
+
if (cx < minX)
|
|
5846
|
+
minX = cx;
|
|
5847
|
+
if (cx > maxX)
|
|
5848
|
+
maxX = cx;
|
|
5849
|
+
}
|
|
5850
|
+
const range = maxX - minX;
|
|
5851
|
+
for (let index = 0; index < glyphs.length; index++) {
|
|
5852
|
+
const glyph = glyphs[index];
|
|
6007
5853
|
const centerX = (glyph.bounds.min.x + glyph.bounds.max.x) / 2;
|
|
6008
5854
|
const centerY = (glyph.bounds.min.y + glyph.bounds.max.y) / 2;
|
|
6009
5855
|
const centerZ = (glyph.bounds.min.z + glyph.bounds.max.z) / 2;
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
5856
|
+
const baselineY = glyph.bounds.min.y;
|
|
5857
|
+
const progress = range > 0 ? (centerX - minX) / range : 0;
|
|
5858
|
+
const start = glyph.vertexStart;
|
|
5859
|
+
const end = Math.min(start + glyph.vertexCount, vertexCount);
|
|
5860
|
+
if (end <= start)
|
|
5861
|
+
continue;
|
|
5862
|
+
glyphIndices.fill(index, start, end);
|
|
5863
|
+
glyphLineIndices.fill(glyph.lineIndex, start, end);
|
|
5864
|
+
glyphProgress.fill(progress, start, end);
|
|
5865
|
+
glyphBaselineY.fill(baselineY, start, end);
|
|
5866
|
+
for (let v = start * 3; v < end * 3; v += 3) {
|
|
5867
|
+
glyphCenters[v] = centerX;
|
|
5868
|
+
glyphCenters[v + 1] = centerY;
|
|
5869
|
+
glyphCenters[v + 2] = centerZ;
|
|
6019
5870
|
}
|
|
6020
|
-
}
|
|
5871
|
+
}
|
|
6021
5872
|
return {
|
|
6022
5873
|
glyphCenter: glyphCenters,
|
|
6023
5874
|
glyphIndex: glyphIndices,
|
|
6024
|
-
glyphLineIndex: glyphLineIndices
|
|
5875
|
+
glyphLineIndex: glyphLineIndices,
|
|
5876
|
+
glyphProgress,
|
|
5877
|
+
glyphBaselineY
|
|
6025
5878
|
};
|
|
6026
5879
|
}
|
|
6027
5880
|
resetHelpers() {
|