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