earcut 3.0.1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,25 +4,62 @@ typeof define === 'function' && define.amd ? define(['exports'], factory) :
4
4
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.earcut = {}));
5
5
  })(this, (function (exports) { 'use strict';
6
6
 
7
+ /**
8
+ * A vertex in a circular doubly linked list representing a polygon ring.
9
+ * `prev`/`next` are always linked (set immediately after {@link createNode}), so they're typed
10
+ * non-null; `prevZ`/`nextZ` are the z-order list links and are null at the ends.
11
+ *
12
+ * @typedef {object} Node
13
+ * @property {number} i vertex index in the coordinates array
14
+ * @property {number} x vertex x coordinate
15
+ * @property {number} y vertex y coordinate
16
+ * @property {Node} prev previous vertex node in the polygon ring
17
+ * @property {Node} next next vertex node in the polygon ring
18
+ * @property {number} z z-order curve value; doubles as the owning block index during eliminateHoles
19
+ * @property {Node | null} prevZ previous node in z-order
20
+ * @property {Node | null} nextZ next node in z-order
21
+ */
22
+
23
+ // single-vertex holes to preserve through filterPoints (steiner points); kept off the Node
24
+ // shape since they're rare — the empty-set fast path means non-steiner inputs pay nothing
25
+ /** @type {Set<Node>} */
26
+ const steiners = new Set();
27
+
28
+ /**
29
+ * Triangulate a polygon given as a flat array of vertex coordinates.
30
+ *
31
+ * @param {ArrayLike<number>} data flat array of vertex coordinates
32
+ * @param {ArrayLike<number> | null} [holeIndices] indices (in vertices, not coordinates) where each hole ring starts
33
+ * @param {number} [dim=2] number of coordinates per vertex in `data`
34
+ * @returns {number[]} triangles as triplets of vertex indices into `data`
35
+ * @example earcut([10,0, 0,50, 60,60, 70,10]); // [1,0,3, 3,2,1]
36
+ */
7
37
  function earcut(data, holeIndices, dim = 2) {
8
38
 
9
39
  const hasHoles = holeIndices && holeIndices.length;
10
40
  const outerLen = hasHoles ? holeIndices[0] * dim : data.length;
41
+ if (steiners.size) steiners.clear();
42
+
11
43
  let outerNode = linkedList(data, 0, outerLen, dim, true);
44
+ /** @type {number[]} */
12
45
  const triangles = [];
13
46
 
14
47
  if (!outerNode || outerNode.next === outerNode.prev) return triangles;
15
48
 
16
- let minX, minY, invSize;
49
+ let minX = 0, minY = 0, invSize = 0;
17
50
 
18
- if (hasHoles) outerNode = eliminateHoles(data, holeIndices, outerNode, dim);
51
+ if (hasHoles) {
52
+ outerNode = eliminateHoles(data, holeIndices, outerNode, dim);
53
+ // collapse collinear/coincident points across the whole merged ring once before clipping
54
+ outerNode = filterPoints(outerNode);
55
+ }
19
56
 
20
57
  // if the shape is not too simple, we'll use z-order curve hash later; calculate polygon bbox
21
58
  if (data.length > 80 * dim) {
22
- minX = Infinity;
23
- minY = Infinity;
24
- let maxX = -Infinity;
25
- let maxY = -Infinity;
59
+ minX = data[0];
60
+ minY = data[1];
61
+ let maxX = minX;
62
+ let maxY = minY;
26
63
 
27
64
  for (let i = dim; i < outerLen; i += dim) {
28
65
  const x = data[i];
@@ -44,8 +81,10 @@ function earcut(data, holeIndices, dim = 2) {
44
81
  }
45
82
 
46
83
  // create a circular doubly linked list from polygon points in the specified winding order
84
+ /** @param {ArrayLike<number>} data @param {number} start @param {number} end @param {number} dim @param {boolean} clockwise @returns {Node | null} */
47
85
  function linkedList(data, start, end, dim, clockwise) {
48
- let last;
86
+ /** @type {Node | null} */
87
+ let last = null;
49
88
 
50
89
  if (clockwise === (signedArea(data, start, end, dim) > 0)) {
51
90
  for (let i = start; i < end; i += dim) last = insertNode(i / dim | 0, data[i], data[i + 1], last);
@@ -61,24 +100,27 @@ function linkedList(data, start, end, dim, clockwise) {
61
100
  return last;
62
101
  }
63
102
 
64
- // eliminate colinear or duplicate points
65
- function filterPoints(start, end) {
66
- if (!start) return start;
67
- if (!end) end = start;
103
+ // Remove collinear or coincident points; removability depends only on a node's immediate
104
+ // neighbors, so we sweep forward and re-check the predecessor after each removal. With no `end`
105
+ // we sweep the whole ring, lapping until nothing is removable (the fixpoint the clipper needs).
106
+ // With an explicit `end` we heal only the dirty window around a bridge/diagonal cut, stopping at
107
+ // `end` rather than lapping — O(window) instead of O(ring).
108
+ /** @param {Node} start @param {Node} [end] @returns {Node} */
109
+ function filterPoints(start, end = start) {
110
+ const full = end === start;
68
111
 
69
- let p = start,
70
- again;
112
+ let p = start, again;
71
113
  do {
72
114
  again = false;
73
-
74
- if (!p.steiner && (equals(p, p.next) || area(p.prev, p, p.next) === 0)) {
115
+ if (p !== p.next && (steiners.size === 0 || !steiners.has(p)) &&
116
+ (equals(p, p.next) || area(p.prev, p, p.next) === 0)) {
117
+ if (full || p === end) end = p.prev; // pull the stop bound back past the removal
75
118
  removeNode(p);
76
- p = end = p.prev;
77
- if (p === p.next) break;
119
+ p = p.prev; // re-check the predecessor
78
120
  again = true;
79
-
80
- } else {
121
+ } else if (full || p !== end) {
81
122
  p = p.next;
123
+ again = !full; // local heal: keep looping until the sweep reaches end
82
124
  }
83
125
  } while (again || p !== end);
84
126
 
@@ -86,6 +128,7 @@ function filterPoints(start, end) {
86
128
  }
87
129
 
88
130
  // main ear slicing loop which triangulates a polygon (given as a linked list)
131
+ /** @param {Node | null} ear @param {number[]} triangles @param {number} dim @param {number} minX @param {number} minY @param {number} invSize @param {number} pass */
89
132
  function earcutLinked(ear, triangles, dim, minX, minY, invSize, pass) {
90
133
  if (!ear) return;
91
134
 
@@ -97,16 +140,16 @@ function earcutLinked(ear, triangles, dim, minX, minY, invSize, pass) {
97
140
  // iterate through ears, slicing them one by one
98
141
  while (ear.prev !== ear.next) {
99
142
  const prev = ear.prev;
143
+ /** @type {Node} */
100
144
  const next = ear.next;
101
145
 
102
- if (invSize ? isEarHashed(ear, minX, minY, invSize) : isEar(ear)) {
146
+ if (area(prev, ear, next) < 0 && (invSize ? isEarHashed(ear, minX, minY, invSize) : isEar(ear))) {
103
147
  triangles.push(prev.i, ear.i, next.i); // cut off the triangle
104
148
 
105
149
  removeNode(ear);
106
150
 
107
- // skipping the next vertex leads to less sliver triangles
108
- ear = next.next;
109
- stop = next.next;
151
+ ear = next;
152
+ stop = next;
110
153
 
111
154
  continue;
112
155
  }
@@ -135,14 +178,17 @@ function earcutLinked(ear, triangles, dim, minX, minY, invSize, pass) {
135
178
  }
136
179
 
137
180
  // check whether a polygon node forms a valid ear with adjacent nodes
181
+ /** @param {Node} ear @returns {boolean} */
138
182
  function isEar(ear) {
139
183
  const a = ear.prev,
140
184
  b = ear,
141
185
  c = ear.next;
142
186
 
143
- if (area(a, b, c) >= 0) return false; // reflex, can't be an ear
187
+ // reflex check (area(a, b, c) >= 0) is hoisted into the earcutLinked caller to avoid non-inlined call here
144
188
 
145
- // now make sure we don't have other points inside the potential ear
189
+ // make sure we don't have other points inside the potential ear; the point-in-triangle
190
+ // test (false when the point coincides with the first vertex a) is inlined here and in
191
+ // isEarHashed rather than called — V8 doesn't inline it and the call sits in the hot loop
146
192
  const ax = a.x, bx = b.x, cx = c.x, ay = a.y, by = b.y, cy = c.y;
147
193
 
148
194
  // triangle bbox
@@ -154,7 +200,7 @@ function isEar(ear) {
154
200
  let p = c.next;
155
201
  while (p !== a) {
156
202
  if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 &&
157
- pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, p.x, p.y) &&
203
+ !(ax === p.x && ay === p.y) && (cx - p.x) * (ay - p.y) >= (ax - p.x) * (cy - p.y) && (ax - p.x) * (by - p.y) >= (bx - p.x) * (ay - p.y) && (bx - p.x) * (cy - p.y) >= (cx - p.x) * (by - p.y) &&
158
204
  area(p.prev, p, p.next) >= 0) return false;
159
205
  p = p.next;
160
206
  }
@@ -162,12 +208,13 @@ function isEar(ear) {
162
208
  return true;
163
209
  }
164
210
 
211
+ /** @param {Node} ear @param {number} minX @param {number} minY @param {number} invSize @returns {boolean} */
165
212
  function isEarHashed(ear, minX, minY, invSize) {
166
213
  const a = ear.prev,
167
214
  b = ear,
168
215
  c = ear.next;
169
216
 
170
- if (area(a, b, c) >= 0) return false; // reflex, can't be an ear
217
+ // reflex check is hoisted into the earcutLinked caller (see isEar)
171
218
 
172
219
  const ax = a.x, bx = b.x, cx = c.x, ay = a.y, by = b.y, cy = c.y;
173
220
 
@@ -186,26 +233,26 @@ function isEarHashed(ear, minX, minY, invSize) {
186
233
 
187
234
  // look for points inside the triangle in both directions
188
235
  while (p && p.z >= minZ && n && n.z <= maxZ) {
189
- if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 && p !== a && p !== c &&
190
- pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, p.x, p.y) && area(p.prev, p, p.next) >= 0) return false;
236
+ if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 && p !== c &&
237
+ !(ax === p.x && ay === p.y) && (cx - p.x) * (ay - p.y) >= (ax - p.x) * (cy - p.y) && (ax - p.x) * (by - p.y) >= (bx - p.x) * (ay - p.y) && (bx - p.x) * (cy - p.y) >= (cx - p.x) * (by - p.y) && area(p.prev, p, p.next) >= 0) return false;
191
238
  p = p.prevZ;
192
239
 
193
- if (n.x >= x0 && n.x <= x1 && n.y >= y0 && n.y <= y1 && n !== a && n !== c &&
194
- pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, n.x, n.y) && area(n.prev, n, n.next) >= 0) return false;
240
+ if (n.x >= x0 && n.x <= x1 && n.y >= y0 && n.y <= y1 && n !== c &&
241
+ !(ax === n.x && ay === n.y) && (cx - n.x) * (ay - n.y) >= (ax - n.x) * (cy - n.y) && (ax - n.x) * (by - n.y) >= (bx - n.x) * (ay - n.y) && (bx - n.x) * (cy - n.y) >= (cx - n.x) * (by - n.y) && area(n.prev, n, n.next) >= 0) return false;
195
242
  n = n.nextZ;
196
243
  }
197
244
 
198
245
  // look for remaining points in decreasing z-order
199
246
  while (p && p.z >= minZ) {
200
- if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 && p !== a && p !== c &&
201
- pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, p.x, p.y) && area(p.prev, p, p.next) >= 0) return false;
247
+ if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 && p !== c &&
248
+ !(ax === p.x && ay === p.y) && (cx - p.x) * (ay - p.y) >= (ax - p.x) * (cy - p.y) && (ax - p.x) * (by - p.y) >= (bx - p.x) * (ay - p.y) && (bx - p.x) * (cy - p.y) >= (cx - p.x) * (by - p.y) && area(p.prev, p, p.next) >= 0) return false;
202
249
  p = p.prevZ;
203
250
  }
204
251
 
205
252
  // look for remaining points in increasing z-order
206
253
  while (n && n.z <= maxZ) {
207
- if (n.x >= x0 && n.x <= x1 && n.y >= y0 && n.y <= y1 && n !== a && n !== c &&
208
- pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, n.x, n.y) && area(n.prev, n, n.next) >= 0) return false;
254
+ if (n.x >= x0 && n.x <= x1 && n.y >= y0 && n.y <= y1 && n !== c &&
255
+ !(ax === n.x && ay === n.y) && (cx - n.x) * (ay - n.y) >= (ax - n.x) * (cy - n.y) && (ax - n.x) * (by - n.y) >= (bx - n.x) * (ay - n.y) && (bx - n.x) * (cy - n.y) >= (cx - n.x) * (by - n.y) && area(n.prev, n, n.next) >= 0) return false;
209
256
  n = n.nextZ;
210
257
  }
211
258
 
@@ -213,13 +260,15 @@ function isEarHashed(ear, minX, minY, invSize) {
213
260
  }
214
261
 
215
262
  // go through all polygon nodes and cure small local self-intersections
263
+ /** @param {Node} start @param {number[]} triangles @returns {Node} */
216
264
  function cureLocalIntersections(start, triangles) {
217
265
  let p = start;
266
+ let cured = false;
218
267
  do {
219
268
  const a = p.prev,
220
269
  b = p.next.next;
221
270
 
222
- if (!equals(a, b) && intersects(a, p, p.next, b) && locallyInside(a, b) && locallyInside(b, a)) {
271
+ if (intersects(a, p, p.next, b, false) && locallyInside(a, b) && locallyInside(b, a)) {
223
272
 
224
273
  triangles.push(a.i, p.i, b.i);
225
274
 
@@ -228,14 +277,16 @@ function cureLocalIntersections(start, triangles) {
228
277
  removeNode(p.next);
229
278
 
230
279
  p = start = b;
280
+ cured = true;
231
281
  }
232
282
  p = p.next;
233
283
  } while (p !== start);
234
284
 
235
- return filterPoints(p);
285
+ return cured ? filterPoints(p) : p;
236
286
  }
237
287
 
238
288
  // try splitting polygon into two and triangulate them independently
289
+ /** @param {Node} start @param {number[]} triangles @param {number} dim @param {number} minX @param {number} minY @param {number} invSize */
239
290
  function splitEarcut(start, triangles, dim, minX, minY, invSize) {
240
291
  // look for a valid diagonal that divides the polygon into two
241
292
  let a = start;
@@ -261,44 +312,51 @@ function splitEarcut(start, triangles, dim, minX, minY, invSize) {
261
312
  } while (a !== start);
262
313
  }
263
314
 
315
+ // true only while eliminateHoles merges holes, so removeNode keeps the block index live (growBlock)
316
+ let indexActive = false;
317
+
264
318
  // link every hole into the outer loop, producing a single-ring polygon without holes
319
+ /** @param {ArrayLike<number>} data @param {ArrayLike<number>} holeIndices @param {Node} outerNode @param {number} dim @returns {Node} */
265
320
  function eliminateHoles(data, holeIndices, outerNode, dim) {
266
321
  const queue = [];
267
322
 
268
323
  for (let i = 0, len = holeIndices.length; i < len; i++) {
269
324
  const start = holeIndices[i] * dim;
270
325
  const end = i < len - 1 ? holeIndices[i + 1] * dim : data.length;
271
- const list = linkedList(data, start, end, dim, false);
272
- if (list === list.next) list.steiner = true;
326
+ const list = /** @type {Node} */ (linkedList(data, start, end, dim, false));
327
+ if (list === list.next) steiners.add(list);
273
328
  queue.push(getLeftmost(list));
274
329
  }
275
330
 
276
331
  queue.sort(compareXYSlope);
277
332
 
278
- // process holes from left to right
333
+ // block-bbox index for findHoleBridge, grown append-only as holes merge (see notes
334
+ // above buildBlockIndex). Seed it with the outer ring, then append each merged hole.
335
+ buildBlockIndex(data.length / dim, holeIndices.length);
336
+ indexSegment(outerNode, outerNode);
337
+
338
+ // process holes from left to right; indexActive lets removeNode keep block bboxes live as
339
+ // filterPoints heals edges during merges (see growBlock)
340
+ indexActive = true;
279
341
  for (let i = 0; i < queue.length; i++) {
280
342
  outerNode = eliminateHole(queue[i], outerNode);
281
343
  }
344
+ indexActive = false;
282
345
 
283
346
  return outerNode;
284
347
  }
285
348
 
349
+ /** @param {Node} a @param {Node} b @returns {number} */
286
350
  function compareXYSlope(a, b) {
287
- let result = a.x - b.x;
288
351
  // when the left-most point of 2 holes meet at a vertex, sort the holes counterclockwise so that when we find
289
352
  // the bridge to the outer shell is always the point that they meet at.
290
- if (result === 0) {
291
- result = a.y - b.y;
292
- if (result === 0) {
293
- const aSlope = (a.next.y - a.y) / (a.next.x - a.x);
294
- const bSlope = (b.next.y - b.y) / (b.next.x - b.x);
295
- result = aSlope - bSlope;
296
- }
297
- }
298
- return result;
353
+ return a.x - b.x || a.y - b.y ||
354
+ (a.next.y - a.y) / (a.next.x - a.x) -
355
+ (b.next.y - b.y) / (b.next.x - b.x);
299
356
  }
300
357
 
301
- // find a bridge between vertices that connects hole with an outer ring and and link it
358
+ // find a bridge between vertices that connects hole with an outer ring and link it
359
+ /** @param {Node} hole @param {Node} outerNode @returns {Node} */
302
360
  function eliminateHole(hole, outerNode) {
303
361
  const bridge = findHoleBridge(hole, outerNode);
304
362
  if (!bridge) {
@@ -307,35 +365,143 @@ function eliminateHole(hole, outerNode) {
307
365
 
308
366
  const bridgeReverse = splitPolygon(bridge, hole);
309
367
 
310
- // filter collinear points around the cuts
368
+ // index the merged-in segment before filtering: in ring order the splice runs
369
+ // bridge -> hole -> bridgeReverse -> bridge2 -> (bridge's old next), covering the
370
+ // hole's edges and both new slit edges. filterPoints below only drops collinear /
371
+ // coincident points, so these bboxes stay valid (conservative) supersets.
372
+ const bridge2 = bridgeReverse.next;
373
+ indexSegment(bridge, bridge2.next);
374
+
375
+ // heal collinear/coincident points around the two new slit edges
311
376
  filterPoints(bridgeReverse, bridgeReverse.next);
312
377
  return filterPoints(bridge, bridge.next);
313
378
  }
314
379
 
380
+ // Block-bbox index for findHoleBridge (issue #183): one [minX,minY,maxX,maxY] bbox per K
381
+ // consecutive ring edges, in a flat Float64Array, so the leftward-ray scan can skip whole
382
+ // blocks in O(1) instead of walking the entire merged ring. Grown append-only — the outer
383
+ // ring seeds it, then each merged hole appends a segment (head node, stop node, K-blocks
384
+ // over head..stop); independent segments, not a ring tiling, since splices land mid-ring.
385
+ // Buffers are sized once from the input upper bound and reused across calls.
386
+ //
387
+ // filterPoints only drops collinear/coincident points, so a stale bbox stays a conservative
388
+ // superset of its live edges (never a false skip); the scan skips dead nodes (p.prev.next !==
389
+ // p) and lazily advances a dead stop. Blocks are scanned in append (not ring) order, so the
390
+ // chosen bridge can differ from the un-indexed code — a different but equally valid result.
391
+ const K = 16; // edges per block
392
+
393
+ let blockBBox = new Float64Array(0); // [minX,minY,maxX,maxY] per block
394
+ let numBlocks = 0;
395
+ /** @type {Node[]} */
396
+ const blockHead = []; // first node of each block's segment
397
+ /** @type {Node[]} */
398
+ const blockStop = []; // node just past each block's segment (exclusive walk bound)
399
+
400
+ /** @param {number} maxNodes @param {number} numHoles */
401
+ function buildBlockIndex(maxNodes, numHoles) {
402
+ // upper bound: every input node indexed once, +2 bridge nodes per hole, plus a partial
403
+ // trailing block per appended segment (outer ring + one per hole)
404
+ const maxBlocks = Math.ceil((maxNodes + 2 * numHoles) / K) + numHoles + 2;
405
+ if (blockBBox.length < maxBlocks * 4) blockBBox = new Float64Array(maxBlocks * 4);
406
+ numBlocks = 0;
407
+ }
408
+
409
+ // index the ring run head..stop (exclusive) as ceil(len / K) blocks; head === stop means
410
+ // the whole ring. each block's bbox covers both endpoints of every edge it owns.
411
+ /** @param {Node} head @param {Node} stop */
412
+ function indexSegment(head, stop) {
413
+ let p = head;
414
+ do {
415
+ const b = numBlocks++;
416
+ blockHead[b] = p;
417
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
418
+ let k = 0;
419
+ do {
420
+ const c = p.next; // edge p->c; bbox must bound both endpoints
421
+ p.z = b; // reuse z as the owning block during eliminateHoles (see growBlock)
422
+ if (p.x < minX) minX = p.x; if (p.x > maxX) maxX = p.x;
423
+ if (p.y < minY) minY = p.y; if (p.y > maxY) maxY = p.y;
424
+ if (c.x < minX) minX = c.x; if (c.x > maxX) maxX = c.x;
425
+ if (c.y < minY) minY = c.y; if (c.y > maxY) maxY = c.y;
426
+ p = c;
427
+ } while (++k < K && p !== stop);
428
+ blockStop[b] = p;
429
+ const g = b * 4;
430
+ blockBBox[g] = minX; blockBBox[g + 1] = minY; blockBBox[g + 2] = maxX; blockBBox[g + 3] = maxY;
431
+ } while (p !== stop);
432
+ }
433
+
434
+ // when filterPoints heals an edge head->tail (removing the collinear node between them), the
435
+ // healed edge can extend past head's frozen block bbox if its old far endpoint lived in another
436
+ // block; grow head's block bbox to cover tail so the leftward-ray prune can't false-skip it.
437
+ /** @param {Node} head @param {Node} tail */
438
+ function growBlock(head, tail) {
439
+ const g = head.z * 4;
440
+ if (tail.x < blockBBox[g]) blockBBox[g] = tail.x;
441
+ if (tail.y < blockBBox[g + 1]) blockBBox[g + 1] = tail.y;
442
+ if (tail.x > blockBBox[g + 2]) blockBBox[g + 2] = tail.x;
443
+ if (tail.y > blockBBox[g + 3]) blockBBox[g + 3] = tail.y;
444
+ }
445
+
446
+ /** @param {number} b @returns {Node} */
447
+ function liveBlockStop(b) {
448
+ let stop = blockStop[b];
449
+ while (stop.prev.next !== stop) stop = stop.next;
450
+ blockStop[b] = stop;
451
+ return stop;
452
+ }
453
+
454
+ // the block's head node can be removed by filterPoints during merges; advance it to the next
455
+ // live node so the walk doesn't start on (and immediately terminate at) a dead node. For the
456
+ // single full-ring seed block (head === stop) the same forward advance keeps them equal, so the
457
+ // do-while still laps the whole ring instead of collapsing to an empty walk.
458
+ /** @param {number} b @returns {Node} */
459
+ function liveBlockHead(b) {
460
+ let head = blockHead[b];
461
+ while (head.prev.next !== head) head = head.next;
462
+ blockHead[b] = head;
463
+ return head;
464
+ }
465
+
315
466
  // David Eberly's algorithm for finding a bridge between hole and outer polygon
467
+ /** @param {Node} hole @param {Node} outerNode @returns {Node | null} */
316
468
  function findHoleBridge(hole, outerNode) {
317
469
  let p = outerNode;
318
470
  const hx = hole.x;
319
471
  const hy = hole.y;
320
472
  let qx = -Infinity;
473
+ /** @type {Node | undefined} */
321
474
  let m;
322
475
 
323
476
  // find a segment intersected by a ray from the hole's leftmost point to the left;
324
477
  // segment's endpoint with lesser x will be potential connection point
325
478
  // unless they intersect at a vertex, then choose the vertex
326
479
  if (equals(hole, p)) return p;
327
- do {
328
- if (equals(hole, p.next)) return p.next;
329
- else if (hy <= p.y && hy >= p.next.y && p.next.y !== p.y) {
330
- const x = p.x + (hy - p.y) * (p.next.x - p.x) / (p.next.y - p.y);
331
- if (x <= hx && x > qx) {
332
- qx = x;
333
- m = p.x < p.next.x ? p : p.next;
334
- if (x === hx) return m; // hole touches outer segment; pick leftmost endpoint
480
+
481
+ // scan blocks; skip any whose bbox can't hold a crossing that beats qx and lies left
482
+ // of hx (the prune Morton order can't express explicit per-axis [minY,maxY]/[minX,maxX])
483
+ for (let b = 0, g = 0; b < numBlocks; b++, g += 4) {
484
+ if (hy < blockBBox[g + 1] || hy > blockBBox[g + 3] || blockBBox[g] > hx || blockBBox[g + 2] <= qx) continue;
485
+
486
+ // ensure the walk's exclusive bound is live so we don't overrun into other blocks
487
+ const stop = liveBlockStop(b);
488
+
489
+ p = liveBlockHead(b);
490
+ do {
491
+ if (p.prev.next === p) { // skip nodes removed by filterPoints (stale in the index)
492
+ if (equals(hole, p.next)) return p.next;
493
+ else if (hy <= p.y && hy >= p.next.y && p.next.y !== p.y) {
494
+ const x = p.x + (hy - p.y) * (p.next.x - p.x) / (p.next.y - p.y);
495
+ if (x <= hx && x > qx) {
496
+ qx = x;
497
+ m = p.x < p.next.x ? p : p.next;
498
+ if (x === hx) return m; // hole touches outer segment; pick leftmost endpoint
499
+ }
500
+ }
335
501
  }
336
- }
337
- p = p.next;
338
- } while (p !== outerNode);
502
+ p = p.next;
503
+ } while (p !== stop);
504
+ }
339
505
 
340
506
  if (!m) return null;
341
507
 
@@ -343,108 +509,110 @@ function findHoleBridge(hole, outerNode) {
343
509
  // if there are no points found, we have a valid connection;
344
510
  // otherwise choose the point of the minimum angle with the ray as connection point
345
511
 
346
- const stop = m;
347
512
  const mx = m.x;
348
513
  const my = m.y;
514
+ const tminY = Math.min(hy, my); // the triangle's y span; x span is [mx, hx]
515
+ const tmaxY = Math.max(hy, my);
349
516
  let tanMin = Infinity;
350
517
 
351
- p = m;
518
+ // scan the same blocks; skip any whose bbox can't overlap the triangle's [mx,hx]×[tminY,tmaxY] box
519
+ for (let b = 0, g = 0; b < numBlocks; b++, g += 4) {
520
+ if (blockBBox[g + 2] < mx || blockBBox[g] > hx || blockBBox[g + 3] < tminY || blockBBox[g + 1] > tmaxY) continue;
352
521
 
353
- do {
354
- if (hx >= p.x && p.x >= mx && hx !== p.x &&
355
- pointInTriangle(hy < my ? hx : qx, hy, mx, my, hy < my ? qx : hx, hy, p.x, p.y)) {
522
+ const stop = liveBlockStop(b);
523
+
524
+ p = liveBlockHead(b);
525
+ do {
526
+ if (p.prev.next === p && hx >= p.x && p.x >= mx && hx !== p.x && // skip dead nodes
527
+ pointInTriangle(hy < my ? hx : qx, hy, mx, my, hy < my ? qx : hx, hy, p.x, p.y)) {
356
528
 
357
- const tan = Math.abs(hy - p.y) / (hx - p.x); // tangential
529
+ const tan = Math.abs(hy - p.y) / (hx - p.x); // tangential
358
530
 
359
- if (locallyInside(p, hole) &&
360
- (tan < tanMin || (tan === tanMin && (p.x > m.x || (p.x === m.x && sectorContainsSector(m, p)))))) {
361
- m = p;
362
- tanMin = tan;
531
+ // if hole point sits on p's horizontal edge (T-junction touch): the bridge runs
532
+ // along that edge locallyInside rejects it as collinear, but it's valid
533
+ if ((locallyInside(p, hole) || (p.y === hy && p.next.y === hy && p.next.x > hx)) &&
534
+ (tan < tanMin || (tan === tanMin && (p.x > m.x || (p.x === m.x && sectorContainsSector(m, p)))))) {
535
+ m = p;
536
+ tanMin = tan;
537
+ }
363
538
  }
364
- }
365
539
 
366
- p = p.next;
367
- } while (p !== stop);
540
+ p = p.next;
541
+ } while (p !== stop);
542
+ }
368
543
 
369
544
  return m;
370
545
  }
371
546
 
372
547
  // whether sector in vertex m contains sector in vertex p in the same coordinates
548
+ /** @param {Node} m @param {Node} p @returns {boolean} */
373
549
  function sectorContainsSector(m, p) {
374
550
  return area(m.prev, m, p.prev) < 0 && area(p.next, m, m.next) < 0;
375
551
  }
376
552
 
377
- // interlink polygon nodes in z-order
553
+ // scratch array of node refs, reused across calls and grown on demand
554
+ /** @type {Node[]} */
555
+ const sortArr = [];
556
+
557
+ // interlink polygon nodes in z-order: collect into an array, quicksort by z, relink
558
+ /** @param {Node} start @param {number} minX @param {number} minY @param {number} invSize */
378
559
  function indexCurve(start, minX, minY, invSize) {
379
560
  let p = start;
561
+ let n = 0;
380
562
  do {
381
- if (p.z === 0) p.z = zOrder(p.x, p.y, minX, minY, invSize);
382
- p.prevZ = p.prev;
383
- p.nextZ = p.next;
563
+ // always (re)compute: z may still hold a block index left over from eliminateHoles
564
+ p.z = zOrder(p.x, p.y, minX, minY, invSize);
565
+ sortArr[n++] = p;
384
566
  p = p.next;
385
567
  } while (p !== start);
386
568
 
387
- p.prevZ.nextZ = null;
388
- p.prevZ = null;
389
-
390
- sortLinked(p);
391
- }
569
+ quicksortNodes(sortArr, 0, n - 1);
392
570
 
393
- // Simon Tatham's linked list merge sort algorithm
394
- // http://www.chiark.greenend.org.uk/~sgtatham/algorithms/listsort.html
395
- function sortLinked(list) {
396
- let numMerges;
397
- let inSize = 1;
398
-
399
- do {
400
- let p = list;
401
- let e;
402
- list = null;
403
- let tail = null;
404
- numMerges = 0;
405
-
406
- while (p) {
407
- numMerges++;
408
- let q = p;
409
- let pSize = 0;
410
- for (let i = 0; i < inSize; i++) {
411
- pSize++;
412
- q = q.nextZ;
413
- if (!q) break;
414
- }
415
- let qSize = inSize;
416
-
417
- while (pSize > 0 || (qSize > 0 && q)) {
418
-
419
- if (pSize !== 0 && (qSize === 0 || !q || p.z <= q.z)) {
420
- e = p;
421
- p = p.nextZ;
422
- pSize--;
423
- } else {
424
- e = q;
425
- q = q.nextZ;
426
- qSize--;
427
- }
428
-
429
- if (tail) tail.nextZ = e;
430
- else list = e;
431
-
432
- e.prevZ = tail;
433
- tail = e;
434
- }
435
-
436
- p = q;
571
+ /** @type {Node | null} */
572
+ let prev = null;
573
+ for (let i = 0; i < n; i++) {
574
+ const node = sortArr[i];
575
+ node.prevZ = prev;
576
+ if (prev) prev.nextZ = node;
577
+ prev = node;
578
+ }
579
+ /** @type {Node} */ (prev).nextZ = null;
580
+ }
581
+
582
+ // quicksort an array of nodes by z; middle-element pivot + insertion sort for small ranges
583
+ /** @param {Node[]} arr @param {number} left @param {number} right */
584
+ function quicksortNodes(arr, left, right) {
585
+ while (right - left > 20) {
586
+ // middle pivot splits already-sorted/reversed runs evenly; real ring-order-by-z data
587
+ // is non-adversarial, so the median-of-three guard isn't needed
588
+ const pivot = arr[(left + right) >> 1].z;
589
+
590
+ let i = left, j = right, t;
591
+ while (i <= j) {
592
+ while (arr[i].z < pivot) i++;
593
+ while (arr[j].z > pivot) j--;
594
+ if (i <= j) { t = arr[i]; arr[i] = arr[j]; arr[j] = t; i++; j--; }
437
595
  }
438
-
439
- tail.nextZ = null;
440
- inSize *= 2;
441
-
442
- } while (numMerges > 1);
443
-
444
- return list;
596
+ // recurse into the smaller half, loop on the larger to bound stack depth
597
+ if (j - left < right - i) {
598
+ quicksortNodes(arr, left, j);
599
+ left = i;
600
+ } else {
601
+ quicksortNodes(arr, i, right);
602
+ right = j;
603
+ }
604
+ }
605
+ // insertion sort the small remaining range
606
+ for (let i = left + 1; i <= right; i++) {
607
+ const node = arr[i], z = node.z;
608
+ let j = i - 1;
609
+ while (j >= left && arr[j].z > z) { arr[j + 1] = arr[j]; j--; }
610
+ arr[j + 1] = node;
611
+ }
445
612
  }
446
613
 
447
614
  // z-order of a point given coords and inverse of the longer side of data bbox
615
+ /** @param {number} x @param {number} y @param {number} minX @param {number} minY @param {number} invSize @returns {number} */
448
616
  function zOrder(x, y, minX, minY, invSize) {
449
617
  // coords are transformed into non-negative 15-bit integer range
450
618
  x = (x - minX) * invSize | 0;
@@ -464,6 +632,7 @@ function zOrder(x, y, minX, minY, invSize) {
464
632
  }
465
633
 
466
634
  // find the leftmost node of a polygon ring
635
+ /** @param {Node} start @returns {Node} */
467
636
  function getLeftmost(start) {
468
637
  let p = start,
469
638
  leftmost = start;
@@ -476,43 +645,45 @@ function getLeftmost(start) {
476
645
  }
477
646
 
478
647
  // check if a point lies within a convex triangle
648
+ /** @param {number} ax @param {number} ay @param {number} bx @param {number} by @param {number} cx @param {number} cy @param {number} px @param {number} py @returns {boolean} */
479
649
  function pointInTriangle(ax, ay, bx, by, cx, cy, px, py) {
480
650
  return (cx - px) * (ay - py) >= (ax - px) * (cy - py) &&
481
651
  (ax - px) * (by - py) >= (bx - px) * (ay - py) &&
482
652
  (bx - px) * (cy - py) >= (cx - px) * (by - py);
483
653
  }
484
654
 
485
- // check if a point lies within a convex triangle but false if its equal to the first point of the triangle
486
- function pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, px, py) {
487
- return !(ax === px && ay === py) && pointInTriangle(ax, ay, bx, by, cx, cy, px, py);
488
- }
489
-
490
655
  // check if a diagonal between two polygon nodes is valid (lies in polygon interior)
656
+ /** @param {Node} a @param {Node} b @returns {boolean} true when the diagonal is valid */
491
657
  function isValidDiagonal(a, b) {
492
- return a.next.i !== b.i && a.prev.i !== b.i && !intersectsPolygon(a, b) && // dones't intersect other edges
658
+ return a.next.i !== b.i && !intersectsPolygon(a, b) && // doesn't intersect other edges
493
659
  (locallyInside(a, b) && locallyInside(b, a) && middleInside(a, b) && // locally visible
494
- (area(a.prev, a, b.prev) || area(a, b.prev, b)) || // does not create opposite-facing sectors
660
+ (area(a.prev, a, b.prev) !== 0 || area(a, b.prev, b) !== 0) || // does not create opposite-facing sectors
495
661
  equals(a, b) && area(a.prev, a, a.next) > 0 && area(b.prev, b, b.next) > 0); // special zero-length case
496
662
  }
497
663
 
498
664
  // signed area of a triangle
665
+ /** @param {Node} p @param {Node} q @param {Node} r @returns {number} */
499
666
  function area(p, q, r) {
500
667
  return (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y);
501
668
  }
502
669
 
503
670
  // check if two points are equal
671
+ /** @param {Node} p1 @param {Node} p2 @returns {boolean} */
504
672
  function equals(p1, p2) {
505
673
  return p1.x === p2.x && p1.y === p2.y;
506
674
  }
507
675
 
508
- // check if two segments intersect
509
- function intersects(p1, q1, p2, q2) {
510
- const o1 = sign(area(p1, q1, p2));
511
- const o2 = sign(area(p1, q1, q2));
512
- const o3 = sign(area(p2, q2, p1));
513
- const o4 = sign(area(p2, q2, q1));
676
+ // check if two segments intersect; by default includes collinear boundary touches
677
+ /** @param {Node} p1 @param {Node} q1 @param {Node} p2 @param {Node} q2 @param {boolean} [includeBoundary] @returns {boolean} */
678
+ function intersects(p1, q1, p2, q2, includeBoundary = true) {
679
+ const o1 = area(p1, q1, p2);
680
+ const o2 = area(p1, q1, q2);
681
+ const o3 = area(p2, q2, p1);
682
+ const o4 = area(p2, q2, q1);
683
+
684
+ if (((o1 > 0 && o2 < 0) || (o1 < 0 && o2 > 0)) && ((o3 > 0 && o4 < 0) || (o3 < 0 && o4 > 0))) return true;
514
685
 
515
- if (o1 !== o2 && o3 !== o4) return true; // general case
686
+ if (!includeBoundary) return false;
516
687
 
517
688
  if (o1 === 0 && onSegment(p1, p2, q1)) return true; // p1, q1 and p2 are collinear and p2 lies on p1q1
518
689
  if (o2 === 0 && onSegment(p1, q2, q1)) return true; // p1, q1 and q2 are collinear and q2 lies on p1q1
@@ -523,27 +694,39 @@ function intersects(p1, q1, p2, q2) {
523
694
  }
524
695
 
525
696
  // for collinear points p, q, r, check if point q lies on segment pr
697
+ /** @param {Node} p @param {Node} q @param {Node} r @returns {boolean} */
526
698
  function onSegment(p, q, r) {
527
699
  return q.x <= Math.max(p.x, r.x) && q.x >= Math.min(p.x, r.x) && q.y <= Math.max(p.y, r.y) && q.y >= Math.min(p.y, r.y);
528
700
  }
529
701
 
530
- function sign(num) {
531
- return num > 0 ? 1 : num < 0 ? -1 : 0;
532
- }
533
-
534
702
  // check if a polygon diagonal intersects any polygon segments
703
+ /** @param {Node} a @param {Node} b @returns {boolean} */
535
704
  function intersectsPolygon(a, b) {
705
+ // diagonal bbox; an edge whose bbox can't overlap it can't intersect it, so
706
+ // skip the orientation test for those (the common case — the diagonal is short)
707
+ const minX = Math.min(a.x, b.x);
708
+ const maxX = Math.max(a.x, b.x);
709
+ const minY = Math.min(a.y, b.y);
710
+ const maxY = Math.max(a.y, b.y);
711
+
536
712
  let p = a;
537
713
  do {
538
- if (p.i !== a.i && p.next.i !== a.i && p.i !== b.i && p.next.i !== b.i &&
539
- intersects(p, p.next, a, b)) return true;
540
- p = p.next;
714
+ const n = p.next;
715
+ if ((p.x > maxX && n.x > maxX) || (p.x < minX && n.x < minX) ||
716
+ (p.y > maxY && n.y > maxY) || (p.y < minY && n.y < minY)) {
717
+ p = n;
718
+ continue;
719
+ }
720
+ if (p.i !== a.i && n.i !== a.i && p.i !== b.i && n.i !== b.i &&
721
+ intersects(p, n, a, b)) return true;
722
+ p = n;
541
723
  } while (p !== a);
542
724
 
543
725
  return false;
544
726
  }
545
727
 
546
728
  // check if a polygon diagonal is locally inside the polygon
729
+ /** @param {Node} a @param {Node} b @returns {boolean} */
547
730
  function locallyInside(a, b) {
548
731
  return area(a.prev, a, a.next) < 0 ?
549
732
  area(a, b, a.next) >= 0 && area(a, a.prev, b) >= 0 :
@@ -551,16 +734,17 @@ function locallyInside(a, b) {
551
734
  }
552
735
 
553
736
  // check if the middle point of a polygon diagonal is inside the polygon
737
+ /** @param {Node} a @param {Node} b @returns {boolean} */
554
738
  function middleInside(a, b) {
555
739
  let p = a;
556
740
  let inside = false;
557
741
  const px = (a.x + b.x) / 2;
558
742
  const py = (a.y + b.y) / 2;
559
743
  do {
560
- if (((p.y > py) !== (p.next.y > py)) && p.next.y !== p.y &&
561
- (px < (p.next.x - p.x) * (py - p.y) / (p.next.y - p.y) + p.x))
744
+ const n = p.next;
745
+ if (((p.y > py) !== (n.y > py)) && (px < (n.x - p.x) * (py - p.y) / (n.y - p.y) + p.x))
562
746
  inside = !inside;
563
- p = p.next;
747
+ p = n;
564
748
  } while (p !== a);
565
749
 
566
750
  return inside;
@@ -568,6 +752,7 @@ function middleInside(a, b) {
568
752
 
569
753
  // link two polygon vertices with a bridge; if the vertices belong to the same ring, it splits polygon into two;
570
754
  // if one belongs to the outer ring and another to a hole, it merges it into a single ring
755
+ /** @param {Node} a @param {Node} b @returns {Node} */
571
756
  function splitPolygon(a, b) {
572
757
  const a2 = createNode(a.i, a.x, a.y),
573
758
  b2 = createNode(b.i, b.x, b.y),
@@ -590,6 +775,7 @@ function splitPolygon(a, b) {
590
775
  }
591
776
 
592
777
  // create a node and optionally link it with previous one (in a circular doubly linked list)
778
+ /** @param {number} i @param {number} x @param {number} y @param {Node | null} last @returns {Node} */
593
779
  function insertNode(i, x, y, last) {
594
780
  const p = createNode(i, x, y);
595
781
 
@@ -606,29 +792,43 @@ function insertNode(i, x, y, last) {
606
792
  return p;
607
793
  }
608
794
 
795
+ /** @param {Node} p */
609
796
  function removeNode(p) {
610
797
  p.next.prev = p.prev;
611
798
  p.prev.next = p.next;
612
799
 
613
800
  if (p.prevZ) p.prevZ.nextZ = p.nextZ;
614
801
  if (p.nextZ) p.nextZ.prevZ = p.prevZ;
802
+
803
+ // keep the hole-bridge index's block bboxes covering the healed prev->next edge
804
+ if (indexActive) growBlock(p.prev, p.next);
615
805
  }
616
806
 
807
+ /** @param {number} i @param {number} x @param {number} y @returns {Node} */
617
808
  function createNode(i, x, y) {
618
- return {
809
+ // prev/next are assigned by the caller before any read, so the null init is cast away here
810
+ return /** @type {Node} */ (/** @type {unknown} */ ({
619
811
  i, // vertex index in coordinates array
620
812
  x, y, // vertex coordinates
621
813
  prev: null, // previous and next vertex nodes in a polygon ring
622
814
  next: null,
623
- z: 0, // z-order curve value
815
+ z: 0, // z-order curve value; doubles as owning block in the hole-bridge index during eliminateHoles
624
816
  prevZ: null, // previous and next nodes in z-order
625
- nextZ: null,
626
- steiner: false // indicates whether this is a steiner point
627
- };
628
- }
629
-
630
- // return a percentage difference between the polygon area and its triangulation area;
631
- // used to verify correctness of triangulation
817
+ nextZ: null
818
+ }));
819
+ }
820
+
821
+ /**
822
+ * Return the relative difference between the polygon area and the area of its triangulation
823
+ * a value near 0 means a correct triangulation. Useful for verifying output in tests.
824
+ *
825
+ * @param {ArrayLike<number>} data
826
+ * @param {ArrayLike<number> | null} holeIndices
827
+ * @param {number} dim number of coordinates per vertex in `data`
828
+ * @param {ArrayLike<number>} triangles output of {@link earcut}
829
+ * @returns {number}
830
+ * @example deviation(data, holes, dim, earcut(data, holes, dim)); // ~0 if correct
831
+ */
632
832
  function deviation(data, holeIndices, dim, triangles) {
633
833
  const hasHoles = holeIndices && holeIndices.length;
634
834
  const outerLen = hasHoles ? holeIndices[0] * dim : data.length;
@@ -656,6 +856,7 @@ function deviation(data, holeIndices, dim, triangles) {
656
856
  Math.abs((trianglesArea - polygonArea) / polygonArea);
657
857
  }
658
858
 
859
+ /** @param {ArrayLike<number>} data @param {number} start @param {number} end @param {number} dim @returns {number} */
659
860
  function signedArea(data, start, end, dim) {
660
861
  let sum = 0;
661
862
  for (let i = start, j = end - dim; i < end; i += dim) {
@@ -665,7 +866,13 @@ function signedArea(data, start, end, dim) {
665
866
  return sum;
666
867
  }
667
868
 
668
- // turn a polygon in a multi-dimensional array form (e.g. as in GeoJSON) into a form Earcut accepts
869
+ /**
870
+ * Turn a polygon in multi-dimensional array form (e.g. as in GeoJSON) into the flat form Earcut accepts.
871
+ *
872
+ * @param {ReadonlyArray<ReadonlyArray<ArrayLike<number>>>} data array of rings; the first ring is the outer contour, the rest are holes
873
+ * @returns {{vertices: number[], holes: number[], dimensions: number}}
874
+ * @example const {vertices, holes, dimensions} = flatten(geojson.coordinates);
875
+ */
669
876
  function flatten(data) {
670
877
  const vertices = [];
671
878
  const holes = [];