pts 0.12.9 → 1.0.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/README.md +92 -80
- package/dist/index.d.mts +6488 -1245
- package/dist/index.d.mts.map +1 -0
- package/dist/index.d.ts +6488 -1245
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10733 -10668
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +10684 -10600
- package/dist/index.mjs.map +1 -0
- package/dist/pts.js +10868 -10768
- package/dist/pts.js.map +1 -0
- package/dist/pts.min.js +3 -5
- package/dist/pts.min.js.map +1 -0
- package/package.json +92 -31
- package/src/Canvas.ts +1737 -0
- package/src/Color.ts +1109 -0
- package/src/Create.ts +1834 -0
- package/src/Dom.ts +940 -0
- package/src/Form.ts +312 -0
- package/src/Image.ts +722 -0
- package/src/LinearAlgebra.ts +530 -0
- package/src/Num.ts +1093 -0
- package/src/Op.ts +2442 -0
- package/src/Physics.ts +1233 -0
- package/src/Play.ts +861 -0
- package/src/Pt.ts +1310 -0
- package/src/Space.ts +898 -0
- package/src/Svg.ts +1652 -0
- package/src/Types.ts +318 -0
- package/src/Typography.ts +228 -0
- package/src/UI.ts +757 -0
- package/src/Util.ts +454 -0
- package/src/_module.ts +18 -0
- package/src/_path.ts +1402 -0
- package/src/_script.ts +94 -0
- package/src/_triangulate.ts +925 -0
- package/src/uheprng.ts +153 -0
package/src/_path.ts
ADDED
|
@@ -0,0 +1,1402 @@
|
|
|
1
|
+
/*! Pts.js is licensed under Apache License 2.0. Copyright © 2017-current William Ngan and contributors. (https://github.com/williamngan/pts) */
|
|
2
|
+
|
|
3
|
+
import { Pt, Group } from "./Pt";
|
|
4
|
+
import { Util } from "./Util";
|
|
5
|
+
import { crossingParameter, orient2d } from "./_triangulate";
|
|
6
|
+
import { type PolygonLike, type PtLike, type PtLikeIterable } from "./Types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Internal planar overlay used by [`Path`](#link).
|
|
10
|
+
*
|
|
11
|
+
* Every mode is the same computation: build the arrangement of all input
|
|
12
|
+
* edges, label each face with the winding number of every shape, keep the
|
|
13
|
+
* faces the mode asks for, and either merge them (unite, intersect, exclude,
|
|
14
|
+
* minus front, minus back) or emit each one (divide, crop).
|
|
15
|
+
*
|
|
16
|
+
* Input vertices within `tol` are merged before constructing edges, and a
|
|
17
|
+
* vertex within `tol` of an edge's interior then splits that edge. This is an
|
|
18
|
+
* intentional geometric tolerance, not the precision of a Float32 point: it
|
|
19
|
+
* makes T-junctions, shared edges, and edges that coincide up to rounding
|
|
20
|
+
* share their vertices. Both happen before any crossing is computed, so no
|
|
21
|
+
* crossing is decided on geometry that a later split would bend. The proper
|
|
22
|
+
* crossings of the resulting sub-edges are then found with exact orientation
|
|
23
|
+
* predicates and an exactly evaluated crossing parameter, and merged at
|
|
24
|
+
* double rounding precision, independently of the input tolerance.
|
|
25
|
+
* Coincident sub-edges carry sparse per-shape winding deltas. Faces are
|
|
26
|
+
* half-edge cycles, labeled by flooding from one seed per component. Holes
|
|
27
|
+
* find their outer ring with a ray to the west. Output coordinates are
|
|
28
|
+
* Float32 Pts, so features below that precision may still collapse, and
|
|
29
|
+
* rings thinner than `tol` (hairlines left by edges that coincide up to
|
|
30
|
+
* rounding) are dropped.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/** The seven Path modes. */
|
|
34
|
+
export type PathMode =
|
|
35
|
+
| "unite"
|
|
36
|
+
| "intersect"
|
|
37
|
+
| "exclude"
|
|
38
|
+
| "minusFront"
|
|
39
|
+
| "minusBack"
|
|
40
|
+
| "divide"
|
|
41
|
+
| "crop";
|
|
42
|
+
|
|
43
|
+
// The snapping grid has cells of 64 tol, so a point only needs a neighboring
|
|
44
|
+
// cell checked when it lies within tol of that cell's border, and the cell
|
|
45
|
+
// indices stay within ±2^14 (tol is a fixed fraction of the largest
|
|
46
|
+
// coordinate), which keeps every key a small integer.
|
|
47
|
+
const CELL_TOLS = 64;
|
|
48
|
+
const CELL_OFFSET = 16384;
|
|
49
|
+
const CELL_SPAN = 32768;
|
|
50
|
+
// A westward ray query either scans every edge or walks the bounding-box
|
|
51
|
+
// tree; scanning costs about E per query and the tree about E log E to build,
|
|
52
|
+
// so the tree pays off after this many queries times log2(E). The number of
|
|
53
|
+
// queries is known before they start: components for the seeds, holes for
|
|
54
|
+
// the owners.
|
|
55
|
+
const RAY_TREE_PER_LOG = 1.5;
|
|
56
|
+
// Candidate pairs from the first sweep are kept, up to this many, so that the
|
|
57
|
+
// second sweep can replay them when the first registered no split (the common
|
|
58
|
+
// case). Beyond the cap they are streamed and enumerated again.
|
|
59
|
+
const PAIR_BUFFER = 1 << 18;
|
|
60
|
+
|
|
61
|
+
/** The rings of a shape as arrays of points: one ring (its first item is a point) or a list of rings. */
|
|
62
|
+
function ringsOf(shape: PolygonLike): PtLike[][] {
|
|
63
|
+
const list = Util.iterToArray(shape as Iterable<unknown>);
|
|
64
|
+
if (list.length === 0) return [];
|
|
65
|
+
const first = list[0];
|
|
66
|
+
if (first != null && typeof first[0] === "number") return [list];
|
|
67
|
+
const rings: PtLike[][] = new Array(list.length);
|
|
68
|
+
for (let i = 0; i < list.length; i++) rings[i] = Util.iterToArray(list[i]);
|
|
69
|
+
return rings;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A trig-free stand-in for `atan2`: increases counterclockwise over [0, 4),
|
|
74
|
+
* starting at west like atan2's range does, so the sort order and the
|
|
75
|
+
* "largest angle" at a leftmost vertex are the same as with atan2.
|
|
76
|
+
*/
|
|
77
|
+
function pseudoAngle(dx: number, dy: number): number {
|
|
78
|
+
const s = (dx < 0 ? -dx : dx) + (dy < 0 ? -dy : dy);
|
|
79
|
+
// counterclockwise from east: [0, 4)
|
|
80
|
+
const d =
|
|
81
|
+
dy >= 0
|
|
82
|
+
? dx >= 0
|
|
83
|
+
? dy / s
|
|
84
|
+
: 1 - dx / s
|
|
85
|
+
: dx < 0
|
|
86
|
+
? 2 - dy / s
|
|
87
|
+
: 3 + dx / s;
|
|
88
|
+
return d >= 2 ? d - 2 : d + 2;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Add a winding delta, keeping zero entries out of the sparse vector. */
|
|
92
|
+
function addWinding(
|
|
93
|
+
vector: Map<number, number>,
|
|
94
|
+
shape: number,
|
|
95
|
+
delta: number,
|
|
96
|
+
): void {
|
|
97
|
+
const value = (vector.get(shape) ?? 0) + delta;
|
|
98
|
+
if (value === 0) vector.delete(shape);
|
|
99
|
+
else vector.set(shape, value);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** How many distinct points a ring has, up to three: enough to tell a real ring from a degenerate one. */
|
|
103
|
+
function distinctPoints(ring: PtLike[]): number {
|
|
104
|
+
const seen: PtLike[] = [];
|
|
105
|
+
for (let i = 0; i < ring.length && seen.length < 3; i++) {
|
|
106
|
+
const p = ring[i];
|
|
107
|
+
let dup = false;
|
|
108
|
+
for (let j = 0; j < seen.length; j++) {
|
|
109
|
+
if (seen[j][0] === p[0] && seen[j][1] === p[1]) {
|
|
110
|
+
dup = true;
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (!dup) seen.push(p);
|
|
115
|
+
}
|
|
116
|
+
return seen.length;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Reorder `items[lo, hi)` so the item at `k` has the key it would have when
|
|
121
|
+
* sorted, smaller keys before it and larger after (Hoare quickselect).
|
|
122
|
+
*/
|
|
123
|
+
function selectByKey(
|
|
124
|
+
items: Uint32Array,
|
|
125
|
+
key: Float64Array,
|
|
126
|
+
lo: number,
|
|
127
|
+
hi: number,
|
|
128
|
+
k: number,
|
|
129
|
+
): void {
|
|
130
|
+
let l = lo;
|
|
131
|
+
let r = hi - 1;
|
|
132
|
+
while (l < r) {
|
|
133
|
+
const pivot = key[items[(l + r) >> 1]];
|
|
134
|
+
let i = l;
|
|
135
|
+
let j = r;
|
|
136
|
+
while (i <= j) {
|
|
137
|
+
while (key[items[i]] < pivot) i++;
|
|
138
|
+
while (key[items[j]] > pivot) j--;
|
|
139
|
+
if (i <= j) {
|
|
140
|
+
const t = items[i];
|
|
141
|
+
items[i] = items[j];
|
|
142
|
+
items[j] = t;
|
|
143
|
+
i++;
|
|
144
|
+
j--;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (k <= j) r = j;
|
|
148
|
+
else if (k >= i) l = i;
|
|
149
|
+
else return;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Output perimeter, for the hairline test. */
|
|
154
|
+
function outputPerimeter(ring: Group): number {
|
|
155
|
+
let length = 0;
|
|
156
|
+
for (let i = 0, n = ring.length; i < n; i++) {
|
|
157
|
+
const a = ring[i];
|
|
158
|
+
const b = ring[i === n - 1 ? 0 : i + 1];
|
|
159
|
+
length += Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
160
|
+
}
|
|
161
|
+
return length;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Output area relative to the first point, avoiding large-offset cancellation. */
|
|
165
|
+
function outputArea(ring: Group): number {
|
|
166
|
+
let area = 0;
|
|
167
|
+
const a = ring[0];
|
|
168
|
+
for (let i = 1; i + 1 < ring.length; i++) {
|
|
169
|
+
const b = ring[i],
|
|
170
|
+
c = ring[i + 1];
|
|
171
|
+
area += (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1]);
|
|
172
|
+
}
|
|
173
|
+
return area / 2;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Edge bounding boxes and the edges ordered by their left side, for a sweep. */
|
|
177
|
+
type SweepBoxes = {
|
|
178
|
+
minX: Float64Array;
|
|
179
|
+
maxX: Float64Array;
|
|
180
|
+
minY: Float64Array;
|
|
181
|
+
maxY: Float64Array;
|
|
182
|
+
order: Uint32Array;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/** Bounds of nonhorizontal edges used by westward rays. */
|
|
186
|
+
type RayNode = {
|
|
187
|
+
x0: number;
|
|
188
|
+
x1: number;
|
|
189
|
+
y0: number;
|
|
190
|
+
y1: number;
|
|
191
|
+
// A ray in this interval crosses every edge in the node. If the node is
|
|
192
|
+
// wholly west of the query, its aggregate winding can be added at once.
|
|
193
|
+
allY0: number;
|
|
194
|
+
allY1: number;
|
|
195
|
+
sum: Map<number, number>;
|
|
196
|
+
edges?: number[];
|
|
197
|
+
left?: RayNode;
|
|
198
|
+
right?: RayNode;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
/** Internal arrangement, also exposed to the geometry invariant tests. */
|
|
202
|
+
export class Overlay {
|
|
203
|
+
tol = 0;
|
|
204
|
+
private rounding = 0;
|
|
205
|
+
k = 0; // number of shapes
|
|
206
|
+
|
|
207
|
+
// vertices in doubles, with a grid hash for snapping
|
|
208
|
+
vx: number[] = [];
|
|
209
|
+
vy: number[] = [];
|
|
210
|
+
private cells = new Map<number, number[]>();
|
|
211
|
+
|
|
212
|
+
// input edges, directed, one shape each
|
|
213
|
+
ea: number[] = [];
|
|
214
|
+
eb: number[] = [];
|
|
215
|
+
es: number[] = [];
|
|
216
|
+
|
|
217
|
+
// split events: (edge, parameter, vertex)
|
|
218
|
+
private spE: number[] = [];
|
|
219
|
+
private spT: number[] = [];
|
|
220
|
+
private spV: number[] = [];
|
|
221
|
+
|
|
222
|
+
// graph edges, low vertex to high vertex, with nonzero per-shape deltas
|
|
223
|
+
gu: number[] = [];
|
|
224
|
+
gv: number[] = [];
|
|
225
|
+
deltas: Map<number, number>[] = [];
|
|
226
|
+
|
|
227
|
+
// half-edges: 2e runs low to high, 2e + 1 the other way
|
|
228
|
+
offset!: Int32Array; // outgoing half-edges of vertex v are out[offset[v] .. offset[v + 1])
|
|
229
|
+
out!: Int32Array;
|
|
230
|
+
slot!: Int32Array; // index of each half-edge in its origin's sorted list
|
|
231
|
+
next!: Int32Array; // the next half-edge of the face on the left
|
|
232
|
+
cycle!: Int32Array; // cycle id per half-edge
|
|
233
|
+
cycleArea: number[] = [];
|
|
234
|
+
cycleStart: number[] = [];
|
|
235
|
+
// per cycle: how many shapes wind around its face, and whether the first
|
|
236
|
+
// and last shapes do. That is all the modes need, so the full sparse
|
|
237
|
+
// winding vectors are kept only when `label(true)` asks for them (tests).
|
|
238
|
+
count!: Int32Array;
|
|
239
|
+
hasFirst!: Uint8Array;
|
|
240
|
+
hasLast!: Uint8Array;
|
|
241
|
+
labels?: Map<number, number>[];
|
|
242
|
+
// undefined: not built yet; null: built, but no edge can cross a ray
|
|
243
|
+
private rayIndex: RayNode | null | undefined;
|
|
244
|
+
|
|
245
|
+
origin(h: number): number {
|
|
246
|
+
return h & 1 ? this.gv[h >> 1] : this.gu[h >> 1];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
target(h: number): number {
|
|
250
|
+
return h & 1 ? this.gu[h >> 1] : this.gv[h >> 1];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ------------------------------------------------------------- input
|
|
254
|
+
|
|
255
|
+
/** Read the shapes into snapped vertices and directed edges. False when there is nothing to combine. */
|
|
256
|
+
read(shapes: Iterable<PolygonLike> | PtLikeIterable): boolean {
|
|
257
|
+
let list: unknown[] = Util.iterToArray(shapes as Iterable<unknown>);
|
|
258
|
+
// A single ring of points is the likeliest misuse (every Polygon function
|
|
259
|
+
// takes one Group); treat it as a list of one shape.
|
|
260
|
+
if (
|
|
261
|
+
list.length > 0 &&
|
|
262
|
+
list[0] != null &&
|
|
263
|
+
typeof (list[0] as ArrayLike<unknown>)[0] === "number"
|
|
264
|
+
) {
|
|
265
|
+
list = [list];
|
|
266
|
+
}
|
|
267
|
+
this.k = list.length;
|
|
268
|
+
const rings: PtLike[][] = [];
|
|
269
|
+
const ringShape: number[] = [];
|
|
270
|
+
let maxAbs = 0;
|
|
271
|
+
|
|
272
|
+
// first pass: the scale that sets the tolerance, and which rings are usable
|
|
273
|
+
for (let s = 0; s < list.length; s++) {
|
|
274
|
+
const shapeRings = ringsOf(list[s] as PolygonLike);
|
|
275
|
+
for (let r = 0; r < shapeRings.length; r++) {
|
|
276
|
+
const ring = shapeRings[r];
|
|
277
|
+
if (ring.length < 3) continue;
|
|
278
|
+
let finite = true;
|
|
279
|
+
let max = maxAbs;
|
|
280
|
+
for (let i = 0, n = ring.length; i < n; i++) {
|
|
281
|
+
const x = +ring[i][0];
|
|
282
|
+
const y = +ring[i][1];
|
|
283
|
+
if (!(Number.isFinite(x) && Number.isFinite(y))) {
|
|
284
|
+
finite = false;
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
const ax = x < 0 ? -x : x;
|
|
288
|
+
const ay = y < 0 ? -y : y;
|
|
289
|
+
if (ax > max) max = ax;
|
|
290
|
+
if (ay > max) max = ay;
|
|
291
|
+
}
|
|
292
|
+
if (!finite) {
|
|
293
|
+
Util.warn("Path skipped a ring with a non-finite coordinate");
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
maxAbs = max;
|
|
297
|
+
rings.push(ring);
|
|
298
|
+
ringShape.push(s);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (rings.length === 0 || maxAbs === 0) return false;
|
|
302
|
+
this.tol = maxAbs * 1e-6;
|
|
303
|
+
this.rounding = maxAbs * Number.EPSILON * 8;
|
|
304
|
+
|
|
305
|
+
// second pass: snap each ring's points to vertices, dropping consecutive
|
|
306
|
+
// repeats and the closing repeat, then emit its edges
|
|
307
|
+
const ids: number[] = [];
|
|
308
|
+
for (let r = 0; r < rings.length; r++) {
|
|
309
|
+
const ring = rings[r];
|
|
310
|
+
ids.length = 0;
|
|
311
|
+
let prev = -1;
|
|
312
|
+
for (let i = 0, n = ring.length; i < n; i++) {
|
|
313
|
+
const v = this.vertexAt(+ring[i][0], +ring[i][1]);
|
|
314
|
+
if (v !== prev) {
|
|
315
|
+
ids.push(v);
|
|
316
|
+
prev = v;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
while (ids.length > 1 && ids[ids.length - 1] === ids[0]) ids.pop();
|
|
320
|
+
if (ids.length >= 3) {
|
|
321
|
+
for (let i = 0, n = ids.length; i < n; i++) {
|
|
322
|
+
this.ea.push(ids[i]);
|
|
323
|
+
this.eb.push(ids[i === n - 1 ? 0 : i + 1]);
|
|
324
|
+
this.es.push(ringShape[r]);
|
|
325
|
+
}
|
|
326
|
+
} else if (distinctPoints(ring) >= 3) {
|
|
327
|
+
Util.warn(
|
|
328
|
+
"Path dropped a ring whose points merge within the tolerance; use local coordinates for small shapes at large offsets",
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return this.ea.length > 0;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** The vertex within `tol` of (x, y), or a new one. */
|
|
336
|
+
vertexAt(x: number, y: number, tol = this.tol): number {
|
|
337
|
+
const cellSize = this.tol * CELL_TOLS;
|
|
338
|
+
const fx = x / cellSize;
|
|
339
|
+
const fy = y / cellSize;
|
|
340
|
+
const cx = Math.floor(fx);
|
|
341
|
+
const cy = Math.floor(fy);
|
|
342
|
+
// neighbors to check: only across a border closer than tol
|
|
343
|
+
const border = tol / cellSize;
|
|
344
|
+
const x0 = fx - cx <= border ? -1 : 0;
|
|
345
|
+
const x1 = fx - cx >= 1 - border ? 1 : 0;
|
|
346
|
+
const y0 = fy - cy <= border ? -1 : 0;
|
|
347
|
+
const y1 = fy - cy >= 1 - border ? 1 : 0;
|
|
348
|
+
const t2 = tol * tol;
|
|
349
|
+
for (let i = x0; i <= x1; i++) {
|
|
350
|
+
for (let j = y0; j <= y1; j++) {
|
|
351
|
+
const cell = this.cells.get(
|
|
352
|
+
(cx + i + CELL_OFFSET) * CELL_SPAN + (cy + j + CELL_OFFSET),
|
|
353
|
+
);
|
|
354
|
+
if (!cell) continue;
|
|
355
|
+
for (let n = 0; n < cell.length; n++) {
|
|
356
|
+
const v = cell[n];
|
|
357
|
+
const dx = this.vx[v] - x;
|
|
358
|
+
const dy = this.vy[v] - y;
|
|
359
|
+
if (dx * dx + dy * dy <= t2) return v;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const id = this.vx.length;
|
|
364
|
+
this.vx.push(x);
|
|
365
|
+
this.vy.push(y);
|
|
366
|
+
const key = (cx + CELL_OFFSET) * CELL_SPAN + (cy + CELL_OFFSET);
|
|
367
|
+
const cell = this.cells.get(key);
|
|
368
|
+
if (cell) cell.push(id);
|
|
369
|
+
else this.cells.set(key, [id]);
|
|
370
|
+
return id;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ------------------------------------------------------------ splits
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Split edges where they meet, in two phases. First, a vertex within tol of
|
|
377
|
+
* an edge's interior splits that edge there, so T-junctions, shared and
|
|
378
|
+
* overlapping edges, and vertices that miss an edge by rounding all become
|
|
379
|
+
* shared vertices. That bends edges by up to tol, which is why it happens
|
|
380
|
+
* before any crossing is computed. The second phase finds the proper
|
|
381
|
+
* crossings of the resulting sub-edges with exact predicates, so no
|
|
382
|
+
* crossing is decided on geometry that a later split would change.
|
|
383
|
+
*/
|
|
384
|
+
split(): void {
|
|
385
|
+
let boxes = this.boxes();
|
|
386
|
+
const pairs = this.sweep(boxes, this.tol, true);
|
|
387
|
+
if (this.spE.length > 0) {
|
|
388
|
+
// the sub-edges are new edges; enumerate their pairs again, exactly
|
|
389
|
+
this.applySplits();
|
|
390
|
+
boxes = this.boxes();
|
|
391
|
+
this.sweep(boxes, 0, false);
|
|
392
|
+
} else if (pairs) {
|
|
393
|
+
for (let i = 0; i < pairs.length; i += 2) {
|
|
394
|
+
this.crossPair(pairs[i], pairs[i + 1]);
|
|
395
|
+
}
|
|
396
|
+
} else {
|
|
397
|
+
this.sweep(boxes, 0, false);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Bounding boxes of the current edges, with the edges ordered by their left side. */
|
|
402
|
+
private boxes(): SweepBoxes {
|
|
403
|
+
const E = this.ea.length;
|
|
404
|
+
const vx = this.vx;
|
|
405
|
+
const vy = this.vy;
|
|
406
|
+
const minX = new Float64Array(E);
|
|
407
|
+
const maxX = new Float64Array(E);
|
|
408
|
+
const minY = new Float64Array(E);
|
|
409
|
+
const maxY = new Float64Array(E);
|
|
410
|
+
for (let e = 0; e < E; e++) {
|
|
411
|
+
const ax = vx[this.ea[e]];
|
|
412
|
+
const ay = vy[this.ea[e]];
|
|
413
|
+
const bx = vx[this.eb[e]];
|
|
414
|
+
const by = vy[this.eb[e]];
|
|
415
|
+
minX[e] = ax < bx ? ax : bx;
|
|
416
|
+
maxX[e] = ax < bx ? bx : ax;
|
|
417
|
+
minY[e] = ay < by ? ay : by;
|
|
418
|
+
maxY[e] = ay < by ? by : ay;
|
|
419
|
+
}
|
|
420
|
+
const order = new Uint32Array(E);
|
|
421
|
+
for (let e = 0; e < E; e++) order[e] = e;
|
|
422
|
+
order.sort((p, q) => minX[p] - minX[q]);
|
|
423
|
+
return { minX, maxX, minY, maxY, order };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Run a phase over every two edges whose boxes, grown by `margin`, overlap:
|
|
428
|
+
* the touch phase (which also returns the pairs, unless there are more than
|
|
429
|
+
* the buffer holds) or the crossing phase. Pairs are visited as they are
|
|
430
|
+
* found rather than listed first: shapes that all share a vertex have
|
|
431
|
+
* quadratically many candidate pairs, more than an array can hold.
|
|
432
|
+
*/
|
|
433
|
+
private sweep(
|
|
434
|
+
boxes: SweepBoxes,
|
|
435
|
+
margin: number,
|
|
436
|
+
touching: boolean,
|
|
437
|
+
): number[] | undefined {
|
|
438
|
+
const { minX, maxX, minY, maxY, order } = boxes;
|
|
439
|
+
let pairs: number[] | undefined = touching ? [] : undefined;
|
|
440
|
+
for (let i = 0, E = order.length; i < E; i++) {
|
|
441
|
+
const e1 = order[i];
|
|
442
|
+
const limit = maxX[e1] + margin;
|
|
443
|
+
const lo = minY[e1] - margin;
|
|
444
|
+
const hi = maxY[e1] + margin;
|
|
445
|
+
for (let j = i + 1; j < E; j++) {
|
|
446
|
+
const e2 = order[j];
|
|
447
|
+
if (minX[e2] > limit) break;
|
|
448
|
+
if (minY[e2] > hi || maxY[e2] < lo) continue;
|
|
449
|
+
if (touching) {
|
|
450
|
+
this.touchPair(e1, e2, boxes);
|
|
451
|
+
if (pairs) {
|
|
452
|
+
if (pairs.length < 2 * PAIR_BUFFER) pairs.push(e1, e2);
|
|
453
|
+
else pairs = undefined;
|
|
454
|
+
}
|
|
455
|
+
} else {
|
|
456
|
+
this.crossPair(e1, e2);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return pairs;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** Phase one: each endpoint of one edge within tol of the other edge's interior splits it. */
|
|
464
|
+
private touchPair(e1: number, e2: number, boxes: SweepBoxes): void {
|
|
465
|
+
const a = this.ea[e1];
|
|
466
|
+
const b = this.eb[e1];
|
|
467
|
+
const c = this.ea[e2];
|
|
468
|
+
const d = this.eb[e2];
|
|
469
|
+
// an endpoint can only touch an edge whose box it is in (grown by tol)
|
|
470
|
+
if (this.inBox(a, e2, boxes)) this.touch(e2, a, c, d);
|
|
471
|
+
if (this.inBox(b, e2, boxes)) this.touch(e2, b, c, d);
|
|
472
|
+
if (this.inBox(c, e1, boxes)) this.touch(e1, c, a, b);
|
|
473
|
+
if (this.inBox(d, e1, boxes)) this.touch(e1, d, a, b);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
private inBox(v: number, e: number, boxes: SweepBoxes): boolean {
|
|
477
|
+
const x = this.vx[v];
|
|
478
|
+
const y = this.vy[v];
|
|
479
|
+
const tol = this.tol;
|
|
480
|
+
return (
|
|
481
|
+
x >= boxes.minX[e] - tol &&
|
|
482
|
+
x <= boxes.maxX[e] + tol &&
|
|
483
|
+
y >= boxes.minY[e] - tol &&
|
|
484
|
+
y <= boxes.maxY[e] + tol
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** Register vertex `v` on edge `e` (from `p` to `q`) when it lies within tol of the edge's interior. */
|
|
489
|
+
private touch(e: number, v: number, p: number, q: number): void {
|
|
490
|
+
if (v === p || v === q) return;
|
|
491
|
+
const vx = this.vx;
|
|
492
|
+
const vy = this.vy;
|
|
493
|
+
const px = vx[p];
|
|
494
|
+
const py = vy[p];
|
|
495
|
+
const rx = vx[q] - px;
|
|
496
|
+
const ry = vy[q] - py;
|
|
497
|
+
const len2 = rx * rx + ry * ry;
|
|
498
|
+
const dx = vx[v] - px;
|
|
499
|
+
const dy = vy[v] - py;
|
|
500
|
+
const t = (dx * rx + dy * ry) / len2;
|
|
501
|
+
if (t <= 0 || t >= 1) return;
|
|
502
|
+
const cross = rx * dy - ry * dx;
|
|
503
|
+
if (cross * cross > this.tol * this.tol * len2) return;
|
|
504
|
+
this.registerSplit(e, v);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/** Phase two: split two sub-edges that share no vertex at their proper crossing. */
|
|
508
|
+
private crossPair(e1: number, e2: number): void {
|
|
509
|
+
const a = this.ea[e1];
|
|
510
|
+
const b = this.eb[e1];
|
|
511
|
+
const c = this.ea[e2];
|
|
512
|
+
const d = this.eb[e2];
|
|
513
|
+
if (a === c || a === d || b === c || b === d) return;
|
|
514
|
+
|
|
515
|
+
// Exact signs keep shallow crossings distinct from collinear overlaps.
|
|
516
|
+
const vx = this.vx;
|
|
517
|
+
const vy = this.vy;
|
|
518
|
+
const ax = vx[a];
|
|
519
|
+
const ay = vy[a];
|
|
520
|
+
const bx = vx[b];
|
|
521
|
+
const by = vy[b];
|
|
522
|
+
const cx = vx[c];
|
|
523
|
+
const cy = vy[c];
|
|
524
|
+
const dx = vx[d];
|
|
525
|
+
const dy = vy[d];
|
|
526
|
+
if (
|
|
527
|
+
orient2d(ax, ay, bx, by, cx, cy) * orient2d(ax, ay, bx, by, dx, dy) >=
|
|
528
|
+
0
|
|
529
|
+
)
|
|
530
|
+
return;
|
|
531
|
+
if (
|
|
532
|
+
orient2d(cx, cy, dx, dy, ax, ay) * orient2d(cx, cy, dx, dy, bx, by) >=
|
|
533
|
+
0
|
|
534
|
+
)
|
|
535
|
+
return;
|
|
536
|
+
const t = crossingParameter(ax, ay, bx, by, cx, cy, dx, dy);
|
|
537
|
+
const v = this.vertexAt(
|
|
538
|
+
ax + t * (bx - ax),
|
|
539
|
+
ay + t * (by - ay),
|
|
540
|
+
this.rounding,
|
|
541
|
+
);
|
|
542
|
+
this.registerSplit(e1, v);
|
|
543
|
+
this.registerSplit(e2, v);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/** Replace every edge by its pieces between the registered split vertices, in order. */
|
|
547
|
+
private applySplits(): void {
|
|
548
|
+
const E = this.ea.length;
|
|
549
|
+
const S = this.spE.length;
|
|
550
|
+
const spE = this.spE;
|
|
551
|
+
const spT = this.spT;
|
|
552
|
+
const spV = this.spV;
|
|
553
|
+
const order = new Uint32Array(S);
|
|
554
|
+
for (let i = 0; i < S; i++) order[i] = i;
|
|
555
|
+
order.sort((p, q) => spE[p] - spE[q] || spT[p] - spT[q]);
|
|
556
|
+
const ea: number[] = [];
|
|
557
|
+
const eb: number[] = [];
|
|
558
|
+
const es: number[] = [];
|
|
559
|
+
let si = 0;
|
|
560
|
+
for (let e = 0; e < E; e++) {
|
|
561
|
+
let u = this.ea[e];
|
|
562
|
+
const end = this.eb[e];
|
|
563
|
+
const s = this.es[e];
|
|
564
|
+
while (si < S && spE[order[si]] === e) {
|
|
565
|
+
const v = spV[order[si++]];
|
|
566
|
+
if (v === u) continue;
|
|
567
|
+
ea.push(u);
|
|
568
|
+
eb.push(v);
|
|
569
|
+
es.push(s);
|
|
570
|
+
u = v;
|
|
571
|
+
}
|
|
572
|
+
if (u !== end) {
|
|
573
|
+
ea.push(u);
|
|
574
|
+
eb.push(end);
|
|
575
|
+
es.push(s);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
this.ea = ea;
|
|
579
|
+
this.eb = eb;
|
|
580
|
+
this.es = es;
|
|
581
|
+
spE.length = 0;
|
|
582
|
+
spT.length = 0;
|
|
583
|
+
spV.length = 0;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/** Record that edge `e` passes through vertex `v`. The parameter is always the projection, so repeats sort together. */
|
|
587
|
+
private registerSplit(e: number, v: number): void {
|
|
588
|
+
const a = this.ea[e];
|
|
589
|
+
const b = this.eb[e];
|
|
590
|
+
if (v === a || v === b) return;
|
|
591
|
+
const ax = this.vx[a];
|
|
592
|
+
const ay = this.vy[a];
|
|
593
|
+
const rx = this.vx[b] - ax;
|
|
594
|
+
const ry = this.vy[b] - ay;
|
|
595
|
+
const t =
|
|
596
|
+
((this.vx[v] - ax) * rx + (this.vy[v] - ay) * ry) / (rx * rx + ry * ry);
|
|
597
|
+
if (t <= 0 || t >= 1) return;
|
|
598
|
+
this.spE.push(e);
|
|
599
|
+
this.spT.push(t);
|
|
600
|
+
this.spV.push(v);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// ------------------------------------------------------------- graph
|
|
604
|
+
|
|
605
|
+
/** Turn the split edges into unique graph edges with per-shape deltas; drop edges that separate nothing. */
|
|
606
|
+
merge(): void {
|
|
607
|
+
if (this.spE.length > 0) this.applySplits();
|
|
608
|
+
const V = this.vx.length;
|
|
609
|
+
const index = new Map<number, number>();
|
|
610
|
+
const gu = this.gu;
|
|
611
|
+
const gv = this.gv;
|
|
612
|
+
const deltas = this.deltas;
|
|
613
|
+
for (let e = 0, E = this.ea.length; e < E; e++) {
|
|
614
|
+
const u = this.ea[e];
|
|
615
|
+
const v = this.eb[e];
|
|
616
|
+
const lo = u < v ? u : v;
|
|
617
|
+
const hi = u < v ? v : u;
|
|
618
|
+
const key = lo * V + hi;
|
|
619
|
+
let g = index.get(key);
|
|
620
|
+
if (g === undefined) {
|
|
621
|
+
g = gu.length;
|
|
622
|
+
gu.push(lo);
|
|
623
|
+
gv.push(hi);
|
|
624
|
+
deltas.push(new Map());
|
|
625
|
+
index.set(key, g);
|
|
626
|
+
}
|
|
627
|
+
addWinding(deltas[g], this.es[e], u === lo ? 1 : -1);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// compact away edges whose deltas are all zero
|
|
631
|
+
let n = 0;
|
|
632
|
+
for (let e = 0, len = gu.length; e < len; e++) {
|
|
633
|
+
if (deltas[e].size === 0) continue;
|
|
634
|
+
if (n !== e) {
|
|
635
|
+
gu[n] = gu[e];
|
|
636
|
+
gv[n] = gv[e];
|
|
637
|
+
deltas[n] = deltas[e];
|
|
638
|
+
}
|
|
639
|
+
n++;
|
|
640
|
+
}
|
|
641
|
+
gu.length = n;
|
|
642
|
+
gv.length = n;
|
|
643
|
+
deltas.length = n;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/** The ray tree once enough queries justify building it; until then callers scan every edge. */
|
|
647
|
+
private rayTree(): RayNode | null | undefined {
|
|
648
|
+
return this.rayIndex;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/** Build the ray tree if the coming `queries` are more than scanning is worth. */
|
|
652
|
+
private expectRayQueries(queries: number): void {
|
|
653
|
+
const E = this.gu.length;
|
|
654
|
+
if (
|
|
655
|
+
this.rayIndex === undefined &&
|
|
656
|
+
queries > RAY_TREE_PER_LOG * Math.log2(E + 1) + 1
|
|
657
|
+
) {
|
|
658
|
+
this.rayIndex = this.indexRays() ?? null;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/** Balanced bounding-box tree for seed winding and nearest boundary queries. */
|
|
663
|
+
private indexRays(): RayNode | undefined {
|
|
664
|
+
const E = this.gu.length;
|
|
665
|
+
const vx = this.vx;
|
|
666
|
+
const vy = this.vy;
|
|
667
|
+
let n = 0;
|
|
668
|
+
const edges = new Uint32Array(E);
|
|
669
|
+
for (let e = 0; e < E; e++) {
|
|
670
|
+
if (vy[this.gu[e]] !== vy[this.gv[e]]) edges[n++] = e;
|
|
671
|
+
}
|
|
672
|
+
if (n === 0) return undefined;
|
|
673
|
+
// edge centers, so each level partitions by a plain number
|
|
674
|
+
const cx = new Float64Array(E);
|
|
675
|
+
const cy = new Float64Array(E);
|
|
676
|
+
for (let e = 0; e < E; e++) {
|
|
677
|
+
cx[e] = vx[this.gu[e]] + vx[this.gv[e]];
|
|
678
|
+
cy[e] = vy[this.gu[e]] + vy[this.gv[e]];
|
|
679
|
+
}
|
|
680
|
+
const build = (lo: number, hi: number): RayNode => {
|
|
681
|
+
const node: RayNode = {
|
|
682
|
+
x0: Infinity,
|
|
683
|
+
x1: -Infinity,
|
|
684
|
+
y0: Infinity,
|
|
685
|
+
y1: -Infinity,
|
|
686
|
+
allY0: -Infinity,
|
|
687
|
+
allY1: Infinity,
|
|
688
|
+
sum: new Map(),
|
|
689
|
+
};
|
|
690
|
+
for (let i = lo; i < hi; i++) {
|
|
691
|
+
const e = edges[i];
|
|
692
|
+
const u = this.gu[e];
|
|
693
|
+
const v = this.gv[e];
|
|
694
|
+
const y0 = Math.min(vy[u], vy[v]);
|
|
695
|
+
const y1 = Math.max(vy[u], vy[v]);
|
|
696
|
+
node.x0 = Math.min(node.x0, vx[u], vx[v]);
|
|
697
|
+
node.x1 = Math.max(node.x1, vx[u], vx[v]);
|
|
698
|
+
node.y0 = Math.min(node.y0, y0);
|
|
699
|
+
node.y1 = Math.max(node.y1, y1);
|
|
700
|
+
node.allY0 = Math.max(node.allY0, y0);
|
|
701
|
+
node.allY1 = Math.min(node.allY1, y1);
|
|
702
|
+
}
|
|
703
|
+
if (hi - lo <= 8) {
|
|
704
|
+
node.edges = Array.from(edges.subarray(lo, hi));
|
|
705
|
+
if (node.allY0 < node.allY1) {
|
|
706
|
+
for (const e of node.edges) {
|
|
707
|
+
const sign = vy[this.gv[e]] > vy[this.gu[e]] ? -1 : 1;
|
|
708
|
+
for (const [s, d] of this.deltas[e])
|
|
709
|
+
addWinding(node.sum, s, sign * d);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
} else {
|
|
713
|
+
const key = node.x1 - node.x0 >= node.y1 - node.y0 ? cx : cy;
|
|
714
|
+
const mid = (lo + hi) >> 1;
|
|
715
|
+
selectByKey(edges, key, lo, hi, mid);
|
|
716
|
+
node.left = build(lo, mid);
|
|
717
|
+
node.right = build(mid, hi);
|
|
718
|
+
if (node.allY0 < node.allY1) {
|
|
719
|
+
for (const child of [node.left, node.right])
|
|
720
|
+
for (const [s, d] of child.sum) addWinding(node.sum, s, d);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
return node;
|
|
724
|
+
};
|
|
725
|
+
return build(0, n);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// ------------------------------------------------------------- faces
|
|
729
|
+
|
|
730
|
+
/** Order the half-edges around each vertex and trace the cycles that bound the faces. */
|
|
731
|
+
trace(): void {
|
|
732
|
+
const E = this.gu.length;
|
|
733
|
+
const H = 2 * E;
|
|
734
|
+
const V = this.vx.length;
|
|
735
|
+
const vx = this.vx;
|
|
736
|
+
const vy = this.vy;
|
|
737
|
+
const gu = this.gu;
|
|
738
|
+
const gv = this.gv;
|
|
739
|
+
|
|
740
|
+
const angle = new Float64Array(H);
|
|
741
|
+
for (let e = 0; e < E; e++) {
|
|
742
|
+
const dx = vx[gv[e]] - vx[gu[e]];
|
|
743
|
+
const dy = vy[gv[e]] - vy[gu[e]];
|
|
744
|
+
angle[2 * e] = pseudoAngle(dx, dy);
|
|
745
|
+
angle[2 * e + 1] = pseudoAngle(-dx, -dy);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const offset = new Int32Array(V + 1);
|
|
749
|
+
for (let e = 0; e < E; e++) {
|
|
750
|
+
offset[gu[e] + 1]++;
|
|
751
|
+
offset[gv[e] + 1]++;
|
|
752
|
+
}
|
|
753
|
+
for (let v = 0; v < V; v++) offset[v + 1] += offset[v];
|
|
754
|
+
const out = new Int32Array(H);
|
|
755
|
+
const cursor = offset.slice(0, V);
|
|
756
|
+
for (let e = 0; e < E; e++) {
|
|
757
|
+
out[cursor[gu[e]]++] = 2 * e;
|
|
758
|
+
out[cursor[gv[e]]++] = 2 * e + 1;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// counterclockwise order around each vertex; the lists are tiny, so an
|
|
762
|
+
// insertion sort in place beats extracting them
|
|
763
|
+
const slot = new Int32Array(H);
|
|
764
|
+
for (let v = 0; v < V; v++) {
|
|
765
|
+
const lo = offset[v];
|
|
766
|
+
const hi = offset[v + 1];
|
|
767
|
+
for (let i = lo + 1; i < hi; i++) {
|
|
768
|
+
const h = out[i];
|
|
769
|
+
const a = angle[h];
|
|
770
|
+
let j = i - 1;
|
|
771
|
+
while (
|
|
772
|
+
j >= lo &&
|
|
773
|
+
(angle[out[j]] > a ||
|
|
774
|
+
(angle[out[j]] === a && this.target(out[j]) > this.target(h)))
|
|
775
|
+
) {
|
|
776
|
+
out[j + 1] = out[j];
|
|
777
|
+
j--;
|
|
778
|
+
}
|
|
779
|
+
out[j + 1] = h;
|
|
780
|
+
}
|
|
781
|
+
for (let i = lo; i < hi; i++) slot[out[i]] = i - lo;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// the face on the left of h continues along the first outgoing half-edge
|
|
785
|
+
// clockwise from h's twin
|
|
786
|
+
const next = new Int32Array(H);
|
|
787
|
+
for (let h = 0; h < H; h++) {
|
|
788
|
+
const v = this.target(h);
|
|
789
|
+
const base = offset[v];
|
|
790
|
+
const n = offset[v + 1] - base;
|
|
791
|
+
const s = slot[h ^ 1];
|
|
792
|
+
next[h] = out[base + (s === 0 ? n - 1 : s - 1)];
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const cycle = new Int32Array(H).fill(-1);
|
|
796
|
+
const cycleArea = this.cycleArea;
|
|
797
|
+
const cycleStart = this.cycleStart;
|
|
798
|
+
for (let h = 0; h < H; h++) {
|
|
799
|
+
if (cycle[h] >= 0) continue;
|
|
800
|
+
const c = cycleArea.length;
|
|
801
|
+
let area = 0;
|
|
802
|
+
let cur = h;
|
|
803
|
+
do {
|
|
804
|
+
cycle[cur] = c;
|
|
805
|
+
const o = this.origin(cur);
|
|
806
|
+
const t = this.target(cur);
|
|
807
|
+
area += vx[o] * vy[t] - vx[t] * vy[o];
|
|
808
|
+
cur = next[cur];
|
|
809
|
+
} while (cur !== h);
|
|
810
|
+
cycleArea.push(area / 2);
|
|
811
|
+
cycleStart.push(h);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
this.offset = offset;
|
|
815
|
+
this.out = out;
|
|
816
|
+
this.slot = slot;
|
|
817
|
+
this.next = next;
|
|
818
|
+
this.cycle = cycle;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Label every cycle with what the modes need to know about the winding
|
|
823
|
+
* numbers on its face. With `full`, the sparse winding vectors are kept too.
|
|
824
|
+
*/
|
|
825
|
+
label(full = false): void {
|
|
826
|
+
const V = this.vx.length;
|
|
827
|
+
const E = this.gu.length;
|
|
828
|
+
const C = this.cycleArea.length;
|
|
829
|
+
const vx = this.vx;
|
|
830
|
+
const vy = this.vy;
|
|
831
|
+
const offset = this.offset;
|
|
832
|
+
|
|
833
|
+
// connected components by union-find over the graph edges
|
|
834
|
+
const parent = new Int32Array(V);
|
|
835
|
+
for (let v = 0; v < V; v++) parent[v] = v;
|
|
836
|
+
const find = (v: number) => {
|
|
837
|
+
while (parent[v] !== v) {
|
|
838
|
+
parent[v] = parent[parent[v]];
|
|
839
|
+
v = parent[v];
|
|
840
|
+
}
|
|
841
|
+
return v;
|
|
842
|
+
};
|
|
843
|
+
for (let e = 0; e < E; e++) {
|
|
844
|
+
const a = find(this.gu[e]);
|
|
845
|
+
const b = find(this.gv[e]);
|
|
846
|
+
if (a !== b) parent[a] = b;
|
|
847
|
+
}
|
|
848
|
+
const leftmost = new Int32Array(V).fill(-1);
|
|
849
|
+
let components = 0;
|
|
850
|
+
for (let v = 0; v < V; v++) {
|
|
851
|
+
if (offset[v + 1] === offset[v]) continue; // no edges left
|
|
852
|
+
const r = find(v);
|
|
853
|
+
const l = leftmost[r];
|
|
854
|
+
if (l < 0) components++;
|
|
855
|
+
if (l < 0 || vx[v] < vx[l] || (vx[v] === vx[l] && vy[v] < vy[l])) {
|
|
856
|
+
leftmost[r] = v;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
const count = new Int32Array(C);
|
|
861
|
+
const hasFirst = new Uint8Array(C);
|
|
862
|
+
const hasLast = new Uint8Array(C);
|
|
863
|
+
const labels = full ? new Array<Map<number, number>>(C) : undefined;
|
|
864
|
+
const labeled = new Uint8Array(C);
|
|
865
|
+
const last = this.k - 1;
|
|
866
|
+
// One winding vector, edited on the way into a neighboring face and
|
|
867
|
+
// restored on the way back, so the memory is one vector plus three
|
|
868
|
+
// numbers per face rather than a vector per face.
|
|
869
|
+
const w = new Map<number, number>();
|
|
870
|
+
const record = (c: number) => {
|
|
871
|
+
count[c] = w.size;
|
|
872
|
+
hasFirst[c] = w.has(0) ? 1 : 0;
|
|
873
|
+
hasLast[c] = w.has(last) ? 1 : 0;
|
|
874
|
+
if (labels) labels[c] = new Map(w);
|
|
875
|
+
labeled[c] = 1;
|
|
876
|
+
};
|
|
877
|
+
const step = (h: number, dir: number) => {
|
|
878
|
+
const sign = (h & 1 ? 1 : -1) * dir;
|
|
879
|
+
for (const [s, d] of this.deltas[h >> 1]) addWinding(w, s, sign * d);
|
|
880
|
+
};
|
|
881
|
+
// depth-first frames: the cycle, the next half-edge to look across, and
|
|
882
|
+
// the half-edge that led into the cycle
|
|
883
|
+
const fc = new Int32Array(C);
|
|
884
|
+
const fh = new Int32Array(C);
|
|
885
|
+
const fvia = new Int32Array(C);
|
|
886
|
+
if (components > 1) this.expectRayQueries(components);
|
|
887
|
+
for (let r = 0; r < V; r++) {
|
|
888
|
+
const p = leftmost[r];
|
|
889
|
+
if (p < 0) continue;
|
|
890
|
+
// Every edge at the leftmost vertex points into the east half-plane, so
|
|
891
|
+
// the outgoing half-edge with the largest angle has the component's
|
|
892
|
+
// unbounded face on its left. Its winding numbers are exactly the
|
|
893
|
+
// graph's crossings of the ray going west from here.
|
|
894
|
+
const g = this.out[offset[p + 1] - 1];
|
|
895
|
+
const c0 = this.cycle[g];
|
|
896
|
+
if (labeled[c0]) continue;
|
|
897
|
+
w.clear();
|
|
898
|
+
// A connected graph has no other component around its unbounded face.
|
|
899
|
+
if (components > 1) this.windingWest(p, w);
|
|
900
|
+
record(c0);
|
|
901
|
+
let depth = 0;
|
|
902
|
+
fc[0] = c0;
|
|
903
|
+
fh[0] = this.cycleStart[c0];
|
|
904
|
+
fvia[0] = -1;
|
|
905
|
+
while (depth >= 0) {
|
|
906
|
+
const h = fh[depth];
|
|
907
|
+
if (h < 0) {
|
|
908
|
+
// every neighbor of this cycle is labeled: restore and back out
|
|
909
|
+
if (fvia[depth] >= 0) step(fvia[depth], -1);
|
|
910
|
+
depth--;
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
913
|
+
const hn = this.next[h];
|
|
914
|
+
fh[depth] = hn === this.cycleStart[fc[depth]] ? -1 : hn;
|
|
915
|
+
const c2 = this.cycle[h ^ 1];
|
|
916
|
+
if (labeled[c2]) continue;
|
|
917
|
+
step(h, 1);
|
|
918
|
+
record(c2);
|
|
919
|
+
depth++;
|
|
920
|
+
fc[depth] = c2;
|
|
921
|
+
fh[depth] = this.cycleStart[c2];
|
|
922
|
+
fvia[depth] = h;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
this.count = count;
|
|
926
|
+
this.hasFirst = hasFirst;
|
|
927
|
+
this.hasLast = hasLast;
|
|
928
|
+
this.labels = labels;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/** Add the winding numbers just west of vertex p into a sparse label. */
|
|
932
|
+
private windingWest(p: number, labels: Map<number, number>): void {
|
|
933
|
+
const px = this.vx[p];
|
|
934
|
+
const py = this.vy[p];
|
|
935
|
+
const gu = this.gu;
|
|
936
|
+
const gv = this.gv;
|
|
937
|
+
const vy = this.vy;
|
|
938
|
+
const deltas = this.deltas;
|
|
939
|
+
const cross = (e: number) => {
|
|
940
|
+
const u = gu[e];
|
|
941
|
+
const v = gv[e];
|
|
942
|
+
if (u === p || v === p) return; // meets the ray at p itself
|
|
943
|
+
const uy = vy[u];
|
|
944
|
+
const wy = vy[v];
|
|
945
|
+
if (uy > py === wy > py) return;
|
|
946
|
+
if (this.crossingX(u, v, py) >= px) return;
|
|
947
|
+
// a ring winding counterclockwise around p crosses the west ray downward
|
|
948
|
+
const sign = wy > uy ? -1 : 1;
|
|
949
|
+
for (const [s, d] of deltas[e]) addWinding(labels, s, sign * d);
|
|
950
|
+
};
|
|
951
|
+
const tree = this.rayTree();
|
|
952
|
+
if (tree === undefined) {
|
|
953
|
+
for (let e = 0, E = this.gu.length; e < E; e++) cross(e);
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
const stack = tree ? [tree] : [];
|
|
957
|
+
while (stack.length > 0) {
|
|
958
|
+
const node = stack.pop()!;
|
|
959
|
+
if (node.x0 >= px || py < node.y0 || py >= node.y1) continue;
|
|
960
|
+
if (node.x1 < px && py >= node.allY0 && py < node.allY1) {
|
|
961
|
+
for (const [s, d] of node.sum) addWinding(labels, s, d);
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
if (!node.edges) {
|
|
965
|
+
stack.push(node.left!, node.right!);
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
for (const e of node.edges) cross(e);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* Where edge u→v crosses the horizontal line at y, given that it straddles it
|
|
974
|
+
* by the half-open rule. An endpoint on the line is the crossing itself, taken
|
|
975
|
+
* exactly so that ties at a vertex are exact ties.
|
|
976
|
+
*/
|
|
977
|
+
private crossingX(u: number, v: number, y: number): number {
|
|
978
|
+
const uy = this.vy[u];
|
|
979
|
+
const wy = this.vy[v];
|
|
980
|
+
if (uy === y) return this.vx[u];
|
|
981
|
+
if (wy === y) return this.vx[v];
|
|
982
|
+
const ux = this.vx[u];
|
|
983
|
+
return ux + ((y - uy) * (this.vx[v] - ux)) / (wy - uy);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// ----------------------------------------------------------- selection
|
|
987
|
+
|
|
988
|
+
/** Which cycles bound kept faces (on their left) for a mode. */
|
|
989
|
+
select(mode: PathMode): Uint8Array {
|
|
990
|
+
const C = this.cycleArea.length;
|
|
991
|
+
const k = this.k;
|
|
992
|
+
const keep = new Uint8Array(C);
|
|
993
|
+
for (let c = 0; c < C; c++) {
|
|
994
|
+
const count = this.count[c];
|
|
995
|
+
const first = this.hasFirst[c] === 1;
|
|
996
|
+
const last = this.hasLast[c] === 1;
|
|
997
|
+
let on = false;
|
|
998
|
+
switch (mode) {
|
|
999
|
+
case "intersect":
|
|
1000
|
+
on = count === k;
|
|
1001
|
+
break;
|
|
1002
|
+
case "exclude":
|
|
1003
|
+
on = (count & 1) === 1;
|
|
1004
|
+
break;
|
|
1005
|
+
case "minusFront":
|
|
1006
|
+
on = first && count === 1;
|
|
1007
|
+
break;
|
|
1008
|
+
case "minusBack":
|
|
1009
|
+
on = last && count === 1;
|
|
1010
|
+
break;
|
|
1011
|
+
case "crop":
|
|
1012
|
+
on = last && count >= 2;
|
|
1013
|
+
break;
|
|
1014
|
+
default: // unite, divide
|
|
1015
|
+
on = count > 0;
|
|
1016
|
+
}
|
|
1017
|
+
keep[c] = on ? 1 : 0;
|
|
1018
|
+
}
|
|
1019
|
+
return keep;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
/**
|
|
1023
|
+
* Rings of the merged kept region: the half-edges with a kept face on the
|
|
1024
|
+
* left and an unkept face on the right, traced with the same turning rule
|
|
1025
|
+
* restricted to those half-edges.
|
|
1026
|
+
*/
|
|
1027
|
+
mergedRings(keep: Uint8Array, ringOf: Int32Array): number[][] {
|
|
1028
|
+
const H = ringOf.length;
|
|
1029
|
+
const cycle = this.cycle;
|
|
1030
|
+
const boundary = (h: number) =>
|
|
1031
|
+
keep[cycle[h]] === 1 && keep[cycle[h ^ 1]] === 0;
|
|
1032
|
+
const rings: number[][] = [];
|
|
1033
|
+
for (let h = 0; h < H; h++) {
|
|
1034
|
+
if (ringOf[h] >= 0 || !boundary(h)) continue;
|
|
1035
|
+
const r = rings.length;
|
|
1036
|
+
const seq: number[] = [];
|
|
1037
|
+
let cur = h;
|
|
1038
|
+
do {
|
|
1039
|
+
ringOf[cur] = r;
|
|
1040
|
+
seq.push(cur);
|
|
1041
|
+
// first boundary half-edge clockwise from the twin
|
|
1042
|
+
const v = this.target(cur);
|
|
1043
|
+
const base = this.offset[v];
|
|
1044
|
+
const n = this.offset[v + 1] - base;
|
|
1045
|
+
const s = this.slot[cur ^ 1];
|
|
1046
|
+
let found = -1;
|
|
1047
|
+
for (let i = 1; i <= n; i++) {
|
|
1048
|
+
const cand = this.out[base + ((s - i + n) % n)];
|
|
1049
|
+
if (boundary(cand)) {
|
|
1050
|
+
found = cand;
|
|
1051
|
+
break;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
if (found < 0) break; // cannot happen on a planar graph; end the ring
|
|
1055
|
+
cur = found;
|
|
1056
|
+
} while (cur !== h);
|
|
1057
|
+
rings.push(seq);
|
|
1058
|
+
}
|
|
1059
|
+
return rings;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/**
|
|
1063
|
+
* Split each traced walk at repeated vertices into simple rings. A boundary
|
|
1064
|
+
* that touches itself at a vertex (a hole pinched to its outer ring, two
|
|
1065
|
+
* holes meeting, two regions meeting at a corner) is one closed walk; its
|
|
1066
|
+
* simple pieces are separate rings. Returns the walk each ring came from.
|
|
1067
|
+
*/
|
|
1068
|
+
simpleRings(
|
|
1069
|
+
walks: number[][],
|
|
1070
|
+
ringOf: Int32Array,
|
|
1071
|
+
): { rings: number[][]; walk: number[] } {
|
|
1072
|
+
const at = new Int32Array(this.vx.length).fill(-1); // stack position of a vertex
|
|
1073
|
+
const rings: number[][] = [];
|
|
1074
|
+
const walk: number[] = [];
|
|
1075
|
+
const emit = (seq: number[], w: number) => {
|
|
1076
|
+
for (let i = 0; i < seq.length; i++) ringOf[seq[i]] = rings.length;
|
|
1077
|
+
rings.push(seq);
|
|
1078
|
+
walk.push(w);
|
|
1079
|
+
};
|
|
1080
|
+
for (let w = 0; w < walks.length; w++) {
|
|
1081
|
+
const seq = walks[w];
|
|
1082
|
+
const open: number[] = [];
|
|
1083
|
+
for (let i = 0; i < seq.length; i++) {
|
|
1084
|
+
const h = seq[i];
|
|
1085
|
+
const o = this.origin(h);
|
|
1086
|
+
const pos = at[o];
|
|
1087
|
+
if (pos >= 0) {
|
|
1088
|
+
// back at a vertex already on the walk: the part since then is a loop
|
|
1089
|
+
const loop = open.splice(pos);
|
|
1090
|
+
for (let j = 0; j < loop.length; j++) at[this.origin(loop[j])] = -1;
|
|
1091
|
+
emit(loop, w);
|
|
1092
|
+
}
|
|
1093
|
+
at[o] = open.length;
|
|
1094
|
+
open.push(h);
|
|
1095
|
+
}
|
|
1096
|
+
for (let j = 0; j < open.length; j++) at[this.origin(open[j])] = -1;
|
|
1097
|
+
emit(open, w);
|
|
1098
|
+
}
|
|
1099
|
+
return { rings, walk };
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
/** Rings of every kept face, one per cycle. */
|
|
1103
|
+
faceRings(keep: Uint8Array, ringOf: Int32Array): number[][] {
|
|
1104
|
+
const rings: number[][] = [];
|
|
1105
|
+
for (let c = 0, C = this.cycleArea.length; c < C; c++) {
|
|
1106
|
+
if (!keep[c]) continue;
|
|
1107
|
+
const r = rings.length;
|
|
1108
|
+
const seq: number[] = [];
|
|
1109
|
+
const start = this.cycleStart[c];
|
|
1110
|
+
let h = start;
|
|
1111
|
+
do {
|
|
1112
|
+
ringOf[h] = r;
|
|
1113
|
+
seq.push(h);
|
|
1114
|
+
h = this.next[h];
|
|
1115
|
+
} while (h !== start);
|
|
1116
|
+
rings.push(seq);
|
|
1117
|
+
}
|
|
1118
|
+
return rings;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
/**
|
|
1122
|
+
* Group the rings into polygons: each counterclockwise ring followed by the
|
|
1123
|
+
* clockwise rings (holes) that puncture its face. A hole's face is found by
|
|
1124
|
+
* a ray west from its leftmost vertex: the nearest ring edge crossed has
|
|
1125
|
+
* that face on its east side.
|
|
1126
|
+
*/
|
|
1127
|
+
assemble(rings: number[][], ringOf: Int32Array, walk: number[]): Group[][] {
|
|
1128
|
+
const R = rings.length;
|
|
1129
|
+
const vx = this.vx;
|
|
1130
|
+
const vy = this.vy;
|
|
1131
|
+
const area = new Float64Array(R);
|
|
1132
|
+
const left = new Int32Array(R);
|
|
1133
|
+
for (let r = 0; r < R; r++) {
|
|
1134
|
+
const seq = rings[r];
|
|
1135
|
+
let a = 0;
|
|
1136
|
+
let l = this.origin(seq[0]);
|
|
1137
|
+
for (let i = 0; i < seq.length; i++) {
|
|
1138
|
+
const o = this.origin(seq[i]);
|
|
1139
|
+
const t = this.target(seq[i]);
|
|
1140
|
+
a += vx[o] * vy[t] - vx[t] * vy[o];
|
|
1141
|
+
if (vx[o] < vx[l] || (vx[o] === vx[l] && vy[o] < vy[l])) l = o;
|
|
1142
|
+
}
|
|
1143
|
+
area[r] = a / 2;
|
|
1144
|
+
left[r] = l;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
const sliver = this.tol * this.tol;
|
|
1148
|
+
const parent = new Int32Array(R).fill(-2); // -2 unresolved, -1 none
|
|
1149
|
+
|
|
1150
|
+
// A hole split off a pinched walk may have another piece of the walk at
|
|
1151
|
+
// its own leftmost vertex, where a westward ray starts inside that piece
|
|
1152
|
+
// or finds nothing. The pieces of one walk bound one region: a hole's
|
|
1153
|
+
// outer is the smallest counterclockwise piece of the walk that contains
|
|
1154
|
+
// it, and a hole inside none of them shares the walk's outermost piece's
|
|
1155
|
+
// surroundings, found by a ray from the walk's leftmost vertex.
|
|
1156
|
+
const rayFrom = left;
|
|
1157
|
+
const pieces = new Map<number, number[]>();
|
|
1158
|
+
if (walk.length > 0 && walk[walk.length - 1] !== R - 1) {
|
|
1159
|
+
// some walk split into several rings (walk ids repeat)
|
|
1160
|
+
for (let r = 0; r < R; r++) {
|
|
1161
|
+
const list = pieces.get(walk[r]);
|
|
1162
|
+
if (list) list.push(r);
|
|
1163
|
+
else pieces.set(walk[r], [r]);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
for (const list of pieces.values()) {
|
|
1167
|
+
if (list.length < 2) continue;
|
|
1168
|
+
let l = left[list[0]];
|
|
1169
|
+
for (const q of list) {
|
|
1170
|
+
const v = left[q];
|
|
1171
|
+
if (vx[v] < vx[l] || (vx[v] === vx[l] && vy[v] < vy[l])) l = v;
|
|
1172
|
+
}
|
|
1173
|
+
for (const r of list) {
|
|
1174
|
+
if (area[r] >= 0) continue;
|
|
1175
|
+
const o = this.origin(rings[r][0]);
|
|
1176
|
+
const t = this.target(rings[r][0]);
|
|
1177
|
+
const mx = (vx[o] + vx[t]) / 2;
|
|
1178
|
+
const my = (vy[o] + vy[t]) / 2;
|
|
1179
|
+
let best = -1;
|
|
1180
|
+
for (const q of list) {
|
|
1181
|
+
if (area[q] <= sliver || (best >= 0 && area[q] >= area[best]))
|
|
1182
|
+
continue;
|
|
1183
|
+
if (this.encloses(rings[q], mx, my)) best = q;
|
|
1184
|
+
}
|
|
1185
|
+
if (best >= 0) parent[r] = best;
|
|
1186
|
+
else rayFrom[r] = l;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
let holes = 0;
|
|
1191
|
+
for (let r = 0; r < R; r++) if (area[r] < 0 && parent[r] === -2) holes++;
|
|
1192
|
+
this.expectRayQueries(holes);
|
|
1193
|
+
const resolve = (r: number): number => {
|
|
1194
|
+
const chain: number[] = [];
|
|
1195
|
+
let p = r;
|
|
1196
|
+
while (p >= 0 && area[p] < 0 && parent[p] === -2) {
|
|
1197
|
+
chain.push(p);
|
|
1198
|
+
parent[p] = -1; // stop if inconsistent geometry creates a cycle
|
|
1199
|
+
p = this.westHit(rayFrom[p], ringOf);
|
|
1200
|
+
}
|
|
1201
|
+
if (p >= 0 && area[p] < 0) p = parent[p];
|
|
1202
|
+
if (p >= 0 && !(area[p] > sliver)) p = -1;
|
|
1203
|
+
for (const hole of chain) parent[hole] = p;
|
|
1204
|
+
return p;
|
|
1205
|
+
};
|
|
1206
|
+
|
|
1207
|
+
// A ring whose mean width (twice the area over the perimeter) is below the
|
|
1208
|
+
// tolerance is a hairline: the trace of edges that coincide up to rounding,
|
|
1209
|
+
// such as a vertex that touches an edge to float32 precision. It is
|
|
1210
|
+
// invisible filled and a stray line stroked, so it is dropped.
|
|
1211
|
+
const hairline = (ring: Group, a: number) =>
|
|
1212
|
+
Math.abs(a) < (this.tol * outputPerimeter(ring)) / 2;
|
|
1213
|
+
const outers = new Int32Array(R).fill(-1);
|
|
1214
|
+
const polygons: Group[][] = [];
|
|
1215
|
+
for (let r = 0; r < R; r++) {
|
|
1216
|
+
if (area[r] > sliver) {
|
|
1217
|
+
const outer = this.ring(rings[r]);
|
|
1218
|
+
const a = outputArea(outer);
|
|
1219
|
+
if (a <= 0 || hairline(outer, a)) continue;
|
|
1220
|
+
outers[r] = polygons.length;
|
|
1221
|
+
polygons.push([outer]);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
for (let r = 0; r < R; r++) {
|
|
1225
|
+
if (area[r] >= -sliver) continue;
|
|
1226
|
+
const p = resolve(r);
|
|
1227
|
+
if (p >= 0 && outers[p] >= 0) {
|
|
1228
|
+
const hole = this.ring(rings[r]);
|
|
1229
|
+
const a = outputArea(hole);
|
|
1230
|
+
if (a < 0 && !hairline(hole, a)) polygons[outers[p]].push(hole);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
return polygons;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
/** Whether a simple ring encloses a point that is not on its boundary (crossing parity). */
|
|
1237
|
+
private encloses(seq: number[], x: number, y: number): boolean {
|
|
1238
|
+
const vx = this.vx;
|
|
1239
|
+
const vy = this.vy;
|
|
1240
|
+
let inside = false;
|
|
1241
|
+
for (let i = 0; i < seq.length; i++) {
|
|
1242
|
+
const o = this.origin(seq[i]);
|
|
1243
|
+
const t = this.target(seq[i]);
|
|
1244
|
+
const oy = vy[o];
|
|
1245
|
+
const ty = vy[t];
|
|
1246
|
+
if (oy > y === ty > y) continue;
|
|
1247
|
+
const xc = vx[o] + ((y - oy) * (vx[t] - vx[o])) / (ty - oy);
|
|
1248
|
+
if (xc < x) inside = !inside;
|
|
1249
|
+
}
|
|
1250
|
+
return inside;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
/** The ring, among those in `ringOf`, whose edge is the nearest crossing of the ray west from vertex p and faces it. */
|
|
1254
|
+
private westHit(p: number, ringOf: Int32Array): number {
|
|
1255
|
+
const px = this.vx[p];
|
|
1256
|
+
const py = this.vy[p];
|
|
1257
|
+
let best = -Infinity;
|
|
1258
|
+
let bestSlope = -Infinity;
|
|
1259
|
+
let ring = -1;
|
|
1260
|
+
const gu = this.gu;
|
|
1261
|
+
const gv = this.gv;
|
|
1262
|
+
const vx = this.vx;
|
|
1263
|
+
const vy = this.vy;
|
|
1264
|
+
const visit = (e: number) => {
|
|
1265
|
+
if (ringOf[2 * e] < 0 && ringOf[2 * e + 1] < 0) return;
|
|
1266
|
+
const u = gu[e];
|
|
1267
|
+
const v = gv[e];
|
|
1268
|
+
if (u === p || v === p) return; // meets the ray at p itself
|
|
1269
|
+
const uy = vy[u];
|
|
1270
|
+
const wy = vy[v];
|
|
1271
|
+
if (uy > py === wy > py) return;
|
|
1272
|
+
const xc = this.crossingX(u, v, py);
|
|
1273
|
+
if (xc >= px) return;
|
|
1274
|
+
// Crossings exactly at a vertex tie on x; the ray sits just above the
|
|
1275
|
+
// vertex, so the most eastward upward edge is the nearest.
|
|
1276
|
+
const ux = vx[u];
|
|
1277
|
+
const wx = vx[v];
|
|
1278
|
+
const up = wy > uy;
|
|
1279
|
+
const slope = up ? (wx - ux) / (wy - uy) : (ux - wx) / (uy - wy);
|
|
1280
|
+
if (xc > best || (xc === best && slope > bestSlope)) {
|
|
1281
|
+
best = xc;
|
|
1282
|
+
bestSlope = slope;
|
|
1283
|
+
// The face east of the crossing is on the left of the downward
|
|
1284
|
+
// half-edge. If that half-edge has no ring the geometry is inconsistent
|
|
1285
|
+
// at the tol scale, and the hole is dropped rather than misplaced.
|
|
1286
|
+
ring = ringOf[up ? 2 * e + 1 : 2 * e];
|
|
1287
|
+
}
|
|
1288
|
+
};
|
|
1289
|
+
const tree = this.rayTree();
|
|
1290
|
+
if (tree === undefined) {
|
|
1291
|
+
for (let e = 0, E = this.gu.length; e < E; e++) visit(e);
|
|
1292
|
+
return ring;
|
|
1293
|
+
}
|
|
1294
|
+
const stack = tree ? [tree] : [];
|
|
1295
|
+
while (stack.length > 0) {
|
|
1296
|
+
const node = stack.pop()!;
|
|
1297
|
+
if (node.x0 >= px || node.x1 < best || py < node.y0 || py >= node.y1)
|
|
1298
|
+
continue;
|
|
1299
|
+
if (!node.edges) {
|
|
1300
|
+
// Search the more eastward child first so its hit can prune the west.
|
|
1301
|
+
const a = node.left!,
|
|
1302
|
+
b = node.right!;
|
|
1303
|
+
if (a.x1 < b.x1) stack.push(a, b);
|
|
1304
|
+
else stack.push(b, a);
|
|
1305
|
+
continue;
|
|
1306
|
+
}
|
|
1307
|
+
for (const e of node.edges) visit(e);
|
|
1308
|
+
}
|
|
1309
|
+
return ring;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
/** Remove straight-through vertices without changing any bend in the boundary. */
|
|
1313
|
+
private ring(seq: number[]): Group {
|
|
1314
|
+
const vx = this.vx;
|
|
1315
|
+
const vy = this.vy;
|
|
1316
|
+
const n = seq.length;
|
|
1317
|
+
const ids: number[] = [];
|
|
1318
|
+
for (let i = 0; i < n; i++) ids.push(this.origin(seq[i]));
|
|
1319
|
+
const keep: number[] = [ids[0]];
|
|
1320
|
+
const straight = (prev: number, v: number, w: number) => {
|
|
1321
|
+
const rx = vx[w] - vx[prev];
|
|
1322
|
+
const ry = vy[w] - vy[prev];
|
|
1323
|
+
const t =
|
|
1324
|
+
((vx[v] - vx[prev]) * rx + (vy[v] - vy[prev]) * ry) /
|
|
1325
|
+
(rx * rx + ry * ry);
|
|
1326
|
+
return (
|
|
1327
|
+
t > 0 &&
|
|
1328
|
+
t < 1 &&
|
|
1329
|
+
orient2d(vx[prev], vy[prev], vx[w], vy[w], vx[v], vy[v]) === 0
|
|
1330
|
+
);
|
|
1331
|
+
};
|
|
1332
|
+
// Keep an anchor through the linear pass. Only reconsider it after the
|
|
1333
|
+
// closing edge's actual neighbors are known.
|
|
1334
|
+
for (let i = 1; i < n; i++) {
|
|
1335
|
+
const v = ids[i];
|
|
1336
|
+
const w = ids[i === n - 1 ? 0 : i + 1];
|
|
1337
|
+
if (straight(keep[keep.length - 1], v, w)) continue;
|
|
1338
|
+
keep.push(v);
|
|
1339
|
+
}
|
|
1340
|
+
if (keep.length > 3 && straight(keep[keep.length - 1], keep[0], keep[1]))
|
|
1341
|
+
keep.shift();
|
|
1342
|
+
const g = new Group();
|
|
1343
|
+
for (let i = 0; i < keep.length; i++) {
|
|
1344
|
+
const pt = new Pt(2);
|
|
1345
|
+
pt[0] = vx[keep[i]];
|
|
1346
|
+
pt[1] = vy[keep[i]];
|
|
1347
|
+
const prev = g[g.length - 1];
|
|
1348
|
+
// Distinct double intersections can round to the same output Pt.
|
|
1349
|
+
if (!prev || prev[0] !== pt[0] || prev[1] !== pt[1]) g.push(pt);
|
|
1350
|
+
}
|
|
1351
|
+
if (
|
|
1352
|
+
g.length > 1 &&
|
|
1353
|
+
g[0][0] === g[g.length - 1][0] &&
|
|
1354
|
+
g[0][1] === g[g.length - 1][1]
|
|
1355
|
+
)
|
|
1356
|
+
g.pop();
|
|
1357
|
+
return g;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
/**
|
|
1362
|
+
* Combine shapes with a Path mode. Returns the rings of the merged region
|
|
1363
|
+
* for the merging modes, or one ring list per face for `divide` and `crop`.
|
|
1364
|
+
*/
|
|
1365
|
+
export function overlay(
|
|
1366
|
+
shapes: Iterable<PolygonLike> | PtLikeIterable,
|
|
1367
|
+
mode: "divide" | "crop",
|
|
1368
|
+
): Group[][];
|
|
1369
|
+
export function overlay(
|
|
1370
|
+
shapes: Iterable<PolygonLike> | PtLikeIterable,
|
|
1371
|
+
mode: Exclude<PathMode, "divide" | "crop">,
|
|
1372
|
+
): Group[];
|
|
1373
|
+
export function overlay(
|
|
1374
|
+
shapes: Iterable<PolygonLike> | PtLikeIterable,
|
|
1375
|
+
mode: PathMode,
|
|
1376
|
+
): Group[] | Group[][];
|
|
1377
|
+
export function overlay(
|
|
1378
|
+
shapes: Iterable<PolygonLike> | PtLikeIterable,
|
|
1379
|
+
mode: PathMode,
|
|
1380
|
+
): Group[] | Group[][] {
|
|
1381
|
+
const faces = mode === "divide" || mode === "crop";
|
|
1382
|
+
const ov = new Overlay();
|
|
1383
|
+
if (!ov.read(shapes)) return [];
|
|
1384
|
+
ov.split();
|
|
1385
|
+
ov.merge();
|
|
1386
|
+
if (ov.gu.length === 0) return [];
|
|
1387
|
+
ov.trace();
|
|
1388
|
+
ov.label();
|
|
1389
|
+
const keep = ov.select(mode);
|
|
1390
|
+
const ringOf = new Int32Array(2 * ov.gu.length).fill(-1);
|
|
1391
|
+
const walks = faces
|
|
1392
|
+
? ov.faceRings(keep, ringOf)
|
|
1393
|
+
: ov.mergedRings(keep, ringOf);
|
|
1394
|
+
const { rings, walk } = ov.simpleRings(walks, ringOf);
|
|
1395
|
+
const polygons = ov.assemble(rings, ringOf, walk);
|
|
1396
|
+
if (faces) return polygons;
|
|
1397
|
+
const flat: Group[] = [];
|
|
1398
|
+
for (let i = 0; i < polygons.length; i++) {
|
|
1399
|
+
for (let j = 0; j < polygons[i].length; j++) flat.push(polygons[i][j]);
|
|
1400
|
+
}
|
|
1401
|
+
return flat;
|
|
1402
|
+
}
|