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.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
|
|
@@ -78,7 +78,9 @@ class PerformanceLogger {
|
|
|
78
78
|
// Find the metric in reverse order (most recent first)
|
|
79
79
|
for (let i = this.metrics.length - 1; i >= 0; i--) {
|
|
80
80
|
const metric = this.metrics[i];
|
|
81
|
-
if (metric.name === name &&
|
|
81
|
+
if (metric.name === name &&
|
|
82
|
+
metric.startTime === startTime &&
|
|
83
|
+
!metric.endTime) {
|
|
82
84
|
metric.endTime = endTime;
|
|
83
85
|
metric.duration = duration;
|
|
84
86
|
break;
|
|
@@ -466,7 +468,9 @@ class LineBreak {
|
|
|
466
468
|
const char = chars[i];
|
|
467
469
|
const nextChar = i < chars.length - 1 ? chars[i + 1] : null;
|
|
468
470
|
if (/\s/.test(char)) {
|
|
469
|
-
const width = widths
|
|
471
|
+
const width = widths
|
|
472
|
+
? (widths[i] ?? measureText(char))
|
|
473
|
+
: measureText(char);
|
|
470
474
|
items.push({
|
|
471
475
|
type: ItemType.GLUE,
|
|
472
476
|
width,
|
|
@@ -839,7 +843,9 @@ class LineBreak {
|
|
|
839
843
|
if (breaks.length === 0) {
|
|
840
844
|
// For first emergency attempt, use initialEmergencyStretch
|
|
841
845
|
// For subsequent iterations (short line detection), progressively increase
|
|
842
|
-
currentEmergencyStretch =
|
|
846
|
+
currentEmergencyStretch =
|
|
847
|
+
initialEmergencyStretch +
|
|
848
|
+
iteration * width * SHORT_LINE_EMERGENCY_STRETCH_INCREMENT;
|
|
843
849
|
breaks = LineBreak.findBreakpoints(currentItems, width, tolerance, looseness, true, currentEmergencyStretch, context);
|
|
844
850
|
}
|
|
845
851
|
// Last resort: allow higher badness (but not infinite)
|
|
@@ -1720,12 +1726,12 @@ class FontMetadataExtractor {
|
|
|
1720
1726
|
try {
|
|
1721
1727
|
if (gsubTableOffset) {
|
|
1722
1728
|
const gsubData = this.extractFeatureDataFromTable(view, gsubTableOffset, nameTableOffset);
|
|
1723
|
-
gsubData.features.forEach(f => features.add(f));
|
|
1729
|
+
gsubData.features.forEach((f) => features.add(f));
|
|
1724
1730
|
Object.assign(featureNames, gsubData.names);
|
|
1725
1731
|
}
|
|
1726
1732
|
if (gposTableOffset) {
|
|
1727
1733
|
const gposData = this.extractFeatureDataFromTable(view, gposTableOffset, nameTableOffset);
|
|
1728
|
-
gposData.features.forEach(f => features.add(f));
|
|
1734
|
+
gposData.features.forEach((f) => features.add(f));
|
|
1729
1735
|
Object.assign(featureNames, gposData.names);
|
|
1730
1736
|
}
|
|
1731
1737
|
}
|
|
@@ -2373,231 +2379,50 @@ class Vec3 {
|
|
|
2373
2379
|
}
|
|
2374
2380
|
}
|
|
2375
2381
|
|
|
2376
|
-
//
|
|
2377
|
-
class
|
|
2378
|
-
constructor(
|
|
2382
|
+
// Map-based cache with no eviction policy
|
|
2383
|
+
class Cache {
|
|
2384
|
+
constructor() {
|
|
2379
2385
|
this.cache = new Map();
|
|
2380
|
-
this.head = null;
|
|
2381
|
-
this.tail = null;
|
|
2382
|
-
this.stats = {
|
|
2383
|
-
hits: 0,
|
|
2384
|
-
misses: 0,
|
|
2385
|
-
evictions: 0,
|
|
2386
|
-
size: 0,
|
|
2387
|
-
memoryUsage: 0
|
|
2388
|
-
};
|
|
2389
|
-
this.options = {
|
|
2390
|
-
maxEntries: options.maxEntries ?? Infinity,
|
|
2391
|
-
maxMemoryBytes: options.maxMemoryBytes ?? Infinity,
|
|
2392
|
-
calculateSize: options.calculateSize ?? (() => 0),
|
|
2393
|
-
onEvict: options.onEvict
|
|
2394
|
-
};
|
|
2395
2386
|
}
|
|
2396
2387
|
get(key) {
|
|
2397
|
-
|
|
2398
|
-
if (node) {
|
|
2399
|
-
this.stats.hits++;
|
|
2400
|
-
this.moveToHead(node);
|
|
2401
|
-
return node.value;
|
|
2402
|
-
}
|
|
2403
|
-
else {
|
|
2404
|
-
this.stats.misses++;
|
|
2405
|
-
return undefined;
|
|
2406
|
-
}
|
|
2388
|
+
return this.cache.get(key);
|
|
2407
2389
|
}
|
|
2408
2390
|
has(key) {
|
|
2409
2391
|
return this.cache.has(key);
|
|
2410
2392
|
}
|
|
2411
2393
|
set(key, value) {
|
|
2412
|
-
|
|
2413
|
-
const existingNode = this.cache.get(key);
|
|
2414
|
-
if (existingNode) {
|
|
2415
|
-
const oldSize = this.options.calculateSize(existingNode.value);
|
|
2416
|
-
const newSize = this.options.calculateSize(value);
|
|
2417
|
-
this.stats.memoryUsage = this.stats.memoryUsage - oldSize + newSize;
|
|
2418
|
-
existingNode.value = value;
|
|
2419
|
-
this.moveToHead(existingNode);
|
|
2420
|
-
return;
|
|
2421
|
-
}
|
|
2422
|
-
const size = this.options.calculateSize(value);
|
|
2423
|
-
// Evict entries if we exceed limits
|
|
2424
|
-
this.evictIfNeeded(size);
|
|
2425
|
-
// Create new node
|
|
2426
|
-
const node = {
|
|
2427
|
-
key,
|
|
2428
|
-
value,
|
|
2429
|
-
prev: null,
|
|
2430
|
-
next: null
|
|
2431
|
-
};
|
|
2432
|
-
this.cache.set(key, node);
|
|
2433
|
-
this.addToHead(node);
|
|
2434
|
-
this.stats.size = this.cache.size;
|
|
2435
|
-
this.stats.memoryUsage += size;
|
|
2394
|
+
this.cache.set(key, value);
|
|
2436
2395
|
}
|
|
2437
2396
|
delete(key) {
|
|
2438
|
-
|
|
2439
|
-
if (!node)
|
|
2440
|
-
return false;
|
|
2441
|
-
const size = this.options.calculateSize(node.value);
|
|
2442
|
-
this.removeNode(node);
|
|
2443
|
-
this.cache.delete(key);
|
|
2444
|
-
this.stats.size = this.cache.size;
|
|
2445
|
-
this.stats.memoryUsage -= size;
|
|
2446
|
-
if (this.options.onEvict) {
|
|
2447
|
-
this.options.onEvict(key, node.value);
|
|
2448
|
-
}
|
|
2449
|
-
return true;
|
|
2397
|
+
return this.cache.delete(key);
|
|
2450
2398
|
}
|
|
2451
2399
|
clear() {
|
|
2452
|
-
if (this.options.onEvict) {
|
|
2453
|
-
for (const [key, node] of this.cache) {
|
|
2454
|
-
this.options.onEvict(key, node.value);
|
|
2455
|
-
}
|
|
2456
|
-
}
|
|
2457
2400
|
this.cache.clear();
|
|
2458
|
-
this.head = null;
|
|
2459
|
-
this.tail = null;
|
|
2460
|
-
this.stats = {
|
|
2461
|
-
hits: 0,
|
|
2462
|
-
misses: 0,
|
|
2463
|
-
evictions: 0,
|
|
2464
|
-
size: 0,
|
|
2465
|
-
memoryUsage: 0
|
|
2466
|
-
};
|
|
2467
|
-
}
|
|
2468
|
-
getStats() {
|
|
2469
|
-
const total = this.stats.hits + this.stats.misses;
|
|
2470
|
-
const hitRate = total > 0 ? (this.stats.hits / total) * 100 : 0;
|
|
2471
|
-
const memoryUsageMB = this.stats.memoryUsage / (1024 * 1024);
|
|
2472
|
-
return {
|
|
2473
|
-
...this.stats,
|
|
2474
|
-
hitRate,
|
|
2475
|
-
memoryUsageMB
|
|
2476
|
-
};
|
|
2477
|
-
}
|
|
2478
|
-
keys() {
|
|
2479
|
-
const keys = [];
|
|
2480
|
-
let current = this.head;
|
|
2481
|
-
while (current) {
|
|
2482
|
-
keys.push(current.key);
|
|
2483
|
-
current = current.next;
|
|
2484
|
-
}
|
|
2485
|
-
return keys;
|
|
2486
2401
|
}
|
|
2487
2402
|
get size() {
|
|
2488
2403
|
return this.cache.size;
|
|
2489
2404
|
}
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
while (this.cache.size >= this.options.maxEntries && this.tail) {
|
|
2493
|
-
this.evictTail();
|
|
2494
|
-
}
|
|
2495
|
-
// Evict by memory usage
|
|
2496
|
-
if (this.options.maxMemoryBytes < Infinity) {
|
|
2497
|
-
while (this.tail &&
|
|
2498
|
-
this.stats.memoryUsage + requiredSize > this.options.maxMemoryBytes) {
|
|
2499
|
-
this.evictTail();
|
|
2500
|
-
}
|
|
2501
|
-
}
|
|
2502
|
-
}
|
|
2503
|
-
evictTail() {
|
|
2504
|
-
if (!this.tail)
|
|
2505
|
-
return;
|
|
2506
|
-
const nodeToRemove = this.tail;
|
|
2507
|
-
const size = this.options.calculateSize(nodeToRemove.value);
|
|
2508
|
-
this.removeTail();
|
|
2509
|
-
this.cache.delete(nodeToRemove.key);
|
|
2510
|
-
this.stats.size = this.cache.size;
|
|
2511
|
-
this.stats.memoryUsage -= size;
|
|
2512
|
-
this.stats.evictions++;
|
|
2513
|
-
if (this.options.onEvict) {
|
|
2514
|
-
this.options.onEvict(nodeToRemove.key, nodeToRemove.value);
|
|
2515
|
-
}
|
|
2516
|
-
}
|
|
2517
|
-
addToHead(node) {
|
|
2518
|
-
node.prev = null;
|
|
2519
|
-
node.next = null;
|
|
2520
|
-
if (!this.head) {
|
|
2521
|
-
this.head = this.tail = node;
|
|
2522
|
-
}
|
|
2523
|
-
else {
|
|
2524
|
-
node.next = this.head;
|
|
2525
|
-
this.head.prev = node;
|
|
2526
|
-
this.head = node;
|
|
2527
|
-
}
|
|
2528
|
-
}
|
|
2529
|
-
removeNode(node) {
|
|
2530
|
-
if (node.prev) {
|
|
2531
|
-
node.prev.next = node.next;
|
|
2532
|
-
}
|
|
2533
|
-
else {
|
|
2534
|
-
this.head = node.next;
|
|
2535
|
-
}
|
|
2536
|
-
if (node.next) {
|
|
2537
|
-
node.next.prev = node.prev;
|
|
2538
|
-
}
|
|
2539
|
-
else {
|
|
2540
|
-
this.tail = node.prev;
|
|
2541
|
-
}
|
|
2542
|
-
}
|
|
2543
|
-
removeTail() {
|
|
2544
|
-
if (this.tail) {
|
|
2545
|
-
this.removeNode(this.tail);
|
|
2546
|
-
}
|
|
2405
|
+
keys() {
|
|
2406
|
+
return Array.from(this.cache.keys());
|
|
2547
2407
|
}
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
this.addToHead(node);
|
|
2408
|
+
getStats() {
|
|
2409
|
+
return {
|
|
2410
|
+
size: this.cache.size
|
|
2411
|
+
};
|
|
2553
2412
|
}
|
|
2554
2413
|
}
|
|
2555
2414
|
|
|
2556
|
-
const DEFAULT_CACHE_SIZE_MB = 250;
|
|
2557
2415
|
function getGlyphCacheKey(fontId, glyphId, depth, removeOverlaps) {
|
|
2558
2416
|
const roundedDepth = Math.round(depth * 1000) / 1000;
|
|
2559
2417
|
return `${fontId}_${glyphId}_${roundedDepth}_${removeOverlaps}`;
|
|
2560
2418
|
}
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
size += glyph.normals.length * 4;
|
|
2565
|
-
size += glyph.indices.length * glyph.indices.BYTES_PER_ELEMENT;
|
|
2566
|
-
size += 24; // 2 Vec3s
|
|
2567
|
-
size += 256; // Object overhead
|
|
2568
|
-
return size;
|
|
2569
|
-
}
|
|
2570
|
-
const globalGlyphCache = new LRUCache({
|
|
2571
|
-
maxEntries: Infinity,
|
|
2572
|
-
maxMemoryBytes: DEFAULT_CACHE_SIZE_MB * 1024 * 1024,
|
|
2573
|
-
calculateSize: calculateGlyphMemoryUsage
|
|
2574
|
-
});
|
|
2575
|
-
function createGlyphCache(maxCacheSizeMB = DEFAULT_CACHE_SIZE_MB) {
|
|
2576
|
-
return new LRUCache({
|
|
2577
|
-
maxEntries: Infinity,
|
|
2578
|
-
maxMemoryBytes: maxCacheSizeMB * 1024 * 1024,
|
|
2579
|
-
calculateSize: calculateGlyphMemoryUsage
|
|
2580
|
-
});
|
|
2419
|
+
const globalGlyphCache = new Cache();
|
|
2420
|
+
function createGlyphCache() {
|
|
2421
|
+
return new Cache();
|
|
2581
2422
|
}
|
|
2582
|
-
|
|
2583
|
-
const
|
|
2584
|
-
|
|
2585
|
-
calculateSize: (contours) => {
|
|
2586
|
-
let size = 0;
|
|
2587
|
-
for (const path of contours.paths) {
|
|
2588
|
-
size += path.points.length * 16; // Vec2 = 2 floats * 8 bytes
|
|
2589
|
-
}
|
|
2590
|
-
return size + 64; // bounds overhead
|
|
2591
|
-
}
|
|
2592
|
-
});
|
|
2593
|
-
const globalWordCache = new LRUCache({
|
|
2594
|
-
maxEntries: 1000,
|
|
2595
|
-
calculateSize: calculateGlyphMemoryUsage
|
|
2596
|
-
});
|
|
2597
|
-
const globalClusteringCache = new LRUCache({
|
|
2598
|
-
maxEntries: 2000,
|
|
2599
|
-
calculateSize: () => 1
|
|
2600
|
-
});
|
|
2423
|
+
const globalContourCache = new Cache();
|
|
2424
|
+
const globalWordCache = new Cache();
|
|
2425
|
+
const globalClusteringCache = new Cache();
|
|
2601
2426
|
|
|
2602
2427
|
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
|
|
2603
2428
|
|
|
@@ -2726,7 +2551,7 @@ class Tessellator {
|
|
|
2726
2551
|
let extrusionContours = needsExtrusionContours
|
|
2727
2552
|
? needsWindingReversal
|
|
2728
2553
|
? tessContours
|
|
2729
|
-
: originalContours ?? this.pathsToContours(paths)
|
|
2554
|
+
: (originalContours ?? this.pathsToContours(paths))
|
|
2730
2555
|
: [];
|
|
2731
2556
|
if (removeOverlaps) {
|
|
2732
2557
|
logger.log('Two-pass: boundary extraction then triangulation');
|
|
@@ -2759,7 +2584,10 @@ class Tessellator {
|
|
|
2759
2584
|
? 'libtess returned empty result from triangulation pass'
|
|
2760
2585
|
: 'libtess returned empty result from single-pass triangulation';
|
|
2761
2586
|
logger.warn(warning);
|
|
2762
|
-
return {
|
|
2587
|
+
return {
|
|
2588
|
+
triangles: { vertices: [], indices: [] },
|
|
2589
|
+
contours: extrusionContours
|
|
2590
|
+
};
|
|
2763
2591
|
}
|
|
2764
2592
|
return {
|
|
2765
2593
|
triangles: {
|
|
@@ -2929,47 +2757,79 @@ class Tessellator {
|
|
|
2929
2757
|
|
|
2930
2758
|
class Extruder {
|
|
2931
2759
|
constructor() { }
|
|
2932
|
-
packEdge(a, b) {
|
|
2933
|
-
const lo = a < b ? a : b;
|
|
2934
|
-
const hi = a < b ? b : a;
|
|
2935
|
-
return lo * 0x100000000 + hi;
|
|
2936
|
-
}
|
|
2937
2760
|
extrude(geometry, depth = 0, unitsPerEm) {
|
|
2938
2761
|
const points = geometry.triangles.vertices;
|
|
2939
2762
|
const triangleIndices = geometry.triangles.indices;
|
|
2940
2763
|
const numPoints = points.length / 2;
|
|
2941
|
-
//
|
|
2764
|
+
// Boundary edges are those that appear in exactly one triangle
|
|
2942
2765
|
let boundaryEdges = [];
|
|
2943
2766
|
if (depth !== 0) {
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
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) {
|
|
2947
2772
|
const a = triangleIndices[i];
|
|
2948
2773
|
const b = triangleIndices[i + 1];
|
|
2949
2774
|
const c = triangleIndices[i + 2];
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
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
|
+
}
|
|
2965
2827
|
}
|
|
2966
2828
|
boundaryEdges = [];
|
|
2967
|
-
for (const [
|
|
2968
|
-
if (count
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
if (edge)
|
|
2972
|
-
boundaryEdges.push(edge);
|
|
2829
|
+
for (const [v0, v1, count] of edgeMap.values()) {
|
|
2830
|
+
if (count === 1) {
|
|
2831
|
+
boundaryEdges.push([v0, v1]);
|
|
2832
|
+
}
|
|
2973
2833
|
}
|
|
2974
2834
|
}
|
|
2975
2835
|
const sideEdgeCount = depth === 0 ? 0 : boundaryEdges.length;
|
|
@@ -2983,7 +2843,6 @@ class Extruder {
|
|
|
2983
2843
|
: triangleIndices.length * 2 + sideEdgeCount * 6;
|
|
2984
2844
|
const indices = new Uint32Array(indexCount);
|
|
2985
2845
|
if (depth === 0) {
|
|
2986
|
-
// Single-sided flat geometry at z=0
|
|
2987
2846
|
let vPos = 0;
|
|
2988
2847
|
for (let i = 0; i < points.length; i += 2) {
|
|
2989
2848
|
vertices[vPos] = points[i];
|
|
@@ -2994,16 +2853,11 @@ class Extruder {
|
|
|
2994
2853
|
normals[vPos + 2] = 1;
|
|
2995
2854
|
vPos += 3;
|
|
2996
2855
|
}
|
|
2997
|
-
|
|
2998
|
-
for (let i = 0; i < triangleIndices.length; i++) {
|
|
2999
|
-
indices[i] = triangleIndices[i];
|
|
3000
|
-
}
|
|
2856
|
+
indices.set(triangleIndices);
|
|
3001
2857
|
return { vertices, normals, indices };
|
|
3002
2858
|
}
|
|
3003
|
-
// Extruded geometry: front at z=0, back at z=depth
|
|
3004
2859
|
const minBackOffset = unitsPerEm * 0.000025;
|
|
3005
2860
|
const backZ = depth <= minBackOffset ? minBackOffset : depth;
|
|
3006
|
-
// Generate both caps in one pass
|
|
3007
2861
|
for (let p = 0, vi = 0; p < points.length; p += 2, vi++) {
|
|
3008
2862
|
const x = points[p];
|
|
3009
2863
|
const y = points[p + 1];
|
|
@@ -3024,41 +2878,39 @@ class Extruder {
|
|
|
3024
2878
|
normals[baseD + 1] = 0;
|
|
3025
2879
|
normals[baseD + 2] = 1;
|
|
3026
2880
|
}
|
|
3027
|
-
//
|
|
3028
|
-
|
|
3029
|
-
for (let i = 0; i <
|
|
3030
|
-
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];
|
|
3031
2885
|
}
|
|
3032
|
-
//
|
|
3033
|
-
for (let i = 0; i <
|
|
3034
|
-
indices[
|
|
2886
|
+
// Back cap faces +Z, use original winding
|
|
2887
|
+
for (let i = 0; i < triLen; i++) {
|
|
2888
|
+
indices[triLen + i] = triangleIndices[i] + numPoints;
|
|
3035
2889
|
}
|
|
3036
|
-
// Side walls
|
|
3037
2890
|
let nextVertex = numPoints * 2;
|
|
3038
|
-
let idxPos =
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
const
|
|
3042
|
-
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;
|
|
3043
2899
|
const p0x = points[u2];
|
|
3044
2900
|
const p0y = points[u2 + 1];
|
|
3045
2901
|
const p1x = points[v2];
|
|
3046
2902
|
const p1y = points[v2 + 1];
|
|
3047
|
-
// Perpendicular normal for this wall segment
|
|
3048
|
-
// Uses the edge direction from the cap triangulation so winding does not depend on contour direction
|
|
3049
2903
|
const ex = p1x - p0x;
|
|
3050
2904
|
const ey = p1y - p0y;
|
|
3051
2905
|
const lenSq = ex * ex + ey * ey;
|
|
3052
2906
|
let nx = 0;
|
|
3053
2907
|
let ny = 0;
|
|
3054
|
-
if (lenSq >
|
|
2908
|
+
if (lenSq > 1e-10) {
|
|
3055
2909
|
const invLen = 1 / Math.sqrt(lenSq);
|
|
3056
2910
|
nx = ey * invLen;
|
|
3057
2911
|
ny = -ex * invLen;
|
|
3058
2912
|
}
|
|
3059
|
-
const
|
|
3060
|
-
const base = baseVertex * 3;
|
|
3061
|
-
// Wall quad: front edge at z=0, back edge at z=depth
|
|
2913
|
+
const base = nextVertex * 3;
|
|
3062
2914
|
vertices[base] = p0x;
|
|
3063
2915
|
vertices[base + 1] = p0y;
|
|
3064
2916
|
vertices[base + 2] = 0;
|
|
@@ -3071,7 +2923,6 @@ class Extruder {
|
|
|
3071
2923
|
vertices[base + 9] = p1x;
|
|
3072
2924
|
vertices[base + 10] = p1y;
|
|
3073
2925
|
vertices[base + 11] = backZ;
|
|
3074
|
-
// Wall normals point perpendicular to edge
|
|
3075
2926
|
normals[base] = nx;
|
|
3076
2927
|
normals[base + 1] = ny;
|
|
3077
2928
|
normals[base + 2] = 0;
|
|
@@ -3084,13 +2935,14 @@ class Extruder {
|
|
|
3084
2935
|
normals[base + 9] = nx;
|
|
3085
2936
|
normals[base + 10] = ny;
|
|
3086
2937
|
normals[base + 11] = 0;
|
|
3087
|
-
|
|
3088
|
-
indices[idxPos
|
|
3089
|
-
indices[idxPos
|
|
3090
|
-
indices[idxPos
|
|
3091
|
-
indices[idxPos
|
|
3092
|
-
indices[idxPos
|
|
3093
|
-
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;
|
|
3094
2946
|
nextVertex += 4;
|
|
3095
2947
|
}
|
|
3096
2948
|
return { vertices, normals, indices };
|
|
@@ -3416,9 +3268,7 @@ class PathOptimizer {
|
|
|
3416
3268
|
const v1LenSq = v1x * v1x + v1y * v1y;
|
|
3417
3269
|
const v2LenSq = v2x * v2x + v2y * v2y;
|
|
3418
3270
|
const minLenSq = this.config.minSegmentLength * this.config.minSegmentLength;
|
|
3419
|
-
if (angle > threshold ||
|
|
3420
|
-
v1LenSq < minLenSq ||
|
|
3421
|
-
v2LenSq < minLenSq) {
|
|
3271
|
+
if (angle > threshold || v1LenSq < minLenSq || v2LenSq < minLenSq) {
|
|
3422
3272
|
result.push(current);
|
|
3423
3273
|
}
|
|
3424
3274
|
else {
|
|
@@ -4109,7 +3959,7 @@ class GlyphGeometryBuilder {
|
|
|
4109
3959
|
].join('|');
|
|
4110
3960
|
}
|
|
4111
3961
|
// Build instanced geometry from glyph contours
|
|
4112
|
-
buildInstancedGeometry(clustersByLine, depth, removeOverlaps, isCFF, separateGlyphs = false, coloredTextIndices) {
|
|
3962
|
+
buildInstancedGeometry(clustersByLine, depth, removeOverlaps, isCFF, scale, separateGlyphs = false, coloredTextIndices) {
|
|
4113
3963
|
if (isLogEnabled) {
|
|
4114
3964
|
let wordCount = 0;
|
|
4115
3965
|
for (let i = 0; i < clustersByLine.length; i++) {
|
|
@@ -4180,7 +4030,7 @@ class GlyphGeometryBuilder {
|
|
|
4180
4030
|
const relativePositions = cluster.glyphs.map((g) => new Vec3(g.x ?? 0, g.y ?? 0, 0));
|
|
4181
4031
|
boundaryGroups = this.clusterer.cluster(clusterGlyphContours, relativePositions);
|
|
4182
4032
|
this.clusteringCache.set(cacheKey, {
|
|
4183
|
-
glyphIds: cluster.glyphs.map(g => g.g),
|
|
4033
|
+
glyphIds: cluster.glyphs.map((g) => g.g),
|
|
4184
4034
|
groups: boundaryGroups
|
|
4185
4035
|
});
|
|
4186
4036
|
}
|
|
@@ -4287,9 +4137,9 @@ class GlyphGeometryBuilder {
|
|
|
4287
4137
|
const py = task.py;
|
|
4288
4138
|
const pz = task.pz;
|
|
4289
4139
|
for (let j = 0; j < v.length; j += 3) {
|
|
4290
|
-
vertexArray[vertexPos++] = v[j] + px;
|
|
4291
|
-
vertexArray[vertexPos++] = v[j + 1] + py;
|
|
4292
|
-
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;
|
|
4293
4143
|
}
|
|
4294
4144
|
normalArray.set(n, normalPos);
|
|
4295
4145
|
normalPos += n.length;
|
|
@@ -4299,6 +4149,20 @@ class GlyphGeometryBuilder {
|
|
|
4299
4149
|
}
|
|
4300
4150
|
}
|
|
4301
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
|
+
}
|
|
4302
4166
|
return {
|
|
4303
4167
|
vertices: vertexArray,
|
|
4304
4168
|
normals: normalArray,
|
|
@@ -4310,7 +4174,6 @@ class GlyphGeometryBuilder {
|
|
|
4310
4174
|
getClusterKey(glyphs, depth, removeOverlaps) {
|
|
4311
4175
|
if (glyphs.length === 0)
|
|
4312
4176
|
return '';
|
|
4313
|
-
// Normalize positions relative to the first glyph in the cluster
|
|
4314
4177
|
const refX = glyphs[0].x ?? 0;
|
|
4315
4178
|
const refY = glyphs[0].y ?? 0;
|
|
4316
4179
|
const parts = glyphs.map((g) => {
|
|
@@ -4570,7 +4433,8 @@ class TextShaper {
|
|
|
4570
4433
|
if (LineBreak.isCJOpeningPunctuation(currentChar)) {
|
|
4571
4434
|
shouldApply = false;
|
|
4572
4435
|
}
|
|
4573
|
-
if (LineBreak.isCJPunctuation(currentChar) &&
|
|
4436
|
+
if (LineBreak.isCJPunctuation(currentChar) &&
|
|
4437
|
+
LineBreak.isCJPunctuation(nextChar)) {
|
|
4574
4438
|
shouldApply = false;
|
|
4575
4439
|
}
|
|
4576
4440
|
if (shouldApply) {
|
|
@@ -5364,6 +5228,7 @@ class TextRangeQuery {
|
|
|
5364
5228
|
}
|
|
5365
5229
|
|
|
5366
5230
|
const DEFAULT_MAX_TEXT_LENGTH = 100000;
|
|
5231
|
+
const DEFAULT_FONT_SIZE = 72;
|
|
5367
5232
|
class Text {
|
|
5368
5233
|
static { this.patternCache = new Map(); }
|
|
5369
5234
|
static { this.hbInitPromise = null; }
|
|
@@ -5374,7 +5239,7 @@ class Text {
|
|
|
5374
5239
|
// Stringify with sorted keys for cache stability
|
|
5375
5240
|
static stableStringify(obj) {
|
|
5376
5241
|
const keys = Object.keys(obj).sort();
|
|
5377
|
-
const pairs = keys.map(k => `${k}:${obj[k]}`);
|
|
5242
|
+
const pairs = keys.map((k) => `${k}:${obj[k]}`);
|
|
5378
5243
|
return pairs.join(',');
|
|
5379
5244
|
}
|
|
5380
5245
|
constructor() {
|
|
@@ -5437,7 +5302,7 @@ class Text {
|
|
|
5437
5302
|
return {
|
|
5438
5303
|
...newResult,
|
|
5439
5304
|
getLoadedFont: () => text.getLoadedFont(),
|
|
5440
|
-
|
|
5305
|
+
getCacheSize: () => text.getCacheSize(),
|
|
5441
5306
|
clearCache: () => text.clearCache(),
|
|
5442
5307
|
measureTextWidth: (textString, letterSpacing) => text.measureTextWidth(textString, letterSpacing),
|
|
5443
5308
|
update
|
|
@@ -5446,7 +5311,7 @@ class Text {
|
|
|
5446
5311
|
return {
|
|
5447
5312
|
...result,
|
|
5448
5313
|
getLoadedFont: () => text.getLoadedFont(),
|
|
5449
|
-
|
|
5314
|
+
getCacheSize: () => text.getCacheSize(),
|
|
5450
5315
|
clearCache: () => text.clearCache(),
|
|
5451
5316
|
measureTextWidth: (textString, letterSpacing) => text.measureTextWidth(textString, letterSpacing),
|
|
5452
5317
|
update
|
|
@@ -5578,7 +5443,7 @@ class Text {
|
|
|
5578
5443
|
async createGeometry(options) {
|
|
5579
5444
|
perfLogger.start('Text.createGeometry', {
|
|
5580
5445
|
textLength: options.text.length,
|
|
5581
|
-
size: options.size ||
|
|
5446
|
+
size: options.size || DEFAULT_FONT_SIZE,
|
|
5582
5447
|
hasLayout: !!options.layout,
|
|
5583
5448
|
mode: 'cached'
|
|
5584
5449
|
});
|
|
@@ -5592,7 +5457,7 @@ class Text {
|
|
|
5592
5457
|
this.updateFontVariations(options);
|
|
5593
5458
|
if (!this.geometryBuilder) {
|
|
5594
5459
|
const cache = options.maxCacheSizeMB
|
|
5595
|
-
? createGlyphCache(
|
|
5460
|
+
? createGlyphCache()
|
|
5596
5461
|
: globalGlyphCache;
|
|
5597
5462
|
this.geometryBuilder = new GlyphGeometryBuilder(cache, this.loadedFont);
|
|
5598
5463
|
this.geometryBuilder.setFontId(this.currentFontId);
|
|
@@ -5612,7 +5477,9 @@ class Text {
|
|
|
5612
5477
|
// to selectively use glyph-level caching (separate vertices) only for clusters containing
|
|
5613
5478
|
// colored text, while non-colored clusters can still use fast cluster-level merging
|
|
5614
5479
|
let coloredTextIndices;
|
|
5615
|
-
if (options.color &&
|
|
5480
|
+
if (options.color &&
|
|
5481
|
+
typeof options.color === 'object' &&
|
|
5482
|
+
!Array.isArray(options.color)) {
|
|
5616
5483
|
if (options.color.byText || options.color.byCharRange) {
|
|
5617
5484
|
// Build the set manually since glyphs don't exist yet
|
|
5618
5485
|
coloredTextIndices = new Set();
|
|
@@ -5636,10 +5503,9 @@ class Text {
|
|
|
5636
5503
|
}
|
|
5637
5504
|
}
|
|
5638
5505
|
}
|
|
5639
|
-
const shapedResult = this.geometryBuilder.buildInstancedGeometry(clustersByLine, layoutData.depth, shouldRemoveOverlaps, this.loadedFont.metrics.isCFF, options.
|
|
5640
|
-
const
|
|
5641
|
-
|
|
5642
|
-
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) {
|
|
5643
5509
|
const glyphAttrs = this.createGlyphAttributes(result.vertices.length / 3, result.glyphs);
|
|
5644
5510
|
result.glyphAttributes = glyphAttrs;
|
|
5645
5511
|
}
|
|
@@ -5706,18 +5572,20 @@ class Text {
|
|
|
5706
5572
|
if (!this.loadedFont) {
|
|
5707
5573
|
throw new Error('Font not loaded. Use Text.create() with a font option');
|
|
5708
5574
|
}
|
|
5709
|
-
const { text, size =
|
|
5575
|
+
const { text, size = DEFAULT_FONT_SIZE, depth = 0, lineHeight = 1.0, letterSpacing = 0, layout = {} } = options;
|
|
5710
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;
|
|
5711
5578
|
let widthInFontUnits;
|
|
5712
5579
|
if (width !== undefined) {
|
|
5713
|
-
widthInFontUnits = width *
|
|
5580
|
+
widthInFontUnits = width * fontUnitsPerPixel;
|
|
5714
5581
|
}
|
|
5715
5582
|
// Keep depth behavior consistent with Extruder: extremely small non-zero depths
|
|
5716
|
-
// are clamped to a minimum back offset
|
|
5717
|
-
const
|
|
5718
|
-
const rawDepthInFontUnits = depth * depthScale;
|
|
5583
|
+
// are clamped to a minimum back offset to prevent Z fighting
|
|
5584
|
+
const rawDepthInFontUnits = depth * fontUnitsPerPixel;
|
|
5719
5585
|
const minExtrudeDepth = this.loadedFont.upem * 0.000025;
|
|
5720
|
-
const depthInFontUnits = rawDepthInFontUnits <= 0
|
|
5586
|
+
const depthInFontUnits = rawDepthInFontUnits <= 0
|
|
5587
|
+
? 0
|
|
5588
|
+
: Math.max(rawDepthInFontUnits, minExtrudeDepth);
|
|
5721
5589
|
if (!this.textLayout) {
|
|
5722
5590
|
this.textLayout = new TextLayout(this.loadedFont);
|
|
5723
5591
|
}
|
|
@@ -5756,7 +5624,8 @@ class Text {
|
|
|
5756
5624
|
align,
|
|
5757
5625
|
direction,
|
|
5758
5626
|
depth: depthInFontUnits,
|
|
5759
|
-
size
|
|
5627
|
+
size,
|
|
5628
|
+
pixelsPerFontUnit: 1 / fontUnitsPerPixel
|
|
5760
5629
|
};
|
|
5761
5630
|
}
|
|
5762
5631
|
applyColorSystem(vertices, glyphInfoArray, color, originalText) {
|
|
@@ -5852,8 +5721,8 @@ class Text {
|
|
|
5852
5721
|
}
|
|
5853
5722
|
return { colors, coloredRanges };
|
|
5854
5723
|
}
|
|
5855
|
-
finalizeGeometry(vertices, normals, indices, glyphInfoArray, planeBounds, options,
|
|
5856
|
-
const { layout = {}
|
|
5724
|
+
finalizeGeometry(vertices, normals, indices, glyphInfoArray, planeBounds, options, originalText) {
|
|
5725
|
+
const { layout = {} } = options;
|
|
5857
5726
|
const { width, align = layout.direction === 'rtl' ? 'right' : 'left' } = layout;
|
|
5858
5727
|
if (!this.textLayout) {
|
|
5859
5728
|
this.textLayout = new TextLayout(this.loadedFont);
|
|
@@ -5866,35 +5735,14 @@ class Text {
|
|
|
5866
5735
|
const offset = alignmentResult.offset;
|
|
5867
5736
|
planeBounds.min.x = alignmentResult.adjustedBounds.min.x;
|
|
5868
5737
|
planeBounds.max.x = alignmentResult.adjustedBounds.max.x;
|
|
5869
|
-
|
|
5870
|
-
const offsetScaled = offset * finalScale;
|
|
5871
|
-
// Scale vertices only (normals are unit vectors, don't scale)
|
|
5872
|
-
if (offsetScaled === 0) {
|
|
5873
|
-
for (let i = 0; i < vertices.length; i++) {
|
|
5874
|
-
vertices[i] *= finalScale;
|
|
5875
|
-
}
|
|
5876
|
-
}
|
|
5877
|
-
else {
|
|
5738
|
+
if (offset !== 0) {
|
|
5878
5739
|
for (let i = 0; i < vertices.length; i += 3) {
|
|
5879
|
-
vertices[i]
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
planeBounds.min.y *= finalScale;
|
|
5886
|
-
planeBounds.min.z *= finalScale;
|
|
5887
|
-
planeBounds.max.x *= finalScale;
|
|
5888
|
-
planeBounds.max.y *= finalScale;
|
|
5889
|
-
planeBounds.max.z *= finalScale;
|
|
5890
|
-
for (let i = 0; i < glyphInfoArray.length; i++) {
|
|
5891
|
-
const glyphInfo = glyphInfoArray[i];
|
|
5892
|
-
glyphInfo.bounds.min.x = glyphInfo.bounds.min.x * finalScale + offsetScaled;
|
|
5893
|
-
glyphInfo.bounds.min.y *= finalScale;
|
|
5894
|
-
glyphInfo.bounds.min.z *= finalScale;
|
|
5895
|
-
glyphInfo.bounds.max.x = glyphInfo.bounds.max.x * finalScale + offsetScaled;
|
|
5896
|
-
glyphInfo.bounds.max.y *= finalScale;
|
|
5897
|
-
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
|
+
}
|
|
5898
5746
|
}
|
|
5899
5747
|
let colors;
|
|
5900
5748
|
let coloredRanges;
|
|
@@ -5919,8 +5767,7 @@ class Text {
|
|
|
5919
5767
|
verticesGenerated,
|
|
5920
5768
|
pointsRemovedByVisvalingam: optimizationStats.pointsRemovedByVisvalingam,
|
|
5921
5769
|
pointsRemovedByColinear: optimizationStats.pointsRemovedByColinear,
|
|
5922
|
-
originalPointCount: optimizationStats.originalPointCount
|
|
5923
|
-
...(cacheStats || {})
|
|
5770
|
+
originalPointCount: optimizationStats.originalPointCount
|
|
5924
5771
|
},
|
|
5925
5772
|
query: (options) => {
|
|
5926
5773
|
if (!originalText) {
|
|
@@ -5955,13 +5802,11 @@ class Text {
|
|
|
5955
5802
|
static registerPattern(language, pattern) {
|
|
5956
5803
|
Text.patternCache.set(language, pattern);
|
|
5957
5804
|
}
|
|
5958
|
-
static clearFontCache() {
|
|
5959
|
-
Text.fontCache.clear();
|
|
5960
|
-
Text.fontCacheMemoryBytes = 0;
|
|
5961
|
-
}
|
|
5962
5805
|
static setMaxFontCacheMemoryMB(limitMB) {
|
|
5963
5806
|
Text.maxFontCacheMemoryBytes =
|
|
5964
|
-
limitMB === Infinity
|
|
5807
|
+
limitMB === Infinity
|
|
5808
|
+
? Infinity
|
|
5809
|
+
: Math.max(1, Math.floor(limitMB)) * 1024 * 1024;
|
|
5965
5810
|
Text.enforceFontCacheMemoryLimit();
|
|
5966
5811
|
}
|
|
5967
5812
|
getLoadedFont() {
|
|
@@ -5973,11 +5818,11 @@ class Text {
|
|
|
5973
5818
|
}
|
|
5974
5819
|
return TextMeasurer.measureTextWidth(this.loadedFont, text, letterSpacing);
|
|
5975
5820
|
}
|
|
5976
|
-
|
|
5821
|
+
getCacheSize() {
|
|
5977
5822
|
if (this.geometryBuilder) {
|
|
5978
|
-
return this.geometryBuilder.getCacheStats();
|
|
5823
|
+
return this.geometryBuilder.getCacheStats().size;
|
|
5979
5824
|
}
|
|
5980
|
-
return
|
|
5825
|
+
return 0;
|
|
5981
5826
|
}
|
|
5982
5827
|
clearCache() {
|
|
5983
5828
|
if (this.geometryBuilder) {
|
|
@@ -5988,25 +5833,45 @@ class Text {
|
|
|
5988
5833
|
const glyphCenters = new Float32Array(vertexCount * 3);
|
|
5989
5834
|
const glyphIndices = new Float32Array(vertexCount);
|
|
5990
5835
|
const glyphLineIndices = new Float32Array(vertexCount);
|
|
5991
|
-
|
|
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];
|
|
5992
5850
|
const centerX = (glyph.bounds.min.x + glyph.bounds.max.x) / 2;
|
|
5993
5851
|
const centerY = (glyph.bounds.min.y + glyph.bounds.max.y) / 2;
|
|
5994
5852
|
const centerZ = (glyph.bounds.min.z + glyph.bounds.max.z) / 2;
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
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;
|
|
6004
5867
|
}
|
|
6005
|
-
}
|
|
5868
|
+
}
|
|
6006
5869
|
return {
|
|
6007
5870
|
glyphCenter: glyphCenters,
|
|
6008
5871
|
glyphIndex: glyphIndices,
|
|
6009
|
-
glyphLineIndex: glyphLineIndices
|
|
5872
|
+
glyphLineIndex: glyphLineIndices,
|
|
5873
|
+
glyphProgress,
|
|
5874
|
+
glyphBaselineY
|
|
6010
5875
|
};
|
|
6011
5876
|
}
|
|
6012
5877
|
resetHelpers() {
|