earcut 3.0.2 → 3.1.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.
@@ -4,16 +4,53 @@ 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
+ // set by filterPoints whenever it removes at least one node; read by earcutLinked's stall
29
+ // handler to decide whether another clip pass is worth attempting before the costlier stages
30
+ let filteredOut = false;
31
+
32
+ /**
33
+ * Triangulate a polygon given as a flat array of vertex coordinates.
34
+ *
35
+ * @param {ArrayLike<number>} data flat array of vertex coordinates
36
+ * @param {ArrayLike<number> | null} [holeIndices] indices (in vertices, not coordinates) where each hole ring starts
37
+ * @param {number} [dim=2] number of coordinates per vertex in `data`
38
+ * @returns {number[]} triangles as triplets of vertex indices into `data`
39
+ * @example earcut([10,0, 0,50, 60,60, 70,10]); // [1,0,3, 3,2,1]
40
+ */
7
41
  function earcut(data, holeIndices, dim = 2) {
8
42
 
9
43
  const hasHoles = holeIndices && holeIndices.length;
10
44
  const outerLen = hasHoles ? holeIndices[0] * dim : data.length;
45
+ if (steiners.size) steiners.clear();
46
+
11
47
  let outerNode = linkedList(data, 0, outerLen, dim, true);
48
+ /** @type {number[]} */
12
49
  const triangles = [];
13
50
 
14
51
  if (!outerNode || outerNode.next === outerNode.prev) return triangles;
15
52
 
16
- let minX, minY, invSize;
53
+ let minX = 0, minY = 0, invSize = 0;
17
54
 
18
55
  if (hasHoles) outerNode = eliminateHoles(data, holeIndices, outerNode, dim);
19
56
 
@@ -38,14 +75,16 @@ function earcut(data, holeIndices, dim = 2) {
38
75
  invSize = invSize !== 0 ? 32767 / invSize : 0;
39
76
  }
40
77
 
41
- earcutLinked(outerNode, triangles, dim, minX, minY, invSize, 0);
78
+ earcutLinked(outerNode, triangles, minX, minY, invSize);
42
79
 
43
80
  return triangles;
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,28 @@ 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
118
+ filteredOut = true;
75
119
  removeNode(p);
76
- p = end = p.prev;
77
- if (p === p.next) break;
120
+ p = p.prev; // re-check the predecessor
78
121
  again = true;
79
-
80
- } else {
122
+ } else if (full || p !== end) {
81
123
  p = p.next;
124
+ again = !full; // local heal: keep looping until the sweep reaches end
82
125
  }
83
126
  } while (again || p !== end);
84
127
 
@@ -86,28 +129,25 @@ function filterPoints(start, end) {
86
129
  }
87
130
 
88
131
  // main ear slicing loop which triangulates a polygon (given as a linked list)
89
- function earcutLinked(ear, triangles, dim, minX, minY, invSize, pass) {
90
- if (!ear) return;
91
-
132
+ /** @param {Node} ear @param {number[]} triangles @param {number} minX @param {number} minY @param {number} invSize */
133
+ function earcutLinked(ear, triangles, minX, minY, invSize) {
92
134
  // interlink polygon nodes in z-order
93
- if (!pass && invSize) indexCurve(ear, minX, minY, invSize);
135
+ if (invSize) indexCurve(ear, minX, minY, invSize);
94
136
 
95
- let stop = ear;
137
+ let stop = ear, cured = false;
96
138
 
97
139
  // iterate through ears, slicing them one by one
98
140
  while (ear.prev !== ear.next) {
99
141
  const prev = ear.prev;
142
+ /** @type {Node} */
100
143
  const next = ear.next;
101
144
 
102
- if (invSize ? isEarHashed(ear, minX, minY, invSize) : isEar(ear)) {
145
+ if (area(prev, ear, next) < 0 && (invSize ? isEarHashed(ear, minX, minY, invSize) : isEar(ear))) {
103
146
  triangles.push(prev.i, ear.i, next.i); // cut off the triangle
104
147
 
105
148
  removeNode(ear);
106
-
107
- // skipping the next vertex leads to less sliver triangles
108
- ear = next.next;
109
- stop = next.next;
110
-
149
+ ear = next;
150
+ stop = next;
111
151
  continue;
112
152
  }
113
153
 
@@ -115,111 +155,85 @@ function earcutLinked(ear, triangles, dim, minX, minY, invSize, pass) {
115
155
 
116
156
  // if we looped through the whole remaining polygon and can't find any more ears
117
157
  if (ear === stop) {
118
- // try filtering points and slicing again
119
- if (!pass) {
120
- earcutLinked(filterPoints(ear), triangles, dim, minX, minY, invSize, 1);
121
-
122
- // if this didn't work, try curing all small self-intersections locally
123
- } else if (pass === 1) {
124
- ear = cureLocalIntersections(filterPoints(ear), triangles);
125
- earcutLinked(ear, triangles, dim, minX, minY, invSize, 2);
126
-
127
- // as a last resort, try splitting the remaining polygon into two
128
- } else if (pass === 2) {
129
- splitEarcut(ear, triangles, dim, minX, minY, invSize);
158
+ // try filtering collinear/coincident points and slicing again — repeat as long as
159
+ // filtering actually removes nodes, since each removal can expose new ears
160
+ filteredOut = false;
161
+ ear = filterPoints(ear);
162
+ if (filteredOut) { stop = ear; continue; }
163
+
164
+ // filtering is exhausted: cure small local self-intersections once, then retry
165
+ if (!cured) {
166
+ ear = cureLocalIntersections(ear, triangles);
167
+ stop = ear;
168
+ cured = true;
169
+ continue;
130
170
  }
131
171
 
172
+ // as a last resort, try splitting the remaining polygon into two
173
+ splitEarcut(ear, triangles, minX, minY, invSize);
132
174
  break;
133
175
  }
134
176
  }
135
177
  }
136
178
 
137
179
  // check whether a polygon node forms a valid ear with adjacent nodes
180
+ /** @param {Node} ear @returns {boolean} */
138
181
  function isEar(ear) {
139
- const a = ear.prev,
140
- b = ear,
141
- c = ear.next;
142
-
143
- if (area(a, b, c) >= 0) return false; // reflex, can't be an ear
144
-
145
- // now make sure we don't have other points inside the potential ear
146
- const ax = a.x, bx = b.x, cx = c.x, ay = a.y, by = b.y, cy = c.y;
147
-
148
- // triangle bbox
149
- const x0 = Math.min(ax, bx, cx),
182
+ // reflex check (area(a, b, c) >= 0) is hoisted into the earcutLinked caller to avoid non-inlined call here
183
+ const a = ear.prev, b = ear, c = ear.next,
184
+ ax = a.x, bx = b.x, cx = c.x, ay = a.y, by = b.y, cy = c.y,
185
+ x0 = Math.min(ax, bx, cx), // triangle bbox
150
186
  y0 = Math.min(ay, by, cy),
151
187
  x1 = Math.max(ax, bx, cx),
152
188
  y1 = Math.max(ay, by, cy);
153
189
 
190
+ // make sure we don't have other points inside the potential ear
154
191
  let p = c.next;
155
192
  while (p !== a) {
156
- 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) &&
158
- area(p.prev, p, p.next) >= 0) return false;
193
+ if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 && !(ax === p.x && ay === p.y) &&
194
+ pointInTriangle(ax, ay, bx, by, cx, cy, p.x, p.y) && area(p.prev, p, p.next) >= 0) return false;
159
195
  p = p.next;
160
196
  }
161
-
162
197
  return true;
163
198
  }
164
199
 
200
+ /** @param {Node} ear @param {number} minX @param {number} minY @param {number} invSize @returns {boolean} */
165
201
  function isEarHashed(ear, minX, minY, invSize) {
166
- const a = ear.prev,
167
- b = ear,
168
- c = ear.next;
169
-
170
- if (area(a, b, c) >= 0) return false; // reflex, can't be an ear
171
-
172
- const ax = a.x, bx = b.x, cx = c.x, ay = a.y, by = b.y, cy = c.y;
173
-
174
- // triangle bbox
175
- const x0 = Math.min(ax, bx, cx),
202
+ // reflex check is hoisted into the earcutLinked caller (see isEar)
203
+ const a = ear.prev, b = ear, c = ear.next,
204
+ ax = a.x, bx = b.x, cx = c.x, ay = a.y, by = b.y, cy = c.y,
205
+ x0 = Math.min(ax, bx, cx), // triangle bbox
176
206
  y0 = Math.min(ay, by, cy),
177
207
  x1 = Math.max(ax, bx, cx),
178
- y1 = Math.max(ay, by, cy);
179
-
180
- // z-order range for the current triangle bbox;
181
- const minZ = zOrder(x0, y0, minX, minY, invSize),
208
+ y1 = Math.max(ay, by, cy),
209
+ minZ = zOrder(x0, y0, minX, minY, invSize), // z-order range for the current triangle bbox;
182
210
  maxZ = zOrder(x1, y1, minX, minY, invSize);
183
211
 
184
- let p = ear.prevZ,
185
- n = ear.nextZ;
186
-
187
- // look for points inside the triangle in both directions
188
- 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;
191
- p = p.prevZ;
192
-
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;
195
- n = n.nextZ;
196
- }
197
-
198
- // look for remaining points in decreasing z-order
199
- 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;
212
+ let p = ear.prevZ;
213
+ while (p && p.z >= minZ) { // look for points inside the triangle in decreasing z-order
214
+ if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 && p !== c && !(ax === p.x && ay === p.y) &&
215
+ pointInTriangle(ax, ay, bx, by, cx, cy, p.x, p.y) && area(p.prev, p, p.next) >= 0) return false;
202
216
  p = p.prevZ;
203
217
  }
204
-
205
- // look for remaining points in increasing z-order
206
- 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;
218
+ let n = ear.nextZ;
219
+ while (n && n.z <= maxZ) { // look for points in increasing z-order
220
+ if (n.x >= x0 && n.x <= x1 && n.y >= y0 && n.y <= y1 && n !== c && !(ax === n.x && ay === n.y) &&
221
+ pointInTriangle(ax, ay, bx, by, cx, cy, n.x, n.y) && area(n.prev, n, n.next) >= 0) return false;
209
222
  n = n.nextZ;
210
223
  }
211
-
212
224
  return true;
213
225
  }
214
226
 
215
227
  // go through all polygon nodes and cure small local self-intersections
228
+ /** @param {Node} start @param {number[]} triangles @returns {Node} */
216
229
  function cureLocalIntersections(start, triangles) {
217
230
  let p = start;
231
+ let cured = false;
218
232
  do {
219
233
  const a = p.prev,
220
234
  b = p.next.next;
221
235
 
222
- if (!equals(a, b) && intersects(a, p, p.next, b) && locallyInside(a, b) && locallyInside(b, a)) {
236
+ if (intersects(a, p, p.next, b, false) && locallyInside(a, b) && locallyInside(b, a)) {
223
237
 
224
238
  triangles.push(a.i, p.i, b.i);
225
239
 
@@ -228,15 +242,17 @@ function cureLocalIntersections(start, triangles) {
228
242
  removeNode(p.next);
229
243
 
230
244
  p = start = b;
245
+ cured = true;
231
246
  }
232
247
  p = p.next;
233
248
  } while (p !== start);
234
249
 
235
- return filterPoints(p);
250
+ return cured ? filterPoints(p) : p;
236
251
  }
237
252
 
238
253
  // try splitting polygon into two and triangulate them independently
239
- function splitEarcut(start, triangles, dim, minX, minY, invSize) {
254
+ /** @param {Node} start @param {number[]} triangles @param {number} minX @param {number} minY @param {number} invSize */
255
+ function splitEarcut(start, triangles, minX, minY, invSize) {
240
256
  // look for a valid diagonal that divides the polygon into two
241
257
  let a = start;
242
258
  do {
@@ -251,8 +267,8 @@ function splitEarcut(start, triangles, dim, minX, minY, invSize) {
251
267
  c = filterPoints(c, c.next);
252
268
 
253
269
  // run earcut on each half
254
- earcutLinked(a, triangles, dim, minX, minY, invSize, 0);
255
- earcutLinked(c, triangles, dim, minX, minY, invSize, 0);
270
+ earcutLinked(a, triangles, minX, minY, invSize);
271
+ earcutLinked(c, triangles, minX, minY, invSize);
256
272
  return;
257
273
  }
258
274
  b = b.next;
@@ -261,44 +277,52 @@ function splitEarcut(start, triangles, dim, minX, minY, invSize) {
261
277
  } while (a !== start);
262
278
  }
263
279
 
280
+ // true only while eliminateHoles merges holes, so removeNode keeps the block index live (growBlock)
281
+ let indexActive = false;
282
+
264
283
  // link every hole into the outer loop, producing a single-ring polygon without holes
284
+ /** @param {ArrayLike<number>} data @param {ArrayLike<number>} holeIndices @param {Node} outerNode @param {number} dim @returns {Node} */
265
285
  function eliminateHoles(data, holeIndices, outerNode, dim) {
266
286
  const queue = [];
267
287
 
268
288
  for (let i = 0, len = holeIndices.length; i < len; i++) {
269
289
  const start = holeIndices[i] * dim;
270
290
  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;
291
+ const list = /** @type {Node} */ (linkedList(data, start, end, dim, false));
292
+ if (list === list.next) steiners.add(list);
273
293
  queue.push(getLeftmost(list));
274
294
  }
275
295
 
276
296
  queue.sort(compareXYSlope);
277
297
 
278
- // process holes from left to right
298
+ // block-bbox index for findHoleBridge, grown append-only as holes merge (see notes
299
+ // above buildBlockIndex). Seed it with the outer ring, then append each merged hole.
300
+ buildBlockIndex(data.length / dim, holeIndices.length);
301
+ indexSegment(outerNode, outerNode);
302
+
303
+ // process holes from left to right; indexActive lets removeNode keep block bboxes live as
304
+ // filterPoints heals edges during merges (see growBlock)
305
+ indexActive = true;
279
306
  for (let i = 0; i < queue.length; i++) {
280
307
  outerNode = eliminateHole(queue[i], outerNode);
281
308
  }
309
+ indexActive = false;
282
310
 
283
- return outerNode;
311
+ // collapse collinear/coincident points across the whole merged ring once before clipping
312
+ return filterPoints(outerNode);
284
313
  }
285
314
 
315
+ /** @param {Node} a @param {Node} b @returns {number} */
286
316
  function compareXYSlope(a, b) {
287
- let result = a.x - b.x;
288
317
  // when the left-most point of 2 holes meet at a vertex, sort the holes counterclockwise so that when we find
289
318
  // 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;
319
+ return a.x - b.x || a.y - b.y ||
320
+ (a.next.y - a.y) / (a.next.x - a.x) -
321
+ (b.next.y - b.y) / (b.next.x - b.x);
299
322
  }
300
323
 
301
324
  // find a bridge between vertices that connects hole with an outer ring and link it
325
+ /** @param {Node} hole @param {Node} outerNode @returns {Node} */
302
326
  function eliminateHole(hole, outerNode) {
303
327
  const bridge = findHoleBridge(hole, outerNode);
304
328
  if (!bridge) {
@@ -307,35 +331,143 @@ function eliminateHole(hole, outerNode) {
307
331
 
308
332
  const bridgeReverse = splitPolygon(bridge, hole);
309
333
 
310
- // filter collinear points around the cuts
334
+ // index the merged-in segment before filtering: in ring order the splice runs
335
+ // bridge -> hole -> bridgeReverse -> bridge2 -> (bridge's old next), covering the
336
+ // hole's edges and both new slit edges. filterPoints below only drops collinear /
337
+ // coincident points, so these bboxes stay valid (conservative) supersets.
338
+ const bridge2 = bridgeReverse.next;
339
+ indexSegment(bridge, bridge2.next);
340
+
341
+ // heal collinear/coincident points around the two new slit edges
311
342
  filterPoints(bridgeReverse, bridgeReverse.next);
312
343
  return filterPoints(bridge, bridge.next);
313
344
  }
314
345
 
346
+ // Block-bbox index for findHoleBridge (issue #183): one [minX,minY,maxX,maxY] bbox per K
347
+ // consecutive ring edges, in a flat Float64Array, so the leftward-ray scan can skip whole
348
+ // blocks in O(1) instead of walking the entire merged ring. Grown append-only — the outer
349
+ // ring seeds it, then each merged hole appends a segment (head node, stop node, K-blocks
350
+ // over head..stop); independent segments, not a ring tiling, since splices land mid-ring.
351
+ // Buffers are sized once from the input upper bound and reused across calls.
352
+ //
353
+ // filterPoints only drops collinear/coincident points, so a stale bbox stays a conservative
354
+ // superset of its live edges (never a false skip); the scan skips dead nodes (p.prev.next !==
355
+ // p) and lazily advances a dead stop. Blocks are scanned in append (not ring) order, so the
356
+ // chosen bridge can differ from the un-indexed code — a different but equally valid result.
357
+ const K = 16; // edges per block
358
+
359
+ let blockBBox = new Float64Array(0); // [minX,minY,maxX,maxY] per block
360
+ let numBlocks = 0;
361
+ /** @type {Node[]} */
362
+ const blockHead = []; // first node of each block's segment
363
+ /** @type {Node[]} */
364
+ const blockStop = []; // node just past each block's segment (exclusive walk bound)
365
+
366
+ /** @param {number} maxNodes @param {number} numHoles */
367
+ function buildBlockIndex(maxNodes, numHoles) {
368
+ // upper bound: every input node indexed once, +2 bridge nodes per hole, plus a partial
369
+ // trailing block per appended segment (outer ring + one per hole)
370
+ const maxBlocks = Math.ceil((maxNodes + 2 * numHoles) / K) + numHoles + 2;
371
+ if (blockBBox.length < maxBlocks * 4) blockBBox = new Float64Array(maxBlocks * 4);
372
+ numBlocks = 0;
373
+ }
374
+
375
+ // index the ring run head..stop (exclusive) as ceil(len / K) blocks; head === stop means
376
+ // the whole ring. each block's bbox covers both endpoints of every edge it owns.
377
+ /** @param {Node} head @param {Node} stop */
378
+ function indexSegment(head, stop) {
379
+ let p = head;
380
+ do {
381
+ const b = numBlocks++;
382
+ blockHead[b] = p;
383
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
384
+ let k = 0;
385
+ do {
386
+ const c = p.next; // edge p->c; bbox must bound both endpoints
387
+ p.z = b; // reuse z as the owning block during eliminateHoles (see growBlock)
388
+ if (p.x < minX) minX = p.x; if (p.x > maxX) maxX = p.x;
389
+ if (p.y < minY) minY = p.y; if (p.y > maxY) maxY = p.y;
390
+ if (c.x < minX) minX = c.x; if (c.x > maxX) maxX = c.x;
391
+ if (c.y < minY) minY = c.y; if (c.y > maxY) maxY = c.y;
392
+ p = c;
393
+ } while (++k < K && p !== stop);
394
+ blockStop[b] = p;
395
+ const g = b * 4;
396
+ blockBBox[g] = minX; blockBBox[g + 1] = minY; blockBBox[g + 2] = maxX; blockBBox[g + 3] = maxY;
397
+ } while (p !== stop);
398
+ }
399
+
400
+ // when filterPoints heals an edge head->tail (removing the collinear node between them), the
401
+ // healed edge can extend past head's frozen block bbox if its old far endpoint lived in another
402
+ // block; grow head's block bbox to cover tail so the leftward-ray prune can't false-skip it.
403
+ /** @param {Node} head @param {Node} tail */
404
+ function growBlock(head, tail) {
405
+ const g = head.z * 4;
406
+ if (tail.x < blockBBox[g]) blockBBox[g] = tail.x;
407
+ if (tail.y < blockBBox[g + 1]) blockBBox[g + 1] = tail.y;
408
+ if (tail.x > blockBBox[g + 2]) blockBBox[g + 2] = tail.x;
409
+ if (tail.y > blockBBox[g + 3]) blockBBox[g + 3] = tail.y;
410
+ }
411
+
412
+ /** @param {number} b @returns {Node} */
413
+ function liveBlockStop(b) {
414
+ let stop = blockStop[b];
415
+ while (stop.prev.next !== stop) stop = stop.next;
416
+ blockStop[b] = stop;
417
+ return stop;
418
+ }
419
+
420
+ // the block's head node can be removed by filterPoints during merges; advance it to the next
421
+ // live node so the walk doesn't start on (and immediately terminate at) a dead node. For the
422
+ // single full-ring seed block (head === stop) the same forward advance keeps them equal, so the
423
+ // do-while still laps the whole ring instead of collapsing to an empty walk.
424
+ /** @param {number} b @returns {Node} */
425
+ function liveBlockHead(b) {
426
+ let head = blockHead[b];
427
+ while (head.prev.next !== head) head = head.next;
428
+ blockHead[b] = head;
429
+ return head;
430
+ }
431
+
315
432
  // David Eberly's algorithm for finding a bridge between hole and outer polygon
433
+ /** @param {Node} hole @param {Node} outerNode @returns {Node | null} */
316
434
  function findHoleBridge(hole, outerNode) {
317
435
  let p = outerNode;
318
436
  const hx = hole.x;
319
437
  const hy = hole.y;
320
438
  let qx = -Infinity;
439
+ /** @type {Node | undefined} */
321
440
  let m;
322
441
 
323
442
  // find a segment intersected by a ray from the hole's leftmost point to the left;
324
443
  // segment's endpoint with lesser x will be potential connection point
325
444
  // unless they intersect at a vertex, then choose the vertex
326
445
  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
446
+
447
+ // scan blocks; skip any whose bbox can't hold a crossing that beats qx and lies left
448
+ // of hx (the prune Morton order can't express explicit per-axis [minY,maxY]/[minX,maxX])
449
+ for (let b = 0, g = 0; b < numBlocks; b++, g += 4) {
450
+ if (hy < blockBBox[g + 1] || hy > blockBBox[g + 3] || blockBBox[g] > hx || blockBBox[g + 2] <= qx) continue;
451
+
452
+ // ensure the walk's exclusive bound is live so we don't overrun into other blocks
453
+ const stop = liveBlockStop(b);
454
+
455
+ p = liveBlockHead(b);
456
+ do {
457
+ if (p.prev.next === p) { // skip nodes removed by filterPoints (stale in the index)
458
+ if (equals(hole, p.next)) return p.next;
459
+ else if (hy <= p.y && hy >= p.next.y && p.next.y !== p.y) {
460
+ const x = p.x + (hy - p.y) * (p.next.x - p.x) / (p.next.y - p.y);
461
+ if (x <= hx && x > qx) {
462
+ qx = x;
463
+ m = p.x < p.next.x ? p : p.next;
464
+ if (x === hx) return m; // hole touches outer segment; pick leftmost endpoint
465
+ }
466
+ }
335
467
  }
336
- }
337
- p = p.next;
338
- } while (p !== outerNode);
468
+ p = p.next;
469
+ } while (p !== stop);
470
+ }
339
471
 
340
472
  if (!m) return null;
341
473
 
@@ -343,108 +475,131 @@ function findHoleBridge(hole, outerNode) {
343
475
  // if there are no points found, we have a valid connection;
344
476
  // otherwise choose the point of the minimum angle with the ray as connection point
345
477
 
346
- const stop = m;
347
478
  const mx = m.x;
348
479
  const my = m.y;
480
+ const tminY = Math.min(hy, my); // the triangle's y span; x span is [mx, hx]
481
+ const tmaxY = Math.max(hy, my);
349
482
  let tanMin = Infinity;
350
483
 
351
- p = m;
484
+ // scan the same blocks; skip any whose bbox can't overlap the triangle's [mx,hx]×[tminY,tmaxY] box
485
+ for (let b = 0, g = 0; b < numBlocks; b++, g += 4) {
486
+ if (blockBBox[g + 2] < mx || blockBBox[g] > hx || blockBBox[g + 3] < tminY || blockBBox[g + 1] > tmaxY) continue;
352
487
 
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)) {
488
+ const stop = liveBlockStop(b);
489
+
490
+ p = liveBlockHead(b);
491
+ do {
492
+ if (p.prev.next === p && hx >= p.x && p.x >= mx && hx !== p.x && // skip dead nodes
493
+ pointInTriangle(hy < my ? hx : qx, hy, mx, my, hy < my ? qx : hx, hy, p.x, p.y)) {
356
494
 
357
- const tan = Math.abs(hy - p.y) / (hx - p.x); // tangential
495
+ const tan = Math.abs(hy - p.y) / (hx - p.x); // tangential
358
496
 
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;
497
+ // if hole point sits on p's horizontal edge (T-junction touch): the bridge runs
498
+ // along that edge locallyInside rejects it as collinear, but it's valid
499
+ if ((locallyInside(p, hole) || (p.y === hy && p.next.y === hy && p.next.x > hx)) &&
500
+ (tan < tanMin || (tan === tanMin && (p.x > m.x || (p.x === m.x && sectorContainsSector(m, p)))))) {
501
+ m = p;
502
+ tanMin = tan;
503
+ }
363
504
  }
364
- }
365
505
 
366
- p = p.next;
367
- } while (p !== stop);
506
+ p = p.next;
507
+ } while (p !== stop);
508
+ }
368
509
 
369
510
  return m;
370
511
  }
371
512
 
372
513
  // whether sector in vertex m contains sector in vertex p in the same coordinates
514
+ /** @param {Node} m @param {Node} p @returns {boolean} */
373
515
  function sectorContainsSector(m, p) {
374
516
  return area(m.prev, m, p.prev) < 0 && area(p.next, m, m.next) < 0;
375
517
  }
376
518
 
377
- // interlink polygon nodes in z-order
519
+ // scratch buffers reused across calls and grown on demand: two node-ref arrays that
520
+ // ping-pong during the radix passes, plus parallel z-value arrays so the passes read
521
+ // z from contiguous memory instead of dereferencing each node. 256-entry histogram for
522
+ // 8-bit digits; the small histogram keeps per-call setup cheap (most rings are short)
523
+ /** @type {Node[]} */
524
+ const sortArr = [];
525
+ /** @type {Node[]} */
526
+ let sortBuf = [];
527
+ let zArr = new Uint32Array(0);
528
+ let zBuf = new Uint32Array(0);
529
+ const counts = new Uint32Array(256);
530
+
531
+ // interlink polygon nodes in z-order: collect into an array, sort by z, relink
532
+ /** @param {Node} start @param {number} minX @param {number} minY @param {number} invSize */
378
533
  function indexCurve(start, minX, minY, invSize) {
379
534
  let p = start;
535
+ let n = 0;
380
536
  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;
537
+ // always (re)compute: z may still hold a block index left over from eliminateHoles
538
+ p.z = zOrder(p.x, p.y, minX, minY, invSize);
539
+ sortArr[n++] = p;
384
540
  p = p.next;
385
541
  } while (p !== start);
386
542
 
387
- p.prevZ.nextZ = null;
388
- p.prevZ = null;
389
-
390
- sortLinked(p);
391
- }
392
-
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;
543
+ sortNodes(n);
431
544
 
432
- e.prevZ = tail;
433
- tail = e;
434
- }
435
-
436
- p = q;
545
+ /** @type {Node | null} */
546
+ let prev = null;
547
+ for (let i = 0; i < n; i++) {
548
+ const node = sortArr[i];
549
+ node.prevZ = prev;
550
+ if (prev) prev.nextZ = node;
551
+ prev = node;
552
+ }
553
+ /** @type {Node} */ (prev).nextZ = null;
554
+ }
555
+
556
+ // sort the first n nodes of sortArr by z, in place: insertion sort for small n (cheaper
557
+ // than histogram setup), else LSD radix in four 8-bit passes (covering z's 30 bits)
558
+ /** @param {number} n */
559
+ function sortNodes(n) {
560
+ if (n <= 32) {
561
+ for (let i = 1; i < n; i++) {
562
+ const node = sortArr[i], z = node.z;
563
+ let j = i - 1;
564
+ while (j >= 0 && sortArr[j].z > z) { sortArr[j + 1] = sortArr[j]; j--; }
565
+ sortArr[j + 1] = node;
437
566
  }
567
+ return;
568
+ }
438
569
 
439
- tail.nextZ = null;
440
- inSize *= 2;
441
-
442
- } while (numMerges > 1);
443
-
444
- return list;
570
+ if (zArr.length < n) {
571
+ zArr = new Uint32Array(n);
572
+ zBuf = new Uint32Array(n);
573
+ sortBuf = new Array(n);
574
+ }
575
+ for (let i = 0; i < n; i++) zArr[i] = sortArr[i].z;
576
+
577
+ // even pass count lands the sorted result back in sortArr
578
+ radixPass(n, sortArr, zArr, sortBuf, zBuf, 0);
579
+ radixPass(n, sortBuf, zBuf, sortArr, zArr, 8);
580
+ radixPass(n, sortArr, zArr, sortBuf, zBuf, 16);
581
+ radixPass(n, sortBuf, zBuf, sortArr, zArr, 24);
582
+ }
583
+
584
+ // one LSD radix pass: stably scatter the first n nodes (and their z) from src to dst,
585
+ // bucketed by the 8-bit digit of z at the given bit shift
586
+ /** @param {number} n @param {Node[]} src @param {Uint32Array} srcZ @param {Node[]} dst @param {Uint32Array} dstZ @param {number} shift */
587
+ function radixPass(n, src, srcZ, dst, dstZ, shift) {
588
+ counts.fill(0);
589
+ for (let i = 0; i < n; i++) counts[(srcZ[i] >>> shift) & 0xff]++;
590
+ // turn per-bucket counts into start offsets (prefix sum)
591
+ let sum = 0;
592
+ for (let b = 0; b < 256; b++) { const c = counts[b]; counts[b] = sum; sum += c; }
593
+ for (let i = 0; i < n; i++) {
594
+ const z = srcZ[i];
595
+ const pos = counts[(z >>> shift) & 0xff]++;
596
+ dst[pos] = src[i];
597
+ dstZ[pos] = z;
598
+ }
445
599
  }
446
600
 
447
601
  // z-order of a point given coords and inverse of the longer side of data bbox
602
+ /** @param {number} x @param {number} y @param {number} minX @param {number} minY @param {number} invSize @returns {number} */
448
603
  function zOrder(x, y, minX, minY, invSize) {
449
604
  // coords are transformed into non-negative 15-bit integer range
450
605
  x = (x - minX) * invSize | 0;
@@ -464,6 +619,7 @@ function zOrder(x, y, minX, minY, invSize) {
464
619
  }
465
620
 
466
621
  // find the leftmost node of a polygon ring
622
+ /** @param {Node} start @returns {Node} */
467
623
  function getLeftmost(start) {
468
624
  let p = start,
469
625
  leftmost = start;
@@ -476,43 +632,45 @@ function getLeftmost(start) {
476
632
  }
477
633
 
478
634
  // check if a point lies within a convex triangle
635
+ /** @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
636
  function pointInTriangle(ax, ay, bx, by, cx, cy, px, py) {
480
637
  return (cx - px) * (ay - py) >= (ax - px) * (cy - py) &&
481
638
  (ax - px) * (by - py) >= (bx - px) * (ay - py) &&
482
639
  (bx - px) * (cy - py) >= (cx - px) * (by - py);
483
640
  }
484
641
 
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
642
  // check if a diagonal between two polygon nodes is valid (lies in polygon interior)
643
+ /** @param {Node} a @param {Node} b @returns {boolean} true when the diagonal is valid */
491
644
  function isValidDiagonal(a, b) {
492
- return a.next.i !== b.i && a.prev.i !== b.i && !intersectsPolygon(a, b) && // doesn't intersect other edges
493
- (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
495
- equals(a, b) && area(a.prev, a, a.next) > 0 && area(b.prev, b, b.next) > 0); // special zero-length case
645
+ const zeroLength = equals(a, b) && area(a.prev, a, a.next) > 0 && area(b.prev, b, b.next) > 0; // degenerate case
646
+ return a.next.i !== b.i && (zeroLength || locallyInside(a, b) && locallyInside(b, a) && // // locally visible
647
+ (area(a.prev, a, b.prev) !== 0 || area(a, b.prev, b) !== 0)) && // no opposite-facing sectors
648
+ !intersectsPolygon(a, b) && (zeroLength || middleInside(a, b)); // doesn't intersect other edges, diagonal inside polygon
496
649
  }
497
650
 
498
651
  // signed area of a triangle
652
+ /** @param {Node} p @param {Node} q @param {Node} r @returns {number} */
499
653
  function area(p, q, r) {
500
654
  return (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y);
501
655
  }
502
656
 
503
657
  // check if two points are equal
658
+ /** @param {Node} p1 @param {Node} p2 @returns {boolean} */
504
659
  function equals(p1, p2) {
505
660
  return p1.x === p2.x && p1.y === p2.y;
506
661
  }
507
662
 
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));
663
+ // check if two segments intersect; by default includes collinear boundary touches
664
+ /** @param {Node} p1 @param {Node} q1 @param {Node} p2 @param {Node} q2 @param {boolean} [includeBoundary] @returns {boolean} */
665
+ function intersects(p1, q1, p2, q2, includeBoundary = true) {
666
+ const o1 = area(p1, q1, p2);
667
+ const o2 = area(p1, q1, q2);
668
+ const o3 = area(p2, q2, p1);
669
+ const o4 = area(p2, q2, q1);
514
670
 
515
- if (o1 !== o2 && o3 !== o4) return true; // general case
671
+ if (((o1 > 0 && o2 < 0) || (o1 < 0 && o2 > 0)) && ((o3 > 0 && o4 < 0) || (o3 < 0 && o4 > 0))) return true;
672
+
673
+ if (!includeBoundary) return false;
516
674
 
517
675
  if (o1 === 0 && onSegment(p1, p2, q1)) return true; // p1, q1 and p2 are collinear and p2 lies on p1q1
518
676
  if (o2 === 0 && onSegment(p1, q2, q1)) return true; // p1, q1 and q2 are collinear and q2 lies on p1q1
@@ -523,27 +681,39 @@ function intersects(p1, q1, p2, q2) {
523
681
  }
524
682
 
525
683
  // for collinear points p, q, r, check if point q lies on segment pr
684
+ /** @param {Node} p @param {Node} q @param {Node} r @returns {boolean} */
526
685
  function onSegment(p, q, r) {
527
686
  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
687
  }
529
688
 
530
- function sign(num) {
531
- return num > 0 ? 1 : num < 0 ? -1 : 0;
532
- }
533
-
534
689
  // check if a polygon diagonal intersects any polygon segments
690
+ /** @param {Node} a @param {Node} b @returns {boolean} */
535
691
  function intersectsPolygon(a, b) {
692
+ // diagonal bbox; an edge whose bbox can't overlap it can't intersect it, so
693
+ // skip the orientation test for those (the common case — the diagonal is short)
694
+ const minX = Math.min(a.x, b.x);
695
+ const maxX = Math.max(a.x, b.x);
696
+ const minY = Math.min(a.y, b.y);
697
+ const maxY = Math.max(a.y, b.y);
698
+
536
699
  let p = a;
537
700
  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;
701
+ const n = p.next;
702
+ if ((p.x > maxX && n.x > maxX) || (p.x < minX && n.x < minX) ||
703
+ (p.y > maxY && n.y > maxY) || (p.y < minY && n.y < minY)) {
704
+ p = n;
705
+ continue;
706
+ }
707
+ if (p.i !== a.i && n.i !== a.i && p.i !== b.i && n.i !== b.i &&
708
+ intersects(p, n, a, b)) return true;
709
+ p = n;
541
710
  } while (p !== a);
542
711
 
543
712
  return false;
544
713
  }
545
714
 
546
715
  // check if a polygon diagonal is locally inside the polygon
716
+ /** @param {Node} a @param {Node} b @returns {boolean} */
547
717
  function locallyInside(a, b) {
548
718
  return area(a.prev, a, a.next) < 0 ?
549
719
  area(a, b, a.next) >= 0 && area(a, a.prev, b) >= 0 :
@@ -551,16 +721,17 @@ function locallyInside(a, b) {
551
721
  }
552
722
 
553
723
  // check if the middle point of a polygon diagonal is inside the polygon
724
+ /** @param {Node} a @param {Node} b @returns {boolean} */
554
725
  function middleInside(a, b) {
555
726
  let p = a;
556
727
  let inside = false;
557
728
  const px = (a.x + b.x) / 2;
558
729
  const py = (a.y + b.y) / 2;
559
730
  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))
731
+ const n = p.next;
732
+ if (((p.y > py) !== (n.y > py)) && (px < (n.x - p.x) * (py - p.y) / (n.y - p.y) + p.x))
562
733
  inside = !inside;
563
- p = p.next;
734
+ p = n;
564
735
  } while (p !== a);
565
736
 
566
737
  return inside;
@@ -568,6 +739,7 @@ function middleInside(a, b) {
568
739
 
569
740
  // link two polygon vertices with a bridge; if the vertices belong to the same ring, it splits polygon into two;
570
741
  // if one belongs to the outer ring and another to a hole, it merges it into a single ring
742
+ /** @param {Node} a @param {Node} b @returns {Node} */
571
743
  function splitPolygon(a, b) {
572
744
  const a2 = createNode(a.i, a.x, a.y),
573
745
  b2 = createNode(b.i, b.x, b.y),
@@ -590,6 +762,7 @@ function splitPolygon(a, b) {
590
762
  }
591
763
 
592
764
  // create a node and optionally link it with previous one (in a circular doubly linked list)
765
+ /** @param {number} i @param {number} x @param {number} y @param {Node | null} last @returns {Node} */
593
766
  function insertNode(i, x, y, last) {
594
767
  const p = createNode(i, x, y);
595
768
 
@@ -606,29 +779,43 @@ function insertNode(i, x, y, last) {
606
779
  return p;
607
780
  }
608
781
 
782
+ /** @param {Node} p */
609
783
  function removeNode(p) {
610
784
  p.next.prev = p.prev;
611
785
  p.prev.next = p.next;
612
786
 
613
787
  if (p.prevZ) p.prevZ.nextZ = p.nextZ;
614
788
  if (p.nextZ) p.nextZ.prevZ = p.prevZ;
789
+
790
+ // keep the hole-bridge index's block bboxes covering the healed prev->next edge
791
+ if (indexActive) growBlock(p.prev, p.next);
615
792
  }
616
793
 
794
+ /** @param {number} i @param {number} x @param {number} y @returns {Node} */
617
795
  function createNode(i, x, y) {
618
- return {
796
+ // prev/next are assigned by the caller before any read, so the null init is cast away here
797
+ return /** @type {Node} */ (/** @type {unknown} */ ({
619
798
  i, // vertex index in coordinates array
620
799
  x, y, // vertex coordinates
621
800
  prev: null, // previous and next vertex nodes in a polygon ring
622
801
  next: null,
623
- z: 0, // z-order curve value
802
+ z: 0, // z-order curve value; doubles as owning block in the hole-bridge index during eliminateHoles
624
803
  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
804
+ nextZ: null
805
+ }));
806
+ }
807
+
808
+ /**
809
+ * Return the relative difference between the polygon area and the area of its triangulation
810
+ * a value near 0 means a correct triangulation. Useful for verifying output in tests.
811
+ *
812
+ * @param {ArrayLike<number>} data
813
+ * @param {ArrayLike<number> | null} holeIndices
814
+ * @param {number} dim number of coordinates per vertex in `data`
815
+ * @param {ArrayLike<number>} triangles output of {@link earcut}
816
+ * @returns {number}
817
+ * @example deviation(data, holes, dim, earcut(data, holes, dim)); // ~0 if correct
818
+ */
632
819
  function deviation(data, holeIndices, dim, triangles) {
633
820
  const hasHoles = holeIndices && holeIndices.length;
634
821
  const outerLen = hasHoles ? holeIndices[0] * dim : data.length;
@@ -656,6 +843,7 @@ function deviation(data, holeIndices, dim, triangles) {
656
843
  Math.abs((trianglesArea - polygonArea) / polygonArea);
657
844
  }
658
845
 
846
+ /** @param {ArrayLike<number>} data @param {number} start @param {number} end @param {number} dim @returns {number} */
659
847
  function signedArea(data, start, end, dim) {
660
848
  let sum = 0;
661
849
  for (let i = start, j = end - dim; i < end; i += dim) {
@@ -665,7 +853,13 @@ function signedArea(data, start, end, dim) {
665
853
  return sum;
666
854
  }
667
855
 
668
- // turn a polygon in a multi-dimensional array form (e.g. as in GeoJSON) into a form Earcut accepts
856
+ /**
857
+ * Turn a polygon in multi-dimensional array form (e.g. as in GeoJSON) into the flat form Earcut accepts.
858
+ *
859
+ * @param {ReadonlyArray<ReadonlyArray<ArrayLike<number>>>} data array of rings; the first ring is the outer contour, the rest are holes
860
+ * @returns {{vertices: number[], holes: number[], dimensions: number}}
861
+ * @example const {vertices, holes, dimensions} = flatten(geojson.coordinates);
862
+ */
669
863
  function flatten(data) {
670
864
  const vertices = [];
671
865
  const holes = [];