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