three-text 0.4.10 → 0.4.12
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 +257 -38
- package/dist/index.cjs +3409 -3356
- package/dist/index.d.ts +166 -9
- package/dist/index.js +3405 -3357
- package/dist/index.min.cjs +765 -746
- package/dist/index.min.js +756 -737
- package/dist/index.umd.js +3548 -3487
- package/dist/index.umd.min.js +800 -776
- package/dist/p5/index.cjs +2738 -5
- package/dist/p5/index.js +2738 -5
- package/dist/slug/index.cjs +380 -0
- package/dist/slug/index.d.ts +62 -0
- package/dist/slug/index.js +374 -0
- package/dist/three/index.cjs +50 -35
- package/dist/three/index.js +50 -35
- package/dist/three/react.cjs +5 -2
- package/dist/three/react.d.ts +69 -120
- package/dist/three/react.js +6 -3
- package/dist/types/core/Text.d.ts +3 -10
- package/dist/types/core/cache/sharedCaches.d.ts +2 -1
- package/dist/types/core/shaping/DrawCallbacks.d.ts +11 -3
- package/dist/types/core/shaping/TextShaper.d.ts +1 -5
- package/dist/types/core/types.d.ts +87 -0
- package/dist/types/index.d.ts +7 -3
- package/dist/types/{core/cache → mesh}/GlyphContourCollector.d.ts +4 -4
- package/dist/types/{core/cache → mesh}/GlyphGeometryBuilder.d.ts +5 -5
- package/dist/types/mesh/MeshGeometryBuilder.d.ts +18 -0
- package/dist/types/{core → mesh}/geometry/BoundaryClusterer.d.ts +1 -1
- package/dist/types/{core → mesh}/geometry/Extruder.d.ts +1 -1
- package/dist/types/{core → mesh}/geometry/PathOptimizer.d.ts +1 -3
- package/dist/types/{core → mesh}/geometry/Polygonizer.d.ts +11 -6
- package/dist/types/{core → mesh}/geometry/Tessellator.d.ts +1 -1
- package/dist/types/react/utils.d.ts +2 -0
- package/dist/types/vector/GlyphOutlineCollector.d.ts +25 -0
- package/dist/types/vector/GlyphVectorGeometryBuilder.d.ts +26 -0
- package/dist/types/vector/LoopBlinnGeometry.d.ts +68 -0
- package/dist/types/vector/index.d.ts +29 -0
- package/dist/types/vector/loopBlinnTSL.d.ts +11 -0
- package/dist/types/vector/react.d.ts +24 -0
- package/dist/types/vector/webgl/index.d.ts +7 -0
- package/dist/types/vector/webgpu/index.d.ts +11 -0
- package/dist/vector/index.cjs +1458 -0
- package/dist/vector/index.d.ts +122 -0
- package/dist/vector/index.js +1434 -0
- package/dist/vector/react.cjs +153 -0
- package/dist/vector/react.d.ts +317 -0
- package/dist/vector/react.js +132 -0
- package/dist/vector/types/slug-lib/src/SlugPacker.d.ts +17 -0
- package/dist/vector/types/slug-lib/src/WebGL2Renderer.d.ts +21 -0
- package/dist/vector/types/slug-lib/src/WebGPURenderer.d.ts +16 -0
- package/dist/vector/types/slug-lib/src/index.d.ts +15 -0
- package/dist/vector/types/slug-lib/src/shaderStrings.d.ts +9 -0
- package/dist/vector/types/slug-lib/src/types.d.ts +34 -0
- package/dist/vector/types/src/core/types.d.ts +381 -0
- package/dist/vector/types/src/hyphenation/HyphenationPatternLoader.d.ts +2 -0
- package/dist/vector/types/src/hyphenation/index.d.ts +7 -0
- package/dist/vector/types/src/hyphenation/types.d.ts +6 -0
- package/dist/vector/types/src/utils/Cache.d.ts +14 -0
- package/dist/vector/types/src/utils/vectors.d.ts +75 -0
- package/dist/vector/types/src/vector/VectorDataBuilder.d.ts +30 -0
- package/dist/vector/types/src/vector/VectorThreeAdapter.d.ts +27 -0
- package/dist/vector/types/src/vector/index.d.ts +15 -0
- package/dist/vector/webgl/index.cjs +229 -0
- package/dist/vector/webgl/index.d.ts +53 -0
- package/dist/vector/webgl/index.js +227 -0
- package/dist/vector/webgpu/index.cjs +321 -0
- package/dist/vector/webgpu/index.d.ts +57 -0
- package/dist/vector/webgpu/index.js +319 -0
- package/dist/webgl-vector/index.cjs +243 -0
- package/dist/webgl-vector/index.d.ts +34 -0
- package/dist/webgl-vector/index.js +241 -0
- package/dist/webgpu-vector/index.cjs +336 -0
- package/dist/webgpu-vector/index.d.ts +38 -0
- package/dist/webgpu-vector/index.js +334 -0
- package/package.json +48 -3
- package/dist/types/utils/MinHeap.d.ts +0 -14
package/dist/p5/index.cjs
CHANGED
|
@@ -1,6 +1,2736 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var Text = require('../index.cjs');
|
|
4
|
+
var sharedCaches = require('../index.cjs');
|
|
5
|
+
var DrawCallbacks = require('../index.cjs');
|
|
6
|
+
var TextLayout = require('../index.cjs');
|
|
7
|
+
var TextRangeQuery = require('../index.cjs');
|
|
8
|
+
|
|
9
|
+
// 2D Vector
|
|
10
|
+
class Vec2 {
|
|
11
|
+
constructor(x = 0, y = 0) {
|
|
12
|
+
this.x = x;
|
|
13
|
+
this.y = y;
|
|
14
|
+
}
|
|
15
|
+
set(x, y) {
|
|
16
|
+
this.x = x;
|
|
17
|
+
this.y = y;
|
|
18
|
+
return this;
|
|
19
|
+
}
|
|
20
|
+
clone() {
|
|
21
|
+
return new Vec2(this.x, this.y);
|
|
22
|
+
}
|
|
23
|
+
copy(v) {
|
|
24
|
+
this.x = v.x;
|
|
25
|
+
this.y = v.y;
|
|
26
|
+
return this;
|
|
27
|
+
}
|
|
28
|
+
add(v) {
|
|
29
|
+
this.x += v.x;
|
|
30
|
+
this.y += v.y;
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
33
|
+
sub(v) {
|
|
34
|
+
this.x -= v.x;
|
|
35
|
+
this.y -= v.y;
|
|
36
|
+
return this;
|
|
37
|
+
}
|
|
38
|
+
multiply(scalar) {
|
|
39
|
+
this.x *= scalar;
|
|
40
|
+
this.y *= scalar;
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
divide(scalar) {
|
|
44
|
+
this.x /= scalar;
|
|
45
|
+
this.y /= scalar;
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
length() {
|
|
49
|
+
return Math.sqrt(this.x * this.x + this.y * this.y);
|
|
50
|
+
}
|
|
51
|
+
lengthSq() {
|
|
52
|
+
return this.x * this.x + this.y * this.y;
|
|
53
|
+
}
|
|
54
|
+
normalize() {
|
|
55
|
+
const len = this.length();
|
|
56
|
+
if (len > 0) {
|
|
57
|
+
this.divide(len);
|
|
58
|
+
}
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
dot(v) {
|
|
62
|
+
return this.x * v.x + this.y * v.y;
|
|
63
|
+
}
|
|
64
|
+
distanceTo(v) {
|
|
65
|
+
const dx = this.x - v.x;
|
|
66
|
+
const dy = this.y - v.y;
|
|
67
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
68
|
+
}
|
|
69
|
+
distanceToSquared(v) {
|
|
70
|
+
const dx = this.x - v.x;
|
|
71
|
+
const dy = this.y - v.y;
|
|
72
|
+
return dx * dx + dy * dy;
|
|
73
|
+
}
|
|
74
|
+
equals(v) {
|
|
75
|
+
return this.x === v.x && this.y === v.y;
|
|
76
|
+
}
|
|
77
|
+
angle() {
|
|
78
|
+
return Math.atan2(this.y, this.x);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// 3D Vector
|
|
82
|
+
class Vec3 {
|
|
83
|
+
constructor(x = 0, y = 0, z = 0) {
|
|
84
|
+
this.x = x;
|
|
85
|
+
this.y = y;
|
|
86
|
+
this.z = z;
|
|
87
|
+
}
|
|
88
|
+
set(x, y, z) {
|
|
89
|
+
this.x = x;
|
|
90
|
+
this.y = y;
|
|
91
|
+
this.z = z;
|
|
92
|
+
return this;
|
|
93
|
+
}
|
|
94
|
+
clone() {
|
|
95
|
+
return new Vec3(this.x, this.y, this.z);
|
|
96
|
+
}
|
|
97
|
+
copy(v) {
|
|
98
|
+
this.x = v.x;
|
|
99
|
+
this.y = v.y;
|
|
100
|
+
this.z = v.z;
|
|
101
|
+
return this;
|
|
102
|
+
}
|
|
103
|
+
add(v) {
|
|
104
|
+
this.x += v.x;
|
|
105
|
+
this.y += v.y;
|
|
106
|
+
this.z += v.z;
|
|
107
|
+
return this;
|
|
108
|
+
}
|
|
109
|
+
sub(v) {
|
|
110
|
+
this.x -= v.x;
|
|
111
|
+
this.y -= v.y;
|
|
112
|
+
this.z -= v.z;
|
|
113
|
+
return this;
|
|
114
|
+
}
|
|
115
|
+
multiply(scalar) {
|
|
116
|
+
this.x *= scalar;
|
|
117
|
+
this.y *= scalar;
|
|
118
|
+
this.z *= scalar;
|
|
119
|
+
return this;
|
|
120
|
+
}
|
|
121
|
+
divide(scalar) {
|
|
122
|
+
this.x /= scalar;
|
|
123
|
+
this.y /= scalar;
|
|
124
|
+
this.z /= scalar;
|
|
125
|
+
return this;
|
|
126
|
+
}
|
|
127
|
+
length() {
|
|
128
|
+
return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
|
|
129
|
+
}
|
|
130
|
+
lengthSq() {
|
|
131
|
+
return this.x * this.x + this.y * this.y + this.z * this.z;
|
|
132
|
+
}
|
|
133
|
+
normalize() {
|
|
134
|
+
const len = this.length();
|
|
135
|
+
if (len > 0) {
|
|
136
|
+
this.divide(len);
|
|
137
|
+
}
|
|
138
|
+
return this;
|
|
139
|
+
}
|
|
140
|
+
dot(v) {
|
|
141
|
+
return this.x * v.x + this.y * v.y + this.z * v.z;
|
|
142
|
+
}
|
|
143
|
+
cross(v) {
|
|
144
|
+
const x = this.y * v.z - this.z * v.y;
|
|
145
|
+
const y = this.z * v.x - this.x * v.z;
|
|
146
|
+
const z = this.x * v.y - this.y * v.x;
|
|
147
|
+
this.x = x;
|
|
148
|
+
this.y = y;
|
|
149
|
+
this.z = z;
|
|
150
|
+
return this;
|
|
151
|
+
}
|
|
152
|
+
distanceTo(v) {
|
|
153
|
+
const dx = this.x - v.x;
|
|
154
|
+
const dy = this.y - v.y;
|
|
155
|
+
const dz = this.z - v.z;
|
|
156
|
+
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
157
|
+
}
|
|
158
|
+
distanceToSquared(v) {
|
|
159
|
+
const dx = this.x - v.x;
|
|
160
|
+
const dy = this.y - v.y;
|
|
161
|
+
const dz = this.z - v.z;
|
|
162
|
+
return dx * dx + dy * dy + dz * dz;
|
|
163
|
+
}
|
|
164
|
+
equals(v) {
|
|
165
|
+
return this.x === v.x && this.y === v.y && this.z === v.z;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Cached flag check at module load time for zero-cost logging
|
|
170
|
+
const isLogEnabled = (() => {
|
|
171
|
+
if (typeof window !== 'undefined' && window.THREE_TEXT_LOG) {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
if (typeof globalThis !== 'undefined' &&
|
|
175
|
+
globalThis.process?.env?.THREE_TEXT_LOG === 'true') {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
return false;
|
|
179
|
+
})();
|
|
180
|
+
class Logger {
|
|
181
|
+
warn(message, ...args) {
|
|
182
|
+
console.warn(message, ...args);
|
|
183
|
+
}
|
|
184
|
+
error(message, ...args) {
|
|
185
|
+
console.error(message, ...args);
|
|
186
|
+
}
|
|
187
|
+
log(message, ...args) {
|
|
188
|
+
isLogEnabled && console.log(message, ...args);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const logger = new Logger();
|
|
192
|
+
|
|
193
|
+
class PerformanceLogger {
|
|
194
|
+
constructor() {
|
|
195
|
+
this.metrics = [];
|
|
196
|
+
this.activeTimers = new Map();
|
|
197
|
+
}
|
|
198
|
+
start(name, metadata) {
|
|
199
|
+
// Early exit if disabled - no metric collection
|
|
200
|
+
if (!isLogEnabled)
|
|
201
|
+
return;
|
|
202
|
+
const startTime = performance.now();
|
|
203
|
+
// Generate unique key for nested timing support
|
|
204
|
+
const timerKey = `${name}_${startTime}`;
|
|
205
|
+
this.activeTimers.set(timerKey, startTime);
|
|
206
|
+
this.metrics.push({
|
|
207
|
+
name,
|
|
208
|
+
startTime,
|
|
209
|
+
metadata
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
end(name) {
|
|
213
|
+
// Early exit if disabled
|
|
214
|
+
if (!isLogEnabled)
|
|
215
|
+
return null;
|
|
216
|
+
const endTime = performance.now();
|
|
217
|
+
// Find the most recent matching timer by scanning backwards
|
|
218
|
+
let timerKey;
|
|
219
|
+
let startTime;
|
|
220
|
+
for (const [key, time] of Array.from(this.activeTimers.entries()).reverse()) {
|
|
221
|
+
if (key.startsWith(`${name}_`)) {
|
|
222
|
+
timerKey = key;
|
|
223
|
+
startTime = time;
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (startTime === undefined || !timerKey) {
|
|
228
|
+
logger.warn(`Performance timer "${name}" was not started`);
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
const duration = endTime - startTime;
|
|
232
|
+
this.activeTimers.delete(timerKey);
|
|
233
|
+
// Find the metric in reverse order (most recent first)
|
|
234
|
+
for (let i = this.metrics.length - 1; i >= 0; i--) {
|
|
235
|
+
const metric = this.metrics[i];
|
|
236
|
+
if (metric.name === name &&
|
|
237
|
+
metric.startTime === startTime &&
|
|
238
|
+
!metric.endTime) {
|
|
239
|
+
metric.endTime = endTime;
|
|
240
|
+
metric.duration = duration;
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
console.log(`${name}: ${duration.toFixed(2)}ms`);
|
|
245
|
+
return duration;
|
|
246
|
+
}
|
|
247
|
+
getSummary() {
|
|
248
|
+
if (!isLogEnabled)
|
|
249
|
+
return {};
|
|
250
|
+
const summary = {};
|
|
251
|
+
for (const metric of this.metrics) {
|
|
252
|
+
if (!metric.duration)
|
|
253
|
+
continue;
|
|
254
|
+
const existing = summary[metric.name];
|
|
255
|
+
if (existing) {
|
|
256
|
+
existing.count++;
|
|
257
|
+
existing.totalDuration += metric.duration;
|
|
258
|
+
existing.avgDuration = existing.totalDuration / existing.count;
|
|
259
|
+
existing.lastDuration = metric.duration;
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
summary[metric.name] = {
|
|
263
|
+
count: 1,
|
|
264
|
+
avgDuration: metric.duration,
|
|
265
|
+
totalDuration: metric.duration,
|
|
266
|
+
lastDuration: metric.duration
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return summary;
|
|
271
|
+
}
|
|
272
|
+
printSummary() {
|
|
273
|
+
if (!isLogEnabled)
|
|
274
|
+
return;
|
|
275
|
+
const summary = this.getSummary();
|
|
276
|
+
console.table(summary);
|
|
277
|
+
console.log('Operations:', Object.keys(summary).sort().join(', '));
|
|
278
|
+
}
|
|
279
|
+
printBaseline() {
|
|
280
|
+
if (!isLogEnabled)
|
|
281
|
+
return;
|
|
282
|
+
const summary = this.getSummary();
|
|
283
|
+
Object.entries(summary).forEach(([name, stats]) => {
|
|
284
|
+
console.log(`BASELINE ${name}: ${stats.avgDuration.toFixed(2)}ms avg (${stats.count} calls)`);
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
clear() {
|
|
288
|
+
if (!isLogEnabled)
|
|
289
|
+
return;
|
|
290
|
+
this.metrics.length = 0;
|
|
291
|
+
this.activeTimers.clear();
|
|
292
|
+
}
|
|
293
|
+
time(name, fn, metadata) {
|
|
294
|
+
if (!isLogEnabled)
|
|
295
|
+
return fn();
|
|
296
|
+
this.start(name, metadata);
|
|
297
|
+
try {
|
|
298
|
+
return fn();
|
|
299
|
+
}
|
|
300
|
+
finally {
|
|
301
|
+
this.end(name);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
async timeAsync(name, fn, metadata) {
|
|
305
|
+
if (!isLogEnabled)
|
|
306
|
+
return fn();
|
|
307
|
+
this.start(name, metadata);
|
|
308
|
+
try {
|
|
309
|
+
return await fn();
|
|
310
|
+
}
|
|
311
|
+
finally {
|
|
312
|
+
this.end(name);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
// Create a single instance
|
|
317
|
+
// When debug is disabled, all methods return immediately with minimal overhead
|
|
318
|
+
const perfLogger = new PerformanceLogger();
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* @license
|
|
322
|
+
* libtess-ts - TypeScript port of the SGI GLU tessellator
|
|
323
|
+
* Original Code: OpenGL Sample Implementation, Version 1.2.1,
|
|
324
|
+
* released January 26, 2000. Copyright (c) 1991-2000 Silicon Graphics, Inc.
|
|
325
|
+
* Copyright 2012, Google Inc. All Rights Reserved.
|
|
326
|
+
* Copyright 2026, Countertype LLC. All Rights Reserved.
|
|
327
|
+
* SGI Free Software License B (Version 2.0)
|
|
328
|
+
* http://oss.sgi.com/projects/FreeB/
|
|
329
|
+
*/
|
|
330
|
+
function t(t,i){return t.s===i.s&&t.t===i.t}function i(t,i){return i.s>t.s||t.s===i.s&&i.t>=t.t}function s(t,i){return i.t>t.t||t.t===i.t&&i.s>=t.s}function e(t){return i(t.h.i,t.i)}function n(t){return i(t.i,t.h.i)}function h(t,i){return Math.abs(t.s-i.s)+Math.abs(t.t-i.t)}function r(t,i,s){let e=i.s-t.s,n=s.s-i.s;return e+n>0?n>e?i.t-t.t+e/(e+n)*(t.t-s.t):i.t-s.t+n/(e+n)*(s.t-t.t):0}function l(t,i,s){let e=i.s-t.s,n=s.s-i.s;return e+n>0?(i.t-s.t)*e+(i.t-t.t)*n:0}function o(t,i,s){let e=i.t-t.t,n=s.t-i.t;return e+n>0?n>e?i.s-t.s+e/(e+n)*(t.s-s.s):i.s-s.s+n/(e+n)*(s.s-t.s):0}function u(t,i,s){let e=i.t-t.t,n=s.t-i.t;return e+n>0?(i.s-s.s)*e+(i.s-t.s)*n:0}function c(t,i,s,e){return (t=0>t?0:t)>(s=0>s?0:s)?e+s/(t+s)*(i-e):0===s?(i+e)/2:i+t/(t+s)*(e-i)}function a(t,s,e){const n=t.event,h=s.l,o=e.l;return h.h.i===n?o.h.i===n?i(h.i,o.i)?0>=l(o.h.i,h.i,o.i):l(h.h.i,o.i,h.i)>=0:0>=l(o.h.i,n,o.i):o.h.i===n?l(h.h.i,n,h.i)>=0:r(h.h.i,n,h.i)>=r(o.h.i,n,o.i)}function f(t){return t.o}function d(t){return t.next}function w(t,i){t.u+=i.u,t.h.u+=i.h.u;}function E(t,i){i.l.N=null,t._.delete(i);}function N(t,i,s){t.A.delete(i.l),i.I=0,i.l=s,s.N=i;}function _(t,i){let s,e=i.l.i;do{i=d(i);}while(i.l.i===e);return i.I&&(s=t.A.connect(f(i).l.h,i.l.O),N(t,i,s),i=d(i)),i}function A(t){let i=t.l.h.i;do{t=d(t);}while(t.l.h.i===i);return t}function I(t,i,s){const e=new Z;return e.l=s,t._.insertBefore(i,e),e.I=0,e.k=0,e.T=0,s.N=e,e}function g(t,i){switch(t.M){case Y.ODD:return !!(1&i);case Y.NONZERO:return 0!==i;case Y.POSITIVE:return i>0;case Y.NEGATIVE:return 0>i;case Y.ABS_GEQ_TWO:return i>=2||-2>=i}throw Error("Invalid winding rule")}function O(t,i){const s=i.l,e=s.D;e.p=i.p,e.L=s,E(t,i);}function k(t,i,s){let e,n=null,h=i,r=i.l;for(;h!==s;){if(h.I=0,n=f(h),e=n.l,e.i!=r.i){if(!n.I){O(t,h);break}e=t.A.connect(r.C.h,e.h),N(t,n,e);}r.C!==e&&(t.A.splice(e.h.O,e),t.A.splice(r,e)),O(t,h),r=n.l,h=n;}return r}function y(t,i,s,e,n,h){let r,l,o,u,c=1;o=s;do{I(t,i,o.h),o=o.C;}while(o!==e);for(null===n&&(n=f(i).l.h.C),l=i,u=n;r=f(l),o=r.l.h,o.i===u.i;)o.C!==u&&(t.A.splice(o.h.O,o),t.A.splice(u.h.O,o)),r.G=l.G-o.u,r.p=g(t,r.G),l.T=1,!c&&D(t,l)&&(w(o,u),E(t,l),t.A.delete(u)),c=0,l=r,u=o;l.T=1,h&&C(t,l);}function T(t,i,s,e,n){i.data=null,t.R&&(rt[0]=i.coords[0],rt[1]=i.coords[1],rt[2]=i.coords[2],i.data=t.R(rt,s,e,t.m)),null===i.data&&(n?(t.v(100156),t.U=1):i.data=s[0]);}function b(t,i,s){t.R&&(ht[0]=i.i.data,ht[1]=s.i.data,ht[2]=null,ht[3]=null,nt[0]=.5,nt[1]=.5,nt[2]=0,nt[3]=0,T(t,i.i,ht,nt,0)),t.A.splice(i,s);}function M(t,i,s,e,n){let r=h(i,t),l=h(s,t),o=.5*l/(r+l),u=.5*r/(r+l);void 0!==e&&void 0!==n&&(e[n]=o,e[n+1]=u),t.coords[0]+=o*i.coords[0]+u*s.coords[0],t.coords[1]+=o*i.coords[1]+u*s.coords[1],t.coords[2]+=o*i.coords[2]+u*s.coords[2];}function D(i,s){let e=f(s);const n=s.l,h=e.l;if(h.i.s>n.i.s||n.i.s===h.i.s&&h.i.t>=n.i.t){if(l(h.h.i,n.i,h.i)>0)return 0;t(n.i,h.i)?n.i!==h.i&&(i.S.delete(n.i.B),b(i,h.h.O,n)):(i.A.V(h.h),i.A.splice(n,h.h.O),s.T=e.T=1);}else {if(0>l(n.h.i,h.i,n.i))return 0;d(s).T=s.T=1,i.A.V(n.h),i.A.splice(h.h.O,n);}return 1}function p(t,i){let s=f(i);const e=i.l,n=s.l;let h;if(n.h.i.s>e.h.i.s||e.h.i.s===n.h.i.s&&n.h.i.t>=e.h.i.t){if(0>l(e.h.i,n.h.i,e.i))return 0;d(i).T=i.T=1,h=t.A.V(e),t.A.splice(n.h,h),h.D.p=i.p;}else {if(l(n.h.i,e.h.i,n.i)>0)return 0;i.T=s.T=1,h=t.A.V(n),t.A.splice(e.O,n.h),h.h.D.p=i.p;}return 1}function L(e,n){let h=f(n),a=n.l,w=h.l;const E=a.i,N=w.i;let I,g,O=a.h.i,b=w.h.i;const p=st||(st=new X);let L,C;if(E===N)return 0;if(I=Math.min(E.t,O.t),g=Math.max(N.t,b.t),I>g)return 0;if(i(E,N)){if(l(b,E,N)>0)return 0}else if(0>l(O,N,E))return 0;return ((t,e,n,h,a)=>{let f,d,w;i(t,e)||(w=t,t=e,e=w),i(n,h)||(w=n,n=h,h=w),i(t,n)||(w=t,t=n,n=w,w=e,e=h,h=w),i(n,e)?i(e,h)?(f=r(t,n,e),d=r(n,e,h),0>f+d&&(f=-f,d=-d),a.s=c(f,n.s,d,e.s)):(f=l(t,n,e),d=-l(t,h,e),0>f+d&&(f=-f,d=-d),a.s=c(f,n.s,d,h.s)):a.s=.5*(n.s+e.s),s(t,e)||(w=t,t=e,e=w),s(n,h)||(w=n,n=h,h=w),s(t,n)||(w=t,t=n,n=w,w=e,e=h,h=w),s(n,e)?s(e,h)?(f=o(t,n,e),d=o(n,e,h),0>f+d&&(f=-f,d=-d),a.t=c(f,n.t,d,e.t)):(f=u(t,n,e),d=-u(t,h,e),0>f+d&&(f=-f,d=-d),a.t=c(f,n.t,d,h.t)):a.t=.5*(n.t+e.t);})(O,E,b,N,p),(e.event.s>p.s||p.s===e.event.s&&e.event.t>=p.t)&&(p.s=e.event.s,p.t=e.event.t),L=N.s>E.s||E.s===N.s&&N.t>=E.t?E:N,(p.s>L.s||L.s===p.s&&p.t>=L.t)&&(p.s=L.s,p.t=L.t),t(p,E)||t(p,N)?(D(e,n),0):!t(O,e.event)&&l(O,e.event,p)>=0||!t(b,e.event)&&0>=l(b,e.event,p)?b===e.event?(e.A.V(a.h),e.A.splice(w.h,a),a=f(n=_(e,n)).l,k(e,f(n),h),y(e,n,a.h.O,a,a,1),1):O===e.event?(e.A.V(w.h),e.A.splice(a.O,w.h.O),h=n,C=f(n=A(n)).l.h.C,h.l=w.h.O,w=k(e,h,null),y(e,n,w.C,a.h.C,C,1),1):(0>l(O,e.event,p)||(d(n).T=n.T=1,e.A.V(a.h),a.i.s=e.event.s,a.i.t=e.event.t),l(b,e.event,p)>0||(n.T=h.T=1,e.A.V(w.h),w.i.s=e.event.s,w.i.t=e.event.t),0):(e.A.V(a.h),e.A.V(w.h),e.A.splice(w.h.O,a),a.i.s=p.s,a.i.t=p.t,a.i.B=e.S.P(a.i),((t,i,s,e,n,h)=>{nt[0]=0,nt[1]=0,nt[2]=0,nt[3]=0,ht[0]=s.data,ht[1]=e.data,ht[2]=n.data,ht[3]=h.data,i.coords[0]=i.coords[1]=i.coords[2]=0,M(i,s,e,nt,0),M(i,n,h,nt,2),T(t,i,ht,nt,1);})(e,a.i,E,O,N,b),d(n).T=n.T=h.T=1,0)}function C(t,i){let s,e,n=f(i);for(;;){for(;n.T;)i=n,n=f(n);if(!i.T&&(n=i,null===(i=d(i))||!i.T))return;if(i.T=0,s=i.l,e=n.l,s.h.i!==e.h.i&&p(t,i)&&(n.I?(E(t,n),t.A.delete(e),n=f(i),e=n.l):i.I&&(E(t,i),t.A.delete(s),s=(i=d(n)).l)),s.i!==e.i)if(s.h.i===e.h.i||i.I||n.I||s.h.i!==t.event&&e.h.i!==t.event)D(t,i);else if(L(t,i))return;s.i===e.i&&s.h.i===e.h.i&&(w(e,s),E(t,i),t.A.delete(s),i=d(n));}}function G(s,n){let h,r,o,u,c,a;const w=et||(et=new Z);w.l=n.L.h,h=s._.search(w),r=f(h),r&&(u=h.l,c=r.l,0!==l(u.h.i,n,u.i)?(o=i(c.h.i,u.h.i)?h:r,h.p||o.I?(a=o===h?s.A.connect(n.L.h,u.O):s.A.connect(c.h.C.h,n.L).h,o.I?N(s,o,a):((t,i)=>{i.G=d(i).G+i.l.u,i.p=g(t,i.G);})(s,I(s,h,a)),R(s,n)):y(s,h,n.L,n.L,null,1)):((i,s,n)=>{let h,r,l,o,u;if(h=s.l,t(h.i,n))b(i,h,n.L);else {if(!t(h.h.i,n))return i.A.V(h.h),s.I&&(i.A.delete(h.C),s.I=0),i.A.splice(n.L,h),void R(i,n);u=f(s=A(s)),l=u.l.h,r=o=l.C,u.I&&(E(i,u),i.A.delete(l),l=r.h.O),i.A.splice(n.L,l),e(r)||(r=null),y(i,s,l.C,o,r,1);}})(s,h,n));}function R(s,e){s.event=e;let n=e.L;for(;null===n.N;)if(n=n.C,n===e.L)return void G(s,e);let h=_(s,n.N),r=f(h);const l=r.l;let o=k(s,r,null);o.C===l?((s,e,n)=>{let h,r=n.C,l=f(e),o=e.l,u=l.l,c=0;o.h.i!==u.h.i&&L(s,e),t(o.i,s.event)&&(s.A.splice(r.h.O,o),r=f(e=_(s,e)).l,k(s,f(e),l),c=1),t(u.i,s.event)&&(s.A.splice(n,u.h.O),n=k(s,l,null),c=1),c?y(s,e,n.C,r,r,1):(h=i(u.i,o.i)?u.h.O:o,h=s.A.connect(n.C.h,h),y(s,e,h,h.C,h.C,0),h.h.N.I=1,C(s,e));})(s,h,o):y(s,h,o.C,l,l,1);}function m(t,i,s,e){const n=new Z;let h=t.A.F();h.i.s=s,h.i.t=e,h.h.i.s=i,h.h.i.t=e,t.event=h.h.i,n.l=h,n.G=0,n.p=0,n.I=0,n.k=1,n.T=0,t._.P(n);}function v(t,i){const s=t.Y;let e=s.next,n=e.coords[0],h=e.coords[1],r=e.coords[2],l=n,o=h,u=r,c=e,a=e,f=e,d=e,w=e,E=e;for(e=s.next;e!==s;e=e.next){const t=e.coords[0],i=e.coords[1],s=e.coords[2];n>t&&(n=t,c=e),t>l&&(l=t,d=e),h>i&&(h=i,a=e),i>o&&(o=i,w=e),r>s&&(r=s,f=e),s>u&&(u=s,E=e);}let N=0,_=l-n;const A=o-h;let I,g;if(A>_&&(N=1,_=A),u-r>_&&(N=2),0===N){if(n>=l)return i[0]=0,i[1]=0,void(i[2]=1);I=c,g=d;}else if(1===N){if(h>=o)return i[0]=0,i[1]=0,void(i[2]=1);I=a,g=w;}else {if(r>=u)return i[0]=0,i[1]=0,void(i[2]=1);I=f,g=E;}const O=I.coords[0]-g.coords[0],k=I.coords[1]-g.coords[1],y=I.coords[2]-g.coords[2];let T=0;for(e=s.next;e!==s;e=e.next){const t=e.coords[0]-g.coords[0],s=e.coords[1]-g.coords[1],n=e.coords[2]-g.coords[2],h=k*n-y*s,r=y*t-O*n,l=O*s-k*t,o=h*h+r*r+l*l;o>T&&(T=o,i[0]=h,i[1]=r,i[2]=l);}T>0||(i[0]=i[1]=i[2]=0,Math.abs(k)>Math.abs(O)?i[Math.abs(y)>Math.abs(k)?2:1]=1:i[Math.abs(y)>Math.abs(O)?2:0]=1);}function x(t,i){let s,e,n,h=t.j,r=t.Y,l=0;for(s=h.next;s!==h;s=s.next)if(n=s.L,n.u>0)do{l+=(n.i.s-n.h.i.s)*(n.i.t+n.h.i.t),n=n.O;}while(n!==s.L);if(0>l){for(e=r.next;e!==r;e=e.next)e.t=-e.t;i[0]=-i[0],i[1]=-i[1],i[2]=-i[2];}}function U(t,i){let s=0;for(let e=i.j.next;e!==i.j;e=e.next)e.p&&(s||(t.H(4),s=1),B(e,t));s&&t.q();}function S(t,i,s,e){0>i.s*(s.t-e.t)+s.s*(e.t-i.t)+e.s*(i.t-s.t)?(t.W(i.data),t.W(e.data),t.W(s.data)):(t.W(i.data),t.W(s.data),t.W(e.data));}function B(t,s){let e=0,n=t.L;do{ot[e++]=n.i,n=n.O;}while(n!==t.L);if(3>e)return;if(3===e)return void S(s,ot[0],ot[1],ot[2]);(t=>{if(t>ut.length){const i=2*t;ut=new Int8Array(i),ct=new Int32Array(i),at=new Int32Array(i);}})(e);let h=0,r=0;for(let t=1;e>t;t++)i(ot[t],ot[h])||(h=t),i(ot[t],ot[r])&&(r=t);if(h===r)return;let l=0;ct[l]=h,ut[l]=1,l++;let o=(h+1)%e,u=(h+e-1)%e;for(;o!==r||u!==r;){let t;t=o===r?0:u===r?1:!i(ot[o],ot[u]),t?(ct[l]=o,ut[l]=1,l++,o=(o+1)%e):(ct[l]=u,ut[l]=0,l++,u=(u+e-1)%e);}ct[l]=r,ut[l]=1,l++;let c=0;at[c++]=0,at[c++]=1;for(let t=2;l-1>t;t++)if(ut[t]!==ut[at[c-1]]){for(;c>1;){const i=at[--c];S(s,ot[ct[t]],ot[ct[i]],ot[ct[at[c-1]]]);}--c,at[c++]=t-1,at[c++]=t;}else {let i=at[--c];for(;c>0;){const e=ot[ct[t]],n=ot[ct[i]],h=ot[ct[at[c-1]]],r=e.s*(n.t-h.t)+n.s*(h.t-e.t)+h.s*(e.t-n.t);if(!(1===ut[t]?0>=r:r>=0))break;S(s,e,n,h),i=at[--c];}at[c++]=i,at[c++]=t;}for(;c>1;){const t=at[--c];S(s,ot[ct[l-1]],ot[ct[t]],ot[ct[at[c-1]]]);}}function V(t,i){let s;for(let e=t.Z.next;e!==t.Z;e=s)s=e.next,e.h.D.p!==e.D.p?e.u=e.D.p?i:-i:t.delete(e);}function P(t,i){let s=0,e=-1;for(let n=i.j.o;n!==i.j;n=n.o){if(!n.p)continue;s||(t.H(4),s=1);let i=n.L;do{{const s=i.h&&i.h.D&&i.h.D.p?0:1;e!==s&&(e=s,t.K(!!e));}t.W(i.i.data),i=i.O;}while(i!==n.L)}s&&t.q();}function F(t,i){for(let s=i.j.next;s!==i.j;s=s.next){if(!s.p)continue;t.H(2);let i=s.L;do{t.W(i.i.data),i=i.O;}while(i!==s.L);t.q();}}var Y,j,H,z;(t=>{t[t.ODD=0]="ODD",t[t.NONZERO=1]="NONZERO",t[t.POSITIVE=2]="POSITIVE",t[t.NEGATIVE=3]="NEGATIVE",t[t.ABS_GEQ_TWO=4]="ABS_GEQ_TWO";})(Y||(Y={})),(t=>{t[t.X=0]="POLYGONS",t[t.J=1]="CONNECTED_POLYGONS",t[t.$=2]="BOUNDARY_CONTOURS";})(j||(j={})),(t=>{t[t.BEGIN=100100]="BEGIN",t[t.EDGE_FLAG=100104]="EDGE_FLAG",t[t.VERTEX=100101]="VERTEX",t[t.END=100102]="END",t[t.ERROR=100103]="ERROR",t[t.COMBINE=100105]="COMBINE",t[t.BEGIN_DATA=100106]="BEGIN_DATA",t[t.EDGE_FLAG_DATA=100110]="EDGE_FLAG_DATA",t[t.VERTEX_DATA=100107]="VERTEX_DATA",t[t.END_DATA=100108]="END_DATA",t[t.ERROR_DATA=100109]="ERROR_DATA",t[t.COMBINE_DATA=100111]="COMBINE_DATA",t[t.WINDING_RULE=100140]="WINDING_RULE",t[t.BOUNDARY_ONLY=100141]="BOUNDARY_ONLY",t[t.TOLERANCE=100142]="TOLERANCE";})(H||(H={})),(t=>{t[t.tt=100151]="MISSING_BEGIN_POLYGON",t[t.it=100152]="MISSING_BEGIN_CONTOUR",t[t.st=100153]="MISSING_END_POLYGON",t[t.et=100154]="MISSING_END_CONTOUR",t[t.nt=100155]="COORD_TOO_LARGE",t[t.ht=100156]="NEED_COMBINE_CALLBACK";})(z||(z={}));class Z{next;o;l=null;G=0;p=0;k=0;T=0;I=0}class K{next;i;h;C;O;D;N=null;u=0}class X{next;o;L;coords=[0,0,0];s=0;t=0;B=0;data=null}class Q{next;o;L;p=0}class J{Y;j;Z;rt;vertexCount=0;constructor(){const t=new X,i=new Q,s=new K,e=new K;t.next=t.o=t,i.next=i.o=i,s.next=s,s.h=e,e.next=e,e.h=s,this.Y=t,this.j=i,this.Z=s,this.rt=e;}lt(t){const i=new K,s=new K,e=t.h.next;return s.next=e,e.h.next=i,i.next=t,t.h.next=s,i.h=s,i.C=i,i.O=s,i.u=0,i.N=null,s.h=i,s.C=s,s.O=i,s.u=0,s.N=null,i}ot(t,i){const s=t.C,e=i.C;s.h.O=i,e.h.O=t,t.C=e,i.C=s;}ut(t,i,s){const e=t,n=s.o;e.o=n,n.next=e,e.next=s,s.o=e,e.L=i,++this.vertexCount;let h=i;do{h.i=e,h=h.C;}while(h!==i)}ct(t,i,s){const e=t,n=s.o;e.o=n,n.next=e,e.next=s,s.o=e,e.L=i,e.p=s.p;let h=i;do{h.D=e,h=h.O;}while(h!==i)}ft(t){const i=t.next,s=t.h.next;i.h.next=s,s.h.next=i;}dt(t,i){const s=t.L;let e=s;do{e.i=i,e=e.C;}while(e!==s);const n=t.o,h=t.next;h.o=n,n.next=h,--this.vertexCount;}wt(t,i){const s=t.L;let e=s;do{e.D=i,e=e.O;}while(e!==s);const n=t.o,h=t.next;h.o=n,n.next=h;}F(){const t=new X,i=new X,s=new Q,e=this.lt(this.Z);return this.ut(t,e,this.Y),this.ut(i,e.h,this.Y),this.ct(s,e,this.j),e}splice(t,i){let s=0,e=0;if(t!==i){if(i.i!==t.i&&(e=1,this.dt(i.i,t.i)),i.D!==t.D&&(s=1,this.wt(i.D,t.D)),this.ot(i,t),!e){const s=new X;this.ut(s,i,t.i),t.i.L=t;}if(!s){const s=new Q;this.ct(s,i,t.D),t.D.L=t;}}}delete(t){const i=t.h;let s=0;if(t.D!==t.h.D&&(s=1,this.wt(t.D,t.h.D)),t.C===t)this.dt(t.i,null);else if(t.h.D.L=t.h.O,t.i.L=t.C,this.ot(t,t.h.O),!s){const i=new Q;this.ct(i,t,t.D);}i.C===i?(this.dt(i.i,null),this.wt(i.D,null)):(t.D.L=i.h.O,i.i.L=i.C,this.ot(i,i.h.O)),this.ft(t);}Et(t){const i=this.lt(t),s=i.h;this.ot(i,t.O),i.i=t.h.i;const e=new X;return this.ut(e,s,i.i),i.D=s.D=t.D,i}V(t){const i=this.Et(t).h;return this.ot(t.h,t.h.h.O),this.ot(t.h,i),t.h.i=i.i,i.h.i.L=i.h,i.h.D=t.h.D,i.u=t.u,i.h.u=t.h.u,i}connect(t,i){let s=0;const e=this.lt(t),n=e.h;if(i.D!==t.D&&(s=1,this.wt(i.D,t.D)),this.ot(e,t.O),this.ot(n,i),e.i=t.h.i,n.i=i.i,e.D=n.D=t.D,t.D.L=n,!s){const i=new Q;this.ct(i,e,t.D);}return e}Nt(t){const i=t.L;let s,e,n,h,r;e=i.O;do{s=e,e=s.O,s.D=null,s.h.D||(s.C===s?this.dt(s.i,null):(s.i.L=s.C,this.ot(s,s.h.O)),n=s.h,n.C===n?this.dt(n.i,null):(n.i.L=n.C,this.ot(n,n.h.O)),this.ft(s));}while(s!=i);h=t.o,r=t.next,r.o=h,h.next=r;}_t(t){let i=t.L,s=0;do{s++,i=i.O;}while(i!==t.L);return s}check(){}}class ${max=0;At;It;gt;Ot=0;kt=0;size=0;constructor(t){this.max=t,this.At=new Int32Array(t+1),this.It=Array(t+1).fill(null),this.gt=new Int32Array(t+1),this.Ot=0,this.At[1]=1,this.It[1]=null;}reset(t){if(t+1>this.max)this.max=t,this.At=new Int32Array(t+1),this.It=Array(t+1).fill(null),this.gt=new Int32Array(t+1);else {const t=this.It;for(let i=1;this.size>=i;i++)t[i]=null;}this.size=0,this.kt=0,this.Ot=0,this.At[1]=1,this.It[1]=null;}yt(t){const i=this.At,s=this.It,e=this.gt;let n=i[t];for(;;){let h=t<<1;if(h>this.size)break;let r=h,l=i[h];if(this.size>=h+1){const t=i[h+1],e=s[t],n=s[l];(n.s>e.s||e.s===n.s&&n.t>=e.t)&&(r=h+1,l=t);}const o=s[n],u=s[l];if(u.s>o.s||o.s===u.s&&u.t>=o.t)break;i[t]=l,e[l]=t,t=r;}i[t]=n,e[n]=t;}Tt(t){const i=this.At,s=this.It,e=this.gt;let n=i[t];for(;;){const h=t>>1;if(0===h)break;const r=i[h],l=s[r],o=s[n];if(o.s>l.s||l.s===o.s&&o.t>=l.t)break;i[t]=r,e[r]=t,t=h;}i[t]=n,e[n]=t;}init(){for(let t=this.size>>1;t>=1;--t)this.yt(t);this.Ot=1;}bt(){return 0===this.size}min(){return 0===this.size?null:this.It[this.At[1]]}P(t){let i,s;if(i=++this.size,2*i>this.max){this.max*=2;const t=new Int32Array(this.max+1),i=new Int32Array(this.max+1),s=Array(this.max+1).fill(null);t.set(this.At),i.set(this.gt);for(let t=0;this.It.length>t;t++)s[t]=this.It[t];this.At=t,this.gt=i,this.It=s;}return 0===this.kt?s=i:(s=this.kt,this.kt=this.gt[s]),this.At[i]=s,this.gt[s]=i,this.It[s]=t,this.Ot&&this.Tt(i),s}Mt(){const t=this.At,i=this.It,s=this.gt;let e=t[1],n=i[e];return this.size>0&&(t[1]=t[this.size],s[t[1]]=1,i[e]=null,s[e]=this.kt,this.kt=e,--this.size,this.size>0&&this.yt(1)),n}delete(t){const i=this.At,s=this.It,e=this.gt;let n;if(n=e[t],i[n]=i[this.size],e[i[n]]=n,--this.size,this.size>=n)if(n>1){const t=s[i[n>>1]],e=s[i[n]];e.s>t.s||t.s===e.s&&e.t>=t.t?this.yt(n):this.Tt(n);}else this.yt(n);s[t]=null,e[t]=this.kt,this.kt=t;}}class tt{Dt;keys;order=null;size=0;max=0;Ot=0;Lt;constructor(t){this.max=t,this.size=0,this.Ot=0,this.Lt=128>=t,this.Dt=new $(t),this.Lt||(this.keys=Array(t).fill(null));}reset(t){this.Dt.reset(t),this.Lt=128>=t,this.Lt||this.keys&&this.max>=t||(this.keys=Array(t).fill(null)),t>this.max&&(this.max=t),this.size=0,this.Ot=0,this.order=null;}P(t){if(this.Lt||this.Ot)return this.Dt.P(t);const i=this.size;if(++this.size>=this.max){const t=this.max;this.max*=2;const i=Array(this.max).fill(null);for(let s=0;t>s;s++)i[s]=this.keys[s];this.keys=i;}return this.keys[i]=t,-(i+1)}init(){if(this.Lt)return this.Ot=1,this.Dt.init(),1;this.order=Array(this.size);for(let t=0;this.size>t;t++)this.order[t]=t;const t=this.keys;return this.order.sort((i,s)=>{const e=t[i],n=t[s];return n.s>e.s?1:e.s>n.s||e.t>n.t?-1:1}),this.max=this.size,this.Ot=1,this.Dt.init(),1}Mt(){if(this.Lt||0===this.size)return this.Dt.Mt();const t=this.keys[this.order[this.size-1]];if(!this.Dt.bt()){const s=this.Dt.min();if(s&&i(s,t))return this.Dt.Mt()}do{--this.size;}while(this.size>0&&null===this.keys[this.order[this.size-1]]);return t}min(){if(this.Lt||0===this.size)return this.Dt.min();const t=this.keys[this.order[this.size-1]];if(!this.Dt.bt()){const s=this.Dt.min();if(s&&i(s,t))return s}return t}delete(t){0>t?this.keys[-(t+1)]=null:this.Dt.delete(t);}bt(){return (this.Lt||0===this.size)&&this.Dt.bt()}}class it{head=new Z;frame;constructor(t){this.frame=t,this.head.next=this.head,this.head.o=this.head;}min(){return this.head.next}max(){return this.head.o}P(t){return this.insertBefore(this.head,t)}search(t){let i=this.head;do{i=i.next;}while(null!==i.l&&!a(this.frame,t,i));return i}insertBefore(t,i){do{t=t.o;}while(null!==t.l&&!a(this.frame,t,i));return i.next=t.next,t.next.o=i,i.o=t,t.next=i,i}delete(t){t.next.o=t.o,t.o.next=t.next;}}let st=null,et=null;const nt=[0,0,0,0],ht=[null,null,null,null],rt=[0,0,0];class lt{static Ct(t,i){t.u+=i.u,t.h.u+=i.h.u;}static Gt(t,s){let h,r,o;if(h=s.L,h.O===h||h.O.O===h)throw Error("Monotone region has degenerate topology");for(;i(h.h.i,h.i);h=h.C.h);for(;i(h.i,h.h.i);h=h.O);for(r=h.C.h;h.O!==r;)if(i(h.h.i,r.i)){for(;r.O!==h&&(e(r.O)||0>=l(r.i,r.h.i,r.O.h.i));)o=t.connect(r.O,r),r=o.h;r=r.C.h;}else {for(;r.O!==h&&(n(h.C.h)||l(h.h.i,h.i,h.C.h.i)>=0);)o=t.connect(h,h.C.h),h=o.h;h=h.O;}if(r.O===h)throw Error("Monotone region has insufficient vertices");for(;r.O.O!==h;)o=t.connect(r.O,r),r=o.h;return 1}static tessellateInterior(t){let i;for(let s=t.j.next;s!==t.j;s=i)i=s.next,s.p&<.Gt(t,s);return 1}}let ot=[],ut=new Int8Array(64),ct=new Int32Array(64),at=new Int32Array(64);var ft;(t=>{t[t.Rt=0]="T_DORMANT",t[t.vt=1]="T_IN_POLYGON",t[t.xt=2]="T_IN_CONTOUR";})(ft||(ft={}));class dt{state=ft.Rt;Ut=null;St=0;Bt=0;Vt=1;Pt=0;Ft=0;Yt=0;jt=0;A;Ht=[0,0,0];zt;qt;Wt;Zt;M=Y.ODD;Kt=Y.ODD;_;S;event;Xt;Qt;Jt;$t;ti;ii;R;si;m=null;U=0;gluTessCallback(t,i){const s=i||null;switch(t){case 100100:case 100106:this.Xt=s;break;case 100104:case 100110:this.$t=s,this.flagBoundary=1;break;case 100101:case 100107:this.Qt=s;break;case 100102:case 100108:this.Jt=s;break;case 100103:this.ti=s;break;case 100109:this.ii=s;break;case 100105:case 100111:this.R=s;break;case 100112:this.si=s;break;default:throw Error("GLU_INVALID_ENUM")}}gluTessProperty(t,i){switch(t){case 100140:{const t=i,s=100130>t?t:t-100130;if(0>s||s>4)throw Error("GLU_INVALID_VALUE");this.Kt=t,this.M=s;break}case 100141:this.ei=!!i;break;case 100142:break;default:throw Error("GLU_INVALID_ENUM")}}gluGetTessProperty(t){switch(t){case 100140:return this.Kt;case 100141:return this.ei;case 100142:return 0;default:throw Error("GLU_INVALID_ENUM")}}gluTessNormal(t,i,s){this.Ht[0]=t,this.Ht[1]=i,this.Ht[2]=s,0===s||t||i||(this.Vt=s>0?1:-1);}H(t){this.Xt&&this.Xt(t,this.m);}W(t){this.Qt&&this.Qt(t,this.m);}q(){this.Jt&&this.Jt(this.m);}K(t){this.$t&&this.$t(t,this.m);}v(t){this.ii?this.ii(t,this.m):this.ti&&this.ti(t);}ni(t){if(this.state!==t)for(;this.state!==t;)t>this.state?this.state===ft.Rt?(this.v(100151),this.gluTessBeginPolygon()):this.state===ft.vt&&(this.v(100152),this.gluTessBeginContour()):this.state===ft.xt?(this.v(100154),this.gluTessEndContour()):this.state===ft.vt&&(this.v(100153),this.gluTessEndPolygon());}ei=0;flagBoundary=0;hi(){const t=this.A,i=t.Y,s=this.Ht[0],e=this.Ht[1],n=this.Ht[2];let h,r;if(this.zt||(this.zt=[0,0,0]),this.qt||(this.qt=[0,0,0]),this.Wt||(this.Wt=[0,0]),this.Zt||(this.Zt=[0,0]),h=this.zt,r=this.qt,this.Bt){h[0]=1,h[1]=0,h[2]=0;const t=n>0?1:-1;return r[0]=0,r[1]=t,r[2]=0,this.Wt[0]=this.Pt,this.Wt[1]=this.Yt,this.Zt[0]=this.Ft,void(this.Zt[1]=this.jt)}if(!this.St&&!s&&!e){let s;h[0]=1,h[1]=0,h[2]=0;let e=0;if(n)s=n>0?1:-1;else {const i=[0,0,0];v(t,i),s=i[2]>0?1:-1,e=1;}r[0]=0,r[1]=s,r[2]=0;let l=i.next,o=l.coords[0],u=l.coords[1]*s;l.s=o,l.t=u;let c=o,a=o,f=u,d=u;for(l=l.next;l!==i;l=l.next)o=l.coords[0],u=l.coords[1]*s,l.s=o,l.t=u,c>o?c=o:o>a&&(a=o),f>u?f=u:u>d&&(d=u);return this.Wt[0]=c,this.Zt[0]=a,void(e?(x(t,this.qt),r[1]!==s?(this.Wt[1]=-d,this.Zt[1]=-f):(this.Wt[1]=f,this.Zt[1]=d)):(this.Wt[1]=f,this.Zt[1]=d))}let l=0;const o=[s,e,n];s||e||n||(v(t,o),l=1);const u=(t=>{let i=0;return Math.abs(t[1])>Math.abs(t[0])&&(i=1),Math.abs(t[2])>Math.abs(t[i])&&(i=2),i})(o);h[u]=0,h[(u+1)%3]=1,h[(u+2)%3]=0,r[u]=0,r[(u+1)%3]=0,r[(u+2)%3]=o[u]>0?1:-1;let c=i.next;c.s=c.coords[0]*h[0]+c.coords[1]*h[1]+c.coords[2]*h[2],c.t=c.coords[0]*r[0]+c.coords[1]*r[1]+c.coords[2]*r[2];let a=c.s,f=c.s,d=c.t,w=c.t;for(c=c.next;c!==i;c=c.next){const t=c.coords[0]*h[0]+c.coords[1]*h[1]+c.coords[2]*h[2],i=c.coords[0]*r[0]+c.coords[1]*r[1]+c.coords[2]*r[2];c.s=t,c.t=i,a>t?a=t:t>f&&(f=t),d>i?d=i:i>w&&(w=i);}l&&x(t,this.qt),l&&r[(u+2)%3]!==(o[u]>0?1:-1)?(this.Wt[0]=a,this.Zt[0]=f,this.Wt[1]=-w,this.Zt[1]=-d):(this.Wt[0]=a,this.Zt[0]=f,this.Wt[1]=d,this.Zt[1]=w);}gluTessBeginPolygon(t){this.ni(ft.Rt),this.state=ft.vt,this.U=0,this.A=new J,this.St=0,this.Bt=0!==this.Ht[2]&&!this.Ht[0]&&!this.Ht[1],this.Pt=1/0,this.Ft=-1/0,this.Yt=1/0,this.jt=-1/0,this.m=t;}gluTessBeginContour(){this.ni(ft.vt),this.state=ft.xt,this.Ut=null;}gluTessVertex(t,i){this.ni(ft.xt);let s=t[0],e=t[1],n=t.length>2?t[2]:0,h=0;-1e150>s?(s=-1e150,h=1):s>1e150&&(s=1e150,h=1),-1e150>e?(e=-1e150,h=1):e>1e150&&(e=1e150,h=1),-1e150>n?(n=-1e150,h=1):n>1e150&&(n=1e150,h=1),h&&this.v(100155);let r=this.Ut;if(null===r?(r=this.A.F(),this.A.splice(r,r.h)):(this.A.V(r),r=r.O),r.i.data=i||null,r.i.coords[0]=s,r.i.coords[1]=e,0!==n?(r.i.coords[2]=n,this.St=1,this.Bt=0):r.i.coords[2]=0,this.Bt){r.i.s=s;const t=e*this.Vt;r.i.t=t,this.Pt>s&&(this.Pt=s),s>this.Ft&&(this.Ft=s),this.Yt>t&&(this.Yt=t),t>this.jt&&(this.jt=t);}r.u=1,r.h.u=-1,this.Ut=r;}gluTessEndContour(){this.ni(ft.xt),this.state=ft.vt;}gluTessEndPolygon(){this.ni(ft.vt),this.state=ft.Rt,this.compute(this.M,void 0,0);const t=this.A;this.U||(this.ei?(V(t,1),F(this,t)):this.flagBoundary?(lt.tessellateInterior(t),P(this,t)):U(this,t)),this.si&&this.si(t),this.A=null,this.Ut=null,this.event=null,this.m=null,this._=null;}gluDeleteTess(){this.ni(ft.Rt);}compute(i=Y.ODD,s,e=0){this.state!==ft.Rt&&this.state===ft.vt&&(this.state=ft.Rt),this.A||(this.A=new J),s&&(this.Ht[0]=s[0],this.Ht[1]=s[1],this.Ht[2]=s[2]),this.M=i,this.hi(),function(i,s=1){let e,n;if((i=>{let s,e,n,h=i.A.Z;for(s=h.next;s!==h;s=e)e=s.next,n=s.O,t(s.i,s.h.i)&&s.O.O!==s&&(b(i,n,s),i.A.delete(s),s=n,n=s.O),n.O===s&&(n!==s&&(n!==e&&n!==e.h||(e=e.next),i.A.delete(n)),s!==e&&s!==e.h||(e=e.next),i.A.delete(s));})(i),!(t=>{let i,s,e,n=t.A.vertexCount+8;for(t.S?(t.S.reset(n),i=t.S):i=t.S=new tt(n),e=t.A.Y,s=e.next;s!==e;s=s.next)s.B=i.P(s);return s!==e?0:(i.init(),1)})(i))return 0;for((t=>{t._=new it(t);let i=t.Zt[0]-t.Wt[0],s=t.Zt[1]-t.Wt[1],e=t.Wt[0]-i,n=t.Zt[0]+i,h=t.Zt[1]+s;m(t,e,n,t.Wt[1]-s),m(t,e,n,h);})(i);null!==(e=i.S.Mt());){for(;n=i.S.min(),null!==n&&t(n,e);)n=i.S.Mt(),b(i,e.L,n.L);R(i,e);}i.event=i._.min().l.i,(t=>{let i;for(;null!==(i=t._.min()).l;)E(t,i);})(i),((t,i)=>{let s,e,n;for(s=i.j.next;s!==i.j;s=e)e=s.next,n=s.L,n.O.O===n&&(w(n.C,n),t.A.delete(n));})(i,i.A),s&&i.A.check();}(this,e);}renderBoundary(){this.A&&(V(this.A,1),F(this,this.A));}renderTriangles(t=0){this.A&&(t?(lt.tessellateInterior(this.A),P(this,this.A)):U(this,this.A));}}
|
|
331
|
+
|
|
332
|
+
class Tessellator {
|
|
333
|
+
process(paths, removeOverlaps = true, isCFF = false, needsExtrusionContours = true) {
|
|
334
|
+
if (paths.length === 0) {
|
|
335
|
+
return { triangles: { vertices: [], indices: [] }, contours: [] };
|
|
336
|
+
}
|
|
337
|
+
const valid = paths.filter((path) => path.points.length >= 3);
|
|
338
|
+
if (valid.length === 0) {
|
|
339
|
+
return { triangles: { vertices: [], indices: [] }, contours: [] };
|
|
340
|
+
}
|
|
341
|
+
logger.log(`Tessellator: removeOverlaps=${removeOverlaps}, processing ${valid.length} paths`);
|
|
342
|
+
return this.tessellate(valid, removeOverlaps, isCFF, needsExtrusionContours);
|
|
343
|
+
}
|
|
344
|
+
processContours(contours, removeOverlaps = true, isCFF = false, needsExtrusionContours = true) {
|
|
345
|
+
if (contours.length === 0) {
|
|
346
|
+
return { triangles: { vertices: [], indices: [] }, contours: [] };
|
|
347
|
+
}
|
|
348
|
+
return this.tessellateContours(contours, removeOverlaps, isCFF, needsExtrusionContours);
|
|
349
|
+
}
|
|
350
|
+
tessellate(paths, removeOverlaps, isCFF, needsExtrusionContours) {
|
|
351
|
+
// libtess expects CCW winding; TTF outer contours are CW
|
|
352
|
+
const needsWindingReversal = !isCFF && !removeOverlaps;
|
|
353
|
+
let originalContours;
|
|
354
|
+
let tessContours;
|
|
355
|
+
if (needsWindingReversal) {
|
|
356
|
+
tessContours = this.pathsToContours(paths, true);
|
|
357
|
+
if (removeOverlaps || needsExtrusionContours) {
|
|
358
|
+
originalContours = this.pathsToContours(paths);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
originalContours = this.pathsToContours(paths);
|
|
363
|
+
tessContours = originalContours;
|
|
364
|
+
}
|
|
365
|
+
let extrusionContours = needsExtrusionContours
|
|
366
|
+
? needsWindingReversal
|
|
367
|
+
? tessContours
|
|
368
|
+
: (originalContours ?? this.pathsToContours(paths))
|
|
369
|
+
: [];
|
|
370
|
+
if (removeOverlaps) {
|
|
371
|
+
logger.log('Two-pass: boundary extraction then triangulation');
|
|
372
|
+
perfLogger.start('Tessellator.boundaryPass', {
|
|
373
|
+
contourCount: tessContours.length
|
|
374
|
+
});
|
|
375
|
+
const boundaryResult = this.performTessellation(originalContours, 'boundary');
|
|
376
|
+
perfLogger.end('Tessellator.boundaryPass');
|
|
377
|
+
if (!boundaryResult) {
|
|
378
|
+
logger.warn('libtess returned empty result from boundary pass');
|
|
379
|
+
return { triangles: { vertices: [], indices: [] }, contours: [] };
|
|
380
|
+
}
|
|
381
|
+
// Boundary pass normalizes winding (outer CCW, holes CW)
|
|
382
|
+
tessContours = this.boundaryToContours(boundaryResult);
|
|
383
|
+
if (needsExtrusionContours) {
|
|
384
|
+
extrusionContours = tessContours;
|
|
385
|
+
}
|
|
386
|
+
logger.log(`Boundary pass created ${tessContours.length} contours. Starting triangulation pass.`);
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
logger.log(`Single-pass triangulation for ${isCFF ? 'CFF' : 'TTF'}`);
|
|
390
|
+
}
|
|
391
|
+
perfLogger.start('Tessellator.triangulationPass', {
|
|
392
|
+
contourCount: tessContours.length
|
|
393
|
+
});
|
|
394
|
+
const triangleResult = this.performTessellation(tessContours, 'triangles');
|
|
395
|
+
perfLogger.end('Tessellator.triangulationPass');
|
|
396
|
+
if (!triangleResult) {
|
|
397
|
+
const warning = removeOverlaps
|
|
398
|
+
? 'libtess returned empty result from triangulation pass'
|
|
399
|
+
: 'libtess returned empty result from single-pass triangulation';
|
|
400
|
+
logger.warn(warning);
|
|
401
|
+
return {
|
|
402
|
+
triangles: { vertices: [], indices: [] },
|
|
403
|
+
contours: extrusionContours
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
return {
|
|
407
|
+
triangles: {
|
|
408
|
+
vertices: triangleResult.vertices,
|
|
409
|
+
indices: triangleResult.indices || []
|
|
410
|
+
},
|
|
411
|
+
contours: extrusionContours,
|
|
412
|
+
contoursAreBoundary: removeOverlaps
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
tessellateContours(contours, removeOverlaps, isCFF, needsExtrusionContours) {
|
|
416
|
+
const needsWindingReversal = !isCFF && !removeOverlaps;
|
|
417
|
+
let originalContours;
|
|
418
|
+
let tessContours;
|
|
419
|
+
if (needsWindingReversal) {
|
|
420
|
+
tessContours = this.reverseContours(contours);
|
|
421
|
+
if (removeOverlaps || needsExtrusionContours) {
|
|
422
|
+
originalContours = contours;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
originalContours = contours;
|
|
427
|
+
tessContours = contours;
|
|
428
|
+
}
|
|
429
|
+
let extrusionContours = needsExtrusionContours
|
|
430
|
+
? needsWindingReversal
|
|
431
|
+
? tessContours
|
|
432
|
+
: (originalContours ?? contours)
|
|
433
|
+
: [];
|
|
434
|
+
if (removeOverlaps) {
|
|
435
|
+
logger.log('Two-pass: boundary extraction then triangulation');
|
|
436
|
+
perfLogger.start('Tessellator.boundaryPass', {
|
|
437
|
+
contourCount: tessContours.length
|
|
438
|
+
});
|
|
439
|
+
const boundaryResult = this.performTessellation(originalContours, 'boundary');
|
|
440
|
+
perfLogger.end('Tessellator.boundaryPass');
|
|
441
|
+
if (!boundaryResult) {
|
|
442
|
+
logger.warn('libtess returned empty result from boundary pass');
|
|
443
|
+
return { triangles: { vertices: [], indices: [] }, contours: [] };
|
|
444
|
+
}
|
|
445
|
+
tessContours = this.boundaryToContours(boundaryResult);
|
|
446
|
+
if (needsExtrusionContours) {
|
|
447
|
+
extrusionContours = tessContours;
|
|
448
|
+
}
|
|
449
|
+
logger.log(`Boundary pass created ${tessContours.length} contours. Starting triangulation pass.`);
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
logger.log(`Single-pass triangulation for ${isCFF ? 'CFF' : 'TTF'}`);
|
|
453
|
+
}
|
|
454
|
+
perfLogger.start('Tessellator.triangulationPass', {
|
|
455
|
+
contourCount: tessContours.length
|
|
456
|
+
});
|
|
457
|
+
const triangleResult = this.performTessellation(tessContours, 'triangles');
|
|
458
|
+
perfLogger.end('Tessellator.triangulationPass');
|
|
459
|
+
if (!triangleResult) {
|
|
460
|
+
const warning = removeOverlaps
|
|
461
|
+
? 'libtess returned empty result from triangulation pass'
|
|
462
|
+
: 'libtess returned empty result from single-pass triangulation';
|
|
463
|
+
logger.warn(warning);
|
|
464
|
+
return {
|
|
465
|
+
triangles: { vertices: [], indices: [] },
|
|
466
|
+
contours: extrusionContours
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
return {
|
|
470
|
+
triangles: {
|
|
471
|
+
vertices: triangleResult.vertices,
|
|
472
|
+
indices: triangleResult.indices || []
|
|
473
|
+
},
|
|
474
|
+
contours: extrusionContours,
|
|
475
|
+
contoursAreBoundary: removeOverlaps
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
pathsToContours(paths, reversePoints = false) {
|
|
479
|
+
const contours = new Array(paths.length);
|
|
480
|
+
for (let p = 0; p < paths.length; p++) {
|
|
481
|
+
const points = paths[p].points;
|
|
482
|
+
const pointCount = points.length;
|
|
483
|
+
// Clipper-style paths can be explicitly closed by repeating the first point at the end
|
|
484
|
+
// Normalize to a single closing vertex for stable side wall generation
|
|
485
|
+
const isClosed = pointCount > 1 &&
|
|
486
|
+
points[0].x === points[pointCount - 1].x &&
|
|
487
|
+
points[0].y === points[pointCount - 1].y;
|
|
488
|
+
const end = isClosed ? pointCount - 1 : pointCount;
|
|
489
|
+
// +1 to append a closing vertex
|
|
490
|
+
const contour = new Array((end + 1) * 2);
|
|
491
|
+
let i = 0;
|
|
492
|
+
if (reversePoints) {
|
|
493
|
+
for (let k = end - 1; k >= 0; k--) {
|
|
494
|
+
const pt = points[k];
|
|
495
|
+
contour[i++] = pt.x;
|
|
496
|
+
contour[i++] = pt.y;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
for (let k = 0; k < end; k++) {
|
|
501
|
+
const pt = points[k];
|
|
502
|
+
contour[i++] = pt.x;
|
|
503
|
+
contour[i++] = pt.y;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
// Some glyphs omit closePath, leaving gaps in extruded side walls
|
|
507
|
+
if (i >= 2) {
|
|
508
|
+
contour[i++] = contour[0];
|
|
509
|
+
contour[i++] = contour[1];
|
|
510
|
+
}
|
|
511
|
+
contours[p] = contour;
|
|
512
|
+
}
|
|
513
|
+
return contours;
|
|
514
|
+
}
|
|
515
|
+
reverseContours(contours) {
|
|
516
|
+
const reversed = new Array(contours.length);
|
|
517
|
+
for (let i = 0; i < contours.length; i++) {
|
|
518
|
+
reversed[i] = this.reverseContour(contours[i]);
|
|
519
|
+
}
|
|
520
|
+
return reversed;
|
|
521
|
+
}
|
|
522
|
+
reverseContour(contour) {
|
|
523
|
+
const len = contour.length;
|
|
524
|
+
if (len === 0)
|
|
525
|
+
return [];
|
|
526
|
+
const isClosed = len >= 4 &&
|
|
527
|
+
contour[0] === contour[len - 2] &&
|
|
528
|
+
contour[1] === contour[len - 1];
|
|
529
|
+
const end = isClosed ? len - 2 : len;
|
|
530
|
+
if (end === 0)
|
|
531
|
+
return [];
|
|
532
|
+
const reversed = new Array(end + 2);
|
|
533
|
+
let out = 0;
|
|
534
|
+
for (let i = end - 2; i >= 0; i -= 2) {
|
|
535
|
+
reversed[out++] = contour[i];
|
|
536
|
+
reversed[out++] = contour[i + 1];
|
|
537
|
+
}
|
|
538
|
+
if (out >= 2) {
|
|
539
|
+
reversed[out++] = reversed[0];
|
|
540
|
+
reversed[out++] = reversed[1];
|
|
541
|
+
}
|
|
542
|
+
return reversed;
|
|
543
|
+
}
|
|
544
|
+
performTessellation(contours, mode) {
|
|
545
|
+
const tess = new dt();
|
|
546
|
+
tess.gluTessProperty(H.WINDING_RULE, Y.NONZERO);
|
|
547
|
+
const vertices = [];
|
|
548
|
+
const indices = [];
|
|
549
|
+
const contourIndices = [];
|
|
550
|
+
let currentContour = [];
|
|
551
|
+
if (mode === 'boundary') {
|
|
552
|
+
tess.gluTessProperty(H.BOUNDARY_ONLY, 1);
|
|
553
|
+
}
|
|
554
|
+
if (mode === 'triangles') {
|
|
555
|
+
tess.gluTessCallback(H.VERTEX_DATA, (data) => {
|
|
556
|
+
indices.push(data);
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
tess.gluTessCallback(H.BEGIN, () => {
|
|
561
|
+
currentContour = [];
|
|
562
|
+
});
|
|
563
|
+
tess.gluTessCallback(H.VERTEX_DATA, (data) => {
|
|
564
|
+
currentContour.push(data);
|
|
565
|
+
});
|
|
566
|
+
tess.gluTessCallback(H.END, () => {
|
|
567
|
+
if (currentContour.length > 0) {
|
|
568
|
+
contourIndices.push(currentContour);
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
tess.gluTessCallback(H.COMBINE, (coords) => {
|
|
573
|
+
const idx = vertices.length / 2;
|
|
574
|
+
vertices.push(coords[0], coords[1]);
|
|
575
|
+
return idx;
|
|
576
|
+
});
|
|
577
|
+
tess.gluTessCallback(H.ERROR, (errno) => {
|
|
578
|
+
logger.warn(`libtess error: ${errno}`);
|
|
579
|
+
});
|
|
580
|
+
tess.gluTessNormal(0, 0, 1);
|
|
581
|
+
tess.gluTessBeginPolygon();
|
|
582
|
+
for (const contour of contours) {
|
|
583
|
+
tess.gluTessBeginContour();
|
|
584
|
+
for (let i = 0; i < contour.length; i += 2) {
|
|
585
|
+
const idx = vertices.length / 2;
|
|
586
|
+
vertices.push(contour[i], contour[i + 1]);
|
|
587
|
+
tess.gluTessVertex([contour[i], contour[i + 1]], idx);
|
|
588
|
+
}
|
|
589
|
+
tess.gluTessEndContour();
|
|
590
|
+
}
|
|
591
|
+
tess.gluTessEndPolygon();
|
|
592
|
+
if (vertices.length === 0) {
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
if (mode === 'triangles') {
|
|
596
|
+
return { vertices, indices };
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
return { vertices, contourIndices };
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
boundaryToContours(boundaryResult) {
|
|
603
|
+
if (!boundaryResult.contourIndices) {
|
|
604
|
+
return [];
|
|
605
|
+
}
|
|
606
|
+
const contours = [];
|
|
607
|
+
for (const indices of boundaryResult.contourIndices) {
|
|
608
|
+
const contour = [];
|
|
609
|
+
for (const idx of indices) {
|
|
610
|
+
const vertIdx = idx * 2;
|
|
611
|
+
contour.push(boundaryResult.vertices[vertIdx], boundaryResult.vertices[vertIdx + 1]);
|
|
612
|
+
}
|
|
613
|
+
if (contour.length > 2) {
|
|
614
|
+
if (contour[0] !== contour[contour.length - 2] ||
|
|
615
|
+
contour[1] !== contour[contour.length - 1]) {
|
|
616
|
+
contour.push(contour[0], contour[1]);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
contours.push(contour);
|
|
620
|
+
}
|
|
621
|
+
return contours;
|
|
622
|
+
}
|
|
623
|
+
// Check if contours need winding normalization via boundary pass
|
|
624
|
+
// Returns false if topology is simple enough to skip the expensive pass
|
|
625
|
+
needsWindingNormalization(contours) {
|
|
626
|
+
if (contours.length === 0)
|
|
627
|
+
return false;
|
|
628
|
+
// Heuristic 1: Single contour never needs normalization
|
|
629
|
+
if (contours.length === 1)
|
|
630
|
+
return false;
|
|
631
|
+
// Heuristic 2: All same winding = all outers, no holes
|
|
632
|
+
// Compute signed areas
|
|
633
|
+
let firstSign = null;
|
|
634
|
+
for (const contour of contours) {
|
|
635
|
+
const area = this.signedArea(contour);
|
|
636
|
+
const sign = area >= 0 ? 1 : -1;
|
|
637
|
+
if (firstSign === null) {
|
|
638
|
+
firstSign = sign;
|
|
639
|
+
}
|
|
640
|
+
else if (sign !== firstSign) {
|
|
641
|
+
// Mixed winding detected → might have holes or complex topology
|
|
642
|
+
return true;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
// All same winding → simple topology, no normalization needed
|
|
646
|
+
return false;
|
|
647
|
+
}
|
|
648
|
+
// Compute signed area (CCW = positive, CW = negative)
|
|
649
|
+
signedArea(contour) {
|
|
650
|
+
let area = 0;
|
|
651
|
+
const len = contour.length;
|
|
652
|
+
if (len < 6)
|
|
653
|
+
return 0; // Need at least 3 points
|
|
654
|
+
for (let i = 0; i < len; i += 2) {
|
|
655
|
+
const x1 = contour[i];
|
|
656
|
+
const y1 = contour[i + 1];
|
|
657
|
+
const x2 = contour[(i + 2) % len];
|
|
658
|
+
const y2 = contour[(i + 3) % len];
|
|
659
|
+
area += x1 * y2 - x2 * y1;
|
|
660
|
+
}
|
|
661
|
+
return area / 2;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
class Extruder {
|
|
666
|
+
constructor() { }
|
|
667
|
+
extrude(geometry, depth = 0, unitsPerEm) {
|
|
668
|
+
const points = geometry.triangles.vertices;
|
|
669
|
+
const triangleIndices = geometry.triangles.indices;
|
|
670
|
+
const contours = geometry.contours;
|
|
671
|
+
const contoursAreBoundary = geometry.contoursAreBoundary === true;
|
|
672
|
+
const pointLen = points.length;
|
|
673
|
+
const numPoints = pointLen / 2;
|
|
674
|
+
// Use boundary contours for side walls when available
|
|
675
|
+
let boundaryEdges = [];
|
|
676
|
+
let sideEdgeCount = 0;
|
|
677
|
+
let useContours = false;
|
|
678
|
+
if (depth !== 0) {
|
|
679
|
+
if (contoursAreBoundary && contours.length > 0) {
|
|
680
|
+
useContours = true;
|
|
681
|
+
for (const contour of contours) {
|
|
682
|
+
const contourPointCount = contour.length >> 1;
|
|
683
|
+
if (contourPointCount >= 2) {
|
|
684
|
+
sideEdgeCount += contourPointCount - 1;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
else {
|
|
689
|
+
// Use a directionless key (min/max) to detect shared edges
|
|
690
|
+
// Store the directed edge (a->b) and mark as null when seen twice
|
|
691
|
+
const edgeMap = new Map();
|
|
692
|
+
const triLen = triangleIndices.length;
|
|
693
|
+
for (let i = 0; i < triLen; i += 3) {
|
|
694
|
+
const a = triangleIndices[i];
|
|
695
|
+
const b = triangleIndices[i + 1];
|
|
696
|
+
const c = triangleIndices[i + 2];
|
|
697
|
+
let key, packed;
|
|
698
|
+
if (a < b) {
|
|
699
|
+
key = (a << 16) | b;
|
|
700
|
+
}
|
|
701
|
+
else {
|
|
702
|
+
key = (b << 16) | a;
|
|
703
|
+
}
|
|
704
|
+
packed = (a << 16) | b;
|
|
705
|
+
let data = edgeMap.get(key);
|
|
706
|
+
if (data === undefined) {
|
|
707
|
+
edgeMap.set(key, packed);
|
|
708
|
+
}
|
|
709
|
+
else if (data !== null) {
|
|
710
|
+
edgeMap.set(key, null);
|
|
711
|
+
}
|
|
712
|
+
if (b < c) {
|
|
713
|
+
key = (b << 16) | c;
|
|
714
|
+
}
|
|
715
|
+
else {
|
|
716
|
+
key = (c << 16) | b;
|
|
717
|
+
}
|
|
718
|
+
packed = (b << 16) | c;
|
|
719
|
+
data = edgeMap.get(key);
|
|
720
|
+
if (data === undefined) {
|
|
721
|
+
edgeMap.set(key, packed);
|
|
722
|
+
}
|
|
723
|
+
else if (data !== null) {
|
|
724
|
+
edgeMap.set(key, null);
|
|
725
|
+
}
|
|
726
|
+
if (c < a) {
|
|
727
|
+
key = (c << 16) | a;
|
|
728
|
+
}
|
|
729
|
+
else {
|
|
730
|
+
key = (a << 16) | c;
|
|
731
|
+
}
|
|
732
|
+
packed = (c << 16) | a;
|
|
733
|
+
data = edgeMap.get(key);
|
|
734
|
+
if (data === undefined) {
|
|
735
|
+
edgeMap.set(key, packed);
|
|
736
|
+
}
|
|
737
|
+
else if (data !== null) {
|
|
738
|
+
edgeMap.set(key, null);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
boundaryEdges = [];
|
|
742
|
+
for (const packedEdge of edgeMap.values()) {
|
|
743
|
+
if (packedEdge === null)
|
|
744
|
+
continue;
|
|
745
|
+
boundaryEdges.push(packedEdge >>> 16, packedEdge & 0xffff);
|
|
746
|
+
}
|
|
747
|
+
sideEdgeCount = boundaryEdges.length >> 1;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
const sideVertexCount = sideEdgeCount * 4;
|
|
751
|
+
const baseVertexCount = depth === 0 ? numPoints : numPoints * 2;
|
|
752
|
+
const vertexCount = baseVertexCount + sideVertexCount;
|
|
753
|
+
const vertices = new Float32Array(vertexCount * 3);
|
|
754
|
+
const normals = new Float32Array(vertexCount * 3);
|
|
755
|
+
const indexCount = depth === 0
|
|
756
|
+
? triangleIndices.length
|
|
757
|
+
: triangleIndices.length * 2 + sideEdgeCount * 6;
|
|
758
|
+
const indices = new Uint32Array(indexCount);
|
|
759
|
+
if (depth === 0) {
|
|
760
|
+
for (let p = 0, vPos = 0; p < pointLen; p += 2, vPos += 3) {
|
|
761
|
+
vertices[vPos] = points[p];
|
|
762
|
+
vertices[vPos + 1] = points[p + 1];
|
|
763
|
+
vertices[vPos + 2] = 0;
|
|
764
|
+
normals[vPos] = 0;
|
|
765
|
+
normals[vPos + 1] = 0;
|
|
766
|
+
normals[vPos + 2] = 1;
|
|
767
|
+
}
|
|
768
|
+
indices.set(triangleIndices);
|
|
769
|
+
return { vertices, normals, indices };
|
|
770
|
+
}
|
|
771
|
+
const minBackOffset = unitsPerEm * 0.000025;
|
|
772
|
+
const backZ = depth <= minBackOffset ? minBackOffset : depth;
|
|
773
|
+
const backOffset = numPoints * 3;
|
|
774
|
+
for (let p = 0, vi = 0, base0 = 0; p < pointLen; p += 2, vi++, base0 += 3) {
|
|
775
|
+
const x = points[p];
|
|
776
|
+
const y = points[p + 1];
|
|
777
|
+
// Cap at z=0
|
|
778
|
+
vertices[base0] = x;
|
|
779
|
+
vertices[base0 + 1] = y;
|
|
780
|
+
vertices[base0 + 2] = 0;
|
|
781
|
+
normals[base0] = 0;
|
|
782
|
+
normals[base0 + 1] = 0;
|
|
783
|
+
normals[base0 + 2] = -1;
|
|
784
|
+
// Cap at z=depth
|
|
785
|
+
const baseD = base0 + backOffset;
|
|
786
|
+
vertices[baseD] = x;
|
|
787
|
+
vertices[baseD + 1] = y;
|
|
788
|
+
vertices[baseD + 2] = backZ;
|
|
789
|
+
normals[baseD] = 0;
|
|
790
|
+
normals[baseD + 1] = 0;
|
|
791
|
+
normals[baseD + 2] = 1;
|
|
792
|
+
}
|
|
793
|
+
// Front cap faces -Z, reverse winding from libtess CCW output
|
|
794
|
+
const triLen = triangleIndices.length;
|
|
795
|
+
for (let i = 0; i < triLen; i++) {
|
|
796
|
+
indices[i] = triangleIndices[triLen - 1 - i];
|
|
797
|
+
}
|
|
798
|
+
// Back cap faces +Z, use original winding
|
|
799
|
+
for (let i = 0; i < triLen; i++) {
|
|
800
|
+
indices[triLen + i] = triangleIndices[i] + numPoints;
|
|
801
|
+
}
|
|
802
|
+
let nextVertex = numPoints * 2;
|
|
803
|
+
let idxPos = triLen * 2;
|
|
804
|
+
if (useContours) {
|
|
805
|
+
for (const contour of contours) {
|
|
806
|
+
const contourLen = contour.length;
|
|
807
|
+
if (contourLen < 4)
|
|
808
|
+
continue;
|
|
809
|
+
for (let i = 0; i < contourLen - 2; i += 2) {
|
|
810
|
+
const p0x = contour[i];
|
|
811
|
+
const p0y = contour[i + 1];
|
|
812
|
+
const p1x = contour[i + 2];
|
|
813
|
+
const p1y = contour[i + 3];
|
|
814
|
+
const ex = p1x - p0x;
|
|
815
|
+
const ey = p1y - p0y;
|
|
816
|
+
const lenSq = ex * ex + ey * ey;
|
|
817
|
+
let nx = 0;
|
|
818
|
+
let ny = 0;
|
|
819
|
+
if (lenSq > 1e-10) {
|
|
820
|
+
const invLen = 1 / Math.sqrt(lenSq);
|
|
821
|
+
nx = ey * invLen;
|
|
822
|
+
ny = -ex * invLen;
|
|
823
|
+
}
|
|
824
|
+
const base = nextVertex * 3;
|
|
825
|
+
vertices[base] = p0x;
|
|
826
|
+
vertices[base + 1] = p0y;
|
|
827
|
+
vertices[base + 2] = 0;
|
|
828
|
+
vertices[base + 3] = p1x;
|
|
829
|
+
vertices[base + 4] = p1y;
|
|
830
|
+
vertices[base + 5] = 0;
|
|
831
|
+
vertices[base + 6] = p0x;
|
|
832
|
+
vertices[base + 7] = p0y;
|
|
833
|
+
vertices[base + 8] = backZ;
|
|
834
|
+
vertices[base + 9] = p1x;
|
|
835
|
+
vertices[base + 10] = p1y;
|
|
836
|
+
vertices[base + 11] = backZ;
|
|
837
|
+
normals[base] = nx;
|
|
838
|
+
normals[base + 1] = ny;
|
|
839
|
+
normals[base + 2] = 0;
|
|
840
|
+
normals[base + 3] = nx;
|
|
841
|
+
normals[base + 4] = ny;
|
|
842
|
+
normals[base + 5] = 0;
|
|
843
|
+
normals[base + 6] = nx;
|
|
844
|
+
normals[base + 7] = ny;
|
|
845
|
+
normals[base + 8] = 0;
|
|
846
|
+
normals[base + 9] = nx;
|
|
847
|
+
normals[base + 10] = ny;
|
|
848
|
+
normals[base + 11] = 0;
|
|
849
|
+
const baseVertex = nextVertex;
|
|
850
|
+
indices[idxPos] = baseVertex;
|
|
851
|
+
indices[idxPos + 1] = baseVertex + 1;
|
|
852
|
+
indices[idxPos + 2] = baseVertex + 2;
|
|
853
|
+
indices[idxPos + 3] = baseVertex + 1;
|
|
854
|
+
indices[idxPos + 4] = baseVertex + 3;
|
|
855
|
+
indices[idxPos + 5] = baseVertex + 2;
|
|
856
|
+
idxPos += 6;
|
|
857
|
+
nextVertex += 4;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
else {
|
|
862
|
+
for (let e = 0; e < sideEdgeCount; e++) {
|
|
863
|
+
const edgeIndex = e << 1;
|
|
864
|
+
const u = boundaryEdges[edgeIndex];
|
|
865
|
+
const v = boundaryEdges[edgeIndex + 1];
|
|
866
|
+
const u2 = u << 1;
|
|
867
|
+
const v2 = v << 1;
|
|
868
|
+
const p0x = points[u2];
|
|
869
|
+
const p0y = points[u2 + 1];
|
|
870
|
+
const p1x = points[v2];
|
|
871
|
+
const p1y = points[v2 + 1];
|
|
872
|
+
const ex = p1x - p0x;
|
|
873
|
+
const ey = p1y - p0y;
|
|
874
|
+
const lenSq = ex * ex + ey * ey;
|
|
875
|
+
let nx = 0;
|
|
876
|
+
let ny = 0;
|
|
877
|
+
if (lenSq > 1e-10) {
|
|
878
|
+
const invLen = 1 / Math.sqrt(lenSq);
|
|
879
|
+
nx = ey * invLen;
|
|
880
|
+
ny = -ex * invLen;
|
|
881
|
+
}
|
|
882
|
+
const base = nextVertex * 3;
|
|
883
|
+
vertices[base] = p0x;
|
|
884
|
+
vertices[base + 1] = p0y;
|
|
885
|
+
vertices[base + 2] = 0;
|
|
886
|
+
vertices[base + 3] = p1x;
|
|
887
|
+
vertices[base + 4] = p1y;
|
|
888
|
+
vertices[base + 5] = 0;
|
|
889
|
+
vertices[base + 6] = p0x;
|
|
890
|
+
vertices[base + 7] = p0y;
|
|
891
|
+
vertices[base + 8] = backZ;
|
|
892
|
+
vertices[base + 9] = p1x;
|
|
893
|
+
vertices[base + 10] = p1y;
|
|
894
|
+
vertices[base + 11] = backZ;
|
|
895
|
+
normals[base] = nx;
|
|
896
|
+
normals[base + 1] = ny;
|
|
897
|
+
normals[base + 2] = 0;
|
|
898
|
+
normals[base + 3] = nx;
|
|
899
|
+
normals[base + 4] = ny;
|
|
900
|
+
normals[base + 5] = 0;
|
|
901
|
+
normals[base + 6] = nx;
|
|
902
|
+
normals[base + 7] = ny;
|
|
903
|
+
normals[base + 8] = 0;
|
|
904
|
+
normals[base + 9] = nx;
|
|
905
|
+
normals[base + 10] = ny;
|
|
906
|
+
normals[base + 11] = 0;
|
|
907
|
+
const baseVertex = nextVertex;
|
|
908
|
+
indices[idxPos] = baseVertex;
|
|
909
|
+
indices[idxPos + 1] = baseVertex + 1;
|
|
910
|
+
indices[idxPos + 2] = baseVertex + 2;
|
|
911
|
+
indices[idxPos + 3] = baseVertex + 1;
|
|
912
|
+
indices[idxPos + 4] = baseVertex + 3;
|
|
913
|
+
indices[idxPos + 5] = baseVertex + 2;
|
|
914
|
+
idxPos += 6;
|
|
915
|
+
nextVertex += 4;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
return { vertices, normals, indices };
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
const OVERLAP_EPSILON = 1e-3;
|
|
923
|
+
class BoundaryClusterer {
|
|
924
|
+
constructor() { }
|
|
925
|
+
cluster(glyphContoursList, positions) {
|
|
926
|
+
perfLogger.start('BoundaryClusterer.cluster', {
|
|
927
|
+
glyphCount: glyphContoursList.length
|
|
928
|
+
});
|
|
929
|
+
const n = glyphContoursList.length;
|
|
930
|
+
if (n === 0) {
|
|
931
|
+
perfLogger.end('BoundaryClusterer.cluster');
|
|
932
|
+
return [];
|
|
933
|
+
}
|
|
934
|
+
if (n === 1) {
|
|
935
|
+
perfLogger.end('BoundaryClusterer.cluster');
|
|
936
|
+
return [[0]];
|
|
937
|
+
}
|
|
938
|
+
const result = this.clusterSweepLine(glyphContoursList, positions);
|
|
939
|
+
perfLogger.end('BoundaryClusterer.cluster');
|
|
940
|
+
return result;
|
|
941
|
+
}
|
|
942
|
+
clusterSweepLine(glyphContoursList, positions) {
|
|
943
|
+
const n = glyphContoursList.length;
|
|
944
|
+
const bounds = new Array(n);
|
|
945
|
+
const events = new Array(2 * n);
|
|
946
|
+
let eventIndex = 0;
|
|
947
|
+
for (let i = 0; i < n; i++) {
|
|
948
|
+
bounds[i] = this.getWorldBounds(glyphContoursList[i], positions[i]);
|
|
949
|
+
events[eventIndex++] = [bounds[i].minX, 0, i];
|
|
950
|
+
events[eventIndex++] = [bounds[i].maxX, 1, i];
|
|
951
|
+
}
|
|
952
|
+
events.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
|
|
953
|
+
const parent = Array.from({ length: n }, (_, i) => i);
|
|
954
|
+
const rank = new Array(n).fill(0);
|
|
955
|
+
function find(x) {
|
|
956
|
+
return parent[x] === x ? x : (parent[x] = find(parent[x]));
|
|
957
|
+
}
|
|
958
|
+
function union(x, y) {
|
|
959
|
+
const px = find(x);
|
|
960
|
+
const py = find(y);
|
|
961
|
+
if (px === py)
|
|
962
|
+
return;
|
|
963
|
+
if (rank[px] < rank[py]) {
|
|
964
|
+
parent[px] = py;
|
|
965
|
+
}
|
|
966
|
+
else if (rank[px] > rank[py]) {
|
|
967
|
+
parent[py] = px;
|
|
968
|
+
}
|
|
969
|
+
else {
|
|
970
|
+
parent[py] = px;
|
|
971
|
+
rank[px]++;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
const active = new Set();
|
|
975
|
+
for (const [, eventType, glyphIndex] of events) {
|
|
976
|
+
if (eventType === 0) {
|
|
977
|
+
const bounds1 = bounds[glyphIndex];
|
|
978
|
+
for (const activeIndex of active) {
|
|
979
|
+
const bounds2 = bounds[activeIndex];
|
|
980
|
+
if (bounds1.minY < bounds2.maxY + OVERLAP_EPSILON &&
|
|
981
|
+
bounds1.maxY > bounds2.minY - OVERLAP_EPSILON) {
|
|
982
|
+
union(glyphIndex, activeIndex);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
active.add(glyphIndex);
|
|
986
|
+
}
|
|
987
|
+
else {
|
|
988
|
+
active.delete(glyphIndex);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
const clusters = new Map();
|
|
992
|
+
for (let i = 0; i < n; i++) {
|
|
993
|
+
const root = find(i);
|
|
994
|
+
let list = clusters.get(root);
|
|
995
|
+
if (!list) {
|
|
996
|
+
list = [];
|
|
997
|
+
clusters.set(root, list);
|
|
998
|
+
}
|
|
999
|
+
list.push(i);
|
|
1000
|
+
}
|
|
1001
|
+
return Array.from(clusters.values());
|
|
1002
|
+
}
|
|
1003
|
+
getWorldBounds(contours, position) {
|
|
1004
|
+
return {
|
|
1005
|
+
minX: contours.bounds.min.x + position.x,
|
|
1006
|
+
minY: contours.bounds.min.y + position.y,
|
|
1007
|
+
maxX: contours.bounds.max.x + position.x,
|
|
1008
|
+
maxY: contours.bounds.max.y + position.y
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
const DEFAULT_OPTIMIZATION_CONFIG = {
|
|
1014
|
+
enabled: true,
|
|
1015
|
+
areaThreshold: 1.0
|
|
1016
|
+
};
|
|
1017
|
+
// Scratch buffers reused across calls. Grown on demand, never shrunk
|
|
1018
|
+
let _cap = 1024;
|
|
1019
|
+
let _px = new Float64Array(_cap);
|
|
1020
|
+
let _py = new Float64Array(_cap);
|
|
1021
|
+
let _area = new Float64Array(_cap);
|
|
1022
|
+
let _prev = new Int32Array(_cap);
|
|
1023
|
+
let _next = new Int32Array(_cap);
|
|
1024
|
+
let _heap = new Int32Array(_cap);
|
|
1025
|
+
let _hpos = new Int32Array(_cap);
|
|
1026
|
+
function ensureCap(n) {
|
|
1027
|
+
if (n <= _cap)
|
|
1028
|
+
return;
|
|
1029
|
+
_cap = 1;
|
|
1030
|
+
while (_cap < n)
|
|
1031
|
+
_cap <<= 1;
|
|
1032
|
+
_px = new Float64Array(_cap);
|
|
1033
|
+
_py = new Float64Array(_cap);
|
|
1034
|
+
_area = new Float64Array(_cap);
|
|
1035
|
+
_prev = new Int32Array(_cap);
|
|
1036
|
+
_next = new Int32Array(_cap);
|
|
1037
|
+
_heap = new Int32Array(_cap);
|
|
1038
|
+
_hpos = new Int32Array(_cap);
|
|
1039
|
+
}
|
|
1040
|
+
class PathOptimizer {
|
|
1041
|
+
constructor(config) {
|
|
1042
|
+
this.stats = {
|
|
1043
|
+
pointsRemovedByVisvalingam: 0,
|
|
1044
|
+
originalPointCount: 0
|
|
1045
|
+
};
|
|
1046
|
+
this.config = config;
|
|
1047
|
+
}
|
|
1048
|
+
setConfig(config) {
|
|
1049
|
+
this.config = config;
|
|
1050
|
+
}
|
|
1051
|
+
optimizePath(path) {
|
|
1052
|
+
const points = path.points;
|
|
1053
|
+
const n = points.length;
|
|
1054
|
+
if (n < 5 || !this.config.enabled) {
|
|
1055
|
+
if (this.config.enabled)
|
|
1056
|
+
this.stats.originalPointCount += n;
|
|
1057
|
+
return path;
|
|
1058
|
+
}
|
|
1059
|
+
this.stats.originalPointCount += n;
|
|
1060
|
+
const removed = simplifyVW(points, n, this.config.areaThreshold);
|
|
1061
|
+
if (removed === 0)
|
|
1062
|
+
return path;
|
|
1063
|
+
this.stats.pointsRemovedByVisvalingam += removed;
|
|
1064
|
+
const result = new Array(n - removed);
|
|
1065
|
+
let idx = 0;
|
|
1066
|
+
let out = 0;
|
|
1067
|
+
while (idx >= 0) {
|
|
1068
|
+
result[out++] = points[idx];
|
|
1069
|
+
idx = _next[idx];
|
|
1070
|
+
}
|
|
1071
|
+
return { ...path, points: result };
|
|
1072
|
+
}
|
|
1073
|
+
getStats() {
|
|
1074
|
+
return { ...this.stats };
|
|
1075
|
+
}
|
|
1076
|
+
resetStats() {
|
|
1077
|
+
this.stats = {
|
|
1078
|
+
pointsRemovedByVisvalingam: 0,
|
|
1079
|
+
originalPointCount: 0
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
// Visvalingam-Whyatt simplification. Iteratively removes the point
|
|
1084
|
+
// whose removal causes the smallest change in contour area (measured
|
|
1085
|
+
// as the triangle formed with its two neighbors) until all remaining
|
|
1086
|
+
// triangles exceed areaThreshold
|
|
1087
|
+
//
|
|
1088
|
+
// Uses parallel typed arrays for the linked list and a binary min-heap
|
|
1089
|
+
// with an index-based position map for O(log n) extract and update.
|
|
1090
|
+
// Coordinates are flattened into Float64Array to keep area computation
|
|
1091
|
+
// on contiguous memory. Areas are stored as 2x actual (skipping the
|
|
1092
|
+
// shoelace /2) and the threshold is doubled to match
|
|
1093
|
+
//
|
|
1094
|
+
// Returns the number of points removed. The caller reads _next[] to
|
|
1095
|
+
// walk the surviving linked list
|
|
1096
|
+
function simplifyVW(points, n, areaThreshold) {
|
|
1097
|
+
if (n <= 3)
|
|
1098
|
+
return 0;
|
|
1099
|
+
ensureCap(n);
|
|
1100
|
+
const px = _px;
|
|
1101
|
+
const py = _py;
|
|
1102
|
+
const area = _area;
|
|
1103
|
+
const prev = _prev;
|
|
1104
|
+
const next = _next;
|
|
1105
|
+
const heap = _heap;
|
|
1106
|
+
const hpos = _hpos;
|
|
1107
|
+
// Flatten coordinates and initialize doubly-linked list
|
|
1108
|
+
for (let i = 0; i < n; i++) {
|
|
1109
|
+
const p = points[i];
|
|
1110
|
+
px[i] = p.x;
|
|
1111
|
+
py[i] = p.y;
|
|
1112
|
+
prev[i] = i - 1;
|
|
1113
|
+
next[i] = i + 1;
|
|
1114
|
+
}
|
|
1115
|
+
next[n - 1] = -1;
|
|
1116
|
+
// Compute triangle areas for interior points and seed the heap
|
|
1117
|
+
const heapLen0 = n - 2;
|
|
1118
|
+
area[0] = Infinity;
|
|
1119
|
+
area[n - 1] = Infinity;
|
|
1120
|
+
for (let i = 1; i < n - 1; i++) {
|
|
1121
|
+
area[i] = area2x(px, py, i - 1, i, i + 1);
|
|
1122
|
+
heap[i - 1] = i;
|
|
1123
|
+
hpos[i] = i - 1;
|
|
1124
|
+
}
|
|
1125
|
+
hpos[0] = -1;
|
|
1126
|
+
hpos[n - 1] = -1;
|
|
1127
|
+
// Bottom-up heapify, O(n)
|
|
1128
|
+
let heapLen = heapLen0;
|
|
1129
|
+
for (let i = (heapLen >> 1) - 1; i >= 0; i--) {
|
|
1130
|
+
siftDown(heap, hpos, area, i, heapLen);
|
|
1131
|
+
}
|
|
1132
|
+
const threshold2x = areaThreshold * 2;
|
|
1133
|
+
const maxRemovals = n - 3;
|
|
1134
|
+
let removed = 0;
|
|
1135
|
+
while (heapLen > 0 && removed < maxRemovals) {
|
|
1136
|
+
const minIdx = heap[0];
|
|
1137
|
+
if (area[minIdx] > threshold2x)
|
|
1138
|
+
break;
|
|
1139
|
+
// Extract min
|
|
1140
|
+
heapLen--;
|
|
1141
|
+
if (heapLen > 0) {
|
|
1142
|
+
const last = heap[heapLen];
|
|
1143
|
+
heap[0] = last;
|
|
1144
|
+
hpos[last] = 0;
|
|
1145
|
+
siftDown(heap, hpos, area, 0, heapLen);
|
|
1146
|
+
}
|
|
1147
|
+
hpos[minIdx] = -1;
|
|
1148
|
+
// Unlink
|
|
1149
|
+
const pi = prev[minIdx];
|
|
1150
|
+
const ni = next[minIdx];
|
|
1151
|
+
if (pi >= 0)
|
|
1152
|
+
next[pi] = ni;
|
|
1153
|
+
if (ni >= 0)
|
|
1154
|
+
prev[ni] = pi;
|
|
1155
|
+
removed++;
|
|
1156
|
+
// Recompute prev neighbor's area and update heap position
|
|
1157
|
+
if (pi >= 0 && prev[pi] >= 0) {
|
|
1158
|
+
const oldArea = area[pi];
|
|
1159
|
+
const newArea = area2x(px, py, prev[pi], pi, ni);
|
|
1160
|
+
area[pi] = newArea;
|
|
1161
|
+
const pos = hpos[pi];
|
|
1162
|
+
if (pos >= 0) {
|
|
1163
|
+
if (newArea < oldArea)
|
|
1164
|
+
siftUp(heap, hpos, area, pos);
|
|
1165
|
+
else if (newArea > oldArea)
|
|
1166
|
+
siftDown(heap, hpos, area, pos, heapLen);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
// Same for next neighbor
|
|
1170
|
+
if (ni >= 0 && next[ni] >= 0) {
|
|
1171
|
+
const oldArea = area[ni];
|
|
1172
|
+
const newArea = area2x(px, py, pi, ni, next[ni]);
|
|
1173
|
+
area[ni] = newArea;
|
|
1174
|
+
const pos = hpos[ni];
|
|
1175
|
+
if (pos >= 0) {
|
|
1176
|
+
if (newArea < oldArea)
|
|
1177
|
+
siftUp(heap, hpos, area, pos);
|
|
1178
|
+
else if (newArea > oldArea)
|
|
1179
|
+
siftDown(heap, hpos, area, pos, heapLen);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
return removed;
|
|
1184
|
+
}
|
|
1185
|
+
function siftUp(heap, hpos, area, i) {
|
|
1186
|
+
const idx = heap[i];
|
|
1187
|
+
const val = area[idx];
|
|
1188
|
+
while (i > 0) {
|
|
1189
|
+
const parent = (i - 1) >> 1;
|
|
1190
|
+
const pidx = heap[parent];
|
|
1191
|
+
if (area[pidx] <= val)
|
|
1192
|
+
break;
|
|
1193
|
+
heap[i] = pidx;
|
|
1194
|
+
hpos[pidx] = i;
|
|
1195
|
+
i = parent;
|
|
1196
|
+
}
|
|
1197
|
+
heap[i] = idx;
|
|
1198
|
+
hpos[idx] = i;
|
|
1199
|
+
}
|
|
1200
|
+
function siftDown(heap, hpos, area, i, len) {
|
|
1201
|
+
const idx = heap[i];
|
|
1202
|
+
const val = area[idx];
|
|
1203
|
+
const half = len >> 1;
|
|
1204
|
+
while (i < half) {
|
|
1205
|
+
let child = (i << 1) + 1;
|
|
1206
|
+
let childIdx = heap[child];
|
|
1207
|
+
let childVal = area[childIdx];
|
|
1208
|
+
const right = child + 1;
|
|
1209
|
+
if (right < len) {
|
|
1210
|
+
const rIdx = heap[right];
|
|
1211
|
+
const rVal = area[rIdx];
|
|
1212
|
+
if (rVal < childVal) {
|
|
1213
|
+
child = right;
|
|
1214
|
+
childIdx = rIdx;
|
|
1215
|
+
childVal = rVal;
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
if (childVal >= val)
|
|
1219
|
+
break;
|
|
1220
|
+
heap[i] = childIdx;
|
|
1221
|
+
hpos[childIdx] = i;
|
|
1222
|
+
i = child;
|
|
1223
|
+
}
|
|
1224
|
+
heap[i] = idx;
|
|
1225
|
+
hpos[idx] = i;
|
|
1226
|
+
}
|
|
1227
|
+
// Doubled triangle area via the shoelace formula. Skipping the /2
|
|
1228
|
+
// means callers compare against a doubled threshold instead
|
|
1229
|
+
function area2x(px, py, i1, i2, i3) {
|
|
1230
|
+
const v = px[i1] * (py[i2] - py[i3]) +
|
|
1231
|
+
px[i2] * (py[i3] - py[i1]) +
|
|
1232
|
+
px[i3] * (py[i1] - py[i2]);
|
|
1233
|
+
return v < 0 ? -v : v;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
/**
|
|
1237
|
+
* @license
|
|
1238
|
+
* Anti-Grain Geometry - Version 2.4
|
|
1239
|
+
* Copyright (C) 2002-2005 Maxim Shemanarev (McSeem)
|
|
1240
|
+
*
|
|
1241
|
+
* This software is a partial port of the AGG library, specifically the adaptive
|
|
1242
|
+
* subdivision algorithm for polygonization. The original software was available
|
|
1243
|
+
* at http://www.antigrain.com and was distributed under the BSD 3-Clause License
|
|
1244
|
+
*
|
|
1245
|
+
* Redistribution and use in source and binary forms, with or without
|
|
1246
|
+
* modification, are permitted provided that the following conditions
|
|
1247
|
+
* are met:
|
|
1248
|
+
*
|
|
1249
|
+
* 1. Redistributions of source code must retain the above copyright
|
|
1250
|
+
* notice, this list of conditions and the following disclaimer.
|
|
1251
|
+
*
|
|
1252
|
+
* 2. Redistributions in binary form must reproduce the above copyright
|
|
1253
|
+
* notice, this list of conditions and the following disclaimer in
|
|
1254
|
+
* the documentation and/or other materials provided with the
|
|
1255
|
+
* distribution.
|
|
1256
|
+
*
|
|
1257
|
+
* 3. The name of the author may not be used to endorse or promote
|
|
1258
|
+
* products derived from this software without specific prior
|
|
1259
|
+
* written permission.
|
|
1260
|
+
*
|
|
1261
|
+
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
|
1262
|
+
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
1263
|
+
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
1264
|
+
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
|
|
1265
|
+
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
1266
|
+
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
1267
|
+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
|
1268
|
+
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
|
1269
|
+
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
|
|
1270
|
+
* IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
1271
|
+
* POSSIBILITY OF SUCH DAMAGE.
|
|
1272
|
+
*/
|
|
1273
|
+
const DEFAULT_CURVE_FIDELITY = {
|
|
1274
|
+
distanceTolerance: 0.5,
|
|
1275
|
+
angleTolerance: 0.2,
|
|
1276
|
+
cuspLimit: 0,
|
|
1277
|
+
collinearityEpsilon: 1e-6,
|
|
1278
|
+
recursionLimit: 16
|
|
1279
|
+
};
|
|
1280
|
+
const COLLINEARITY_EPSILON = DEFAULT_CURVE_FIDELITY.collinearityEpsilon;
|
|
1281
|
+
// Module-level state for the recursive subdivision functions,
|
|
1282
|
+
// set from instance config before each polygonize call
|
|
1283
|
+
// Output array, reset before each polygonize call
|
|
1284
|
+
let _out;
|
|
1285
|
+
// Cached tolerance state
|
|
1286
|
+
let _distTolSq = 0;
|
|
1287
|
+
let _colEps = 0;
|
|
1288
|
+
let _maxLvl = 0;
|
|
1289
|
+
let _angleTol = 0;
|
|
1290
|
+
let _tanAngSq = 0;
|
|
1291
|
+
let _cuspLim = 0;
|
|
1292
|
+
let _tanCuspSq = 0;
|
|
1293
|
+
// Collinearity checks in the recursive core prevent near-duplicate points
|
|
1294
|
+
function emit(x, y) {
|
|
1295
|
+
_out.push(new Vec2(x, y));
|
|
1296
|
+
}
|
|
1297
|
+
// Quadratic recursive subdivision (AGG curve3_div)
|
|
1298
|
+
function quadRec(x1, y1, x2, y2, x3, y3, level) {
|
|
1299
|
+
if (level > _maxLvl)
|
|
1300
|
+
return;
|
|
1301
|
+
const x12 = (x1 + x2) * 0.5;
|
|
1302
|
+
const y12 = (y1 + y2) * 0.5;
|
|
1303
|
+
const x23 = (x2 + x3) * 0.5;
|
|
1304
|
+
const y23 = (y2 + y3) * 0.5;
|
|
1305
|
+
const x123 = (x12 + x23) * 0.5;
|
|
1306
|
+
const y123 = (y12 + y23) * 0.5;
|
|
1307
|
+
const dx = x3 - x1;
|
|
1308
|
+
const dy = y3 - y1;
|
|
1309
|
+
let d = Math.abs((x2 - x3) * dy - (y2 - y3) * dx);
|
|
1310
|
+
if (d > _colEps) {
|
|
1311
|
+
if (d * d <= _distTolSq * (dx * dx + dy * dy)) {
|
|
1312
|
+
if (_angleTol > 0) {
|
|
1313
|
+
const v1x = x2 - x1;
|
|
1314
|
+
const v1y = y2 - y1;
|
|
1315
|
+
const v2x = x3 - x2;
|
|
1316
|
+
const v2y = y3 - y2;
|
|
1317
|
+
const cross = v1x * v2y - v1y * v2x;
|
|
1318
|
+
const dot = v1x * v2x + v1y * v2y;
|
|
1319
|
+
if (dot > 0 && cross * cross < _tanAngSq * dot * dot) {
|
|
1320
|
+
emit(x123, y123);
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
else {
|
|
1325
|
+
emit(x123, y123);
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
else {
|
|
1331
|
+
let da = dx * dx + dy * dy;
|
|
1332
|
+
if (da === 0) {
|
|
1333
|
+
d = (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1);
|
|
1334
|
+
}
|
|
1335
|
+
else {
|
|
1336
|
+
d = ((x2 - x1) * dx + (y2 - y1) * dy) / da;
|
|
1337
|
+
if (d > 0 && d < 1)
|
|
1338
|
+
return;
|
|
1339
|
+
if (d <= 0)
|
|
1340
|
+
d = (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1);
|
|
1341
|
+
else if (d >= 1)
|
|
1342
|
+
d = (x2 - x3) * (x2 - x3) + (y2 - y3) * (y2 - y3);
|
|
1343
|
+
else {
|
|
1344
|
+
const px = x1 + d * dx;
|
|
1345
|
+
const py = y1 + d * dy;
|
|
1346
|
+
d = (x2 - px) * (x2 - px) + (y2 - py) * (y2 - py);
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
if (d < _distTolSq) {
|
|
1350
|
+
emit(x2, y2);
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
const nl = level + 1;
|
|
1355
|
+
quadRec(x1, y1, x12, y12, x123, y123, nl);
|
|
1356
|
+
quadRec(x123, y123, x23, y23, x3, y3, nl);
|
|
1357
|
+
}
|
|
1358
|
+
// Cubic recursive subdivision (AGG curve4_div)
|
|
1359
|
+
// cubicBegin handles level 0, which always subdivides
|
|
1360
|
+
function cubicBegin(x1, y1, x2, y2, x3, y3, x4, y4) {
|
|
1361
|
+
const x12 = (x1 + x2) * 0.5;
|
|
1362
|
+
const y12 = (y1 + y2) * 0.5;
|
|
1363
|
+
const x23 = (x2 + x3) * 0.5;
|
|
1364
|
+
const y23 = (y2 + y3) * 0.5;
|
|
1365
|
+
const x34 = (x3 + x4) * 0.5;
|
|
1366
|
+
const y34 = (y3 + y4) * 0.5;
|
|
1367
|
+
const x123 = (x12 + x23) * 0.5;
|
|
1368
|
+
const y123 = (y12 + y23) * 0.5;
|
|
1369
|
+
const x234 = (x23 + x34) * 0.5;
|
|
1370
|
+
const y234 = (y23 + y34) * 0.5;
|
|
1371
|
+
const x1234 = (x123 + x234) * 0.5;
|
|
1372
|
+
const y1234 = (y123 + y234) * 0.5;
|
|
1373
|
+
cubicRec(x1, y1, x12, y12, x123, y123, x1234, y1234, 1);
|
|
1374
|
+
cubicRec(x1234, y1234, x234, y234, x34, y34, x4, y4, 1);
|
|
1375
|
+
}
|
|
1376
|
+
function cubicRec(x1, y1, x2, y2, x3, y3, x4, y4, level) {
|
|
1377
|
+
if (level > _maxLvl)
|
|
1378
|
+
return;
|
|
1379
|
+
const x12 = (x1 + x2) * 0.5;
|
|
1380
|
+
const y12 = (y1 + y2) * 0.5;
|
|
1381
|
+
const x23 = (x2 + x3) * 0.5;
|
|
1382
|
+
const y23 = (y2 + y3) * 0.5;
|
|
1383
|
+
const x34 = (x3 + x4) * 0.5;
|
|
1384
|
+
const y34 = (y3 + y4) * 0.5;
|
|
1385
|
+
const x123 = (x12 + x23) * 0.5;
|
|
1386
|
+
const y123 = (y12 + y23) * 0.5;
|
|
1387
|
+
const x234 = (x23 + x34) * 0.5;
|
|
1388
|
+
const y234 = (y23 + y34) * 0.5;
|
|
1389
|
+
const x1234 = (x123 + x234) * 0.5;
|
|
1390
|
+
const y1234 = (y123 + y234) * 0.5;
|
|
1391
|
+
const dx = x4 - x1;
|
|
1392
|
+
const dy = y4 - y1;
|
|
1393
|
+
let d2 = Math.abs((x2 - x4) * dy - (y2 - y4) * dx);
|
|
1394
|
+
let d3 = Math.abs((x3 - x4) * dy - (y3 - y4) * dx);
|
|
1395
|
+
const sc = (d2 > _colEps ? 2 : 0) + (d3 > _colEps ? 1 : 0);
|
|
1396
|
+
switch (sc) {
|
|
1397
|
+
case 0: {
|
|
1398
|
+
let k = dx * dx + dy * dy;
|
|
1399
|
+
if (k === 0) {
|
|
1400
|
+
d2 = (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1);
|
|
1401
|
+
d3 = (x3 - x1) * (x3 - x1) + (y3 - y1) * (y3 - y1);
|
|
1402
|
+
}
|
|
1403
|
+
else {
|
|
1404
|
+
k = 1 / k;
|
|
1405
|
+
let t1 = x2 - x1;
|
|
1406
|
+
let t2 = y2 - y1;
|
|
1407
|
+
d2 = k * (t1 * dx + t2 * dy);
|
|
1408
|
+
t1 = x3 - x1;
|
|
1409
|
+
t2 = y3 - y1;
|
|
1410
|
+
d3 = k * (t1 * dx + t2 * dy);
|
|
1411
|
+
if (d2 > 0 && d2 < 1 && d3 > 0 && d3 < 1)
|
|
1412
|
+
return;
|
|
1413
|
+
if (d2 <= 0)
|
|
1414
|
+
d2 = (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1);
|
|
1415
|
+
else if (d2 >= 1)
|
|
1416
|
+
d2 = (x2 - x4) * (x2 - x4) + (y2 - y4) * (y2 - y4);
|
|
1417
|
+
else {
|
|
1418
|
+
const px = x1 + d2 * dx, py = y1 + d2 * dy;
|
|
1419
|
+
d2 = (x2 - px) * (x2 - px) + (y2 - py) * (y2 - py);
|
|
1420
|
+
}
|
|
1421
|
+
if (d3 <= 0)
|
|
1422
|
+
d3 = (x3 - x1) * (x3 - x1) + (y3 - y1) * (y3 - y1);
|
|
1423
|
+
else if (d3 >= 1)
|
|
1424
|
+
d3 = (x3 - x4) * (x3 - x4) + (y3 - y4) * (y3 - y4);
|
|
1425
|
+
else {
|
|
1426
|
+
const px = x1 + d3 * dx, py = y1 + d3 * dy;
|
|
1427
|
+
d3 = (x3 - px) * (x3 - px) + (y3 - py) * (y3 - py);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
if (d2 > d3) {
|
|
1431
|
+
if (d2 < _distTolSq) {
|
|
1432
|
+
emit(x2, y2);
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
else {
|
|
1437
|
+
if (d3 < _distTolSq) {
|
|
1438
|
+
emit(x3, y3);
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
break;
|
|
1443
|
+
}
|
|
1444
|
+
case 1:
|
|
1445
|
+
if (d3 * d3 <= _distTolSq * (dx * dx + dy * dy)) {
|
|
1446
|
+
if (_angleTol > 0) {
|
|
1447
|
+
const v1x = x3 - x2, v1y = y3 - y2;
|
|
1448
|
+
const v2x = x4 - x3, v2y = y4 - y3;
|
|
1449
|
+
const cross = v1x * v2y - v1y * v2x;
|
|
1450
|
+
const dot = v1x * v2x + v1y * v2y;
|
|
1451
|
+
if (dot > 0 && cross * cross < _tanAngSq * dot * dot) {
|
|
1452
|
+
emit(x2, y2);
|
|
1453
|
+
emit(x3, y3);
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
if (_cuspLim > 0 &&
|
|
1457
|
+
(dot <= 0 || cross * cross > _tanCuspSq * dot * dot)) {
|
|
1458
|
+
emit(x3, y3);
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
else {
|
|
1463
|
+
emit(x23, y23);
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
break;
|
|
1468
|
+
case 2:
|
|
1469
|
+
if (d2 * d2 <= _distTolSq * (dx * dx + dy * dy)) {
|
|
1470
|
+
if (_angleTol > 0) {
|
|
1471
|
+
const v1x = x2 - x1, v1y = y2 - y1;
|
|
1472
|
+
const v2x = x3 - x2, v2y = y3 - y2;
|
|
1473
|
+
const cross = v1x * v2y - v1y * v2x;
|
|
1474
|
+
const dot = v1x * v2x + v1y * v2y;
|
|
1475
|
+
if (dot > 0 && cross * cross < _tanAngSq * dot * dot) {
|
|
1476
|
+
emit(x2, y2);
|
|
1477
|
+
emit(x3, y3);
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
if (_cuspLim > 0 &&
|
|
1481
|
+
(dot <= 0 || cross * cross > _tanCuspSq * dot * dot)) {
|
|
1482
|
+
emit(x2, y2);
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
else {
|
|
1487
|
+
emit(x23, y23);
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
break;
|
|
1492
|
+
case 3: {
|
|
1493
|
+
if ((d2 + d3) * (d2 + d3) <= _distTolSq * (dx * dx + dy * dy)) {
|
|
1494
|
+
if (_angleTol > 0) {
|
|
1495
|
+
const a1x = x2 - x1, a1y = y2 - y1;
|
|
1496
|
+
const a2x = x3 - x2, a2y = y3 - y2;
|
|
1497
|
+
const c1 = a1x * a2y - a1y * a2x;
|
|
1498
|
+
const dot1 = a1x * a2x + a1y * a2y;
|
|
1499
|
+
const b2x = x4 - x3, b2y = y4 - y3;
|
|
1500
|
+
const c2 = a2x * b2y - a2y * b2x;
|
|
1501
|
+
const dot2 = a2x * b2x + a2y * b2y;
|
|
1502
|
+
// Sum of unsigned angles via tangent addition identity
|
|
1503
|
+
if (dot1 > 0 && dot2 > 0) {
|
|
1504
|
+
const ac1 = c1 < 0 ? -c1 : c1;
|
|
1505
|
+
const ac2 = c2 < 0 ? -c2 : c2;
|
|
1506
|
+
const cc = ac1 * dot2 + ac2 * dot1;
|
|
1507
|
+
const cd = dot1 * dot2 - ac1 * ac2;
|
|
1508
|
+
if (cd > 0 && cc * cc < _tanAngSq * cd * cd) {
|
|
1509
|
+
emit(x23, y23);
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
if (_cuspLim > 0) {
|
|
1514
|
+
if (dot1 <= 0 || c1 * c1 > _tanCuspSq * dot1 * dot1) {
|
|
1515
|
+
emit(x2, y2);
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
if (dot2 <= 0 || c2 * c2 > _tanCuspSq * dot2 * dot2) {
|
|
1519
|
+
emit(x3, y3);
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
else {
|
|
1525
|
+
emit(x23, y23);
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
break;
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
const nl = level + 1;
|
|
1533
|
+
cubicRec(x1, y1, x12, y12, x123, y123, x1234, y1234, nl);
|
|
1534
|
+
cubicRec(x1234, y1234, x234, y234, x34, y34, x4, y4, nl);
|
|
1535
|
+
}
|
|
1536
|
+
class Polygonizer {
|
|
1537
|
+
constructor(curveFidelityConfig) {
|
|
1538
|
+
this.curveSteps = null;
|
|
1539
|
+
// Precomputed tolerances
|
|
1540
|
+
this._distTolSq = 0;
|
|
1541
|
+
this._angleTol = 0;
|
|
1542
|
+
this._tanAngSq = 0;
|
|
1543
|
+
this._cuspLim = 0;
|
|
1544
|
+
this._tanCuspSq = 0;
|
|
1545
|
+
this._colEps = 0;
|
|
1546
|
+
this._maxLvl = 0;
|
|
1547
|
+
this.curveFidelityConfig = {
|
|
1548
|
+
...DEFAULT_CURVE_FIDELITY,
|
|
1549
|
+
...curveFidelityConfig
|
|
1550
|
+
};
|
|
1551
|
+
this.precompute();
|
|
1552
|
+
}
|
|
1553
|
+
setCurveFidelityConfig(curveFidelityConfig) {
|
|
1554
|
+
this.curveFidelityConfig = {
|
|
1555
|
+
...DEFAULT_CURVE_FIDELITY,
|
|
1556
|
+
...curveFidelityConfig
|
|
1557
|
+
};
|
|
1558
|
+
this.precompute();
|
|
1559
|
+
}
|
|
1560
|
+
precompute() {
|
|
1561
|
+
const c = this.curveFidelityConfig;
|
|
1562
|
+
const dt = c.distanceTolerance ?? DEFAULT_CURVE_FIDELITY.distanceTolerance;
|
|
1563
|
+
this._distTolSq = dt * dt;
|
|
1564
|
+
this._angleTol = c.angleTolerance ?? DEFAULT_CURVE_FIDELITY.angleTolerance;
|
|
1565
|
+
this._tanAngSq = this._angleTol > 0
|
|
1566
|
+
? Math.tan(this._angleTol) ** 2 : 0;
|
|
1567
|
+
this._cuspLim = c.cuspLimit ?? 0;
|
|
1568
|
+
this._tanCuspSq = this._cuspLim > 0
|
|
1569
|
+
? Math.tan(this._cuspLim) ** 2 : 0;
|
|
1570
|
+
this._colEps = c.collinearityEpsilon ?? DEFAULT_CURVE_FIDELITY.collinearityEpsilon;
|
|
1571
|
+
this._maxLvl = c.recursionLimit ?? DEFAULT_CURVE_FIDELITY.recursionLimit;
|
|
1572
|
+
}
|
|
1573
|
+
// Set module-level state from instance tolerances
|
|
1574
|
+
activate() {
|
|
1575
|
+
_distTolSq = this._distTolSq;
|
|
1576
|
+
_angleTol = this._angleTol;
|
|
1577
|
+
_tanAngSq = this._tanAngSq;
|
|
1578
|
+
_cuspLim = this._cuspLim;
|
|
1579
|
+
_tanCuspSq = this._tanCuspSq;
|
|
1580
|
+
_colEps = this._colEps;
|
|
1581
|
+
_maxLvl = this._maxLvl;
|
|
1582
|
+
_out = [];
|
|
1583
|
+
}
|
|
1584
|
+
// Fixed-step subdivision; overrides adaptive curveFidelity when set
|
|
1585
|
+
setCurveSteps(curveSteps) {
|
|
1586
|
+
if (curveSteps === undefined || curveSteps === null) {
|
|
1587
|
+
this.curveSteps = null;
|
|
1588
|
+
return;
|
|
1589
|
+
}
|
|
1590
|
+
if (!Number.isFinite(curveSteps)) {
|
|
1591
|
+
this.curveSteps = null;
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
const stepsInt = Math.round(curveSteps);
|
|
1595
|
+
this.curveSteps = stepsInt >= 1 ? stepsInt : null;
|
|
1596
|
+
}
|
|
1597
|
+
polygonizeQuadratic(start, control, end) {
|
|
1598
|
+
if (this.curveSteps !== null) {
|
|
1599
|
+
return this.polygonizeQuadraticFixedSteps(start, control, end, this.curveSteps);
|
|
1600
|
+
}
|
|
1601
|
+
this.activate();
|
|
1602
|
+
quadRec(start.x, start.y, control.x, control.y, end.x, end.y, 0);
|
|
1603
|
+
emit(end.x, end.y);
|
|
1604
|
+
return _out;
|
|
1605
|
+
}
|
|
1606
|
+
polygonizeCubic(start, control1, control2, end) {
|
|
1607
|
+
if (this.curveSteps !== null) {
|
|
1608
|
+
return this.polygonizeCubicFixedSteps(start, control1, control2, end, this.curveSteps);
|
|
1609
|
+
}
|
|
1610
|
+
this.activate();
|
|
1611
|
+
cubicBegin(start.x, start.y, control1.x, control1.y, control2.x, control2.y, end.x, end.y);
|
|
1612
|
+
emit(end.x, end.y);
|
|
1613
|
+
return _out;
|
|
1614
|
+
}
|
|
1615
|
+
polygonizeQuadraticFixedSteps(start, control, end, steps) {
|
|
1616
|
+
this.activate();
|
|
1617
|
+
for (let i = 1; i <= steps; i++) {
|
|
1618
|
+
const t = i / steps;
|
|
1619
|
+
const x12 = start.x + (control.x - start.x) * t;
|
|
1620
|
+
const y12 = start.y + (control.y - start.y) * t;
|
|
1621
|
+
const x23 = control.x + (end.x - control.x) * t;
|
|
1622
|
+
const y23 = control.y + (end.y - control.y) * t;
|
|
1623
|
+
emit(x12 + (x23 - x12) * t, y12 + (y23 - y12) * t);
|
|
1624
|
+
}
|
|
1625
|
+
return _out;
|
|
1626
|
+
}
|
|
1627
|
+
polygonizeCubicFixedSteps(start, control1, control2, end, steps) {
|
|
1628
|
+
this.activate();
|
|
1629
|
+
for (let i = 1; i <= steps; i++) {
|
|
1630
|
+
const t = i / steps;
|
|
1631
|
+
const x12 = start.x + (control1.x - start.x) * t;
|
|
1632
|
+
const y12 = start.y + (control1.y - start.y) * t;
|
|
1633
|
+
const x23 = control1.x + (control2.x - control1.x) * t;
|
|
1634
|
+
const y23 = control1.y + (control2.y - control1.y) * t;
|
|
1635
|
+
const x34 = control2.x + (end.x - control2.x) * t;
|
|
1636
|
+
const y34 = control2.y + (end.y - control2.y) * t;
|
|
1637
|
+
const x123 = x12 + (x23 - x12) * t;
|
|
1638
|
+
const y123 = y12 + (y23 - y12) * t;
|
|
1639
|
+
const x234 = x23 + (x34 - x23) * t;
|
|
1640
|
+
const y234 = y23 + (y34 - y23) * t;
|
|
1641
|
+
emit(x123 + (x234 - x123) * t, y123 + (y234 - y123) * t);
|
|
1642
|
+
}
|
|
1643
|
+
return _out;
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
class GlyphContourCollector {
|
|
1648
|
+
constructor(curveFidelityConfig, optimizationConfig) {
|
|
1649
|
+
this.currentGlyphId = 0;
|
|
1650
|
+
this.currentTextIndex = 0;
|
|
1651
|
+
this.currentGlyphPaths = [];
|
|
1652
|
+
this.currentPath = null;
|
|
1653
|
+
this.currentPoint = null;
|
|
1654
|
+
this.currentGlyphBounds = {
|
|
1655
|
+
min: new Vec2(Infinity, Infinity),
|
|
1656
|
+
max: new Vec2(-Infinity, -Infinity)
|
|
1657
|
+
};
|
|
1658
|
+
this.collectedGlyphs = [];
|
|
1659
|
+
this.glyphPositions = [];
|
|
1660
|
+
this.glyphTextIndices = [];
|
|
1661
|
+
this.currentPosition = new Vec2(0, 0);
|
|
1662
|
+
this.polygonizer = new Polygonizer(curveFidelityConfig);
|
|
1663
|
+
this.pathOptimizer = new PathOptimizer({
|
|
1664
|
+
...DEFAULT_OPTIMIZATION_CONFIG,
|
|
1665
|
+
...optimizationConfig
|
|
1666
|
+
});
|
|
1667
|
+
}
|
|
1668
|
+
setPosition(x, y) {
|
|
1669
|
+
this.currentPosition.set(x, y);
|
|
1670
|
+
}
|
|
1671
|
+
updatePosition(dx, dy) {
|
|
1672
|
+
this.currentPosition.x += dx;
|
|
1673
|
+
this.currentPosition.y += dy;
|
|
1674
|
+
}
|
|
1675
|
+
beginGlyph(glyphId, textIndex) {
|
|
1676
|
+
if (this.currentGlyphPaths.length > 0) {
|
|
1677
|
+
this.finishGlyph();
|
|
1678
|
+
}
|
|
1679
|
+
this.currentGlyphId = glyphId;
|
|
1680
|
+
this.currentTextIndex = textIndex;
|
|
1681
|
+
this.currentGlyphPaths = [];
|
|
1682
|
+
this.currentGlyphBounds.min.set(Infinity, Infinity);
|
|
1683
|
+
this.currentGlyphBounds.max.set(-Infinity, -Infinity);
|
|
1684
|
+
// Record position for this glyph
|
|
1685
|
+
this.glyphPositions.push(this.currentPosition.clone());
|
|
1686
|
+
// Time polygonization + path optimization per glyph
|
|
1687
|
+
perfLogger.start('Glyph.polygonizeAndOptimize', {
|
|
1688
|
+
glyphId,
|
|
1689
|
+
textIndex
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
finishGlyph() {
|
|
1693
|
+
if (this.currentPath) {
|
|
1694
|
+
this.finishPath();
|
|
1695
|
+
}
|
|
1696
|
+
if (this.currentGlyphPaths.length > 0) {
|
|
1697
|
+
this.collectedGlyphs.push({
|
|
1698
|
+
glyphId: this.currentGlyphId,
|
|
1699
|
+
paths: this.currentGlyphPaths,
|
|
1700
|
+
bounds: {
|
|
1701
|
+
min: {
|
|
1702
|
+
x: this.currentGlyphBounds.min.x,
|
|
1703
|
+
y: this.currentGlyphBounds.min.y
|
|
1704
|
+
},
|
|
1705
|
+
max: {
|
|
1706
|
+
x: this.currentGlyphBounds.max.x,
|
|
1707
|
+
y: this.currentGlyphBounds.max.y
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
});
|
|
1711
|
+
// Track textIndex separately
|
|
1712
|
+
this.glyphTextIndices.push(this.currentTextIndex);
|
|
1713
|
+
}
|
|
1714
|
+
// Stop timing for this glyph (even if it ended up empty)
|
|
1715
|
+
perfLogger.end('Glyph.polygonizeAndOptimize');
|
|
1716
|
+
this.currentGlyphPaths = [];
|
|
1717
|
+
}
|
|
1718
|
+
onMoveTo(x, y) {
|
|
1719
|
+
if (this.currentPath) {
|
|
1720
|
+
this.finishPath();
|
|
1721
|
+
}
|
|
1722
|
+
this.currentPoint = new Vec2(x, y);
|
|
1723
|
+
this.updateBounds(this.currentPoint);
|
|
1724
|
+
this.currentPath = {
|
|
1725
|
+
points: [this.currentPoint],
|
|
1726
|
+
glyphIndex: this.currentGlyphId
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
onLineTo(x, y) {
|
|
1730
|
+
if (!this.currentPath || !this.currentPoint)
|
|
1731
|
+
return;
|
|
1732
|
+
const point = new Vec2(x, y);
|
|
1733
|
+
this.updateBounds(point);
|
|
1734
|
+
this.currentPath.points.push(point);
|
|
1735
|
+
this.currentPoint = point;
|
|
1736
|
+
}
|
|
1737
|
+
onQuadTo(cx, cy, x, y) {
|
|
1738
|
+
if (!this.currentPath || !this.currentPoint)
|
|
1739
|
+
return;
|
|
1740
|
+
const start = this.currentPoint;
|
|
1741
|
+
const control = new Vec2(cx, cy);
|
|
1742
|
+
const end = new Vec2(x, y);
|
|
1743
|
+
const dx = end.x - start.x;
|
|
1744
|
+
const dy = end.y - start.y;
|
|
1745
|
+
const d = Math.abs((control.x - end.x) * dy - (control.y - end.y) * dx);
|
|
1746
|
+
if (d < COLLINEARITY_EPSILON) {
|
|
1747
|
+
this.onLineTo(x, y);
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
const flattenedPoints = this.polygonizer.polygonizeQuadratic(start, control, end);
|
|
1751
|
+
for (let i = 0; i < flattenedPoints.length; i++) {
|
|
1752
|
+
const pt = flattenedPoints[i];
|
|
1753
|
+
this.updateBounds(pt);
|
|
1754
|
+
this.currentPath.points.push(pt);
|
|
1755
|
+
}
|
|
1756
|
+
this.currentPoint = end;
|
|
1757
|
+
}
|
|
1758
|
+
onCubicTo(c1x, c1y, c2x, c2y, x, y) {
|
|
1759
|
+
if (!this.currentPath || !this.currentPoint)
|
|
1760
|
+
return;
|
|
1761
|
+
const start = this.currentPoint;
|
|
1762
|
+
const control1 = new Vec2(c1x, c1y);
|
|
1763
|
+
const control2 = new Vec2(c2x, c2y);
|
|
1764
|
+
const end = new Vec2(x, y);
|
|
1765
|
+
const dx = end.x - start.x;
|
|
1766
|
+
const dy = end.y - start.y;
|
|
1767
|
+
const d1 = Math.abs((control1.x - end.x) * dy - (control1.y - end.y) * dx);
|
|
1768
|
+
const d2 = Math.abs((control2.x - end.x) * dy - (control2.y - end.y) * dx);
|
|
1769
|
+
if (d1 < COLLINEARITY_EPSILON && d2 < COLLINEARITY_EPSILON) {
|
|
1770
|
+
this.onLineTo(x, y);
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
const flattenedPoints = this.polygonizer.polygonizeCubic(start, control1, control2, end);
|
|
1774
|
+
for (let i = 0; i < flattenedPoints.length; i++) {
|
|
1775
|
+
const pt = flattenedPoints[i];
|
|
1776
|
+
this.updateBounds(pt);
|
|
1777
|
+
this.currentPath.points.push(pt);
|
|
1778
|
+
}
|
|
1779
|
+
this.currentPoint = end;
|
|
1780
|
+
}
|
|
1781
|
+
onClosePath() {
|
|
1782
|
+
if (!this.currentPath || !this.currentPoint)
|
|
1783
|
+
return;
|
|
1784
|
+
const firstPoint = this.currentPath.points[0];
|
|
1785
|
+
if (!this.currentPoint.equals(firstPoint)) {
|
|
1786
|
+
this.currentPath.points.push(firstPoint);
|
|
1787
|
+
}
|
|
1788
|
+
this.finishPath();
|
|
1789
|
+
}
|
|
1790
|
+
finishPath() {
|
|
1791
|
+
if (this.currentPath) {
|
|
1792
|
+
const path = this.pathOptimizer.optimizePath(this.currentPath);
|
|
1793
|
+
this.currentGlyphPaths.push(path);
|
|
1794
|
+
this.currentPath = null;
|
|
1795
|
+
this.currentPoint = null;
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
updateBounds(point) {
|
|
1799
|
+
this.currentGlyphBounds.min.x = Math.min(this.currentGlyphBounds.min.x, point.x);
|
|
1800
|
+
this.currentGlyphBounds.min.y = Math.min(this.currentGlyphBounds.min.y, point.y);
|
|
1801
|
+
this.currentGlyphBounds.max.x = Math.max(this.currentGlyphBounds.max.x, point.x);
|
|
1802
|
+
this.currentGlyphBounds.max.y = Math.max(this.currentGlyphBounds.max.y, point.y);
|
|
1803
|
+
}
|
|
1804
|
+
getCollectedGlyphs() {
|
|
1805
|
+
// Finish any pending glyph
|
|
1806
|
+
if (this.currentGlyphPaths.length > 0) {
|
|
1807
|
+
this.finishGlyph();
|
|
1808
|
+
}
|
|
1809
|
+
return this.collectedGlyphs;
|
|
1810
|
+
}
|
|
1811
|
+
getGlyphPositions() {
|
|
1812
|
+
return this.glyphPositions;
|
|
1813
|
+
}
|
|
1814
|
+
getTextIndices() {
|
|
1815
|
+
return this.glyphTextIndices;
|
|
1816
|
+
}
|
|
1817
|
+
reset() {
|
|
1818
|
+
this.collectedGlyphs = [];
|
|
1819
|
+
this.glyphPositions = [];
|
|
1820
|
+
this.glyphTextIndices = [];
|
|
1821
|
+
this.currentGlyphPaths = [];
|
|
1822
|
+
this.currentPath = null;
|
|
1823
|
+
this.currentPoint = null;
|
|
1824
|
+
this.currentGlyphId = 0;
|
|
1825
|
+
this.currentTextIndex = 0;
|
|
1826
|
+
this.currentPosition.set(0, 0);
|
|
1827
|
+
this.currentGlyphBounds = {
|
|
1828
|
+
min: new Vec2(Infinity, Infinity),
|
|
1829
|
+
max: new Vec2(-Infinity, -Infinity)
|
|
1830
|
+
};
|
|
1831
|
+
}
|
|
1832
|
+
setCurveFidelityConfig(config) {
|
|
1833
|
+
this.polygonizer.setCurveFidelityConfig(config);
|
|
1834
|
+
}
|
|
1835
|
+
setCurveSteps(curveSteps) {
|
|
1836
|
+
this.polygonizer.setCurveSteps(curveSteps);
|
|
1837
|
+
}
|
|
1838
|
+
setGeometryOptimization(options) {
|
|
1839
|
+
this.pathOptimizer.setConfig({
|
|
1840
|
+
...DEFAULT_OPTIMIZATION_CONFIG,
|
|
1841
|
+
...options
|
|
1842
|
+
});
|
|
1843
|
+
}
|
|
1844
|
+
getOptimizationStats() {
|
|
1845
|
+
return this.pathOptimizer.getStats();
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
class GlyphGeometryBuilder {
|
|
1850
|
+
constructor(cache, loadedFont) {
|
|
1851
|
+
this.fontId = 'default';
|
|
1852
|
+
this.cacheKeyPrefix = 'default';
|
|
1853
|
+
this.emptyGlyphs = new Set();
|
|
1854
|
+
this.clusterPositions = [];
|
|
1855
|
+
this.clusterContoursScratch = [];
|
|
1856
|
+
this.taskScratch = [];
|
|
1857
|
+
this.cache = cache;
|
|
1858
|
+
this.loadedFont = loadedFont;
|
|
1859
|
+
this.tessellator = new Tessellator();
|
|
1860
|
+
this.extruder = new Extruder();
|
|
1861
|
+
this.clusterer = new BoundaryClusterer();
|
|
1862
|
+
this.collector = new GlyphContourCollector();
|
|
1863
|
+
this.drawCallbacks = DrawCallbacks.getSharedDrawCallbackHandler(this.loadedFont);
|
|
1864
|
+
this.drawCallbacks.createDrawFuncs(this.loadedFont, this.collector);
|
|
1865
|
+
this.contourCache = sharedCaches.globalContourCache;
|
|
1866
|
+
this.wordCache = sharedCaches.globalWordCache;
|
|
1867
|
+
this.clusteringCache = sharedCaches.globalClusteringCache;
|
|
1868
|
+
}
|
|
1869
|
+
getOptimizationStats() {
|
|
1870
|
+
return this.collector.getOptimizationStats();
|
|
1871
|
+
}
|
|
1872
|
+
setCurveFidelityConfig(config) {
|
|
1873
|
+
this.curveFidelityConfig = config;
|
|
1874
|
+
this.collector.setCurveFidelityConfig(config);
|
|
1875
|
+
this.updateCacheKeyPrefix();
|
|
1876
|
+
}
|
|
1877
|
+
setCurveSteps(curveSteps) {
|
|
1878
|
+
// Normalize: unset for undefined/null/non-finite/<=0
|
|
1879
|
+
if (curveSteps === undefined || curveSteps === null) {
|
|
1880
|
+
this.curveSteps = undefined;
|
|
1881
|
+
}
|
|
1882
|
+
else if (!Number.isFinite(curveSteps)) {
|
|
1883
|
+
this.curveSteps = undefined;
|
|
1884
|
+
}
|
|
1885
|
+
else {
|
|
1886
|
+
const stepsInt = Math.round(curveSteps);
|
|
1887
|
+
this.curveSteps = stepsInt >= 1 ? stepsInt : undefined;
|
|
1888
|
+
}
|
|
1889
|
+
this.collector.setCurveSteps(this.curveSteps);
|
|
1890
|
+
this.updateCacheKeyPrefix();
|
|
1891
|
+
}
|
|
1892
|
+
setGeometryOptimization(options) {
|
|
1893
|
+
this.geometryOptimizationOptions = options;
|
|
1894
|
+
this.collector.setGeometryOptimization(options);
|
|
1895
|
+
this.updateCacheKeyPrefix();
|
|
1896
|
+
}
|
|
1897
|
+
setFontId(fontId) {
|
|
1898
|
+
this.fontId = fontId;
|
|
1899
|
+
this.updateCacheKeyPrefix();
|
|
1900
|
+
}
|
|
1901
|
+
updateCacheKeyPrefix() {
|
|
1902
|
+
this.cacheKeyPrefix = `${this.fontId}__${this.getGeometryConfigSignature()}`;
|
|
1903
|
+
}
|
|
1904
|
+
getGeometryConfigSignature() {
|
|
1905
|
+
const curveSignature = (() => {
|
|
1906
|
+
if (this.curveSteps !== undefined) {
|
|
1907
|
+
return `cf:steps:${this.curveSteps}`;
|
|
1908
|
+
}
|
|
1909
|
+
const distanceTolerance = this.curveFidelityConfig?.distanceTolerance ??
|
|
1910
|
+
DEFAULT_CURVE_FIDELITY.distanceTolerance;
|
|
1911
|
+
const angleTolerance = this.curveFidelityConfig?.angleTolerance ??
|
|
1912
|
+
DEFAULT_CURVE_FIDELITY.angleTolerance;
|
|
1913
|
+
return `cf:${distanceTolerance.toFixed(4)},${angleTolerance.toFixed(4)}`;
|
|
1914
|
+
})();
|
|
1915
|
+
const enabled = this.geometryOptimizationOptions?.enabled ??
|
|
1916
|
+
DEFAULT_OPTIMIZATION_CONFIG.enabled;
|
|
1917
|
+
const areaThreshold = this.geometryOptimizationOptions?.areaThreshold ??
|
|
1918
|
+
DEFAULT_OPTIMIZATION_CONFIG.areaThreshold;
|
|
1919
|
+
// Use fixed precision to keep cache keys stable and avoid float noise
|
|
1920
|
+
return [
|
|
1921
|
+
curveSignature,
|
|
1922
|
+
`opt:${enabled ? 1 : 0},${areaThreshold.toFixed(4)}`
|
|
1923
|
+
].join('|');
|
|
1924
|
+
}
|
|
1925
|
+
buildInstancedGeometry(clustersByLine, depth, removeOverlaps, isCFF, scale, separateGlyphs = false, coloredTextIndices) {
|
|
1926
|
+
if (isLogEnabled) {
|
|
1927
|
+
let wordCount = 0;
|
|
1928
|
+
for (let i = 0; i < clustersByLine.length; i++) {
|
|
1929
|
+
wordCount += clustersByLine[i].length;
|
|
1930
|
+
}
|
|
1931
|
+
perfLogger.start('GlyphGeometryBuilder.buildInstancedGeometry', {
|
|
1932
|
+
lineCount: clustersByLine.length,
|
|
1933
|
+
wordCount,
|
|
1934
|
+
depth,
|
|
1935
|
+
removeOverlaps
|
|
1936
|
+
});
|
|
1937
|
+
}
|
|
1938
|
+
else {
|
|
1939
|
+
perfLogger.start('GlyphGeometryBuilder.buildInstancedGeometry');
|
|
1940
|
+
}
|
|
1941
|
+
const tasks = this.taskScratch;
|
|
1942
|
+
tasks.length = 0;
|
|
1943
|
+
let taskCount = 0;
|
|
1944
|
+
let totalVertexFloats = 0;
|
|
1945
|
+
let totalNormalFloats = 0;
|
|
1946
|
+
let totalIndexCount = 0;
|
|
1947
|
+
let vertexCursor = 0; // vertex offset (not float offset)
|
|
1948
|
+
const pushTask = (data, px, py, pz) => {
|
|
1949
|
+
const vertexStart = vertexCursor;
|
|
1950
|
+
let task = tasks[taskCount];
|
|
1951
|
+
if (task) {
|
|
1952
|
+
task.data = data;
|
|
1953
|
+
task.px = px;
|
|
1954
|
+
task.py = py;
|
|
1955
|
+
task.pz = pz;
|
|
1956
|
+
task.vertexStart = vertexStart;
|
|
1957
|
+
}
|
|
1958
|
+
else {
|
|
1959
|
+
task = { data, px, py, pz, vertexStart };
|
|
1960
|
+
tasks[taskCount] = task;
|
|
1961
|
+
}
|
|
1962
|
+
taskCount++;
|
|
1963
|
+
totalVertexFloats += data.vertices.length;
|
|
1964
|
+
totalNormalFloats += data.normals.length;
|
|
1965
|
+
totalIndexCount += data.indices.length;
|
|
1966
|
+
vertexCursor += data.vertices.length / 3;
|
|
1967
|
+
return vertexStart;
|
|
1968
|
+
};
|
|
1969
|
+
const glyphInfos = [];
|
|
1970
|
+
const planeBounds = {
|
|
1971
|
+
min: { x: Infinity, y: Infinity, z: 0 },
|
|
1972
|
+
max: { x: -Infinity, y: -Infinity, z: depth }
|
|
1973
|
+
};
|
|
1974
|
+
for (let lineIndex = 0; lineIndex < clustersByLine.length; lineIndex++) {
|
|
1975
|
+
const line = clustersByLine[lineIndex];
|
|
1976
|
+
for (const cluster of line) {
|
|
1977
|
+
const clusterX = cluster.position.x;
|
|
1978
|
+
const clusterY = cluster.position.y;
|
|
1979
|
+
const clusterZ = cluster.position.z;
|
|
1980
|
+
const clusterGlyphContours = [];
|
|
1981
|
+
for (const glyph of cluster.glyphs) {
|
|
1982
|
+
clusterGlyphContours.push(this.getContoursForGlyph(glyph.g));
|
|
1983
|
+
}
|
|
1984
|
+
let boundaryGroups;
|
|
1985
|
+
if (cluster.glyphs.length <= 1) {
|
|
1986
|
+
boundaryGroups = [[0]];
|
|
1987
|
+
}
|
|
1988
|
+
else {
|
|
1989
|
+
// Check clustering cache (same text + glyph IDs + positions = same overlap groups)
|
|
1990
|
+
// Key must be font-specific; glyph ids/bounds differ between fonts
|
|
1991
|
+
// Positions must match since overlap detection depends on relative glyph placement
|
|
1992
|
+
const cacheKey = `${this.cacheKeyPrefix}_${cluster.text}`;
|
|
1993
|
+
const cached = this.clusteringCache.get(cacheKey);
|
|
1994
|
+
let isValid = false;
|
|
1995
|
+
if (cached && cached.glyphIds.length === cluster.glyphs.length) {
|
|
1996
|
+
isValid = true;
|
|
1997
|
+
for (let i = 0; i < cluster.glyphs.length; i++) {
|
|
1998
|
+
const glyph = cluster.glyphs[i];
|
|
1999
|
+
const cachedPos = cached.positions[i];
|
|
2000
|
+
if (cached.glyphIds[i] !== glyph.g ||
|
|
2001
|
+
cachedPos.x !== (glyph.x ?? 0) ||
|
|
2002
|
+
cachedPos.y !== (glyph.y ?? 0)) {
|
|
2003
|
+
isValid = false;
|
|
2004
|
+
break;
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
if (isValid && cached) {
|
|
2009
|
+
boundaryGroups = cached.groups;
|
|
2010
|
+
}
|
|
2011
|
+
else {
|
|
2012
|
+
const glyphCount = cluster.glyphs.length;
|
|
2013
|
+
if (this.clusterPositions.length < glyphCount) {
|
|
2014
|
+
for (let i = this.clusterPositions.length; i < glyphCount; i++) {
|
|
2015
|
+
this.clusterPositions.push(new Vec3(0, 0, 0));
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
this.clusterPositions.length = glyphCount;
|
|
2019
|
+
for (let i = 0; i < glyphCount; i++) {
|
|
2020
|
+
const glyph = cluster.glyphs[i];
|
|
2021
|
+
const pos = this.clusterPositions[i];
|
|
2022
|
+
pos.x = glyph.x ?? 0;
|
|
2023
|
+
pos.y = glyph.y ?? 0;
|
|
2024
|
+
pos.z = 0;
|
|
2025
|
+
}
|
|
2026
|
+
boundaryGroups = this.clusterer.cluster(clusterGlyphContours, this.clusterPositions);
|
|
2027
|
+
this.clusteringCache.set(cacheKey, {
|
|
2028
|
+
glyphIds: cluster.glyphs.map((g) => g.g),
|
|
2029
|
+
positions: cluster.glyphs.map((g) => ({
|
|
2030
|
+
x: g.x ?? 0,
|
|
2031
|
+
y: g.y ?? 0
|
|
2032
|
+
})),
|
|
2033
|
+
groups: boundaryGroups
|
|
2034
|
+
});
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
const forceSeparate = separateGlyphs;
|
|
2038
|
+
// Split boundary groups so colored and non-colored glyphs don't merge together
|
|
2039
|
+
// This preserves overlap removal within each color class while keeping
|
|
2040
|
+
// geometry separate for accurate vertex coloring
|
|
2041
|
+
let finalGroups = boundaryGroups;
|
|
2042
|
+
if (coloredTextIndices && coloredTextIndices.size > 0) {
|
|
2043
|
+
finalGroups = [];
|
|
2044
|
+
for (const group of boundaryGroups) {
|
|
2045
|
+
if (group.length <= 1) {
|
|
2046
|
+
finalGroups.push(group);
|
|
2047
|
+
}
|
|
2048
|
+
else {
|
|
2049
|
+
// Split group into colored and non-colored sub-groups
|
|
2050
|
+
const coloredIndices = [];
|
|
2051
|
+
const nonColoredIndices = [];
|
|
2052
|
+
for (const idx of group) {
|
|
2053
|
+
const glyph = cluster.glyphs[idx];
|
|
2054
|
+
if (coloredTextIndices.has(glyph.absoluteTextIndex)) {
|
|
2055
|
+
coloredIndices.push(idx);
|
|
2056
|
+
}
|
|
2057
|
+
else {
|
|
2058
|
+
nonColoredIndices.push(idx);
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
// Add non-empty sub-groups
|
|
2062
|
+
if (coloredIndices.length > 0) {
|
|
2063
|
+
finalGroups.push(coloredIndices);
|
|
2064
|
+
}
|
|
2065
|
+
if (nonColoredIndices.length > 0) {
|
|
2066
|
+
finalGroups.push(nonColoredIndices);
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
// Iterate over the geometric groups identified by BoundaryClusterer
|
|
2072
|
+
// logical groups (words) split into geometric sub-groups
|
|
2073
|
+
for (const groupIndices of finalGroups) {
|
|
2074
|
+
const isOverlappingGroup = groupIndices.length > 1;
|
|
2075
|
+
const shouldCluster = isOverlappingGroup && !forceSeparate;
|
|
2076
|
+
if (shouldCluster) {
|
|
2077
|
+
// Cluster-level caching for this specific group of overlapping glyphs
|
|
2078
|
+
const subClusterGlyphs = groupIndices.map((i) => cluster.glyphs[i]);
|
|
2079
|
+
const clusterKey = this.getClusterKey(subClusterGlyphs, depth, removeOverlaps);
|
|
2080
|
+
let cachedCluster = this.wordCache.get(clusterKey);
|
|
2081
|
+
if (!cachedCluster) {
|
|
2082
|
+
const clusterContours = this.clusterContoursScratch;
|
|
2083
|
+
let contourIndex = 0;
|
|
2084
|
+
const refX = subClusterGlyphs[0].x ?? 0;
|
|
2085
|
+
const refY = subClusterGlyphs[0].y ?? 0;
|
|
2086
|
+
for (let i = 0; i < groupIndices.length; i++) {
|
|
2087
|
+
const originalIndex = groupIndices[i];
|
|
2088
|
+
const glyphContours = clusterGlyphContours[originalIndex];
|
|
2089
|
+
const glyph = cluster.glyphs[originalIndex];
|
|
2090
|
+
const relX = (glyph.x ?? 0) - refX;
|
|
2091
|
+
const relY = (glyph.y ?? 0) - refY;
|
|
2092
|
+
for (const path of glyphContours.paths) {
|
|
2093
|
+
const points = path.points;
|
|
2094
|
+
const pointCount = points.length;
|
|
2095
|
+
if (pointCount < 3)
|
|
2096
|
+
continue;
|
|
2097
|
+
const isClosed = pointCount > 1 &&
|
|
2098
|
+
points[0].x === points[pointCount - 1].x &&
|
|
2099
|
+
points[0].y === points[pointCount - 1].y;
|
|
2100
|
+
const end = isClosed ? pointCount - 1 : pointCount;
|
|
2101
|
+
const needed = (end + 1) * 2;
|
|
2102
|
+
let contour = clusterContours[contourIndex];
|
|
2103
|
+
if (!contour || contour.length < needed) {
|
|
2104
|
+
contour = new Array(needed);
|
|
2105
|
+
clusterContours[contourIndex] = contour;
|
|
2106
|
+
}
|
|
2107
|
+
else {
|
|
2108
|
+
contour.length = needed;
|
|
2109
|
+
}
|
|
2110
|
+
let out = 0;
|
|
2111
|
+
for (let k = 0; k < end; k++) {
|
|
2112
|
+
const pt = points[k];
|
|
2113
|
+
contour[out++] = pt.x + relX;
|
|
2114
|
+
contour[out++] = pt.y + relY;
|
|
2115
|
+
}
|
|
2116
|
+
if (out >= 2) {
|
|
2117
|
+
contour[out++] = contour[0];
|
|
2118
|
+
contour[out++] = contour[1];
|
|
2119
|
+
}
|
|
2120
|
+
contourIndex++;
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
clusterContours.length = contourIndex;
|
|
2124
|
+
cachedCluster = this.tessellateGlyphCluster(clusterContours, depth, isCFF);
|
|
2125
|
+
this.wordCache.set(clusterKey, cachedCluster);
|
|
2126
|
+
}
|
|
2127
|
+
// Calculate the absolute position of this sub-cluster based on its first glyph
|
|
2128
|
+
// (since the cached geometry is relative to that first glyph)
|
|
2129
|
+
const firstGlyphInGroup = subClusterGlyphs[0];
|
|
2130
|
+
const groupPosX = clusterX + (firstGlyphInGroup.x ?? 0);
|
|
2131
|
+
const groupPosY = clusterY + (firstGlyphInGroup.y ?? 0);
|
|
2132
|
+
const groupPosZ = clusterZ;
|
|
2133
|
+
const vertexStart = pushTask(cachedCluster, groupPosX, groupPosY, groupPosZ);
|
|
2134
|
+
const clusterVertexCount = cachedCluster.vertices.length / 3;
|
|
2135
|
+
for (let i = 0; i < groupIndices.length; i++) {
|
|
2136
|
+
const originalIndex = groupIndices[i];
|
|
2137
|
+
const glyph = cluster.glyphs[originalIndex];
|
|
2138
|
+
const glyphContours = clusterGlyphContours[originalIndex];
|
|
2139
|
+
const glyphPosX = clusterX + (glyph.x ?? 0);
|
|
2140
|
+
const glyphPosY = clusterY + (glyph.y ?? 0);
|
|
2141
|
+
const glyphPosZ = clusterZ;
|
|
2142
|
+
const glyphInfo = this.createGlyphInfo(glyph, vertexStart, clusterVertexCount, glyphPosX, glyphPosY, glyphPosZ, glyphContours, depth);
|
|
2143
|
+
glyphInfos.push(glyphInfo);
|
|
2144
|
+
this.updatePlaneBounds(glyphInfo.bounds, planeBounds);
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
else {
|
|
2148
|
+
// Glyph-level caching (standard path for isolated glyphs or when forced separate)
|
|
2149
|
+
for (const i of groupIndices) {
|
|
2150
|
+
const glyph = cluster.glyphs[i];
|
|
2151
|
+
const glyphContours = clusterGlyphContours[i];
|
|
2152
|
+
const glyphPosX = clusterX + (glyph.x ?? 0);
|
|
2153
|
+
const glyphPosY = clusterY + (glyph.y ?? 0);
|
|
2154
|
+
const glyphPosZ = clusterZ;
|
|
2155
|
+
// Skip glyphs with no paths (spaces, zero-width characters, etc.)
|
|
2156
|
+
if (glyphContours.paths.length === 0) {
|
|
2157
|
+
const glyphInfo = this.createGlyphInfo(glyph, 0, 0, glyphPosX, glyphPosY, glyphPosZ, glyphContours, depth);
|
|
2158
|
+
glyphInfos.push(glyphInfo);
|
|
2159
|
+
continue;
|
|
2160
|
+
}
|
|
2161
|
+
const glyphCacheKey = sharedCaches.getGlyphCacheKey(this.cacheKeyPrefix, glyph.g, depth, removeOverlaps);
|
|
2162
|
+
let cachedGlyph = this.cache.get(glyphCacheKey);
|
|
2163
|
+
if (!cachedGlyph) {
|
|
2164
|
+
cachedGlyph = this.tessellateGlyph(glyphContours, depth, removeOverlaps, isCFF);
|
|
2165
|
+
this.cache.set(glyphCacheKey, cachedGlyph);
|
|
2166
|
+
}
|
|
2167
|
+
else {
|
|
2168
|
+
cachedGlyph.useCount++;
|
|
2169
|
+
}
|
|
2170
|
+
const vertexStart = pushTask(cachedGlyph, glyphPosX, glyphPosY, glyphPosZ);
|
|
2171
|
+
const glyphInfo = this.createGlyphInfo(glyph, vertexStart, cachedGlyph.vertices.length / 3, glyphPosX, glyphPosY, glyphPosZ, glyphContours, depth);
|
|
2172
|
+
glyphInfos.push(glyphInfo);
|
|
2173
|
+
this.updatePlaneBounds(glyphInfo.bounds, planeBounds);
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
tasks.length = taskCount;
|
|
2180
|
+
const vertexArray = new Float32Array(totalVertexFloats);
|
|
2181
|
+
const normalArray = new Float32Array(totalNormalFloats);
|
|
2182
|
+
const indexArray = new Uint32Array(totalIndexCount);
|
|
2183
|
+
let vertexPos = 0; // float index (multiple of 3)
|
|
2184
|
+
let normalPos = 0; // float index (multiple of 3)
|
|
2185
|
+
let indexPos = 0; // index count
|
|
2186
|
+
for (let t = 0; t < tasks.length; t++) {
|
|
2187
|
+
const task = tasks[t];
|
|
2188
|
+
const v = task.data.vertices;
|
|
2189
|
+
const n = task.data.normals;
|
|
2190
|
+
const idx = task.data.indices;
|
|
2191
|
+
const px = task.px;
|
|
2192
|
+
const py = task.py;
|
|
2193
|
+
const pz = task.pz;
|
|
2194
|
+
const offsetX = px * scale;
|
|
2195
|
+
const offsetY = py * scale;
|
|
2196
|
+
const offsetZ = pz * scale;
|
|
2197
|
+
const vLen = v.length;
|
|
2198
|
+
let outPos = vertexPos;
|
|
2199
|
+
for (let j = 0; j < vLen; j += 3) {
|
|
2200
|
+
vertexArray[outPos] = v[j] * scale + offsetX;
|
|
2201
|
+
vertexArray[outPos + 1] = v[j + 1] * scale + offsetY;
|
|
2202
|
+
vertexArray[outPos + 2] = v[j + 2] * scale + offsetZ;
|
|
2203
|
+
outPos += 3;
|
|
2204
|
+
}
|
|
2205
|
+
vertexPos = outPos;
|
|
2206
|
+
normalArray.set(n, normalPos);
|
|
2207
|
+
normalPos += n.length;
|
|
2208
|
+
const vertexStart = task.vertexStart;
|
|
2209
|
+
const idxLen = idx.length;
|
|
2210
|
+
let outIndexPos = indexPos;
|
|
2211
|
+
for (let j = 0; j < idxLen; j++) {
|
|
2212
|
+
indexArray[outIndexPos++] = idx[j] + vertexStart;
|
|
2213
|
+
}
|
|
2214
|
+
indexPos = outIndexPos;
|
|
2215
|
+
}
|
|
2216
|
+
perfLogger.end('GlyphGeometryBuilder.buildInstancedGeometry');
|
|
2217
|
+
planeBounds.min.x *= scale;
|
|
2218
|
+
planeBounds.min.y *= scale;
|
|
2219
|
+
planeBounds.min.z *= scale;
|
|
2220
|
+
planeBounds.max.x *= scale;
|
|
2221
|
+
planeBounds.max.y *= scale;
|
|
2222
|
+
planeBounds.max.z *= scale;
|
|
2223
|
+
for (let i = 0; i < glyphInfos.length; i++) {
|
|
2224
|
+
glyphInfos[i].bounds.min.x *= scale;
|
|
2225
|
+
glyphInfos[i].bounds.min.y *= scale;
|
|
2226
|
+
glyphInfos[i].bounds.min.z *= scale;
|
|
2227
|
+
glyphInfos[i].bounds.max.x *= scale;
|
|
2228
|
+
glyphInfos[i].bounds.max.y *= scale;
|
|
2229
|
+
glyphInfos[i].bounds.max.z *= scale;
|
|
2230
|
+
}
|
|
2231
|
+
return {
|
|
2232
|
+
vertices: vertexArray,
|
|
2233
|
+
normals: normalArray,
|
|
2234
|
+
indices: indexArray,
|
|
2235
|
+
glyphInfos,
|
|
2236
|
+
planeBounds
|
|
2237
|
+
};
|
|
2238
|
+
}
|
|
2239
|
+
getClusterKey(glyphs, depth, removeOverlaps) {
|
|
2240
|
+
if (glyphs.length === 0)
|
|
2241
|
+
return '';
|
|
2242
|
+
const refX = glyphs[0].x ?? 0;
|
|
2243
|
+
const refY = glyphs[0].y ?? 0;
|
|
2244
|
+
const parts = glyphs.map((g) => {
|
|
2245
|
+
const relX = (g.x ?? 0) - refX;
|
|
2246
|
+
const relY = (g.y ?? 0) - refY;
|
|
2247
|
+
return `${g.g}:${relX},${relY}`;
|
|
2248
|
+
});
|
|
2249
|
+
const ids = parts.join('|');
|
|
2250
|
+
const roundedDepth = Math.round(depth * 1000) / 1000;
|
|
2251
|
+
return `${this.cacheKeyPrefix}_${ids}_${roundedDepth}_${removeOverlaps}`;
|
|
2252
|
+
}
|
|
2253
|
+
createGlyphInfo(glyph, vertexStart, vertexCount, positionX, positionY, positionZ, contours, depth) {
|
|
2254
|
+
return {
|
|
2255
|
+
textIndex: glyph.absoluteTextIndex,
|
|
2256
|
+
lineIndex: glyph.lineIndex,
|
|
2257
|
+
vertexStart,
|
|
2258
|
+
vertexCount,
|
|
2259
|
+
bounds: {
|
|
2260
|
+
min: {
|
|
2261
|
+
x: contours.bounds.min.x + positionX,
|
|
2262
|
+
y: contours.bounds.min.y + positionY,
|
|
2263
|
+
z: positionZ
|
|
2264
|
+
},
|
|
2265
|
+
max: {
|
|
2266
|
+
x: contours.bounds.max.x + positionX,
|
|
2267
|
+
y: contours.bounds.max.y + positionY,
|
|
2268
|
+
z: positionZ + depth
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
};
|
|
2272
|
+
}
|
|
2273
|
+
getContoursForGlyph(glyphId) {
|
|
2274
|
+
// Fast path: skip HarfBuzz draw for known-empty glyphs (spaces, zero-width, etc)
|
|
2275
|
+
if (this.emptyGlyphs.has(glyphId)) {
|
|
2276
|
+
return {
|
|
2277
|
+
glyphId,
|
|
2278
|
+
paths: [],
|
|
2279
|
+
bounds: {
|
|
2280
|
+
min: { x: 0, y: 0 },
|
|
2281
|
+
max: { x: 0, y: 0 }
|
|
2282
|
+
}
|
|
2283
|
+
};
|
|
2284
|
+
}
|
|
2285
|
+
const key = `${this.cacheKeyPrefix}_${glyphId}`;
|
|
2286
|
+
const cached = this.contourCache.get(key);
|
|
2287
|
+
if (cached) {
|
|
2288
|
+
return cached;
|
|
2289
|
+
}
|
|
2290
|
+
// Rebind collector before draw operation
|
|
2291
|
+
this.drawCallbacks.setCollector(this.collector);
|
|
2292
|
+
this.collector.reset();
|
|
2293
|
+
this.collector.beginGlyph(glyphId, 0);
|
|
2294
|
+
this.loadedFont.module.exports.hb_font_draw_glyph(this.loadedFont.font.ptr, glyphId, this.drawCallbacks.getDrawFuncsPtr(), 0);
|
|
2295
|
+
this.collector.finishGlyph();
|
|
2296
|
+
const collected = this.collector.getCollectedGlyphs()[0];
|
|
2297
|
+
const contours = collected || {
|
|
2298
|
+
glyphId,
|
|
2299
|
+
paths: [],
|
|
2300
|
+
bounds: {
|
|
2301
|
+
min: { x: 0, y: 0 },
|
|
2302
|
+
max: { x: 0, y: 0 }
|
|
2303
|
+
}
|
|
2304
|
+
};
|
|
2305
|
+
// Mark glyph as empty for future fast-path
|
|
2306
|
+
if (contours.paths.length === 0) {
|
|
2307
|
+
this.emptyGlyphs.add(glyphId);
|
|
2308
|
+
}
|
|
2309
|
+
this.contourCache.set(key, contours);
|
|
2310
|
+
return contours;
|
|
2311
|
+
}
|
|
2312
|
+
tessellateGlyphCluster(contours, depth, isCFF) {
|
|
2313
|
+
const processedGeometry = this.tessellator.processContours(contours, true, isCFF, depth !== 0);
|
|
2314
|
+
return this.extrudeAndPackage(processedGeometry, depth);
|
|
2315
|
+
}
|
|
2316
|
+
extrudeAndPackage(processedGeometry, depth) {
|
|
2317
|
+
perfLogger.start('Extruder.extrude', {
|
|
2318
|
+
depth,
|
|
2319
|
+
upem: this.loadedFont.upem
|
|
2320
|
+
});
|
|
2321
|
+
const extrudedResult = this.extruder.extrude(processedGeometry, depth, this.loadedFont.upem);
|
|
2322
|
+
perfLogger.end('Extruder.extrude');
|
|
2323
|
+
const vertices = extrudedResult.vertices;
|
|
2324
|
+
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
|
2325
|
+
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
|
2326
|
+
for (let i = 0; i < vertices.length; i += 3) {
|
|
2327
|
+
const x = vertices[i];
|
|
2328
|
+
const y = vertices[i + 1];
|
|
2329
|
+
const z = vertices[i + 2];
|
|
2330
|
+
if (x < minX)
|
|
2331
|
+
minX = x;
|
|
2332
|
+
if (x > maxX)
|
|
2333
|
+
maxX = x;
|
|
2334
|
+
if (y < minY)
|
|
2335
|
+
minY = y;
|
|
2336
|
+
if (y > maxY)
|
|
2337
|
+
maxY = y;
|
|
2338
|
+
if (z < minZ)
|
|
2339
|
+
minZ = z;
|
|
2340
|
+
if (z > maxZ)
|
|
2341
|
+
maxZ = z;
|
|
2342
|
+
}
|
|
2343
|
+
const boundsMin = new Vec3(minX, minY, minZ);
|
|
2344
|
+
const boundsMax = new Vec3(maxX, maxY, maxZ);
|
|
2345
|
+
return {
|
|
2346
|
+
geometry: processedGeometry,
|
|
2347
|
+
vertices: extrudedResult.vertices,
|
|
2348
|
+
normals: extrudedResult.normals,
|
|
2349
|
+
indices: extrudedResult.indices,
|
|
2350
|
+
bounds: { min: boundsMin, max: boundsMax },
|
|
2351
|
+
useCount: 1
|
|
2352
|
+
};
|
|
2353
|
+
}
|
|
2354
|
+
tessellateGlyph(glyphContours, depth, removeOverlaps, isCFF) {
|
|
2355
|
+
perfLogger.start('GlyphGeometryBuilder.tessellateGlyph', {
|
|
2356
|
+
glyphId: glyphContours.glyphId,
|
|
2357
|
+
pathCount: glyphContours.paths.length
|
|
2358
|
+
});
|
|
2359
|
+
const processedGeometry = this.tessellator.process(glyphContours.paths, removeOverlaps, isCFF, depth !== 0);
|
|
2360
|
+
perfLogger.end('GlyphGeometryBuilder.tessellateGlyph');
|
|
2361
|
+
return this.extrudeAndPackage(processedGeometry, depth);
|
|
2362
|
+
}
|
|
2363
|
+
updatePlaneBounds(glyphBounds, planeBounds) {
|
|
2364
|
+
const pMin = planeBounds.min;
|
|
2365
|
+
const pMax = planeBounds.max;
|
|
2366
|
+
const gMin = glyphBounds.min;
|
|
2367
|
+
const gMax = glyphBounds.max;
|
|
2368
|
+
if (gMin.x < pMin.x)
|
|
2369
|
+
pMin.x = gMin.x;
|
|
2370
|
+
if (gMin.y < pMin.y)
|
|
2371
|
+
pMin.y = gMin.y;
|
|
2372
|
+
if (gMin.z < pMin.z)
|
|
2373
|
+
pMin.z = gMin.z;
|
|
2374
|
+
if (gMax.x > pMax.x)
|
|
2375
|
+
pMax.x = gMax.x;
|
|
2376
|
+
if (gMax.y > pMax.y)
|
|
2377
|
+
pMax.y = gMax.y;
|
|
2378
|
+
if (gMax.z > pMax.z)
|
|
2379
|
+
pMax.z = gMax.z;
|
|
2380
|
+
}
|
|
2381
|
+
getCacheStats() {
|
|
2382
|
+
return this.cache.getStats();
|
|
2383
|
+
}
|
|
2384
|
+
clearCache() {
|
|
2385
|
+
this.cache.clear();
|
|
2386
|
+
this.wordCache.clear();
|
|
2387
|
+
this.clusteringCache.clear();
|
|
2388
|
+
this.contourCache.clear();
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
class MeshGeometryBuilder {
|
|
2393
|
+
constructor(loadedFont, fontId) {
|
|
2394
|
+
this.loadedFont = loadedFont;
|
|
2395
|
+
this.fontId = fontId;
|
|
2396
|
+
}
|
|
2397
|
+
setFont(loadedFont, fontId) {
|
|
2398
|
+
this.loadedFont = loadedFont;
|
|
2399
|
+
this.fontId = fontId;
|
|
2400
|
+
this.geometryBuilder = undefined;
|
|
2401
|
+
}
|
|
2402
|
+
build(layout, options) {
|
|
2403
|
+
perfLogger.start('MeshGeometryBuilder.build', {
|
|
2404
|
+
textLength: options.text.length
|
|
2405
|
+
});
|
|
2406
|
+
try {
|
|
2407
|
+
if (!this.geometryBuilder) {
|
|
2408
|
+
this.geometryBuilder = new GlyphGeometryBuilder(sharedCaches.globalGlyphCache, this.loadedFont);
|
|
2409
|
+
this.geometryBuilder.setFontId(this.fontId);
|
|
2410
|
+
}
|
|
2411
|
+
const useCurveSteps = options.curveSteps !== undefined &&
|
|
2412
|
+
options.curveSteps !== null &&
|
|
2413
|
+
options.curveSteps > 0;
|
|
2414
|
+
this.geometryBuilder.setCurveSteps(options.curveSteps);
|
|
2415
|
+
this.geometryBuilder.setCurveFidelityConfig(useCurveSteps ? undefined : options.curveFidelity);
|
|
2416
|
+
this.geometryBuilder.setGeometryOptimization(options.geometryOptimization);
|
|
2417
|
+
const shouldRemoveOverlaps = options.removeOverlaps ?? this.loadedFont.isVariable ?? false;
|
|
2418
|
+
let coloredTextIndices;
|
|
2419
|
+
let byTextMatches;
|
|
2420
|
+
if (options.color &&
|
|
2421
|
+
typeof options.color === 'object' &&
|
|
2422
|
+
!Array.isArray(options.color)) {
|
|
2423
|
+
if (options.color.byText || options.color.byCharRange) {
|
|
2424
|
+
coloredTextIndices = new Set();
|
|
2425
|
+
if (options.color.byText) {
|
|
2426
|
+
byTextMatches = [];
|
|
2427
|
+
for (const pattern of Object.keys(options.color.byText)) {
|
|
2428
|
+
let index = 0;
|
|
2429
|
+
while ((index = options.text.indexOf(pattern, index)) !== -1) {
|
|
2430
|
+
byTextMatches.push({
|
|
2431
|
+
pattern,
|
|
2432
|
+
start: index,
|
|
2433
|
+
end: index + pattern.length
|
|
2434
|
+
});
|
|
2435
|
+
for (let i = index; i < index + pattern.length; i++) {
|
|
2436
|
+
coloredTextIndices.add(i);
|
|
2437
|
+
}
|
|
2438
|
+
index += pattern.length;
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
if (options.color.byCharRange) {
|
|
2443
|
+
for (const range of options.color.byCharRange) {
|
|
2444
|
+
for (let i = range.start; i < range.end; i++) {
|
|
2445
|
+
coloredTextIndices.add(i);
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
const shapedResult = this.geometryBuilder.buildInstancedGeometry(layout.clustersByLine, layout.layoutData.depth, shouldRemoveOverlaps, this.loadedFont.metrics.isCFF, layout.layoutData.pixelsPerFontUnit, options.perGlyphAttributes ?? false, coloredTextIndices);
|
|
2452
|
+
const result = this.finalizeGeometry(shapedResult.vertices, shapedResult.normals, shapedResult.indices, shapedResult.glyphInfos, shapedResult.planeBounds, options, options.text, byTextMatches);
|
|
2453
|
+
if (options.perGlyphAttributes) {
|
|
2454
|
+
const glyphAttrs = this.createGlyphAttributes(result.vertices.length / 3, result.glyphs);
|
|
2455
|
+
result.glyphAttributes = glyphAttrs;
|
|
2456
|
+
}
|
|
2457
|
+
return result;
|
|
2458
|
+
}
|
|
2459
|
+
finally {
|
|
2460
|
+
perfLogger.end('MeshGeometryBuilder.build');
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
getCacheSize() {
|
|
2464
|
+
return this.geometryBuilder?.getCacheStats().size ?? 0;
|
|
2465
|
+
}
|
|
2466
|
+
clearCache() {
|
|
2467
|
+
this.geometryBuilder?.clearCache();
|
|
2468
|
+
}
|
|
2469
|
+
reset() {
|
|
2470
|
+
this.geometryBuilder = undefined;
|
|
2471
|
+
this.textLayout = undefined;
|
|
2472
|
+
}
|
|
2473
|
+
finalizeGeometry(vertices, normals, indices, glyphInfoArray, planeBounds, options, originalText, byTextMatches) {
|
|
2474
|
+
const { layout = {} } = options;
|
|
2475
|
+
const { width, align = layout.direction === 'rtl' ? 'right' : 'left' } = layout;
|
|
2476
|
+
if (!this.textLayout) {
|
|
2477
|
+
this.textLayout = new TextLayout.TextLayout(this.loadedFont);
|
|
2478
|
+
}
|
|
2479
|
+
const alignmentResult = this.textLayout.computeAlignmentOffset({
|
|
2480
|
+
width,
|
|
2481
|
+
align,
|
|
2482
|
+
planeBounds
|
|
2483
|
+
});
|
|
2484
|
+
const offset = alignmentResult.offset;
|
|
2485
|
+
planeBounds.min.x = alignmentResult.adjustedBounds.min.x;
|
|
2486
|
+
planeBounds.max.x = alignmentResult.adjustedBounds.max.x;
|
|
2487
|
+
if (offset !== 0) {
|
|
2488
|
+
for (let i = 0; i < vertices.length; i += 3) {
|
|
2489
|
+
vertices[i] += offset;
|
|
2490
|
+
}
|
|
2491
|
+
for (let i = 0; i < glyphInfoArray.length; i++) {
|
|
2492
|
+
glyphInfoArray[i].bounds.min.x += offset;
|
|
2493
|
+
glyphInfoArray[i].bounds.max.x += offset;
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
let colors;
|
|
2497
|
+
let coloredRanges;
|
|
2498
|
+
if (options.color) {
|
|
2499
|
+
const colorResult = this.applyColorSystem(vertices, glyphInfoArray, options.color, options.text, byTextMatches);
|
|
2500
|
+
colors = colorResult.colors;
|
|
2501
|
+
coloredRanges = colorResult.coloredRanges;
|
|
2502
|
+
}
|
|
2503
|
+
const optimizationStats = this.geometryBuilder.getOptimizationStats();
|
|
2504
|
+
const trianglesGenerated = indices.length / 3;
|
|
2505
|
+
const verticesGenerated = vertices.length / 3;
|
|
2506
|
+
return {
|
|
2507
|
+
vertices,
|
|
2508
|
+
normals,
|
|
2509
|
+
indices,
|
|
2510
|
+
colors,
|
|
2511
|
+
glyphs: glyphInfoArray,
|
|
2512
|
+
planeBounds,
|
|
2513
|
+
stats: {
|
|
2514
|
+
trianglesGenerated,
|
|
2515
|
+
verticesGenerated,
|
|
2516
|
+
pointsRemovedByVisvalingam: optimizationStats.pointsRemovedByVisvalingam,
|
|
2517
|
+
originalPointCount: optimizationStats.originalPointCount
|
|
2518
|
+
},
|
|
2519
|
+
query: (() => {
|
|
2520
|
+
let cachedQuery = null;
|
|
2521
|
+
return (queryOptions) => {
|
|
2522
|
+
if (!originalText) {
|
|
2523
|
+
throw new Error('Original text not available for querying');
|
|
2524
|
+
}
|
|
2525
|
+
if (!cachedQuery) {
|
|
2526
|
+
cachedQuery = new TextRangeQuery.TextRangeQuery(originalText, glyphInfoArray);
|
|
2527
|
+
}
|
|
2528
|
+
return cachedQuery.execute(queryOptions);
|
|
2529
|
+
};
|
|
2530
|
+
})(),
|
|
2531
|
+
coloredRanges,
|
|
2532
|
+
glyphAttributes: undefined
|
|
2533
|
+
};
|
|
2534
|
+
}
|
|
2535
|
+
applyColorSystem(vertices, glyphInfoArray, color, originalText, byTextMatches) {
|
|
2536
|
+
const vertexCount = vertices.length / 3;
|
|
2537
|
+
const colors = new Float32Array(vertexCount * 3);
|
|
2538
|
+
const coloredRanges = [];
|
|
2539
|
+
if (Array.isArray(color)) {
|
|
2540
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
2541
|
+
const baseIndex = i * 3;
|
|
2542
|
+
colors[baseIndex] = color[0];
|
|
2543
|
+
colors[baseIndex + 1] = color[1];
|
|
2544
|
+
colors[baseIndex + 2] = color[2];
|
|
2545
|
+
}
|
|
2546
|
+
coloredRanges.push({
|
|
2547
|
+
start: 0,
|
|
2548
|
+
end: originalText.length,
|
|
2549
|
+
originalText,
|
|
2550
|
+
color,
|
|
2551
|
+
bounds: [],
|
|
2552
|
+
glyphs: glyphInfoArray,
|
|
2553
|
+
lineIndices: [...new Set(glyphInfoArray.map((g) => g.lineIndex))]
|
|
2554
|
+
});
|
|
2555
|
+
}
|
|
2556
|
+
else {
|
|
2557
|
+
const defaultColor = color.default || [1, 1, 1];
|
|
2558
|
+
for (let i = 0; i < colors.length; i += 3) {
|
|
2559
|
+
colors[i] = defaultColor[0];
|
|
2560
|
+
colors[i + 1] = defaultColor[1];
|
|
2561
|
+
colors[i + 2] = defaultColor[2];
|
|
2562
|
+
}
|
|
2563
|
+
let glyphsByTextIndex;
|
|
2564
|
+
if ((color.byText && byTextMatches) || color.byCharRange) {
|
|
2565
|
+
glyphsByTextIndex = new Map();
|
|
2566
|
+
for (const glyph of glyphInfoArray) {
|
|
2567
|
+
const existing = glyphsByTextIndex.get(glyph.textIndex);
|
|
2568
|
+
if (existing) {
|
|
2569
|
+
existing.push(glyph);
|
|
2570
|
+
}
|
|
2571
|
+
else {
|
|
2572
|
+
glyphsByTextIndex.set(glyph.textIndex, [glyph]);
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
if (color.byText && byTextMatches && glyphsByTextIndex) {
|
|
2577
|
+
for (const match of byTextMatches) {
|
|
2578
|
+
const targetColor = color.byText[match.pattern];
|
|
2579
|
+
if (!targetColor)
|
|
2580
|
+
continue;
|
|
2581
|
+
const matchGlyphs = [];
|
|
2582
|
+
const lineGroups = new Map();
|
|
2583
|
+
for (let i = match.start; i < match.end; i++) {
|
|
2584
|
+
const glyphs = glyphsByTextIndex.get(i);
|
|
2585
|
+
if (glyphs) {
|
|
2586
|
+
for (const glyph of glyphs) {
|
|
2587
|
+
matchGlyphs.push(glyph);
|
|
2588
|
+
const lineGlyphs = lineGroups.get(glyph.lineIndex);
|
|
2589
|
+
if (lineGlyphs) {
|
|
2590
|
+
lineGlyphs.push(glyph);
|
|
2591
|
+
}
|
|
2592
|
+
else {
|
|
2593
|
+
lineGroups.set(glyph.lineIndex, [glyph]);
|
|
2594
|
+
}
|
|
2595
|
+
for (let v = 0; v < glyph.vertexCount; v++) {
|
|
2596
|
+
const vertexIndex = (glyph.vertexStart + v) * 3;
|
|
2597
|
+
if (vertexIndex >= 0 && vertexIndex < colors.length) {
|
|
2598
|
+
colors[vertexIndex] = targetColor[0];
|
|
2599
|
+
colors[vertexIndex + 1] = targetColor[1];
|
|
2600
|
+
colors[vertexIndex + 2] = targetColor[2];
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
const bounds = Array.from(lineGroups.values()).map((lineGlyphs) => this.calculateGlyphBounds(lineGlyphs));
|
|
2607
|
+
coloredRanges.push({
|
|
2608
|
+
start: match.start,
|
|
2609
|
+
end: match.end,
|
|
2610
|
+
originalText: match.pattern,
|
|
2611
|
+
color: targetColor,
|
|
2612
|
+
bounds,
|
|
2613
|
+
glyphs: matchGlyphs,
|
|
2614
|
+
lineIndices: Array.from(lineGroups.keys()).sort((a, b) => a - b)
|
|
2615
|
+
});
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
if (color.byCharRange && glyphsByTextIndex) {
|
|
2619
|
+
for (const range of color.byCharRange) {
|
|
2620
|
+
const rangeGlyphs = [];
|
|
2621
|
+
const lineGroups = new Map();
|
|
2622
|
+
for (let i = range.start; i < range.end; i++) {
|
|
2623
|
+
const glyphs = glyphsByTextIndex.get(i);
|
|
2624
|
+
if (glyphs) {
|
|
2625
|
+
for (const glyph of glyphs) {
|
|
2626
|
+
rangeGlyphs.push(glyph);
|
|
2627
|
+
const lineGlyphs = lineGroups.get(glyph.lineIndex);
|
|
2628
|
+
if (lineGlyphs) {
|
|
2629
|
+
lineGlyphs.push(glyph);
|
|
2630
|
+
}
|
|
2631
|
+
else {
|
|
2632
|
+
lineGroups.set(glyph.lineIndex, [glyph]);
|
|
2633
|
+
}
|
|
2634
|
+
for (let v = 0; v < glyph.vertexCount; v++) {
|
|
2635
|
+
const vertexIndex = (glyph.vertexStart + v) * 3;
|
|
2636
|
+
if (vertexIndex >= 0 && vertexIndex < colors.length) {
|
|
2637
|
+
colors[vertexIndex] = range.color[0];
|
|
2638
|
+
colors[vertexIndex + 1] = range.color[1];
|
|
2639
|
+
colors[vertexIndex + 2] = range.color[2];
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
const bounds = Array.from(lineGroups.values()).map((lineGlyphs) => this.calculateGlyphBounds(lineGlyphs));
|
|
2646
|
+
coloredRanges.push({
|
|
2647
|
+
start: range.start,
|
|
2648
|
+
end: range.end,
|
|
2649
|
+
originalText: originalText.slice(range.start, range.end),
|
|
2650
|
+
color: range.color,
|
|
2651
|
+
bounds,
|
|
2652
|
+
glyphs: rangeGlyphs,
|
|
2653
|
+
lineIndices: Array.from(lineGroups.keys()).sort((a, b) => a - b)
|
|
2654
|
+
});
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
return { colors, coloredRanges };
|
|
2659
|
+
}
|
|
2660
|
+
calculateGlyphBounds(glyphs) {
|
|
2661
|
+
if (glyphs.length === 0) {
|
|
2662
|
+
return {
|
|
2663
|
+
min: { x: 0, y: 0, z: 0 },
|
|
2664
|
+
max: { x: 0, y: 0, z: 0 }
|
|
2665
|
+
};
|
|
2666
|
+
}
|
|
2667
|
+
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
|
2668
|
+
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
|
2669
|
+
for (const glyph of glyphs) {
|
|
2670
|
+
if (glyph.bounds.min.x < minX)
|
|
2671
|
+
minX = glyph.bounds.min.x;
|
|
2672
|
+
if (glyph.bounds.min.y < minY)
|
|
2673
|
+
minY = glyph.bounds.min.y;
|
|
2674
|
+
if (glyph.bounds.min.z < minZ)
|
|
2675
|
+
minZ = glyph.bounds.min.z;
|
|
2676
|
+
if (glyph.bounds.max.x > maxX)
|
|
2677
|
+
maxX = glyph.bounds.max.x;
|
|
2678
|
+
if (glyph.bounds.max.y > maxY)
|
|
2679
|
+
maxY = glyph.bounds.max.y;
|
|
2680
|
+
if (glyph.bounds.max.z > maxZ)
|
|
2681
|
+
maxZ = glyph.bounds.max.z;
|
|
2682
|
+
}
|
|
2683
|
+
return {
|
|
2684
|
+
min: { x: minX, y: minY, z: minZ },
|
|
2685
|
+
max: { x: maxX, y: maxY, z: maxZ }
|
|
2686
|
+
};
|
|
2687
|
+
}
|
|
2688
|
+
createGlyphAttributes(vertexCount, glyphs) {
|
|
2689
|
+
const glyphCenters = new Float32Array(vertexCount * 3);
|
|
2690
|
+
const glyphIndices = new Float32Array(vertexCount);
|
|
2691
|
+
const glyphLineIndices = new Float32Array(vertexCount);
|
|
2692
|
+
const glyphProgress = new Float32Array(vertexCount);
|
|
2693
|
+
const glyphBaselineY = new Float32Array(vertexCount);
|
|
2694
|
+
let minX = Infinity;
|
|
2695
|
+
let maxX = -Infinity;
|
|
2696
|
+
for (let i = 0; i < glyphs.length; i++) {
|
|
2697
|
+
const cx = (glyphs[i].bounds.min.x + glyphs[i].bounds.max.x) / 2;
|
|
2698
|
+
if (cx < minX)
|
|
2699
|
+
minX = cx;
|
|
2700
|
+
if (cx > maxX)
|
|
2701
|
+
maxX = cx;
|
|
2702
|
+
}
|
|
2703
|
+
const range = maxX - minX;
|
|
2704
|
+
for (let index = 0; index < glyphs.length; index++) {
|
|
2705
|
+
const glyph = glyphs[index];
|
|
2706
|
+
const centerX = (glyph.bounds.min.x + glyph.bounds.max.x) / 2;
|
|
2707
|
+
const centerY = (glyph.bounds.min.y + glyph.bounds.max.y) / 2;
|
|
2708
|
+
const centerZ = (glyph.bounds.min.z + glyph.bounds.max.z) / 2;
|
|
2709
|
+
const baselineY = glyph.bounds.min.y;
|
|
2710
|
+
const progress = range > 0 ? (centerX - minX) / range : 0;
|
|
2711
|
+
const start = glyph.vertexStart;
|
|
2712
|
+
const end = Math.min(start + glyph.vertexCount, vertexCount);
|
|
2713
|
+
if (end <= start)
|
|
2714
|
+
continue;
|
|
2715
|
+
glyphIndices.fill(index, start, end);
|
|
2716
|
+
glyphLineIndices.fill(glyph.lineIndex, start, end);
|
|
2717
|
+
glyphProgress.fill(progress, start, end);
|
|
2718
|
+
glyphBaselineY.fill(baselineY, start, end);
|
|
2719
|
+
for (let v = start * 3; v < end * 3; v += 3) {
|
|
2720
|
+
glyphCenters[v] = centerX;
|
|
2721
|
+
glyphCenters[v + 1] = centerY;
|
|
2722
|
+
glyphCenters[v + 2] = centerZ;
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
return {
|
|
2726
|
+
glyphCenter: glyphCenters,
|
|
2727
|
+
glyphIndex: glyphIndices,
|
|
2728
|
+
glyphLineIndex: glyphLineIndices,
|
|
2729
|
+
glyphProgress,
|
|
2730
|
+
glyphBaselineY
|
|
2731
|
+
};
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
4
2734
|
|
|
5
2735
|
// p5.js adapter
|
|
6
2736
|
function convertToP5Geometry(p5Instance, textGeometry) {
|
|
@@ -85,18 +2815,21 @@ if (typeof window !== 'undefined' && window.p5) {
|
|
|
85
2815
|
}
|
|
86
2816
|
const { font, ...coreOptions } = options;
|
|
87
2817
|
try {
|
|
88
|
-
const
|
|
2818
|
+
const fullOptions = {
|
|
89
2819
|
text,
|
|
90
2820
|
font: font.buffer,
|
|
91
2821
|
fontVariations: font.variations,
|
|
92
2822
|
...coreOptions
|
|
93
|
-
}
|
|
2823
|
+
};
|
|
2824
|
+
const layoutHandle = await Text.Text.create(fullOptions);
|
|
2825
|
+
const meshPipeline = new MeshGeometryBuilder(layoutHandle.loadedFont, layoutHandle.fontId);
|
|
2826
|
+
const meshResult = meshPipeline.build(layoutHandle, fullOptions);
|
|
94
2827
|
const p5Instance = this;
|
|
95
|
-
const geometry = convertToP5Geometry(p5Instance,
|
|
2828
|
+
const geometry = convertToP5Geometry(p5Instance, meshResult);
|
|
96
2829
|
return {
|
|
97
2830
|
geometry,
|
|
98
|
-
planeBounds:
|
|
99
|
-
glyphs:
|
|
2831
|
+
planeBounds: meshResult.planeBounds,
|
|
2832
|
+
glyphs: meshResult.glyphs
|
|
100
2833
|
};
|
|
101
2834
|
}
|
|
102
2835
|
catch (err) {
|