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