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