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