three-text 0.2.18 → 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 +14 -10
- package/dist/index.cjs +215 -350
- package/dist/index.d.ts +27 -50
- package/dist/index.js +215 -350
- package/dist/index.min.cjs +704 -723
- package/dist/index.min.js +705 -724
- package/dist/index.umd.js +215 -350
- package/dist/index.umd.min.js +710 -729
- package/dist/three/index.cjs +4 -1
- package/dist/three/index.d.ts +2 -1
- package/dist/three/index.js +4 -1
- package/dist/three/react.cjs +2 -1
- package/dist/three/react.d.ts +15 -17
- package/dist/three/react.js +2 -1
- package/dist/types/core/Text.d.ts +1 -5
- 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 +10 -4
- package/dist/types/index.d.ts +1 -1
- package/dist/types/three/index.d.ts +2 -1
- package/dist/types/three/react.d.ts +1 -0
- 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 +22 -6
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
|
|
@@ -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
|
}
|
|
@@ -2376,231 +2382,50 @@ class Vec3 {
|
|
|
2376
2382
|
}
|
|
2377
2383
|
}
|
|
2378
2384
|
|
|
2379
|
-
//
|
|
2380
|
-
class
|
|
2381
|
-
constructor(
|
|
2385
|
+
// Map-based cache with no eviction policy
|
|
2386
|
+
class Cache {
|
|
2387
|
+
constructor() {
|
|
2382
2388
|
this.cache = new Map();
|
|
2383
|
-
this.head = null;
|
|
2384
|
-
this.tail = null;
|
|
2385
|
-
this.stats = {
|
|
2386
|
-
hits: 0,
|
|
2387
|
-
misses: 0,
|
|
2388
|
-
evictions: 0,
|
|
2389
|
-
size: 0,
|
|
2390
|
-
memoryUsage: 0
|
|
2391
|
-
};
|
|
2392
|
-
this.options = {
|
|
2393
|
-
maxEntries: options.maxEntries ?? Infinity,
|
|
2394
|
-
maxMemoryBytes: options.maxMemoryBytes ?? Infinity,
|
|
2395
|
-
calculateSize: options.calculateSize ?? (() => 0),
|
|
2396
|
-
onEvict: options.onEvict
|
|
2397
|
-
};
|
|
2398
2389
|
}
|
|
2399
2390
|
get(key) {
|
|
2400
|
-
|
|
2401
|
-
if (node) {
|
|
2402
|
-
this.stats.hits++;
|
|
2403
|
-
this.moveToHead(node);
|
|
2404
|
-
return node.value;
|
|
2405
|
-
}
|
|
2406
|
-
else {
|
|
2407
|
-
this.stats.misses++;
|
|
2408
|
-
return undefined;
|
|
2409
|
-
}
|
|
2391
|
+
return this.cache.get(key);
|
|
2410
2392
|
}
|
|
2411
2393
|
has(key) {
|
|
2412
2394
|
return this.cache.has(key);
|
|
2413
2395
|
}
|
|
2414
2396
|
set(key, value) {
|
|
2415
|
-
|
|
2416
|
-
const existingNode = this.cache.get(key);
|
|
2417
|
-
if (existingNode) {
|
|
2418
|
-
const oldSize = this.options.calculateSize(existingNode.value);
|
|
2419
|
-
const newSize = this.options.calculateSize(value);
|
|
2420
|
-
this.stats.memoryUsage = this.stats.memoryUsage - oldSize + newSize;
|
|
2421
|
-
existingNode.value = value;
|
|
2422
|
-
this.moveToHead(existingNode);
|
|
2423
|
-
return;
|
|
2424
|
-
}
|
|
2425
|
-
const size = this.options.calculateSize(value);
|
|
2426
|
-
// Evict entries if we exceed limits
|
|
2427
|
-
this.evictIfNeeded(size);
|
|
2428
|
-
// Create new node
|
|
2429
|
-
const node = {
|
|
2430
|
-
key,
|
|
2431
|
-
value,
|
|
2432
|
-
prev: null,
|
|
2433
|
-
next: null
|
|
2434
|
-
};
|
|
2435
|
-
this.cache.set(key, node);
|
|
2436
|
-
this.addToHead(node);
|
|
2437
|
-
this.stats.size = this.cache.size;
|
|
2438
|
-
this.stats.memoryUsage += size;
|
|
2397
|
+
this.cache.set(key, value);
|
|
2439
2398
|
}
|
|
2440
2399
|
delete(key) {
|
|
2441
|
-
|
|
2442
|
-
if (!node)
|
|
2443
|
-
return false;
|
|
2444
|
-
const size = this.options.calculateSize(node.value);
|
|
2445
|
-
this.removeNode(node);
|
|
2446
|
-
this.cache.delete(key);
|
|
2447
|
-
this.stats.size = this.cache.size;
|
|
2448
|
-
this.stats.memoryUsage -= size;
|
|
2449
|
-
if (this.options.onEvict) {
|
|
2450
|
-
this.options.onEvict(key, node.value);
|
|
2451
|
-
}
|
|
2452
|
-
return true;
|
|
2400
|
+
return this.cache.delete(key);
|
|
2453
2401
|
}
|
|
2454
2402
|
clear() {
|
|
2455
|
-
if (this.options.onEvict) {
|
|
2456
|
-
for (const [key, node] of this.cache) {
|
|
2457
|
-
this.options.onEvict(key, node.value);
|
|
2458
|
-
}
|
|
2459
|
-
}
|
|
2460
2403
|
this.cache.clear();
|
|
2461
|
-
this.head = null;
|
|
2462
|
-
this.tail = null;
|
|
2463
|
-
this.stats = {
|
|
2464
|
-
hits: 0,
|
|
2465
|
-
misses: 0,
|
|
2466
|
-
evictions: 0,
|
|
2467
|
-
size: 0,
|
|
2468
|
-
memoryUsage: 0
|
|
2469
|
-
};
|
|
2470
|
-
}
|
|
2471
|
-
getStats() {
|
|
2472
|
-
const total = this.stats.hits + this.stats.misses;
|
|
2473
|
-
const hitRate = total > 0 ? (this.stats.hits / total) * 100 : 0;
|
|
2474
|
-
const memoryUsageMB = this.stats.memoryUsage / (1024 * 1024);
|
|
2475
|
-
return {
|
|
2476
|
-
...this.stats,
|
|
2477
|
-
hitRate,
|
|
2478
|
-
memoryUsageMB
|
|
2479
|
-
};
|
|
2480
|
-
}
|
|
2481
|
-
keys() {
|
|
2482
|
-
const keys = [];
|
|
2483
|
-
let current = this.head;
|
|
2484
|
-
while (current) {
|
|
2485
|
-
keys.push(current.key);
|
|
2486
|
-
current = current.next;
|
|
2487
|
-
}
|
|
2488
|
-
return keys;
|
|
2489
2404
|
}
|
|
2490
2405
|
get size() {
|
|
2491
2406
|
return this.cache.size;
|
|
2492
2407
|
}
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
while (this.cache.size >= this.options.maxEntries && this.tail) {
|
|
2496
|
-
this.evictTail();
|
|
2497
|
-
}
|
|
2498
|
-
// Evict by memory usage
|
|
2499
|
-
if (this.options.maxMemoryBytes < Infinity) {
|
|
2500
|
-
while (this.tail &&
|
|
2501
|
-
this.stats.memoryUsage + requiredSize > this.options.maxMemoryBytes) {
|
|
2502
|
-
this.evictTail();
|
|
2503
|
-
}
|
|
2504
|
-
}
|
|
2505
|
-
}
|
|
2506
|
-
evictTail() {
|
|
2507
|
-
if (!this.tail)
|
|
2508
|
-
return;
|
|
2509
|
-
const nodeToRemove = this.tail;
|
|
2510
|
-
const size = this.options.calculateSize(nodeToRemove.value);
|
|
2511
|
-
this.removeTail();
|
|
2512
|
-
this.cache.delete(nodeToRemove.key);
|
|
2513
|
-
this.stats.size = this.cache.size;
|
|
2514
|
-
this.stats.memoryUsage -= size;
|
|
2515
|
-
this.stats.evictions++;
|
|
2516
|
-
if (this.options.onEvict) {
|
|
2517
|
-
this.options.onEvict(nodeToRemove.key, nodeToRemove.value);
|
|
2518
|
-
}
|
|
2519
|
-
}
|
|
2520
|
-
addToHead(node) {
|
|
2521
|
-
node.prev = null;
|
|
2522
|
-
node.next = null;
|
|
2523
|
-
if (!this.head) {
|
|
2524
|
-
this.head = this.tail = node;
|
|
2525
|
-
}
|
|
2526
|
-
else {
|
|
2527
|
-
node.next = this.head;
|
|
2528
|
-
this.head.prev = node;
|
|
2529
|
-
this.head = node;
|
|
2530
|
-
}
|
|
2531
|
-
}
|
|
2532
|
-
removeNode(node) {
|
|
2533
|
-
if (node.prev) {
|
|
2534
|
-
node.prev.next = node.next;
|
|
2535
|
-
}
|
|
2536
|
-
else {
|
|
2537
|
-
this.head = node.next;
|
|
2538
|
-
}
|
|
2539
|
-
if (node.next) {
|
|
2540
|
-
node.next.prev = node.prev;
|
|
2541
|
-
}
|
|
2542
|
-
else {
|
|
2543
|
-
this.tail = node.prev;
|
|
2544
|
-
}
|
|
2545
|
-
}
|
|
2546
|
-
removeTail() {
|
|
2547
|
-
if (this.tail) {
|
|
2548
|
-
this.removeNode(this.tail);
|
|
2549
|
-
}
|
|
2408
|
+
keys() {
|
|
2409
|
+
return Array.from(this.cache.keys());
|
|
2550
2410
|
}
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
this.addToHead(node);
|
|
2411
|
+
getStats() {
|
|
2412
|
+
return {
|
|
2413
|
+
size: this.cache.size
|
|
2414
|
+
};
|
|
2556
2415
|
}
|
|
2557
2416
|
}
|
|
2558
2417
|
|
|
2559
|
-
const DEFAULT_CACHE_SIZE_MB = 250;
|
|
2560
2418
|
function getGlyphCacheKey(fontId, glyphId, depth, removeOverlaps) {
|
|
2561
2419
|
const roundedDepth = Math.round(depth * 1000) / 1000;
|
|
2562
2420
|
return `${fontId}_${glyphId}_${roundedDepth}_${removeOverlaps}`;
|
|
2563
2421
|
}
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
size += glyph.normals.length * 4;
|
|
2568
|
-
size += glyph.indices.length * glyph.indices.BYTES_PER_ELEMENT;
|
|
2569
|
-
size += 24; // 2 Vec3s
|
|
2570
|
-
size += 256; // Object overhead
|
|
2571
|
-
return size;
|
|
2572
|
-
}
|
|
2573
|
-
const globalGlyphCache = new LRUCache({
|
|
2574
|
-
maxEntries: Infinity,
|
|
2575
|
-
maxMemoryBytes: DEFAULT_CACHE_SIZE_MB * 1024 * 1024,
|
|
2576
|
-
calculateSize: calculateGlyphMemoryUsage
|
|
2577
|
-
});
|
|
2578
|
-
function createGlyphCache(maxCacheSizeMB = DEFAULT_CACHE_SIZE_MB) {
|
|
2579
|
-
return new LRUCache({
|
|
2580
|
-
maxEntries: Infinity,
|
|
2581
|
-
maxMemoryBytes: maxCacheSizeMB * 1024 * 1024,
|
|
2582
|
-
calculateSize: calculateGlyphMemoryUsage
|
|
2583
|
-
});
|
|
2422
|
+
const globalGlyphCache = new Cache();
|
|
2423
|
+
function createGlyphCache() {
|
|
2424
|
+
return new Cache();
|
|
2584
2425
|
}
|
|
2585
|
-
|
|
2586
|
-
const
|
|
2587
|
-
|
|
2588
|
-
calculateSize: (contours) => {
|
|
2589
|
-
let size = 0;
|
|
2590
|
-
for (const path of contours.paths) {
|
|
2591
|
-
size += path.points.length * 16; // Vec2 = 2 floats * 8 bytes
|
|
2592
|
-
}
|
|
2593
|
-
return size + 64; // bounds overhead
|
|
2594
|
-
}
|
|
2595
|
-
});
|
|
2596
|
-
const globalWordCache = new LRUCache({
|
|
2597
|
-
maxEntries: 1000,
|
|
2598
|
-
calculateSize: calculateGlyphMemoryUsage
|
|
2599
|
-
});
|
|
2600
|
-
const globalClusteringCache = new LRUCache({
|
|
2601
|
-
maxEntries: 2000,
|
|
2602
|
-
calculateSize: () => 1
|
|
2603
|
-
});
|
|
2426
|
+
const globalContourCache = new Cache();
|
|
2427
|
+
const globalWordCache = new Cache();
|
|
2428
|
+
const globalClusteringCache = new Cache();
|
|
2604
2429
|
|
|
2605
2430
|
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
|
|
2606
2431
|
|
|
@@ -2729,7 +2554,7 @@ class Tessellator {
|
|
|
2729
2554
|
let extrusionContours = needsExtrusionContours
|
|
2730
2555
|
? needsWindingReversal
|
|
2731
2556
|
? tessContours
|
|
2732
|
-
: originalContours ?? this.pathsToContours(paths)
|
|
2557
|
+
: (originalContours ?? this.pathsToContours(paths))
|
|
2733
2558
|
: [];
|
|
2734
2559
|
if (removeOverlaps) {
|
|
2735
2560
|
logger.log('Two-pass: boundary extraction then triangulation');
|
|
@@ -2762,7 +2587,10 @@ class Tessellator {
|
|
|
2762
2587
|
? 'libtess returned empty result from triangulation pass'
|
|
2763
2588
|
: 'libtess returned empty result from single-pass triangulation';
|
|
2764
2589
|
logger.warn(warning);
|
|
2765
|
-
return {
|
|
2590
|
+
return {
|
|
2591
|
+
triangles: { vertices: [], indices: [] },
|
|
2592
|
+
contours: extrusionContours
|
|
2593
|
+
};
|
|
2766
2594
|
}
|
|
2767
2595
|
return {
|
|
2768
2596
|
triangles: {
|
|
@@ -2932,47 +2760,79 @@ class Tessellator {
|
|
|
2932
2760
|
|
|
2933
2761
|
class Extruder {
|
|
2934
2762
|
constructor() { }
|
|
2935
|
-
packEdge(a, b) {
|
|
2936
|
-
const lo = a < b ? a : b;
|
|
2937
|
-
const hi = a < b ? b : a;
|
|
2938
|
-
return lo * 0x100000000 + hi;
|
|
2939
|
-
}
|
|
2940
2763
|
extrude(geometry, depth = 0, unitsPerEm) {
|
|
2941
2764
|
const points = geometry.triangles.vertices;
|
|
2942
2765
|
const triangleIndices = geometry.triangles.indices;
|
|
2943
2766
|
const numPoints = points.length / 2;
|
|
2944
|
-
//
|
|
2767
|
+
// Boundary edges are those that appear in exactly one triangle
|
|
2945
2768
|
let boundaryEdges = [];
|
|
2946
2769
|
if (depth !== 0) {
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
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) {
|
|
2950
2775
|
const a = triangleIndices[i];
|
|
2951
2776
|
const b = triangleIndices[i + 1];
|
|
2952
2777
|
const c = triangleIndices[i + 2];
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
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
|
+
}
|
|
2968
2830
|
}
|
|
2969
2831
|
boundaryEdges = [];
|
|
2970
|
-
for (const [
|
|
2971
|
-
if (count
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
if (edge)
|
|
2975
|
-
boundaryEdges.push(edge);
|
|
2832
|
+
for (const [v0, v1, count] of edgeMap.values()) {
|
|
2833
|
+
if (count === 1) {
|
|
2834
|
+
boundaryEdges.push([v0, v1]);
|
|
2835
|
+
}
|
|
2976
2836
|
}
|
|
2977
2837
|
}
|
|
2978
2838
|
const sideEdgeCount = depth === 0 ? 0 : boundaryEdges.length;
|
|
@@ -2986,7 +2846,6 @@ class Extruder {
|
|
|
2986
2846
|
: triangleIndices.length * 2 + sideEdgeCount * 6;
|
|
2987
2847
|
const indices = new Uint32Array(indexCount);
|
|
2988
2848
|
if (depth === 0) {
|
|
2989
|
-
// Single-sided flat geometry at z=0
|
|
2990
2849
|
let vPos = 0;
|
|
2991
2850
|
for (let i = 0; i < points.length; i += 2) {
|
|
2992
2851
|
vertices[vPos] = points[i];
|
|
@@ -2997,16 +2856,11 @@ class Extruder {
|
|
|
2997
2856
|
normals[vPos + 2] = 1;
|
|
2998
2857
|
vPos += 3;
|
|
2999
2858
|
}
|
|
3000
|
-
|
|
3001
|
-
for (let i = 0; i < triangleIndices.length; i++) {
|
|
3002
|
-
indices[i] = triangleIndices[i];
|
|
3003
|
-
}
|
|
2859
|
+
indices.set(triangleIndices);
|
|
3004
2860
|
return { vertices, normals, indices };
|
|
3005
2861
|
}
|
|
3006
|
-
// Extruded geometry: front at z=0, back at z=depth
|
|
3007
2862
|
const minBackOffset = unitsPerEm * 0.000025;
|
|
3008
2863
|
const backZ = depth <= minBackOffset ? minBackOffset : depth;
|
|
3009
|
-
// Generate both caps in one pass
|
|
3010
2864
|
for (let p = 0, vi = 0; p < points.length; p += 2, vi++) {
|
|
3011
2865
|
const x = points[p];
|
|
3012
2866
|
const y = points[p + 1];
|
|
@@ -3027,41 +2881,39 @@ class Extruder {
|
|
|
3027
2881
|
normals[baseD + 1] = 0;
|
|
3028
2882
|
normals[baseD + 2] = 1;
|
|
3029
2883
|
}
|
|
3030
|
-
//
|
|
3031
|
-
|
|
3032
|
-
for (let i = 0; i <
|
|
3033
|
-
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];
|
|
3034
2888
|
}
|
|
3035
|
-
//
|
|
3036
|
-
for (let i = 0; i <
|
|
3037
|
-
indices[
|
|
2889
|
+
// Back cap faces +Z, use original winding
|
|
2890
|
+
for (let i = 0; i < triLen; i++) {
|
|
2891
|
+
indices[triLen + i] = triangleIndices[i] + numPoints;
|
|
3038
2892
|
}
|
|
3039
|
-
// Side walls
|
|
3040
2893
|
let nextVertex = numPoints * 2;
|
|
3041
|
-
let idxPos =
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
const
|
|
3045
|
-
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;
|
|
3046
2902
|
const p0x = points[u2];
|
|
3047
2903
|
const p0y = points[u2 + 1];
|
|
3048
2904
|
const p1x = points[v2];
|
|
3049
2905
|
const p1y = points[v2 + 1];
|
|
3050
|
-
// Perpendicular normal for this wall segment
|
|
3051
|
-
// Uses the edge direction from the cap triangulation so winding does not depend on contour direction
|
|
3052
2906
|
const ex = p1x - p0x;
|
|
3053
2907
|
const ey = p1y - p0y;
|
|
3054
2908
|
const lenSq = ex * ex + ey * ey;
|
|
3055
2909
|
let nx = 0;
|
|
3056
2910
|
let ny = 0;
|
|
3057
|
-
if (lenSq >
|
|
2911
|
+
if (lenSq > 1e-10) {
|
|
3058
2912
|
const invLen = 1 / Math.sqrt(lenSq);
|
|
3059
2913
|
nx = ey * invLen;
|
|
3060
2914
|
ny = -ex * invLen;
|
|
3061
2915
|
}
|
|
3062
|
-
const
|
|
3063
|
-
const base = baseVertex * 3;
|
|
3064
|
-
// Wall quad: front edge at z=0, back edge at z=depth
|
|
2916
|
+
const base = nextVertex * 3;
|
|
3065
2917
|
vertices[base] = p0x;
|
|
3066
2918
|
vertices[base + 1] = p0y;
|
|
3067
2919
|
vertices[base + 2] = 0;
|
|
@@ -3074,7 +2926,6 @@ class Extruder {
|
|
|
3074
2926
|
vertices[base + 9] = p1x;
|
|
3075
2927
|
vertices[base + 10] = p1y;
|
|
3076
2928
|
vertices[base + 11] = backZ;
|
|
3077
|
-
// Wall normals point perpendicular to edge
|
|
3078
2929
|
normals[base] = nx;
|
|
3079
2930
|
normals[base + 1] = ny;
|
|
3080
2931
|
normals[base + 2] = 0;
|
|
@@ -3087,13 +2938,14 @@ class Extruder {
|
|
|
3087
2938
|
normals[base + 9] = nx;
|
|
3088
2939
|
normals[base + 10] = ny;
|
|
3089
2940
|
normals[base + 11] = 0;
|
|
3090
|
-
|
|
3091
|
-
indices[idxPos
|
|
3092
|
-
indices[idxPos
|
|
3093
|
-
indices[idxPos
|
|
3094
|
-
indices[idxPos
|
|
3095
|
-
indices[idxPos
|
|
3096
|
-
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;
|
|
3097
2949
|
nextVertex += 4;
|
|
3098
2950
|
}
|
|
3099
2951
|
return { vertices, normals, indices };
|
|
@@ -3419,9 +3271,7 @@ class PathOptimizer {
|
|
|
3419
3271
|
const v1LenSq = v1x * v1x + v1y * v1y;
|
|
3420
3272
|
const v2LenSq = v2x * v2x + v2y * v2y;
|
|
3421
3273
|
const minLenSq = this.config.minSegmentLength * this.config.minSegmentLength;
|
|
3422
|
-
if (angle > threshold ||
|
|
3423
|
-
v1LenSq < minLenSq ||
|
|
3424
|
-
v2LenSq < minLenSq) {
|
|
3274
|
+
if (angle > threshold || v1LenSq < minLenSq || v2LenSq < minLenSq) {
|
|
3425
3275
|
result.push(current);
|
|
3426
3276
|
}
|
|
3427
3277
|
else {
|
|
@@ -4112,7 +3962,7 @@ class GlyphGeometryBuilder {
|
|
|
4112
3962
|
].join('|');
|
|
4113
3963
|
}
|
|
4114
3964
|
// Build instanced geometry from glyph contours
|
|
4115
|
-
buildInstancedGeometry(clustersByLine, depth, removeOverlaps, isCFF, separateGlyphs = false, coloredTextIndices) {
|
|
3965
|
+
buildInstancedGeometry(clustersByLine, depth, removeOverlaps, isCFF, scale, separateGlyphs = false, coloredTextIndices) {
|
|
4116
3966
|
if (isLogEnabled) {
|
|
4117
3967
|
let wordCount = 0;
|
|
4118
3968
|
for (let i = 0; i < clustersByLine.length; i++) {
|
|
@@ -4183,7 +4033,7 @@ class GlyphGeometryBuilder {
|
|
|
4183
4033
|
const relativePositions = cluster.glyphs.map((g) => new Vec3(g.x ?? 0, g.y ?? 0, 0));
|
|
4184
4034
|
boundaryGroups = this.clusterer.cluster(clusterGlyphContours, relativePositions);
|
|
4185
4035
|
this.clusteringCache.set(cacheKey, {
|
|
4186
|
-
glyphIds: cluster.glyphs.map(g => g.g),
|
|
4036
|
+
glyphIds: cluster.glyphs.map((g) => g.g),
|
|
4187
4037
|
groups: boundaryGroups
|
|
4188
4038
|
});
|
|
4189
4039
|
}
|
|
@@ -4290,9 +4140,9 @@ class GlyphGeometryBuilder {
|
|
|
4290
4140
|
const py = task.py;
|
|
4291
4141
|
const pz = task.pz;
|
|
4292
4142
|
for (let j = 0; j < v.length; j += 3) {
|
|
4293
|
-
vertexArray[vertexPos++] = v[j] + px;
|
|
4294
|
-
vertexArray[vertexPos++] = v[j + 1] + py;
|
|
4295
|
-
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;
|
|
4296
4146
|
}
|
|
4297
4147
|
normalArray.set(n, normalPos);
|
|
4298
4148
|
normalPos += n.length;
|
|
@@ -4302,6 +4152,20 @@ class GlyphGeometryBuilder {
|
|
|
4302
4152
|
}
|
|
4303
4153
|
}
|
|
4304
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
|
+
}
|
|
4305
4169
|
return {
|
|
4306
4170
|
vertices: vertexArray,
|
|
4307
4171
|
normals: normalArray,
|
|
@@ -4313,7 +4177,6 @@ class GlyphGeometryBuilder {
|
|
|
4313
4177
|
getClusterKey(glyphs, depth, removeOverlaps) {
|
|
4314
4178
|
if (glyphs.length === 0)
|
|
4315
4179
|
return '';
|
|
4316
|
-
// Normalize positions relative to the first glyph in the cluster
|
|
4317
4180
|
const refX = glyphs[0].x ?? 0;
|
|
4318
4181
|
const refY = glyphs[0].y ?? 0;
|
|
4319
4182
|
const parts = glyphs.map((g) => {
|
|
@@ -4573,7 +4436,8 @@ class TextShaper {
|
|
|
4573
4436
|
if (LineBreak.isCJOpeningPunctuation(currentChar)) {
|
|
4574
4437
|
shouldApply = false;
|
|
4575
4438
|
}
|
|
4576
|
-
if (LineBreak.isCJPunctuation(currentChar) &&
|
|
4439
|
+
if (LineBreak.isCJPunctuation(currentChar) &&
|
|
4440
|
+
LineBreak.isCJPunctuation(nextChar)) {
|
|
4577
4441
|
shouldApply = false;
|
|
4578
4442
|
}
|
|
4579
4443
|
if (shouldApply) {
|
|
@@ -5367,6 +5231,7 @@ class TextRangeQuery {
|
|
|
5367
5231
|
}
|
|
5368
5232
|
|
|
5369
5233
|
const DEFAULT_MAX_TEXT_LENGTH = 100000;
|
|
5234
|
+
const DEFAULT_FONT_SIZE = 72;
|
|
5370
5235
|
class Text {
|
|
5371
5236
|
static { this.patternCache = new Map(); }
|
|
5372
5237
|
static { this.hbInitPromise = null; }
|
|
@@ -5377,7 +5242,7 @@ class Text {
|
|
|
5377
5242
|
// Stringify with sorted keys for cache stability
|
|
5378
5243
|
static stableStringify(obj) {
|
|
5379
5244
|
const keys = Object.keys(obj).sort();
|
|
5380
|
-
const pairs = keys.map(k => `${k}:${obj[k]}`);
|
|
5245
|
+
const pairs = keys.map((k) => `${k}:${obj[k]}`);
|
|
5381
5246
|
return pairs.join(',');
|
|
5382
5247
|
}
|
|
5383
5248
|
constructor() {
|
|
@@ -5440,7 +5305,7 @@ class Text {
|
|
|
5440
5305
|
return {
|
|
5441
5306
|
...newResult,
|
|
5442
5307
|
getLoadedFont: () => text.getLoadedFont(),
|
|
5443
|
-
|
|
5308
|
+
getCacheSize: () => text.getCacheSize(),
|
|
5444
5309
|
clearCache: () => text.clearCache(),
|
|
5445
5310
|
measureTextWidth: (textString, letterSpacing) => text.measureTextWidth(textString, letterSpacing),
|
|
5446
5311
|
update
|
|
@@ -5449,7 +5314,7 @@ class Text {
|
|
|
5449
5314
|
return {
|
|
5450
5315
|
...result,
|
|
5451
5316
|
getLoadedFont: () => text.getLoadedFont(),
|
|
5452
|
-
|
|
5317
|
+
getCacheSize: () => text.getCacheSize(),
|
|
5453
5318
|
clearCache: () => text.clearCache(),
|
|
5454
5319
|
measureTextWidth: (textString, letterSpacing) => text.measureTextWidth(textString, letterSpacing),
|
|
5455
5320
|
update
|
|
@@ -5581,7 +5446,7 @@ class Text {
|
|
|
5581
5446
|
async createGeometry(options) {
|
|
5582
5447
|
perfLogger.start('Text.createGeometry', {
|
|
5583
5448
|
textLength: options.text.length,
|
|
5584
|
-
size: options.size ||
|
|
5449
|
+
size: options.size || DEFAULT_FONT_SIZE,
|
|
5585
5450
|
hasLayout: !!options.layout,
|
|
5586
5451
|
mode: 'cached'
|
|
5587
5452
|
});
|
|
@@ -5595,7 +5460,7 @@ class Text {
|
|
|
5595
5460
|
this.updateFontVariations(options);
|
|
5596
5461
|
if (!this.geometryBuilder) {
|
|
5597
5462
|
const cache = options.maxCacheSizeMB
|
|
5598
|
-
? createGlyphCache(
|
|
5463
|
+
? createGlyphCache()
|
|
5599
5464
|
: globalGlyphCache;
|
|
5600
5465
|
this.geometryBuilder = new GlyphGeometryBuilder(cache, this.loadedFont);
|
|
5601
5466
|
this.geometryBuilder.setFontId(this.currentFontId);
|
|
@@ -5615,7 +5480,9 @@ class Text {
|
|
|
5615
5480
|
// to selectively use glyph-level caching (separate vertices) only for clusters containing
|
|
5616
5481
|
// colored text, while non-colored clusters can still use fast cluster-level merging
|
|
5617
5482
|
let coloredTextIndices;
|
|
5618
|
-
if (options.color &&
|
|
5483
|
+
if (options.color &&
|
|
5484
|
+
typeof options.color === 'object' &&
|
|
5485
|
+
!Array.isArray(options.color)) {
|
|
5619
5486
|
if (options.color.byText || options.color.byCharRange) {
|
|
5620
5487
|
// Build the set manually since glyphs don't exist yet
|
|
5621
5488
|
coloredTextIndices = new Set();
|
|
@@ -5639,10 +5506,9 @@ class Text {
|
|
|
5639
5506
|
}
|
|
5640
5507
|
}
|
|
5641
5508
|
}
|
|
5642
|
-
const shapedResult = this.geometryBuilder.buildInstancedGeometry(clustersByLine, layoutData.depth, shouldRemoveOverlaps, this.loadedFont.metrics.isCFF, options.
|
|
5643
|
-
const
|
|
5644
|
-
|
|
5645
|
-
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) {
|
|
5646
5512
|
const glyphAttrs = this.createGlyphAttributes(result.vertices.length / 3, result.glyphs);
|
|
5647
5513
|
result.glyphAttributes = glyphAttrs;
|
|
5648
5514
|
}
|
|
@@ -5709,18 +5575,20 @@ class Text {
|
|
|
5709
5575
|
if (!this.loadedFont) {
|
|
5710
5576
|
throw new Error('Font not loaded. Use Text.create() with a font option');
|
|
5711
5577
|
}
|
|
5712
|
-
const { text, size =
|
|
5578
|
+
const { text, size = DEFAULT_FONT_SIZE, depth = 0, lineHeight = 1.0, letterSpacing = 0, layout = {} } = options;
|
|
5713
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;
|
|
5714
5581
|
let widthInFontUnits;
|
|
5715
5582
|
if (width !== undefined) {
|
|
5716
|
-
widthInFontUnits = width *
|
|
5583
|
+
widthInFontUnits = width * fontUnitsPerPixel;
|
|
5717
5584
|
}
|
|
5718
5585
|
// Keep depth behavior consistent with Extruder: extremely small non-zero depths
|
|
5719
|
-
// are clamped to a minimum back offset
|
|
5720
|
-
const
|
|
5721
|
-
const rawDepthInFontUnits = depth * depthScale;
|
|
5586
|
+
// are clamped to a minimum back offset to prevent Z fighting
|
|
5587
|
+
const rawDepthInFontUnits = depth * fontUnitsPerPixel;
|
|
5722
5588
|
const minExtrudeDepth = this.loadedFont.upem * 0.000025;
|
|
5723
|
-
const depthInFontUnits = rawDepthInFontUnits <= 0
|
|
5589
|
+
const depthInFontUnits = rawDepthInFontUnits <= 0
|
|
5590
|
+
? 0
|
|
5591
|
+
: Math.max(rawDepthInFontUnits, minExtrudeDepth);
|
|
5724
5592
|
if (!this.textLayout) {
|
|
5725
5593
|
this.textLayout = new TextLayout(this.loadedFont);
|
|
5726
5594
|
}
|
|
@@ -5759,7 +5627,8 @@ class Text {
|
|
|
5759
5627
|
align,
|
|
5760
5628
|
direction,
|
|
5761
5629
|
depth: depthInFontUnits,
|
|
5762
|
-
size
|
|
5630
|
+
size,
|
|
5631
|
+
pixelsPerFontUnit: 1 / fontUnitsPerPixel
|
|
5763
5632
|
};
|
|
5764
5633
|
}
|
|
5765
5634
|
applyColorSystem(vertices, glyphInfoArray, color, originalText) {
|
|
@@ -5855,8 +5724,8 @@ class Text {
|
|
|
5855
5724
|
}
|
|
5856
5725
|
return { colors, coloredRanges };
|
|
5857
5726
|
}
|
|
5858
|
-
finalizeGeometry(vertices, normals, indices, glyphInfoArray, planeBounds, options,
|
|
5859
|
-
const { layout = {}
|
|
5727
|
+
finalizeGeometry(vertices, normals, indices, glyphInfoArray, planeBounds, options, originalText) {
|
|
5728
|
+
const { layout = {} } = options;
|
|
5860
5729
|
const { width, align = layout.direction === 'rtl' ? 'right' : 'left' } = layout;
|
|
5861
5730
|
if (!this.textLayout) {
|
|
5862
5731
|
this.textLayout = new TextLayout(this.loadedFont);
|
|
@@ -5869,35 +5738,14 @@ class Text {
|
|
|
5869
5738
|
const offset = alignmentResult.offset;
|
|
5870
5739
|
planeBounds.min.x = alignmentResult.adjustedBounds.min.x;
|
|
5871
5740
|
planeBounds.max.x = alignmentResult.adjustedBounds.max.x;
|
|
5872
|
-
|
|
5873
|
-
const offsetScaled = offset * finalScale;
|
|
5874
|
-
// Scale vertices only (normals are unit vectors, don't scale)
|
|
5875
|
-
if (offsetScaled === 0) {
|
|
5876
|
-
for (let i = 0; i < vertices.length; i++) {
|
|
5877
|
-
vertices[i] *= finalScale;
|
|
5878
|
-
}
|
|
5879
|
-
}
|
|
5880
|
-
else {
|
|
5741
|
+
if (offset !== 0) {
|
|
5881
5742
|
for (let i = 0; i < vertices.length; i += 3) {
|
|
5882
|
-
vertices[i]
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
planeBounds.min.y *= finalScale;
|
|
5889
|
-
planeBounds.min.z *= finalScale;
|
|
5890
|
-
planeBounds.max.x *= finalScale;
|
|
5891
|
-
planeBounds.max.y *= finalScale;
|
|
5892
|
-
planeBounds.max.z *= finalScale;
|
|
5893
|
-
for (let i = 0; i < glyphInfoArray.length; i++) {
|
|
5894
|
-
const glyphInfo = glyphInfoArray[i];
|
|
5895
|
-
glyphInfo.bounds.min.x = glyphInfo.bounds.min.x * finalScale + offsetScaled;
|
|
5896
|
-
glyphInfo.bounds.min.y *= finalScale;
|
|
5897
|
-
glyphInfo.bounds.min.z *= finalScale;
|
|
5898
|
-
glyphInfo.bounds.max.x = glyphInfo.bounds.max.x * finalScale + offsetScaled;
|
|
5899
|
-
glyphInfo.bounds.max.y *= finalScale;
|
|
5900
|
-
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
|
+
}
|
|
5901
5749
|
}
|
|
5902
5750
|
let colors;
|
|
5903
5751
|
let coloredRanges;
|
|
@@ -5922,8 +5770,7 @@ class Text {
|
|
|
5922
5770
|
verticesGenerated,
|
|
5923
5771
|
pointsRemovedByVisvalingam: optimizationStats.pointsRemovedByVisvalingam,
|
|
5924
5772
|
pointsRemovedByColinear: optimizationStats.pointsRemovedByColinear,
|
|
5925
|
-
originalPointCount: optimizationStats.originalPointCount
|
|
5926
|
-
...(cacheStats || {})
|
|
5773
|
+
originalPointCount: optimizationStats.originalPointCount
|
|
5927
5774
|
},
|
|
5928
5775
|
query: (options) => {
|
|
5929
5776
|
if (!originalText) {
|
|
@@ -5958,13 +5805,11 @@ class Text {
|
|
|
5958
5805
|
static registerPattern(language, pattern) {
|
|
5959
5806
|
Text.patternCache.set(language, pattern);
|
|
5960
5807
|
}
|
|
5961
|
-
static clearFontCache() {
|
|
5962
|
-
Text.fontCache.clear();
|
|
5963
|
-
Text.fontCacheMemoryBytes = 0;
|
|
5964
|
-
}
|
|
5965
5808
|
static setMaxFontCacheMemoryMB(limitMB) {
|
|
5966
5809
|
Text.maxFontCacheMemoryBytes =
|
|
5967
|
-
limitMB === Infinity
|
|
5810
|
+
limitMB === Infinity
|
|
5811
|
+
? Infinity
|
|
5812
|
+
: Math.max(1, Math.floor(limitMB)) * 1024 * 1024;
|
|
5968
5813
|
Text.enforceFontCacheMemoryLimit();
|
|
5969
5814
|
}
|
|
5970
5815
|
getLoadedFont() {
|
|
@@ -5976,11 +5821,11 @@ class Text {
|
|
|
5976
5821
|
}
|
|
5977
5822
|
return TextMeasurer.measureTextWidth(this.loadedFont, text, letterSpacing);
|
|
5978
5823
|
}
|
|
5979
|
-
|
|
5824
|
+
getCacheSize() {
|
|
5980
5825
|
if (this.geometryBuilder) {
|
|
5981
|
-
return this.geometryBuilder.getCacheStats();
|
|
5826
|
+
return this.geometryBuilder.getCacheStats().size;
|
|
5982
5827
|
}
|
|
5983
|
-
return
|
|
5828
|
+
return 0;
|
|
5984
5829
|
}
|
|
5985
5830
|
clearCache() {
|
|
5986
5831
|
if (this.geometryBuilder) {
|
|
@@ -5991,25 +5836,45 @@ class Text {
|
|
|
5991
5836
|
const glyphCenters = new Float32Array(vertexCount * 3);
|
|
5992
5837
|
const glyphIndices = new Float32Array(vertexCount);
|
|
5993
5838
|
const glyphLineIndices = new Float32Array(vertexCount);
|
|
5994
|
-
|
|
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];
|
|
5995
5853
|
const centerX = (glyph.bounds.min.x + glyph.bounds.max.x) / 2;
|
|
5996
5854
|
const centerY = (glyph.bounds.min.y + glyph.bounds.max.y) / 2;
|
|
5997
5855
|
const centerZ = (glyph.bounds.min.z + glyph.bounds.max.z) / 2;
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
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;
|
|
6007
5870
|
}
|
|
6008
|
-
}
|
|
5871
|
+
}
|
|
6009
5872
|
return {
|
|
6010
5873
|
glyphCenter: glyphCenters,
|
|
6011
5874
|
glyphIndex: glyphIndices,
|
|
6012
|
-
glyphLineIndex: glyphLineIndices
|
|
5875
|
+
glyphLineIndex: glyphLineIndices,
|
|
5876
|
+
glyphProgress,
|
|
5877
|
+
glyphBaselineY
|
|
6013
5878
|
};
|
|
6014
5879
|
}
|
|
6015
5880
|
resetHelpers() {
|