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