pattapatta 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.md +98 -0
- package/dist/circle-BDbKfrdR.d.ts +12 -0
- package/dist/circlePacking/index.d.ts +78 -0
- package/dist/circlePacking/index.js +381 -0
- package/dist/circlePacking/index.js.map +1 -0
- package/dist/cli.js +365 -0
- package/dist/cli.js.map +1 -0
- package/dist/construction/index.d.ts +26 -0
- package/dist/construction/index.js +102 -0
- package/dist/construction/index.js.map +1 -0
- package/dist/contour/index.d.ts +20 -0
- package/dist/contour/index.js +118 -0
- package/dist/contour/index.js.map +1 -0
- package/dist/conversion/index.d.ts +31 -0
- package/dist/conversion/index.js +99 -0
- package/dist/conversion/index.js.map +1 -0
- package/dist/group-BNjVVYSQ.d.ts +13 -0
- package/dist/hatch/index.d.ts +41 -0
- package/dist/hatch/index.js +253 -0
- package/dist/hatch/index.js.map +1 -0
- package/dist/hull/index.d.ts +16 -0
- package/dist/hull/index.js +97 -0
- package/dist/hull/index.js.map +1 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.js +3933 -0
- package/dist/index.js.map +1 -0
- package/dist/meshing/index.d.ts +125 -0
- package/dist/meshing/index.js +1333 -0
- package/dist/meshing/index.js.map +1 -0
- package/dist/morphology/index.d.ts +58 -0
- package/dist/morphology/index.js +268 -0
- package/dist/morphology/index.js.map +1 -0
- package/dist/optimisation/index.d.ts +43 -0
- package/dist/optimisation/index.js +400 -0
- package/dist/optimisation/index.js.map +1 -0
- package/dist/path-0RjhYjJs.d.ts +21 -0
- package/dist/pointSet/index.d.ts +24 -0
- package/dist/pointSet/index.js +139 -0
- package/dist/pointSet/index.js.map +1 -0
- package/dist/polygonisation/index.d.ts +44 -0
- package/dist/polygonisation/index.js +310 -0
- package/dist/polygonisation/index.js.map +1 -0
- package/dist/predicates/index.d.ts +35 -0
- package/dist/predicates/index.js +159 -0
- package/dist/predicates/index.js.map +1 -0
- package/dist/processing/index.d.ts +54 -0
- package/dist/processing/index.js +457 -0
- package/dist/processing/index.js.map +1 -0
- package/dist/segment-BKuIFMKD.d.ts +10 -0
- package/dist/segmentSet/index.d.ts +25 -0
- package/dist/segmentSet/index.js +109 -0
- package/dist/segmentSet/index.js.map +1 -0
- package/dist/shapeBoolean/index.d.ts +41 -0
- package/dist/shapeBoolean/index.js +211 -0
- package/dist/shapeBoolean/index.js.map +1 -0
- package/dist/svg/index.d.ts +38 -0
- package/dist/svg/index.js +317 -0
- package/dist/svg/index.js.map +1 -0
- package/dist/tiling/index.d.ts +32 -0
- package/dist/tiling/index.js +344 -0
- package/dist/tiling/index.js.map +1 -0
- package/dist/transformation/index.d.ts +37 -0
- package/dist/transformation/index.js +184 -0
- package/dist/transformation/index.js.map +1 -0
- package/dist/triangulation/index.d.ts +48 -0
- package/dist/triangulation/index.js +334 -0
- package/dist/triangulation/index.js.map +1 -0
- package/dist/vec2-CWyXN1Wj.d.ts +10 -0
- package/dist/voronoi/index.d.ts +31 -0
- package/dist/voronoi/index.js +211 -0
- package/dist/voronoi/index.js.map +1 -0
- package/package.json +165 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3933 @@
|
|
|
1
|
+
import { FillRule, JoinType, inflatePathsD, EndType, simplifyPathD, minkowskiSumD, minkowskiDiffD, triangulateD, unionD, isPositiveD, intersectD, differenceD, xorD, areaD, pointInPolygonD, PointInPolygonResult } from 'clipper2-ts';
|
|
2
|
+
import { Delaunay } from 'd3-delaunay';
|
|
3
|
+
|
|
4
|
+
// src/types/vec2.ts
|
|
5
|
+
function vec2(x, y) {
|
|
6
|
+
return { x, y };
|
|
7
|
+
}
|
|
8
|
+
function cloneVec2(v) {
|
|
9
|
+
return { x: v.x, y: v.y };
|
|
10
|
+
}
|
|
11
|
+
function equalsVec2(a, b, eps = 1e-9) {
|
|
12
|
+
return Math.abs(a.x - b.x) <= eps && Math.abs(a.y - b.y) <= eps;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// src/types/circle.ts
|
|
16
|
+
function circle(x, y, r) {
|
|
17
|
+
return { x, y, r };
|
|
18
|
+
}
|
|
19
|
+
function circleCenter(c) {
|
|
20
|
+
return { x: c.x, y: c.y };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// src/types/segment.ts
|
|
24
|
+
function segment(a, b) {
|
|
25
|
+
return { a, b };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/types/path.ts
|
|
29
|
+
function path(rings, closed = true) {
|
|
30
|
+
return {
|
|
31
|
+
rings: rings.map((ring2) => ring2.map(cloneVec2)),
|
|
32
|
+
closed
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function polyline(points) {
|
|
36
|
+
return path([points], false);
|
|
37
|
+
}
|
|
38
|
+
function polygon(exterior, holes = []) {
|
|
39
|
+
return path([exterior, ...holes], true);
|
|
40
|
+
}
|
|
41
|
+
function normalizeRing(ring2, eps = 1e-9) {
|
|
42
|
+
if (ring2.length < 2) return ring2.map(cloneVec2);
|
|
43
|
+
const first = ring2[0];
|
|
44
|
+
const last = ring2[ring2.length - 1];
|
|
45
|
+
if (first && last && equalsVec2(first, last, eps)) {
|
|
46
|
+
return ring2.slice(0, -1).map(cloneVec2);
|
|
47
|
+
}
|
|
48
|
+
return ring2.map(cloneVec2);
|
|
49
|
+
}
|
|
50
|
+
function clonePath(p) {
|
|
51
|
+
return {
|
|
52
|
+
closed: p.closed,
|
|
53
|
+
rings: p.rings.map((ring2) => ring2.map(cloneVec2))
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/types/group.ts
|
|
58
|
+
function group(paths = []) {
|
|
59
|
+
return { paths: paths.map(clonePath) };
|
|
60
|
+
}
|
|
61
|
+
function groupFromPaths(...paths) {
|
|
62
|
+
return group(paths);
|
|
63
|
+
}
|
|
64
|
+
function cloneGroup(g) {
|
|
65
|
+
return group(g.paths);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/svg/pathData.ts
|
|
69
|
+
function parsePathData(d) {
|
|
70
|
+
const tokens = tokenize(d);
|
|
71
|
+
const rings = [];
|
|
72
|
+
let current = [];
|
|
73
|
+
let cx = 0;
|
|
74
|
+
let cy = 0;
|
|
75
|
+
let startX = 0;
|
|
76
|
+
let startY = 0;
|
|
77
|
+
let sawClose = false;
|
|
78
|
+
let i = 0;
|
|
79
|
+
const pushPoint = (x, y) => {
|
|
80
|
+
current.push(vec2(x, y));
|
|
81
|
+
cx = x;
|
|
82
|
+
cy = y;
|
|
83
|
+
};
|
|
84
|
+
const finishSubpath = () => {
|
|
85
|
+
if (current.length === 0) return;
|
|
86
|
+
const ring2 = normalizeRing(current);
|
|
87
|
+
if (ring2.length > 0) rings.push(ring2);
|
|
88
|
+
current = [];
|
|
89
|
+
};
|
|
90
|
+
while (i < tokens.length) {
|
|
91
|
+
const raw = tokens[i];
|
|
92
|
+
if (raw === void 0) break;
|
|
93
|
+
i += 1;
|
|
94
|
+
if (!/^[MmLlHhVvZz]$/.test(raw)) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`Unsupported SVG path command or token "${raw}". Phase 1 supports M/L/H/V/Z only.`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
const relative = raw === raw.toLowerCase();
|
|
100
|
+
const kind = raw.toUpperCase();
|
|
101
|
+
if (kind === "Z") {
|
|
102
|
+
sawClose = true;
|
|
103
|
+
finishSubpath();
|
|
104
|
+
cx = startX;
|
|
105
|
+
cy = startY;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (kind === "M") {
|
|
109
|
+
finishSubpath();
|
|
110
|
+
const pair = readPair(tokens, i);
|
|
111
|
+
i = pair.next;
|
|
112
|
+
const x = relative ? cx + pair.x : pair.x;
|
|
113
|
+
const y = relative ? cy + pair.y : pair.y;
|
|
114
|
+
pushPoint(x, y);
|
|
115
|
+
startX = x;
|
|
116
|
+
startY = y;
|
|
117
|
+
while (i < tokens.length && isNumberToken(tokens[i])) {
|
|
118
|
+
const next = readPair(tokens, i);
|
|
119
|
+
i = next.next;
|
|
120
|
+
const nx = relative ? cx + next.x : next.x;
|
|
121
|
+
const ny = relative ? cy + next.y : next.y;
|
|
122
|
+
pushPoint(nx, ny);
|
|
123
|
+
}
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (kind === "L") {
|
|
127
|
+
while (i < tokens.length && isNumberToken(tokens[i])) {
|
|
128
|
+
const pair = readPair(tokens, i);
|
|
129
|
+
i = pair.next;
|
|
130
|
+
const x = relative ? cx + pair.x : pair.x;
|
|
131
|
+
const y = relative ? cy + pair.y : pair.y;
|
|
132
|
+
pushPoint(x, y);
|
|
133
|
+
}
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (kind === "H") {
|
|
137
|
+
while (i < tokens.length && isNumberToken(tokens[i])) {
|
|
138
|
+
const v = readNumber(tokens, i);
|
|
139
|
+
i = v.next;
|
|
140
|
+
const x = relative ? cx + v.value : v.value;
|
|
141
|
+
pushPoint(x, cy);
|
|
142
|
+
}
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (kind === "V") {
|
|
146
|
+
while (i < tokens.length && isNumberToken(tokens[i])) {
|
|
147
|
+
const v = readNumber(tokens, i);
|
|
148
|
+
i = v.next;
|
|
149
|
+
const y = relative ? cy + v.value : v.value;
|
|
150
|
+
pushPoint(cx, y);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
finishSubpath();
|
|
155
|
+
return { rings, closed: sawClose };
|
|
156
|
+
}
|
|
157
|
+
function serializePathData(rings, closed) {
|
|
158
|
+
const parts = [];
|
|
159
|
+
for (const ring2 of rings) {
|
|
160
|
+
if (ring2.length === 0) continue;
|
|
161
|
+
const first = ring2[0];
|
|
162
|
+
if (!first) continue;
|
|
163
|
+
parts.push(`M ${fmt(first.x)} ${fmt(first.y)}`);
|
|
164
|
+
for (let i = 1; i < ring2.length; i += 1) {
|
|
165
|
+
const p = ring2[i];
|
|
166
|
+
if (!p) continue;
|
|
167
|
+
parts.push(`L ${fmt(p.x)} ${fmt(p.y)}`);
|
|
168
|
+
}
|
|
169
|
+
if (closed) parts.push("Z");
|
|
170
|
+
}
|
|
171
|
+
return parts.join(" ");
|
|
172
|
+
}
|
|
173
|
+
function tokenize(d) {
|
|
174
|
+
const out = [];
|
|
175
|
+
const re = /([MmLlHhVvZz])|([+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?)/g;
|
|
176
|
+
let m;
|
|
177
|
+
while ((m = re.exec(d)) !== null) {
|
|
178
|
+
const token = m[1] ?? m[2];
|
|
179
|
+
if (token) out.push(token);
|
|
180
|
+
}
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
183
|
+
function isNumberToken(token) {
|
|
184
|
+
return token !== void 0 && /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/.test(token);
|
|
185
|
+
}
|
|
186
|
+
function readNumber(tokens, i) {
|
|
187
|
+
const t = tokens[i];
|
|
188
|
+
if (!isNumberToken(t) || t === void 0) {
|
|
189
|
+
throw new Error(`Expected number in path data at token index ${i}`);
|
|
190
|
+
}
|
|
191
|
+
return { value: Number(t), next: i + 1 };
|
|
192
|
+
}
|
|
193
|
+
function readPair(tokens, i) {
|
|
194
|
+
const x = readNumber(tokens, i);
|
|
195
|
+
const y = readNumber(tokens, x.next);
|
|
196
|
+
return { x: x.value, y: y.value, next: y.next };
|
|
197
|
+
}
|
|
198
|
+
function fmt(n) {
|
|
199
|
+
if (Number.isInteger(n)) return String(n);
|
|
200
|
+
return String(Number(n.toPrecision(12)));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/svg/parse.ts
|
|
204
|
+
var SHAPE_RE = /<(path|rect|circle|line|polyline|polygon)\b[^>]*\/?>/gi;
|
|
205
|
+
function parseSvg(svg) {
|
|
206
|
+
const paths = [];
|
|
207
|
+
for (const match of svg.matchAll(SHAPE_RE)) {
|
|
208
|
+
const el = match[0];
|
|
209
|
+
const tag = match[1]?.toLowerCase();
|
|
210
|
+
if (!tag) continue;
|
|
211
|
+
if (tag === "path") {
|
|
212
|
+
const d = getAttr(el, "d");
|
|
213
|
+
if (!d) continue;
|
|
214
|
+
const parsed = parsePathData(d);
|
|
215
|
+
if (parsed.rings.length === 0) continue;
|
|
216
|
+
paths.push(path(parsed.rings, parsed.closed));
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (tag === "rect") {
|
|
220
|
+
const x = num(getAttr(el, "x"), 0);
|
|
221
|
+
const y = num(getAttr(el, "y"), 0);
|
|
222
|
+
const w = num(getAttr(el, "width"), 0);
|
|
223
|
+
const h = num(getAttr(el, "height"), 0);
|
|
224
|
+
if (w === 0 || h === 0) continue;
|
|
225
|
+
paths.push(
|
|
226
|
+
polygon([
|
|
227
|
+
vec2(x, y),
|
|
228
|
+
vec2(x + w, y),
|
|
229
|
+
vec2(x + w, y + h),
|
|
230
|
+
vec2(x, y + h)
|
|
231
|
+
])
|
|
232
|
+
);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (tag === "circle") {
|
|
236
|
+
const cx = num(getAttr(el, "cx"), 0);
|
|
237
|
+
const cy = num(getAttr(el, "cy"), 0);
|
|
238
|
+
const r = num(getAttr(el, "r"), 0);
|
|
239
|
+
if (r <= 0) continue;
|
|
240
|
+
paths.push(polygon(circleRing(cx, cy, r, 64)));
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (tag === "line") {
|
|
244
|
+
const x1 = num(getAttr(el, "x1"), 0);
|
|
245
|
+
const y1 = num(getAttr(el, "y1"), 0);
|
|
246
|
+
const x2 = num(getAttr(el, "x2"), 0);
|
|
247
|
+
const y2 = num(getAttr(el, "y2"), 0);
|
|
248
|
+
paths.push(polyline([vec2(x1, y1), vec2(x2, y2)]));
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (tag === "polyline") {
|
|
252
|
+
const pts = parsePoints(getAttr(el, "points") ?? "");
|
|
253
|
+
if (pts.length >= 2) paths.push(polyline(pts));
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (tag === "polygon") {
|
|
257
|
+
const pts = parsePoints(getAttr(el, "points") ?? "");
|
|
258
|
+
if (pts.length >= 3) paths.push(polygon(pts));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return group(paths);
|
|
262
|
+
}
|
|
263
|
+
function getAttr(el, name) {
|
|
264
|
+
const re = new RegExp(`\\b${name}\\s*=\\s*["']([^"']*)["']`, "i");
|
|
265
|
+
const m = re.exec(el);
|
|
266
|
+
return m?.[1];
|
|
267
|
+
}
|
|
268
|
+
function num(value, fallback) {
|
|
269
|
+
if (value === void 0 || value === "") return fallback;
|
|
270
|
+
const n = Number(value);
|
|
271
|
+
return Number.isFinite(n) ? n : fallback;
|
|
272
|
+
}
|
|
273
|
+
function parsePoints(points) {
|
|
274
|
+
const nums = points.trim().split(/[\s,]+/).filter(Boolean).map(Number);
|
|
275
|
+
const out = [];
|
|
276
|
+
for (let i = 0; i + 1 < nums.length; i += 2) {
|
|
277
|
+
const x = nums[i];
|
|
278
|
+
const y = nums[i + 1];
|
|
279
|
+
if (x === void 0 || y === void 0) continue;
|
|
280
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) continue;
|
|
281
|
+
out.push(vec2(x, y));
|
|
282
|
+
}
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
function circleRing(cx, cy, r, segments) {
|
|
286
|
+
const ring2 = [];
|
|
287
|
+
for (let i = 0; i < segments; i += 1) {
|
|
288
|
+
const t = i / segments * Math.PI * 2;
|
|
289
|
+
ring2.push(vec2(cx + r * Math.cos(t), cy + r * Math.sin(t)));
|
|
290
|
+
}
|
|
291
|
+
return ring2;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// src/svg/serialize.ts
|
|
295
|
+
function toSvg(g, options = {}) {
|
|
296
|
+
const stroke = options.stroke ?? "#000";
|
|
297
|
+
const strokeWidth = options.strokeWidth ?? 1;
|
|
298
|
+
const viewBox = options.viewBox ?? inferViewBox(g);
|
|
299
|
+
const widthAttr = options.width !== void 0 ? ` width="${options.width}"` : "";
|
|
300
|
+
const heightAttr = options.height !== void 0 ? ` height="${options.height}"` : "";
|
|
301
|
+
const body = g.paths.map((p) => pathElement(p, stroke, strokeWidth)).filter(Boolean).join("\n ");
|
|
302
|
+
return [
|
|
303
|
+
`<?xml version="1.0" encoding="UTF-8"?>`,
|
|
304
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBox}"${widthAttr}${heightAttr} fill="none">`,
|
|
305
|
+
body ? ` ${body}` : "",
|
|
306
|
+
`</svg>`
|
|
307
|
+
].filter((line) => line !== "").join("\n");
|
|
308
|
+
}
|
|
309
|
+
function pathElement(p, stroke, strokeWidth) {
|
|
310
|
+
if (p.rings.length === 0) return "";
|
|
311
|
+
const d = serializePathData(p.rings, p.closed);
|
|
312
|
+
if (!d) return "";
|
|
313
|
+
return `<path d="${d}" stroke="${stroke}" stroke-width="${strokeWidth}" fill="none" />`;
|
|
314
|
+
}
|
|
315
|
+
function inferViewBox(g) {
|
|
316
|
+
let minX = Infinity;
|
|
317
|
+
let minY = Infinity;
|
|
318
|
+
let maxX = -Infinity;
|
|
319
|
+
let maxY = -Infinity;
|
|
320
|
+
for (const p of g.paths) {
|
|
321
|
+
for (const ring2 of p.rings) {
|
|
322
|
+
for (const v of ring2) {
|
|
323
|
+
minX = Math.min(minX, v.x);
|
|
324
|
+
minY = Math.min(minY, v.y);
|
|
325
|
+
maxX = Math.max(maxX, v.x);
|
|
326
|
+
maxY = Math.max(maxY, v.y);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (!Number.isFinite(minX)) return "0 0 100 100";
|
|
331
|
+
const pad = 1;
|
|
332
|
+
const w = Math.max(maxX - minX, 0) + pad * 2;
|
|
333
|
+
const h = Math.max(maxY - minY, 0) + pad * 2;
|
|
334
|
+
return `${minX - pad} ${minY - pad} ${w} ${h}`;
|
|
335
|
+
}
|
|
336
|
+
var DEFAULT_FILL_RULE = FillRule.NonZero;
|
|
337
|
+
function ringToPathD(ring2) {
|
|
338
|
+
const pts = [];
|
|
339
|
+
for (const v of normalizeRing(ring2)) {
|
|
340
|
+
pts.push({ x: v.x, y: v.y, z: 0 });
|
|
341
|
+
}
|
|
342
|
+
return pts;
|
|
343
|
+
}
|
|
344
|
+
function pathToPathsD(p) {
|
|
345
|
+
if (!p.closed || p.rings.length === 0) {
|
|
346
|
+
throw new Error("Boolean ops require a closed path with at least one ring");
|
|
347
|
+
}
|
|
348
|
+
return p.rings.map((ring2) => ringToPathD(ring2));
|
|
349
|
+
}
|
|
350
|
+
function pathsDToGroup(paths) {
|
|
351
|
+
if (paths.length === 0) return group([]);
|
|
352
|
+
const outers = [];
|
|
353
|
+
const holes = [];
|
|
354
|
+
for (const pd of paths) {
|
|
355
|
+
const ring2 = pathDToRing(pd);
|
|
356
|
+
if (ring2.length < 3) continue;
|
|
357
|
+
if (isPositiveD(pd)) {
|
|
358
|
+
outers.push({ ring: ring2, holes: [] });
|
|
359
|
+
} else {
|
|
360
|
+
holes.push(ring2);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
for (const hole of holes) {
|
|
364
|
+
const sample = hole[0];
|
|
365
|
+
if (!sample) continue;
|
|
366
|
+
let assigned = false;
|
|
367
|
+
for (const outer of outers) {
|
|
368
|
+
if (pointInRing(sample, outer.ring)) {
|
|
369
|
+
outer.holes.push(hole);
|
|
370
|
+
assigned = true;
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (!assigned) {
|
|
375
|
+
outers.push({ ring: [...hole].reverse(), holes: [] });
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return group(outers.map((o) => path([o.ring, ...o.holes], true)));
|
|
379
|
+
}
|
|
380
|
+
function pathDToRing(pd) {
|
|
381
|
+
return normalizeRing(pd.map((p) => vec2(p.x, p.y)));
|
|
382
|
+
}
|
|
383
|
+
function pointInRing(point, ring2) {
|
|
384
|
+
const result = pointInPolygonD(
|
|
385
|
+
{ x: point.x, y: point.y, z: 0 },
|
|
386
|
+
ringToPathD(ring2)
|
|
387
|
+
);
|
|
388
|
+
return result === PointInPolygonResult.IsInside || result === PointInPolygonResult.IsOn;
|
|
389
|
+
}
|
|
390
|
+
function clipUnion(a, b) {
|
|
391
|
+
return unionD(a, b, DEFAULT_FILL_RULE);
|
|
392
|
+
}
|
|
393
|
+
function clipIntersect(a, b) {
|
|
394
|
+
return intersectD(a, b, DEFAULT_FILL_RULE);
|
|
395
|
+
}
|
|
396
|
+
function clipDifference(a, b) {
|
|
397
|
+
return differenceD(a, b, DEFAULT_FILL_RULE);
|
|
398
|
+
}
|
|
399
|
+
function clipXor(a, b) {
|
|
400
|
+
return xorD(a, b, DEFAULT_FILL_RULE);
|
|
401
|
+
}
|
|
402
|
+
function pathsArea(paths) {
|
|
403
|
+
let sum = 0;
|
|
404
|
+
for (const p of paths) {
|
|
405
|
+
sum += Math.abs(areaD(p));
|
|
406
|
+
}
|
|
407
|
+
return sum;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// src/shapeBoolean/index.ts
|
|
411
|
+
function ensureClosed(p) {
|
|
412
|
+
if (!p.closed) {
|
|
413
|
+
throw new Error("shapeBoolean requires closed paths");
|
|
414
|
+
}
|
|
415
|
+
return p;
|
|
416
|
+
}
|
|
417
|
+
function union(a, b) {
|
|
418
|
+
const result = clipUnion(pathToPathsD(ensureClosed(a)), pathToPathsD(ensureClosed(b)));
|
|
419
|
+
return pathsDToGroup(result);
|
|
420
|
+
}
|
|
421
|
+
function unionAll(paths) {
|
|
422
|
+
if (paths.length === 0) return group([]);
|
|
423
|
+
let acc = pathToPathsD(ensureClosed(paths[0]));
|
|
424
|
+
for (let i = 1; i < paths.length; i++) {
|
|
425
|
+
acc = clipUnion(acc, pathToPathsD(ensureClosed(paths[i])));
|
|
426
|
+
}
|
|
427
|
+
return pathsDToGroup(acc);
|
|
428
|
+
}
|
|
429
|
+
function intersect(a, b) {
|
|
430
|
+
const result = clipIntersect(
|
|
431
|
+
pathToPathsD(ensureClosed(a)),
|
|
432
|
+
pathToPathsD(ensureClosed(b))
|
|
433
|
+
);
|
|
434
|
+
return pathsDToGroup(result);
|
|
435
|
+
}
|
|
436
|
+
function subtract(a, b) {
|
|
437
|
+
const result = clipDifference(
|
|
438
|
+
pathToPathsD(ensureClosed(a)),
|
|
439
|
+
pathToPathsD(ensureClosed(b))
|
|
440
|
+
);
|
|
441
|
+
return pathsDToGroup(result);
|
|
442
|
+
}
|
|
443
|
+
function subtractAll(base, shapes) {
|
|
444
|
+
let acc = pathToPathsD(ensureClosed(base));
|
|
445
|
+
for (const s of shapes) {
|
|
446
|
+
acc = clipDifference(acc, pathToPathsD(ensureClosed(s)));
|
|
447
|
+
}
|
|
448
|
+
return pathsDToGroup(acc);
|
|
449
|
+
}
|
|
450
|
+
function symDifference(a, b) {
|
|
451
|
+
const result = clipXor(pathToPathsD(ensureClosed(a)), pathToPathsD(ensureClosed(b)));
|
|
452
|
+
return pathsDToGroup(result);
|
|
453
|
+
}
|
|
454
|
+
function complement(shape, width2, height2) {
|
|
455
|
+
const frame = polygon([
|
|
456
|
+
vec2(0, 0),
|
|
457
|
+
vec2(width2, 0),
|
|
458
|
+
vec2(width2, height2),
|
|
459
|
+
vec2(0, height2)
|
|
460
|
+
]);
|
|
461
|
+
return subtract(frame, shape);
|
|
462
|
+
}
|
|
463
|
+
function occlusionSubtract(g) {
|
|
464
|
+
const paths = g.paths;
|
|
465
|
+
const visible = [];
|
|
466
|
+
for (let i = 0; i < paths.length; i++) {
|
|
467
|
+
const subject = paths[i];
|
|
468
|
+
if (!subject) continue;
|
|
469
|
+
const occluders = paths.slice(i + 1);
|
|
470
|
+
const piece = occluders.length === 0 ? groupFromPaths(ensureClosed(subject)) : subtractAll(subject, occluders);
|
|
471
|
+
for (const p of piece.paths) {
|
|
472
|
+
visible.push(p);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return group(visible);
|
|
476
|
+
}
|
|
477
|
+
function overlapRegions(shapes, merged) {
|
|
478
|
+
if (shapes.length < 2) return group([]);
|
|
479
|
+
const pairs = [];
|
|
480
|
+
for (let i = 0; i < shapes.length; i++) {
|
|
481
|
+
for (let j = i + 1; j < shapes.length; j++) {
|
|
482
|
+
const a = shapes[i];
|
|
483
|
+
const b = shapes[j];
|
|
484
|
+
if (!a || !b) continue;
|
|
485
|
+
for (const p of intersect(a, b).paths) {
|
|
486
|
+
pairs.push(p);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
if (!merged) return group(pairs);
|
|
491
|
+
return unionAll(pairs);
|
|
492
|
+
}
|
|
493
|
+
var shapeBoolean = {
|
|
494
|
+
union,
|
|
495
|
+
unionAll,
|
|
496
|
+
intersect,
|
|
497
|
+
subtract,
|
|
498
|
+
subtractAll,
|
|
499
|
+
symDifference,
|
|
500
|
+
complement,
|
|
501
|
+
occlusionSubtract,
|
|
502
|
+
overlapRegions
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
// src/predicates/index.ts
|
|
506
|
+
function ringArea(ring2) {
|
|
507
|
+
if (ring2.length < 3) return 0;
|
|
508
|
+
let sum = 0;
|
|
509
|
+
for (let i = 0; i < ring2.length; i++) {
|
|
510
|
+
const a = ring2[i];
|
|
511
|
+
const b = ring2[(i + 1) % ring2.length];
|
|
512
|
+
sum += a.x * b.y - b.x * a.y;
|
|
513
|
+
}
|
|
514
|
+
return sum / 2;
|
|
515
|
+
}
|
|
516
|
+
function area(p) {
|
|
517
|
+
if (!p.closed || p.rings.length === 0) return 0;
|
|
518
|
+
try {
|
|
519
|
+
return pathsArea(pathToPathsD(p));
|
|
520
|
+
} catch {
|
|
521
|
+
let total = 0;
|
|
522
|
+
for (let i = 0; i < p.rings.length; i++) {
|
|
523
|
+
const a = Math.abs(ringArea(p.rings[i]));
|
|
524
|
+
total += i === 0 ? a : -a;
|
|
525
|
+
}
|
|
526
|
+
return Math.max(0, total);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
function areaGroup(g) {
|
|
530
|
+
return g.paths.reduce((sum, p) => sum + area(p), 0);
|
|
531
|
+
}
|
|
532
|
+
function centroid(p) {
|
|
533
|
+
const ring2 = p.rings[0];
|
|
534
|
+
if (!ring2 || ring2.length === 0) return vec2(0, 0);
|
|
535
|
+
const a = ringArea(ring2);
|
|
536
|
+
if (Math.abs(a) < 1e-12) {
|
|
537
|
+
let sx = 0;
|
|
538
|
+
let sy = 0;
|
|
539
|
+
for (const v of ring2) {
|
|
540
|
+
sx += v.x;
|
|
541
|
+
sy += v.y;
|
|
542
|
+
}
|
|
543
|
+
return vec2(sx / ring2.length, sy / ring2.length);
|
|
544
|
+
}
|
|
545
|
+
let cx = 0;
|
|
546
|
+
let cy = 0;
|
|
547
|
+
for (let i = 0; i < ring2.length; i++) {
|
|
548
|
+
const p0 = ring2[i];
|
|
549
|
+
const p1 = ring2[(i + 1) % ring2.length];
|
|
550
|
+
const cross4 = p0.x * p1.y - p1.x * p0.y;
|
|
551
|
+
cx += (p0.x + p1.x) * cross4;
|
|
552
|
+
cy += (p0.y + p1.y) * cross4;
|
|
553
|
+
}
|
|
554
|
+
return vec2(cx / (6 * a), cy / (6 * a));
|
|
555
|
+
}
|
|
556
|
+
function containsPoint(p, point) {
|
|
557
|
+
if (!p.closed || !p.rings[0]) return false;
|
|
558
|
+
if (!pointInRing(point, p.rings[0])) return false;
|
|
559
|
+
for (let i = 1; i < p.rings.length; i++) {
|
|
560
|
+
const hole = p.rings[i];
|
|
561
|
+
if (hole && pointInRing(point, hole)) return false;
|
|
562
|
+
}
|
|
563
|
+
return true;
|
|
564
|
+
}
|
|
565
|
+
function bounds(p) {
|
|
566
|
+
let minX = Infinity;
|
|
567
|
+
let minY = Infinity;
|
|
568
|
+
let maxX = -Infinity;
|
|
569
|
+
let maxY = -Infinity;
|
|
570
|
+
for (const ring2 of p.rings) {
|
|
571
|
+
for (const v of ring2) {
|
|
572
|
+
minX = Math.min(minX, v.x);
|
|
573
|
+
minY = Math.min(minY, v.y);
|
|
574
|
+
maxX = Math.max(maxX, v.x);
|
|
575
|
+
maxY = Math.max(maxY, v.y);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
if (!Number.isFinite(minX)) {
|
|
579
|
+
return { minX: 0, minY: 0, maxX: 0, maxY: 0 };
|
|
580
|
+
}
|
|
581
|
+
return { minX, minY, maxX, maxY };
|
|
582
|
+
}
|
|
583
|
+
function width(p) {
|
|
584
|
+
const b = bounds(p);
|
|
585
|
+
return b.maxX - b.minX;
|
|
586
|
+
}
|
|
587
|
+
function height(p) {
|
|
588
|
+
const b = bounds(p);
|
|
589
|
+
return b.maxY - b.minY;
|
|
590
|
+
}
|
|
591
|
+
function areaSimilarity(a, b) {
|
|
592
|
+
const aa = area(a);
|
|
593
|
+
const bb = area(b);
|
|
594
|
+
const denom = Math.max(aa, bb, 1e-12);
|
|
595
|
+
return 1 - Math.abs(aa - bb) / denom;
|
|
596
|
+
}
|
|
597
|
+
var predicates = {
|
|
598
|
+
area,
|
|
599
|
+
areaGroup,
|
|
600
|
+
ringArea,
|
|
601
|
+
centroid,
|
|
602
|
+
containsPoint,
|
|
603
|
+
bounds,
|
|
604
|
+
width,
|
|
605
|
+
height,
|
|
606
|
+
areaSimilarity
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
// src/compare/index.ts
|
|
610
|
+
function ringsToPath(rings) {
|
|
611
|
+
const converted = rings.map(
|
|
612
|
+
(ring2) => ring2.map((pt) => vec2(pt[0], pt[1]))
|
|
613
|
+
);
|
|
614
|
+
return polygon(converted[0] ?? [], converted.slice(1));
|
|
615
|
+
}
|
|
616
|
+
function oraclePathsToGroup(paths) {
|
|
617
|
+
return {
|
|
618
|
+
paths: paths.map((p) => ringsToPath(p.rings))
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
function relativeAreaError(a, b) {
|
|
622
|
+
const aa = areaGroup(a);
|
|
623
|
+
const bb = areaGroup(b);
|
|
624
|
+
const denom = Math.max(Math.abs(aa), Math.abs(bb), 1e-12);
|
|
625
|
+
return Math.abs(aa - bb) / denom;
|
|
626
|
+
}
|
|
627
|
+
function assertAreaClose(actual, expectedArea, tol = 1e-3) {
|
|
628
|
+
const got = areaGroup(actual);
|
|
629
|
+
const denom = Math.max(Math.abs(expectedArea), 1e-12);
|
|
630
|
+
const err = Math.abs(got - expectedArea) / denom;
|
|
631
|
+
if (err > tol) {
|
|
632
|
+
throw new Error(
|
|
633
|
+
`Area mismatch: got ${got}, expected ${expectedArea}, relErr=${err}`
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
function containmentAgreement(a, b, samples = 20) {
|
|
638
|
+
const bounds2 = unionBounds(a, b);
|
|
639
|
+
if (bounds2.maxX === bounds2.minX || bounds2.maxY === bounds2.minY) return 1;
|
|
640
|
+
let agree = 0;
|
|
641
|
+
let total = 0;
|
|
642
|
+
for (let i = 0; i < samples; i++) {
|
|
643
|
+
for (let j = 0; j < samples; j++) {
|
|
644
|
+
const x = bounds2.minX + (i + 0.5) / samples * (bounds2.maxX - bounds2.minX);
|
|
645
|
+
const y = bounds2.minY + (j + 0.5) / samples * (bounds2.maxY - bounds2.minY);
|
|
646
|
+
const p = vec2(x, y);
|
|
647
|
+
const inA = pointInGroup(a, p);
|
|
648
|
+
const inB = pointInGroup(b, p);
|
|
649
|
+
if (inA === inB) agree += 1;
|
|
650
|
+
total += 1;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return agree / total;
|
|
654
|
+
}
|
|
655
|
+
function pointInGroup(g, p) {
|
|
656
|
+
for (const path3 of g.paths) {
|
|
657
|
+
if (!path3.closed || !path3.rings[0]) continue;
|
|
658
|
+
if (!pointInPoly(p, path3.rings[0])) continue;
|
|
659
|
+
let inHole = false;
|
|
660
|
+
for (let i = 1; i < path3.rings.length; i++) {
|
|
661
|
+
const hole = path3.rings[i];
|
|
662
|
+
if (hole && pointInPoly(p, hole)) {
|
|
663
|
+
inHole = true;
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
if (!inHole) return true;
|
|
668
|
+
}
|
|
669
|
+
return false;
|
|
670
|
+
}
|
|
671
|
+
function pointInPoly(point, ring2) {
|
|
672
|
+
let inside = false;
|
|
673
|
+
for (let i = 0, j = ring2.length - 1; i < ring2.length; j = i++) {
|
|
674
|
+
const xi = ring2[i].x;
|
|
675
|
+
const yi = ring2[i].y;
|
|
676
|
+
const xj = ring2[j].x;
|
|
677
|
+
const yj = ring2[j].y;
|
|
678
|
+
const intersect2 = yi > point.y !== yj > point.y && point.x < (xj - xi) * (point.y - yi) / (yj - yi + 0) + xi;
|
|
679
|
+
if (intersect2) inside = !inside;
|
|
680
|
+
}
|
|
681
|
+
return inside;
|
|
682
|
+
}
|
|
683
|
+
function unionBounds(a, b) {
|
|
684
|
+
let minX = Infinity;
|
|
685
|
+
let minY = Infinity;
|
|
686
|
+
let maxX = -Infinity;
|
|
687
|
+
let maxY = -Infinity;
|
|
688
|
+
for (const g of [a, b]) {
|
|
689
|
+
for (const p of g.paths) {
|
|
690
|
+
for (const ring2 of p.rings) {
|
|
691
|
+
for (const v of ring2) {
|
|
692
|
+
minX = Math.min(minX, v.x);
|
|
693
|
+
minY = Math.min(minY, v.y);
|
|
694
|
+
maxX = Math.max(maxX, v.x);
|
|
695
|
+
maxY = Math.max(maxY, v.y);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
return { minX, minY, maxX, maxY };
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// src/segmentSet/index.ts
|
|
704
|
+
function f32(n) {
|
|
705
|
+
return Math.fround(n);
|
|
706
|
+
}
|
|
707
|
+
function parallelSegments(centerX, centerY, length, spacing, angle, n) {
|
|
708
|
+
const edges = [];
|
|
709
|
+
if (n < 1) return edges;
|
|
710
|
+
const center = vec2(f32(centerX), f32(centerY));
|
|
711
|
+
let dx = f32(Math.cos(angle + Math.PI / 2) * spacing);
|
|
712
|
+
let dy = f32(Math.sin(angle + Math.PI / 2) * spacing);
|
|
713
|
+
const cos = f32(Math.cos(angle));
|
|
714
|
+
const sin = f32(Math.sin(angle));
|
|
715
|
+
const l = f32(length);
|
|
716
|
+
let i;
|
|
717
|
+
let offX = 0;
|
|
718
|
+
let offY = 0;
|
|
719
|
+
if (n % 2 === 1) {
|
|
720
|
+
const a = vec2(f32(center.x + cos * l), f32(center.y + sin * l));
|
|
721
|
+
const b = vec2(f32(center.x + cos * -l), f32(center.y + sin * -l));
|
|
722
|
+
edges.push(segment(a, b));
|
|
723
|
+
i = 1;
|
|
724
|
+
} else {
|
|
725
|
+
dx = f32(dx / 2);
|
|
726
|
+
dy = f32(dy / 2);
|
|
727
|
+
i = 1;
|
|
728
|
+
{
|
|
729
|
+
let a = vec2(f32(center.x + dx * i), f32(center.y + dy * i));
|
|
730
|
+
const b = vec2(f32(a.x + cos * l), f32(a.y + sin * l));
|
|
731
|
+
a = vec2(f32(a.x + cos * -l), f32(a.y + sin * -l));
|
|
732
|
+
edges.push(segment(a, b));
|
|
733
|
+
}
|
|
734
|
+
{
|
|
735
|
+
let a = vec2(f32(center.x + dx * -i), f32(center.y + dy * -i));
|
|
736
|
+
const b = vec2(f32(a.x + cos * l), f32(a.y + sin * l));
|
|
737
|
+
a = vec2(f32(a.x + cos * -l), f32(a.y + sin * -l));
|
|
738
|
+
edges.push(segment(a, b));
|
|
739
|
+
}
|
|
740
|
+
i = 2;
|
|
741
|
+
dx = f32(dx * 2);
|
|
742
|
+
dy = f32(dy * 2);
|
|
743
|
+
offX = f32(dx / 2);
|
|
744
|
+
offY = f32(dy / 2);
|
|
745
|
+
}
|
|
746
|
+
for (; i < 1 + Math.floor(n / 2); i++) {
|
|
747
|
+
{
|
|
748
|
+
let a = vec2(
|
|
749
|
+
f32(center.x + dx * i - offX),
|
|
750
|
+
f32(center.y + dy * i - offY)
|
|
751
|
+
);
|
|
752
|
+
const b = vec2(f32(a.x + cos * l), f32(a.y + sin * l));
|
|
753
|
+
a = vec2(f32(a.x + cos * -l), f32(a.y + sin * -l));
|
|
754
|
+
edges.push(segment(a, b));
|
|
755
|
+
}
|
|
756
|
+
{
|
|
757
|
+
let a = vec2(
|
|
758
|
+
f32(center.x + dx * -i + offX),
|
|
759
|
+
f32(center.y + dy * -i + offY)
|
|
760
|
+
);
|
|
761
|
+
const b = vec2(f32(a.x + cos * l), f32(a.y + sin * l));
|
|
762
|
+
a = vec2(f32(a.x + cos * -l), f32(a.y + sin * -l));
|
|
763
|
+
edges.push(segment(a, b));
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
return edges;
|
|
767
|
+
}
|
|
768
|
+
function filterByMinLength(segments, minLength) {
|
|
769
|
+
return segments.filter((s) => segmentLength(s) >= minLength);
|
|
770
|
+
}
|
|
771
|
+
function filterAxisAligned(segments, angleDelta) {
|
|
772
|
+
return segments.filter((s) => {
|
|
773
|
+
const ang = Math.abs(Math.atan2(s.b.y - s.a.y, s.b.x - s.a.x));
|
|
774
|
+
const norm = Math.min(ang % Math.PI, Math.PI - ang % Math.PI);
|
|
775
|
+
const nearH = norm <= angleDelta || Math.abs(norm - Math.PI) <= angleDelta;
|
|
776
|
+
const nearV = Math.abs(norm - Math.PI / 2) <= angleDelta;
|
|
777
|
+
return !(nearH || nearV);
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
function segmentLength(s) {
|
|
781
|
+
const dx = s.b.x - s.a.x;
|
|
782
|
+
const dy = s.b.y - s.a.y;
|
|
783
|
+
return Math.hypot(dx, dy);
|
|
784
|
+
}
|
|
785
|
+
function segmentsToOpenPaths(segments) {
|
|
786
|
+
return segments.map((s) => ({
|
|
787
|
+
rings: [[s.a, s.b]],
|
|
788
|
+
closed: false
|
|
789
|
+
}));
|
|
790
|
+
}
|
|
791
|
+
var segmentSet = {
|
|
792
|
+
parallelSegments,
|
|
793
|
+
filterByMinLength,
|
|
794
|
+
filterAxisAligned,
|
|
795
|
+
segmentLength,
|
|
796
|
+
segmentsToOpenPaths
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
// src/segmentSet/clip.ts
|
|
800
|
+
function clipSegmentToPath(seg, clip) {
|
|
801
|
+
if (!clip.closed || !clip.rings[0]) return [];
|
|
802
|
+
const hits = [];
|
|
803
|
+
const pushHit = (t, p) => {
|
|
804
|
+
if (t < -1e-9 || t > 1 + 1e-9) return;
|
|
805
|
+
const tt = Math.min(1, Math.max(0, t));
|
|
806
|
+
for (const h of hits) {
|
|
807
|
+
if (Math.abs(h.t - tt) < 1e-9) return;
|
|
808
|
+
}
|
|
809
|
+
hits.push({ t: tt, p });
|
|
810
|
+
};
|
|
811
|
+
if (containsPoint(clip, seg.a)) pushHit(0, seg.a);
|
|
812
|
+
if (containsPoint(clip, seg.b)) pushHit(1, seg.b);
|
|
813
|
+
for (const ring2 of clip.rings) {
|
|
814
|
+
for (let i = 0; i < ring2.length; i++) {
|
|
815
|
+
const e0 = ring2[i];
|
|
816
|
+
const e1 = ring2[(i + 1) % ring2.length];
|
|
817
|
+
const hit = segmentIntersection(seg.a, seg.b, e0, e1);
|
|
818
|
+
if (hit) pushHit(hit.t, hit.p);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
hits.sort((a, b) => a.t - b.t);
|
|
822
|
+
if (hits.length < 2) return [];
|
|
823
|
+
const out = [];
|
|
824
|
+
for (let i = 0; i + 1 < hits.length; i++) {
|
|
825
|
+
const h0 = hits[i];
|
|
826
|
+
const h1 = hits[i + 1];
|
|
827
|
+
if (h1.t - h0.t < 1e-9) continue;
|
|
828
|
+
const mid3 = vec2((h0.p.x + h1.p.x) / 2, (h0.p.y + h1.p.y) / 2);
|
|
829
|
+
if (containsPoint(clip, mid3)) {
|
|
830
|
+
out.push(segment(h0.p, h1.p));
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
return out;
|
|
834
|
+
}
|
|
835
|
+
function clipSegmentsToPath(segments, clip) {
|
|
836
|
+
const out = [];
|
|
837
|
+
for (const s of segments) {
|
|
838
|
+
out.push(...clipSegmentToPath(s, clip));
|
|
839
|
+
}
|
|
840
|
+
return out;
|
|
841
|
+
}
|
|
842
|
+
function segmentIntersection(a, b, c, d) {
|
|
843
|
+
const rX = b.x - a.x;
|
|
844
|
+
const rY = b.y - a.y;
|
|
845
|
+
const sX = d.x - c.x;
|
|
846
|
+
const sY = d.y - c.y;
|
|
847
|
+
const denom = rX * sY - rY * sX;
|
|
848
|
+
if (Math.abs(denom) < 1e-12) return null;
|
|
849
|
+
const t = ((c.x - a.x) * sY - (c.y - a.y) * sX) / denom;
|
|
850
|
+
const u = ((c.x - a.x) * rY - (c.y - a.y) * rX) / denom;
|
|
851
|
+
if (t < -1e-9 || t > 1 + 1e-9 || u < -1e-9 || u > 1 + 1e-9) return null;
|
|
852
|
+
return {
|
|
853
|
+
t,
|
|
854
|
+
p: vec2(a.x + t * rX, a.y + t * rY)
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// src/hatch/index.ts
|
|
859
|
+
function parallel(path3, options = {}) {
|
|
860
|
+
if (!path3.closed || !path3.rings[0]) {
|
|
861
|
+
throw new Error("hatch.parallel requires a closed path");
|
|
862
|
+
}
|
|
863
|
+
const bounds2 = pathBounds(path3);
|
|
864
|
+
const center = options.center ?? {
|
|
865
|
+
x: (bounds2.minX + bounds2.maxX) / 2,
|
|
866
|
+
y: (bounds2.minY + bounds2.maxY) / 2
|
|
867
|
+
};
|
|
868
|
+
const diag = Math.hypot(bounds2.maxX - bounds2.minX, bounds2.maxY - bounds2.minY);
|
|
869
|
+
const length = options.length ?? Math.max(diag, 1e-6);
|
|
870
|
+
const spacing = Math.max(options.spacing ?? 0.15, 1e-9);
|
|
871
|
+
const angle = options.angle ?? Math.PI / 4;
|
|
872
|
+
const count = options.count ?? hatchLineCount(bounds2, spacing, angle);
|
|
873
|
+
const raw = parallelSegments(
|
|
874
|
+
center.x,
|
|
875
|
+
center.y,
|
|
876
|
+
length,
|
|
877
|
+
spacing,
|
|
878
|
+
angle,
|
|
879
|
+
count
|
|
880
|
+
);
|
|
881
|
+
return clipSegmentsToPath(raw, path3);
|
|
882
|
+
}
|
|
883
|
+
function cross(path3, options = {}) {
|
|
884
|
+
const angle = options.angle ?? Math.PI / 4;
|
|
885
|
+
const a = parallel(path3, options);
|
|
886
|
+
const b = parallel(path3, { ...options, angle: angle + Math.PI / 2 });
|
|
887
|
+
return [...a, ...b];
|
|
888
|
+
}
|
|
889
|
+
function hatchLineCount(bounds2, spacing, angle) {
|
|
890
|
+
const nx = Math.cos(angle + Math.PI / 2);
|
|
891
|
+
const ny = Math.sin(angle + Math.PI / 2);
|
|
892
|
+
const corners = [
|
|
893
|
+
[bounds2.minX, bounds2.minY],
|
|
894
|
+
[bounds2.maxX, bounds2.minY],
|
|
895
|
+
[bounds2.maxX, bounds2.maxY],
|
|
896
|
+
[bounds2.minX, bounds2.maxY]
|
|
897
|
+
];
|
|
898
|
+
let minP = Infinity;
|
|
899
|
+
let maxP = -Infinity;
|
|
900
|
+
for (const [x, y] of corners) {
|
|
901
|
+
const p = x * nx + y * ny;
|
|
902
|
+
minP = Math.min(minP, p);
|
|
903
|
+
maxP = Math.max(maxP, p);
|
|
904
|
+
}
|
|
905
|
+
const extent = maxP - minP;
|
|
906
|
+
return Math.max(1, Math.ceil(extent / spacing) + 1);
|
|
907
|
+
}
|
|
908
|
+
function pathBounds(path3) {
|
|
909
|
+
let minX = Infinity;
|
|
910
|
+
let minY = Infinity;
|
|
911
|
+
let maxX = -Infinity;
|
|
912
|
+
let maxY = -Infinity;
|
|
913
|
+
for (const ring2 of path3.rings) {
|
|
914
|
+
for (const v of ring2) {
|
|
915
|
+
minX = Math.min(minX, v.x);
|
|
916
|
+
minY = Math.min(minY, v.y);
|
|
917
|
+
maxX = Math.max(maxX, v.x);
|
|
918
|
+
maxY = Math.max(maxY, v.y);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
return { minX, minY, maxX, maxY };
|
|
922
|
+
}
|
|
923
|
+
var hatch = {
|
|
924
|
+
parallel,
|
|
925
|
+
cross
|
|
926
|
+
};
|
|
927
|
+
|
|
928
|
+
// src/circlePacking/index.ts
|
|
929
|
+
function squareLatticePack(path3, diameter, mode = "overlap") {
|
|
930
|
+
const d = Math.max(diameter, 0.1);
|
|
931
|
+
const r = d / 2;
|
|
932
|
+
const b = bounds(path3);
|
|
933
|
+
const out = [];
|
|
934
|
+
const accept = mode === "contained" ? circleContainedInPath : circleOverlapsPath;
|
|
935
|
+
const inset = mode === "contained" ? r : 0;
|
|
936
|
+
const { ox, oy, nX, nY } = centeredLatticeGrid(b, d, d, inset, d);
|
|
937
|
+
if (nX < 1 || nY < 1) return out;
|
|
938
|
+
for (let i = 0; i < nX; i++) {
|
|
939
|
+
for (let j = 0; j < nY; j++) {
|
|
940
|
+
const c = circle(ox + i * d, oy + j * d, r);
|
|
941
|
+
if (accept(c, path3)) out.push(c);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
return out;
|
|
945
|
+
}
|
|
946
|
+
function hexLatticePack(path3, diameter, mode = "overlap") {
|
|
947
|
+
const d = Math.max(diameter, 0.1);
|
|
948
|
+
const r = d / 2;
|
|
949
|
+
const b = bounds(path3);
|
|
950
|
+
const z = r * Math.sqrt(3);
|
|
951
|
+
const out = [];
|
|
952
|
+
const accept = mode === "contained" ? circleContainedInPath : circleOverlapsPath;
|
|
953
|
+
const inset = mode === "contained" ? r : 0;
|
|
954
|
+
const { ox, nX, nY } = centeredLatticeGrid(b, z, d, inset, d);
|
|
955
|
+
if (nX < 1 || nY < 1) return out;
|
|
956
|
+
const innerH = b.maxY - b.minY - 2 * inset;
|
|
957
|
+
const extentY = (nY - 1) * d + r;
|
|
958
|
+
const oy = b.minY + inset + (innerH - extentY) / 2 + r;
|
|
959
|
+
let offset = 0;
|
|
960
|
+
for (let i = 0; i < nX; i++) {
|
|
961
|
+
offset = offset === r ? 0 : r;
|
|
962
|
+
for (let j = 0; j < nY; j++) {
|
|
963
|
+
const c = circle(ox + i * z, oy + j * d - offset, r);
|
|
964
|
+
if (accept(c, path3)) out.push(c);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
return out;
|
|
968
|
+
}
|
|
969
|
+
function centeredLatticeGrid(b, stepX, stepY, inset, endPad) {
|
|
970
|
+
const innerW = b.maxX - b.minX - 2 * inset;
|
|
971
|
+
const innerH = b.maxY - b.minY - 2 * inset;
|
|
972
|
+
if (innerW < -1e-9 || innerH < -1e-9) {
|
|
973
|
+
return { ox: 0, oy: 0, nX: 0, nY: 0 };
|
|
974
|
+
}
|
|
975
|
+
let nX;
|
|
976
|
+
let nY;
|
|
977
|
+
if (inset > 0) {
|
|
978
|
+
nX = Math.floor(innerW / stepX + 1e-9) + 1;
|
|
979
|
+
nY = Math.floor(innerH / stepY + 1e-9) + 1;
|
|
980
|
+
} else {
|
|
981
|
+
nX = Math.floor((innerW + endPad) / stepX - 1e-12) + 1;
|
|
982
|
+
nY = Math.floor((innerH + endPad) / stepY - 1e-12) + 1;
|
|
983
|
+
}
|
|
984
|
+
const spanX = (nX - 1) * stepX;
|
|
985
|
+
const spanY = (nY - 1) * stepY;
|
|
986
|
+
const ox = b.minX + inset + (innerW - spanX) / 2;
|
|
987
|
+
const oy = b.minY + inset + (innerH - spanY) / 2;
|
|
988
|
+
return { ox, oy, nX, nY };
|
|
989
|
+
}
|
|
990
|
+
function stochasticPack(path3, points, minRadius, seed = 1) {
|
|
991
|
+
const rng = mulberry32(seed >>> 0);
|
|
992
|
+
const b = bounds(path3);
|
|
993
|
+
const packing = [];
|
|
994
|
+
for (let attempt = 0; attempt < points; attempt++) {
|
|
995
|
+
const p = vecInBounds(b, rng);
|
|
996
|
+
if (!containsPoint(path3, p)) continue;
|
|
997
|
+
const maxR = clearanceRadius(p, path3, packing);
|
|
998
|
+
if (maxR < minRadius) continue;
|
|
999
|
+
packing.push(circle(p.x, p.y, maxR));
|
|
1000
|
+
}
|
|
1001
|
+
return packing;
|
|
1002
|
+
}
|
|
1003
|
+
function maximumInscribedPack(path3, n, tolerance = 1) {
|
|
1004
|
+
return obstaclePack(path3, [], n, tolerance);
|
|
1005
|
+
}
|
|
1006
|
+
function maximumInscribedPackUntil(path3, minRadius, tolerance = 1) {
|
|
1007
|
+
const packing = [];
|
|
1008
|
+
const tol = Math.max(0.01, tolerance);
|
|
1009
|
+
const minR = Math.max(0.01, minRadius);
|
|
1010
|
+
for (; ; ) {
|
|
1011
|
+
const next = findLargestEmptyCircle(path3, packing, tol);
|
|
1012
|
+
if (!next || next.r < minR) break;
|
|
1013
|
+
packing.push(next);
|
|
1014
|
+
}
|
|
1015
|
+
return packing;
|
|
1016
|
+
}
|
|
1017
|
+
function obstaclePack(path3, obstacles, n, tolerance = 1) {
|
|
1018
|
+
const packing = obstacles.map((c) => ({ ...c }));
|
|
1019
|
+
const placed = [];
|
|
1020
|
+
const tol = Math.max(0.01, tolerance);
|
|
1021
|
+
for (let i = 0; i < n; i++) {
|
|
1022
|
+
const next = findLargestEmptyCircle(path3, packing, tol);
|
|
1023
|
+
if (!next || next.r <= 1e-9) break;
|
|
1024
|
+
packing.push(next);
|
|
1025
|
+
placed.push(next);
|
|
1026
|
+
}
|
|
1027
|
+
return placed;
|
|
1028
|
+
}
|
|
1029
|
+
function frontChainPack(path3, radiusMin, radiusMax, seed = 1) {
|
|
1030
|
+
let rMin = Math.max(1e-6, Math.min(radiusMin, radiusMax));
|
|
1031
|
+
let rMax = Math.max(rMin, Math.max(radiusMin, radiusMax));
|
|
1032
|
+
const rng = mulberry32(seed >>> 0);
|
|
1033
|
+
const b = bounds(path3);
|
|
1034
|
+
const packing = [];
|
|
1035
|
+
const area2 = Math.max(1e-6, (b.maxX - b.minX) * (b.maxY - b.minY));
|
|
1036
|
+
const capacity = Math.max(
|
|
1037
|
+
4,
|
|
1038
|
+
Math.ceil(area2 / (Math.PI * rMin * rMin) * 2)
|
|
1039
|
+
);
|
|
1040
|
+
const trySeed = () => {
|
|
1041
|
+
const p = vecInBounds(b, rng);
|
|
1042
|
+
const r = rMin + rng() * (rMax - rMin);
|
|
1043
|
+
const c = circle(p.x, p.y, r);
|
|
1044
|
+
if (!overlapsAny(c, packing) && circleOverlapsPath(c, path3)) {
|
|
1045
|
+
packing.push(c);
|
|
1046
|
+
return true;
|
|
1047
|
+
}
|
|
1048
|
+
return false;
|
|
1049
|
+
};
|
|
1050
|
+
for (let i = 0; i < 8; i++) trySeed();
|
|
1051
|
+
let fails = 0;
|
|
1052
|
+
while (packing.length < capacity) {
|
|
1053
|
+
const stallLimit = Math.max(500, packing.length * 12);
|
|
1054
|
+
if (fails >= stallLimit) break;
|
|
1055
|
+
if (packing.length === 0) {
|
|
1056
|
+
if (trySeed()) fails = 0;
|
|
1057
|
+
else fails++;
|
|
1058
|
+
continue;
|
|
1059
|
+
}
|
|
1060
|
+
if (fails > 0 && fails % 50 === 0) {
|
|
1061
|
+
if (trySeed()) {
|
|
1062
|
+
fails = 0;
|
|
1063
|
+
continue;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
const base = packing[Math.floor(rng() * packing.length)];
|
|
1067
|
+
const ang = rng() * Math.PI * 2;
|
|
1068
|
+
const r = rMin + rng() * (rMax - rMin);
|
|
1069
|
+
const dist2 = base.r + r;
|
|
1070
|
+
const c = circle(
|
|
1071
|
+
base.x + Math.cos(ang) * dist2,
|
|
1072
|
+
base.y + Math.sin(ang) * dist2,
|
|
1073
|
+
r
|
|
1074
|
+
);
|
|
1075
|
+
if (c.x < b.minX - rMax || c.x > b.maxX + rMax || c.y < b.minY - rMax || c.y > b.maxY + rMax) {
|
|
1076
|
+
fails++;
|
|
1077
|
+
continue;
|
|
1078
|
+
}
|
|
1079
|
+
if (!overlapsAny(c, packing) && circleOverlapsPath(c, path3)) {
|
|
1080
|
+
packing.push(c);
|
|
1081
|
+
fails = 0;
|
|
1082
|
+
} else {
|
|
1083
|
+
fails++;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
return packing.filter((c) => circleOverlapsPath(c, path3));
|
|
1087
|
+
}
|
|
1088
|
+
function repulsionPack(path3, radiusMin, radiusMax, seed = 1, iterations = 80) {
|
|
1089
|
+
const rMin = Math.max(1e-6, Math.min(radiusMin, radiusMax));
|
|
1090
|
+
const rMax = Math.max(rMin, Math.max(radiusMin, radiusMax));
|
|
1091
|
+
const rng = mulberry32(seed >>> 0);
|
|
1092
|
+
const b = bounds(path3);
|
|
1093
|
+
const area2 = Math.max(1e-6, (b.maxX - b.minX) * (b.maxY - b.minY));
|
|
1094
|
+
const avgR = (rMin + rMax) / 2;
|
|
1095
|
+
const target = Math.max(
|
|
1096
|
+
4,
|
|
1097
|
+
Math.floor(area2 / (Math.PI * avgR * avgR) * 0.6)
|
|
1098
|
+
);
|
|
1099
|
+
const circles = [];
|
|
1100
|
+
for (let i = 0; i < target; i++) {
|
|
1101
|
+
const p = vecInBounds(b, rng);
|
|
1102
|
+
circles.push(circle(p.x, p.y, rMin + rng() * (rMax - rMin)));
|
|
1103
|
+
}
|
|
1104
|
+
for (let iter = 0; iter < iterations; iter++) {
|
|
1105
|
+
for (let i = 0; i < circles.length; i++) {
|
|
1106
|
+
for (let j = i + 1; j < circles.length; j++) {
|
|
1107
|
+
const a = circles[i];
|
|
1108
|
+
const bC = circles[j];
|
|
1109
|
+
const dx = bC.x - a.x;
|
|
1110
|
+
const dy = bC.y - a.y;
|
|
1111
|
+
const dist2 = Math.hypot(dx, dy) || 1e-9;
|
|
1112
|
+
const minDist = a.r + bC.r;
|
|
1113
|
+
if (dist2 >= minDist) continue;
|
|
1114
|
+
const push = (minDist - dist2) / 2 * 0.5;
|
|
1115
|
+
const ux = dx / dist2;
|
|
1116
|
+
const uy = dy / dist2;
|
|
1117
|
+
a.x -= ux * push;
|
|
1118
|
+
a.y -= uy * push;
|
|
1119
|
+
bC.x += ux * push;
|
|
1120
|
+
bC.y += uy * push;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
for (const c of circles) {
|
|
1124
|
+
c.x = Math.min(b.maxX + c.r, Math.max(b.minX - c.r, c.x));
|
|
1125
|
+
c.y = Math.min(b.maxY + c.r, Math.max(b.minY - c.r, c.y));
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
return circles.filter((c) => circleOverlapsPath(c, path3));
|
|
1129
|
+
}
|
|
1130
|
+
function circleOverlapsPath(c, path3) {
|
|
1131
|
+
const center = { x: c.x, y: c.y };
|
|
1132
|
+
const dist2 = distanceToPolygon(center, path3);
|
|
1133
|
+
return dist2 <= c.r * 0.95;
|
|
1134
|
+
}
|
|
1135
|
+
function circleContainedInPath(c, path3) {
|
|
1136
|
+
const center = { x: c.x, y: c.y };
|
|
1137
|
+
if (!containsPoint(path3, center)) return false;
|
|
1138
|
+
return distanceToBoundary(center, path3) + 1e-9 >= c.r;
|
|
1139
|
+
}
|
|
1140
|
+
function distanceToPolygon(p, path3) {
|
|
1141
|
+
if (containsPoint(path3, p)) return 0;
|
|
1142
|
+
return distanceToBoundary(p, path3);
|
|
1143
|
+
}
|
|
1144
|
+
function findLargestEmptyCircle(path3, obstacles, tolerance) {
|
|
1145
|
+
const b = bounds(path3);
|
|
1146
|
+
const diag = Math.hypot(b.maxX - b.minX, b.maxY - b.minY) || 1;
|
|
1147
|
+
const step = Math.max(diag * 0.02 * Math.sqrt(tolerance), diag * 0.01);
|
|
1148
|
+
let best = null;
|
|
1149
|
+
for (let x = b.minX; x <= b.maxX; x += step) {
|
|
1150
|
+
for (let y = b.minY; y <= b.maxY; y += step) {
|
|
1151
|
+
const p = { x, y };
|
|
1152
|
+
if (!containsPoint(path3, p)) continue;
|
|
1153
|
+
const r = clearanceRadius(p, path3, obstacles);
|
|
1154
|
+
if (!best || r > best.r) best = circle(p.x, p.y, r);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
if (!best) return null;
|
|
1158
|
+
let cur = best;
|
|
1159
|
+
let span = step;
|
|
1160
|
+
for (let iter = 0; iter < 8; iter++) {
|
|
1161
|
+
let improved = cur;
|
|
1162
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
1163
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
1164
|
+
if (dx === 0 && dy === 0) continue;
|
|
1165
|
+
const p = { x: cur.x + dx * span, y: cur.y + dy * span };
|
|
1166
|
+
if (!containsPoint(path3, p)) continue;
|
|
1167
|
+
const r = clearanceRadius(p, path3, obstacles);
|
|
1168
|
+
if (r > improved.r) improved = circle(p.x, p.y, r);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
cur = improved;
|
|
1172
|
+
span *= 0.5;
|
|
1173
|
+
}
|
|
1174
|
+
return cur;
|
|
1175
|
+
}
|
|
1176
|
+
function clearanceRadius(p, path3, obstacles) {
|
|
1177
|
+
let r = distanceToBoundary(p, path3);
|
|
1178
|
+
for (const c of obstacles) {
|
|
1179
|
+
r = Math.min(r, Math.hypot(p.x - c.x, p.y - c.y) - c.r);
|
|
1180
|
+
}
|
|
1181
|
+
return Math.max(0, r);
|
|
1182
|
+
}
|
|
1183
|
+
function overlapsAny(c, packing) {
|
|
1184
|
+
for (const o of packing) {
|
|
1185
|
+
if (Math.hypot(c.x - o.x, c.y - o.y) < c.r + o.r - 1e-9) return true;
|
|
1186
|
+
}
|
|
1187
|
+
return false;
|
|
1188
|
+
}
|
|
1189
|
+
function distanceToBoundary(p, path3) {
|
|
1190
|
+
let min = Infinity;
|
|
1191
|
+
for (const ring2 of path3.rings) {
|
|
1192
|
+
for (let i = 0; i < ring2.length; i++) {
|
|
1193
|
+
const a = ring2[i];
|
|
1194
|
+
const b = ring2[(i + 1) % ring2.length];
|
|
1195
|
+
min = Math.min(min, pointSegmentDistance(p, a, b));
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
return min;
|
|
1199
|
+
}
|
|
1200
|
+
function pointSegmentDistance(p, a, b) {
|
|
1201
|
+
const abx = b.x - a.x;
|
|
1202
|
+
const aby = b.y - a.y;
|
|
1203
|
+
const len2 = abx * abx + aby * aby;
|
|
1204
|
+
if (len2 < 1e-18) return Math.hypot(p.x - a.x, p.y - a.y);
|
|
1205
|
+
let t = ((p.x - a.x) * abx + (p.y - a.y) * aby) / len2;
|
|
1206
|
+
t = Math.max(0, Math.min(1, t));
|
|
1207
|
+
return Math.hypot(p.x - (a.x + t * abx), p.y - (a.y + t * aby));
|
|
1208
|
+
}
|
|
1209
|
+
function vecInBounds(b, rng) {
|
|
1210
|
+
return {
|
|
1211
|
+
x: b.minX + rng() * (b.maxX - b.minX),
|
|
1212
|
+
y: b.minY + rng() * (b.maxY - b.minY)
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
function mulberry32(a) {
|
|
1216
|
+
return function() {
|
|
1217
|
+
let t = a += 1831565813;
|
|
1218
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
1219
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
1220
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
var circlePacking = {
|
|
1224
|
+
squareLatticePack,
|
|
1225
|
+
hexLatticePack,
|
|
1226
|
+
stochasticPack,
|
|
1227
|
+
maximumInscribedPack,
|
|
1228
|
+
maximumInscribedPackUntil,
|
|
1229
|
+
obstaclePack,
|
|
1230
|
+
frontChainPack,
|
|
1231
|
+
repulsionPack,
|
|
1232
|
+
circleOverlapsPath,
|
|
1233
|
+
circleContainedInPath
|
|
1234
|
+
};
|
|
1235
|
+
function buffer(path3, delta, options = {}) {
|
|
1236
|
+
const subject = pathToPathsD(path3);
|
|
1237
|
+
const join = options.join === "miter" ? JoinType.Miter : options.join === "square" ? JoinType.Square : JoinType.Round;
|
|
1238
|
+
const inflated = inflatePathsD(subject, delta, join, EndType.Polygon);
|
|
1239
|
+
return pathsDToGroup(inflated);
|
|
1240
|
+
}
|
|
1241
|
+
function erosionDilation(p, amount) {
|
|
1242
|
+
const a = Math.abs(amount);
|
|
1243
|
+
if (a < 1e-12) return group([p]);
|
|
1244
|
+
const eroded = buffer(p, -a);
|
|
1245
|
+
if (eroded.paths.length === 0) return eroded;
|
|
1246
|
+
const dilated = [];
|
|
1247
|
+
for (const q of eroded.paths) {
|
|
1248
|
+
dilated.push(...buffer(q, a).paths);
|
|
1249
|
+
}
|
|
1250
|
+
return group(dilated);
|
|
1251
|
+
}
|
|
1252
|
+
function dilationErosion(p, amount) {
|
|
1253
|
+
const a = Math.abs(amount);
|
|
1254
|
+
if (a < 1e-12) return group([p]);
|
|
1255
|
+
const dilated = buffer(p, a);
|
|
1256
|
+
if (dilated.paths.length === 0) return dilated;
|
|
1257
|
+
const eroded = [];
|
|
1258
|
+
for (const q of dilated.paths) {
|
|
1259
|
+
eroded.push(...buffer(q, -a).paths);
|
|
1260
|
+
}
|
|
1261
|
+
return group(eroded);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// src/morphology/index.ts
|
|
1265
|
+
function simplify(p, epsilon) {
|
|
1266
|
+
if (epsilon <= 0 || p.rings.length === 0) return p;
|
|
1267
|
+
const rings = [];
|
|
1268
|
+
for (const ring2 of p.rings) {
|
|
1269
|
+
const simplified = simplifyPathD(ringToPathD(ring2), epsilon, p.closed);
|
|
1270
|
+
const out = pathDToRing(simplified);
|
|
1271
|
+
if (out.length >= (p.closed ? 3 : 2)) rings.push(out);
|
|
1272
|
+
}
|
|
1273
|
+
if (rings.length === 0) return path([], p.closed);
|
|
1274
|
+
return path(rings, p.closed);
|
|
1275
|
+
}
|
|
1276
|
+
function reducePrecision(p, decimals) {
|
|
1277
|
+
const f = 10 ** Math.max(0, Math.floor(decimals));
|
|
1278
|
+
const round = (n) => Math.round(n * f) / f;
|
|
1279
|
+
return path(
|
|
1280
|
+
p.rings.map(
|
|
1281
|
+
(ring2) => normalizeRing(ring2.map((v) => ({ x: round(v.x), y: round(v.y) })))
|
|
1282
|
+
),
|
|
1283
|
+
p.closed
|
|
1284
|
+
);
|
|
1285
|
+
}
|
|
1286
|
+
function chaikinCut(p, iterations = 1, ratio = 0.25) {
|
|
1287
|
+
const r = Math.min(0.49, Math.max(0.01, ratio));
|
|
1288
|
+
let rings = p.rings;
|
|
1289
|
+
for (let i = 0; i < Math.max(1, Math.floor(iterations)); i++) {
|
|
1290
|
+
rings = rings.map((ring2) => chaikinRing(ring2, r, p.closed));
|
|
1291
|
+
}
|
|
1292
|
+
return path(rings, p.closed);
|
|
1293
|
+
}
|
|
1294
|
+
function smooth(p, iterations = 2) {
|
|
1295
|
+
return chaikinCut(p, iterations);
|
|
1296
|
+
}
|
|
1297
|
+
function radialWarp(p, amplitude, frequency = 3, phase = 0) {
|
|
1298
|
+
const c = centroid(p);
|
|
1299
|
+
return mapVerts(p, (v) => {
|
|
1300
|
+
const dx = v.x - c.x;
|
|
1301
|
+
const dy = v.y - c.y;
|
|
1302
|
+
const dist2 = Math.hypot(dx, dy);
|
|
1303
|
+
if (dist2 < 1e-12) return v;
|
|
1304
|
+
const theta = Math.atan2(dy, dx);
|
|
1305
|
+
const s = 1 + amplitude * Math.cos(frequency * theta + phase);
|
|
1306
|
+
return { x: c.x + dx * s, y: c.y + dy * s };
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
function sineWarp(p, amplitude, frequency = 1, phase = 0) {
|
|
1310
|
+
return mapVerts(p, (v) => ({
|
|
1311
|
+
x: v.x,
|
|
1312
|
+
y: v.y + amplitude * Math.sin(2 * Math.PI * frequency * v.x + phase)
|
|
1313
|
+
}));
|
|
1314
|
+
}
|
|
1315
|
+
function minkSum(a, b) {
|
|
1316
|
+
const pattern = ringToPathD(b.rings[0] ?? []);
|
|
1317
|
+
const subject = ringToPathD(a.rings[0] ?? []);
|
|
1318
|
+
return pathsDToGroup(minkowskiSumD(pattern, subject, true));
|
|
1319
|
+
}
|
|
1320
|
+
function minkDifference(a, b) {
|
|
1321
|
+
const pattern = ringToPathD(b.rings[0] ?? []);
|
|
1322
|
+
const subject = ringToPathD(a.rings[0] ?? []);
|
|
1323
|
+
return pathsDToGroup(minkowskiDiffD(pattern, subject, true));
|
|
1324
|
+
}
|
|
1325
|
+
function chaikinRing(ring2, ratio, closed) {
|
|
1326
|
+
if (ring2.length < 2) return ring2.map((v) => ({ ...v }));
|
|
1327
|
+
const out = [];
|
|
1328
|
+
const n = ring2.length;
|
|
1329
|
+
const edges = closed ? n : n - 1;
|
|
1330
|
+
for (let i = 0; i < edges; i++) {
|
|
1331
|
+
const a = ring2[i];
|
|
1332
|
+
const b = ring2[(i + 1) % n];
|
|
1333
|
+
out.push({
|
|
1334
|
+
x: a.x * (1 - ratio) + b.x * ratio,
|
|
1335
|
+
y: a.y * (1 - ratio) + b.y * ratio
|
|
1336
|
+
});
|
|
1337
|
+
out.push({
|
|
1338
|
+
x: a.x * ratio + b.x * (1 - ratio),
|
|
1339
|
+
y: a.y * ratio + b.y * (1 - ratio)
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
if (!closed) {
|
|
1343
|
+
out.unshift({ ...ring2[0] });
|
|
1344
|
+
out.push({ ...ring2[n - 1] });
|
|
1345
|
+
}
|
|
1346
|
+
return closed ? normalizeRing(out) : out;
|
|
1347
|
+
}
|
|
1348
|
+
function mapVerts(p, fn) {
|
|
1349
|
+
return path(
|
|
1350
|
+
p.rings.map((ring2) => normalizeRing(ring2.map(fn))),
|
|
1351
|
+
p.closed
|
|
1352
|
+
);
|
|
1353
|
+
}
|
|
1354
|
+
var morphology = {
|
|
1355
|
+
buffer,
|
|
1356
|
+
erosionDilation,
|
|
1357
|
+
dilationErosion,
|
|
1358
|
+
simplify,
|
|
1359
|
+
reducePrecision,
|
|
1360
|
+
chaikinCut,
|
|
1361
|
+
smooth,
|
|
1362
|
+
radialWarp,
|
|
1363
|
+
sineWarp,
|
|
1364
|
+
minkSum,
|
|
1365
|
+
minkDifference
|
|
1366
|
+
};
|
|
1367
|
+
|
|
1368
|
+
// src/transformation/index.ts
|
|
1369
|
+
function mapPath(p, fn) {
|
|
1370
|
+
return path(
|
|
1371
|
+
p.rings.map((ring2) => normalizeRing(ring2.map(fn))),
|
|
1372
|
+
p.closed
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
function translate(p, dx, dy) {
|
|
1376
|
+
return mapPath(p, (v) => ({ x: v.x + dx, y: v.y + dy }));
|
|
1377
|
+
}
|
|
1378
|
+
function translateToOrigin(p) {
|
|
1379
|
+
const c = centroid(p);
|
|
1380
|
+
return translate(p, -c.x, -c.y);
|
|
1381
|
+
}
|
|
1382
|
+
function translateCentroidTo(p, target) {
|
|
1383
|
+
const c = centroid(p);
|
|
1384
|
+
return translate(p, target.x - c.x, target.y - c.y);
|
|
1385
|
+
}
|
|
1386
|
+
function rotate(p, angleRad, pivot) {
|
|
1387
|
+
const o = pivot ?? { x: 0, y: 0 };
|
|
1388
|
+
const cos = Math.cos(angleRad);
|
|
1389
|
+
const sin = Math.sin(angleRad);
|
|
1390
|
+
return mapPath(p, (v) => {
|
|
1391
|
+
const x = v.x - o.x;
|
|
1392
|
+
const y = v.y - o.y;
|
|
1393
|
+
return {
|
|
1394
|
+
x: o.x + x * cos - y * sin,
|
|
1395
|
+
y: o.y + x * sin + y * cos
|
|
1396
|
+
};
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
function rotateAroundCenter(p, angleRad) {
|
|
1400
|
+
return rotate(p, angleRad, centroid(p));
|
|
1401
|
+
}
|
|
1402
|
+
function scale(p, sx, sy = sx, pivot) {
|
|
1403
|
+
const o = pivot ?? { x: 0, y: 0 };
|
|
1404
|
+
return mapPath(p, (v) => ({
|
|
1405
|
+
x: o.x + (v.x - o.x) * sx,
|
|
1406
|
+
y: o.y + (v.y - o.y) * sy
|
|
1407
|
+
}));
|
|
1408
|
+
}
|
|
1409
|
+
function originScale(p, s) {
|
|
1410
|
+
return scale(p, s, s, { x: 0, y: 0 });
|
|
1411
|
+
}
|
|
1412
|
+
function flipHorizontal(p, x = 0) {
|
|
1413
|
+
return mapPath(p, (v) => ({ x: 2 * x - v.x, y: v.y }));
|
|
1414
|
+
}
|
|
1415
|
+
function flipVertical(p, y = 0) {
|
|
1416
|
+
return mapPath(p, (v) => ({ x: v.x, y: 2 * y - v.y }));
|
|
1417
|
+
}
|
|
1418
|
+
function resizeByWidth(p, width2) {
|
|
1419
|
+
const b = bounds(p);
|
|
1420
|
+
const w = b.maxX - b.minX;
|
|
1421
|
+
if (w < 1e-12) return clonePath(p);
|
|
1422
|
+
return scale(p, width2 / w, width2 / w, { x: b.minX, y: b.minY });
|
|
1423
|
+
}
|
|
1424
|
+
function resizeByHeight(p, height2) {
|
|
1425
|
+
const b = bounds(p);
|
|
1426
|
+
const h = b.maxY - b.minY;
|
|
1427
|
+
if (h < 1e-12) return clonePath(p);
|
|
1428
|
+
return scale(p, height2 / h, height2 / h, { x: b.minX, y: b.minY });
|
|
1429
|
+
}
|
|
1430
|
+
function translateCornerTo(p, target) {
|
|
1431
|
+
const b = bounds(p);
|
|
1432
|
+
return translate(p, target.x - b.minX, target.y - b.minY);
|
|
1433
|
+
}
|
|
1434
|
+
function shear(p, shx, shy = 0, pivot) {
|
|
1435
|
+
const o = pivot ?? { x: 0, y: 0 };
|
|
1436
|
+
return mapPath(p, (v) => {
|
|
1437
|
+
const x = v.x - o.x;
|
|
1438
|
+
const y = v.y - o.y;
|
|
1439
|
+
return {
|
|
1440
|
+
x: o.x + x + shx * y,
|
|
1441
|
+
y: o.y + y + shy * x
|
|
1442
|
+
};
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
var transformation = {
|
|
1446
|
+
translate,
|
|
1447
|
+
translateToOrigin,
|
|
1448
|
+
translateCentroidTo,
|
|
1449
|
+
translateCornerTo,
|
|
1450
|
+
rotate,
|
|
1451
|
+
rotateAroundCenter,
|
|
1452
|
+
scale,
|
|
1453
|
+
originScale,
|
|
1454
|
+
flipHorizontal,
|
|
1455
|
+
flipVertical,
|
|
1456
|
+
resizeByWidth,
|
|
1457
|
+
resizeByHeight,
|
|
1458
|
+
shear
|
|
1459
|
+
};
|
|
1460
|
+
|
|
1461
|
+
// src/processing/index.ts
|
|
1462
|
+
function extractPerimeter(p) {
|
|
1463
|
+
const exterior = p.rings[0];
|
|
1464
|
+
if (!exterior) return path([], true);
|
|
1465
|
+
return polygon(exterior);
|
|
1466
|
+
}
|
|
1467
|
+
function extractBoundary(p) {
|
|
1468
|
+
return extractPerimeter(p);
|
|
1469
|
+
}
|
|
1470
|
+
function extractHoles(p) {
|
|
1471
|
+
const holes = [];
|
|
1472
|
+
for (let i = 1; i < p.rings.length; i++) {
|
|
1473
|
+
const hole = p.rings[i];
|
|
1474
|
+
if (hole && hole.length >= 3) holes.push(polygon(hole));
|
|
1475
|
+
}
|
|
1476
|
+
return holes;
|
|
1477
|
+
}
|
|
1478
|
+
function densify(p, maxSegLen) {
|
|
1479
|
+
if (maxSegLen <= 0) return p;
|
|
1480
|
+
return path(
|
|
1481
|
+
p.rings.map((ring2) => densifyRing(ring2, maxSegLen, p.closed)),
|
|
1482
|
+
p.closed
|
|
1483
|
+
);
|
|
1484
|
+
}
|
|
1485
|
+
function densifyRing(ring2, maxSegLen, closed) {
|
|
1486
|
+
if (ring2.length < 2) return [...ring2];
|
|
1487
|
+
const out = [];
|
|
1488
|
+
const n = ring2.length;
|
|
1489
|
+
const edges = closed ? n : n - 1;
|
|
1490
|
+
for (let i = 0; i < edges; i++) {
|
|
1491
|
+
const a = ring2[i];
|
|
1492
|
+
const b = ring2[(i + 1) % n];
|
|
1493
|
+
out.push(a);
|
|
1494
|
+
const dist2 = Math.hypot(b.x - a.x, b.y - a.y);
|
|
1495
|
+
const steps = Math.floor(dist2 / maxSegLen);
|
|
1496
|
+
for (let s = 1; s < steps; s++) {
|
|
1497
|
+
const t = s / steps;
|
|
1498
|
+
out.push({
|
|
1499
|
+
x: a.x + (b.x - a.x) * t,
|
|
1500
|
+
y: a.y + (b.y - a.y) * t
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
if (!closed) out.push(ring2[n - 1]);
|
|
1505
|
+
return closed ? normalizeRing(out) : out;
|
|
1506
|
+
}
|
|
1507
|
+
function removeSmallHoles(p, minArea2) {
|
|
1508
|
+
if (p.rings.length <= 1) return p;
|
|
1509
|
+
const exterior = p.rings[0];
|
|
1510
|
+
const kept = p.rings.slice(1).filter((hole) => {
|
|
1511
|
+
const a = Math.abs(shoelace(hole));
|
|
1512
|
+
return a >= minArea2;
|
|
1513
|
+
});
|
|
1514
|
+
return path([exterior, ...kept], p.closed);
|
|
1515
|
+
}
|
|
1516
|
+
function generateRandomPoints(p, count, seed = 1) {
|
|
1517
|
+
const rng = mulberry322(seed >>> 0);
|
|
1518
|
+
const b = bounds(p);
|
|
1519
|
+
const out = [];
|
|
1520
|
+
let attempts = 0;
|
|
1521
|
+
const maxAttempts = Math.max(count * 50, 100);
|
|
1522
|
+
while (out.length < count && attempts < maxAttempts) {
|
|
1523
|
+
attempts++;
|
|
1524
|
+
const pt = {
|
|
1525
|
+
x: b.minX + rng() * (b.maxX - b.minX),
|
|
1526
|
+
y: b.minY + rng() * (b.maxY - b.minY)
|
|
1527
|
+
};
|
|
1528
|
+
if (containsPoint(p, pt)) out.push(pt);
|
|
1529
|
+
}
|
|
1530
|
+
return out;
|
|
1531
|
+
}
|
|
1532
|
+
function generateRandomGridPoints(p, spacing) {
|
|
1533
|
+
const step = Math.max(spacing, 1e-9);
|
|
1534
|
+
const b = bounds(p);
|
|
1535
|
+
const out = [];
|
|
1536
|
+
for (let x = b.minX; x <= b.maxX + 1e-12; x += step) {
|
|
1537
|
+
for (let y = b.minY; y <= b.maxY + 1e-12; y += step) {
|
|
1538
|
+
const pt = { x, y };
|
|
1539
|
+
if (containsPoint(p, pt)) out.push(pt);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
return out;
|
|
1543
|
+
}
|
|
1544
|
+
function pointsOnExterior(p, count) {
|
|
1545
|
+
const ring2 = p.rings[0];
|
|
1546
|
+
if (!ring2 || ring2.length < 2 || count <= 0) return [];
|
|
1547
|
+
const lengths = edgeLengths(ring2);
|
|
1548
|
+
const total = lengths.reduce((s, L) => s + L, 0);
|
|
1549
|
+
if (total < 1e-12) return [];
|
|
1550
|
+
const out = [];
|
|
1551
|
+
for (let i = 0; i < count; i++) {
|
|
1552
|
+
const target = i / count * total;
|
|
1553
|
+
out.push(pointAtArcLength(ring2, lengths, target));
|
|
1554
|
+
}
|
|
1555
|
+
return out;
|
|
1556
|
+
}
|
|
1557
|
+
function segmentsOnExterior(p) {
|
|
1558
|
+
const ring2 = p.rings[0];
|
|
1559
|
+
if (!ring2 || ring2.length < 2) return [];
|
|
1560
|
+
const n = ring2.length;
|
|
1561
|
+
const closed = p.closed;
|
|
1562
|
+
const edges = closed ? n : n - 1;
|
|
1563
|
+
const out = [];
|
|
1564
|
+
for (let i = 0; i < edges; i++) {
|
|
1565
|
+
out.push(segment(ring2[i], ring2[(i + 1) % n]));
|
|
1566
|
+
}
|
|
1567
|
+
return out;
|
|
1568
|
+
}
|
|
1569
|
+
function slice(p, a, b) {
|
|
1570
|
+
const dx = b.x - a.x;
|
|
1571
|
+
const dy = b.y - a.y;
|
|
1572
|
+
const len = Math.hypot(dx, dy);
|
|
1573
|
+
if (len < 1e-12) return group([p]);
|
|
1574
|
+
const nx = -dy / len;
|
|
1575
|
+
const ny = dx / len;
|
|
1576
|
+
const bb = bounds(p);
|
|
1577
|
+
const span = Math.hypot(bb.maxX - bb.minX, bb.maxY - bb.minY) * 4 + 10;
|
|
1578
|
+
const left = halfPlanePoly(a, nx, ny, span);
|
|
1579
|
+
const right = halfPlanePoly(a, -nx, -ny, span);
|
|
1580
|
+
const parts = [];
|
|
1581
|
+
for (const half of [left, right]) {
|
|
1582
|
+
parts.push(...intersect(p, half).paths);
|
|
1583
|
+
}
|
|
1584
|
+
return group(parts);
|
|
1585
|
+
}
|
|
1586
|
+
function centroidSplit(p) {
|
|
1587
|
+
const c = centroid(p);
|
|
1588
|
+
const bb = bounds(p);
|
|
1589
|
+
const pad = Math.max(bb.maxY - bb.minY, 1) + 1;
|
|
1590
|
+
return slice(p, { x: c.x, y: c.y - pad }, { x: c.x, y: c.y + pad });
|
|
1591
|
+
}
|
|
1592
|
+
function dissolve(paths) {
|
|
1593
|
+
return unionAll(paths);
|
|
1594
|
+
}
|
|
1595
|
+
function intersectionPoints(a, b) {
|
|
1596
|
+
const segsA = allSegments(a);
|
|
1597
|
+
const segsB = allSegments(b);
|
|
1598
|
+
const out = [];
|
|
1599
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1600
|
+
for (const sa of segsA) {
|
|
1601
|
+
for (const sb of segsB) {
|
|
1602
|
+
const hit = segmentIntersection2(sa.a, sa.b, sb.a, sb.b);
|
|
1603
|
+
if (!hit) continue;
|
|
1604
|
+
const key = `${hit.x.toFixed(9)},${hit.y.toFixed(9)}`;
|
|
1605
|
+
if (seen.has(key)) continue;
|
|
1606
|
+
seen.add(key);
|
|
1607
|
+
out.push(hit);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
return out;
|
|
1611
|
+
}
|
|
1612
|
+
function halfPlanePoly(origin, nx, ny, span) {
|
|
1613
|
+
const tx = -ny;
|
|
1614
|
+
const ty = nx;
|
|
1615
|
+
const o = origin;
|
|
1616
|
+
return polygon([
|
|
1617
|
+
{
|
|
1618
|
+
x: o.x - tx * span + nx * 1e-9,
|
|
1619
|
+
y: o.y - ty * span + ny * 1e-9
|
|
1620
|
+
},
|
|
1621
|
+
{
|
|
1622
|
+
x: o.x + tx * span + nx * 1e-9,
|
|
1623
|
+
y: o.y + ty * span + ny * 1e-9
|
|
1624
|
+
},
|
|
1625
|
+
{
|
|
1626
|
+
x: o.x + tx * span + nx * span,
|
|
1627
|
+
y: o.y + ty * span + ny * span
|
|
1628
|
+
},
|
|
1629
|
+
{
|
|
1630
|
+
x: o.x - tx * span + nx * span,
|
|
1631
|
+
y: o.y - ty * span + ny * span
|
|
1632
|
+
}
|
|
1633
|
+
]);
|
|
1634
|
+
}
|
|
1635
|
+
function allSegments(p) {
|
|
1636
|
+
const out = [];
|
|
1637
|
+
for (const ring2 of p.rings) {
|
|
1638
|
+
const n = ring2.length;
|
|
1639
|
+
const edges = p.closed ? n : Math.max(0, n - 1);
|
|
1640
|
+
for (let i = 0; i < edges; i++) {
|
|
1641
|
+
out.push(segment(ring2[i], ring2[(i + 1) % n]));
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
return out;
|
|
1645
|
+
}
|
|
1646
|
+
function segmentIntersection2(a, b, c, d) {
|
|
1647
|
+
const rX = b.x - a.x;
|
|
1648
|
+
const rY = b.y - a.y;
|
|
1649
|
+
const sX = d.x - c.x;
|
|
1650
|
+
const sY = d.y - c.y;
|
|
1651
|
+
const den = rX * sY - rY * sX;
|
|
1652
|
+
if (Math.abs(den) < 1e-14) return null;
|
|
1653
|
+
const t = ((c.x - a.x) * sY - (c.y - a.y) * sX) / den;
|
|
1654
|
+
const u = ((c.x - a.x) * rY - (c.y - a.y) * rX) / den;
|
|
1655
|
+
if (t < -1e-9 || t > 1 + 1e-9 || u < -1e-9 || u > 1 + 1e-9) return null;
|
|
1656
|
+
return { x: a.x + t * rX, y: a.y + t * rY };
|
|
1657
|
+
}
|
|
1658
|
+
function edgeLengths(ring2, closed) {
|
|
1659
|
+
const n = ring2.length;
|
|
1660
|
+
const edges = n ;
|
|
1661
|
+
const lengths = [];
|
|
1662
|
+
for (let i = 0; i < edges; i++) {
|
|
1663
|
+
const a = ring2[i];
|
|
1664
|
+
const b = ring2[(i + 1) % n];
|
|
1665
|
+
lengths.push(Math.hypot(b.x - a.x, b.y - a.y));
|
|
1666
|
+
}
|
|
1667
|
+
return lengths;
|
|
1668
|
+
}
|
|
1669
|
+
function pointAtArcLength(ring2, lengths, target) {
|
|
1670
|
+
let remain = target;
|
|
1671
|
+
const n = ring2.length;
|
|
1672
|
+
for (let i = 0; i < lengths.length; i++) {
|
|
1673
|
+
const L = lengths[i];
|
|
1674
|
+
if (remain <= L || i === lengths.length - 1) {
|
|
1675
|
+
const t = L < 1e-12 ? 0 : remain / L;
|
|
1676
|
+
const a = ring2[i];
|
|
1677
|
+
const b = ring2[(i + 1) % n];
|
|
1678
|
+
return {
|
|
1679
|
+
x: a.x + (b.x - a.x) * t,
|
|
1680
|
+
y: a.y + (b.y - a.y) * t
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
remain -= L;
|
|
1684
|
+
}
|
|
1685
|
+
return { ...ring2[0] };
|
|
1686
|
+
}
|
|
1687
|
+
function shoelace(ring2) {
|
|
1688
|
+
let sum = 0;
|
|
1689
|
+
for (let i = 0; i < ring2.length; i++) {
|
|
1690
|
+
const a = ring2[i];
|
|
1691
|
+
const b = ring2[(i + 1) % ring2.length];
|
|
1692
|
+
sum += a.x * b.y - b.x * a.y;
|
|
1693
|
+
}
|
|
1694
|
+
return sum / 2;
|
|
1695
|
+
}
|
|
1696
|
+
function mulberry322(a) {
|
|
1697
|
+
return function() {
|
|
1698
|
+
let t = a += 1831565813;
|
|
1699
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
1700
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
1701
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
1702
|
+
};
|
|
1703
|
+
}
|
|
1704
|
+
var processing = {
|
|
1705
|
+
extractPerimeter,
|
|
1706
|
+
extractBoundary,
|
|
1707
|
+
extractHoles,
|
|
1708
|
+
densify,
|
|
1709
|
+
removeSmallHoles,
|
|
1710
|
+
generateRandomPoints,
|
|
1711
|
+
generateRandomGridPoints,
|
|
1712
|
+
pointsOnExterior,
|
|
1713
|
+
segmentsOnExterior,
|
|
1714
|
+
slice,
|
|
1715
|
+
centroidSplit,
|
|
1716
|
+
dissolve,
|
|
1717
|
+
intersectionPoints
|
|
1718
|
+
};
|
|
1719
|
+
|
|
1720
|
+
// src/conversion/index.ts
|
|
1721
|
+
function copy(p) {
|
|
1722
|
+
return clonePath(p);
|
|
1723
|
+
}
|
|
1724
|
+
function flatten(g) {
|
|
1725
|
+
return g.paths.map(clonePath);
|
|
1726
|
+
}
|
|
1727
|
+
function roundVertexCoords(p, decimals) {
|
|
1728
|
+
return reducePrecision(p, decimals);
|
|
1729
|
+
}
|
|
1730
|
+
function toArray(p) {
|
|
1731
|
+
const ring2 = p.rings[0] ?? [];
|
|
1732
|
+
const out = [];
|
|
1733
|
+
for (const v of ring2) {
|
|
1734
|
+
out.push(v.x, v.y);
|
|
1735
|
+
}
|
|
1736
|
+
return out;
|
|
1737
|
+
}
|
|
1738
|
+
function fromArray(coords, closed = true) {
|
|
1739
|
+
const ring2 = [];
|
|
1740
|
+
for (let i = 0; i + 1 < coords.length; i += 2) {
|
|
1741
|
+
ring2.push({ x: coords[i], y: coords[i + 1] });
|
|
1742
|
+
}
|
|
1743
|
+
return path([normalizeRing(ring2)], closed);
|
|
1744
|
+
}
|
|
1745
|
+
function toContours(p) {
|
|
1746
|
+
return p.rings.filter((r) => r.length >= 3).map((r) => path([normalizeRing(r)], true));
|
|
1747
|
+
}
|
|
1748
|
+
function fromContours(contours) {
|
|
1749
|
+
if (contours.length === 0) return path([], true);
|
|
1750
|
+
const rings = contours.map((c) => normalizeRing(c.rings[0] ?? []));
|
|
1751
|
+
return path(rings, true);
|
|
1752
|
+
}
|
|
1753
|
+
var conversion = {
|
|
1754
|
+
copy,
|
|
1755
|
+
flatten,
|
|
1756
|
+
roundVertexCoords,
|
|
1757
|
+
toArray,
|
|
1758
|
+
fromArray,
|
|
1759
|
+
toContours,
|
|
1760
|
+
fromContours,
|
|
1761
|
+
cloneGroup,
|
|
1762
|
+
group
|
|
1763
|
+
};
|
|
1764
|
+
|
|
1765
|
+
// src/contour/index.ts
|
|
1766
|
+
function offsetCurvesOutward(p, distance) {
|
|
1767
|
+
return buffer(p, Math.abs(distance));
|
|
1768
|
+
}
|
|
1769
|
+
function offsetCurvesInward(p, distance) {
|
|
1770
|
+
return buffer(p, -Math.abs(distance));
|
|
1771
|
+
}
|
|
1772
|
+
var contour = {
|
|
1773
|
+
offsetCurvesOutward,
|
|
1774
|
+
offsetCurvesInward
|
|
1775
|
+
};
|
|
1776
|
+
|
|
1777
|
+
// src/hull/index.ts
|
|
1778
|
+
function convexHull(points) {
|
|
1779
|
+
const pts = uniquePoints(points);
|
|
1780
|
+
if (pts.length < 3) return polygon(pts);
|
|
1781
|
+
pts.sort((a, b) => a.x === b.x ? a.y - b.y : a.x - b.x);
|
|
1782
|
+
const lower = [];
|
|
1783
|
+
for (const p of pts) {
|
|
1784
|
+
while (lower.length >= 2 && cross2(lower[lower.length - 2], lower[lower.length - 1], p) <= 0) {
|
|
1785
|
+
lower.pop();
|
|
1786
|
+
}
|
|
1787
|
+
lower.push(p);
|
|
1788
|
+
}
|
|
1789
|
+
const upper = [];
|
|
1790
|
+
for (let i = pts.length - 1; i >= 0; i--) {
|
|
1791
|
+
const p = pts[i];
|
|
1792
|
+
while (upper.length >= 2 && cross2(upper[upper.length - 2], upper[upper.length - 1], p) <= 0) {
|
|
1793
|
+
upper.pop();
|
|
1794
|
+
}
|
|
1795
|
+
upper.push(p);
|
|
1796
|
+
}
|
|
1797
|
+
lower.pop();
|
|
1798
|
+
upper.pop();
|
|
1799
|
+
return polygon([...lower, ...upper]);
|
|
1800
|
+
}
|
|
1801
|
+
function convexHullPath(p) {
|
|
1802
|
+
const pts = [];
|
|
1803
|
+
for (const ring2 of p.rings) pts.push(...ring2);
|
|
1804
|
+
return convexHull(pts);
|
|
1805
|
+
}
|
|
1806
|
+
function boundingBox(p) {
|
|
1807
|
+
const b = bounds(p);
|
|
1808
|
+
return polygon([
|
|
1809
|
+
{ x: b.minX, y: b.minY },
|
|
1810
|
+
{ x: b.maxX, y: b.minY },
|
|
1811
|
+
{ x: b.maxX, y: b.maxY },
|
|
1812
|
+
{ x: b.minX, y: b.maxY }
|
|
1813
|
+
]);
|
|
1814
|
+
}
|
|
1815
|
+
function cross2(o, a, b) {
|
|
1816
|
+
return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
|
|
1817
|
+
}
|
|
1818
|
+
function uniquePoints(points) {
|
|
1819
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1820
|
+
const out = [];
|
|
1821
|
+
for (const p of points) {
|
|
1822
|
+
const key = `${p.x},${p.y}`;
|
|
1823
|
+
if (seen.has(key)) continue;
|
|
1824
|
+
seen.add(key);
|
|
1825
|
+
out.push({ x: p.x, y: p.y });
|
|
1826
|
+
}
|
|
1827
|
+
return out;
|
|
1828
|
+
}
|
|
1829
|
+
var hull = {
|
|
1830
|
+
convexHull,
|
|
1831
|
+
convexHullPath,
|
|
1832
|
+
boundingBox
|
|
1833
|
+
};
|
|
1834
|
+
|
|
1835
|
+
// src/pointSet/index.ts
|
|
1836
|
+
function random(count, minX, minY, maxX, maxY, seed = 1) {
|
|
1837
|
+
const rng = mulberry323(seed >>> 0);
|
|
1838
|
+
const out = [];
|
|
1839
|
+
for (let i = 0; i < count; i++) {
|
|
1840
|
+
out.push({
|
|
1841
|
+
x: minX + rng() * (maxX - minX),
|
|
1842
|
+
y: minY + rng() * (maxY - minY)
|
|
1843
|
+
});
|
|
1844
|
+
}
|
|
1845
|
+
return out;
|
|
1846
|
+
}
|
|
1847
|
+
function squareGrid(spacing, minX, minY, maxX, maxY) {
|
|
1848
|
+
const s = Math.max(spacing, 1e-9);
|
|
1849
|
+
const out = [];
|
|
1850
|
+
for (let x = minX; x <= maxX + 1e-12; x += s) {
|
|
1851
|
+
for (let y = minY; y <= maxY + 1e-12; y += s) {
|
|
1852
|
+
out.push({ x, y });
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
return out;
|
|
1856
|
+
}
|
|
1857
|
+
function hexGrid(spacing, minX, minY, maxX, maxY) {
|
|
1858
|
+
const s = Math.max(spacing, 1e-9);
|
|
1859
|
+
const dx = s;
|
|
1860
|
+
const dy = s * Math.sqrt(3) * 0.5;
|
|
1861
|
+
const out = [];
|
|
1862
|
+
let row = 0;
|
|
1863
|
+
for (let y = minY; y <= maxY + 1e-12; y += dy, row++) {
|
|
1864
|
+
const x0 = minX + (row % 2 === 0 ? 0 : s * 0.5);
|
|
1865
|
+
for (let x = x0; x <= maxX + 1e-12; x += dx) {
|
|
1866
|
+
out.push({ x, y });
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
return out;
|
|
1870
|
+
}
|
|
1871
|
+
function ring(count, center, radius) {
|
|
1872
|
+
const out = [];
|
|
1873
|
+
for (let i = 0; i < count; i++) {
|
|
1874
|
+
const t = i / count * Math.PI * 2;
|
|
1875
|
+
out.push({
|
|
1876
|
+
x: center.x + Math.cos(t) * radius,
|
|
1877
|
+
y: center.y + Math.sin(t) * radius
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1880
|
+
return out;
|
|
1881
|
+
}
|
|
1882
|
+
function mulberry323(a) {
|
|
1883
|
+
return function() {
|
|
1884
|
+
let t = a += 1831565813;
|
|
1885
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
1886
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
1887
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
1888
|
+
};
|
|
1889
|
+
}
|
|
1890
|
+
function poisson(minDistance, minX, minY, maxX, maxY, seed = 1, maxAttempts = 30) {
|
|
1891
|
+
const rng = mulberry323(seed >>> 0);
|
|
1892
|
+
const r = Math.max(minDistance, 1e-9);
|
|
1893
|
+
const cell = r / Math.SQRT2;
|
|
1894
|
+
const w = maxX - minX;
|
|
1895
|
+
const h = maxY - minY;
|
|
1896
|
+
const gw = Math.max(1, Math.ceil(w / cell));
|
|
1897
|
+
const gh = Math.max(1, Math.ceil(h / cell));
|
|
1898
|
+
const grid = Array(gw * gh).fill(null);
|
|
1899
|
+
const points = [];
|
|
1900
|
+
const active = [];
|
|
1901
|
+
const gx = (p) => Math.min(gw - 1, Math.floor((p.x - minX) / cell));
|
|
1902
|
+
const gy = (p) => Math.min(gh - 1, Math.floor((p.y - minY) / cell));
|
|
1903
|
+
const emit = (p) => {
|
|
1904
|
+
const i = points.length;
|
|
1905
|
+
points.push(p);
|
|
1906
|
+
active.push(i);
|
|
1907
|
+
grid[gy(p) * gw + gx(p)] = i;
|
|
1908
|
+
};
|
|
1909
|
+
emit({
|
|
1910
|
+
x: minX + rng() * w,
|
|
1911
|
+
y: minY + rng() * h
|
|
1912
|
+
});
|
|
1913
|
+
while (active.length > 0) {
|
|
1914
|
+
const ai = Math.floor(rng() * active.length);
|
|
1915
|
+
const i = active[ai];
|
|
1916
|
+
const src = points[i];
|
|
1917
|
+
let found = false;
|
|
1918
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1919
|
+
const ang = rng() * Math.PI * 2;
|
|
1920
|
+
const rad = r * (1 + rng());
|
|
1921
|
+
const cand = {
|
|
1922
|
+
x: src.x + Math.cos(ang) * rad,
|
|
1923
|
+
y: src.y + Math.sin(ang) * rad
|
|
1924
|
+
};
|
|
1925
|
+
if (cand.x < minX || cand.x > maxX || cand.y < minY || cand.y > maxY) {
|
|
1926
|
+
continue;
|
|
1927
|
+
}
|
|
1928
|
+
const cx = gx(cand);
|
|
1929
|
+
const cy = gy(cand);
|
|
1930
|
+
let ok = true;
|
|
1931
|
+
for (let yy = Math.max(0, cy - 2); yy <= Math.min(gh - 1, cy + 2) && ok; yy++) {
|
|
1932
|
+
for (let xx = Math.max(0, cx - 2); xx <= Math.min(gw - 1, cx + 2); xx++) {
|
|
1933
|
+
const j = grid[yy * gw + xx];
|
|
1934
|
+
if (j == null) continue;
|
|
1935
|
+
const q = points[j];
|
|
1936
|
+
if (Math.hypot(cand.x - q.x, cand.y - q.y) < r) {
|
|
1937
|
+
ok = false;
|
|
1938
|
+
break;
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
if (ok) {
|
|
1943
|
+
emit(cand);
|
|
1944
|
+
found = true;
|
|
1945
|
+
break;
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
if (!found) active.splice(ai, 1);
|
|
1949
|
+
}
|
|
1950
|
+
return points;
|
|
1951
|
+
}
|
|
1952
|
+
function prunePointsWithinDistance(points, minDistance) {
|
|
1953
|
+
const r = Math.max(minDistance, 0);
|
|
1954
|
+
const out = [];
|
|
1955
|
+
for (const p of points) {
|
|
1956
|
+
if (out.every((q) => Math.hypot(p.x - q.x, p.y - q.y) >= r)) {
|
|
1957
|
+
out.push(p);
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
return out;
|
|
1961
|
+
}
|
|
1962
|
+
var pointSet = {
|
|
1963
|
+
random,
|
|
1964
|
+
squareGrid,
|
|
1965
|
+
hexGrid,
|
|
1966
|
+
ring,
|
|
1967
|
+
poisson,
|
|
1968
|
+
prunePointsWithinDistance
|
|
1969
|
+
};
|
|
1970
|
+
|
|
1971
|
+
// src/triangulation/index.ts
|
|
1972
|
+
function earCutTriangulation(p, options = {}) {
|
|
1973
|
+
if (!p.closed || p.rings.length === 0) return [];
|
|
1974
|
+
const { solution } = triangulateD(
|
|
1975
|
+
pathToPathsD(p),
|
|
1976
|
+
options.precision ?? 8,
|
|
1977
|
+
options.useDelaunay ?? false
|
|
1978
|
+
);
|
|
1979
|
+
const tris = [];
|
|
1980
|
+
for (const pd of solution) {
|
|
1981
|
+
const ring2 = pathDToRing(pd);
|
|
1982
|
+
if (ring2.length >= 3) tris.push(polygon(ring2.slice(0, 3)));
|
|
1983
|
+
}
|
|
1984
|
+
return tris;
|
|
1985
|
+
}
|
|
1986
|
+
function delaunayTriangulation(p) {
|
|
1987
|
+
return earCutTriangulation(p, { useDelaunay: true });
|
|
1988
|
+
}
|
|
1989
|
+
function delaunayTriangulationPoints(points) {
|
|
1990
|
+
if (points.length < 3) return [];
|
|
1991
|
+
const delaunay = Delaunay.from(
|
|
1992
|
+
points,
|
|
1993
|
+
(d) => d.x,
|
|
1994
|
+
(d) => d.y
|
|
1995
|
+
);
|
|
1996
|
+
const tris = [];
|
|
1997
|
+
const { triangles } = delaunay;
|
|
1998
|
+
for (let i = 0; i < triangles.length; i += 3) {
|
|
1999
|
+
const a = points[triangles[i]];
|
|
2000
|
+
const b = points[triangles[i + 1]];
|
|
2001
|
+
const c = points[triangles[i + 2]];
|
|
2002
|
+
tris.push(polygon([a, b, c]));
|
|
2003
|
+
}
|
|
2004
|
+
return tris;
|
|
2005
|
+
}
|
|
2006
|
+
function poissonTriangulationPoints(path3, minDistance, seed = 1) {
|
|
2007
|
+
if (!path3.closed || path3.rings.length === 0) return [];
|
|
2008
|
+
const b = bounds(path3);
|
|
2009
|
+
const samples = poisson(
|
|
2010
|
+
minDistance,
|
|
2011
|
+
b.minX,
|
|
2012
|
+
b.minY,
|
|
2013
|
+
b.maxX,
|
|
2014
|
+
b.maxY,
|
|
2015
|
+
seed
|
|
2016
|
+
).filter((pt) => containsPoint(path3, pt));
|
|
2017
|
+
return dedupePoints([...pathVertices(path3), ...samples]);
|
|
2018
|
+
}
|
|
2019
|
+
function poissonTriangulation(path3, minDistance, seed = 1) {
|
|
2020
|
+
const pts = poissonTriangulationPoints(path3, minDistance, seed);
|
|
2021
|
+
return trianglesInsidePath(delaunayTriangulationPoints(pts), path3);
|
|
2022
|
+
}
|
|
2023
|
+
function refine(path3, options = {}) {
|
|
2024
|
+
if (!path3.closed || path3.rings.length === 0) return [];
|
|
2025
|
+
const minAngle = options.minAngle ?? 20 * Math.PI / 180;
|
|
2026
|
+
const maxIterations = Math.max(1, options.maxIterations ?? 200);
|
|
2027
|
+
const points = dedupePoints(pathVertices(path3));
|
|
2028
|
+
if (points.length < 3) return [];
|
|
2029
|
+
for (let iter = 0; iter < maxIterations; iter++) {
|
|
2030
|
+
const tris = trianglesInsidePath(
|
|
2031
|
+
delaunayTriangulationPoints(points),
|
|
2032
|
+
path3
|
|
2033
|
+
);
|
|
2034
|
+
if (tris.length === 0) break;
|
|
2035
|
+
let skinny = null;
|
|
2036
|
+
let worst = minAngle;
|
|
2037
|
+
for (const t of tris) {
|
|
2038
|
+
const a = minTriangleAngle(t);
|
|
2039
|
+
if (a < worst) {
|
|
2040
|
+
worst = a;
|
|
2041
|
+
skinny = t;
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
if (!skinny) break;
|
|
2045
|
+
const ring2 = skinny.rings[0];
|
|
2046
|
+
const cc = circumcenter(ring2[0], ring2[1], ring2[2]);
|
|
2047
|
+
if (!cc) break;
|
|
2048
|
+
const insert = containsPoint(path3, cc) ? cc : longestEdgeMidpoint(ring2[0], ring2[1], ring2[2]);
|
|
2049
|
+
if (points.some((p) => Math.hypot(p.x - insert.x, p.y - insert.y) < 1e-9)) {
|
|
2050
|
+
break;
|
|
2051
|
+
}
|
|
2052
|
+
points.push(insert);
|
|
2053
|
+
}
|
|
2054
|
+
return trianglesInsidePath(delaunayTriangulationPoints(points), path3);
|
|
2055
|
+
}
|
|
2056
|
+
function pathVertices(path3) {
|
|
2057
|
+
const out = [];
|
|
2058
|
+
for (const ring2 of path3.rings) {
|
|
2059
|
+
for (const v of ring2) out.push({ x: v.x, y: v.y });
|
|
2060
|
+
}
|
|
2061
|
+
return out;
|
|
2062
|
+
}
|
|
2063
|
+
function dedupePoints(points, eps = 1e-9) {
|
|
2064
|
+
const out = [];
|
|
2065
|
+
for (const p of points) {
|
|
2066
|
+
if (out.some((q) => Math.hypot(q.x - p.x, q.y - p.y) < eps)) continue;
|
|
2067
|
+
out.push(p);
|
|
2068
|
+
}
|
|
2069
|
+
return out;
|
|
2070
|
+
}
|
|
2071
|
+
function triangleCentroid(t) {
|
|
2072
|
+
const r = t.rings[0];
|
|
2073
|
+
if (!r || r.length < 3) return null;
|
|
2074
|
+
return {
|
|
2075
|
+
x: (r[0].x + r[1].x + r[2].x) / 3,
|
|
2076
|
+
y: (r[0].y + r[1].y + r[2].y) / 3
|
|
2077
|
+
};
|
|
2078
|
+
}
|
|
2079
|
+
function trianglesInsidePath(tris, path3) {
|
|
2080
|
+
return tris.filter((t) => {
|
|
2081
|
+
const c = triangleCentroid(t);
|
|
2082
|
+
return c != null && containsPoint(path3, c);
|
|
2083
|
+
});
|
|
2084
|
+
}
|
|
2085
|
+
function minTriangleAngle(t) {
|
|
2086
|
+
const r = t.rings[0];
|
|
2087
|
+
if (!r || r.length < 3) return 0;
|
|
2088
|
+
const a = r[0];
|
|
2089
|
+
const b = r[1];
|
|
2090
|
+
const c = r[2];
|
|
2091
|
+
return Math.min(angleAt(a, b, c), angleAt(b, c, a), angleAt(c, a, b));
|
|
2092
|
+
}
|
|
2093
|
+
function angleAt(vertex, p, q) {
|
|
2094
|
+
const ux = p.x - vertex.x;
|
|
2095
|
+
const uy = p.y - vertex.y;
|
|
2096
|
+
const vx = q.x - vertex.x;
|
|
2097
|
+
const vy = q.y - vertex.y;
|
|
2098
|
+
const lu = Math.hypot(ux, uy);
|
|
2099
|
+
const lv = Math.hypot(vx, vy);
|
|
2100
|
+
if (lu < 1e-18 || lv < 1e-18) return 0;
|
|
2101
|
+
const cos = Math.max(-1, Math.min(1, (ux * vx + uy * vy) / (lu * lv)));
|
|
2102
|
+
return Math.acos(cos);
|
|
2103
|
+
}
|
|
2104
|
+
function circumcenter(a, b, c) {
|
|
2105
|
+
const d = 2 * (a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y));
|
|
2106
|
+
if (Math.abs(d) < 1e-18) return null;
|
|
2107
|
+
const a2 = a.x * a.x + a.y * a.y;
|
|
2108
|
+
const b2 = b.x * b.x + b.y * b.y;
|
|
2109
|
+
const c2 = c.x * c.x + c.y * c.y;
|
|
2110
|
+
return {
|
|
2111
|
+
x: (a2 * (b.y - c.y) + b2 * (c.y - a.y) + c2 * (a.y - b.y)) / d,
|
|
2112
|
+
y: (a2 * (c.x - b.x) + b2 * (a.x - c.x) + c2 * (b.x - a.x)) / d
|
|
2113
|
+
};
|
|
2114
|
+
}
|
|
2115
|
+
function longestEdgeMidpoint(a, b, c) {
|
|
2116
|
+
const edges = [
|
|
2117
|
+
[a, b],
|
|
2118
|
+
[b, c],
|
|
2119
|
+
[c, a]
|
|
2120
|
+
];
|
|
2121
|
+
let best = edges[0];
|
|
2122
|
+
let bestLen = -1;
|
|
2123
|
+
for (const e of edges) {
|
|
2124
|
+
const len = Math.hypot(e[0].x - e[1].x, e[0].y - e[1].y);
|
|
2125
|
+
if (len > bestLen) {
|
|
2126
|
+
bestLen = len;
|
|
2127
|
+
best = e;
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
return { x: (best[0].x + best[1].x) / 2, y: (best[0].y + best[1].y) / 2 };
|
|
2131
|
+
}
|
|
2132
|
+
var triangulation = {
|
|
2133
|
+
earCutTriangulation,
|
|
2134
|
+
delaunayTriangulation,
|
|
2135
|
+
delaunayTriangulationPoints,
|
|
2136
|
+
poissonTriangulation,
|
|
2137
|
+
poissonTriangulationPoints,
|
|
2138
|
+
refine
|
|
2139
|
+
};
|
|
2140
|
+
|
|
2141
|
+
// src/optimisation/index.ts
|
|
2142
|
+
function envelope(p) {
|
|
2143
|
+
return boundingBox(p);
|
|
2144
|
+
}
|
|
2145
|
+
function maximumInscribedCircle(p, tolerance = 1) {
|
|
2146
|
+
return maximumInscribedPack(p, 1, tolerance)[0] ?? null;
|
|
2147
|
+
}
|
|
2148
|
+
function largestEmptyCircles(p, minRadius, tolerance = 1) {
|
|
2149
|
+
return maximumInscribedPackUntil(p, minRadius, tolerance);
|
|
2150
|
+
}
|
|
2151
|
+
function largestEmptyCircle(p, tolerance = 1) {
|
|
2152
|
+
return maximumInscribedCircle(p, tolerance);
|
|
2153
|
+
}
|
|
2154
|
+
function maximumInscribedAARectangle(p) {
|
|
2155
|
+
const b = bounds(p);
|
|
2156
|
+
const w = b.maxX - b.minX;
|
|
2157
|
+
const h = b.maxY - b.minY;
|
|
2158
|
+
if (w < 1e-12 || h < 1e-12) return envelope(p);
|
|
2159
|
+
const fits = (m2) => {
|
|
2160
|
+
const rect = [
|
|
2161
|
+
{ x: b.minX + m2, y: b.minY + m2 },
|
|
2162
|
+
{ x: b.maxX - m2, y: b.minY + m2 },
|
|
2163
|
+
{ x: b.maxX - m2, y: b.maxY - m2 },
|
|
2164
|
+
{ x: b.minX + m2, y: b.maxY - m2 }
|
|
2165
|
+
];
|
|
2166
|
+
return rect.every((pt) => containsPoint(p, pt));
|
|
2167
|
+
};
|
|
2168
|
+
if (fits(0)) return envelope(p);
|
|
2169
|
+
let lo = 0;
|
|
2170
|
+
let hi = Math.min(w, h) / 2;
|
|
2171
|
+
let best = hi;
|
|
2172
|
+
for (let i = 0; i < 40; i++) {
|
|
2173
|
+
const mid3 = (lo + hi) / 2;
|
|
2174
|
+
if (fits(mid3)) {
|
|
2175
|
+
best = mid3;
|
|
2176
|
+
hi = mid3;
|
|
2177
|
+
} else {
|
|
2178
|
+
lo = mid3;
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
const m = best;
|
|
2182
|
+
return polygon([
|
|
2183
|
+
{ x: b.minX + m, y: b.minY + m },
|
|
2184
|
+
{ x: b.maxX - m, y: b.minY + m },
|
|
2185
|
+
{ x: b.maxX - m, y: b.maxY - m },
|
|
2186
|
+
{ x: b.minX + m, y: b.maxY - m }
|
|
2187
|
+
]);
|
|
2188
|
+
}
|
|
2189
|
+
function closestPoint(p, point) {
|
|
2190
|
+
let best = point;
|
|
2191
|
+
let bestD = Infinity;
|
|
2192
|
+
for (const ring2 of p.rings) {
|
|
2193
|
+
const n = ring2.length;
|
|
2194
|
+
const edges = p.closed ? n : Math.max(0, n - 1);
|
|
2195
|
+
for (let i = 0; i < edges; i++) {
|
|
2196
|
+
const a = ring2[i];
|
|
2197
|
+
const b = ring2[(i + 1) % n];
|
|
2198
|
+
const q = projectPointToSegment(point, a, b);
|
|
2199
|
+
const d = Math.hypot(q.x - point.x, q.y - point.y);
|
|
2200
|
+
if (d < bestD) {
|
|
2201
|
+
bestD = d;
|
|
2202
|
+
best = q;
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
return best;
|
|
2207
|
+
}
|
|
2208
|
+
function closestVertex(p, point) {
|
|
2209
|
+
let best = p.rings[0]?.[0] ?? point;
|
|
2210
|
+
let bestD = Infinity;
|
|
2211
|
+
for (const ring2 of p.rings) {
|
|
2212
|
+
for (const v of ring2) {
|
|
2213
|
+
const d = Math.hypot(v.x - point.x, v.y - point.y);
|
|
2214
|
+
if (d < bestD) {
|
|
2215
|
+
bestD = d;
|
|
2216
|
+
best = v;
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
return { ...best };
|
|
2221
|
+
}
|
|
2222
|
+
function closestPointPair(points) {
|
|
2223
|
+
if (points.length < 2) return null;
|
|
2224
|
+
let best = [points[0], points[1]];
|
|
2225
|
+
let bestD = Infinity;
|
|
2226
|
+
for (let i = 0; i < points.length; i++) {
|
|
2227
|
+
for (let j = i + 1; j < points.length; j++) {
|
|
2228
|
+
const d = Math.hypot(
|
|
2229
|
+
points[i].x - points[j].x,
|
|
2230
|
+
points[i].y - points[j].y
|
|
2231
|
+
);
|
|
2232
|
+
if (d < bestD) {
|
|
2233
|
+
bestD = d;
|
|
2234
|
+
best = [points[i], points[j]];
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
return best;
|
|
2239
|
+
}
|
|
2240
|
+
function farthestPointPair(points) {
|
|
2241
|
+
if (points.length < 2) return null;
|
|
2242
|
+
let best = [points[0], points[1]];
|
|
2243
|
+
let bestD = -1;
|
|
2244
|
+
for (let i = 0; i < points.length; i++) {
|
|
2245
|
+
for (let j = i + 1; j < points.length; j++) {
|
|
2246
|
+
const d = Math.hypot(
|
|
2247
|
+
points[i].x - points[j].x,
|
|
2248
|
+
points[i].y - points[j].y
|
|
2249
|
+
);
|
|
2250
|
+
if (d > bestD) {
|
|
2251
|
+
bestD = d;
|
|
2252
|
+
best = [points[i], points[j]];
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
return best;
|
|
2257
|
+
}
|
|
2258
|
+
function minimumBoundingCircle(p) {
|
|
2259
|
+
const pts = [];
|
|
2260
|
+
for (const ring2 of p.rings) pts.push(...ring2);
|
|
2261
|
+
const hull2 = convexHull(pts).rings[0] ?? pts;
|
|
2262
|
+
return minDisk(hull2);
|
|
2263
|
+
}
|
|
2264
|
+
function minDisk(points) {
|
|
2265
|
+
return welzl(points.map((p) => ({ ...p })), [], 0);
|
|
2266
|
+
}
|
|
2267
|
+
function welzl(P, R, i) {
|
|
2268
|
+
if (i === P.length || R.length === 3) return diskFromBoundary(R);
|
|
2269
|
+
const d = welzl(P, R, i + 1);
|
|
2270
|
+
const p = P[i];
|
|
2271
|
+
if (Math.hypot(p.x - d.x, p.y - d.y) <= d.r + 1e-9) return d;
|
|
2272
|
+
return welzl(P, [...R, p], i + 1);
|
|
2273
|
+
}
|
|
2274
|
+
function diskFromBoundary(R) {
|
|
2275
|
+
if (R.length === 0) return circle(0, 0, 0);
|
|
2276
|
+
if (R.length === 1) return circle(R[0].x, R[0].y, 0);
|
|
2277
|
+
if (R.length === 2) {
|
|
2278
|
+
const a = R[0];
|
|
2279
|
+
const b = R[1];
|
|
2280
|
+
return circle(
|
|
2281
|
+
(a.x + b.x) / 2,
|
|
2282
|
+
(a.y + b.y) / 2,
|
|
2283
|
+
Math.hypot(a.x - b.x, a.y - b.y) / 2
|
|
2284
|
+
);
|
|
2285
|
+
}
|
|
2286
|
+
return circumcircle(R[0], R[1], R[2]);
|
|
2287
|
+
}
|
|
2288
|
+
function circumcircle(a, b, c) {
|
|
2289
|
+
const d = 2 * (a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y));
|
|
2290
|
+
if (Math.abs(d) < 1e-14) {
|
|
2291
|
+
const pair = farthestPointPair([a, b, c]);
|
|
2292
|
+
return circle(
|
|
2293
|
+
(pair[0].x + pair[1].x) / 2,
|
|
2294
|
+
(pair[0].y + pair[1].y) / 2,
|
|
2295
|
+
Math.hypot(pair[0].x - pair[1].x, pair[0].y - pair[1].y) / 2
|
|
2296
|
+
);
|
|
2297
|
+
}
|
|
2298
|
+
const ux = ((a.x * a.x + a.y * a.y) * (b.y - c.y) + (b.x * b.x + b.y * b.y) * (c.y - a.y) + (c.x * c.x + c.y * c.y) * (a.y - b.y)) / d;
|
|
2299
|
+
const uy = ((a.x * a.x + a.y * a.y) * (c.x - b.x) + (b.x * b.x + b.y * b.y) * (a.x - c.x) + (c.x * c.x + c.y * c.y) * (b.x - a.x)) / d;
|
|
2300
|
+
return circle(ux, uy, Math.hypot(ux - a.x, uy - a.y));
|
|
2301
|
+
}
|
|
2302
|
+
function projectPointToSegment(p, a, b) {
|
|
2303
|
+
const abx = b.x - a.x;
|
|
2304
|
+
const aby = b.y - a.y;
|
|
2305
|
+
const len2 = abx * abx + aby * aby;
|
|
2306
|
+
if (len2 < 1e-18) return { ...a };
|
|
2307
|
+
let t = ((p.x - a.x) * abx + (p.y - a.y) * aby) / len2;
|
|
2308
|
+
t = Math.max(0, Math.min(1, t));
|
|
2309
|
+
return { x: a.x + t * abx, y: a.y + t * aby };
|
|
2310
|
+
}
|
|
2311
|
+
var optimisation = {
|
|
2312
|
+
envelope,
|
|
2313
|
+
maximumInscribedCircle,
|
|
2314
|
+
largestEmptyCircle,
|
|
2315
|
+
largestEmptyCircles,
|
|
2316
|
+
maximumInscribedAARectangle,
|
|
2317
|
+
closestPoint,
|
|
2318
|
+
closestVertex,
|
|
2319
|
+
closestPointPair,
|
|
2320
|
+
farthestPointPair,
|
|
2321
|
+
minimumBoundingCircle
|
|
2322
|
+
};
|
|
2323
|
+
function compoundVoronoi(sites, options = {}) {
|
|
2324
|
+
if (sites.length === 0) return group([]);
|
|
2325
|
+
if (sites.length === 1) {
|
|
2326
|
+
const b2 = options.bounds ?? padBounds(sites, 1);
|
|
2327
|
+
return group([
|
|
2328
|
+
polygon([
|
|
2329
|
+
{ x: b2.minX, y: b2.minY },
|
|
2330
|
+
{ x: b2.maxX, y: b2.minY },
|
|
2331
|
+
{ x: b2.maxX, y: b2.maxY },
|
|
2332
|
+
{ x: b2.minX, y: b2.maxY }
|
|
2333
|
+
])
|
|
2334
|
+
]);
|
|
2335
|
+
}
|
|
2336
|
+
const b = options.bounds ?? padBounds(sites, 0.1);
|
|
2337
|
+
const delaunay = Delaunay.from(
|
|
2338
|
+
sites,
|
|
2339
|
+
(d) => d.x,
|
|
2340
|
+
(d) => d.y
|
|
2341
|
+
);
|
|
2342
|
+
const voronoi2 = delaunay.voronoi([b.minX, b.minY, b.maxX, b.maxY]);
|
|
2343
|
+
const paths = [];
|
|
2344
|
+
for (let i = 0; i < sites.length; i++) {
|
|
2345
|
+
const poly = voronoi2.cellPolygon(i);
|
|
2346
|
+
if (!poly || poly.length < 3) continue;
|
|
2347
|
+
const ring2 = poly.slice(0, -1).map(([x, y]) => ({ x, y }));
|
|
2348
|
+
if (ring2.length >= 3) paths.push(polygon(ring2));
|
|
2349
|
+
}
|
|
2350
|
+
return group(paths);
|
|
2351
|
+
}
|
|
2352
|
+
function innerVoronoi(sites, container, options = {}) {
|
|
2353
|
+
const b = options.bounds ?? {
|
|
2354
|
+
...bounds(container)
|
|
2355
|
+
};
|
|
2356
|
+
const cells = compoundVoronoi(sites, { bounds: b });
|
|
2357
|
+
const clipped = [];
|
|
2358
|
+
for (const cell of cells.paths) {
|
|
2359
|
+
clipped.push(...intersect(cell, container).paths);
|
|
2360
|
+
}
|
|
2361
|
+
return group(clipped);
|
|
2362
|
+
}
|
|
2363
|
+
function innerVoronoiRaw(sites, options = {}) {
|
|
2364
|
+
return compoundVoronoi(sites, options);
|
|
2365
|
+
}
|
|
2366
|
+
function padBounds(sites, padRatio) {
|
|
2367
|
+
let minX = Infinity;
|
|
2368
|
+
let minY = Infinity;
|
|
2369
|
+
let maxX = -Infinity;
|
|
2370
|
+
let maxY = -Infinity;
|
|
2371
|
+
for (const s of sites) {
|
|
2372
|
+
minX = Math.min(minX, s.x);
|
|
2373
|
+
minY = Math.min(minY, s.y);
|
|
2374
|
+
maxX = Math.max(maxX, s.x);
|
|
2375
|
+
maxY = Math.max(maxY, s.y);
|
|
2376
|
+
}
|
|
2377
|
+
const pad = Math.max(maxX - minX, maxY - minY, 1) * padRatio + 1e-6;
|
|
2378
|
+
return {
|
|
2379
|
+
minX: minX - pad,
|
|
2380
|
+
minY: minY - pad,
|
|
2381
|
+
maxX: maxX + pad,
|
|
2382
|
+
maxY: maxY + pad
|
|
2383
|
+
};
|
|
2384
|
+
}
|
|
2385
|
+
var voronoi = {
|
|
2386
|
+
compoundVoronoi,
|
|
2387
|
+
innerVoronoi,
|
|
2388
|
+
innerVoronoiRaw
|
|
2389
|
+
};
|
|
2390
|
+
|
|
2391
|
+
// src/construction/index.ts
|
|
2392
|
+
function createCircle(cx, cy, radius, segments = 64) {
|
|
2393
|
+
return createRegularPolygon(cx, cy, radius, Math.max(3, Math.floor(segments)));
|
|
2394
|
+
}
|
|
2395
|
+
function createRect(x, y, width2, height2) {
|
|
2396
|
+
return polygon([
|
|
2397
|
+
{ x, y },
|
|
2398
|
+
{ x: x + width2, y },
|
|
2399
|
+
{ x: x + width2, y: y + height2 },
|
|
2400
|
+
{ x, y: y + height2 }
|
|
2401
|
+
]);
|
|
2402
|
+
}
|
|
2403
|
+
function createRegularPolygon(cx, cy, radius, sides, rotation = -Math.PI / 2) {
|
|
2404
|
+
const n = Math.max(3, Math.floor(sides));
|
|
2405
|
+
const ring2 = [];
|
|
2406
|
+
for (let i = 0; i < n; i++) {
|
|
2407
|
+
const t = rotation + i / n * Math.PI * 2;
|
|
2408
|
+
ring2.push({ x: cx + Math.cos(t) * radius, y: cy + Math.sin(t) * radius });
|
|
2409
|
+
}
|
|
2410
|
+
return polygon(ring2);
|
|
2411
|
+
}
|
|
2412
|
+
function createRing(cx, cy, outerRadius, innerRadius, segments = 64) {
|
|
2413
|
+
const outer = createCircle(cx, cy, outerRadius, segments).rings[0];
|
|
2414
|
+
const inner = createCircle(cx, cy, Math.min(innerRadius, outerRadius), segments).rings[0];
|
|
2415
|
+
return path([outer, [...inner].reverse()], true);
|
|
2416
|
+
}
|
|
2417
|
+
function createArc(cx, cy, radius, startAngle, endAngle, segments = 32) {
|
|
2418
|
+
const n = Math.max(2, Math.floor(segments));
|
|
2419
|
+
const pts = [];
|
|
2420
|
+
for (let i = 0; i <= n; i++) {
|
|
2421
|
+
const t = startAngle + (endAngle - startAngle) * i / n;
|
|
2422
|
+
pts.push({ x: cx + Math.cos(t) * radius, y: cy + Math.sin(t) * radius });
|
|
2423
|
+
}
|
|
2424
|
+
return polyline(pts);
|
|
2425
|
+
}
|
|
2426
|
+
function createStar(cx, cy, outerRadius, innerRadius, points = 5, rotation = -Math.PI / 2) {
|
|
2427
|
+
const n = Math.max(3, Math.floor(points));
|
|
2428
|
+
const ring2 = [];
|
|
2429
|
+
for (let i = 0; i < n * 2; i++) {
|
|
2430
|
+
const r = i % 2 === 0 ? outerRadius : innerRadius;
|
|
2431
|
+
const t = rotation + i / (n * 2) * Math.PI * 2;
|
|
2432
|
+
ring2.push({ x: cx + Math.cos(t) * r, y: cy + Math.sin(t) * r });
|
|
2433
|
+
}
|
|
2434
|
+
return polygon(ring2);
|
|
2435
|
+
}
|
|
2436
|
+
function createKochSnowflake(cx, cy, radius, iterations = 3) {
|
|
2437
|
+
const tri = createRegularPolygon(cx, cy, radius, 3);
|
|
2438
|
+
let ring2 = tri.rings[0];
|
|
2439
|
+
const iters = Math.max(0, Math.min(6, Math.floor(iterations)));
|
|
2440
|
+
for (let i = 0; i < iters; i++) {
|
|
2441
|
+
ring2 = kochRefine(ring2);
|
|
2442
|
+
}
|
|
2443
|
+
return polygon(ring2);
|
|
2444
|
+
}
|
|
2445
|
+
function kochRefine(ring2) {
|
|
2446
|
+
const out = [];
|
|
2447
|
+
const n = ring2.length;
|
|
2448
|
+
for (let i = 0; i < n; i++) {
|
|
2449
|
+
const a = ring2[i];
|
|
2450
|
+
const b = ring2[(i + 1) % n];
|
|
2451
|
+
const dx = b.x - a.x;
|
|
2452
|
+
const dy = b.y - a.y;
|
|
2453
|
+
const p1 = { x: a.x + dx / 3, y: a.y + dy / 3 };
|
|
2454
|
+
const p2 = { x: a.x + 2 * dx / 3, y: a.y + 2 * dy / 3 };
|
|
2455
|
+
const px = a.x + dx / 2 - dy * (Math.sqrt(3) / 6);
|
|
2456
|
+
const py = a.y + dy / 2 + dx * (Math.sqrt(3) / 6);
|
|
2457
|
+
out.push(a, p1, { x: px, y: py }, p2);
|
|
2458
|
+
}
|
|
2459
|
+
return out;
|
|
2460
|
+
}
|
|
2461
|
+
var construction = {
|
|
2462
|
+
createCircle,
|
|
2463
|
+
createRect,
|
|
2464
|
+
createRegularPolygon,
|
|
2465
|
+
createRing,
|
|
2466
|
+
createArc,
|
|
2467
|
+
createStar,
|
|
2468
|
+
createKochSnowflake
|
|
2469
|
+
};
|
|
2470
|
+
|
|
2471
|
+
// src/tiling/index.ts
|
|
2472
|
+
function squareGrid2(spacing, minX, minY, maxX, maxY) {
|
|
2473
|
+
const s = Math.max(spacing, 1e-9);
|
|
2474
|
+
const paths = [];
|
|
2475
|
+
for (let x = minX; x < maxX - 1e-12; x += s) {
|
|
2476
|
+
for (let y = minY; y < maxY - 1e-12; y += s) {
|
|
2477
|
+
const w = Math.min(s, maxX - x);
|
|
2478
|
+
const h = Math.min(s, maxY - y);
|
|
2479
|
+
if (w > 1e-12 && h > 1e-12) paths.push(createRect(x, y, w, h));
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
return group(paths);
|
|
2483
|
+
}
|
|
2484
|
+
function hexTiling(spacing, minX, minY, maxX, maxY) {
|
|
2485
|
+
const s = Math.max(spacing, 1e-9);
|
|
2486
|
+
const r = s / 2;
|
|
2487
|
+
const dx = s * 0.75;
|
|
2488
|
+
const dy = s * Math.sqrt(3) * 0.5;
|
|
2489
|
+
const paths = [];
|
|
2490
|
+
let row = 0;
|
|
2491
|
+
for (let y = minY + r; y <= maxY + r; y += dy, row++) {
|
|
2492
|
+
const x0 = minX + r + (row % 2 === 0 ? 0 : dx * 0.5);
|
|
2493
|
+
for (let x = x0; x <= maxX + r; x += dx) {
|
|
2494
|
+
paths.push(hexagon(x, y, r));
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
return group(paths);
|
|
2498
|
+
}
|
|
2499
|
+
function hexagon(cx, cy, r) {
|
|
2500
|
+
const ring2 = [];
|
|
2501
|
+
for (let i = 0; i < 6; i++) {
|
|
2502
|
+
const t = Math.PI / 180 * (60 * i - 30);
|
|
2503
|
+
ring2.push({ x: cx + Math.cos(t) * r, y: cy + Math.sin(t) * r });
|
|
2504
|
+
}
|
|
2505
|
+
return polygon(ring2);
|
|
2506
|
+
}
|
|
2507
|
+
function rectSubdivision(p, nx, ny) {
|
|
2508
|
+
const b = bounds(p);
|
|
2509
|
+
const cols = Math.max(1, Math.floor(nx));
|
|
2510
|
+
const rows = Math.max(1, Math.floor(ny));
|
|
2511
|
+
const dw = (b.maxX - b.minX) / cols;
|
|
2512
|
+
const dh = (b.maxY - b.minY) / rows;
|
|
2513
|
+
const paths = [];
|
|
2514
|
+
for (let i = 0; i < cols; i++) {
|
|
2515
|
+
for (let j = 0; j < rows; j++) {
|
|
2516
|
+
const cell = createRect(
|
|
2517
|
+
b.minX + i * dw,
|
|
2518
|
+
b.minY + j * dh,
|
|
2519
|
+
dw,
|
|
2520
|
+
dh
|
|
2521
|
+
);
|
|
2522
|
+
paths.push(...intersect(p, cell).paths);
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
return group(paths);
|
|
2526
|
+
}
|
|
2527
|
+
function quadSubdivision(faces, depth = 1) {
|
|
2528
|
+
let cur = faces;
|
|
2529
|
+
const d = Math.max(0, Math.floor(depth));
|
|
2530
|
+
for (let i = 0; i < d; i++) {
|
|
2531
|
+
const next = [];
|
|
2532
|
+
for (const f of cur) {
|
|
2533
|
+
next.push(...subdivideQuad(f));
|
|
2534
|
+
}
|
|
2535
|
+
cur = next;
|
|
2536
|
+
}
|
|
2537
|
+
return group(cur);
|
|
2538
|
+
}
|
|
2539
|
+
function subdivideQuad(p) {
|
|
2540
|
+
const ring2 = p.rings[0];
|
|
2541
|
+
if (!ring2 || ring2.length < 4) return [p];
|
|
2542
|
+
const [a, b, c, d] = ring2;
|
|
2543
|
+
if (!a || !b || !c || !d) return [p];
|
|
2544
|
+
const mab = mid(a, b);
|
|
2545
|
+
const mbc = mid(b, c);
|
|
2546
|
+
const mcd = mid(c, d);
|
|
2547
|
+
const mda = mid(d, a);
|
|
2548
|
+
const cen = mid(mid(a, c), mid(b, d));
|
|
2549
|
+
return [
|
|
2550
|
+
polygon([a, mab, cen, mda]),
|
|
2551
|
+
polygon([mab, b, mbc, cen]),
|
|
2552
|
+
polygon([cen, mbc, c, mcd]),
|
|
2553
|
+
polygon([mda, cen, mcd, d])
|
|
2554
|
+
];
|
|
2555
|
+
}
|
|
2556
|
+
function triangleSubdivision(faces, depth = 1) {
|
|
2557
|
+
let cur = faces;
|
|
2558
|
+
const d = Math.max(0, Math.floor(depth));
|
|
2559
|
+
for (let i = 0; i < d; i++) {
|
|
2560
|
+
const next = [];
|
|
2561
|
+
for (const f of cur) {
|
|
2562
|
+
next.push(...subdivideTriangle(f));
|
|
2563
|
+
}
|
|
2564
|
+
cur = next;
|
|
2565
|
+
}
|
|
2566
|
+
return group(cur);
|
|
2567
|
+
}
|
|
2568
|
+
function subdivideTriangle(p) {
|
|
2569
|
+
const ring2 = p.rings[0];
|
|
2570
|
+
if (!ring2 || ring2.length < 3) return [p];
|
|
2571
|
+
const a = ring2[0];
|
|
2572
|
+
const b = ring2[1];
|
|
2573
|
+
const c = ring2[2];
|
|
2574
|
+
const mab = mid(a, b);
|
|
2575
|
+
const mbc = mid(b, c);
|
|
2576
|
+
const mca = mid(c, a);
|
|
2577
|
+
return [
|
|
2578
|
+
polygon([a, mab, mca]),
|
|
2579
|
+
polygon([mab, b, mbc]),
|
|
2580
|
+
polygon([mca, mbc, c]),
|
|
2581
|
+
polygon([mab, mbc, mca])
|
|
2582
|
+
];
|
|
2583
|
+
}
|
|
2584
|
+
function sliceDivision(p, spacing, angle = 0) {
|
|
2585
|
+
const s = Math.max(spacing, 1e-9);
|
|
2586
|
+
const b = bounds(p);
|
|
2587
|
+
const cx = (b.minX + b.maxX) / 2;
|
|
2588
|
+
const cy = (b.minY + b.maxY) / 2;
|
|
2589
|
+
const diag = Math.hypot(b.maxX - b.minX, b.maxY - b.minY) + s;
|
|
2590
|
+
const ux = Math.cos(angle);
|
|
2591
|
+
const uy = Math.sin(angle);
|
|
2592
|
+
const nx = -uy;
|
|
2593
|
+
const ny = ux;
|
|
2594
|
+
const parts = [p];
|
|
2595
|
+
for (let t = -diag; t <= diag; t += s) {
|
|
2596
|
+
const a = { x: cx + nx * t - ux * diag, y: cy + ny * t - uy * diag };
|
|
2597
|
+
const bb = { x: cx + nx * t + ux * diag, y: cy + ny * t + uy * diag };
|
|
2598
|
+
const next = [];
|
|
2599
|
+
for (const piece of parts) {
|
|
2600
|
+
const g = slice(piece, a, bb);
|
|
2601
|
+
next.push(...g.paths.length ? g.paths : [piece]);
|
|
2602
|
+
}
|
|
2603
|
+
parts.length = 0;
|
|
2604
|
+
parts.push(...next);
|
|
2605
|
+
}
|
|
2606
|
+
return group(parts);
|
|
2607
|
+
}
|
|
2608
|
+
function hatchSubdivision(p, spacing, angle = 0) {
|
|
2609
|
+
return sliceDivision(p, spacing, angle);
|
|
2610
|
+
}
|
|
2611
|
+
function mid(a, b) {
|
|
2612
|
+
return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
|
|
2613
|
+
}
|
|
2614
|
+
var tiling = {
|
|
2615
|
+
squareGrid: squareGrid2,
|
|
2616
|
+
hexTiling,
|
|
2617
|
+
rectSubdivision,
|
|
2618
|
+
quadSubdivision,
|
|
2619
|
+
triangleSubdivision,
|
|
2620
|
+
sliceDivision,
|
|
2621
|
+
hatchSubdivision
|
|
2622
|
+
};
|
|
2623
|
+
|
|
2624
|
+
// src/polygonisation/index.ts
|
|
2625
|
+
function maxArea(points) {
|
|
2626
|
+
return convexHull(points);
|
|
2627
|
+
}
|
|
2628
|
+
function minArea(points) {
|
|
2629
|
+
const hull2 = convexHull(points).rings[0] ?? [];
|
|
2630
|
+
if (hull2.length < 3) return convexHull(points);
|
|
2631
|
+
if (hull2.length === 3) return polygon(hull2);
|
|
2632
|
+
let best = null;
|
|
2633
|
+
let bestA = Infinity;
|
|
2634
|
+
const h = hull2;
|
|
2635
|
+
const n = h.length;
|
|
2636
|
+
for (let i = 0; i < n; i++) {
|
|
2637
|
+
for (let j = i + 1; j < n; j++) {
|
|
2638
|
+
for (let k = j + 1; k < n; k++) {
|
|
2639
|
+
const tri = polygon([h[i], h[j], h[k]]);
|
|
2640
|
+
if (!coversPoints(tri, points)) continue;
|
|
2641
|
+
const a = area(tri);
|
|
2642
|
+
if (a < bestA) {
|
|
2643
|
+
bestA = a;
|
|
2644
|
+
best = tri;
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
return best ?? convexHull(points);
|
|
2650
|
+
}
|
|
2651
|
+
function minPerimeter(points) {
|
|
2652
|
+
if (points.length === 0) return polygon([]);
|
|
2653
|
+
if (points.length < 3) return polygon(points);
|
|
2654
|
+
const remaining = points.map((p) => ({ ...p }));
|
|
2655
|
+
const ordered = [remaining.shift()];
|
|
2656
|
+
while (remaining.length) {
|
|
2657
|
+
const last = ordered[ordered.length - 1];
|
|
2658
|
+
let bestI = 0;
|
|
2659
|
+
let bestD = Infinity;
|
|
2660
|
+
for (let i = 0; i < remaining.length; i++) {
|
|
2661
|
+
const d = Math.hypot(
|
|
2662
|
+
remaining[i].x - last.x,
|
|
2663
|
+
remaining[i].y - last.y
|
|
2664
|
+
);
|
|
2665
|
+
if (d < bestD) {
|
|
2666
|
+
bestD = d;
|
|
2667
|
+
bestI = i;
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
ordered.push(remaining.splice(bestI, 1)[0]);
|
|
2671
|
+
}
|
|
2672
|
+
return polygon(ordered);
|
|
2673
|
+
}
|
|
2674
|
+
function horizontal(points) {
|
|
2675
|
+
const pts = [...points].sort(
|
|
2676
|
+
(a, b) => a.y === b.y ? a.x - b.x : a.y - b.y
|
|
2677
|
+
);
|
|
2678
|
+
return polygon(pts);
|
|
2679
|
+
}
|
|
2680
|
+
function vertical(points) {
|
|
2681
|
+
const pts = [...points].sort(
|
|
2682
|
+
(a, b) => a.x === b.x ? a.y - b.y : a.x - b.x
|
|
2683
|
+
);
|
|
2684
|
+
return polygon(pts);
|
|
2685
|
+
}
|
|
2686
|
+
function angular(points) {
|
|
2687
|
+
if (points.length === 0) return polygon([]);
|
|
2688
|
+
const c = centroidOf(points);
|
|
2689
|
+
const pts = [...points].sort(
|
|
2690
|
+
(a, b) => Math.atan2(a.y - c.y, a.x - c.x) - Math.atan2(b.y - c.y, b.x - c.x)
|
|
2691
|
+
);
|
|
2692
|
+
return polygon(pts);
|
|
2693
|
+
}
|
|
2694
|
+
function circular(points) {
|
|
2695
|
+
return angular(points);
|
|
2696
|
+
}
|
|
2697
|
+
function onion(points) {
|
|
2698
|
+
const layers = onionLayers(points);
|
|
2699
|
+
return layers[0] ?? polygon([]);
|
|
2700
|
+
}
|
|
2701
|
+
function onionLayers(points) {
|
|
2702
|
+
let remaining = points.map((p) => ({ ...p }));
|
|
2703
|
+
const layers = [];
|
|
2704
|
+
while (remaining.length >= 3) {
|
|
2705
|
+
const hull2 = convexHull(remaining);
|
|
2706
|
+
const ring2 = hull2.rings[0] ?? [];
|
|
2707
|
+
if (ring2.length < 3) break;
|
|
2708
|
+
layers.push(hull2);
|
|
2709
|
+
const onHull = new Set(ring2.map((v) => `${v.x},${v.y}`));
|
|
2710
|
+
remaining = remaining.filter((p) => !onHull.has(`${p.x},${p.y}`));
|
|
2711
|
+
}
|
|
2712
|
+
return layers;
|
|
2713
|
+
}
|
|
2714
|
+
function hilbert(points) {
|
|
2715
|
+
if (points.length === 0) return polygon([]);
|
|
2716
|
+
let minX = Infinity;
|
|
2717
|
+
let minY = Infinity;
|
|
2718
|
+
let maxX = -Infinity;
|
|
2719
|
+
let maxY = -Infinity;
|
|
2720
|
+
for (const p of points) {
|
|
2721
|
+
minX = Math.min(minX, p.x);
|
|
2722
|
+
minY = Math.min(minY, p.y);
|
|
2723
|
+
maxX = Math.max(maxX, p.x);
|
|
2724
|
+
maxY = Math.max(maxY, p.y);
|
|
2725
|
+
}
|
|
2726
|
+
const span = Math.max(maxX - minX, maxY - minY, 1e-9);
|
|
2727
|
+
const order = 10;
|
|
2728
|
+
const n = 1 << order;
|
|
2729
|
+
const scored = points.map((p) => {
|
|
2730
|
+
const x = Math.min(
|
|
2731
|
+
n - 1,
|
|
2732
|
+
Math.max(0, Math.floor((p.x - minX) / span * (n - 1)))
|
|
2733
|
+
);
|
|
2734
|
+
const y = Math.min(
|
|
2735
|
+
n - 1,
|
|
2736
|
+
Math.max(0, Math.floor((p.y - minY) / span * (n - 1)))
|
|
2737
|
+
);
|
|
2738
|
+
return { p, h: xy2d(order, x, y) };
|
|
2739
|
+
});
|
|
2740
|
+
scored.sort((a, b) => a.h - b.h);
|
|
2741
|
+
return polygon(scored.map((s) => s.p));
|
|
2742
|
+
}
|
|
2743
|
+
function coversPoints(poly, points) {
|
|
2744
|
+
const r = poly.rings[0];
|
|
2745
|
+
if (!r || r.length < 3) return false;
|
|
2746
|
+
const a = r[0];
|
|
2747
|
+
const b = r[1];
|
|
2748
|
+
const c = r[2];
|
|
2749
|
+
for (const p of points) {
|
|
2750
|
+
if (!pointInTriangle(p, a, b, c)) return false;
|
|
2751
|
+
}
|
|
2752
|
+
return true;
|
|
2753
|
+
}
|
|
2754
|
+
function pointInTriangle(p, a, b, c) {
|
|
2755
|
+
const v0x = c.x - a.x;
|
|
2756
|
+
const v0y = c.y - a.y;
|
|
2757
|
+
const v1x = b.x - a.x;
|
|
2758
|
+
const v1y = b.y - a.y;
|
|
2759
|
+
const v2x = p.x - a.x;
|
|
2760
|
+
const v2y = p.y - a.y;
|
|
2761
|
+
const dot00 = v0x * v0x + v0y * v0y;
|
|
2762
|
+
const dot01 = v0x * v1x + v0y * v1y;
|
|
2763
|
+
const dot02 = v0x * v2x + v0y * v2y;
|
|
2764
|
+
const dot11 = v1x * v1x + v1y * v1y;
|
|
2765
|
+
const dot12 = v1x * v2x + v1y * v2y;
|
|
2766
|
+
const inv = 1 / (dot00 * dot11 - dot01 * dot01 || 1e-18);
|
|
2767
|
+
const u = (dot11 * dot02 - dot01 * dot12) * inv;
|
|
2768
|
+
const v = (dot00 * dot12 - dot01 * dot02) * inv;
|
|
2769
|
+
return u >= -1e-9 && v >= -1e-9 && u + v <= 1 + 1e-9;
|
|
2770
|
+
}
|
|
2771
|
+
function centroidOf(points) {
|
|
2772
|
+
let sx = 0;
|
|
2773
|
+
let sy = 0;
|
|
2774
|
+
for (const p of points) {
|
|
2775
|
+
sx += p.x;
|
|
2776
|
+
sy += p.y;
|
|
2777
|
+
}
|
|
2778
|
+
return { x: sx / points.length, y: sy / points.length };
|
|
2779
|
+
}
|
|
2780
|
+
function xy2d(order, x, y) {
|
|
2781
|
+
let rx;
|
|
2782
|
+
let ry;
|
|
2783
|
+
let s;
|
|
2784
|
+
let d = 0;
|
|
2785
|
+
for (s = 1 << order - 1; s > 0; s >>= 1) {
|
|
2786
|
+
rx = (x & s) > 0 ? 1 : 0;
|
|
2787
|
+
ry = (y & s) > 0 ? 1 : 0;
|
|
2788
|
+
d += s * s * (3 * rx ^ ry);
|
|
2789
|
+
({ x, y } = rot(s, x, y, rx, ry));
|
|
2790
|
+
}
|
|
2791
|
+
return d;
|
|
2792
|
+
}
|
|
2793
|
+
function rot(n, x, y, rx, ry) {
|
|
2794
|
+
if (ry === 0) {
|
|
2795
|
+
if (rx === 1) {
|
|
2796
|
+
x = n - 1 - x;
|
|
2797
|
+
y = n - 1 - y;
|
|
2798
|
+
}
|
|
2799
|
+
return { x: y, y: x };
|
|
2800
|
+
}
|
|
2801
|
+
return { x, y };
|
|
2802
|
+
}
|
|
2803
|
+
var polygonisation = {
|
|
2804
|
+
maxArea,
|
|
2805
|
+
minArea,
|
|
2806
|
+
minPerimeter,
|
|
2807
|
+
horizontal,
|
|
2808
|
+
vertical,
|
|
2809
|
+
angular,
|
|
2810
|
+
circular,
|
|
2811
|
+
onion,
|
|
2812
|
+
onionLayers,
|
|
2813
|
+
hilbert
|
|
2814
|
+
};
|
|
2815
|
+
|
|
2816
|
+
// src/meshing/index.ts
|
|
2817
|
+
function edgeKey(a, b) {
|
|
2818
|
+
const aKey = `${a.x},${a.y}`;
|
|
2819
|
+
const bKey = `${b.x},${b.y}`;
|
|
2820
|
+
return aKey < bKey ? `${aKey}|${bKey}` : `${bKey}|${aKey}`;
|
|
2821
|
+
}
|
|
2822
|
+
function vertKey(v) {
|
|
2823
|
+
return `${v.x},${v.y}`;
|
|
2824
|
+
}
|
|
2825
|
+
function same(a, b, eps = 1e-12) {
|
|
2826
|
+
return Math.abs(a.x - b.x) < eps && Math.abs(a.y - b.y) < eps;
|
|
2827
|
+
}
|
|
2828
|
+
function dist(a, b) {
|
|
2829
|
+
return Math.hypot(a.x - b.x, a.y - b.y);
|
|
2830
|
+
}
|
|
2831
|
+
function mid2(a, b) {
|
|
2832
|
+
return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
|
|
2833
|
+
}
|
|
2834
|
+
function lerp(a, b, t) {
|
|
2835
|
+
return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
|
|
2836
|
+
}
|
|
2837
|
+
function unique(points) {
|
|
2838
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2839
|
+
const out = [];
|
|
2840
|
+
for (const p of points) {
|
|
2841
|
+
const k = vertKey(p);
|
|
2842
|
+
if (seen.has(k)) continue;
|
|
2843
|
+
seen.add(k);
|
|
2844
|
+
out.push({ ...p });
|
|
2845
|
+
}
|
|
2846
|
+
return out;
|
|
2847
|
+
}
|
|
2848
|
+
function cross3(o, a, b) {
|
|
2849
|
+
return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
|
|
2850
|
+
}
|
|
2851
|
+
function ringOf(face) {
|
|
2852
|
+
return face.rings[0] ?? [];
|
|
2853
|
+
}
|
|
2854
|
+
function buildEdgeMap(faces) {
|
|
2855
|
+
const edges = /* @__PURE__ */ new Map();
|
|
2856
|
+
faces.forEach((f, fi) => {
|
|
2857
|
+
const ring2 = ringOf(f);
|
|
2858
|
+
if (ring2.length < 2) return;
|
|
2859
|
+
const n = ring2.length;
|
|
2860
|
+
const closed = f.closed !== false && n >= 3;
|
|
2861
|
+
const lim = closed ? n : n - 1;
|
|
2862
|
+
for (let i = 0; i < lim; i++) {
|
|
2863
|
+
const a = ring2[i];
|
|
2864
|
+
const b = ring2[(i + 1) % n];
|
|
2865
|
+
const key = edgeKey(a, b);
|
|
2866
|
+
const rec = edges.get(key);
|
|
2867
|
+
if (rec) rec.faceIds.push(fi);
|
|
2868
|
+
else edges.set(key, { a: { ...a }, b: { ...b }, faceIds: [fi] });
|
|
2869
|
+
}
|
|
2870
|
+
});
|
|
2871
|
+
return edges;
|
|
2872
|
+
}
|
|
2873
|
+
function perimeterKeys(edgeMap) {
|
|
2874
|
+
const out = /* @__PURE__ */ new Set();
|
|
2875
|
+
for (const [k, e] of edgeMap) {
|
|
2876
|
+
if (e.faceIds.length === 1) out.add(k);
|
|
2877
|
+
}
|
|
2878
|
+
return out;
|
|
2879
|
+
}
|
|
2880
|
+
function faceAdjacency(faces) {
|
|
2881
|
+
const edgeMap = buildEdgeMap(faces);
|
|
2882
|
+
const adj = faces.map(() => []);
|
|
2883
|
+
for (const e of edgeMap.values()) {
|
|
2884
|
+
if (e.faceIds.length === 2) {
|
|
2885
|
+
const [i, j] = e.faceIds;
|
|
2886
|
+
adj[i].push(j);
|
|
2887
|
+
adj[j].push(i);
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
return adj;
|
|
2891
|
+
}
|
|
2892
|
+
function sitesFromFaces(faces) {
|
|
2893
|
+
return unique(faces.flatMap((f) => ringOf(f)));
|
|
2894
|
+
}
|
|
2895
|
+
function facesFromKeptEdges(tris, kept) {
|
|
2896
|
+
if (tris.length === 0) return [];
|
|
2897
|
+
const parent = tris.map((_, i) => i);
|
|
2898
|
+
const find = (i) => {
|
|
2899
|
+
let x = i;
|
|
2900
|
+
while (parent[x] !== x) {
|
|
2901
|
+
parent[x] = parent[parent[x]];
|
|
2902
|
+
x = parent[x];
|
|
2903
|
+
}
|
|
2904
|
+
return x;
|
|
2905
|
+
};
|
|
2906
|
+
const unite = (a, b) => {
|
|
2907
|
+
const ra = find(a);
|
|
2908
|
+
const rb = find(b);
|
|
2909
|
+
if (ra !== rb) parent[rb] = ra;
|
|
2910
|
+
};
|
|
2911
|
+
const edgeMap = buildEdgeMap(tris);
|
|
2912
|
+
for (const [key, e] of edgeMap) {
|
|
2913
|
+
if (e.faceIds.length === 2 && !kept.has(key)) {
|
|
2914
|
+
unite(e.faceIds[0], e.faceIds[1]);
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
const components = /* @__PURE__ */ new Map();
|
|
2918
|
+
for (let i = 0; i < tris.length; i++) {
|
|
2919
|
+
const r = find(i);
|
|
2920
|
+
const list = components.get(r) ?? [];
|
|
2921
|
+
list.push(i);
|
|
2922
|
+
components.set(r, list);
|
|
2923
|
+
}
|
|
2924
|
+
const out = [];
|
|
2925
|
+
for (const members of components.values()) {
|
|
2926
|
+
const ring2 = boundaryRingOfComponent(tris, members);
|
|
2927
|
+
if (ring2 && ring2.length >= 3) out.push(polygon(ring2));
|
|
2928
|
+
}
|
|
2929
|
+
return out;
|
|
2930
|
+
}
|
|
2931
|
+
function boundaryRingOfComponent(tris, members) {
|
|
2932
|
+
const edgeCount = /* @__PURE__ */ new Map();
|
|
2933
|
+
for (const fi of members) {
|
|
2934
|
+
const ring2 = ringOf(tris[fi]);
|
|
2935
|
+
if (ring2.length < 3) continue;
|
|
2936
|
+
for (let i = 0; i < ring2.length; i++) {
|
|
2937
|
+
const a = ring2[i];
|
|
2938
|
+
const b = ring2[(i + 1) % ring2.length];
|
|
2939
|
+
const key = edgeKey(a, b);
|
|
2940
|
+
const rec = edgeCount.get(key);
|
|
2941
|
+
if (rec) rec.n++;
|
|
2942
|
+
else edgeCount.set(key, { a: { ...a }, b: { ...b }, n: 1 });
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
const boundary = [];
|
|
2946
|
+
for (const e of edgeCount.values()) {
|
|
2947
|
+
if (e.n === 1) boundary.push({ a: e.a, b: e.b });
|
|
2948
|
+
}
|
|
2949
|
+
if (boundary.length < 3) return null;
|
|
2950
|
+
return orderEdgesIntoRing(boundary);
|
|
2951
|
+
}
|
|
2952
|
+
function orderEdgesIntoRing(edges) {
|
|
2953
|
+
if (edges.length === 0) return null;
|
|
2954
|
+
const adj = /* @__PURE__ */ new Map();
|
|
2955
|
+
const add = (from, to) => {
|
|
2956
|
+
const k = vertKey(from);
|
|
2957
|
+
const list = adj.get(k) ?? [];
|
|
2958
|
+
list.push(to);
|
|
2959
|
+
adj.set(k, list);
|
|
2960
|
+
};
|
|
2961
|
+
for (const e of edges) {
|
|
2962
|
+
add(e.a, e.b);
|
|
2963
|
+
add(e.b, e.a);
|
|
2964
|
+
}
|
|
2965
|
+
const start = edges[0].a;
|
|
2966
|
+
const ring2 = [{ ...start }];
|
|
2967
|
+
let prev = start;
|
|
2968
|
+
let curr = edges[0].b;
|
|
2969
|
+
const maxSteps = edges.length + 2;
|
|
2970
|
+
for (let step = 0; step < maxSteps; step++) {
|
|
2971
|
+
ring2.push({ ...curr });
|
|
2972
|
+
if (same(curr, start) && ring2.length > 2) {
|
|
2973
|
+
ring2.pop();
|
|
2974
|
+
return ring2;
|
|
2975
|
+
}
|
|
2976
|
+
const nbrs = adj.get(vertKey(curr)) ?? [];
|
|
2977
|
+
let next = null;
|
|
2978
|
+
for (const n of nbrs) {
|
|
2979
|
+
if (!same(n, prev)) {
|
|
2980
|
+
next = n;
|
|
2981
|
+
break;
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
if (!next) break;
|
|
2985
|
+
prev = curr;
|
|
2986
|
+
curr = next;
|
|
2987
|
+
}
|
|
2988
|
+
return ring2.length >= 3 ? ring2 : null;
|
|
2989
|
+
}
|
|
2990
|
+
function applyEdgeFilter(tris, shouldKeep, preservePerimeter) {
|
|
2991
|
+
const edgeMap = buildEdgeMap(tris);
|
|
2992
|
+
const peri = perimeterKeys(edgeMap);
|
|
2993
|
+
const kept = /* @__PURE__ */ new Set();
|
|
2994
|
+
for (const [key, e] of edgeMap) {
|
|
2995
|
+
const isPeri = peri.has(key);
|
|
2996
|
+
if (isPeri && preservePerimeter) {
|
|
2997
|
+
kept.add(key);
|
|
2998
|
+
continue;
|
|
2999
|
+
}
|
|
3000
|
+
if (shouldKeep(e.a, e.b, isPeri)) kept.add(key);
|
|
3001
|
+
}
|
|
3002
|
+
return facesFromKeptEdges(tris, kept);
|
|
3003
|
+
}
|
|
3004
|
+
function asTriFaces(input) {
|
|
3005
|
+
if (input.length === 0) return [];
|
|
3006
|
+
const first = input[0];
|
|
3007
|
+
if ("rings" in first) return input;
|
|
3008
|
+
return delaunayTriangulationPoints(input);
|
|
3009
|
+
}
|
|
3010
|
+
function pointsOf(input) {
|
|
3011
|
+
if (input.length === 0) return [];
|
|
3012
|
+
const first = input[0];
|
|
3013
|
+
if ("rings" in first) return sitesFromFaces(input);
|
|
3014
|
+
return input;
|
|
3015
|
+
}
|
|
3016
|
+
function extractInnerEdges(faces) {
|
|
3017
|
+
const edgeMap = buildEdgeMap(faces);
|
|
3018
|
+
const out = [];
|
|
3019
|
+
for (const e of edgeMap.values()) {
|
|
3020
|
+
if (e.faceIds.length >= 2) out.push(segment(e.a, e.b));
|
|
3021
|
+
}
|
|
3022
|
+
return out;
|
|
3023
|
+
}
|
|
3024
|
+
function extractInnerVertices(faces) {
|
|
3025
|
+
const edgeMap = buildEdgeMap(faces);
|
|
3026
|
+
const peri = perimeterKeys(edgeMap);
|
|
3027
|
+
const periVerts = /* @__PURE__ */ new Set();
|
|
3028
|
+
for (const key of peri) {
|
|
3029
|
+
const e = edgeMap.get(key);
|
|
3030
|
+
periVerts.add(vertKey(e.a));
|
|
3031
|
+
periVerts.add(vertKey(e.b));
|
|
3032
|
+
}
|
|
3033
|
+
const out = [];
|
|
3034
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3035
|
+
for (const f of faces) {
|
|
3036
|
+
for (const v of ringOf(f)) {
|
|
3037
|
+
const k = vertKey(v);
|
|
3038
|
+
if (periVerts.has(k) || seen.has(k)) continue;
|
|
3039
|
+
seen.add(k);
|
|
3040
|
+
out.push({ ...v });
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
return out;
|
|
3044
|
+
}
|
|
3045
|
+
function findContainingFace(faces, point) {
|
|
3046
|
+
for (const f of faces) {
|
|
3047
|
+
if (containsPoint(f, point)) return f;
|
|
3048
|
+
}
|
|
3049
|
+
return null;
|
|
3050
|
+
}
|
|
3051
|
+
function splitEdges(faces, maxLen) {
|
|
3052
|
+
const lim = Math.max(maxLen, 1e-9);
|
|
3053
|
+
const out = [];
|
|
3054
|
+
for (const f of faces) {
|
|
3055
|
+
out.push(
|
|
3056
|
+
polygon(
|
|
3057
|
+
densifyRing2(ringOf(f), lim),
|
|
3058
|
+
f.rings.slice(1).map((r) => densifyRing2(r, lim))
|
|
3059
|
+
)
|
|
3060
|
+
);
|
|
3061
|
+
}
|
|
3062
|
+
return group(out);
|
|
3063
|
+
}
|
|
3064
|
+
function densifyRing2(ring2, maxLen) {
|
|
3065
|
+
if (ring2.length < 2) return ring2.map((v) => ({ ...v }));
|
|
3066
|
+
const out = [];
|
|
3067
|
+
const n = ring2.length;
|
|
3068
|
+
for (let i = 0; i < n; i++) {
|
|
3069
|
+
const a = ring2[i];
|
|
3070
|
+
const b = ring2[(i + 1) % n];
|
|
3071
|
+
out.push({ ...a });
|
|
3072
|
+
const d = dist(a, b);
|
|
3073
|
+
const steps = Math.floor(d / maxLen);
|
|
3074
|
+
for (let s = 1; s < steps; s++) out.push(lerp(a, b, s / steps));
|
|
3075
|
+
}
|
|
3076
|
+
return out;
|
|
3077
|
+
}
|
|
3078
|
+
function isGabriel(a, b, points) {
|
|
3079
|
+
const m = mid2(a, b);
|
|
3080
|
+
const r = dist(a, b) / 2;
|
|
3081
|
+
for (const p of points) {
|
|
3082
|
+
if (same(p, a) || same(p, b)) continue;
|
|
3083
|
+
if (dist(p, m) < r - 1e-9) return false;
|
|
3084
|
+
}
|
|
3085
|
+
return true;
|
|
3086
|
+
}
|
|
3087
|
+
function isRelativeNeighbor(a, b, points) {
|
|
3088
|
+
const dab = dist(a, b);
|
|
3089
|
+
for (const p of points) {
|
|
3090
|
+
if (same(p, a) || same(p, b)) continue;
|
|
3091
|
+
if (Math.max(dist(p, a), dist(p, b)) < dab - 1e-9) return false;
|
|
3092
|
+
}
|
|
3093
|
+
return true;
|
|
3094
|
+
}
|
|
3095
|
+
function urquhartFaces(input, preservePerimeter = false) {
|
|
3096
|
+
const tris = asTriFaces(input);
|
|
3097
|
+
const edgeMap = buildEdgeMap(tris);
|
|
3098
|
+
const peri = perimeterKeys(edgeMap);
|
|
3099
|
+
const removed = /* @__PURE__ */ new Set();
|
|
3100
|
+
for (const t of tris) {
|
|
3101
|
+
const r = ringOf(t);
|
|
3102
|
+
if (r.length < 3) continue;
|
|
3103
|
+
let longestKey = "";
|
|
3104
|
+
let longestLen = -1;
|
|
3105
|
+
for (let i = 0; i < 3; i++) {
|
|
3106
|
+
const a = r[i];
|
|
3107
|
+
const b = r[(i + 1) % 3];
|
|
3108
|
+
const d = dist(a, b);
|
|
3109
|
+
if (d > longestLen) {
|
|
3110
|
+
longestLen = d;
|
|
3111
|
+
longestKey = edgeKey(a, b);
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
if (longestKey && !(preservePerimeter && peri.has(longestKey))) {
|
|
3115
|
+
removed.add(longestKey);
|
|
3116
|
+
}
|
|
3117
|
+
}
|
|
3118
|
+
const kept = /* @__PURE__ */ new Set();
|
|
3119
|
+
for (const key of edgeMap.keys()) {
|
|
3120
|
+
if (!removed.has(key)) kept.add(key);
|
|
3121
|
+
}
|
|
3122
|
+
return group(facesFromKeptEdges(tris, kept));
|
|
3123
|
+
}
|
|
3124
|
+
function gabrielFaces(input, preservePerimeter = false) {
|
|
3125
|
+
const tris = asTriFaces(input);
|
|
3126
|
+
const pts = pointsOf(input);
|
|
3127
|
+
return group(
|
|
3128
|
+
applyEdgeFilter(
|
|
3129
|
+
tris,
|
|
3130
|
+
(a, b) => isGabriel(a, b, pts),
|
|
3131
|
+
preservePerimeter
|
|
3132
|
+
)
|
|
3133
|
+
);
|
|
3134
|
+
}
|
|
3135
|
+
function relativeNeighborFaces(input, preservePerimeter = false) {
|
|
3136
|
+
const tris = asTriFaces(input);
|
|
3137
|
+
const pts = pointsOf(input);
|
|
3138
|
+
return group(
|
|
3139
|
+
applyEdgeFilter(
|
|
3140
|
+
tris,
|
|
3141
|
+
(a, b) => isRelativeNeighbor(a, b, pts),
|
|
3142
|
+
preservePerimeter
|
|
3143
|
+
)
|
|
3144
|
+
);
|
|
3145
|
+
}
|
|
3146
|
+
function spannerFaces(input, k = 1, preservePerimeter = false) {
|
|
3147
|
+
const tris = asTriFaces(input);
|
|
3148
|
+
const edgeMap = buildEdgeMap(tris);
|
|
3149
|
+
const peri = perimeterKeys(edgeMap);
|
|
3150
|
+
const stretch = Math.max(1, k);
|
|
3151
|
+
const edges = [...edgeMap.entries()].map(([key, e]) => ({
|
|
3152
|
+
key,
|
|
3153
|
+
a: e.a,
|
|
3154
|
+
b: e.b,
|
|
3155
|
+
len: dist(e.a, e.b),
|
|
3156
|
+
peri: peri.has(key)
|
|
3157
|
+
}));
|
|
3158
|
+
edges.sort((a, b) => a.len - b.len);
|
|
3159
|
+
const adj = /* @__PURE__ */ new Map();
|
|
3160
|
+
const link = (a, b, w) => {
|
|
3161
|
+
const ka = vertKey(a);
|
|
3162
|
+
const kb = vertKey(b);
|
|
3163
|
+
if (!adj.has(ka)) adj.set(ka, []);
|
|
3164
|
+
if (!adj.has(kb)) adj.set(kb, []);
|
|
3165
|
+
adj.get(ka).push({ to: kb, w });
|
|
3166
|
+
adj.get(kb).push({ to: ka, w });
|
|
3167
|
+
};
|
|
3168
|
+
const shortest = (from, to) => {
|
|
3169
|
+
const start = vertKey(from);
|
|
3170
|
+
const goal = vertKey(to);
|
|
3171
|
+
const distMap = /* @__PURE__ */ new Map([[start, 0]]);
|
|
3172
|
+
const pq = [{ k: start, d: 0 }];
|
|
3173
|
+
while (pq.length) {
|
|
3174
|
+
pq.sort((x, y) => x.d - y.d);
|
|
3175
|
+
const cur = pq.shift();
|
|
3176
|
+
if (cur.k === goal) return cur.d;
|
|
3177
|
+
if (cur.d > (distMap.get(cur.k) ?? Infinity)) continue;
|
|
3178
|
+
for (const e of adj.get(cur.k) ?? []) {
|
|
3179
|
+
const nd = cur.d + e.w;
|
|
3180
|
+
if (nd < (distMap.get(e.to) ?? Infinity)) {
|
|
3181
|
+
distMap.set(e.to, nd);
|
|
3182
|
+
pq.push({ k: e.to, d: nd });
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
}
|
|
3186
|
+
return Infinity;
|
|
3187
|
+
};
|
|
3188
|
+
const kept = /* @__PURE__ */ new Set();
|
|
3189
|
+
for (const e of edges) {
|
|
3190
|
+
if (e.peri && preservePerimeter) {
|
|
3191
|
+
kept.add(e.key);
|
|
3192
|
+
link(e.a, e.b, e.len);
|
|
3193
|
+
continue;
|
|
3194
|
+
}
|
|
3195
|
+
const pathLen = shortest(e.a, e.b);
|
|
3196
|
+
if (pathLen > stretch * e.len + 1e-9) {
|
|
3197
|
+
kept.add(e.key);
|
|
3198
|
+
link(e.a, e.b, e.len);
|
|
3199
|
+
} else if (e.peri) {
|
|
3200
|
+
kept.add(e.key);
|
|
3201
|
+
link(e.a, e.b, e.len);
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
3204
|
+
return group(facesFromKeptEdges(tris, kept));
|
|
3205
|
+
}
|
|
3206
|
+
function dualFaces(faces) {
|
|
3207
|
+
if (faces.length === 0) return group([]);
|
|
3208
|
+
const edgeMap = buildEdgeMap(faces);
|
|
3209
|
+
const peri = perimeterKeys(edgeMap);
|
|
3210
|
+
const periVerts = /* @__PURE__ */ new Set();
|
|
3211
|
+
for (const key of peri) {
|
|
3212
|
+
const e = edgeMap.get(key);
|
|
3213
|
+
periVerts.add(vertKey(e.a));
|
|
3214
|
+
periVerts.add(vertKey(e.b));
|
|
3215
|
+
}
|
|
3216
|
+
const incident = /* @__PURE__ */ new Map();
|
|
3217
|
+
faces.forEach((f, fi) => {
|
|
3218
|
+
const c = centroid(f);
|
|
3219
|
+
for (const v of ringOf(f)) {
|
|
3220
|
+
const k = vertKey(v);
|
|
3221
|
+
if (periVerts.has(k)) continue;
|
|
3222
|
+
const list = incident.get(k) ?? [];
|
|
3223
|
+
list.push({ fi, ang: Math.atan2(c.y - v.y, c.x - v.x), v });
|
|
3224
|
+
incident.set(k, list);
|
|
3225
|
+
}
|
|
3226
|
+
});
|
|
3227
|
+
const out = [];
|
|
3228
|
+
for (const list of incident.values()) {
|
|
3229
|
+
if (list.length < 3) continue;
|
|
3230
|
+
const byFace = /* @__PURE__ */ new Map();
|
|
3231
|
+
for (const item of list) byFace.set(item.fi, item);
|
|
3232
|
+
const uniqueList = [...byFace.values()];
|
|
3233
|
+
if (uniqueList.length < 3) continue;
|
|
3234
|
+
uniqueList.sort((a, b) => a.ang - b.ang);
|
|
3235
|
+
const ring2 = uniqueList.map((item) => centroid(faces[item.fi]));
|
|
3236
|
+
out.push(polygon(ring2));
|
|
3237
|
+
}
|
|
3238
|
+
return group(out);
|
|
3239
|
+
}
|
|
3240
|
+
function triVerts(t) {
|
|
3241
|
+
const r = ringOf(t);
|
|
3242
|
+
if (r.length < 3) return null;
|
|
3243
|
+
return [r[0], r[1], r[2]];
|
|
3244
|
+
}
|
|
3245
|
+
function midpointMap(faces) {
|
|
3246
|
+
const mids = /* @__PURE__ */ new Map();
|
|
3247
|
+
const edgeMap = buildEdgeMap(faces);
|
|
3248
|
+
for (const [key, e] of edgeMap) {
|
|
3249
|
+
mids.set(key, mid2(e.a, e.b));
|
|
3250
|
+
}
|
|
3251
|
+
return mids;
|
|
3252
|
+
}
|
|
3253
|
+
function centroidQuadrangulation(input, preservePerimeter = true) {
|
|
3254
|
+
const tris = asTriFaces(input);
|
|
3255
|
+
const edgeMap = buildEdgeMap(tris);
|
|
3256
|
+
const centers = tris.map((t) => centroid(t));
|
|
3257
|
+
const out = [];
|
|
3258
|
+
const peri = perimeterKeys(edgeMap);
|
|
3259
|
+
for (const [key, e] of edgeMap) {
|
|
3260
|
+
const ids = e.faceIds;
|
|
3261
|
+
if (ids.length === 2) {
|
|
3262
|
+
const c0 = centers[ids[0]];
|
|
3263
|
+
const c1 = centers[ids[1]];
|
|
3264
|
+
out.push(polygon([e.a, c0, e.b, c1]));
|
|
3265
|
+
} else if (ids.length === 1 && preservePerimeter) {
|
|
3266
|
+
const c0 = centers[ids[0]];
|
|
3267
|
+
out.push(polygon([e.a, e.b, c0]));
|
|
3268
|
+
} else if (ids.length === 1 && !preservePerimeter && peri.has(key)) ;
|
|
3269
|
+
}
|
|
3270
|
+
return group(out);
|
|
3271
|
+
}
|
|
3272
|
+
function splitQuadrangulation(input) {
|
|
3273
|
+
const tris = asTriFaces(input);
|
|
3274
|
+
const mids = midpointMap(tris);
|
|
3275
|
+
const out = [];
|
|
3276
|
+
for (const t of tris) {
|
|
3277
|
+
const v = triVerts(t);
|
|
3278
|
+
if (!v) continue;
|
|
3279
|
+
const [a, b, c] = v;
|
|
3280
|
+
const g = centroid(t);
|
|
3281
|
+
const mab = mids.get(edgeKey(a, b)) ?? mid2(a, b);
|
|
3282
|
+
const mbc = mids.get(edgeKey(b, c)) ?? mid2(b, c);
|
|
3283
|
+
const mca = mids.get(edgeKey(c, a)) ?? mid2(c, a);
|
|
3284
|
+
out.push(polygon([a, mab, g, mca]));
|
|
3285
|
+
out.push(polygon([b, mbc, g, mab]));
|
|
3286
|
+
out.push(polygon([c, mca, g, mbc]));
|
|
3287
|
+
}
|
|
3288
|
+
return group(out);
|
|
3289
|
+
}
|
|
3290
|
+
function edgeCollapseQuadrangulation(input, preservePerimeter = true) {
|
|
3291
|
+
const tris = asTriFaces(input);
|
|
3292
|
+
const edgeMap = buildEdgeMap(tris);
|
|
3293
|
+
const candidates = [];
|
|
3294
|
+
for (const [key, e] of edgeMap) {
|
|
3295
|
+
if (e.faceIds.length !== 2) continue;
|
|
3296
|
+
const [i, j] = e.faceIds;
|
|
3297
|
+
const quad = mergeTriPair(tris[i], tris[j], e.a, e.b);
|
|
3298
|
+
if (!quad) continue;
|
|
3299
|
+
candidates.push({
|
|
3300
|
+
key,
|
|
3301
|
+
a: e.a,
|
|
3302
|
+
b: e.b,
|
|
3303
|
+
i,
|
|
3304
|
+
j,
|
|
3305
|
+
q: quadQuality(quad)
|
|
3306
|
+
});
|
|
3307
|
+
}
|
|
3308
|
+
candidates.sort((x, y) => y.q - x.q);
|
|
3309
|
+
const used = /* @__PURE__ */ new Set();
|
|
3310
|
+
const out = [];
|
|
3311
|
+
for (const c of candidates) {
|
|
3312
|
+
if (used.has(c.i) || used.has(c.j)) continue;
|
|
3313
|
+
const quad = mergeTriPair(tris[c.i], tris[c.j], c.a, c.b);
|
|
3314
|
+
if (!quad) continue;
|
|
3315
|
+
out.push(quad);
|
|
3316
|
+
used.add(c.i);
|
|
3317
|
+
used.add(c.j);
|
|
3318
|
+
}
|
|
3319
|
+
for (let i = 0; i < tris.length; i++) {
|
|
3320
|
+
if (used.has(i)) continue;
|
|
3321
|
+
if (preservePerimeter) out.push(tris[i]);
|
|
3322
|
+
}
|
|
3323
|
+
return group(out);
|
|
3324
|
+
}
|
|
3325
|
+
function matchingQuadrangulation(input) {
|
|
3326
|
+
return edgeCollapseQuadrangulation(input, true);
|
|
3327
|
+
}
|
|
3328
|
+
function mergeTriPair(t0, t1, ea, eb) {
|
|
3329
|
+
const opp0 = oppositeVertex(t0, ea, eb);
|
|
3330
|
+
const opp1 = oppositeVertex(t1, ea, eb);
|
|
3331
|
+
if (!opp0 || !opp1) return null;
|
|
3332
|
+
return polygon([opp0, ea, opp1, eb]);
|
|
3333
|
+
}
|
|
3334
|
+
function oppositeVertex(t, ea, eb) {
|
|
3335
|
+
for (const v of ringOf(t)) {
|
|
3336
|
+
if (!same(v, ea) && !same(v, eb)) return v;
|
|
3337
|
+
}
|
|
3338
|
+
return null;
|
|
3339
|
+
}
|
|
3340
|
+
function quadQuality(q) {
|
|
3341
|
+
const r = ringOf(q);
|
|
3342
|
+
if (r.length < 4) return 0;
|
|
3343
|
+
const lens = [];
|
|
3344
|
+
for (let i = 0; i < 4; i++) {
|
|
3345
|
+
lens.push(dist(r[i], r[(i + 1) % 4]));
|
|
3346
|
+
}
|
|
3347
|
+
const minL = Math.min(...lens);
|
|
3348
|
+
const maxL = Math.max(...lens);
|
|
3349
|
+
if (maxL < 1e-12) return 0;
|
|
3350
|
+
return minL / maxL;
|
|
3351
|
+
}
|
|
3352
|
+
function spiralQuadrangulation(points) {
|
|
3353
|
+
if (points.length < 4) return group([]);
|
|
3354
|
+
const pts = unique(points);
|
|
3355
|
+
const cx = pts.reduce((s, p) => s + p.x, 0) / pts.length;
|
|
3356
|
+
const cy = pts.reduce((s, p) => s + p.y, 0) / pts.length;
|
|
3357
|
+
const withMeta = pts.map((p) => ({
|
|
3358
|
+
p,
|
|
3359
|
+
ang: Math.atan2(p.y - cy, p.x - cx),
|
|
3360
|
+
rad: Math.hypot(p.x - cx, p.y - cy)
|
|
3361
|
+
}));
|
|
3362
|
+
withMeta.sort((a, b) => a.rad - b.rad || a.ang - b.ang);
|
|
3363
|
+
const ringCount = Math.max(2, Math.ceil(Math.sqrt(pts.length)));
|
|
3364
|
+
const rings = Array.from({ length: ringCount }, () => []);
|
|
3365
|
+
withMeta.forEach((item, i) => {
|
|
3366
|
+
const ri = Math.min(
|
|
3367
|
+
ringCount - 1,
|
|
3368
|
+
Math.floor(i / withMeta.length * ringCount)
|
|
3369
|
+
);
|
|
3370
|
+
rings[ri].push(item.p);
|
|
3371
|
+
});
|
|
3372
|
+
for (const ring2 of rings) {
|
|
3373
|
+
ring2.sort(
|
|
3374
|
+
(a, b) => Math.atan2(a.y - cy, a.x - cx) - Math.atan2(b.y - cy, b.x - cx)
|
|
3375
|
+
);
|
|
3376
|
+
}
|
|
3377
|
+
const out = [];
|
|
3378
|
+
for (let r = 0; r < rings.length - 1; r++) {
|
|
3379
|
+
const inner = rings[r];
|
|
3380
|
+
const outer = rings[r + 1];
|
|
3381
|
+
if (inner.length === 0 || outer.length === 0) continue;
|
|
3382
|
+
const n = Math.max(inner.length, outer.length);
|
|
3383
|
+
for (let i = 0; i < n; i++) {
|
|
3384
|
+
const a = inner[i % inner.length];
|
|
3385
|
+
const b = inner[(i + 1) % inner.length];
|
|
3386
|
+
const c = outer[(i + 1) % outer.length];
|
|
3387
|
+
const d = outer[i % outer.length];
|
|
3388
|
+
const verts = unique([a, b, c, d]);
|
|
3389
|
+
if (verts.length >= 3) out.push(polygon(verts.length === 3 ? verts : [a, b, c, d]));
|
|
3390
|
+
}
|
|
3391
|
+
}
|
|
3392
|
+
return group(out);
|
|
3393
|
+
}
|
|
3394
|
+
function vertexNeighborhood(faces) {
|
|
3395
|
+
const edgeMap = buildEdgeMap(faces);
|
|
3396
|
+
const peri = perimeterKeys(edgeMap);
|
|
3397
|
+
const perimeter = /* @__PURE__ */ new Set();
|
|
3398
|
+
for (const key of peri) {
|
|
3399
|
+
const e = edgeMap.get(key);
|
|
3400
|
+
perimeter.add(vertKey(e.a));
|
|
3401
|
+
perimeter.add(vertKey(e.b));
|
|
3402
|
+
}
|
|
3403
|
+
const positions = /* @__PURE__ */ new Map();
|
|
3404
|
+
const neighbors = /* @__PURE__ */ new Map();
|
|
3405
|
+
const link = (a, b) => {
|
|
3406
|
+
const ka = vertKey(a);
|
|
3407
|
+
const kb = vertKey(b);
|
|
3408
|
+
positions.set(ka, { ...a });
|
|
3409
|
+
positions.set(kb, { ...b });
|
|
3410
|
+
if (!neighbors.has(ka)) neighbors.set(ka, /* @__PURE__ */ new Set());
|
|
3411
|
+
if (!neighbors.has(kb)) neighbors.set(kb, /* @__PURE__ */ new Set());
|
|
3412
|
+
neighbors.get(ka).add(kb);
|
|
3413
|
+
neighbors.get(kb).add(ka);
|
|
3414
|
+
};
|
|
3415
|
+
for (const e of edgeMap.values()) link(e.a, e.b);
|
|
3416
|
+
return { positions, neighbors, perimeter };
|
|
3417
|
+
}
|
|
3418
|
+
function remapFaces(faces, positions) {
|
|
3419
|
+
return faces.map(
|
|
3420
|
+
(f) => polygon(
|
|
3421
|
+
ringOf(f).map((v) => {
|
|
3422
|
+
const p = positions.get(vertKey(v));
|
|
3423
|
+
return p ? { ...p } : { ...v };
|
|
3424
|
+
}),
|
|
3425
|
+
f.rings.slice(1).map(
|
|
3426
|
+
(r) => r.map((v) => {
|
|
3427
|
+
const p = positions.get(vertKey(v));
|
|
3428
|
+
return p ? { ...p } : { ...v };
|
|
3429
|
+
})
|
|
3430
|
+
)
|
|
3431
|
+
)
|
|
3432
|
+
);
|
|
3433
|
+
}
|
|
3434
|
+
function smoothMesh(faces, iterationsOrCutoff, preservePerimeter = true) {
|
|
3435
|
+
if (faces.length === 0) return group([]);
|
|
3436
|
+
let { positions, neighbors, perimeter } = vertexNeighborhood(faces);
|
|
3437
|
+
const maxIter = iterationsOrCutoff >= 1 && Number.isInteger(iterationsOrCutoff) ? iterationsOrCutoff : 200;
|
|
3438
|
+
const cutoff = iterationsOrCutoff > 0 && iterationsOrCutoff < 1 ? iterationsOrCutoff : 0;
|
|
3439
|
+
for (let iter = 0; iter < maxIter; iter++) {
|
|
3440
|
+
const next = /* @__PURE__ */ new Map();
|
|
3441
|
+
let maxDisp = 0;
|
|
3442
|
+
for (const [k, p] of positions) {
|
|
3443
|
+
if (preservePerimeter && perimeter.has(k)) {
|
|
3444
|
+
next.set(k, p);
|
|
3445
|
+
continue;
|
|
3446
|
+
}
|
|
3447
|
+
const nbrs = neighbors.get(k);
|
|
3448
|
+
if (!nbrs || nbrs.size === 0) {
|
|
3449
|
+
next.set(k, p);
|
|
3450
|
+
continue;
|
|
3451
|
+
}
|
|
3452
|
+
let sx = 0;
|
|
3453
|
+
let sy = 0;
|
|
3454
|
+
for (const nk of nbrs) {
|
|
3455
|
+
const n = positions.get(nk);
|
|
3456
|
+
sx += n.x;
|
|
3457
|
+
sy += n.y;
|
|
3458
|
+
}
|
|
3459
|
+
const np = { x: sx / nbrs.size, y: sy / nbrs.size };
|
|
3460
|
+
maxDisp = Math.max(maxDisp, dist(p, np));
|
|
3461
|
+
next.set(k, np);
|
|
3462
|
+
}
|
|
3463
|
+
positions = next;
|
|
3464
|
+
if (cutoff > 0 && maxDisp < cutoff) break;
|
|
3465
|
+
}
|
|
3466
|
+
return group(remapFaces(faces, positions));
|
|
3467
|
+
}
|
|
3468
|
+
function subdivideMesh(faces, edgeSplitRatio = 0.5) {
|
|
3469
|
+
const t = Math.min(1, Math.max(0, edgeSplitRatio));
|
|
3470
|
+
const out = [];
|
|
3471
|
+
for (const f of faces) {
|
|
3472
|
+
const ring2 = ringOf(f);
|
|
3473
|
+
if (ring2.length < 3) continue;
|
|
3474
|
+
const c = centroid(f);
|
|
3475
|
+
const n = ring2.length;
|
|
3476
|
+
const splits = [];
|
|
3477
|
+
for (let i = 0; i < n; i++) {
|
|
3478
|
+
const a = ring2[i];
|
|
3479
|
+
const b = ring2[(i + 1) % n];
|
|
3480
|
+
splits.push(lerp(a, b, t));
|
|
3481
|
+
}
|
|
3482
|
+
for (let i = 0; i < n; i++) {
|
|
3483
|
+
const v = ring2[i];
|
|
3484
|
+
const sPrev = splits[(i - 1 + n) % n];
|
|
3485
|
+
const sNext = splits[i];
|
|
3486
|
+
out.push(polygon([v, sNext, c, sPrev]));
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
return group(out);
|
|
3490
|
+
}
|
|
3491
|
+
function simplifyMesh(faces, tolerance, preservePerimeter = true) {
|
|
3492
|
+
if (tolerance <= 0 || faces.length === 0) return group(faces.map((f) => polygon(ringOf(f), f.rings.slice(1))));
|
|
3493
|
+
const edgeMap = buildEdgeMap(faces);
|
|
3494
|
+
const peri = perimeterKeys(edgeMap);
|
|
3495
|
+
const simplified = faces.map((f) => {
|
|
3496
|
+
const ring2 = ringOf(f);
|
|
3497
|
+
if (ring2.length <= 3) return polygon(ring2);
|
|
3498
|
+
if (preservePerimeter) {
|
|
3499
|
+
const s = simplify(f, tolerance);
|
|
3500
|
+
const sr = ringOf(s);
|
|
3501
|
+
if (sr.length < 3) return polygon(ring2);
|
|
3502
|
+
return polygon(sr, s.rings.slice(1));
|
|
3503
|
+
}
|
|
3504
|
+
return simplify(f, tolerance);
|
|
3505
|
+
});
|
|
3506
|
+
const all = unique(simplified.flatMap((f) => ringOf(f)));
|
|
3507
|
+
const snapped = clusterSnap(all, Math.max(tolerance * 0.5, 1e-9));
|
|
3508
|
+
const remap = (v) => {
|
|
3509
|
+
let best = v;
|
|
3510
|
+
let bestD = Infinity;
|
|
3511
|
+
for (const s of snapped) {
|
|
3512
|
+
const d = dist(v, s);
|
|
3513
|
+
if (d < bestD) {
|
|
3514
|
+
bestD = d;
|
|
3515
|
+
best = s;
|
|
3516
|
+
}
|
|
3517
|
+
}
|
|
3518
|
+
return { ...best };
|
|
3519
|
+
};
|
|
3520
|
+
const periVerts = /* @__PURE__ */ new Set();
|
|
3521
|
+
if (preservePerimeter) {
|
|
3522
|
+
for (const key of peri) {
|
|
3523
|
+
const e = edgeMap.get(key);
|
|
3524
|
+
periVerts.add(vertKey(e.a));
|
|
3525
|
+
periVerts.add(vertKey(e.b));
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
return group(
|
|
3529
|
+
simplified.map((f, i) => {
|
|
3530
|
+
const original = ringOf(faces[i]);
|
|
3531
|
+
const ring2 = ringOf(f).map((v, vi) => {
|
|
3532
|
+
if (preservePerimeter && original[vi] && periVerts.has(vertKey(original[vi]))) {
|
|
3533
|
+
return { ...original[vi] };
|
|
3534
|
+
}
|
|
3535
|
+
for (const ov of original) {
|
|
3536
|
+
if (periVerts.has(vertKey(ov)) && dist(v, ov) < tolerance * 2) {
|
|
3537
|
+
return { ...ov };
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3540
|
+
return remap(v);
|
|
3541
|
+
});
|
|
3542
|
+
return polygon(unique(ring2).length >= 3 ? ring2 : original);
|
|
3543
|
+
})
|
|
3544
|
+
);
|
|
3545
|
+
}
|
|
3546
|
+
function clusterSnap(points, tol) {
|
|
3547
|
+
const clusters = [];
|
|
3548
|
+
for (const p of points) {
|
|
3549
|
+
let found = false;
|
|
3550
|
+
for (const c of clusters) {
|
|
3551
|
+
if (dist(p, c[0]) <= tol) {
|
|
3552
|
+
c.push(p);
|
|
3553
|
+
found = true;
|
|
3554
|
+
break;
|
|
3555
|
+
}
|
|
3556
|
+
}
|
|
3557
|
+
if (!found) clusters.push([p]);
|
|
3558
|
+
}
|
|
3559
|
+
return clusters.map((c) => ({
|
|
3560
|
+
x: c.reduce((s, p) => s + p.x, 0) / c.length,
|
|
3561
|
+
y: c.reduce((s, p) => s + p.y, 0) / c.length
|
|
3562
|
+
}));
|
|
3563
|
+
}
|
|
3564
|
+
function stochasticMerge(faces, nClasses, seed = 1) {
|
|
3565
|
+
if (faces.length === 0) return group([]);
|
|
3566
|
+
const classes = Math.max(1, Math.floor(nClasses));
|
|
3567
|
+
const rng = mulberry324(seed >>> 0);
|
|
3568
|
+
const label = faces.map(() => Math.floor(rng() * classes));
|
|
3569
|
+
const adj = faceAdjacency(faces);
|
|
3570
|
+
const parent = faces.map((_, i) => i);
|
|
3571
|
+
const find = (i) => {
|
|
3572
|
+
let x = i;
|
|
3573
|
+
while (parent[x] !== x) {
|
|
3574
|
+
parent[x] = parent[parent[x]];
|
|
3575
|
+
x = parent[x];
|
|
3576
|
+
}
|
|
3577
|
+
return x;
|
|
3578
|
+
};
|
|
3579
|
+
const unite = (a, b) => {
|
|
3580
|
+
const ra = find(a);
|
|
3581
|
+
const rb = find(b);
|
|
3582
|
+
if (ra !== rb) parent[rb] = ra;
|
|
3583
|
+
};
|
|
3584
|
+
for (let i = 0; i < faces.length; i++) {
|
|
3585
|
+
for (const j of adj[i]) {
|
|
3586
|
+
if (j > i && label[i] === label[j]) unite(i, j);
|
|
3587
|
+
}
|
|
3588
|
+
}
|
|
3589
|
+
const comps = /* @__PURE__ */ new Map();
|
|
3590
|
+
for (let i = 0; i < faces.length; i++) {
|
|
3591
|
+
const r = find(i);
|
|
3592
|
+
const list = comps.get(r) ?? [];
|
|
3593
|
+
list.push(i);
|
|
3594
|
+
comps.set(r, list);
|
|
3595
|
+
}
|
|
3596
|
+
const out = [];
|
|
3597
|
+
for (const members of comps.values()) {
|
|
3598
|
+
if (members.length === 1) {
|
|
3599
|
+
out.push(faces[members[0]]);
|
|
3600
|
+
continue;
|
|
3601
|
+
}
|
|
3602
|
+
const merged = dissolveFaces(members.map((i) => faces[i]));
|
|
3603
|
+
out.push(...merged);
|
|
3604
|
+
}
|
|
3605
|
+
return group(out);
|
|
3606
|
+
}
|
|
3607
|
+
function dissolveFaces(faces) {
|
|
3608
|
+
if (faces.length === 0) return [];
|
|
3609
|
+
if (faces.length === 1) return [faces[0]];
|
|
3610
|
+
try {
|
|
3611
|
+
let acc = faces[0];
|
|
3612
|
+
for (let i = 1; i < faces.length; i++) {
|
|
3613
|
+
const u = union(acc, faces[i]);
|
|
3614
|
+
if (u.paths.length === 0) continue;
|
|
3615
|
+
acc = u.paths.reduce(
|
|
3616
|
+
(best, p) => area(p) > area(best) ? p : best
|
|
3617
|
+
);
|
|
3618
|
+
}
|
|
3619
|
+
return [acc];
|
|
3620
|
+
} catch {
|
|
3621
|
+
return [convexHullOf(faces.flatMap((f) => ringOf(f)))];
|
|
3622
|
+
}
|
|
3623
|
+
}
|
|
3624
|
+
function areaMerge(faces, minAreaOrOptions) {
|
|
3625
|
+
let working = [...faces];
|
|
3626
|
+
if (working.length === 0) return group([]);
|
|
3627
|
+
if (typeof minAreaOrOptions === "object" && minAreaOrOptions.remainingFaces != null) {
|
|
3628
|
+
const target = Math.max(1, Math.floor(minAreaOrOptions.remainingFaces));
|
|
3629
|
+
while (working.length > target) {
|
|
3630
|
+
const next = mergeSmallestAdjacent(working);
|
|
3631
|
+
if (next.length >= working.length) break;
|
|
3632
|
+
working = next;
|
|
3633
|
+
}
|
|
3634
|
+
return group(working);
|
|
3635
|
+
}
|
|
3636
|
+
const minArea2 = typeof minAreaOrOptions === "number" ? minAreaOrOptions : minAreaOrOptions.minArea ?? 0;
|
|
3637
|
+
let guard = 0;
|
|
3638
|
+
while (guard++ < working.length + 10) {
|
|
3639
|
+
const smallIdx = working.findIndex((f) => area(f) < minArea2);
|
|
3640
|
+
if (smallIdx < 0) break;
|
|
3641
|
+
const next = mergeFaceIntoNeighbor(working, smallIdx);
|
|
3642
|
+
if (!next) break;
|
|
3643
|
+
working = next;
|
|
3644
|
+
}
|
|
3645
|
+
return group(working);
|
|
3646
|
+
}
|
|
3647
|
+
function mergeSmallestAdjacent(faces) {
|
|
3648
|
+
let best = -1;
|
|
3649
|
+
let bestA = Infinity;
|
|
3650
|
+
for (let i = 0; i < faces.length; i++) {
|
|
3651
|
+
const a = area(faces[i]);
|
|
3652
|
+
if (a < bestA) {
|
|
3653
|
+
bestA = a;
|
|
3654
|
+
best = i;
|
|
3655
|
+
}
|
|
3656
|
+
}
|
|
3657
|
+
if (best < 0) return faces;
|
|
3658
|
+
return mergeFaceIntoNeighbor(faces, best) ?? faces;
|
|
3659
|
+
}
|
|
3660
|
+
function mergeFaceIntoNeighbor(faces, idx) {
|
|
3661
|
+
const adj = faceAdjacency(faces);
|
|
3662
|
+
const nbrs = adj[idx] ?? [];
|
|
3663
|
+
if (nbrs.length === 0) {
|
|
3664
|
+
return faces.filter((_, i) => i !== idx);
|
|
3665
|
+
}
|
|
3666
|
+
let bestN = nbrs[0];
|
|
3667
|
+
let bestA = -1;
|
|
3668
|
+
for (const n of nbrs) {
|
|
3669
|
+
const a = area(faces[n]);
|
|
3670
|
+
if (a > bestA) {
|
|
3671
|
+
bestA = a;
|
|
3672
|
+
bestN = n;
|
|
3673
|
+
}
|
|
3674
|
+
}
|
|
3675
|
+
const merged = dissolveFaces([faces[idx], faces[bestN]]);
|
|
3676
|
+
const out = [];
|
|
3677
|
+
for (let i = 0; i < faces.length; i++) {
|
|
3678
|
+
if (i === idx || i === bestN) continue;
|
|
3679
|
+
out.push(faces[i]);
|
|
3680
|
+
}
|
|
3681
|
+
out.push(...merged);
|
|
3682
|
+
return out;
|
|
3683
|
+
}
|
|
3684
|
+
function convexHullOf(points) {
|
|
3685
|
+
const pts = unique(points);
|
|
3686
|
+
if (pts.length < 3) return polygon(pts);
|
|
3687
|
+
pts.sort((a, b) => a.x === b.x ? a.y - b.y : a.x - b.x);
|
|
3688
|
+
const lower = [];
|
|
3689
|
+
for (const p of pts) {
|
|
3690
|
+
while (lower.length >= 2 && cross3(lower[lower.length - 2], lower[lower.length - 1], p) <= 0) {
|
|
3691
|
+
lower.pop();
|
|
3692
|
+
}
|
|
3693
|
+
lower.push(p);
|
|
3694
|
+
}
|
|
3695
|
+
const upper = [];
|
|
3696
|
+
for (let i = pts.length - 1; i >= 0; i--) {
|
|
3697
|
+
const p = pts[i];
|
|
3698
|
+
while (upper.length >= 2 && cross3(upper[upper.length - 2], upper[upper.length - 1], p) <= 0) {
|
|
3699
|
+
upper.pop();
|
|
3700
|
+
}
|
|
3701
|
+
upper.push(p);
|
|
3702
|
+
}
|
|
3703
|
+
lower.pop();
|
|
3704
|
+
upper.pop();
|
|
3705
|
+
return polygon([...lower, ...upper]);
|
|
3706
|
+
}
|
|
3707
|
+
function mulberry324(a) {
|
|
3708
|
+
return () => {
|
|
3709
|
+
a |= 0;
|
|
3710
|
+
a = a + 1831565813 | 0;
|
|
3711
|
+
let t = Math.imul(a ^ a >>> 15, 1 | a);
|
|
3712
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
3713
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
3714
|
+
};
|
|
3715
|
+
}
|
|
3716
|
+
function findBreaks(faces) {
|
|
3717
|
+
const edgeMap = buildEdgeMap(faces);
|
|
3718
|
+
const segs = [];
|
|
3719
|
+
const peri = [];
|
|
3720
|
+
for (const e of edgeMap.values()) {
|
|
3721
|
+
if (e.faceIds.length === 1) peri.push(e);
|
|
3722
|
+
}
|
|
3723
|
+
const used = /* @__PURE__ */ new Set();
|
|
3724
|
+
for (let i = 0; i < peri.length; i++) {
|
|
3725
|
+
if (used.has(i)) continue;
|
|
3726
|
+
const a = peri[i];
|
|
3727
|
+
for (let j = i + 1; j < peri.length; j++) {
|
|
3728
|
+
if (used.has(j)) continue;
|
|
3729
|
+
const b = peri[j];
|
|
3730
|
+
if (nearDuplicateEdge(a, b, 1e-3)) {
|
|
3731
|
+
segs.push(polyline([a.a, a.b]));
|
|
3732
|
+
segs.push(polyline([b.a, b.b]));
|
|
3733
|
+
used.add(i);
|
|
3734
|
+
used.add(j);
|
|
3735
|
+
break;
|
|
3736
|
+
}
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3739
|
+
return group(segs);
|
|
3740
|
+
}
|
|
3741
|
+
function nearDuplicateEdge(a, b, tol) {
|
|
3742
|
+
const d1 = dist(a.a, b.a) + dist(a.b, b.b);
|
|
3743
|
+
const d2 = dist(a.a, b.b) + dist(a.b, b.a);
|
|
3744
|
+
const len = dist(a.a, a.b);
|
|
3745
|
+
return Math.min(d1, d2) < tol * 2 + len * 0.05 && Math.min(d1, d2) > 1e-9;
|
|
3746
|
+
}
|
|
3747
|
+
function fixBreaks(faces, maxGapWidth) {
|
|
3748
|
+
if (faces.length === 0) return group([]);
|
|
3749
|
+
const tol = Math.max(maxGapWidth, 1e-9);
|
|
3750
|
+
const verts = unique(faces.flatMap((f) => ringOf(f)));
|
|
3751
|
+
const snapped = clusterSnap(verts, tol);
|
|
3752
|
+
const remap = (v) => {
|
|
3753
|
+
let best = snapped[0];
|
|
3754
|
+
let bestD = Infinity;
|
|
3755
|
+
for (const s of snapped) {
|
|
3756
|
+
const d = dist(v, s);
|
|
3757
|
+
if (d < bestD) {
|
|
3758
|
+
bestD = d;
|
|
3759
|
+
best = s;
|
|
3760
|
+
}
|
|
3761
|
+
}
|
|
3762
|
+
return { ...best };
|
|
3763
|
+
};
|
|
3764
|
+
const fixed = faces.map((f) => {
|
|
3765
|
+
const ring2 = unique(ringOf(f).map(remap));
|
|
3766
|
+
return ring2.length >= 3 ? polygon(ring2) : f;
|
|
3767
|
+
});
|
|
3768
|
+
try {
|
|
3769
|
+
return group(dissolveOverlaps(fixed));
|
|
3770
|
+
} catch {
|
|
3771
|
+
return group(fixed);
|
|
3772
|
+
}
|
|
3773
|
+
}
|
|
3774
|
+
function dissolveOverlaps(faces) {
|
|
3775
|
+
let working = [...faces];
|
|
3776
|
+
let changed = true;
|
|
3777
|
+
let guard = 0;
|
|
3778
|
+
while (changed && guard++ < working.length) {
|
|
3779
|
+
changed = false;
|
|
3780
|
+
outer: for (let i = 0; i < working.length; i++) {
|
|
3781
|
+
for (let j = i + 1; j < working.length; j++) {
|
|
3782
|
+
const a = working[i];
|
|
3783
|
+
const b = working[j];
|
|
3784
|
+
const u = union(a, b);
|
|
3785
|
+
const ua = u.paths.reduce((s, p) => s + area(p), 0);
|
|
3786
|
+
if (ua < area(a) + area(b) - 1e-6) {
|
|
3787
|
+
const next = working.filter((_, k) => k !== i && k !== j);
|
|
3788
|
+
next.push(...u.paths);
|
|
3789
|
+
working = next;
|
|
3790
|
+
changed = true;
|
|
3791
|
+
break outer;
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
}
|
|
3796
|
+
return working;
|
|
3797
|
+
}
|
|
3798
|
+
function fixBrokenFaces(coverage, tolerance, polygonise = true) {
|
|
3799
|
+
const tol = Math.max(tolerance, 1e-9);
|
|
3800
|
+
const anchors = [];
|
|
3801
|
+
const endpoints = [];
|
|
3802
|
+
coverage.forEach((p, pi) => {
|
|
3803
|
+
const ring2 = ringOf(p);
|
|
3804
|
+
if (ring2.length === 0) return;
|
|
3805
|
+
if (p.closed) {
|
|
3806
|
+
for (const v of ring2) anchors.push(v);
|
|
3807
|
+
} else {
|
|
3808
|
+
endpoints.push({ pathIdx: pi, end: 0, v: ring2[0] });
|
|
3809
|
+
endpoints.push({
|
|
3810
|
+
pathIdx: pi,
|
|
3811
|
+
end: 1,
|
|
3812
|
+
v: ring2[ring2.length - 1]
|
|
3813
|
+
});
|
|
3814
|
+
}
|
|
3815
|
+
});
|
|
3816
|
+
const snapTargets = clusterSnap(
|
|
3817
|
+
[...anchors, ...endpoints.map((e) => e.v)],
|
|
3818
|
+
tol
|
|
3819
|
+
);
|
|
3820
|
+
const snapTo = (v, preferAnchor) => {
|
|
3821
|
+
let best = v;
|
|
3822
|
+
let bestD = Infinity;
|
|
3823
|
+
for (const s of snapTargets) {
|
|
3824
|
+
const d = dist(v, s);
|
|
3825
|
+
if (d <= tol && d < bestD) {
|
|
3826
|
+
bestD = d;
|
|
3827
|
+
best = s;
|
|
3828
|
+
}
|
|
3829
|
+
}
|
|
3830
|
+
{
|
|
3831
|
+
for (const a of anchors) {
|
|
3832
|
+
if (dist(v, a) <= tol) return { ...a };
|
|
3833
|
+
}
|
|
3834
|
+
}
|
|
3835
|
+
return { ...best };
|
|
3836
|
+
};
|
|
3837
|
+
const snappedPaths = coverage.map((p) => {
|
|
3838
|
+
const ring2 = ringOf(p);
|
|
3839
|
+
if (ring2.length === 0) return p;
|
|
3840
|
+
if (p.closed) return p;
|
|
3841
|
+
const next = ring2.map((v) => ({ ...v }));
|
|
3842
|
+
next[0] = snapTo(ring2[0]);
|
|
3843
|
+
next[next.length - 1] = snapTo(ring2[ring2.length - 1]);
|
|
3844
|
+
return polyline(next);
|
|
3845
|
+
});
|
|
3846
|
+
if (!polygonise) return group(snappedPaths);
|
|
3847
|
+
const out = [];
|
|
3848
|
+
for (const p of snappedPaths) {
|
|
3849
|
+
const ring2 = ringOf(p);
|
|
3850
|
+
if (p.closed) {
|
|
3851
|
+
out.push(p);
|
|
3852
|
+
continue;
|
|
3853
|
+
}
|
|
3854
|
+
if (ring2.length >= 3 && same(ring2[0], ring2[ring2.length - 1], tol * 2)) {
|
|
3855
|
+
out.push(polygon(ring2.slice(0, -1)));
|
|
3856
|
+
} else if (ring2.length >= 3 && dist(ring2[0], ring2[ring2.length - 1]) <= tol * 2) {
|
|
3857
|
+
out.push(polygon(ring2));
|
|
3858
|
+
} else {
|
|
3859
|
+
out.push(p);
|
|
3860
|
+
}
|
|
3861
|
+
}
|
|
3862
|
+
return group(out);
|
|
3863
|
+
}
|
|
3864
|
+
function findIslands(faces) {
|
|
3865
|
+
if (faces.length === 0) return [];
|
|
3866
|
+
const adj = faceAdjacency(faces);
|
|
3867
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3868
|
+
const islands = [];
|
|
3869
|
+
for (let i = 0; i < faces.length; i++) {
|
|
3870
|
+
if (seen.has(i)) continue;
|
|
3871
|
+
const stack = [i];
|
|
3872
|
+
const members = [];
|
|
3873
|
+
seen.add(i);
|
|
3874
|
+
while (stack.length) {
|
|
3875
|
+
const cur = stack.pop();
|
|
3876
|
+
members.push(cur);
|
|
3877
|
+
for (const n of adj[cur]) {
|
|
3878
|
+
if (!seen.has(n)) {
|
|
3879
|
+
seen.add(n);
|
|
3880
|
+
stack.push(n);
|
|
3881
|
+
}
|
|
3882
|
+
}
|
|
3883
|
+
}
|
|
3884
|
+
islands.push(members);
|
|
3885
|
+
}
|
|
3886
|
+
islands.sort((a, b) => b.length - a.length);
|
|
3887
|
+
return islands.map((m) => group(m.map((i) => faces[i])));
|
|
3888
|
+
}
|
|
3889
|
+
function radialSortFaces(faces) {
|
|
3890
|
+
if (faces.length === 0) return [];
|
|
3891
|
+
const cs = faces.map((f) => centroid(f));
|
|
3892
|
+
const cx = cs.reduce((s, p) => s + p.x, 0) / cs.length;
|
|
3893
|
+
const cy = cs.reduce((s, p) => s + p.y, 0) / cs.length;
|
|
3894
|
+
return faces.map((f, i) => ({ f, a: Math.atan2(cs[i].y - cy, cs[i].x - cx) })).sort((a, b) => a.a - b.a).map((x) => x.f);
|
|
3895
|
+
}
|
|
3896
|
+
function centroidSortFaces(faces) {
|
|
3897
|
+
return [...faces].sort((a, b) => {
|
|
3898
|
+
const ca = centroid(a);
|
|
3899
|
+
const cb = centroid(b);
|
|
3900
|
+
return ca.x === cb.x ? ca.y - cb.y : ca.x - cb.x;
|
|
3901
|
+
});
|
|
3902
|
+
}
|
|
3903
|
+
var meshing = {
|
|
3904
|
+
extractInnerEdges,
|
|
3905
|
+
extractInnerVertices,
|
|
3906
|
+
findContainingFace,
|
|
3907
|
+
splitEdges,
|
|
3908
|
+
urquhartFaces,
|
|
3909
|
+
gabrielFaces,
|
|
3910
|
+
relativeNeighborFaces,
|
|
3911
|
+
spannerFaces,
|
|
3912
|
+
dualFaces,
|
|
3913
|
+
centroidQuadrangulation,
|
|
3914
|
+
edgeCollapseQuadrangulation,
|
|
3915
|
+
splitQuadrangulation,
|
|
3916
|
+
spiralQuadrangulation,
|
|
3917
|
+
matchingQuadrangulation,
|
|
3918
|
+
smoothMesh,
|
|
3919
|
+
subdivideMesh,
|
|
3920
|
+
simplifyMesh,
|
|
3921
|
+
stochasticMerge,
|
|
3922
|
+
areaMerge,
|
|
3923
|
+
radialSortFaces,
|
|
3924
|
+
centroidSortFaces,
|
|
3925
|
+
findBreaks,
|
|
3926
|
+
fixBreaks,
|
|
3927
|
+
fixBrokenFaces,
|
|
3928
|
+
findIslands
|
|
3929
|
+
};
|
|
3930
|
+
|
|
3931
|
+
export { angular, area, areaGroup, areaMerge, areaSimilarity, assertAreaClose, boundingBox, bounds, buffer, centroid, centroidQuadrangulation, centroidSortFaces, centroidSplit, chaikinCut, circle, circleCenter, circleContainedInPath, circleOverlapsPath, circlePacking, circular, clipSegmentToPath, clipSegmentsToPath, cloneGroup, clonePath, cloneVec2, closestPoint, closestPointPair, closestVertex, complement, compoundVoronoi, construction, containmentAgreement, containsPoint, contour, conversion, convexHull, convexHullPath, copy, createArc, createCircle, createKochSnowflake, createRect, createRegularPolygon, createRing, createStar, delaunayTriangulation, delaunayTriangulationPoints, densify, dilationErosion, dissolve, dualFaces, earCutTriangulation, edgeCollapseQuadrangulation, envelope, equalsVec2, erosionDilation, extractBoundary, extractHoles, extractInnerEdges, extractInnerVertices, extractPerimeter, farthestPointPair, filterAxisAligned, filterByMinLength, findBreaks, findContainingFace, findIslands, fixBreaks, fixBrokenFaces, flatten, flipHorizontal, flipVertical, fromArray, fromContours, frontChainPack, gabrielFaces, generateRandomGridPoints, generateRandomPoints, group, groupFromPaths, hatch, cross as hatchCross, parallel as hatchParallel, hatchSubdivision, height, hexGrid, hexLatticePack, hexTiling, hilbert as hilbertPolygonise, hull, innerVoronoi, innerVoronoiRaw, intersect, intersectionPoints, largestEmptyCircle, largestEmptyCircles, matchingQuadrangulation, maxArea, maximumInscribedAARectangle, maximumInscribedCircle, maximumInscribedPack, maximumInscribedPackUntil, meshing, minArea, minPerimeter, minimumBoundingCircle, minkDifference, minkSum, morphology, normalizeRing, obstaclePack, occlusionSubtract, offsetCurvesInward, offsetCurvesOutward, onion, onionLayers, optimisation, oraclePathsToGroup, originScale, overlapRegions, parallelSegments, parsePathData, parseSvg, path, ring as pointRing, pointSet, pointsOnExterior, poisson, poissonTriangulation, poissonTriangulationPoints, polygon, polygonisation, horizontal as polygoniseHorizontal, vertical as polygoniseVertical, polyline, predicates, processing, prunePointsWithinDistance, quadSubdivision, radialSortFaces, radialWarp, random as randomPoints, rectSubdivision, reducePrecision, refine, relativeAreaError, relativeNeighborFaces, removeSmallHoles, repulsionPack, resizeByHeight, resizeByWidth, ringArea, ringsToPath, rotate, rotateAroundCenter, roundVertexCoords, scale, segment, segmentLength, segmentSet, segmentsOnExterior, segmentsToOpenPaths, serializePathData, shapeBoolean, shear, simplify, simplifyMesh, sineWarp, slice, sliceDivision, smooth, smoothMesh, spannerFaces, spiralQuadrangulation, splitEdges, splitQuadrangulation, squareGrid, squareLatticePack, squareGrid2 as squareTiling, stochasticMerge, stochasticPack, subdivideMesh, subtract, subtractAll, symDifference, tiling, toArray, toContours, toSvg, transformation, translate, translateCentroidTo, translateCornerTo, translateToOrigin, triangleSubdivision, triangulation, union, unionAll, urquhartFaces, vec2, voronoi, width };
|
|
3932
|
+
//# sourceMappingURL=index.js.map
|
|
3933
|
+
//# sourceMappingURL=index.js.map
|