clipper2-ts 2.0.1-13 → 2.0.1-15

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.
@@ -0,0 +1,1220 @@
1
+ // Sweep-line CDT — Domiter & Žalik 2008 (IJGIS 22:4, 449-462)
2
+ // DT engine from Žalik 2005 (Computer-Aided Design 37, 1027-1038).
3
+ // Edge insertion per Anglada 1997 (Computers & Graphics 21:2, 215-223).
4
+ //
5
+ // Triangle-based adjacency (3 vertices + 3 neighbor tris per triangle).
6
+ // Advancing front: doubly-linked list + hash table (Žalik §3.4, k=100).
7
+ // Zero allocations in the sweep loop.
8
+
9
+ import { orient2dSign, incircleSign } from './predicates.js';
10
+
11
+ export interface CDTResult {
12
+ triangles: Uint32Array;
13
+ triCount: number;
14
+ coords: Float64Array;
15
+ }
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Triangle pool — SoA for cache-friendliness
19
+ // ---------------------------------------------------------------------------
20
+ // tv[t*3+j] = vertex index at local position j (CCW winding)
21
+ // ta[t*3+j] = neighbor triangle opposite vertex j (NONE if boundary)
22
+ // tf[t] bits = fixed-edge flags, bit j ⇔ edge opposite vertex j is constrained
23
+ // td[t] = 1 if triangle is dead (removed during edge event)
24
+
25
+ const NONE = -1;
26
+ let tv: Int32Array;
27
+ let ta: Int32Array;
28
+ let tf: Uint8Array;
29
+ let td: Uint8Array;
30
+ let triCap = 0;
31
+ let triCount = 0;
32
+
33
+ function initTris(cap: number): void {
34
+ triCap = cap; triCount = 0;
35
+ tv = new Int32Array(cap * 3);
36
+ ta = new Int32Array(cap * 3).fill(NONE);
37
+ tf = new Uint8Array(cap);
38
+ td = new Uint8Array(cap);
39
+ }
40
+
41
+ function growTris(): void {
42
+ const c = triCap << 1;
43
+ const v2 = new Int32Array(c * 3); v2.set(tv); tv = v2;
44
+ const a2 = new Int32Array(c * 3).fill(NONE); a2.set(ta); ta = a2;
45
+ const f2 = new Uint8Array(c); f2.set(tf); tf = f2;
46
+ const d2 = new Uint8Array(c); d2.set(td); td = d2;
47
+ triCap = c;
48
+ }
49
+
50
+ function addTri(a: number, b: number, c: number): number {
51
+ if (triCount === triCap) growTris();
52
+ const t = triCount++, t3 = t * 3;
53
+ tv[t3] = a; tv[t3 + 1] = b; tv[t3 + 2] = c;
54
+ ta[t3] = ta[t3 + 1] = ta[t3 + 2] = NONE;
55
+ tf[t] = 0; td[t] = 0;
56
+ return t;
57
+ }
58
+
59
+ function link(t1: number, j1: number, t2: number, j2: number): void {
60
+ ta[t1 * 3 + j1] = t2;
61
+ ta[t2 * 3 + j2] = t1;
62
+ }
63
+
64
+ function localIdx(t: number, v: number): number {
65
+ const t3 = t * 3;
66
+ if (tv[t3] === v) return 0;
67
+ if (tv[t3 + 1] === v) return 1;
68
+ if (tv[t3 + 2] === v) return 2;
69
+ return -1;
70
+ }
71
+
72
+ function adjIdx(t: number, n: number): number {
73
+ const t3 = t * 3;
74
+ if (ta[t3] === n) return 0;
75
+ if (ta[t3 + 1] === n) return 1;
76
+ if (ta[t3 + 2] === n) return 2;
77
+ return -1;
78
+ }
79
+
80
+ function isFixed(t: number, j: number): boolean {
81
+ return (tf[t] & (1 << j)) !== 0;
82
+ }
83
+
84
+ function markFixed(t: number, j: number): void {
85
+ tf[t] |= (1 << j);
86
+ const n = ta[t * 3 + j];
87
+ if (n !== NONE) {
88
+ const nj = adjIdx(n, t);
89
+ if (nj >= 0) tf[n] |= (1 << nj);
90
+ }
91
+ }
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // Coordinates
95
+ // ---------------------------------------------------------------------------
96
+ let px: Float64Array;
97
+ let py: Float64Array;
98
+
99
+ function orient(a: number, b: number, c: number): number {
100
+ return orient2dSign(px[a], py[a], px[b], py[b], px[c], py[c]);
101
+ }
102
+ function inCircle(a: number, b: number, c: number, d: number): number {
103
+ return incircleSign(px[a], py[a], px[b], py[b], px[c], py[c], px[d], py[d]);
104
+ }
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // Advancing front — doubly-linked list + hash table (Žalik §3.4)
108
+ // ---------------------------------------------------------------------------
109
+ // Each AF node stores: vertex, triangle backing the AF edge to the right,
110
+ // prev/next pointers, hash-bucket chain pointer.
111
+ // Per Žalik Fig.14: key = x coordinate, vertex index v_i, triangle index t_i.
112
+
113
+ let afV: Int32Array; // vertex index
114
+ let afT: Int32Array; // triangle below AF edge (this node → next node)
115
+ let afP: Int32Array; // prev pointer
116
+ let afN: Int32Array; // next pointer
117
+ let afBk: Int32Array; // hash-bucket chain
118
+ let afCap = 0;
119
+ let afLen = 0;
120
+
121
+ let afHash: Int32Array;
122
+ let afHSize = 0;
123
+ let afXMin = 0;
124
+ let afXRng = 1;
125
+
126
+ function initAF(n: number, xmin: number, xmax: number): void {
127
+ afCap = Math.max(n + 4, 64); afLen = 0;
128
+ afV = new Int32Array(afCap);
129
+ afT = new Int32Array(afCap);
130
+ afP = new Int32Array(afCap);
131
+ afN = new Int32Array(afCap);
132
+ afBk = new Int32Array(afCap).fill(NONE);
133
+ afHSize = 1 + ((n / 100) | 0); // Žalik eq.(1), k=100
134
+ afHash = new Int32Array(afHSize).fill(NONE);
135
+ afXMin = xmin; afXRng = xmax - xmin || 1;
136
+ }
137
+
138
+ function afBucket(x: number): number {
139
+ let b = ((x - afXMin) / afXRng * (afHSize - 1)) | 0;
140
+ if (b < 0) b = 0; else if (b >= afHSize) b = afHSize - 1;
141
+ return b;
142
+ }
143
+
144
+ function growAF(): void {
145
+ const c = afCap << 1;
146
+ const g = (a: Int32Array) => { const b = new Int32Array(c); b.set(a); return b; };
147
+ afV = g(afV); afT = g(afT); afP = g(afP); afN = g(afN);
148
+ const bk = new Int32Array(c).fill(NONE); bk.set(afBk); afBk = bk;
149
+ afCap = c;
150
+ }
151
+
152
+ function afNew(v: number, tri: number): number {
153
+ if (afLen === afCap) growAF();
154
+ const id = afLen++;
155
+ afV[id] = v; afT[id] = tri;
156
+ afP[id] = afN[id] = NONE;
157
+ const b = afBucket(px[v]);
158
+ afBk[id] = afHash[b]; afHash[b] = id;
159
+ return id;
160
+ }
161
+
162
+ function afDel(id: number): void {
163
+ const p = afP[id], n = afN[id];
164
+ if (p !== NONE) afN[p] = n;
165
+ if (n !== NONE) afP[n] = p;
166
+ const b = afBucket(px[afV[id]]);
167
+ if (afHash[b] === id) { afHash[b] = afBk[id]; }
168
+ else {
169
+ let c = afHash[b];
170
+ while (c !== NONE && afBk[c] !== id) c = afBk[c];
171
+ if (c !== NONE) afBk[c] = afBk[id];
172
+ }
173
+ afBk[id] = NONE;
174
+ }
175
+
176
+ function afIns(v: number, tri: number, left: number, right: number): number {
177
+ const id = afNew(v, tri);
178
+ afP[id] = left; afN[id] = right;
179
+ if (left !== NONE) afN[left] = id;
180
+ if (right !== NONE) afP[right] = id;
181
+ return id;
182
+ }
183
+
184
+ // Locate AF node whose segment [node, node.next] contains vertical
185
+ // projection of x. Returns the LEFT node. (Žalik §3.4)
186
+ function afLocate(x: number): number {
187
+ const b = afBucket(x);
188
+ let best = NONE, bestX = -Infinity;
189
+
190
+ for (let c = afHash[b]; c !== NONE; c = afBk[c]) {
191
+ const nx = px[afV[c]];
192
+ if (nx <= x && nx > bestX) { bestX = nx; best = c; }
193
+ }
194
+ if (best === NONE) {
195
+ for (let d = 1; d < afHSize; d++) {
196
+ for (const bb of [b - d, b + d]) {
197
+ if (bb < 0 || bb >= afHSize) continue;
198
+ for (let c = afHash[bb]; c !== NONE; c = afBk[c]) {
199
+ const nx = px[afV[c]];
200
+ if (nx <= x && nx > bestX) { bestX = nx; best = c; }
201
+ }
202
+ }
203
+ if (best !== NONE) break;
204
+ }
205
+ }
206
+ if (best !== NONE) {
207
+ while (afN[best] !== NONE && px[afV[afN[best]]] <= x) best = afN[best];
208
+ }
209
+ return best;
210
+ }
211
+
212
+ // ---------------------------------------------------------------------------
213
+ // Lawson legalization — Domiter §3.1 / Žalik §2.1
214
+ // ---------------------------------------------------------------------------
215
+ function legalize(t: number, pi: number): void {
216
+ const j = localIdx(t, pi);
217
+ if (j < 0) return;
218
+ const n = ta[t * 3 + j];
219
+ if (n === NONE) return;
220
+ if (isFixed(t, j)) return;
221
+
222
+ const t3 = t * 3;
223
+ const a = tv[t3 + (j + 1) % 3];
224
+ const b = tv[t3 + (j + 2) % 3];
225
+
226
+ const nj = adjIdx(n, t);
227
+ if (nj < 0) return;
228
+ const pk = tv[n * 3 + nj];
229
+
230
+ if (inCircle(a, b, pi, pk) <= 0) return;
231
+
232
+ // Flip edge a-b → pi-pk
233
+ const j1 = (j + 1) % 3, j2 = (j + 2) % 3;
234
+ const nj1 = (nj + 1) % 3, nj2 = (nj + 2) % 3;
235
+ const n3 = n * 3;
236
+
237
+ const tN_oppA = ta[t3 + j1];
238
+ const tN_oppB = ta[t3 + j2];
239
+
240
+ const vnj1 = tv[n3 + nj1], vnj2 = tv[n3 + nj2];
241
+ const nN1 = ta[n3 + nj1];
242
+ const nN2 = ta[n3 + nj2];
243
+
244
+ let nN_bside: number, nN_aside: number;
245
+ if (vnj1 === b) {
246
+ nN_bside = nN2; nN_aside = nN1;
247
+ } else {
248
+ nN_bside = nN1; nN_aside = nN2;
249
+ }
250
+
251
+ // t = (pi, pk, b)
252
+ tv[t3] = pi; tv[t3 + 1] = pk; tv[t3 + 2] = b;
253
+ ta[t3] = nN_bside;
254
+ ta[t3 + 1] = tN_oppA;
255
+ ta[t3 + 2] = n;
256
+ tf[t] = 0;
257
+
258
+ // n = (pi, a, pk)
259
+ tv[n3] = pi; tv[n3 + 1] = a; tv[n3 + 2] = pk;
260
+ ta[n3] = nN_aside;
261
+ ta[n3 + 1] = t;
262
+ ta[n3 + 2] = tN_oppB;
263
+ tf[n] = 0;
264
+
265
+ if (nN_bside !== NONE) { const k = adjIdx(nN_bside, n); if (k >= 0) ta[nN_bside * 3 + k] = t; }
266
+ if (tN_oppB !== NONE) { const k = adjIdx(tN_oppB, t); if (k >= 0) ta[tN_oppB * 3 + k] = n; }
267
+
268
+ legalize(t, pi);
269
+ legalize(n, pi);
270
+ }
271
+
272
+ // ---------------------------------------------------------------------------
273
+ // Helper: link new triangle to old triangle sharing edge (vA, vB)
274
+ // ---------------------------------------------------------------------------
275
+ function linkToOldTri(newTri: number, newOppJ: number, oldTri: number, vA: number, vB: number): void {
276
+ if (oldTri === NONE) return;
277
+ const ai = localIdx(oldTri, vA);
278
+ const bi = localIdx(oldTri, vB);
279
+ if (ai < 0 || bi < 0) return;
280
+ link(newTri, newOppJ, oldTri, 3 - ai - bi);
281
+ }
282
+
283
+ // ---------------------------------------------------------------------------
284
+ // Point event — Domiter §3.4.1
285
+ // ---------------------------------------------------------------------------
286
+ let vertAF: Int32Array; // vertex → AF node id
287
+
288
+ function pointEvent(pi: number): void {
289
+ const x = px[pi];
290
+ const L = afLocate(x);
291
+ if (L === NONE) return;
292
+ const R = afN[L];
293
+ if (R === NONE) return;
294
+
295
+ const vL = afV[L], vR = afV[R];
296
+
297
+ // Duplicate check (Žalik §3.2.4)
298
+ if (px[pi] === px[vL] && py[pi] === py[vL]) return;
299
+ if (px[pi] === px[vR] && py[pi] === py[vR]) return;
300
+
301
+ // Check for left case: projection coincides with P_L (Domiter Fig.8)
302
+ // This happens when px[pi] === px[vL]
303
+ if (px[pi] === px[vL]) {
304
+ // Left case: create two triangles (Domiter Fig.8)
305
+ // Δ(vLL, vL, pi) and Δ(vL, vR, pi)
306
+ const LL = afP[L];
307
+ if (LL === NONE) return;
308
+ const vLL = afV[LL];
309
+
310
+ // Triangle 1: (vL, vR, pi) — same as middle case
311
+ const t1 = addTri(vL, vR, pi);
312
+ linkToOldTri(t1, 2, afT[L], vL, vR);
313
+ legalize(t1, pi);
314
+
315
+ // Triangle 2: (vLL, vL, pi)
316
+ const t2 = addTri(vLL, vL, pi);
317
+ linkToOldTri(t2, 2, afT[LL], vLL, vL);
318
+ // Link t2's edge vL-pi (opposite vLL, local 0) with t1's edge vL-pi (opposite vR, local 1... no)
319
+ // t1 = (vL, vR, pi): edge opposite vR at local 1 = vL-pi? No: edge opposite local 1 = (local2, local0) = (pi, vL)
320
+ // t2 = (vLL, vL, pi): edge opposite vLL at local 0 = (local1, local2) = (vL, pi)
321
+ // So t1 edge (pi, vL) at local 1 ↔ t2 edge (vL, pi) at local 0
322
+ link(t1, 1, t2, 0);
323
+ legalize(t2, pi);
324
+
325
+ // Update AF: remove L, insert pi between LL and R
326
+ afDel(L);
327
+ const piNode = afIns(pi, t1, LL, R);
328
+ vertAF[pi] = piNode;
329
+ afT[LL] = t2;
330
+ afT[piNode] = t1;
331
+
332
+ fanRight(pi, piNode);
333
+ fanLeft(pi, piNode);
334
+ basinDetect(pi, piNode);
335
+ return;
336
+ }
337
+
338
+ // -------------------------------------------------------
339
+ // Middle case — Domiter Fig.7
340
+ // -------------------------------------------------------
341
+ // CCW triangle: (vL, vR, pi) — pi to left of vL→vR
342
+ const t = addTri(vL, vR, pi);
343
+ linkToOldTri(t, 2, afT[L], vL, vR);
344
+ legalize(t, pi);
345
+
346
+ const piNode = afIns(pi, t, L, R);
347
+ vertAF[pi] = piNode;
348
+ afT[L] = t;
349
+ afT[piNode] = t;
350
+
351
+ fanRight(pi, piNode);
352
+ fanLeft(pi, piNode);
353
+ basinDetect(pi, piNode);
354
+ }
355
+
356
+ // ---------------------------------------------------------------------------
357
+ // Fan right / fan left — Žalik §3.2.1, Domiter Fig.9
358
+ // ---------------------------------------------------------------------------
359
+ function fanRight(pi: number, piNode: number): void {
360
+ let cur = piNode;
361
+ while (true) {
362
+ const R = afN[cur];
363
+ if (R === NONE) break;
364
+ const RR = afN[R];
365
+ if (RR === NONE) break;
366
+
367
+ const vR = afV[R], vRR = afV[RR];
368
+
369
+ // Angle at pi between rays pi→vR and pi→vRR: if cos > 0 then < π/2
370
+ const dx1 = px[vR] - px[pi], dy1 = py[vR] - py[pi];
371
+ const dx2 = px[vRR] - px[pi], dy2 = py[vRR] - py[pi];
372
+ if (dx1 * dx2 + dy1 * dy2 <= 0) break;
373
+
374
+ const t = addTri(vR, vRR, pi);
375
+ linkToOldTri(t, 2, afT[R], vR, vRR);
376
+ linkToOldTri(t, 1, afT[cur], vR, pi);
377
+ legalize(t, pi);
378
+
379
+ afDel(R);
380
+ afN[cur] = RR; afP[RR] = cur;
381
+ afT[cur] = t;
382
+ }
383
+ }
384
+
385
+ function fanLeft(pi: number, piNode: number): void {
386
+ let cur = piNode;
387
+ while (true) {
388
+ const L = afP[cur];
389
+ if (L === NONE) break;
390
+ const LL = afP[L];
391
+ if (LL === NONE) break;
392
+
393
+ const vL = afV[L], vLL = afV[LL];
394
+
395
+ const dx1 = px[vL] - px[pi], dy1 = py[vL] - py[pi];
396
+ const dx2 = px[vLL] - px[pi], dy2 = py[vLL] - py[pi];
397
+ if (dx1 * dx2 + dy1 * dy2 <= 0) break;
398
+
399
+ const t = addTri(vLL, vL, pi);
400
+ linkToOldTri(t, 0, afT[L], vL, pi);
401
+ linkToOldTri(t, 2, afT[LL], vLL, vL);
402
+ legalize(t, pi);
403
+
404
+ afDel(L);
405
+ afP[cur] = LL; afN[LL] = cur;
406
+ afT[LL] = t;
407
+ }
408
+ }
409
+
410
+ // ---------------------------------------------------------------------------
411
+ // Basin detection — Žalik Fig.10, Domiter Fig.10
412
+ // ---------------------------------------------------------------------------
413
+ // After inserting pi: check if the angle at vR between rays vR→pi and
414
+ // vR→vR+ is > 3π/4. If so, fill the basin.
415
+ function basinDetect(pi: number, piNode: number): void {
416
+ // Right side basin
417
+ const R = afN[piNode];
418
+ if (R !== NONE) {
419
+ const RR = afN[R];
420
+ if (RR !== NONE) {
421
+ const vR = afV[R], vRR = afV[RR];
422
+ // Slope of line pi→vR+: if angle > 3π/4, cos < -√2/2 ≈ -0.707
423
+ // Actually Žalik says: "slope of line connecting v_i and v_{R+} is
424
+ // determined as to whether it is smaller than 3π/4"
425
+ // This means the angle at vR between AF segments is checked.
426
+ // Basin condition: angle at vR (between vR→pi and vR→vRR) > 3π/4
427
+ const dx1 = px[pi] - px[vR], dy1 = py[pi] - py[vR];
428
+ const dx2 = px[vRR] - px[vR], dy2 = py[vRR] - py[vR];
429
+ const dot = dx1 * dx2 + dy1 * dy2;
430
+ const cross = dx1 * dy2 - dy1 * dx2;
431
+ // angle > 3π/4 iff cos < cos(3π/4) = -√2/2 and the angle is reflex
432
+ // cos(θ) = dot / (|a||b|); if dot < 0 and cross < 0 (reflex), it's a basin
433
+ if (dot < 0 && cross < 0) {
434
+ fillBasinRight(pi, piNode);
435
+ }
436
+ }
437
+ }
438
+
439
+ // Left side basin
440
+ const L = afP[piNode];
441
+ if (L !== NONE) {
442
+ const LL = afP[L];
443
+ if (LL !== NONE) {
444
+ const vL = afV[L], vLL = afV[LL];
445
+ const dx1 = px[pi] - px[vL], dy1 = py[pi] - py[vL];
446
+ const dx2 = px[vLL] - px[vL], dy2 = py[vLL] - py[vL];
447
+ const dot = dx1 * dx2 + dy1 * dy2;
448
+ const cross = dx1 * dy2 - dy1 * dx2;
449
+ if (dot < 0 && cross > 0) {
450
+ fillBasinLeft(pi, piNode);
451
+ }
452
+ }
453
+ }
454
+ }
455
+
456
+ function fillBasinRight(pi: number, piNode: number): void {
457
+ // Find basin bottom: walk right from piNode until y starts increasing
458
+ let R = afN[piNode];
459
+ if (R === NONE) return;
460
+ let RR = afN[R];
461
+ while (RR !== NONE) {
462
+ const vR = afV[R], vRR = afV[RR];
463
+ if (py[vRR] > py[vR]) break; // bottom found at R
464
+ R = RR;
465
+ RR = afN[RR];
466
+ }
467
+ // Now fill: fan from pi to everything between piNode and the basin right border
468
+ // Use the same fan-right logic but without the π/2 angle limit
469
+ let cur = piNode;
470
+ while (true) {
471
+ const rn = afN[cur];
472
+ if (rn === NONE) break;
473
+ const rnn = afN[rn];
474
+ if (rnn === NONE) break;
475
+
476
+ const vR = afV[rn], vRR = afV[rnn];
477
+
478
+ // Only fill while the triple (pi, vR, vRR) is CCW
479
+ if (orient(pi, vR, vRR) <= 0) break;
480
+
481
+ const t = addTri(vR, vRR, pi);
482
+ linkToOldTri(t, 2, afT[rn], vR, vRR);
483
+ linkToOldTri(t, 1, afT[cur], vR, pi);
484
+ legalize(t, pi);
485
+
486
+ afDel(rn);
487
+ afN[cur] = rnn; afP[rnn] = cur;
488
+ afT[cur] = t;
489
+
490
+ // Stop once we've passed the bottom
491
+ if (rn === R) break;
492
+ }
493
+ }
494
+
495
+ function fillBasinLeft(pi: number, piNode: number): void {
496
+ let L = afP[piNode];
497
+ if (L === NONE) return;
498
+ let LL = afP[L];
499
+ while (LL !== NONE) {
500
+ const vL = afV[L], vLL = afV[LL];
501
+ if (py[vLL] > py[vL]) break;
502
+ L = LL;
503
+ LL = afP[LL];
504
+ }
505
+ let cur = piNode;
506
+ while (true) {
507
+ const ln = afP[cur];
508
+ if (ln === NONE) break;
509
+ const lnn = afP[ln];
510
+ if (lnn === NONE) break;
511
+
512
+ const vL = afV[ln], vLL = afV[lnn];
513
+
514
+ if (orient(pi, vLL, vL) <= 0) break;
515
+
516
+ const t = addTri(vLL, vL, pi);
517
+ linkToOldTri(t, 0, afT[ln], vL, pi);
518
+ linkToOldTri(t, 2, afT[lnn], vLL, vL);
519
+ legalize(t, pi);
520
+
521
+ afDel(ln);
522
+ afP[cur] = lnn; afN[lnn] = cur;
523
+ afT[lnn] = t;
524
+
525
+ if (ln === L) break;
526
+ }
527
+ }
528
+
529
+ // ---------------------------------------------------------------------------
530
+ // Edge event — Domiter §3.4.2, Anglada §4
531
+ // ---------------------------------------------------------------------------
532
+ // Pre-allocated scratch arrays for pseudo-polygon vertices.
533
+ // Max size bounded by number of vertices.
534
+ let scrU: Int32Array; // upper pseudo-polygon
535
+ let scrL: Int32Array; // lower pseudo-polygon
536
+ let scrBndT: Int32Array; // boundary tri for each removed tri
537
+ let scrBndJ: Int32Array; // boundary local idx for each removed tri
538
+ let scrRm: Int32Array; // removed triangle indices
539
+
540
+ function edgeEvent(a: number, b: number): void {
541
+ // Check if edge already exists in the triangulation
542
+ if (markExisting(a, b)) return;
543
+
544
+ // Domiter §3.4.2: three cases based on position of edge vs AF.
545
+ //
546
+ // Locate first crossed triangle (Domiter Fig.11): walk triangles around
547
+ // vertex a using cross-product to find which triangle's opposite edge
548
+ // is pierced by ray a→b.
549
+
550
+ // First, find any triangle around vertex a.
551
+ const startT = findTriAround(a);
552
+ if (startT === NONE) return;
553
+
554
+ // Check if edge is entirely along the AF (Domiter Fig.13: no triangles crossed).
555
+ // If a and b are both on the AF and the AF path from a to b doesn't dip below
556
+ // the edge, we only need AF traversal.
557
+ const aNode = vertAF[a], bNode = vertAF[b];
558
+
559
+ // Try to find the first crossed triangle by walking the one-ring around a.
560
+ const first = findFirstCrossed(a, b, startT);
561
+
562
+ if (first === NONE) {
563
+ // No triangle is crossed — Domiter Fig.13: edge lies along/above AF.
564
+ // AF traversal only: lower pseudo-polygon from AF vertices a→b.
565
+ if (aNode !== NONE && bNode !== NONE) {
566
+ edgeEventAFOnly(a, b, aNode, bNode);
567
+ }
568
+ return;
569
+ }
570
+
571
+ // Case 1 (Domiter Fig.12): edge entirely below AF — triangle traversal.
572
+ // Case 3 (Domiter Fig.14): edge crosses AF — mixed traversal.
573
+ // We handle both with the same traversal that switches between triangle
574
+ // and AF traversal as described in Domiter §3.4.2.
575
+ edgeEventTraversal(a, b, first);
576
+ }
577
+
578
+ // Edge insertion with no triangle crossing — Domiter Fig.13.
579
+ // Only a lower pseudo-polygon exists, formed by AF vertices from a to b.
580
+ function edgeEventAFOnly(a: number, b: number, aNode: number, bNode: number): void {
581
+ // Collect AF vertices between a and b (exclusive) into lower pseudo-polygon.
582
+ let nL = 0;
583
+ let nd = afN[aNode];
584
+ while (nd !== NONE && afV[nd] !== b) {
585
+ scrL[nL++] = afV[nd];
586
+ nd = afN[nd];
587
+ }
588
+ if (nL === 0) {
589
+ // Edge a→b is already an AF edge. Just mark it.
590
+ // Find the triangle backing this AF edge and mark it fixed.
591
+ const t = afT[aNode];
592
+ if (t !== NONE) {
593
+ const ai = localIdx(t, a), bi = localIdx(t, b);
594
+ if (ai >= 0 && bi >= 0) markFixed(t, 3 - ai - bi);
595
+ }
596
+ return;
597
+ }
598
+
599
+ // Remove AF nodes between a and b, and triangulate the lower pseudo-polygon.
600
+ // The AF triangles along this stretch become the boundary.
601
+ // Collect removed AF nodes and their backing triangles.
602
+ // Also need to remove the AF nodes and relink a→b.
603
+
604
+ // Remove AF nodes between a and b
605
+ nd = afN[aNode];
606
+ while (nd !== NONE && afV[nd] !== b) {
607
+ const next = afN[nd];
608
+ afDel(nd);
609
+ nd = next;
610
+ }
611
+ // Relink: aNode→bNode
612
+ afN[aNode] = bNode; afP[bNode] = aNode;
613
+
614
+ // Triangulate lower pseudo-polygon (vertices below edge a→b)
615
+ // These are the vertices we collected in scrL[0..nL).
616
+ // Use Anglada's recursive pseudo-polygon triangulation.
617
+ const t = triPseudoPoly(a, b, scrL, 0, nL);
618
+
619
+ // Mark edge a-b as fixed on the resulting triangle
620
+ if (t !== NONE) {
621
+ const ai = localIdx(t, a), bi = localIdx(t, b);
622
+ if (ai >= 0 && bi >= 0) markFixed(t, 3 - ai - bi);
623
+ }
624
+
625
+ // Update AF triangle pointer for a
626
+ if (t !== NONE) afT[aNode] = t;
627
+ }
628
+
629
+ // Edge insertion with triangle traversal — Domiter Fig.12/14, Anglada §4.
630
+ function edgeEventTraversal(a: number, b: number, first: number): void {
631
+ // Walk through triangles pierced by edge a→b (Anglada's AddEdgeCDT).
632
+ // Collect upper and lower pseudo-polygon vertices.
633
+ let nU = 0, nL = 0, nRm = 0;
634
+
635
+ let t = first;
636
+ let v = a; // vertex we entered from
637
+
638
+ while (true) {
639
+ scrRm[nRm++] = t;
640
+ td[t] = 1; // mark dead
641
+
642
+ const t3 = t * 3;
643
+ const v0 = tv[t3], v1 = tv[t3 + 1], v2 = tv[t3 + 2];
644
+
645
+ // Check if b is a vertex of this triangle
646
+ if (v0 === b || v1 === b || v2 === b) {
647
+ // Classify remaining non-{a,b} vertices
648
+ for (let j = 0; j < 3; j++) {
649
+ const vj = tv[t3 + j];
650
+ if (vj === a || vj === b) continue;
651
+ const o = orient(a, b, vj);
652
+ if (o > 0) scrU[nU++] = vj;
653
+ else if (o < 0) scrL[nL++] = vj;
654
+ }
655
+ break;
656
+ }
657
+
658
+ // Find the opposed triangle and vertex (Anglada):
659
+ // t_seq = OpposedTriangle(t, v) — the adjacent triangle of t opposite v
660
+ // v_seq = OpposedVertex(t_seq, t) — the vertex of t_seq not in t
661
+ const vi = localIdx(t, v);
662
+ if (vi < 0) break;
663
+ const tSeq = ta[t3 + vi];
664
+ if (tSeq === NONE) break;
665
+
666
+ const vSeqJ = adjIdx(tSeq, t);
667
+ if (vSeqJ < 0) break;
668
+ const vSeq = tv[tSeq * 3 + vSeqJ];
669
+
670
+ // Classify v_seq
671
+ const oSeq = orient(a, b, vSeq);
672
+ if (oSeq > 0) {
673
+ // v_seq above edge ab
674
+ scrU[nU++] = vSeq;
675
+ // v = vertex shared by t and t_seq above ab
676
+ // The two vertices shared by t and t_seq are the edge opposite v in t
677
+ // and opposite vSeq in tSeq. These are the same two vertices.
678
+ // We need the one that's above ab.
679
+ const e1 = tv[t3 + (vi + 1) % 3], e2 = tv[t3 + (vi + 2) % 3];
680
+ const o1 = (e1 === a || e1 === b) ? 0 : orient(a, b, e1);
681
+ const o2 = (e2 === a || e2 === b) ? 0 : orient(a, b, e2);
682
+ v = (o1 >= 0 && o1 >= o2) ? e1 : e2;
683
+ } else if (oSeq < 0) {
684
+ // v_seq below edge ab
685
+ scrL[nL++] = vSeq;
686
+ const e1 = tv[t3 + (vi + 1) % 3], e2 = tv[t3 + (vi + 2) % 3];
687
+ const o1 = (e1 === a || e1 === b) ? 0 : orient(a, b, e1);
688
+ const o2 = (e2 === a || e2 === b) ? 0 : orient(a, b, e2);
689
+ v = (o1 <= 0 && o1 <= o2) ? e1 : e2;
690
+ } else {
691
+ // v_seq is ON the edge — split into two sub-edges (Domiter)
692
+ // Retriangulate what we have so far, then recurse.
693
+ retriangulateRemoved(a, b, nU, nL, nRm);
694
+ edgeEvent(a, vSeq);
695
+ edgeEvent(vSeq, b);
696
+ return;
697
+ }
698
+
699
+ t = tSeq;
700
+ }
701
+
702
+ retriangulateRemoved(a, b, nU, nL, nRm);
703
+ }
704
+
705
+ // Retriangulate the region left by removed triangles.
706
+ // Upper pseudo-polygon P_U = scrU[0..nU) with edge (a, b)
707
+ // Lower pseudo-polygon P_L = scrL[0..nL) with edge (b, a)
708
+ function retriangulateRemoved(
709
+ a: number, b: number,
710
+ nU: number, nL: number, nRm: number,
711
+ ): void {
712
+ // Save boundary adjacencies from removed triangles.
713
+ // For each removed triangle, check its 3 neighbors. If a neighbor is alive
714
+ // (not dead), record that boundary edge for later linking.
715
+ // We use a simple flat array: for each boundary edge, store [extTri, extJ, e0, e1].
716
+ let nBnd = 0;
717
+ for (let i = 0; i < nRm; i++) {
718
+ const rt = scrRm[i], rt3 = rt * 3;
719
+ for (let j = 0; j < 3; j++) {
720
+ const nb = ta[rt3 + j];
721
+ if (nb === NONE || td[nb]) continue;
722
+ // This edge is a boundary edge. Get its vertices.
723
+ const e0 = tv[rt3 + (j + 1) % 3], e1 = tv[rt3 + (j + 2) % 3];
724
+ const ej = adjIdx(nb, rt);
725
+ if (ej < 0) continue;
726
+ // Store in scratch: [extTri, extJ, e0, e1]
727
+ scrBndT[nBnd] = nb;
728
+ scrBndJ[nBnd] = ej;
729
+ // Encode edge as min/max pair for matching
730
+ // We'll store e0, e1 directly and match by checking both orientations
731
+ scrBndT[nBnd + nRm * 3] = e0; // reuse space after nRm*3
732
+ scrBndJ[nBnd + nRm * 3] = e1;
733
+ nBnd++;
734
+ // Unlink the neighbor's pointer to the dead triangle
735
+ ta[nb * 3 + ej] = NONE;
736
+ }
737
+ }
738
+
739
+ // Triangulate upper and lower pseudo-polygons
740
+ const tU = triPseudoPoly(a, b, scrU, 0, nU);
741
+ const tL = triPseudoPoly(b, a, scrL, 0, nL);
742
+
743
+ // Link tU and tL across edge a-b if both exist
744
+ if (tU !== NONE && tL !== NONE) {
745
+ const ui = localIdx(tU, a), uj = localIdx(tU, b);
746
+ const li = localIdx(tL, b), lj = localIdx(tL, a);
747
+ if (ui >= 0 && uj >= 0 && li >= 0 && lj >= 0) {
748
+ link(tU, 3 - ui - uj, tL, 3 - li - lj);
749
+ }
750
+ }
751
+
752
+ // Reconstitute boundary adjacencies (Anglada §4 last step).
753
+ // For each new triangle, check its unlinked edges against boundary edges.
754
+ // Since the number of boundary edges is small (bounded by the removed region),
755
+ // a nested scan is fast enough.
756
+ reconstituteBoundary(nBnd, nRm);
757
+
758
+ // Mark edge a-b as fixed
759
+ if (tU !== NONE) {
760
+ const ai = localIdx(tU, a), bi = localIdx(tU, b);
761
+ if (ai >= 0 && bi >= 0) markFixed(tU, 3 - ai - bi);
762
+ }
763
+
764
+ // Update AF triangle pointers that may have referenced removed triangles.
765
+ updateAFAfterRemoval(nRm);
766
+ }
767
+
768
+ function reconstituteBoundary(nBnd: number, nRm: number): void {
769
+ // Walk all triangles created since the removal started and link their
770
+ // unconnected edges to boundary neighbors.
771
+ // New triangles were appended starting from triCount - (number created).
772
+ // But we don't track that precisely. Instead, walk new tris by checking
773
+ // triangles from the end that are alive and have NONE adjacencies.
774
+ //
775
+ // Actually, simpler: iterate boundary edges and find which new triangle
776
+ // has that edge.
777
+ for (let i = 0; i < nBnd; i++) {
778
+ const extT = scrBndT[i], extJ = scrBndJ[i];
779
+ const e0 = scrBndT[i + nRm * 3], e1 = scrBndJ[i + nRm * 3];
780
+ // Find the new (alive) triangle that has edge (e0, e1)
781
+ // Walk from extT's perspective: the new triangle should be linkable.
782
+ // Since triPseudoPoly creates new triangles, search backward from triCount.
783
+ for (let t = triCount - 1; t >= 0; t--) {
784
+ if (td[t]) continue;
785
+ const i0 = localIdx(t, e0), i1 = localIdx(t, e1);
786
+ if (i0 >= 0 && i1 >= 0) {
787
+ const oj = 3 - i0 - i1;
788
+ if (ta[t * 3 + oj] === NONE) {
789
+ link(t, oj, extT, extJ);
790
+ break;
791
+ }
792
+ }
793
+ }
794
+ }
795
+ }
796
+
797
+ function updateAFAfterRemoval(nRm: number): void {
798
+ // For each removed triangle, any AF node referencing it needs updating.
799
+ // Walk AF and fix stale triangle pointers.
800
+ for (let nd = 0; nd < afLen; nd++) {
801
+ if (afP[nd] === NONE && afN[nd] === NONE && nd > 0) continue; // orphan
802
+ const t = afT[nd];
803
+ if (t === NONE || !td[t]) continue;
804
+ // This AF node's triangle was removed. Find a live replacement.
805
+ const v = afV[nd];
806
+ const nn = afN[nd];
807
+ if (nn === NONE) continue;
808
+ const vn = afV[nn];
809
+ // Find a live triangle containing both v and vn.
810
+ for (let i = triCount - 1; i >= 0; i--) {
811
+ if (td[i]) continue;
812
+ if (localIdx(i, v) >= 0 && localIdx(i, vn) >= 0) {
813
+ afT[nd] = i;
814
+ break;
815
+ }
816
+ }
817
+ }
818
+ }
819
+
820
+ // ---------------------------------------------------------------------------
821
+ // Pseudo-polygon triangulation — Anglada §4, Fig.8
822
+ // ---------------------------------------------------------------------------
823
+ // Recursive Delaunay triangulation of pseudo-polygon.
824
+ // edge (a, b), vertices P[off..off+len)
825
+ // Returns the triangle on edge a-b, or NONE if empty.
826
+ //
827
+ // The recursion returns the last triangle created (the one on edge ab),
828
+ // and we link children to parents as we go — no Map needed.
829
+
830
+ function triPseudoPoly(
831
+ a: number, b: number,
832
+ P: Int32Array, off: number, len: number,
833
+ ): number {
834
+ if (len === 0) return NONE;
835
+
836
+ // Find c = vertex in P that maximizes Delaunay criterion
837
+ let ci = 0;
838
+ for (let i = 1; i < len; i++) {
839
+ const oa = orient(a, P[off + ci], b);
840
+ if (oa > 0) {
841
+ if (inCircle(a, P[off + ci], b, P[off + i]) > 0) ci = i;
842
+ } else if (oa < 0) {
843
+ if (inCircle(a, b, P[off + ci], P[off + i]) > 0) ci = i;
844
+ } else {
845
+ ci = i;
846
+ }
847
+ }
848
+
849
+ const c = P[off + ci];
850
+
851
+ // Recurse on P_E = P[off..off+ci) with edge (a, c)
852
+ const tE = triPseudoPoly(a, c, P, off, ci);
853
+ // Recurse on P_D = P[off+ci+1..off+len) with edge (c, b)
854
+ const tD = triPseudoPoly(c, b, P, off + ci + 1, len - ci - 1);
855
+
856
+ // Create triangle (a, c, b) — ensure CCW
857
+ const o = orient(a, c, b);
858
+ if (o === 0) return NONE; // degenerate
859
+ let t: number;
860
+ if (o > 0) {
861
+ t = addTri(a, c, b);
862
+ } else {
863
+ t = addTri(a, b, c);
864
+ }
865
+
866
+ // Link to child triangles:
867
+ // tE is on edge a-c → find its local edge a-c and link
868
+ if (tE !== NONE) {
869
+ const ei0 = localIdx(tE, a), ei1 = localIdx(tE, c);
870
+ if (ei0 >= 0 && ei1 >= 0) {
871
+ const ti0 = localIdx(t, a), ti1 = localIdx(t, c);
872
+ if (ti0 >= 0 && ti1 >= 0) {
873
+ link(t, 3 - ti0 - ti1, tE, 3 - ei0 - ei1);
874
+ }
875
+ }
876
+ }
877
+ if (tD !== NONE) {
878
+ const di0 = localIdx(tD, c), di1 = localIdx(tD, b);
879
+ if (di0 >= 0 && di1 >= 0) {
880
+ const ti0 = localIdx(t, c), ti1 = localIdx(t, b);
881
+ if (ti0 >= 0 && ti1 >= 0) {
882
+ link(t, 3 - ti0 - ti1, tD, 3 - di0 - di1);
883
+ }
884
+ }
885
+ }
886
+
887
+ return t;
888
+ }
889
+
890
+ // ---------------------------------------------------------------------------
891
+ // markExisting / findTriAround / findFirstCrossed
892
+ // ---------------------------------------------------------------------------
893
+
894
+ function markExisting(a: number, b: number): boolean {
895
+ const start = findTriAround(a);
896
+ if (start === NONE) return false;
897
+
898
+ // Walk one-ring around a in both directions
899
+ let t = start;
900
+ for (let i = 0; i < triCount; i++) {
901
+ if (td[t]) break;
902
+ const ai = localIdx(t, a);
903
+ if (ai < 0) break;
904
+ const t3 = t * 3;
905
+ if (tv[t3 + (ai + 1) % 3] === b) { markFixed(t, (ai + 2) % 3); return true; }
906
+ if (tv[t3 + (ai + 2) % 3] === b) { markFixed(t, (ai + 1) % 3); return true; }
907
+ t = ta[t3 + (ai + 1) % 3]; // CW
908
+ if (t === NONE || t === start) break;
909
+ }
910
+ t = start;
911
+ for (let i = 0; i < triCount; i++) {
912
+ if (td[t]) break;
913
+ const ai = localIdx(t, a);
914
+ if (ai < 0) break;
915
+ const t3 = t * 3;
916
+ if (tv[t3 + (ai + 1) % 3] === b) { markFixed(t, (ai + 2) % 3); return true; }
917
+ if (tv[t3 + (ai + 2) % 3] === b) { markFixed(t, (ai + 1) % 3); return true; }
918
+ t = ta[t3 + (ai + 2) % 3]; // CCW
919
+ if (t === NONE || t === start) break;
920
+ }
921
+ return false;
922
+ }
923
+
924
+ // Find any live triangle containing vertex v.
925
+ // AF node for v stores the triangle backing the AF edge.
926
+ // After flips, that triangle may no longer contain v, but a neighbor will.
927
+ function findTriAround(v: number): number {
928
+ const nd = vertAF[v];
929
+ if (nd === NONE || nd >= afLen) return NONE;
930
+
931
+ // Check afT[nd] and afT[prev]
932
+ let t = afT[nd];
933
+ if (t !== NONE && !td[t] && localIdx(t, v) >= 0) return t;
934
+
935
+ const p = afP[nd];
936
+ if (p !== NONE) {
937
+ t = afT[p];
938
+ if (t !== NONE && !td[t] && localIdx(t, v) >= 0) return t;
939
+ }
940
+
941
+ // Walk neighbors of those triangles (max 2 hops covers all flip cases)
942
+ for (const seed of [afT[nd], p !== NONE ? afT[p] : NONE]) {
943
+ if (seed === NONE || seed >= triCount) continue;
944
+ if (!td[seed]) {
945
+ for (let j = 0; j < 3; j++) {
946
+ const nb = ta[seed * 3 + j];
947
+ if (nb !== NONE && !td[nb] && localIdx(nb, v) >= 0) return nb;
948
+ }
949
+ }
950
+ }
951
+ return NONE;
952
+ }
953
+
954
+ // Find first triangle around vertex a whose opposite edge is crossed by a→b.
955
+ // Domiter Fig.11: walk triangles around a using cross-product.
956
+ function findFirstCrossed(a: number, b: number, start: number): number {
957
+ // Walk one-ring around a in CW direction
958
+ let t = start;
959
+ for (let i = 0; i < triCount; i++) {
960
+ if (td[t]) { t = NONE; break; }
961
+ const ai = localIdx(t, a);
962
+ if (ai < 0) break;
963
+ const t3 = t * 3;
964
+ const v1 = tv[t3 + (ai + 1) % 3], v2 = tv[t3 + (ai + 2) % 3];
965
+ if (v1 === b || v2 === b) return NONE; // edge already exists
966
+ const o1 = orient(a, b, v1), o2 = orient(a, b, v2);
967
+ if ((o1 > 0 && o2 < 0) || (o1 < 0 && o2 > 0)) return t;
968
+ t = ta[t3 + (ai + 1) % 3]; // CW step
969
+ if (t === NONE || t === start) break;
970
+ }
971
+
972
+ // Try CCW direction
973
+ t = start;
974
+ for (let i = 0; i < triCount; i++) {
975
+ if (td[t]) { t = NONE; break; }
976
+ const ai = localIdx(t, a);
977
+ if (ai < 0) break;
978
+ const t3 = t * 3;
979
+ const v1 = tv[t3 + (ai + 1) % 3], v2 = tv[t3 + (ai + 2) % 3];
980
+ if (v1 === b || v2 === b) return NONE;
981
+ const o1 = orient(a, b, v1), o2 = orient(a, b, v2);
982
+ if ((o1 > 0 && o2 < 0) || (o1 < 0 && o2 > 0)) return t;
983
+ t = ta[t3 + (ai + 2) % 3]; // CCW step
984
+ if (t === NONE || t === start) break;
985
+ }
986
+ return NONE;
987
+ }
988
+
989
+ // ---------------------------------------------------------------------------
990
+ // Finalization — Domiter §3.5
991
+ // ---------------------------------------------------------------------------
992
+ // (i) Remove triangles with artificial points
993
+ // (ii) Add bordering triangles to complete the convex hull
994
+ //
995
+ // Walk the AF from right of P_{-1} to P_{-2}, taking triples of consecutive
996
+ // AF points. If signed area > 0, add triangle and legalize. (Domiter Fig.15a)
997
+ // Then follow artificial triangles from P_{-2} to P_{-1}, deleting them and
998
+ // adding triangles where signed area < 0. (Domiter Fig.15b/c)
999
+
1000
+ function finalize(A1: number, A2: number): void {
1001
+ // Upper convex hull: walk AF from first real point to A2
1002
+ let nd = afN[vertAF[A1]]; // first real point (right of A1)
1003
+ if (nd === NONE) return;
1004
+
1005
+ while (true) {
1006
+ const nn = afN[nd];
1007
+ if (nn === NONE) break;
1008
+ if (afV[nn] === A2) break;
1009
+ const nnn = afN[nn];
1010
+ if (nnn === NONE) break;
1011
+
1012
+ const v1 = afV[nd], v2 = afV[nn], v3 = afV[nnn];
1013
+ if (v2 === A2 || v3 === A2) break;
1014
+
1015
+ const o = orient(v1, v2, v3);
1016
+ if (o > 0) {
1017
+ // Create triangle and remove middle vertex from AF
1018
+ const t = addTri(v1, v2, v3);
1019
+ linkToOldTri(t, 2, afT[nd], v1, v2);
1020
+ linkToOldTri(t, 0, afT[nn], v2, v3);
1021
+ legalize(t, v1);
1022
+ legalize(t, v3);
1023
+
1024
+ afDel(nn);
1025
+ afN[nd] = nnn; afP[nnn] = nd;
1026
+ afT[nd] = t;
1027
+
1028
+ // Step back to check the new triple
1029
+ const pp = afP[nd];
1030
+ if (pp !== NONE && afV[pp] !== A1) nd = pp;
1031
+ } else {
1032
+ nd = nn;
1033
+ }
1034
+ }
1035
+
1036
+ // Lower convex hull: follow artificial triangles from A2 to A1.
1037
+ // (Domiter Fig.15b/c) — These are triangles that include A1 or A2.
1038
+ // The simplest approach: after the upper hull is done, just mark all
1039
+ // triangles containing A1 or A2 as dead.
1040
+ }
1041
+
1042
+ // ---------------------------------------------------------------------------
1043
+ // Main entry point
1044
+ // ---------------------------------------------------------------------------
1045
+
1046
+ export function sweepTriangulate(
1047
+ coords: Float64Array,
1048
+ edges: Uint32Array | null,
1049
+ holes: Float64Array | null,
1050
+ ): CDTResult {
1051
+ const n = coords.length >> 1;
1052
+ if (n < 3) return { triangles: new Uint32Array(0), triCount: 0, coords };
1053
+
1054
+ // Build coordinate arrays with 2 extra artificial points
1055
+ const nv = n + 2;
1056
+ px = new Float64Array(nv);
1057
+ py = new Float64Array(nv);
1058
+ for (let i = 0; i < n; i++) { px[i] = coords[i * 2]; py[i] = coords[i * 2 + 1]; }
1059
+
1060
+ let xmin = px[0], xmax = px[0], ymin = py[0], ymax = py[0];
1061
+ for (let i = 1; i < n; i++) {
1062
+ if (px[i] < xmin) xmin = px[i]; if (px[i] > xmax) xmax = px[i];
1063
+ if (py[i] < ymin) ymin = py[i]; if (py[i] > ymax) ymax = py[i];
1064
+ }
1065
+
1066
+ // Artificial points P_{-1}, P_{-2} — Domiter §3.3, α=0.3
1067
+ const dx = 0.3 * (xmax - xmin + 1);
1068
+ const dy = 0.3 * (ymax - ymin + 1);
1069
+ const A1 = n, A2 = n + 1;
1070
+ px[A1] = xmin - dx; py[A1] = ymin - dy;
1071
+ px[A2] = xmax + dx; py[A2] = ymin - dy;
1072
+
1073
+ // Sort points by y then x — Domiter §3.3
1074
+ const order = new Uint32Array(n);
1075
+ for (let i = 0; i < n; i++) order[i] = i;
1076
+ order.sort((a, b) => { const d = py[a] - py[b]; return d !== 0 ? d : px[a] - px[b]; });
1077
+
1078
+ // Build edge info: upper endpoint → list of lower endpoints
1079
+ // Pre-count edges per vertex to avoid dynamic arrays
1080
+ let edgeLo: Int32Array | null = null;
1081
+ let edgeOff: Int32Array | null = null;
1082
+ if (edges !== null && edges.length > 0) {
1083
+ const ne = edges.length >> 1;
1084
+ const cnt = new Int32Array(n);
1085
+ for (let i = 0; i < ne; i++) {
1086
+ let ea = edges[i * 2], eb = edges[i * 2 + 1];
1087
+ if (py[ea] < py[eb] || (py[ea] === py[eb] && px[ea] < px[eb])) {
1088
+ cnt[eb]++;
1089
+ } else {
1090
+ cnt[ea]++;
1091
+ }
1092
+ }
1093
+ edgeOff = new Int32Array(n + 1);
1094
+ for (let i = 0; i < n; i++) edgeOff[i + 1] = edgeOff[i] + cnt[i];
1095
+ edgeLo = new Int32Array(edgeOff[n]);
1096
+ cnt.fill(0);
1097
+ for (let i = 0; i < ne; i++) {
1098
+ let ea = edges[i * 2], eb = edges[i * 2 + 1];
1099
+ let upper: number;
1100
+ let lower: number;
1101
+ if (py[ea] < py[eb] || (py[ea] === py[eb] && px[ea] < px[eb])) {
1102
+ upper = eb; lower = ea;
1103
+ } else {
1104
+ upper = ea; lower = eb;
1105
+ }
1106
+ edgeLo[edgeOff[upper] + cnt[upper]++] = lower;
1107
+ }
1108
+ }
1109
+
1110
+ // Initialize triangle pool
1111
+ const estTris = Math.max(n * 3, 64);
1112
+ initTris(estTris);
1113
+
1114
+ // Scratch arrays for edge events
1115
+ scrU = new Int32Array(n);
1116
+ scrL = new Int32Array(n);
1117
+ scrRm = new Int32Array(estTris);
1118
+ scrBndT = new Int32Array(estTris * 4);
1119
+ scrBndJ = new Int32Array(estTris * 4);
1120
+
1121
+ // Initialize AF
1122
+ initAF(n, xmin - dx, xmax + dx);
1123
+
1124
+ // vertAF: vertex → AF node id
1125
+ vertAF = new Int32Array(nv).fill(NONE);
1126
+
1127
+ // Initial triangle: (A1, P0, A2) — Domiter Fig.6
1128
+ const p0 = order[0];
1129
+ const t0 = addTri(A1, p0, A2);
1130
+
1131
+ // Initial advancing front: A1 → P0 → A2
1132
+ const nd1 = afNew(A1, t0);
1133
+ const nd0 = afNew(p0, t0);
1134
+ const nd2 = afNew(A2, NONE);
1135
+ afN[nd1] = nd0; afP[nd0] = nd1;
1136
+ afN[nd0] = nd2; afP[nd2] = nd0;
1137
+ vertAF[A1] = nd1; vertAF[p0] = nd0; vertAF[A2] = nd2;
1138
+
1139
+ // Sweep remaining points
1140
+ for (let i = 1; i < n; i++) {
1141
+ const pi = order[i];
1142
+ pointEvent(pi);
1143
+
1144
+ // Edge events
1145
+ if (edgeLo !== null && edgeOff !== null) {
1146
+ const start = edgeOff[pi], end = edgeOff[pi + 1];
1147
+ for (let e = start; e < end; e++) {
1148
+ edgeEvent(pi, edgeLo[e]);
1149
+ }
1150
+ }
1151
+ }
1152
+
1153
+ // Finalization — Domiter §3.5
1154
+ finalize(A1, A2);
1155
+
1156
+ // ----- Output -----
1157
+ // Filter out dead triangles and those with artificial points.
1158
+ let dead: Uint8Array | null = null;
1159
+ let outCount = 0;
1160
+
1161
+ if (holes !== null && holes.length >= 2) {
1162
+ dead = new Uint8Array(triCount);
1163
+ for (let t = 0; t < triCount; t++) {
1164
+ const t3 = t * 3;
1165
+ if (td[t] || tv[t3] >= n || tv[t3 + 1] >= n || tv[t3 + 2] >= n) dead[t] = 1;
1166
+ }
1167
+ const nh = holes.length >> 1;
1168
+ for (let h = 0; h < nh; h++) {
1169
+ const hx = holes[h * 2], hy = holes[h * 2 + 1];
1170
+ const seed = findTriContaining(hx, hy, n, dead);
1171
+ if (seed === NONE) continue;
1172
+ const q = [seed]; dead[seed] = 1;
1173
+ while (q.length) {
1174
+ const ct = q.pop()!;
1175
+ for (let j = 0; j < 3; j++) {
1176
+ if (isFixed(ct, j)) continue;
1177
+ const nb = ta[ct * 3 + j];
1178
+ if (nb === NONE || dead[nb]) continue;
1179
+ dead[nb] = 1; q.push(nb);
1180
+ }
1181
+ }
1182
+ }
1183
+ for (let t = 0; t < triCount; t++) { if (!dead[t]) outCount++; }
1184
+ } else {
1185
+ for (let t = 0; t < triCount; t++) {
1186
+ if (td[t]) continue;
1187
+ const t3 = t * 3;
1188
+ if (tv[t3] < n && tv[t3 + 1] < n && tv[t3 + 2] < n) outCount++;
1189
+ }
1190
+ }
1191
+
1192
+ const out = new Uint32Array(outCount * 3);
1193
+ let idx = 0;
1194
+ for (let t = 0; t < triCount; t++) {
1195
+ if (td[t]) continue;
1196
+ const t3 = t * 3;
1197
+ if (tv[t3] >= n || tv[t3 + 1] >= n || tv[t3 + 2] >= n) {
1198
+ if (dead === null) continue;
1199
+ }
1200
+ if (dead !== null && dead[t]) continue;
1201
+ out[idx++] = tv[t3]; out[idx++] = tv[t3 + 1]; out[idx++] = tv[t3 + 2];
1202
+ }
1203
+
1204
+ return { triangles: out, triCount: outCount, coords };
1205
+ }
1206
+
1207
+ function findTriContaining(hx: number, hy: number, nReal: number, dead: Uint8Array): number {
1208
+ for (let t = 0; t < triCount; t++) {
1209
+ if (dead[t]) continue;
1210
+ const t3 = t * 3;
1211
+ const a = tv[t3], b = tv[t3 + 1], c = tv[t3 + 2];
1212
+ if (a >= nReal || b >= nReal || c >= nReal) continue;
1213
+ const o1 = orient2dSign(px[a], py[a], px[b], py[b], hx, hy);
1214
+ const o2 = orient2dSign(px[b], py[b], px[c], py[c], hx, hy);
1215
+ const o3 = orient2dSign(px[c], py[c], px[a], py[a], hx, hy);
1216
+ if (o1 >= 0 && o2 >= 0 && o3 >= 0) return t;
1217
+ if (o1 <= 0 && o2 <= 0 && o3 <= 0) return t;
1218
+ }
1219
+ return NONE;
1220
+ }