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.
Files changed (76) hide show
  1. package/README.md +257 -38
  2. package/dist/index.cjs +3409 -3356
  3. package/dist/index.d.ts +166 -9
  4. package/dist/index.js +3405 -3357
  5. package/dist/index.min.cjs +765 -746
  6. package/dist/index.min.js +756 -737
  7. package/dist/index.umd.js +3548 -3487
  8. package/dist/index.umd.min.js +800 -776
  9. package/dist/p5/index.cjs +2738 -5
  10. package/dist/p5/index.js +2738 -5
  11. package/dist/slug/index.cjs +380 -0
  12. package/dist/slug/index.d.ts +62 -0
  13. package/dist/slug/index.js +374 -0
  14. package/dist/three/index.cjs +50 -35
  15. package/dist/three/index.js +50 -35
  16. package/dist/three/react.cjs +5 -2
  17. package/dist/three/react.d.ts +69 -120
  18. package/dist/three/react.js +6 -3
  19. package/dist/types/core/Text.d.ts +3 -10
  20. package/dist/types/core/cache/sharedCaches.d.ts +2 -1
  21. package/dist/types/core/shaping/DrawCallbacks.d.ts +11 -3
  22. package/dist/types/core/shaping/TextShaper.d.ts +1 -5
  23. package/dist/types/core/types.d.ts +87 -0
  24. package/dist/types/index.d.ts +7 -3
  25. package/dist/types/{core/cache → mesh}/GlyphContourCollector.d.ts +4 -4
  26. package/dist/types/{core/cache → mesh}/GlyphGeometryBuilder.d.ts +5 -5
  27. package/dist/types/mesh/MeshGeometryBuilder.d.ts +18 -0
  28. package/dist/types/{core → mesh}/geometry/BoundaryClusterer.d.ts +1 -1
  29. package/dist/types/{core → mesh}/geometry/Extruder.d.ts +1 -1
  30. package/dist/types/{core → mesh}/geometry/PathOptimizer.d.ts +1 -3
  31. package/dist/types/{core → mesh}/geometry/Polygonizer.d.ts +11 -6
  32. package/dist/types/{core → mesh}/geometry/Tessellator.d.ts +1 -1
  33. package/dist/types/react/utils.d.ts +2 -0
  34. package/dist/types/vector/GlyphOutlineCollector.d.ts +25 -0
  35. package/dist/types/vector/GlyphVectorGeometryBuilder.d.ts +26 -0
  36. package/dist/types/vector/LoopBlinnGeometry.d.ts +68 -0
  37. package/dist/types/vector/index.d.ts +29 -0
  38. package/dist/types/vector/loopBlinnTSL.d.ts +11 -0
  39. package/dist/types/vector/react.d.ts +24 -0
  40. package/dist/types/vector/webgl/index.d.ts +7 -0
  41. package/dist/types/vector/webgpu/index.d.ts +11 -0
  42. package/dist/vector/index.cjs +1458 -0
  43. package/dist/vector/index.d.ts +122 -0
  44. package/dist/vector/index.js +1434 -0
  45. package/dist/vector/react.cjs +153 -0
  46. package/dist/vector/react.d.ts +317 -0
  47. package/dist/vector/react.js +132 -0
  48. package/dist/vector/types/slug-lib/src/SlugPacker.d.ts +17 -0
  49. package/dist/vector/types/slug-lib/src/WebGL2Renderer.d.ts +21 -0
  50. package/dist/vector/types/slug-lib/src/WebGPURenderer.d.ts +16 -0
  51. package/dist/vector/types/slug-lib/src/index.d.ts +15 -0
  52. package/dist/vector/types/slug-lib/src/shaderStrings.d.ts +9 -0
  53. package/dist/vector/types/slug-lib/src/types.d.ts +34 -0
  54. package/dist/vector/types/src/core/types.d.ts +381 -0
  55. package/dist/vector/types/src/hyphenation/HyphenationPatternLoader.d.ts +2 -0
  56. package/dist/vector/types/src/hyphenation/index.d.ts +7 -0
  57. package/dist/vector/types/src/hyphenation/types.d.ts +6 -0
  58. package/dist/vector/types/src/utils/Cache.d.ts +14 -0
  59. package/dist/vector/types/src/utils/vectors.d.ts +75 -0
  60. package/dist/vector/types/src/vector/VectorDataBuilder.d.ts +30 -0
  61. package/dist/vector/types/src/vector/VectorThreeAdapter.d.ts +27 -0
  62. package/dist/vector/types/src/vector/index.d.ts +15 -0
  63. package/dist/vector/webgl/index.cjs +229 -0
  64. package/dist/vector/webgl/index.d.ts +53 -0
  65. package/dist/vector/webgl/index.js +227 -0
  66. package/dist/vector/webgpu/index.cjs +321 -0
  67. package/dist/vector/webgpu/index.d.ts +57 -0
  68. package/dist/vector/webgpu/index.js +319 -0
  69. package/dist/webgl-vector/index.cjs +243 -0
  70. package/dist/webgl-vector/index.d.ts +34 -0
  71. package/dist/webgl-vector/index.js +241 -0
  72. package/dist/webgpu-vector/index.cjs +336 -0
  73. package/dist/webgpu-vector/index.d.ts +38 -0
  74. package/dist/webgpu-vector/index.js +334 -0
  75. package/package.json +48 -3
  76. 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&&lt.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 result = await Text.Text.create({
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, result);
2828
+ const geometry = convertToP5Geometry(p5Instance, meshResult);
96
2829
  return {
97
2830
  geometry,
98
- planeBounds: result.planeBounds,
99
- glyphs: result.glyphs
2831
+ planeBounds: meshResult.planeBounds,
2832
+ glyphs: meshResult.glyphs
100
2833
  };
101
2834
  }
102
2835
  catch (err) {