pts 0.12.9 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/_path.ts ADDED
@@ -0,0 +1,1402 @@
1
+ /*! Pts.js is licensed under Apache License 2.0. Copyright © 2017-current William Ngan and contributors. (https://github.com/williamngan/pts) */
2
+
3
+ import { Pt, Group } from "./Pt";
4
+ import { Util } from "./Util";
5
+ import { crossingParameter, orient2d } from "./_triangulate";
6
+ import { type PolygonLike, type PtLike, type PtLikeIterable } from "./Types";
7
+
8
+ /**
9
+ * Internal planar overlay used by [`Path`](#link).
10
+ *
11
+ * Every mode is the same computation: build the arrangement of all input
12
+ * edges, label each face with the winding number of every shape, keep the
13
+ * faces the mode asks for, and either merge them (unite, intersect, exclude,
14
+ * minus front, minus back) or emit each one (divide, crop).
15
+ *
16
+ * Input vertices within `tol` are merged before constructing edges, and a
17
+ * vertex within `tol` of an edge's interior then splits that edge. This is an
18
+ * intentional geometric tolerance, not the precision of a Float32 point: it
19
+ * makes T-junctions, shared edges, and edges that coincide up to rounding
20
+ * share their vertices. Both happen before any crossing is computed, so no
21
+ * crossing is decided on geometry that a later split would bend. The proper
22
+ * crossings of the resulting sub-edges are then found with exact orientation
23
+ * predicates and an exactly evaluated crossing parameter, and merged at
24
+ * double rounding precision, independently of the input tolerance.
25
+ * Coincident sub-edges carry sparse per-shape winding deltas. Faces are
26
+ * half-edge cycles, labeled by flooding from one seed per component. Holes
27
+ * find their outer ring with a ray to the west. Output coordinates are
28
+ * Float32 Pts, so features below that precision may still collapse, and
29
+ * rings thinner than `tol` (hairlines left by edges that coincide up to
30
+ * rounding) are dropped.
31
+ */
32
+
33
+ /** The seven Path modes. */
34
+ export type PathMode =
35
+ | "unite"
36
+ | "intersect"
37
+ | "exclude"
38
+ | "minusFront"
39
+ | "minusBack"
40
+ | "divide"
41
+ | "crop";
42
+
43
+ // The snapping grid has cells of 64 tol, so a point only needs a neighboring
44
+ // cell checked when it lies within tol of that cell's border, and the cell
45
+ // indices stay within ±2^14 (tol is a fixed fraction of the largest
46
+ // coordinate), which keeps every key a small integer.
47
+ const CELL_TOLS = 64;
48
+ const CELL_OFFSET = 16384;
49
+ const CELL_SPAN = 32768;
50
+ // A westward ray query either scans every edge or walks the bounding-box
51
+ // tree; scanning costs about E per query and the tree about E log E to build,
52
+ // so the tree pays off after this many queries times log2(E). The number of
53
+ // queries is known before they start: components for the seeds, holes for
54
+ // the owners.
55
+ const RAY_TREE_PER_LOG = 1.5;
56
+ // Candidate pairs from the first sweep are kept, up to this many, so that the
57
+ // second sweep can replay them when the first registered no split (the common
58
+ // case). Beyond the cap they are streamed and enumerated again.
59
+ const PAIR_BUFFER = 1 << 18;
60
+
61
+ /** The rings of a shape as arrays of points: one ring (its first item is a point) or a list of rings. */
62
+ function ringsOf(shape: PolygonLike): PtLike[][] {
63
+ const list = Util.iterToArray(shape as Iterable<unknown>);
64
+ if (list.length === 0) return [];
65
+ const first = list[0];
66
+ if (first != null && typeof first[0] === "number") return [list];
67
+ const rings: PtLike[][] = new Array(list.length);
68
+ for (let i = 0; i < list.length; i++) rings[i] = Util.iterToArray(list[i]);
69
+ return rings;
70
+ }
71
+
72
+ /**
73
+ * A trig-free stand-in for `atan2`: increases counterclockwise over [0, 4),
74
+ * starting at west like atan2's range does, so the sort order and the
75
+ * "largest angle" at a leftmost vertex are the same as with atan2.
76
+ */
77
+ function pseudoAngle(dx: number, dy: number): number {
78
+ const s = (dx < 0 ? -dx : dx) + (dy < 0 ? -dy : dy);
79
+ // counterclockwise from east: [0, 4)
80
+ const d =
81
+ dy >= 0
82
+ ? dx >= 0
83
+ ? dy / s
84
+ : 1 - dx / s
85
+ : dx < 0
86
+ ? 2 - dy / s
87
+ : 3 + dx / s;
88
+ return d >= 2 ? d - 2 : d + 2;
89
+ }
90
+
91
+ /** Add a winding delta, keeping zero entries out of the sparse vector. */
92
+ function addWinding(
93
+ vector: Map<number, number>,
94
+ shape: number,
95
+ delta: number,
96
+ ): void {
97
+ const value = (vector.get(shape) ?? 0) + delta;
98
+ if (value === 0) vector.delete(shape);
99
+ else vector.set(shape, value);
100
+ }
101
+
102
+ /** How many distinct points a ring has, up to three: enough to tell a real ring from a degenerate one. */
103
+ function distinctPoints(ring: PtLike[]): number {
104
+ const seen: PtLike[] = [];
105
+ for (let i = 0; i < ring.length && seen.length < 3; i++) {
106
+ const p = ring[i];
107
+ let dup = false;
108
+ for (let j = 0; j < seen.length; j++) {
109
+ if (seen[j][0] === p[0] && seen[j][1] === p[1]) {
110
+ dup = true;
111
+ break;
112
+ }
113
+ }
114
+ if (!dup) seen.push(p);
115
+ }
116
+ return seen.length;
117
+ }
118
+
119
+ /**
120
+ * Reorder `items[lo, hi)` so the item at `k` has the key it would have when
121
+ * sorted, smaller keys before it and larger after (Hoare quickselect).
122
+ */
123
+ function selectByKey(
124
+ items: Uint32Array,
125
+ key: Float64Array,
126
+ lo: number,
127
+ hi: number,
128
+ k: number,
129
+ ): void {
130
+ let l = lo;
131
+ let r = hi - 1;
132
+ while (l < r) {
133
+ const pivot = key[items[(l + r) >> 1]];
134
+ let i = l;
135
+ let j = r;
136
+ while (i <= j) {
137
+ while (key[items[i]] < pivot) i++;
138
+ while (key[items[j]] > pivot) j--;
139
+ if (i <= j) {
140
+ const t = items[i];
141
+ items[i] = items[j];
142
+ items[j] = t;
143
+ i++;
144
+ j--;
145
+ }
146
+ }
147
+ if (k <= j) r = j;
148
+ else if (k >= i) l = i;
149
+ else return;
150
+ }
151
+ }
152
+
153
+ /** Output perimeter, for the hairline test. */
154
+ function outputPerimeter(ring: Group): number {
155
+ let length = 0;
156
+ for (let i = 0, n = ring.length; i < n; i++) {
157
+ const a = ring[i];
158
+ const b = ring[i === n - 1 ? 0 : i + 1];
159
+ length += Math.hypot(b[0] - a[0], b[1] - a[1]);
160
+ }
161
+ return length;
162
+ }
163
+
164
+ /** Output area relative to the first point, avoiding large-offset cancellation. */
165
+ function outputArea(ring: Group): number {
166
+ let area = 0;
167
+ const a = ring[0];
168
+ for (let i = 1; i + 1 < ring.length; i++) {
169
+ const b = ring[i],
170
+ c = ring[i + 1];
171
+ area += (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1]);
172
+ }
173
+ return area / 2;
174
+ }
175
+
176
+ /** Edge bounding boxes and the edges ordered by their left side, for a sweep. */
177
+ type SweepBoxes = {
178
+ minX: Float64Array;
179
+ maxX: Float64Array;
180
+ minY: Float64Array;
181
+ maxY: Float64Array;
182
+ order: Uint32Array;
183
+ };
184
+
185
+ /** Bounds of nonhorizontal edges used by westward rays. */
186
+ type RayNode = {
187
+ x0: number;
188
+ x1: number;
189
+ y0: number;
190
+ y1: number;
191
+ // A ray in this interval crosses every edge in the node. If the node is
192
+ // wholly west of the query, its aggregate winding can be added at once.
193
+ allY0: number;
194
+ allY1: number;
195
+ sum: Map<number, number>;
196
+ edges?: number[];
197
+ left?: RayNode;
198
+ right?: RayNode;
199
+ };
200
+
201
+ /** Internal arrangement, also exposed to the geometry invariant tests. */
202
+ export class Overlay {
203
+ tol = 0;
204
+ private rounding = 0;
205
+ k = 0; // number of shapes
206
+
207
+ // vertices in doubles, with a grid hash for snapping
208
+ vx: number[] = [];
209
+ vy: number[] = [];
210
+ private cells = new Map<number, number[]>();
211
+
212
+ // input edges, directed, one shape each
213
+ ea: number[] = [];
214
+ eb: number[] = [];
215
+ es: number[] = [];
216
+
217
+ // split events: (edge, parameter, vertex)
218
+ private spE: number[] = [];
219
+ private spT: number[] = [];
220
+ private spV: number[] = [];
221
+
222
+ // graph edges, low vertex to high vertex, with nonzero per-shape deltas
223
+ gu: number[] = [];
224
+ gv: number[] = [];
225
+ deltas: Map<number, number>[] = [];
226
+
227
+ // half-edges: 2e runs low to high, 2e + 1 the other way
228
+ offset!: Int32Array; // outgoing half-edges of vertex v are out[offset[v] .. offset[v + 1])
229
+ out!: Int32Array;
230
+ slot!: Int32Array; // index of each half-edge in its origin's sorted list
231
+ next!: Int32Array; // the next half-edge of the face on the left
232
+ cycle!: Int32Array; // cycle id per half-edge
233
+ cycleArea: number[] = [];
234
+ cycleStart: number[] = [];
235
+ // per cycle: how many shapes wind around its face, and whether the first
236
+ // and last shapes do. That is all the modes need, so the full sparse
237
+ // winding vectors are kept only when `label(true)` asks for them (tests).
238
+ count!: Int32Array;
239
+ hasFirst!: Uint8Array;
240
+ hasLast!: Uint8Array;
241
+ labels?: Map<number, number>[];
242
+ // undefined: not built yet; null: built, but no edge can cross a ray
243
+ private rayIndex: RayNode | null | undefined;
244
+
245
+ origin(h: number): number {
246
+ return h & 1 ? this.gv[h >> 1] : this.gu[h >> 1];
247
+ }
248
+
249
+ target(h: number): number {
250
+ return h & 1 ? this.gu[h >> 1] : this.gv[h >> 1];
251
+ }
252
+
253
+ // ------------------------------------------------------------- input
254
+
255
+ /** Read the shapes into snapped vertices and directed edges. False when there is nothing to combine. */
256
+ read(shapes: Iterable<PolygonLike> | PtLikeIterable): boolean {
257
+ let list: unknown[] = Util.iterToArray(shapes as Iterable<unknown>);
258
+ // A single ring of points is the likeliest misuse (every Polygon function
259
+ // takes one Group); treat it as a list of one shape.
260
+ if (
261
+ list.length > 0 &&
262
+ list[0] != null &&
263
+ typeof (list[0] as ArrayLike<unknown>)[0] === "number"
264
+ ) {
265
+ list = [list];
266
+ }
267
+ this.k = list.length;
268
+ const rings: PtLike[][] = [];
269
+ const ringShape: number[] = [];
270
+ let maxAbs = 0;
271
+
272
+ // first pass: the scale that sets the tolerance, and which rings are usable
273
+ for (let s = 0; s < list.length; s++) {
274
+ const shapeRings = ringsOf(list[s] as PolygonLike);
275
+ for (let r = 0; r < shapeRings.length; r++) {
276
+ const ring = shapeRings[r];
277
+ if (ring.length < 3) continue;
278
+ let finite = true;
279
+ let max = maxAbs;
280
+ for (let i = 0, n = ring.length; i < n; i++) {
281
+ const x = +ring[i][0];
282
+ const y = +ring[i][1];
283
+ if (!(Number.isFinite(x) && Number.isFinite(y))) {
284
+ finite = false;
285
+ break;
286
+ }
287
+ const ax = x < 0 ? -x : x;
288
+ const ay = y < 0 ? -y : y;
289
+ if (ax > max) max = ax;
290
+ if (ay > max) max = ay;
291
+ }
292
+ if (!finite) {
293
+ Util.warn("Path skipped a ring with a non-finite coordinate");
294
+ continue;
295
+ }
296
+ maxAbs = max;
297
+ rings.push(ring);
298
+ ringShape.push(s);
299
+ }
300
+ }
301
+ if (rings.length === 0 || maxAbs === 0) return false;
302
+ this.tol = maxAbs * 1e-6;
303
+ this.rounding = maxAbs * Number.EPSILON * 8;
304
+
305
+ // second pass: snap each ring's points to vertices, dropping consecutive
306
+ // repeats and the closing repeat, then emit its edges
307
+ const ids: number[] = [];
308
+ for (let r = 0; r < rings.length; r++) {
309
+ const ring = rings[r];
310
+ ids.length = 0;
311
+ let prev = -1;
312
+ for (let i = 0, n = ring.length; i < n; i++) {
313
+ const v = this.vertexAt(+ring[i][0], +ring[i][1]);
314
+ if (v !== prev) {
315
+ ids.push(v);
316
+ prev = v;
317
+ }
318
+ }
319
+ while (ids.length > 1 && ids[ids.length - 1] === ids[0]) ids.pop();
320
+ if (ids.length >= 3) {
321
+ for (let i = 0, n = ids.length; i < n; i++) {
322
+ this.ea.push(ids[i]);
323
+ this.eb.push(ids[i === n - 1 ? 0 : i + 1]);
324
+ this.es.push(ringShape[r]);
325
+ }
326
+ } else if (distinctPoints(ring) >= 3) {
327
+ Util.warn(
328
+ "Path dropped a ring whose points merge within the tolerance; use local coordinates for small shapes at large offsets",
329
+ );
330
+ }
331
+ }
332
+ return this.ea.length > 0;
333
+ }
334
+
335
+ /** The vertex within `tol` of (x, y), or a new one. */
336
+ vertexAt(x: number, y: number, tol = this.tol): number {
337
+ const cellSize = this.tol * CELL_TOLS;
338
+ const fx = x / cellSize;
339
+ const fy = y / cellSize;
340
+ const cx = Math.floor(fx);
341
+ const cy = Math.floor(fy);
342
+ // neighbors to check: only across a border closer than tol
343
+ const border = tol / cellSize;
344
+ const x0 = fx - cx <= border ? -1 : 0;
345
+ const x1 = fx - cx >= 1 - border ? 1 : 0;
346
+ const y0 = fy - cy <= border ? -1 : 0;
347
+ const y1 = fy - cy >= 1 - border ? 1 : 0;
348
+ const t2 = tol * tol;
349
+ for (let i = x0; i <= x1; i++) {
350
+ for (let j = y0; j <= y1; j++) {
351
+ const cell = this.cells.get(
352
+ (cx + i + CELL_OFFSET) * CELL_SPAN + (cy + j + CELL_OFFSET),
353
+ );
354
+ if (!cell) continue;
355
+ for (let n = 0; n < cell.length; n++) {
356
+ const v = cell[n];
357
+ const dx = this.vx[v] - x;
358
+ const dy = this.vy[v] - y;
359
+ if (dx * dx + dy * dy <= t2) return v;
360
+ }
361
+ }
362
+ }
363
+ const id = this.vx.length;
364
+ this.vx.push(x);
365
+ this.vy.push(y);
366
+ const key = (cx + CELL_OFFSET) * CELL_SPAN + (cy + CELL_OFFSET);
367
+ const cell = this.cells.get(key);
368
+ if (cell) cell.push(id);
369
+ else this.cells.set(key, [id]);
370
+ return id;
371
+ }
372
+
373
+ // ------------------------------------------------------------ splits
374
+
375
+ /**
376
+ * Split edges where they meet, in two phases. First, a vertex within tol of
377
+ * an edge's interior splits that edge there, so T-junctions, shared and
378
+ * overlapping edges, and vertices that miss an edge by rounding all become
379
+ * shared vertices. That bends edges by up to tol, which is why it happens
380
+ * before any crossing is computed. The second phase finds the proper
381
+ * crossings of the resulting sub-edges with exact predicates, so no
382
+ * crossing is decided on geometry that a later split would change.
383
+ */
384
+ split(): void {
385
+ let boxes = this.boxes();
386
+ const pairs = this.sweep(boxes, this.tol, true);
387
+ if (this.spE.length > 0) {
388
+ // the sub-edges are new edges; enumerate their pairs again, exactly
389
+ this.applySplits();
390
+ boxes = this.boxes();
391
+ this.sweep(boxes, 0, false);
392
+ } else if (pairs) {
393
+ for (let i = 0; i < pairs.length; i += 2) {
394
+ this.crossPair(pairs[i], pairs[i + 1]);
395
+ }
396
+ } else {
397
+ this.sweep(boxes, 0, false);
398
+ }
399
+ }
400
+
401
+ /** Bounding boxes of the current edges, with the edges ordered by their left side. */
402
+ private boxes(): SweepBoxes {
403
+ const E = this.ea.length;
404
+ const vx = this.vx;
405
+ const vy = this.vy;
406
+ const minX = new Float64Array(E);
407
+ const maxX = new Float64Array(E);
408
+ const minY = new Float64Array(E);
409
+ const maxY = new Float64Array(E);
410
+ for (let e = 0; e < E; e++) {
411
+ const ax = vx[this.ea[e]];
412
+ const ay = vy[this.ea[e]];
413
+ const bx = vx[this.eb[e]];
414
+ const by = vy[this.eb[e]];
415
+ minX[e] = ax < bx ? ax : bx;
416
+ maxX[e] = ax < bx ? bx : ax;
417
+ minY[e] = ay < by ? ay : by;
418
+ maxY[e] = ay < by ? by : ay;
419
+ }
420
+ const order = new Uint32Array(E);
421
+ for (let e = 0; e < E; e++) order[e] = e;
422
+ order.sort((p, q) => minX[p] - minX[q]);
423
+ return { minX, maxX, minY, maxY, order };
424
+ }
425
+
426
+ /**
427
+ * Run a phase over every two edges whose boxes, grown by `margin`, overlap:
428
+ * the touch phase (which also returns the pairs, unless there are more than
429
+ * the buffer holds) or the crossing phase. Pairs are visited as they are
430
+ * found rather than listed first: shapes that all share a vertex have
431
+ * quadratically many candidate pairs, more than an array can hold.
432
+ */
433
+ private sweep(
434
+ boxes: SweepBoxes,
435
+ margin: number,
436
+ touching: boolean,
437
+ ): number[] | undefined {
438
+ const { minX, maxX, minY, maxY, order } = boxes;
439
+ let pairs: number[] | undefined = touching ? [] : undefined;
440
+ for (let i = 0, E = order.length; i < E; i++) {
441
+ const e1 = order[i];
442
+ const limit = maxX[e1] + margin;
443
+ const lo = minY[e1] - margin;
444
+ const hi = maxY[e1] + margin;
445
+ for (let j = i + 1; j < E; j++) {
446
+ const e2 = order[j];
447
+ if (minX[e2] > limit) break;
448
+ if (minY[e2] > hi || maxY[e2] < lo) continue;
449
+ if (touching) {
450
+ this.touchPair(e1, e2, boxes);
451
+ if (pairs) {
452
+ if (pairs.length < 2 * PAIR_BUFFER) pairs.push(e1, e2);
453
+ else pairs = undefined;
454
+ }
455
+ } else {
456
+ this.crossPair(e1, e2);
457
+ }
458
+ }
459
+ }
460
+ return pairs;
461
+ }
462
+
463
+ /** Phase one: each endpoint of one edge within tol of the other edge's interior splits it. */
464
+ private touchPair(e1: number, e2: number, boxes: SweepBoxes): void {
465
+ const a = this.ea[e1];
466
+ const b = this.eb[e1];
467
+ const c = this.ea[e2];
468
+ const d = this.eb[e2];
469
+ // an endpoint can only touch an edge whose box it is in (grown by tol)
470
+ if (this.inBox(a, e2, boxes)) this.touch(e2, a, c, d);
471
+ if (this.inBox(b, e2, boxes)) this.touch(e2, b, c, d);
472
+ if (this.inBox(c, e1, boxes)) this.touch(e1, c, a, b);
473
+ if (this.inBox(d, e1, boxes)) this.touch(e1, d, a, b);
474
+ }
475
+
476
+ private inBox(v: number, e: number, boxes: SweepBoxes): boolean {
477
+ const x = this.vx[v];
478
+ const y = this.vy[v];
479
+ const tol = this.tol;
480
+ return (
481
+ x >= boxes.minX[e] - tol &&
482
+ x <= boxes.maxX[e] + tol &&
483
+ y >= boxes.minY[e] - tol &&
484
+ y <= boxes.maxY[e] + tol
485
+ );
486
+ }
487
+
488
+ /** Register vertex `v` on edge `e` (from `p` to `q`) when it lies within tol of the edge's interior. */
489
+ private touch(e: number, v: number, p: number, q: number): void {
490
+ if (v === p || v === q) return;
491
+ const vx = this.vx;
492
+ const vy = this.vy;
493
+ const px = vx[p];
494
+ const py = vy[p];
495
+ const rx = vx[q] - px;
496
+ const ry = vy[q] - py;
497
+ const len2 = rx * rx + ry * ry;
498
+ const dx = vx[v] - px;
499
+ const dy = vy[v] - py;
500
+ const t = (dx * rx + dy * ry) / len2;
501
+ if (t <= 0 || t >= 1) return;
502
+ const cross = rx * dy - ry * dx;
503
+ if (cross * cross > this.tol * this.tol * len2) return;
504
+ this.registerSplit(e, v);
505
+ }
506
+
507
+ /** Phase two: split two sub-edges that share no vertex at their proper crossing. */
508
+ private crossPair(e1: number, e2: number): void {
509
+ const a = this.ea[e1];
510
+ const b = this.eb[e1];
511
+ const c = this.ea[e2];
512
+ const d = this.eb[e2];
513
+ if (a === c || a === d || b === c || b === d) return;
514
+
515
+ // Exact signs keep shallow crossings distinct from collinear overlaps.
516
+ const vx = this.vx;
517
+ const vy = this.vy;
518
+ const ax = vx[a];
519
+ const ay = vy[a];
520
+ const bx = vx[b];
521
+ const by = vy[b];
522
+ const cx = vx[c];
523
+ const cy = vy[c];
524
+ const dx = vx[d];
525
+ const dy = vy[d];
526
+ if (
527
+ orient2d(ax, ay, bx, by, cx, cy) * orient2d(ax, ay, bx, by, dx, dy) >=
528
+ 0
529
+ )
530
+ return;
531
+ if (
532
+ orient2d(cx, cy, dx, dy, ax, ay) * orient2d(cx, cy, dx, dy, bx, by) >=
533
+ 0
534
+ )
535
+ return;
536
+ const t = crossingParameter(ax, ay, bx, by, cx, cy, dx, dy);
537
+ const v = this.vertexAt(
538
+ ax + t * (bx - ax),
539
+ ay + t * (by - ay),
540
+ this.rounding,
541
+ );
542
+ this.registerSplit(e1, v);
543
+ this.registerSplit(e2, v);
544
+ }
545
+
546
+ /** Replace every edge by its pieces between the registered split vertices, in order. */
547
+ private applySplits(): void {
548
+ const E = this.ea.length;
549
+ const S = this.spE.length;
550
+ const spE = this.spE;
551
+ const spT = this.spT;
552
+ const spV = this.spV;
553
+ const order = new Uint32Array(S);
554
+ for (let i = 0; i < S; i++) order[i] = i;
555
+ order.sort((p, q) => spE[p] - spE[q] || spT[p] - spT[q]);
556
+ const ea: number[] = [];
557
+ const eb: number[] = [];
558
+ const es: number[] = [];
559
+ let si = 0;
560
+ for (let e = 0; e < E; e++) {
561
+ let u = this.ea[e];
562
+ const end = this.eb[e];
563
+ const s = this.es[e];
564
+ while (si < S && spE[order[si]] === e) {
565
+ const v = spV[order[si++]];
566
+ if (v === u) continue;
567
+ ea.push(u);
568
+ eb.push(v);
569
+ es.push(s);
570
+ u = v;
571
+ }
572
+ if (u !== end) {
573
+ ea.push(u);
574
+ eb.push(end);
575
+ es.push(s);
576
+ }
577
+ }
578
+ this.ea = ea;
579
+ this.eb = eb;
580
+ this.es = es;
581
+ spE.length = 0;
582
+ spT.length = 0;
583
+ spV.length = 0;
584
+ }
585
+
586
+ /** Record that edge `e` passes through vertex `v`. The parameter is always the projection, so repeats sort together. */
587
+ private registerSplit(e: number, v: number): void {
588
+ const a = this.ea[e];
589
+ const b = this.eb[e];
590
+ if (v === a || v === b) return;
591
+ const ax = this.vx[a];
592
+ const ay = this.vy[a];
593
+ const rx = this.vx[b] - ax;
594
+ const ry = this.vy[b] - ay;
595
+ const t =
596
+ ((this.vx[v] - ax) * rx + (this.vy[v] - ay) * ry) / (rx * rx + ry * ry);
597
+ if (t <= 0 || t >= 1) return;
598
+ this.spE.push(e);
599
+ this.spT.push(t);
600
+ this.spV.push(v);
601
+ }
602
+
603
+ // ------------------------------------------------------------- graph
604
+
605
+ /** Turn the split edges into unique graph edges with per-shape deltas; drop edges that separate nothing. */
606
+ merge(): void {
607
+ if (this.spE.length > 0) this.applySplits();
608
+ const V = this.vx.length;
609
+ const index = new Map<number, number>();
610
+ const gu = this.gu;
611
+ const gv = this.gv;
612
+ const deltas = this.deltas;
613
+ for (let e = 0, E = this.ea.length; e < E; e++) {
614
+ const u = this.ea[e];
615
+ const v = this.eb[e];
616
+ const lo = u < v ? u : v;
617
+ const hi = u < v ? v : u;
618
+ const key = lo * V + hi;
619
+ let g = index.get(key);
620
+ if (g === undefined) {
621
+ g = gu.length;
622
+ gu.push(lo);
623
+ gv.push(hi);
624
+ deltas.push(new Map());
625
+ index.set(key, g);
626
+ }
627
+ addWinding(deltas[g], this.es[e], u === lo ? 1 : -1);
628
+ }
629
+
630
+ // compact away edges whose deltas are all zero
631
+ let n = 0;
632
+ for (let e = 0, len = gu.length; e < len; e++) {
633
+ if (deltas[e].size === 0) continue;
634
+ if (n !== e) {
635
+ gu[n] = gu[e];
636
+ gv[n] = gv[e];
637
+ deltas[n] = deltas[e];
638
+ }
639
+ n++;
640
+ }
641
+ gu.length = n;
642
+ gv.length = n;
643
+ deltas.length = n;
644
+ }
645
+
646
+ /** The ray tree once enough queries justify building it; until then callers scan every edge. */
647
+ private rayTree(): RayNode | null | undefined {
648
+ return this.rayIndex;
649
+ }
650
+
651
+ /** Build the ray tree if the coming `queries` are more than scanning is worth. */
652
+ private expectRayQueries(queries: number): void {
653
+ const E = this.gu.length;
654
+ if (
655
+ this.rayIndex === undefined &&
656
+ queries > RAY_TREE_PER_LOG * Math.log2(E + 1) + 1
657
+ ) {
658
+ this.rayIndex = this.indexRays() ?? null;
659
+ }
660
+ }
661
+
662
+ /** Balanced bounding-box tree for seed winding and nearest boundary queries. */
663
+ private indexRays(): RayNode | undefined {
664
+ const E = this.gu.length;
665
+ const vx = this.vx;
666
+ const vy = this.vy;
667
+ let n = 0;
668
+ const edges = new Uint32Array(E);
669
+ for (let e = 0; e < E; e++) {
670
+ if (vy[this.gu[e]] !== vy[this.gv[e]]) edges[n++] = e;
671
+ }
672
+ if (n === 0) return undefined;
673
+ // edge centers, so each level partitions by a plain number
674
+ const cx = new Float64Array(E);
675
+ const cy = new Float64Array(E);
676
+ for (let e = 0; e < E; e++) {
677
+ cx[e] = vx[this.gu[e]] + vx[this.gv[e]];
678
+ cy[e] = vy[this.gu[e]] + vy[this.gv[e]];
679
+ }
680
+ const build = (lo: number, hi: number): RayNode => {
681
+ const node: RayNode = {
682
+ x0: Infinity,
683
+ x1: -Infinity,
684
+ y0: Infinity,
685
+ y1: -Infinity,
686
+ allY0: -Infinity,
687
+ allY1: Infinity,
688
+ sum: new Map(),
689
+ };
690
+ for (let i = lo; i < hi; i++) {
691
+ const e = edges[i];
692
+ const u = this.gu[e];
693
+ const v = this.gv[e];
694
+ const y0 = Math.min(vy[u], vy[v]);
695
+ const y1 = Math.max(vy[u], vy[v]);
696
+ node.x0 = Math.min(node.x0, vx[u], vx[v]);
697
+ node.x1 = Math.max(node.x1, vx[u], vx[v]);
698
+ node.y0 = Math.min(node.y0, y0);
699
+ node.y1 = Math.max(node.y1, y1);
700
+ node.allY0 = Math.max(node.allY0, y0);
701
+ node.allY1 = Math.min(node.allY1, y1);
702
+ }
703
+ if (hi - lo <= 8) {
704
+ node.edges = Array.from(edges.subarray(lo, hi));
705
+ if (node.allY0 < node.allY1) {
706
+ for (const e of node.edges) {
707
+ const sign = vy[this.gv[e]] > vy[this.gu[e]] ? -1 : 1;
708
+ for (const [s, d] of this.deltas[e])
709
+ addWinding(node.sum, s, sign * d);
710
+ }
711
+ }
712
+ } else {
713
+ const key = node.x1 - node.x0 >= node.y1 - node.y0 ? cx : cy;
714
+ const mid = (lo + hi) >> 1;
715
+ selectByKey(edges, key, lo, hi, mid);
716
+ node.left = build(lo, mid);
717
+ node.right = build(mid, hi);
718
+ if (node.allY0 < node.allY1) {
719
+ for (const child of [node.left, node.right])
720
+ for (const [s, d] of child.sum) addWinding(node.sum, s, d);
721
+ }
722
+ }
723
+ return node;
724
+ };
725
+ return build(0, n);
726
+ }
727
+
728
+ // ------------------------------------------------------------- faces
729
+
730
+ /** Order the half-edges around each vertex and trace the cycles that bound the faces. */
731
+ trace(): void {
732
+ const E = this.gu.length;
733
+ const H = 2 * E;
734
+ const V = this.vx.length;
735
+ const vx = this.vx;
736
+ const vy = this.vy;
737
+ const gu = this.gu;
738
+ const gv = this.gv;
739
+
740
+ const angle = new Float64Array(H);
741
+ for (let e = 0; e < E; e++) {
742
+ const dx = vx[gv[e]] - vx[gu[e]];
743
+ const dy = vy[gv[e]] - vy[gu[e]];
744
+ angle[2 * e] = pseudoAngle(dx, dy);
745
+ angle[2 * e + 1] = pseudoAngle(-dx, -dy);
746
+ }
747
+
748
+ const offset = new Int32Array(V + 1);
749
+ for (let e = 0; e < E; e++) {
750
+ offset[gu[e] + 1]++;
751
+ offset[gv[e] + 1]++;
752
+ }
753
+ for (let v = 0; v < V; v++) offset[v + 1] += offset[v];
754
+ const out = new Int32Array(H);
755
+ const cursor = offset.slice(0, V);
756
+ for (let e = 0; e < E; e++) {
757
+ out[cursor[gu[e]]++] = 2 * e;
758
+ out[cursor[gv[e]]++] = 2 * e + 1;
759
+ }
760
+
761
+ // counterclockwise order around each vertex; the lists are tiny, so an
762
+ // insertion sort in place beats extracting them
763
+ const slot = new Int32Array(H);
764
+ for (let v = 0; v < V; v++) {
765
+ const lo = offset[v];
766
+ const hi = offset[v + 1];
767
+ for (let i = lo + 1; i < hi; i++) {
768
+ const h = out[i];
769
+ const a = angle[h];
770
+ let j = i - 1;
771
+ while (
772
+ j >= lo &&
773
+ (angle[out[j]] > a ||
774
+ (angle[out[j]] === a && this.target(out[j]) > this.target(h)))
775
+ ) {
776
+ out[j + 1] = out[j];
777
+ j--;
778
+ }
779
+ out[j + 1] = h;
780
+ }
781
+ for (let i = lo; i < hi; i++) slot[out[i]] = i - lo;
782
+ }
783
+
784
+ // the face on the left of h continues along the first outgoing half-edge
785
+ // clockwise from h's twin
786
+ const next = new Int32Array(H);
787
+ for (let h = 0; h < H; h++) {
788
+ const v = this.target(h);
789
+ const base = offset[v];
790
+ const n = offset[v + 1] - base;
791
+ const s = slot[h ^ 1];
792
+ next[h] = out[base + (s === 0 ? n - 1 : s - 1)];
793
+ }
794
+
795
+ const cycle = new Int32Array(H).fill(-1);
796
+ const cycleArea = this.cycleArea;
797
+ const cycleStart = this.cycleStart;
798
+ for (let h = 0; h < H; h++) {
799
+ if (cycle[h] >= 0) continue;
800
+ const c = cycleArea.length;
801
+ let area = 0;
802
+ let cur = h;
803
+ do {
804
+ cycle[cur] = c;
805
+ const o = this.origin(cur);
806
+ const t = this.target(cur);
807
+ area += vx[o] * vy[t] - vx[t] * vy[o];
808
+ cur = next[cur];
809
+ } while (cur !== h);
810
+ cycleArea.push(area / 2);
811
+ cycleStart.push(h);
812
+ }
813
+
814
+ this.offset = offset;
815
+ this.out = out;
816
+ this.slot = slot;
817
+ this.next = next;
818
+ this.cycle = cycle;
819
+ }
820
+
821
+ /**
822
+ * Label every cycle with what the modes need to know about the winding
823
+ * numbers on its face. With `full`, the sparse winding vectors are kept too.
824
+ */
825
+ label(full = false): void {
826
+ const V = this.vx.length;
827
+ const E = this.gu.length;
828
+ const C = this.cycleArea.length;
829
+ const vx = this.vx;
830
+ const vy = this.vy;
831
+ const offset = this.offset;
832
+
833
+ // connected components by union-find over the graph edges
834
+ const parent = new Int32Array(V);
835
+ for (let v = 0; v < V; v++) parent[v] = v;
836
+ const find = (v: number) => {
837
+ while (parent[v] !== v) {
838
+ parent[v] = parent[parent[v]];
839
+ v = parent[v];
840
+ }
841
+ return v;
842
+ };
843
+ for (let e = 0; e < E; e++) {
844
+ const a = find(this.gu[e]);
845
+ const b = find(this.gv[e]);
846
+ if (a !== b) parent[a] = b;
847
+ }
848
+ const leftmost = new Int32Array(V).fill(-1);
849
+ let components = 0;
850
+ for (let v = 0; v < V; v++) {
851
+ if (offset[v + 1] === offset[v]) continue; // no edges left
852
+ const r = find(v);
853
+ const l = leftmost[r];
854
+ if (l < 0) components++;
855
+ if (l < 0 || vx[v] < vx[l] || (vx[v] === vx[l] && vy[v] < vy[l])) {
856
+ leftmost[r] = v;
857
+ }
858
+ }
859
+
860
+ const count = new Int32Array(C);
861
+ const hasFirst = new Uint8Array(C);
862
+ const hasLast = new Uint8Array(C);
863
+ const labels = full ? new Array<Map<number, number>>(C) : undefined;
864
+ const labeled = new Uint8Array(C);
865
+ const last = this.k - 1;
866
+ // One winding vector, edited on the way into a neighboring face and
867
+ // restored on the way back, so the memory is one vector plus three
868
+ // numbers per face rather than a vector per face.
869
+ const w = new Map<number, number>();
870
+ const record = (c: number) => {
871
+ count[c] = w.size;
872
+ hasFirst[c] = w.has(0) ? 1 : 0;
873
+ hasLast[c] = w.has(last) ? 1 : 0;
874
+ if (labels) labels[c] = new Map(w);
875
+ labeled[c] = 1;
876
+ };
877
+ const step = (h: number, dir: number) => {
878
+ const sign = (h & 1 ? 1 : -1) * dir;
879
+ for (const [s, d] of this.deltas[h >> 1]) addWinding(w, s, sign * d);
880
+ };
881
+ // depth-first frames: the cycle, the next half-edge to look across, and
882
+ // the half-edge that led into the cycle
883
+ const fc = new Int32Array(C);
884
+ const fh = new Int32Array(C);
885
+ const fvia = new Int32Array(C);
886
+ if (components > 1) this.expectRayQueries(components);
887
+ for (let r = 0; r < V; r++) {
888
+ const p = leftmost[r];
889
+ if (p < 0) continue;
890
+ // Every edge at the leftmost vertex points into the east half-plane, so
891
+ // the outgoing half-edge with the largest angle has the component's
892
+ // unbounded face on its left. Its winding numbers are exactly the
893
+ // graph's crossings of the ray going west from here.
894
+ const g = this.out[offset[p + 1] - 1];
895
+ const c0 = this.cycle[g];
896
+ if (labeled[c0]) continue;
897
+ w.clear();
898
+ // A connected graph has no other component around its unbounded face.
899
+ if (components > 1) this.windingWest(p, w);
900
+ record(c0);
901
+ let depth = 0;
902
+ fc[0] = c0;
903
+ fh[0] = this.cycleStart[c0];
904
+ fvia[0] = -1;
905
+ while (depth >= 0) {
906
+ const h = fh[depth];
907
+ if (h < 0) {
908
+ // every neighbor of this cycle is labeled: restore and back out
909
+ if (fvia[depth] >= 0) step(fvia[depth], -1);
910
+ depth--;
911
+ continue;
912
+ }
913
+ const hn = this.next[h];
914
+ fh[depth] = hn === this.cycleStart[fc[depth]] ? -1 : hn;
915
+ const c2 = this.cycle[h ^ 1];
916
+ if (labeled[c2]) continue;
917
+ step(h, 1);
918
+ record(c2);
919
+ depth++;
920
+ fc[depth] = c2;
921
+ fh[depth] = this.cycleStart[c2];
922
+ fvia[depth] = h;
923
+ }
924
+ }
925
+ this.count = count;
926
+ this.hasFirst = hasFirst;
927
+ this.hasLast = hasLast;
928
+ this.labels = labels;
929
+ }
930
+
931
+ /** Add the winding numbers just west of vertex p into a sparse label. */
932
+ private windingWest(p: number, labels: Map<number, number>): void {
933
+ const px = this.vx[p];
934
+ const py = this.vy[p];
935
+ const gu = this.gu;
936
+ const gv = this.gv;
937
+ const vy = this.vy;
938
+ const deltas = this.deltas;
939
+ const cross = (e: number) => {
940
+ const u = gu[e];
941
+ const v = gv[e];
942
+ if (u === p || v === p) return; // meets the ray at p itself
943
+ const uy = vy[u];
944
+ const wy = vy[v];
945
+ if (uy > py === wy > py) return;
946
+ if (this.crossingX(u, v, py) >= px) return;
947
+ // a ring winding counterclockwise around p crosses the west ray downward
948
+ const sign = wy > uy ? -1 : 1;
949
+ for (const [s, d] of deltas[e]) addWinding(labels, s, sign * d);
950
+ };
951
+ const tree = this.rayTree();
952
+ if (tree === undefined) {
953
+ for (let e = 0, E = this.gu.length; e < E; e++) cross(e);
954
+ return;
955
+ }
956
+ const stack = tree ? [tree] : [];
957
+ while (stack.length > 0) {
958
+ const node = stack.pop()!;
959
+ if (node.x0 >= px || py < node.y0 || py >= node.y1) continue;
960
+ if (node.x1 < px && py >= node.allY0 && py < node.allY1) {
961
+ for (const [s, d] of node.sum) addWinding(labels, s, d);
962
+ continue;
963
+ }
964
+ if (!node.edges) {
965
+ stack.push(node.left!, node.right!);
966
+ continue;
967
+ }
968
+ for (const e of node.edges) cross(e);
969
+ }
970
+ }
971
+
972
+ /**
973
+ * Where edge u→v crosses the horizontal line at y, given that it straddles it
974
+ * by the half-open rule. An endpoint on the line is the crossing itself, taken
975
+ * exactly so that ties at a vertex are exact ties.
976
+ */
977
+ private crossingX(u: number, v: number, y: number): number {
978
+ const uy = this.vy[u];
979
+ const wy = this.vy[v];
980
+ if (uy === y) return this.vx[u];
981
+ if (wy === y) return this.vx[v];
982
+ const ux = this.vx[u];
983
+ return ux + ((y - uy) * (this.vx[v] - ux)) / (wy - uy);
984
+ }
985
+
986
+ // ----------------------------------------------------------- selection
987
+
988
+ /** Which cycles bound kept faces (on their left) for a mode. */
989
+ select(mode: PathMode): Uint8Array {
990
+ const C = this.cycleArea.length;
991
+ const k = this.k;
992
+ const keep = new Uint8Array(C);
993
+ for (let c = 0; c < C; c++) {
994
+ const count = this.count[c];
995
+ const first = this.hasFirst[c] === 1;
996
+ const last = this.hasLast[c] === 1;
997
+ let on = false;
998
+ switch (mode) {
999
+ case "intersect":
1000
+ on = count === k;
1001
+ break;
1002
+ case "exclude":
1003
+ on = (count & 1) === 1;
1004
+ break;
1005
+ case "minusFront":
1006
+ on = first && count === 1;
1007
+ break;
1008
+ case "minusBack":
1009
+ on = last && count === 1;
1010
+ break;
1011
+ case "crop":
1012
+ on = last && count >= 2;
1013
+ break;
1014
+ default: // unite, divide
1015
+ on = count > 0;
1016
+ }
1017
+ keep[c] = on ? 1 : 0;
1018
+ }
1019
+ return keep;
1020
+ }
1021
+
1022
+ /**
1023
+ * Rings of the merged kept region: the half-edges with a kept face on the
1024
+ * left and an unkept face on the right, traced with the same turning rule
1025
+ * restricted to those half-edges.
1026
+ */
1027
+ mergedRings(keep: Uint8Array, ringOf: Int32Array): number[][] {
1028
+ const H = ringOf.length;
1029
+ const cycle = this.cycle;
1030
+ const boundary = (h: number) =>
1031
+ keep[cycle[h]] === 1 && keep[cycle[h ^ 1]] === 0;
1032
+ const rings: number[][] = [];
1033
+ for (let h = 0; h < H; h++) {
1034
+ if (ringOf[h] >= 0 || !boundary(h)) continue;
1035
+ const r = rings.length;
1036
+ const seq: number[] = [];
1037
+ let cur = h;
1038
+ do {
1039
+ ringOf[cur] = r;
1040
+ seq.push(cur);
1041
+ // first boundary half-edge clockwise from the twin
1042
+ const v = this.target(cur);
1043
+ const base = this.offset[v];
1044
+ const n = this.offset[v + 1] - base;
1045
+ const s = this.slot[cur ^ 1];
1046
+ let found = -1;
1047
+ for (let i = 1; i <= n; i++) {
1048
+ const cand = this.out[base + ((s - i + n) % n)];
1049
+ if (boundary(cand)) {
1050
+ found = cand;
1051
+ break;
1052
+ }
1053
+ }
1054
+ if (found < 0) break; // cannot happen on a planar graph; end the ring
1055
+ cur = found;
1056
+ } while (cur !== h);
1057
+ rings.push(seq);
1058
+ }
1059
+ return rings;
1060
+ }
1061
+
1062
+ /**
1063
+ * Split each traced walk at repeated vertices into simple rings. A boundary
1064
+ * that touches itself at a vertex (a hole pinched to its outer ring, two
1065
+ * holes meeting, two regions meeting at a corner) is one closed walk; its
1066
+ * simple pieces are separate rings. Returns the walk each ring came from.
1067
+ */
1068
+ simpleRings(
1069
+ walks: number[][],
1070
+ ringOf: Int32Array,
1071
+ ): { rings: number[][]; walk: number[] } {
1072
+ const at = new Int32Array(this.vx.length).fill(-1); // stack position of a vertex
1073
+ const rings: number[][] = [];
1074
+ const walk: number[] = [];
1075
+ const emit = (seq: number[], w: number) => {
1076
+ for (let i = 0; i < seq.length; i++) ringOf[seq[i]] = rings.length;
1077
+ rings.push(seq);
1078
+ walk.push(w);
1079
+ };
1080
+ for (let w = 0; w < walks.length; w++) {
1081
+ const seq = walks[w];
1082
+ const open: number[] = [];
1083
+ for (let i = 0; i < seq.length; i++) {
1084
+ const h = seq[i];
1085
+ const o = this.origin(h);
1086
+ const pos = at[o];
1087
+ if (pos >= 0) {
1088
+ // back at a vertex already on the walk: the part since then is a loop
1089
+ const loop = open.splice(pos);
1090
+ for (let j = 0; j < loop.length; j++) at[this.origin(loop[j])] = -1;
1091
+ emit(loop, w);
1092
+ }
1093
+ at[o] = open.length;
1094
+ open.push(h);
1095
+ }
1096
+ for (let j = 0; j < open.length; j++) at[this.origin(open[j])] = -1;
1097
+ emit(open, w);
1098
+ }
1099
+ return { rings, walk };
1100
+ }
1101
+
1102
+ /** Rings of every kept face, one per cycle. */
1103
+ faceRings(keep: Uint8Array, ringOf: Int32Array): number[][] {
1104
+ const rings: number[][] = [];
1105
+ for (let c = 0, C = this.cycleArea.length; c < C; c++) {
1106
+ if (!keep[c]) continue;
1107
+ const r = rings.length;
1108
+ const seq: number[] = [];
1109
+ const start = this.cycleStart[c];
1110
+ let h = start;
1111
+ do {
1112
+ ringOf[h] = r;
1113
+ seq.push(h);
1114
+ h = this.next[h];
1115
+ } while (h !== start);
1116
+ rings.push(seq);
1117
+ }
1118
+ return rings;
1119
+ }
1120
+
1121
+ /**
1122
+ * Group the rings into polygons: each counterclockwise ring followed by the
1123
+ * clockwise rings (holes) that puncture its face. A hole's face is found by
1124
+ * a ray west from its leftmost vertex: the nearest ring edge crossed has
1125
+ * that face on its east side.
1126
+ */
1127
+ assemble(rings: number[][], ringOf: Int32Array, walk: number[]): Group[][] {
1128
+ const R = rings.length;
1129
+ const vx = this.vx;
1130
+ const vy = this.vy;
1131
+ const area = new Float64Array(R);
1132
+ const left = new Int32Array(R);
1133
+ for (let r = 0; r < R; r++) {
1134
+ const seq = rings[r];
1135
+ let a = 0;
1136
+ let l = this.origin(seq[0]);
1137
+ for (let i = 0; i < seq.length; i++) {
1138
+ const o = this.origin(seq[i]);
1139
+ const t = this.target(seq[i]);
1140
+ a += vx[o] * vy[t] - vx[t] * vy[o];
1141
+ if (vx[o] < vx[l] || (vx[o] === vx[l] && vy[o] < vy[l])) l = o;
1142
+ }
1143
+ area[r] = a / 2;
1144
+ left[r] = l;
1145
+ }
1146
+
1147
+ const sliver = this.tol * this.tol;
1148
+ const parent = new Int32Array(R).fill(-2); // -2 unresolved, -1 none
1149
+
1150
+ // A hole split off a pinched walk may have another piece of the walk at
1151
+ // its own leftmost vertex, where a westward ray starts inside that piece
1152
+ // or finds nothing. The pieces of one walk bound one region: a hole's
1153
+ // outer is the smallest counterclockwise piece of the walk that contains
1154
+ // it, and a hole inside none of them shares the walk's outermost piece's
1155
+ // surroundings, found by a ray from the walk's leftmost vertex.
1156
+ const rayFrom = left;
1157
+ const pieces = new Map<number, number[]>();
1158
+ if (walk.length > 0 && walk[walk.length - 1] !== R - 1) {
1159
+ // some walk split into several rings (walk ids repeat)
1160
+ for (let r = 0; r < R; r++) {
1161
+ const list = pieces.get(walk[r]);
1162
+ if (list) list.push(r);
1163
+ else pieces.set(walk[r], [r]);
1164
+ }
1165
+ }
1166
+ for (const list of pieces.values()) {
1167
+ if (list.length < 2) continue;
1168
+ let l = left[list[0]];
1169
+ for (const q of list) {
1170
+ const v = left[q];
1171
+ if (vx[v] < vx[l] || (vx[v] === vx[l] && vy[v] < vy[l])) l = v;
1172
+ }
1173
+ for (const r of list) {
1174
+ if (area[r] >= 0) continue;
1175
+ const o = this.origin(rings[r][0]);
1176
+ const t = this.target(rings[r][0]);
1177
+ const mx = (vx[o] + vx[t]) / 2;
1178
+ const my = (vy[o] + vy[t]) / 2;
1179
+ let best = -1;
1180
+ for (const q of list) {
1181
+ if (area[q] <= sliver || (best >= 0 && area[q] >= area[best]))
1182
+ continue;
1183
+ if (this.encloses(rings[q], mx, my)) best = q;
1184
+ }
1185
+ if (best >= 0) parent[r] = best;
1186
+ else rayFrom[r] = l;
1187
+ }
1188
+ }
1189
+
1190
+ let holes = 0;
1191
+ for (let r = 0; r < R; r++) if (area[r] < 0 && parent[r] === -2) holes++;
1192
+ this.expectRayQueries(holes);
1193
+ const resolve = (r: number): number => {
1194
+ const chain: number[] = [];
1195
+ let p = r;
1196
+ while (p >= 0 && area[p] < 0 && parent[p] === -2) {
1197
+ chain.push(p);
1198
+ parent[p] = -1; // stop if inconsistent geometry creates a cycle
1199
+ p = this.westHit(rayFrom[p], ringOf);
1200
+ }
1201
+ if (p >= 0 && area[p] < 0) p = parent[p];
1202
+ if (p >= 0 && !(area[p] > sliver)) p = -1;
1203
+ for (const hole of chain) parent[hole] = p;
1204
+ return p;
1205
+ };
1206
+
1207
+ // A ring whose mean width (twice the area over the perimeter) is below the
1208
+ // tolerance is a hairline: the trace of edges that coincide up to rounding,
1209
+ // such as a vertex that touches an edge to float32 precision. It is
1210
+ // invisible filled and a stray line stroked, so it is dropped.
1211
+ const hairline = (ring: Group, a: number) =>
1212
+ Math.abs(a) < (this.tol * outputPerimeter(ring)) / 2;
1213
+ const outers = new Int32Array(R).fill(-1);
1214
+ const polygons: Group[][] = [];
1215
+ for (let r = 0; r < R; r++) {
1216
+ if (area[r] > sliver) {
1217
+ const outer = this.ring(rings[r]);
1218
+ const a = outputArea(outer);
1219
+ if (a <= 0 || hairline(outer, a)) continue;
1220
+ outers[r] = polygons.length;
1221
+ polygons.push([outer]);
1222
+ }
1223
+ }
1224
+ for (let r = 0; r < R; r++) {
1225
+ if (area[r] >= -sliver) continue;
1226
+ const p = resolve(r);
1227
+ if (p >= 0 && outers[p] >= 0) {
1228
+ const hole = this.ring(rings[r]);
1229
+ const a = outputArea(hole);
1230
+ if (a < 0 && !hairline(hole, a)) polygons[outers[p]].push(hole);
1231
+ }
1232
+ }
1233
+ return polygons;
1234
+ }
1235
+
1236
+ /** Whether a simple ring encloses a point that is not on its boundary (crossing parity). */
1237
+ private encloses(seq: number[], x: number, y: number): boolean {
1238
+ const vx = this.vx;
1239
+ const vy = this.vy;
1240
+ let inside = false;
1241
+ for (let i = 0; i < seq.length; i++) {
1242
+ const o = this.origin(seq[i]);
1243
+ const t = this.target(seq[i]);
1244
+ const oy = vy[o];
1245
+ const ty = vy[t];
1246
+ if (oy > y === ty > y) continue;
1247
+ const xc = vx[o] + ((y - oy) * (vx[t] - vx[o])) / (ty - oy);
1248
+ if (xc < x) inside = !inside;
1249
+ }
1250
+ return inside;
1251
+ }
1252
+
1253
+ /** The ring, among those in `ringOf`, whose edge is the nearest crossing of the ray west from vertex p and faces it. */
1254
+ private westHit(p: number, ringOf: Int32Array): number {
1255
+ const px = this.vx[p];
1256
+ const py = this.vy[p];
1257
+ let best = -Infinity;
1258
+ let bestSlope = -Infinity;
1259
+ let ring = -1;
1260
+ const gu = this.gu;
1261
+ const gv = this.gv;
1262
+ const vx = this.vx;
1263
+ const vy = this.vy;
1264
+ const visit = (e: number) => {
1265
+ if (ringOf[2 * e] < 0 && ringOf[2 * e + 1] < 0) return;
1266
+ const u = gu[e];
1267
+ const v = gv[e];
1268
+ if (u === p || v === p) return; // meets the ray at p itself
1269
+ const uy = vy[u];
1270
+ const wy = vy[v];
1271
+ if (uy > py === wy > py) return;
1272
+ const xc = this.crossingX(u, v, py);
1273
+ if (xc >= px) return;
1274
+ // Crossings exactly at a vertex tie on x; the ray sits just above the
1275
+ // vertex, so the most eastward upward edge is the nearest.
1276
+ const ux = vx[u];
1277
+ const wx = vx[v];
1278
+ const up = wy > uy;
1279
+ const slope = up ? (wx - ux) / (wy - uy) : (ux - wx) / (uy - wy);
1280
+ if (xc > best || (xc === best && slope > bestSlope)) {
1281
+ best = xc;
1282
+ bestSlope = slope;
1283
+ // The face east of the crossing is on the left of the downward
1284
+ // half-edge. If that half-edge has no ring the geometry is inconsistent
1285
+ // at the tol scale, and the hole is dropped rather than misplaced.
1286
+ ring = ringOf[up ? 2 * e + 1 : 2 * e];
1287
+ }
1288
+ };
1289
+ const tree = this.rayTree();
1290
+ if (tree === undefined) {
1291
+ for (let e = 0, E = this.gu.length; e < E; e++) visit(e);
1292
+ return ring;
1293
+ }
1294
+ const stack = tree ? [tree] : [];
1295
+ while (stack.length > 0) {
1296
+ const node = stack.pop()!;
1297
+ if (node.x0 >= px || node.x1 < best || py < node.y0 || py >= node.y1)
1298
+ continue;
1299
+ if (!node.edges) {
1300
+ // Search the more eastward child first so its hit can prune the west.
1301
+ const a = node.left!,
1302
+ b = node.right!;
1303
+ if (a.x1 < b.x1) stack.push(a, b);
1304
+ else stack.push(b, a);
1305
+ continue;
1306
+ }
1307
+ for (const e of node.edges) visit(e);
1308
+ }
1309
+ return ring;
1310
+ }
1311
+
1312
+ /** Remove straight-through vertices without changing any bend in the boundary. */
1313
+ private ring(seq: number[]): Group {
1314
+ const vx = this.vx;
1315
+ const vy = this.vy;
1316
+ const n = seq.length;
1317
+ const ids: number[] = [];
1318
+ for (let i = 0; i < n; i++) ids.push(this.origin(seq[i]));
1319
+ const keep: number[] = [ids[0]];
1320
+ const straight = (prev: number, v: number, w: number) => {
1321
+ const rx = vx[w] - vx[prev];
1322
+ const ry = vy[w] - vy[prev];
1323
+ const t =
1324
+ ((vx[v] - vx[prev]) * rx + (vy[v] - vy[prev]) * ry) /
1325
+ (rx * rx + ry * ry);
1326
+ return (
1327
+ t > 0 &&
1328
+ t < 1 &&
1329
+ orient2d(vx[prev], vy[prev], vx[w], vy[w], vx[v], vy[v]) === 0
1330
+ );
1331
+ };
1332
+ // Keep an anchor through the linear pass. Only reconsider it after the
1333
+ // closing edge's actual neighbors are known.
1334
+ for (let i = 1; i < n; i++) {
1335
+ const v = ids[i];
1336
+ const w = ids[i === n - 1 ? 0 : i + 1];
1337
+ if (straight(keep[keep.length - 1], v, w)) continue;
1338
+ keep.push(v);
1339
+ }
1340
+ if (keep.length > 3 && straight(keep[keep.length - 1], keep[0], keep[1]))
1341
+ keep.shift();
1342
+ const g = new Group();
1343
+ for (let i = 0; i < keep.length; i++) {
1344
+ const pt = new Pt(2);
1345
+ pt[0] = vx[keep[i]];
1346
+ pt[1] = vy[keep[i]];
1347
+ const prev = g[g.length - 1];
1348
+ // Distinct double intersections can round to the same output Pt.
1349
+ if (!prev || prev[0] !== pt[0] || prev[1] !== pt[1]) g.push(pt);
1350
+ }
1351
+ if (
1352
+ g.length > 1 &&
1353
+ g[0][0] === g[g.length - 1][0] &&
1354
+ g[0][1] === g[g.length - 1][1]
1355
+ )
1356
+ g.pop();
1357
+ return g;
1358
+ }
1359
+ }
1360
+
1361
+ /**
1362
+ * Combine shapes with a Path mode. Returns the rings of the merged region
1363
+ * for the merging modes, or one ring list per face for `divide` and `crop`.
1364
+ */
1365
+ export function overlay(
1366
+ shapes: Iterable<PolygonLike> | PtLikeIterable,
1367
+ mode: "divide" | "crop",
1368
+ ): Group[][];
1369
+ export function overlay(
1370
+ shapes: Iterable<PolygonLike> | PtLikeIterable,
1371
+ mode: Exclude<PathMode, "divide" | "crop">,
1372
+ ): Group[];
1373
+ export function overlay(
1374
+ shapes: Iterable<PolygonLike> | PtLikeIterable,
1375
+ mode: PathMode,
1376
+ ): Group[] | Group[][];
1377
+ export function overlay(
1378
+ shapes: Iterable<PolygonLike> | PtLikeIterable,
1379
+ mode: PathMode,
1380
+ ): Group[] | Group[][] {
1381
+ const faces = mode === "divide" || mode === "crop";
1382
+ const ov = new Overlay();
1383
+ if (!ov.read(shapes)) return [];
1384
+ ov.split();
1385
+ ov.merge();
1386
+ if (ov.gu.length === 0) return [];
1387
+ ov.trace();
1388
+ ov.label();
1389
+ const keep = ov.select(mode);
1390
+ const ringOf = new Int32Array(2 * ov.gu.length).fill(-1);
1391
+ const walks = faces
1392
+ ? ov.faceRings(keep, ringOf)
1393
+ : ov.mergedRings(keep, ringOf);
1394
+ const { rings, walk } = ov.simpleRings(walks, ringOf);
1395
+ const polygons = ov.assemble(rings, ringOf, walk);
1396
+ if (faces) return polygons;
1397
+ const flat: Group[] = [];
1398
+ for (let i = 0; i < polygons.length; i++) {
1399
+ for (let j = 0; j < polygons[i].length; j++) flat.push(polygons[i][j]);
1400
+ }
1401
+ return flat;
1402
+ }