three-text 0.4.11 → 0.5.0

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