@weasel-js/geom 1.9.3 → 1.10.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/dist/3d/index.js +1 -1
- package/dist/booleans/index.d.ts +51 -29
- package/dist/booleans/index.js +562 -482
- package/dist/booleans/index.js.map +1 -1
- package/dist/{chunk-7ROQRYOA.js → chunk-755Y4YZS.js} +6 -3
- package/dist/chunk-755Y4YZS.js.map +1 -0
- package/dist/{chunk-6T5I3PR5.js → chunk-MDT7F32A.js} +16 -5
- package/dist/chunk-MDT7F32A.js.map +1 -0
- package/dist/{chunk-HEZTRG2M.js → chunk-OXH3NOXC.js} +10 -3
- package/dist/chunk-OXH3NOXC.js.map +1 -0
- package/dist/{chunk-RQYCDPU5.js → chunk-WQMYOCFB.js} +27 -18
- package/dist/chunk-WQMYOCFB.js.map +1 -0
- package/dist/{chunk-HPVINMOM.js → chunk-XS6ERH3X.js} +2 -2
- package/dist/chunk-XS6ERH3X.js.map +1 -0
- package/dist/cubicMath-CFXbq6vp.d.ts +116 -0
- package/dist/curves/index.d.ts +1 -1
- package/dist/curves/index.js +18 -71
- package/dist/curves/index.js.map +1 -1
- package/dist/index.d.ts +612 -116
- package/dist/index.js +904 -112
- package/dist/index.js.map +1 -1
- package/dist/{path-D0CoudGM.d.ts → path-Bf9KeVxC.d.ts} +10 -11
- package/dist/tessellate/index.d.ts +8 -12
- package/dist/tessellate/index.js +36 -10
- package/dist/tessellate/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-6T5I3PR5.js.map +0 -1
- package/dist/chunk-7ROQRYOA.js.map +0 -1
- package/dist/chunk-HEZTRG2M.js.map +0 -1
- package/dist/chunk-HPVINMOM.js.map +0 -1
- package/dist/chunk-RQYCDPU5.js.map +0 -1
- package/dist/cubicMath-CpCvuEl8.d.ts +0 -37
package/dist/booleans/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { elevateQuadraticToCubic, splitQuadraticAt, splitCubicAt } from '../chunk-URQMDR6Q.js';
|
|
2
|
-
import { DEFAULT_FLATTEN_TOLERANCE, flattenCubic, segmentsCross, pointInPolygon } from '../chunk-
|
|
3
|
-
import '../chunk-
|
|
4
|
-
import { PATH_Z, PATH_Q, PATH_C, PATH_L, PATH_M } from '../chunk-
|
|
2
|
+
import { DEFAULT_FLATTEN_TOLERANCE, fillsOpenSubpaths, flattenCubic, segmentsCross, pointInPolygon } from '../chunk-WQMYOCFB.js';
|
|
3
|
+
import { EPS } from '../chunk-755Y4YZS.js';
|
|
4
|
+
import { PATH_Z, PATH_Q, PATH_C, PATH_L, PATH_M } from '../chunk-OXH3NOXC.js';
|
|
5
5
|
import polygonClipping from 'polygon-clipping';
|
|
6
6
|
|
|
7
7
|
// src/booleans/adapter.ts
|
|
@@ -21,9 +21,12 @@ function pathToMultiPolygon(path, opts = {}) {
|
|
|
21
21
|
const rings = [];
|
|
22
22
|
let current = null;
|
|
23
23
|
let cx = 0, cy = 0;
|
|
24
|
+
let startX = 0, startY = 0;
|
|
24
25
|
let ci = 0;
|
|
25
26
|
const { commands, coords } = path;
|
|
26
|
-
const
|
|
27
|
+
const fillsOpen = fillsOpenSubpaths(path);
|
|
28
|
+
const finalizeCurrent = (closed = fillsOpen) => {
|
|
29
|
+
if (!closed) current = null;
|
|
27
30
|
if (!current || current.length === 0) return;
|
|
28
31
|
const first = current[0];
|
|
29
32
|
const last = current[current.length - 1];
|
|
@@ -40,14 +43,17 @@ function pathToMultiPolygon(path, opts = {}) {
|
|
|
40
43
|
finalizeCurrent();
|
|
41
44
|
cx = coords[ci];
|
|
42
45
|
cy = coords[ci + 1];
|
|
46
|
+
startX = cx;
|
|
47
|
+
startY = cy;
|
|
43
48
|
current = [[cx, cy]];
|
|
44
49
|
ci += 2;
|
|
45
50
|
break;
|
|
46
51
|
}
|
|
47
52
|
case PATH_L: {
|
|
53
|
+
current ??= [[cx, cy]];
|
|
48
54
|
cx = coords[ci];
|
|
49
55
|
cy = coords[ci + 1];
|
|
50
|
-
|
|
56
|
+
current.push([cx, cy]);
|
|
51
57
|
ci += 2;
|
|
52
58
|
break;
|
|
53
59
|
}
|
|
@@ -57,7 +63,8 @@ function pathToMultiPolygon(path, opts = {}) {
|
|
|
57
63
|
const x3 = coords[ci + 4], y3 = coords[ci + 5];
|
|
58
64
|
const out = [];
|
|
59
65
|
flattenCubic(cx, cy, x1, y1, x2, y2, x3, y3, tolerance, out);
|
|
60
|
-
|
|
66
|
+
current ??= [[cx, cy]];
|
|
67
|
+
for (let k = 0; k < out.length; k += 2) current.push([out[k], out[k + 1]]);
|
|
61
68
|
cx = x3;
|
|
62
69
|
cy = y3;
|
|
63
70
|
ci += 6;
|
|
@@ -69,14 +76,17 @@ function pathToMultiPolygon(path, opts = {}) {
|
|
|
69
76
|
const [c1x, c1y, c2x, c2y] = elevateQuadraticToCubic(cx, cy, x1, y1, x2, y2);
|
|
70
77
|
const out = [];
|
|
71
78
|
flattenCubic(cx, cy, c1x, c1y, c2x, c2y, x2, y2, tolerance, out);
|
|
72
|
-
|
|
79
|
+
current ??= [[cx, cy]];
|
|
80
|
+
for (let k = 0; k < out.length; k += 2) current.push([out[k], out[k + 1]]);
|
|
73
81
|
cx = x2;
|
|
74
82
|
cy = y2;
|
|
75
83
|
ci += 4;
|
|
76
84
|
break;
|
|
77
85
|
}
|
|
78
86
|
case PATH_Z: {
|
|
79
|
-
finalizeCurrent();
|
|
87
|
+
finalizeCurrent(true);
|
|
88
|
+
cx = startX;
|
|
89
|
+
cy = startY;
|
|
80
90
|
break;
|
|
81
91
|
}
|
|
82
92
|
default:
|
|
@@ -227,9 +237,15 @@ function pathExclude(...paths) {
|
|
|
227
237
|
return multiPolygonToPath(polygonClipping.xor(head, ...rest));
|
|
228
238
|
}
|
|
229
239
|
function pathDivide(...paths) {
|
|
240
|
+
return pathDividePieces(...paths).map((p) => p.path);
|
|
241
|
+
}
|
|
242
|
+
function pathCrop(...paths) {
|
|
243
|
+
return pathCropPieces(...paths).map((p) => p.path);
|
|
244
|
+
}
|
|
245
|
+
function pathDividePieces(...paths) {
|
|
230
246
|
if (paths.length === 0) return [];
|
|
231
247
|
if (paths.length === 1) {
|
|
232
|
-
return [multiPolygonToPath(pathToMultiPolygon(paths[0]))];
|
|
248
|
+
return [{ path: multiPolygonToPath(pathToMultiPolygon(paths[0])), sources: [0] }];
|
|
233
249
|
}
|
|
234
250
|
const mps = paths.map((p) => pathToMultiPolygon(p));
|
|
235
251
|
const n = mps.length;
|
|
@@ -240,7 +256,9 @@ function pathDivide(...paths) {
|
|
|
240
256
|
for (const mask of subsets) {
|
|
241
257
|
const inside = [];
|
|
242
258
|
const outside = [];
|
|
259
|
+
const sources = [];
|
|
243
260
|
for (let i = 0; i < n; i++) {
|
|
261
|
+
if (mask & 1 << i) sources.push(i);
|
|
244
262
|
(mask & 1 << i ? inside : outside).push(mps[i]);
|
|
245
263
|
}
|
|
246
264
|
const [insideHead, ...insideRest] = inside;
|
|
@@ -252,10 +270,35 @@ function pathDivide(...paths) {
|
|
|
252
270
|
region = polygonClipping.difference(region, outsideUnion);
|
|
253
271
|
if (region.length === 0) continue;
|
|
254
272
|
}
|
|
255
|
-
out.push(multiPolygonToPath(region));
|
|
273
|
+
out.push({ path: multiPolygonToPath(region), sources });
|
|
274
|
+
}
|
|
275
|
+
return out;
|
|
276
|
+
}
|
|
277
|
+
function pathTrim(...paths) {
|
|
278
|
+
const mps = paths.map((p) => pathToMultiPolygon(p));
|
|
279
|
+
const out = [];
|
|
280
|
+
let cover = null;
|
|
281
|
+
for (let i = mps.length - 1; i >= 0; i--) {
|
|
282
|
+
const shown = cover ? polygonClipping.difference(mps[i], cover) : mps[i];
|
|
283
|
+
if (shown.length > 0) out.unshift({ path: multiPolygonToPath(shown), sources: [i] });
|
|
284
|
+
cover = cover ? polygonClipping.union(cover, mps[i]) : mps[i];
|
|
256
285
|
}
|
|
257
286
|
return out;
|
|
258
287
|
}
|
|
288
|
+
function pathMerge(paths, keyOf) {
|
|
289
|
+
const groups = /* @__PURE__ */ new Map();
|
|
290
|
+
for (const piece of pathTrim(...paths)) {
|
|
291
|
+
const key = keyOf(piece.sources[0]);
|
|
292
|
+
const group = groups.get(key);
|
|
293
|
+
if (group) group.push(piece);
|
|
294
|
+
else groups.set(key, [piece]);
|
|
295
|
+
}
|
|
296
|
+
const top = (p) => p.sources[p.sources.length - 1];
|
|
297
|
+
return [...groups.values()].map((g) => g.length === 1 ? g[0] : {
|
|
298
|
+
path: pathUnion(...g.map((p) => p.path)),
|
|
299
|
+
sources: g.map((p) => p.sources[0])
|
|
300
|
+
}).sort((a, b) => top(a) - top(b));
|
|
301
|
+
}
|
|
259
302
|
function popcount(n) {
|
|
260
303
|
let c = 0;
|
|
261
304
|
while (n) {
|
|
@@ -264,153 +307,359 @@ function popcount(n) {
|
|
|
264
307
|
}
|
|
265
308
|
return c;
|
|
266
309
|
}
|
|
267
|
-
function
|
|
310
|
+
function pathCropPieces(...paths) {
|
|
268
311
|
if (paths.length < 2) return [];
|
|
269
312
|
const mps = paths.map((p) => pathToMultiPolygon(p));
|
|
270
313
|
const mask = mps[mps.length - 1];
|
|
271
314
|
const out = [];
|
|
272
315
|
for (let i = 0; i < mps.length - 1; i++) {
|
|
273
316
|
const clipped = polygonClipping.intersection(mps[i], mask);
|
|
274
|
-
if (clipped.length > 0) out.push(multiPolygonToPath(clipped));
|
|
317
|
+
if (clipped.length > 0) out.push({ path: multiPolygonToPath(clipped), sources: [i] });
|
|
275
318
|
}
|
|
276
319
|
return out;
|
|
277
320
|
}
|
|
321
|
+
function trianglesToPath(vertices, indices) {
|
|
322
|
+
const triangles = [];
|
|
323
|
+
for (let i = 0; i + 2 < indices.length; i += 3) {
|
|
324
|
+
const a = indices[i] * 2, b = indices[i + 1] * 2, c = indices[i + 2] * 2;
|
|
325
|
+
const ax = vertices[a], ay = vertices[a + 1];
|
|
326
|
+
const bx = vertices[b], by = vertices[b + 1];
|
|
327
|
+
const cx = vertices[c], cy = vertices[c + 1];
|
|
328
|
+
if ((bx - ax) * (cy - ay) - (cx - ax) * (by - ay) === 0) continue;
|
|
329
|
+
triangles.push([[[[ax, ay], [bx, by], [cx, cy], [ax, ay]]]]);
|
|
330
|
+
}
|
|
331
|
+
if (triangles.length === 0) return multiPolygonToPath([]);
|
|
332
|
+
const [head, ...rest] = triangles;
|
|
333
|
+
return multiPolygonToPath(polygonClipping.union(head, ...rest));
|
|
334
|
+
}
|
|
278
335
|
|
|
279
|
-
// src/booleans/
|
|
280
|
-
function
|
|
281
|
-
|
|
336
|
+
// src/booleans/contourSegments.ts
|
|
337
|
+
function evalSeg(s, t) {
|
|
338
|
+
const u = 1 - t;
|
|
339
|
+
if (s.length === 2) return { x: u * s[0].x + t * s[1].x, y: u * s[0].y + t * s[1].y };
|
|
340
|
+
if (s.length === 3) {
|
|
341
|
+
const a2 = u * u, b2 = 2 * u * t, c2 = t * t;
|
|
342
|
+
return { x: a2 * s[0].x + b2 * s[1].x + c2 * s[2].x, y: a2 * s[0].y + b2 * s[1].y + c2 * s[2].y };
|
|
343
|
+
}
|
|
344
|
+
const a = u * u * u, b = 3 * u * u * t, c = 3 * u * t * t, d = t * t * t;
|
|
345
|
+
return {
|
|
346
|
+
x: a * s[0].x + b * s[1].x + c * s[2].x + d * s[3].x,
|
|
347
|
+
y: a * s[0].y + b * s[1].y + c * s[2].y + d * s[3].y
|
|
348
|
+
};
|
|
282
349
|
}
|
|
283
|
-
function
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
if (boundaries.length === 0) return null;
|
|
289
|
-
pts = openClosedCut(pts, boundaries);
|
|
290
|
-
const loops = interiorLoops(pts, boundaries);
|
|
291
|
-
const pieces = knife(path, pts, opts, pts.length);
|
|
292
|
-
if (loops.length === 0) return pieces;
|
|
293
|
-
const regions = pieces ? pieces.map((p) => orientBoundaries(parseContours(p), outRule)) : [boundaries];
|
|
294
|
-
if (!cutOutLoops(regions, loops)) return pieces;
|
|
295
|
-
return regions.map((r) => toPath(r, outRule));
|
|
350
|
+
function derivSeg(s, t) {
|
|
351
|
+
const n = s.length - 1;
|
|
352
|
+
const d = [];
|
|
353
|
+
for (let i = 0; i < n; i++) d.push({ x: n * (s[i + 1].x - s[i].x), y: n * (s[i + 1].y - s[i].y) });
|
|
354
|
+
return d.length === 1 ? d[0] : evalSeg(d, t);
|
|
296
355
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
return
|
|
356
|
+
function splitSeg(s, t) {
|
|
357
|
+
const pts = (c) => {
|
|
358
|
+
const out = [];
|
|
359
|
+
for (let i = 0; i < c.length; i += 2) out.push({ x: c[i], y: c[i + 1] });
|
|
360
|
+
return out;
|
|
361
|
+
};
|
|
362
|
+
if (s.length === 2) {
|
|
363
|
+
const m = evalSeg(s, t);
|
|
364
|
+
return [[s[0], m], [m, s[1]]];
|
|
302
365
|
}
|
|
303
|
-
|
|
366
|
+
if (s.length === 3) {
|
|
367
|
+
const [l2, r2] = splitQuadraticAt(s[0].x, s[0].y, s[1].x, s[1].y, s[2].x, s[2].y, t);
|
|
368
|
+
return [pts(l2), pts(r2)];
|
|
369
|
+
}
|
|
370
|
+
const [l, r] = splitCubicAt(s[0].x, s[0].y, s[1].x, s[1].y, s[2].x, s[2].y, s[3].x, s[3].y, t);
|
|
371
|
+
return [pts(l), pts(r)];
|
|
304
372
|
}
|
|
305
|
-
function
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
if (
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
373
|
+
function subSeg(s, t0, t1, p0, p1) {
|
|
374
|
+
if (t1 - t0 <= 0) return null;
|
|
375
|
+
let out = s;
|
|
376
|
+
if (t0 > 0) out = splitSeg(out, t0)[1];
|
|
377
|
+
const u = (t1 - t0) / (1 - t0);
|
|
378
|
+
if (u < 1) out = splitSeg(out, u)[0];
|
|
379
|
+
out = out.slice();
|
|
380
|
+
out[0] = p0;
|
|
381
|
+
out[out.length - 1] = p1;
|
|
382
|
+
return out;
|
|
383
|
+
}
|
|
384
|
+
var reverseContour = (c) => c.map((s) => s.slice().reverse()).reverse();
|
|
385
|
+
function sampleContour(c) {
|
|
386
|
+
const out = [];
|
|
387
|
+
for (const s of c) {
|
|
388
|
+
const n = s.length === 2 ? 1 : 16;
|
|
389
|
+
for (let i = 0; i < n; i++) {
|
|
390
|
+
const p = evalSeg(s, i / n);
|
|
391
|
+
out.push(p.x, p.y);
|
|
321
392
|
}
|
|
322
|
-
accepted.push(run);
|
|
323
|
-
partner.set(c0, c1);
|
|
324
|
-
partner.set(c1, c0);
|
|
325
|
-
chord.set(c0, run);
|
|
326
|
-
chord.set(c1, run.slice().reverse());
|
|
327
393
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const
|
|
335
|
-
|
|
394
|
+
return out;
|
|
395
|
+
}
|
|
396
|
+
function signedArea(c) {
|
|
397
|
+
const pts = sampleContour(c);
|
|
398
|
+
let a = 0;
|
|
399
|
+
for (let i = 0; i < pts.length; i += 2) {
|
|
400
|
+
const j = (i + 2) % pts.length;
|
|
401
|
+
a += pts[i] * pts[j + 1] - pts[j] * pts[i + 1];
|
|
336
402
|
}
|
|
337
|
-
|
|
338
|
-
if (!deferred || budget <= 1) return pieces;
|
|
339
|
-
return pieces.flatMap((p) => knife(p, cut, opts, budget - 1) ?? [p]);
|
|
403
|
+
return a / 2;
|
|
340
404
|
}
|
|
341
|
-
function
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
405
|
+
function sideCoeffs(s, o, d) {
|
|
406
|
+
return s.map((p) => d.x * (p.y - o.y) - d.y * (p.x - o.x));
|
|
407
|
+
}
|
|
408
|
+
function bernstein(f, t) {
|
|
409
|
+
const u = 1 - t;
|
|
410
|
+
if (f.length === 2) return u * f[0] + t * f[1];
|
|
411
|
+
if (f.length === 3) return u * u * f[0] + 2 * u * t * f[1] + t * t * f[2];
|
|
412
|
+
return u * u * u * f[0] + 3 * u * u * t * f[1] + 3 * u * t * t * f[2] + t * t * t * f[3];
|
|
413
|
+
}
|
|
414
|
+
function signChanges(f, zeroIsPositive) {
|
|
415
|
+
const sgn = (v) => v > 0 ? 1 : v < 0 ? -1 : zeroIsPositive ? 1 : 0;
|
|
416
|
+
const cuts = [0, ...criticalPoints(f), 1];
|
|
417
|
+
const out = [];
|
|
418
|
+
for (let i = 0; i + 1 < cuts.length; i++) {
|
|
419
|
+
let lo = cuts[i], hi = cuts[i + 1];
|
|
420
|
+
const slo = sgn(bernstein(f, lo)), shi = sgn(bernstein(f, hi));
|
|
421
|
+
if (slo === 0 && i > 0) {
|
|
422
|
+
const before = sgn(bernstein(f, lo - 1e-7)), after = sgn(bernstein(f, lo + 1e-7));
|
|
423
|
+
if (before * after < 0) out.push({ t: lo, before, after });
|
|
352
424
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
} else {
|
|
359
|
-
const last = segs[segs.length - 1];
|
|
360
|
-
const all = [
|
|
361
|
-
{ seg: 0, t: 0, pt: segs[0][0] },
|
|
362
|
-
...stops,
|
|
363
|
-
{ seg: segs.length - 1, t: 1, pt: last[last.length - 1] }
|
|
364
|
-
];
|
|
365
|
-
for (let i = 0; i + 1 < all.length; i++) runs.push(arcBetween(segs, all[i], all[i + 1], false));
|
|
425
|
+
if (slo * shi >= 0) continue;
|
|
426
|
+
for (let it = 0; it < 64; it++) {
|
|
427
|
+
const mid = (lo + hi) / 2;
|
|
428
|
+
if (sgn(bernstein(f, mid)) === slo) lo = mid;
|
|
429
|
+
else hi = mid;
|
|
366
430
|
}
|
|
431
|
+
out.push({ t: (lo + hi) / 2, before: slo, after: shi });
|
|
367
432
|
}
|
|
368
|
-
|
|
369
|
-
const pieces = runs.filter((r) => r.length > 0).map((r) => writePath([{ segs: r, closed: false }], rule));
|
|
370
|
-
if (missed.length > 0) pieces.push(writePath(missed, rule));
|
|
371
|
-
return pieces;
|
|
433
|
+
return out;
|
|
372
434
|
}
|
|
373
|
-
function
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
raw.sort((x, y) => x.seg - y.seg || x.t - y.t);
|
|
386
|
-
const same = (p, q) => Math.abs(p.x - q.x) <= 1e-7 && Math.abs(p.y - q.y) <= 1e-7;
|
|
387
|
-
const groups = [];
|
|
388
|
-
for (const st of raw) {
|
|
389
|
-
const g = groups[groups.length - 1];
|
|
390
|
-
if (g && same(g[0].pt, st.pt)) g.push(st);
|
|
391
|
-
else groups.push([st]);
|
|
435
|
+
function criticalPoints(f) {
|
|
436
|
+
if (f.length < 3) return [];
|
|
437
|
+
const n = f.length - 1;
|
|
438
|
+
const d = f.slice(1).map((v, i) => n * (v - f[i]));
|
|
439
|
+
let A = 0, B, C;
|
|
440
|
+
if (d.length === 2) {
|
|
441
|
+
B = d[1] - d[0];
|
|
442
|
+
C = d[0];
|
|
443
|
+
} else {
|
|
444
|
+
A = d[0] - 2 * d[1] + d[2];
|
|
445
|
+
B = 2 * (d[1] - d[0]);
|
|
446
|
+
C = d[0];
|
|
392
447
|
}
|
|
393
|
-
|
|
394
|
-
|
|
448
|
+
const out = [];
|
|
449
|
+
const scale = Math.max(Math.abs(A), Math.abs(B), Math.abs(C)) || 1;
|
|
450
|
+
if (Math.abs(A) / scale < 1e-12) {
|
|
451
|
+
if (Math.abs(B) / scale > 1e-12) out.push(-C / B);
|
|
452
|
+
} else {
|
|
453
|
+
const disc = B * B - 4 * A * C;
|
|
454
|
+
if (disc >= 0) {
|
|
455
|
+
const q = -0.5 * (B + Math.sign(B || 1) * Math.sqrt(disc));
|
|
456
|
+
out.push(q / A);
|
|
457
|
+
if (q !== 0) out.push(C / q);
|
|
458
|
+
}
|
|
395
459
|
}
|
|
396
|
-
|
|
397
|
-
const lastSeg = sub.segs[sub.segs.length - 1];
|
|
398
|
-
const end = lastSeg[lastSeg.length - 1];
|
|
399
|
-
return groups.filter((g) => g.length % 2 === 1).map((g) => g[0]).filter((st) => sub.closed || !same(st.pt, first) && !same(st.pt, end));
|
|
460
|
+
return out.filter((t) => t > 0 && t < 1).sort((x, y) => x - y);
|
|
400
461
|
}
|
|
401
|
-
function
|
|
402
|
-
const
|
|
403
|
-
for (
|
|
404
|
-
const
|
|
405
|
-
if (
|
|
462
|
+
function endSide(f, fromStart) {
|
|
463
|
+
const n = f.length;
|
|
464
|
+
for (let i = 1; i < n; i++) {
|
|
465
|
+
const v = fromStart ? f[i] : f[n - 1 - i];
|
|
466
|
+
if (v !== 0) return Math.sign(v);
|
|
406
467
|
}
|
|
407
|
-
return
|
|
468
|
+
return 0;
|
|
408
469
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
470
|
+
|
|
471
|
+
// src/booleans/cutCrossings.ts
|
|
472
|
+
function cutPosition(cut, k, pt) {
|
|
473
|
+
const a = cut[k], b = cut[k + 1];
|
|
474
|
+
const d = { x: b.x - a.x, y: b.y - a.y };
|
|
475
|
+
const s = ((pt.x - a.x) * d.x + (pt.y - a.y) * d.y) / (d.x * d.x + d.y * d.y);
|
|
476
|
+
const eps = 1e-9;
|
|
477
|
+
const last = k + 2 === cut.length;
|
|
478
|
+
if (s < (k === 0 ? 0 : -eps)) return null;
|
|
479
|
+
if (last ? s > 1 : s >= 1 - eps) return null;
|
|
480
|
+
return k + Math.min(1, Math.max(0, s));
|
|
481
|
+
}
|
|
482
|
+
function findCrossings(boundaries, cut) {
|
|
483
|
+
const out = [];
|
|
484
|
+
for (let k = 0; k + 1 < cut.length; k++) crossingsOnSegment(boundaries, cut, k, out);
|
|
485
|
+
return out.sort((x, y) => x.s - y.s);
|
|
486
|
+
}
|
|
487
|
+
function crossingsOnSegment(boundaries, cut, k, out) {
|
|
488
|
+
const a = cut[k];
|
|
489
|
+
const d = { x: cut[k + 1].x - a.x, y: cut[k + 1].y - a.y };
|
|
490
|
+
const push = (contour, seg, t, before, after) => {
|
|
491
|
+
const pt = evalSeg(boundaries[contour][seg], t);
|
|
492
|
+
const s = cutPosition(cut, k, pt);
|
|
493
|
+
if (s === null) return;
|
|
494
|
+
out.push({ contour, seg, t, pt, s, enters: before > 0 && after < 0 });
|
|
495
|
+
};
|
|
496
|
+
boundaries.forEach((contour, ci) => {
|
|
497
|
+
const fs = contour.map((s) => sideCoeffs(s, a, d));
|
|
498
|
+
const n = contour.length;
|
|
499
|
+
fs.forEach((f, si) => {
|
|
500
|
+
for (const c of signChanges(f, false)) push(ci, si, c.t, c.before, c.after);
|
|
501
|
+
if (f[0] !== 0) return;
|
|
502
|
+
const after = endSide(f, true);
|
|
503
|
+
if (after === 0) return;
|
|
504
|
+
let before = 0;
|
|
505
|
+
for (let k2 = 1; k2 <= n && before === 0; k2++) before = endSide(fs[(si - k2 + n) % n], false);
|
|
506
|
+
if (before !== 0 && before !== after) push(ci, si, 0, before, after);
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// src/booleans/contours.ts
|
|
512
|
+
function parseContours(path) {
|
|
513
|
+
const fillsOpen = path.kind === "rect" || fillsOpenSubpaths(path);
|
|
514
|
+
return parseSubpaths(path).filter((s) => s.closed || fillsOpen).map((s) => s.segs);
|
|
515
|
+
}
|
|
516
|
+
function parseSubpaths(path, implicitClose = true) {
|
|
517
|
+
if (path.kind === "rect") {
|
|
518
|
+
const { x, y, width: w, height: h } = path;
|
|
519
|
+
const p = [{ x, y }, { x: x + w, y }, { x: x + w, y: y + h }, { x, y: y + h }];
|
|
520
|
+
return [{ segs: p.map((q, i) => [q, p[(i + 1) % 4]]), closed: true }];
|
|
521
|
+
}
|
|
522
|
+
const out = [];
|
|
523
|
+
let cur = [];
|
|
524
|
+
let start = { x: 0, y: 0 };
|
|
525
|
+
let pen = start;
|
|
526
|
+
const finish = (closed) => {
|
|
527
|
+
if (cur.length > 0 && (closed || implicitClose) && (pen.x !== start.x || pen.y !== start.y)) {
|
|
528
|
+
cur.push([pen, start]);
|
|
529
|
+
}
|
|
530
|
+
if (cur.length > 0) out.push({ segs: cur, closed });
|
|
531
|
+
cur = [];
|
|
532
|
+
pen = start;
|
|
533
|
+
};
|
|
534
|
+
const c = path.coords;
|
|
535
|
+
let k = 0;
|
|
536
|
+
for (const cmd of path.commands) {
|
|
537
|
+
if (cmd === PATH_M) {
|
|
538
|
+
finish(false);
|
|
539
|
+
start = pen = { x: c[k], y: c[k + 1] };
|
|
540
|
+
k += 2;
|
|
541
|
+
} else if (cmd === PATH_L) {
|
|
542
|
+
const e = { x: c[k], y: c[k + 1] };
|
|
543
|
+
if (e.x !== pen.x || e.y !== pen.y) cur.push([pen, e]);
|
|
544
|
+
pen = e;
|
|
545
|
+
k += 2;
|
|
546
|
+
} else if (cmd === PATH_Q) {
|
|
547
|
+
const e = { x: c[k + 2], y: c[k + 3] };
|
|
548
|
+
cur.push([pen, { x: c[k], y: c[k + 1] }, e]);
|
|
549
|
+
pen = e;
|
|
550
|
+
k += 4;
|
|
551
|
+
} else if (cmd === PATH_C) {
|
|
552
|
+
const e = { x: c[k + 4], y: c[k + 5] };
|
|
553
|
+
cur.push([pen, { x: c[k], y: c[k + 1] }, { x: c[k + 2], y: c[k + 3] }, e]);
|
|
554
|
+
pen = e;
|
|
555
|
+
k += 6;
|
|
556
|
+
} else if (cmd === PATH_Z) {
|
|
557
|
+
finish(true);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
finish(false);
|
|
561
|
+
return out;
|
|
562
|
+
}
|
|
563
|
+
function contoursIntersect(contours, tol) {
|
|
564
|
+
const edges = [];
|
|
565
|
+
contours.forEach((contour, ci) => {
|
|
566
|
+
const pts = [];
|
|
567
|
+
for (const s of contour) {
|
|
568
|
+
let n = 1;
|
|
569
|
+
if (s.length > 2) {
|
|
570
|
+
let len = 0;
|
|
571
|
+
for (let i = 0; i + 1 < s.length; i++) len += Math.hypot(s[i + 1].x - s[i].x, s[i + 1].y - s[i].y);
|
|
572
|
+
n = Math.min(64, Math.max(2, Math.ceil(Math.sqrt(len / Math.max(tol, 1e-3)))));
|
|
573
|
+
}
|
|
574
|
+
for (let i = 0; i < n; i++) pts.push(evalSeg(s, i / n));
|
|
575
|
+
}
|
|
576
|
+
for (let i = 0; i < pts.length; i++) {
|
|
577
|
+
const p = pts[i], q = pts[(i + 1) % pts.length];
|
|
578
|
+
edges.push({ x0: p.x, y0: p.y, x1: q.x, y1: q.y, c: ci, i, n: pts.length });
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
for (let i = 0; i < edges.length; i++) {
|
|
582
|
+
const e = edges[i];
|
|
583
|
+
for (let j = i + 1; j < edges.length; j++) {
|
|
584
|
+
const f = edges[j];
|
|
585
|
+
if (e.c === f.c && (j - i === 1 || i === 0 && f.i === f.n - 1)) continue;
|
|
586
|
+
if (Math.max(e.x0, e.x1) < Math.min(f.x0, f.x1) || Math.max(f.x0, f.x1) < Math.min(e.x0, e.x1)) continue;
|
|
587
|
+
if (Math.max(e.y0, e.y1) < Math.min(f.y0, f.y1) || Math.max(f.y0, f.y1) < Math.min(e.y0, e.y1)) continue;
|
|
588
|
+
if (segmentsCross(e.x0, e.y0, e.x1, e.y1, f.x0, f.y0, f.x1, f.y1)) return true;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
function windingAt(q, contours) {
|
|
594
|
+
const dir = { x: 1, y: 0 };
|
|
595
|
+
let w = 0;
|
|
596
|
+
for (const contour of contours) {
|
|
597
|
+
for (const s of contour) {
|
|
598
|
+
for (const c of signChanges(sideCoeffs(s, q, dir), true)) {
|
|
599
|
+
if (evalSeg(s, c.t).x > q.x) w += c.after > 0 ? 1 : -1;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return w;
|
|
604
|
+
}
|
|
605
|
+
function extentOf(contours) {
|
|
606
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
607
|
+
for (const c of contours) for (const s of c) for (const p of s) {
|
|
608
|
+
minX = Math.min(minX, p.x);
|
|
609
|
+
maxX = Math.max(maxX, p.x);
|
|
610
|
+
minY = Math.min(minY, p.y);
|
|
611
|
+
maxY = Math.max(maxY, p.y);
|
|
612
|
+
}
|
|
613
|
+
return Math.hypot(maxX - minX, maxY - minY);
|
|
614
|
+
}
|
|
615
|
+
function negligibleArea(contours) {
|
|
616
|
+
const extent = extentOf(contours);
|
|
617
|
+
return extent * extent * EPS * EPS;
|
|
618
|
+
}
|
|
619
|
+
function orientBoundaries(contours, fillRule) {
|
|
620
|
+
const filled = (q) => {
|
|
621
|
+
const w = windingAt(q, contours);
|
|
622
|
+
return fillRule === "evenodd" ? (w & 1) !== 0 : w !== 0;
|
|
623
|
+
};
|
|
624
|
+
const eps = Math.max(extentOf(contours) * 1e-6, 1e-9);
|
|
625
|
+
const out = [];
|
|
626
|
+
for (const contour of contours) {
|
|
627
|
+
let probe = contour[0];
|
|
628
|
+
let best = -1;
|
|
629
|
+
for (const s of contour) {
|
|
630
|
+
const len = Math.hypot(s[s.length - 1].x - s[0].x, s[s.length - 1].y - s[0].y);
|
|
631
|
+
if (len > best) {
|
|
632
|
+
best = len;
|
|
633
|
+
probe = s;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
const p = evalSeg(probe, 0.5);
|
|
637
|
+
let tan = derivSeg(probe, 0.5);
|
|
638
|
+
const tl = Math.hypot(tan.x, tan.y);
|
|
639
|
+
if (tl === 0) continue;
|
|
640
|
+
tan = { x: tan.x / tl, y: tan.y / tl };
|
|
641
|
+
const left = filled({ x: p.x - tan.y * eps, y: p.y + tan.x * eps });
|
|
642
|
+
const right = filled({ x: p.x + tan.y * eps, y: p.y - tan.x * eps });
|
|
643
|
+
if (left && !right) out.push(contour);
|
|
644
|
+
else if (right && !left) out.push(reverseContour(contour));
|
|
645
|
+
}
|
|
646
|
+
return out;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// src/booleans/cutPolyline.ts
|
|
650
|
+
function distinctPoints(cut) {
|
|
651
|
+
const out = [];
|
|
652
|
+
for (const p of cut) {
|
|
653
|
+
const q = out[out.length - 1];
|
|
654
|
+
if (!q || q.x !== p.x || q.y !== p.y) out.push({ x: p.x, y: p.y });
|
|
655
|
+
}
|
|
656
|
+
return out;
|
|
657
|
+
}
|
|
658
|
+
function cutBetween(cut, from, to) {
|
|
659
|
+
const out = [from.pt];
|
|
660
|
+
for (let v = Math.floor(from.s) + 1; v < to.s; v++) out.push(cut[v]);
|
|
661
|
+
out.push(to.pt);
|
|
662
|
+
return out;
|
|
414
663
|
}
|
|
415
664
|
function properCrossing(p0, p1, q0, q1) {
|
|
416
665
|
const r = { x: p1.x - p0.x, y: p1.y - p0.y };
|
|
@@ -502,10 +751,11 @@ function interiorLoops(cut, boundaries) {
|
|
|
502
751
|
}
|
|
503
752
|
function cutOutLoops(regions, loops) {
|
|
504
753
|
let cut = false;
|
|
754
|
+
const minArea = negligibleArea(regions.flat());
|
|
505
755
|
for (const loop of loops) {
|
|
506
756
|
let ring = loop.map((p, i) => [p, loop[(i + 1) % loop.length]]);
|
|
507
757
|
const a = signedArea(ring);
|
|
508
|
-
if (Math.abs(a) <=
|
|
758
|
+
if (Math.abs(a) <= minArea) continue;
|
|
509
759
|
if (a < 0) ring = reverseContour(ring);
|
|
510
760
|
const probe = evalSeg(ring[0], 0.5);
|
|
511
761
|
const at = regions.findIndex((r) => windingAt(probe, r) !== 0);
|
|
@@ -530,361 +780,66 @@ function polylinesCross(a, b) {
|
|
|
530
780
|
}
|
|
531
781
|
return false;
|
|
532
782
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
783
|
+
|
|
784
|
+
// src/booleans/cutLoops.ts
|
|
785
|
+
function traceLoops(boundaries, partner, chord) {
|
|
786
|
+
const byContour = /* @__PURE__ */ new Map();
|
|
787
|
+
for (const c of partner.keys()) {
|
|
788
|
+
const list = byContour.get(c.contour) ?? [];
|
|
789
|
+
list.push(c);
|
|
790
|
+
byContour.set(c.contour, list);
|
|
539
791
|
}
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
792
|
+
const arcFrom = /* @__PURE__ */ new Map();
|
|
793
|
+
const untouched = [];
|
|
794
|
+
boundaries.forEach((contour, ci) => {
|
|
795
|
+
const cuts = byContour.get(ci);
|
|
796
|
+
if (!cuts) {
|
|
797
|
+
untouched.push(contour);
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
cuts.sort((x, y) => x.seg - y.seg || x.t - y.t);
|
|
801
|
+
for (let k = 0; k < cuts.length; k++) {
|
|
802
|
+
const from = cuts[k], to = cuts[(k + 1) % cuts.length];
|
|
803
|
+
arcFrom.set(from, { segs: arcBetween(contour, from, to, cuts.length === 1), end: to });
|
|
804
|
+
}
|
|
805
|
+
});
|
|
806
|
+
const loops = [];
|
|
807
|
+
const seen = /* @__PURE__ */ new Set();
|
|
808
|
+
for (const start of arcFrom.keys()) {
|
|
809
|
+
if (seen.has(start)) continue;
|
|
810
|
+
const loop = [];
|
|
811
|
+
let at = start;
|
|
812
|
+
while (!seen.has(at)) {
|
|
813
|
+
seen.add(at);
|
|
814
|
+
const arc = arcFrom.get(at);
|
|
815
|
+
loop.push(...arc.segs);
|
|
816
|
+
const run = chord.get(arc.end);
|
|
817
|
+
for (let i = 0; i + 1 < run.length; i++) loop.push([run[i], run[i + 1]]);
|
|
818
|
+
at = partner.get(arc.end);
|
|
819
|
+
}
|
|
820
|
+
loops.push(loop);
|
|
821
|
+
}
|
|
822
|
+
return { loops, untouched };
|
|
551
823
|
}
|
|
552
|
-
function
|
|
553
|
-
const
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
return out;
|
|
824
|
+
function arcBetween(contour, from, to, whole) {
|
|
825
|
+
const out = [];
|
|
826
|
+
const add = (s) => {
|
|
827
|
+
if (s) out.push(s);
|
|
557
828
|
};
|
|
558
|
-
if (
|
|
559
|
-
|
|
560
|
-
return
|
|
561
|
-
}
|
|
562
|
-
if (s.length === 3) {
|
|
563
|
-
const [l2, r2] = splitQuadraticAt(s[0].x, s[0].y, s[1].x, s[1].y, s[2].x, s[2].y, t);
|
|
564
|
-
return [pts(l2), pts(r2)];
|
|
829
|
+
if (!whole && to.seg === from.seg && to.t > from.t) {
|
|
830
|
+
add(subSeg(contour[from.seg], from.t, to.t, from.pt, to.pt));
|
|
831
|
+
return out;
|
|
565
832
|
}
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
const u = (t1 - t0) / (1 - t0);
|
|
574
|
-
if (u < 1) out = splitSeg(out, u)[0];
|
|
575
|
-
out = out.slice();
|
|
576
|
-
out[0] = p0;
|
|
577
|
-
out[out.length - 1] = p1;
|
|
578
|
-
return out;
|
|
579
|
-
}
|
|
580
|
-
var reverseContour = (c) => c.map((s) => s.slice().reverse()).reverse();
|
|
581
|
-
function sampleContour(c) {
|
|
582
|
-
const out = [];
|
|
583
|
-
for (const s of c) {
|
|
584
|
-
const n = s.length === 2 ? 1 : 16;
|
|
585
|
-
for (let i = 0; i < n; i++) {
|
|
586
|
-
const p = evalSeg(s, i / n);
|
|
587
|
-
out.push(p.x, p.y);
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
return out;
|
|
591
|
-
}
|
|
592
|
-
function signedArea(c) {
|
|
593
|
-
const pts = sampleContour(c);
|
|
594
|
-
let a = 0;
|
|
595
|
-
for (let i = 0; i < pts.length; i += 2) {
|
|
596
|
-
const j = (i + 2) % pts.length;
|
|
597
|
-
a += pts[i] * pts[j + 1] - pts[j] * pts[i + 1];
|
|
598
|
-
}
|
|
599
|
-
return a / 2;
|
|
600
|
-
}
|
|
601
|
-
function sideCoeffs(s, o, d) {
|
|
602
|
-
return s.map((p) => d.x * (p.y - o.y) - d.y * (p.x - o.x));
|
|
603
|
-
}
|
|
604
|
-
function bernstein(f, t) {
|
|
605
|
-
const u = 1 - t;
|
|
606
|
-
if (f.length === 2) return u * f[0] + t * f[1];
|
|
607
|
-
if (f.length === 3) return u * u * f[0] + 2 * u * t * f[1] + t * t * f[2];
|
|
608
|
-
return u * u * u * f[0] + 3 * u * u * t * f[1] + 3 * u * t * t * f[2] + t * t * t * f[3];
|
|
609
|
-
}
|
|
610
|
-
function signChanges(f, zeroIsPositive) {
|
|
611
|
-
const sgn = (v) => v > 0 ? 1 : v < 0 ? -1 : zeroIsPositive ? 1 : 0;
|
|
612
|
-
const cuts = [0, ...criticalPoints(f), 1];
|
|
613
|
-
const out = [];
|
|
614
|
-
for (let i = 0; i + 1 < cuts.length; i++) {
|
|
615
|
-
let lo = cuts[i], hi = cuts[i + 1];
|
|
616
|
-
const slo = sgn(bernstein(f, lo)), shi = sgn(bernstein(f, hi));
|
|
617
|
-
if (slo === 0 && i > 0) {
|
|
618
|
-
const before = sgn(bernstein(f, lo - 1e-7)), after = sgn(bernstein(f, lo + 1e-7));
|
|
619
|
-
if (before * after < 0) out.push({ t: lo, before, after });
|
|
620
|
-
}
|
|
621
|
-
if (slo * shi >= 0) continue;
|
|
622
|
-
for (let it = 0; it < 64; it++) {
|
|
623
|
-
const mid = (lo + hi) / 2;
|
|
624
|
-
if (sgn(bernstein(f, mid)) === slo) lo = mid;
|
|
625
|
-
else hi = mid;
|
|
626
|
-
}
|
|
627
|
-
out.push({ t: (lo + hi) / 2, before: slo, after: shi });
|
|
628
|
-
}
|
|
629
|
-
return out;
|
|
630
|
-
}
|
|
631
|
-
function criticalPoints(f) {
|
|
632
|
-
if (f.length < 3) return [];
|
|
633
|
-
const n = f.length - 1;
|
|
634
|
-
const d = f.slice(1).map((v, i) => n * (v - f[i]));
|
|
635
|
-
let A = 0, B, C;
|
|
636
|
-
if (d.length === 2) {
|
|
637
|
-
B = d[1] - d[0];
|
|
638
|
-
C = d[0];
|
|
639
|
-
} else {
|
|
640
|
-
A = d[0] - 2 * d[1] + d[2];
|
|
641
|
-
B = 2 * (d[1] - d[0]);
|
|
642
|
-
C = d[0];
|
|
643
|
-
}
|
|
644
|
-
const out = [];
|
|
645
|
-
const scale = Math.max(Math.abs(A), Math.abs(B), Math.abs(C)) || 1;
|
|
646
|
-
if (Math.abs(A) / scale < 1e-12) {
|
|
647
|
-
if (Math.abs(B) / scale > 1e-12) out.push(-C / B);
|
|
648
|
-
} else {
|
|
649
|
-
const disc = B * B - 4 * A * C;
|
|
650
|
-
if (disc >= 0) {
|
|
651
|
-
const q = -0.5 * (B + Math.sign(B || 1) * Math.sqrt(disc));
|
|
652
|
-
out.push(q / A);
|
|
653
|
-
if (q !== 0) out.push(C / q);
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
return out.filter((t) => t > 0 && t < 1).sort((x, y) => x - y);
|
|
657
|
-
}
|
|
658
|
-
function endSide(f, fromStart) {
|
|
659
|
-
const n = f.length;
|
|
660
|
-
for (let i = 1; i < n; i++) {
|
|
661
|
-
const v = fromStart ? f[i] : f[n - 1 - i];
|
|
662
|
-
if (v !== 0) return Math.sign(v);
|
|
663
|
-
}
|
|
664
|
-
return 0;
|
|
665
|
-
}
|
|
666
|
-
function parseContours(path) {
|
|
667
|
-
return parseSubpaths(path).map((s) => s.segs);
|
|
668
|
-
}
|
|
669
|
-
function parseSubpaths(path, implicitClose = true) {
|
|
670
|
-
if (path.kind === "rect") {
|
|
671
|
-
const { x, y, width: w, height: h } = path;
|
|
672
|
-
const p = [{ x, y }, { x: x + w, y }, { x: x + w, y: y + h }, { x, y: y + h }];
|
|
673
|
-
return [{ segs: p.map((q, i) => [q, p[(i + 1) % 4]]), closed: true }];
|
|
674
|
-
}
|
|
675
|
-
const out = [];
|
|
676
|
-
let cur = [];
|
|
677
|
-
let start = { x: 0, y: 0 };
|
|
678
|
-
let pen = start;
|
|
679
|
-
const finish = (closed) => {
|
|
680
|
-
if (cur.length > 0 && (closed || implicitClose) && (pen.x !== start.x || pen.y !== start.y)) {
|
|
681
|
-
cur.push([pen, start]);
|
|
682
|
-
}
|
|
683
|
-
if (cur.length > 0) out.push({ segs: cur, closed });
|
|
684
|
-
cur = [];
|
|
685
|
-
pen = start;
|
|
686
|
-
};
|
|
687
|
-
const c = path.coords;
|
|
688
|
-
let k = 0;
|
|
689
|
-
for (const cmd of path.commands) {
|
|
690
|
-
if (cmd === PATH_M) {
|
|
691
|
-
finish(false);
|
|
692
|
-
start = pen = { x: c[k], y: c[k + 1] };
|
|
693
|
-
k += 2;
|
|
694
|
-
} else if (cmd === PATH_L) {
|
|
695
|
-
const e = { x: c[k], y: c[k + 1] };
|
|
696
|
-
if (e.x !== pen.x || e.y !== pen.y) cur.push([pen, e]);
|
|
697
|
-
pen = e;
|
|
698
|
-
k += 2;
|
|
699
|
-
} else if (cmd === PATH_Q) {
|
|
700
|
-
const e = { x: c[k + 2], y: c[k + 3] };
|
|
701
|
-
cur.push([pen, { x: c[k], y: c[k + 1] }, e]);
|
|
702
|
-
pen = e;
|
|
703
|
-
k += 4;
|
|
704
|
-
} else if (cmd === PATH_C) {
|
|
705
|
-
const e = { x: c[k + 4], y: c[k + 5] };
|
|
706
|
-
cur.push([pen, { x: c[k], y: c[k + 1] }, { x: c[k + 2], y: c[k + 3] }, e]);
|
|
707
|
-
pen = e;
|
|
708
|
-
k += 6;
|
|
709
|
-
} else if (cmd === PATH_Z) {
|
|
710
|
-
finish(true);
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
finish(false);
|
|
714
|
-
return out;
|
|
715
|
-
}
|
|
716
|
-
function contoursIntersect(contours, tol) {
|
|
717
|
-
const edges = [];
|
|
718
|
-
contours.forEach((contour, ci) => {
|
|
719
|
-
const pts = [];
|
|
720
|
-
for (const s of contour) {
|
|
721
|
-
let n = 1;
|
|
722
|
-
if (s.length > 2) {
|
|
723
|
-
let len = 0;
|
|
724
|
-
for (let i = 0; i + 1 < s.length; i++) len += Math.hypot(s[i + 1].x - s[i].x, s[i + 1].y - s[i].y);
|
|
725
|
-
n = Math.min(64, Math.max(2, Math.ceil(Math.sqrt(len / Math.max(tol, 1e-3)))));
|
|
726
|
-
}
|
|
727
|
-
for (let i = 0; i < n; i++) pts.push(evalSeg(s, i / n));
|
|
728
|
-
}
|
|
729
|
-
for (let i = 0; i < pts.length; i++) {
|
|
730
|
-
const p = pts[i], q = pts[(i + 1) % pts.length];
|
|
731
|
-
edges.push({ x0: p.x, y0: p.y, x1: q.x, y1: q.y, c: ci, i, n: pts.length });
|
|
732
|
-
}
|
|
733
|
-
});
|
|
734
|
-
for (let i = 0; i < edges.length; i++) {
|
|
735
|
-
const e = edges[i];
|
|
736
|
-
for (let j = i + 1; j < edges.length; j++) {
|
|
737
|
-
const f = edges[j];
|
|
738
|
-
if (e.c === f.c && (j - i === 1 || i === 0 && f.i === f.n - 1)) continue;
|
|
739
|
-
if (Math.max(e.x0, e.x1) < Math.min(f.x0, f.x1) || Math.max(f.x0, f.x1) < Math.min(e.x0, e.x1)) continue;
|
|
740
|
-
if (Math.max(e.y0, e.y1) < Math.min(f.y0, f.y1) || Math.max(f.y0, f.y1) < Math.min(e.y0, e.y1)) continue;
|
|
741
|
-
if (segmentsCross(e.x0, e.y0, e.x1, e.y1, f.x0, f.y0, f.x1, f.y1)) return true;
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
return false;
|
|
745
|
-
}
|
|
746
|
-
function windingAt(q, contours) {
|
|
747
|
-
const dir = { x: 1, y: 0 };
|
|
748
|
-
let w = 0;
|
|
749
|
-
for (const contour of contours) {
|
|
750
|
-
for (const s of contour) {
|
|
751
|
-
for (const c of signChanges(sideCoeffs(s, q, dir), true)) {
|
|
752
|
-
if (evalSeg(s, c.t).x > q.x) w += c.after > 0 ? 1 : -1;
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
return w;
|
|
757
|
-
}
|
|
758
|
-
function orientBoundaries(contours, fillRule) {
|
|
759
|
-
const filled = (q) => {
|
|
760
|
-
const w = windingAt(q, contours);
|
|
761
|
-
return fillRule === "evenodd" ? (w & 1) !== 0 : w !== 0;
|
|
762
|
-
};
|
|
763
|
-
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
764
|
-
for (const c of contours) for (const s of c) for (const p of s) {
|
|
765
|
-
minX = Math.min(minX, p.x);
|
|
766
|
-
maxX = Math.max(maxX, p.x);
|
|
767
|
-
minY = Math.min(minY, p.y);
|
|
768
|
-
maxY = Math.max(maxY, p.y);
|
|
769
|
-
}
|
|
770
|
-
const eps = Math.max(Math.hypot(maxX - minX, maxY - minY) * 1e-6, 1e-9);
|
|
771
|
-
const out = [];
|
|
772
|
-
for (const contour of contours) {
|
|
773
|
-
let probe = contour[0];
|
|
774
|
-
let best = -1;
|
|
775
|
-
for (const s of contour) {
|
|
776
|
-
const len = Math.hypot(s[s.length - 1].x - s[0].x, s[s.length - 1].y - s[0].y);
|
|
777
|
-
if (len > best) {
|
|
778
|
-
best = len;
|
|
779
|
-
probe = s;
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
const p = evalSeg(probe, 0.5);
|
|
783
|
-
let tan = derivSeg(probe, 0.5);
|
|
784
|
-
const tl = Math.hypot(tan.x, tan.y);
|
|
785
|
-
if (tl === 0) continue;
|
|
786
|
-
tan = { x: tan.x / tl, y: tan.y / tl };
|
|
787
|
-
const left = filled({ x: p.x - tan.y * eps, y: p.y + tan.x * eps });
|
|
788
|
-
const right = filled({ x: p.x + tan.y * eps, y: p.y - tan.x * eps });
|
|
789
|
-
if (left && !right) out.push(contour);
|
|
790
|
-
else if (right && !left) out.push(reverseContour(contour));
|
|
791
|
-
}
|
|
792
|
-
return out;
|
|
793
|
-
}
|
|
794
|
-
function cutPosition(cut, k, pt) {
|
|
795
|
-
const a = cut[k], b = cut[k + 1];
|
|
796
|
-
const d = { x: b.x - a.x, y: b.y - a.y };
|
|
797
|
-
const s = ((pt.x - a.x) * d.x + (pt.y - a.y) * d.y) / (d.x * d.x + d.y * d.y);
|
|
798
|
-
const eps = 1e-9;
|
|
799
|
-
const last = k + 2 === cut.length;
|
|
800
|
-
if (s < (k === 0 ? 0 : -eps)) return null;
|
|
801
|
-
if (last ? s > 1 : s >= 1 - eps) return null;
|
|
802
|
-
return k + Math.min(1, Math.max(0, s));
|
|
803
|
-
}
|
|
804
|
-
function findCrossings(boundaries, cut) {
|
|
805
|
-
const out = [];
|
|
806
|
-
for (let k = 0; k + 1 < cut.length; k++) crossingsOnSegment(boundaries, cut, k, out);
|
|
807
|
-
return out.sort((x, y) => x.s - y.s);
|
|
808
|
-
}
|
|
809
|
-
function crossingsOnSegment(boundaries, cut, k, out) {
|
|
810
|
-
const a = cut[k];
|
|
811
|
-
const d = { x: cut[k + 1].x - a.x, y: cut[k + 1].y - a.y };
|
|
812
|
-
const push = (contour, seg, t, before, after) => {
|
|
813
|
-
const pt = evalSeg(boundaries[contour][seg], t);
|
|
814
|
-
const s = cutPosition(cut, k, pt);
|
|
815
|
-
if (s === null) return;
|
|
816
|
-
out.push({ contour, seg, t, pt, s, enters: before > 0 && after < 0 });
|
|
817
|
-
};
|
|
818
|
-
boundaries.forEach((contour, ci) => {
|
|
819
|
-
const fs = contour.map((s) => sideCoeffs(s, a, d));
|
|
820
|
-
const n = contour.length;
|
|
821
|
-
fs.forEach((f, si) => {
|
|
822
|
-
for (const c of signChanges(f, false)) push(ci, si, c.t, c.before, c.after);
|
|
823
|
-
if (f[0] !== 0) return;
|
|
824
|
-
const after = endSide(f, true);
|
|
825
|
-
if (after === 0) return;
|
|
826
|
-
let before = 0;
|
|
827
|
-
for (let k2 = 1; k2 <= n && before === 0; k2++) before = endSide(fs[(si - k2 + n) % n], false);
|
|
828
|
-
if (before !== 0 && before !== after) push(ci, si, 0, before, after);
|
|
829
|
-
});
|
|
830
|
-
});
|
|
831
|
-
}
|
|
832
|
-
function traceLoops(boundaries, partner, chord) {
|
|
833
|
-
const byContour = /* @__PURE__ */ new Map();
|
|
834
|
-
for (const c of partner.keys()) {
|
|
835
|
-
const list = byContour.get(c.contour) ?? [];
|
|
836
|
-
list.push(c);
|
|
837
|
-
byContour.set(c.contour, list);
|
|
838
|
-
}
|
|
839
|
-
const arcFrom = /* @__PURE__ */ new Map();
|
|
840
|
-
const untouched = [];
|
|
841
|
-
boundaries.forEach((contour, ci) => {
|
|
842
|
-
const cuts = byContour.get(ci);
|
|
843
|
-
if (!cuts) {
|
|
844
|
-
untouched.push(contour);
|
|
845
|
-
return;
|
|
846
|
-
}
|
|
847
|
-
cuts.sort((x, y) => x.seg - y.seg || x.t - y.t);
|
|
848
|
-
for (let k = 0; k < cuts.length; k++) {
|
|
849
|
-
const from = cuts[k], to = cuts[(k + 1) % cuts.length];
|
|
850
|
-
arcFrom.set(from, { segs: arcBetween(contour, from, to, cuts.length === 1), end: to });
|
|
851
|
-
}
|
|
852
|
-
});
|
|
853
|
-
const loops = [];
|
|
854
|
-
const seen = /* @__PURE__ */ new Set();
|
|
855
|
-
for (const start of arcFrom.keys()) {
|
|
856
|
-
if (seen.has(start)) continue;
|
|
857
|
-
const loop = [];
|
|
858
|
-
let at = start;
|
|
859
|
-
while (!seen.has(at)) {
|
|
860
|
-
seen.add(at);
|
|
861
|
-
const arc = arcFrom.get(at);
|
|
862
|
-
loop.push(...arc.segs);
|
|
863
|
-
const run = chord.get(arc.end);
|
|
864
|
-
for (let i = 0; i + 1 < run.length; i++) loop.push([run[i], run[i + 1]]);
|
|
865
|
-
at = partner.get(arc.end);
|
|
866
|
-
}
|
|
867
|
-
loops.push(loop);
|
|
868
|
-
}
|
|
869
|
-
return { loops, untouched };
|
|
870
|
-
}
|
|
871
|
-
function arcBetween(contour, from, to, whole) {
|
|
872
|
-
const out = [];
|
|
873
|
-
const add = (s) => {
|
|
874
|
-
if (s) out.push(s);
|
|
875
|
-
};
|
|
876
|
-
if (!whole && to.seg === from.seg && to.t > from.t) {
|
|
877
|
-
add(subSeg(contour[from.seg], from.t, to.t, from.pt, to.pt));
|
|
878
|
-
return out;
|
|
879
|
-
}
|
|
880
|
-
const n = contour.length;
|
|
881
|
-
const first = contour[from.seg];
|
|
882
|
-
add(subSeg(first, from.t, 1, from.pt, first[first.length - 1]));
|
|
883
|
-
for (let i = (from.seg + 1) % n; i !== to.seg; i = (i + 1) % n) out.push(contour[i]);
|
|
884
|
-
const last = contour[to.seg];
|
|
885
|
-
add(subSeg(last, 0, to.t, last[0], to.pt));
|
|
886
|
-
return out;
|
|
833
|
+
const n = contour.length;
|
|
834
|
+
const first = contour[from.seg];
|
|
835
|
+
add(subSeg(first, from.t, 1, from.pt, first[first.length - 1]));
|
|
836
|
+
for (let i = (from.seg + 1) % n; i !== to.seg; i = (i + 1) % n) out.push(contour[i]);
|
|
837
|
+
const last = contour[to.seg];
|
|
838
|
+
add(subSeg(last, 0, to.t, last[0], to.pt));
|
|
839
|
+
return out;
|
|
887
840
|
}
|
|
841
|
+
|
|
842
|
+
// src/booleans/cutRegions.ts
|
|
888
843
|
function assembleRegions(outers, holes) {
|
|
889
844
|
const regions = outers.map((o) => ({ loops: [o], ring: sampleContour(o), area: signedArea(o) }));
|
|
890
845
|
for (const h of holes) {
|
|
@@ -914,6 +869,131 @@ function writePath(subpaths, fillRule) {
|
|
|
914
869
|
return { kind: "polygon", commands: new Uint8Array(cmds), coords: new Float32Array(xs), fillRule };
|
|
915
870
|
}
|
|
916
871
|
|
|
917
|
-
|
|
872
|
+
// src/booleans/splitBySegment.ts
|
|
873
|
+
function splitPathBySegment(path, a, b, opts = {}) {
|
|
874
|
+
return splitPathByPolyline(path, [a, b], opts);
|
|
875
|
+
}
|
|
876
|
+
function splitPathByPolyline(path, cut, opts = {}) {
|
|
877
|
+
let pts = distinctPoints(cut);
|
|
878
|
+
if (pts.length < 2) return null;
|
|
879
|
+
const outRule = fillRuleOf(path);
|
|
880
|
+
const boundaries = resolveBoundaries(path, opts);
|
|
881
|
+
if (boundaries.length === 0) return null;
|
|
882
|
+
pts = openClosedCut(pts, boundaries);
|
|
883
|
+
const loops = interiorLoops(pts, boundaries);
|
|
884
|
+
const pieces = knife(path, pts, opts, pts.length);
|
|
885
|
+
if (loops.length === 0) return pieces;
|
|
886
|
+
const regions = pieces ? pieces.map((p) => orientBoundaries(parseContours(p), outRule)) : [boundaries];
|
|
887
|
+
if (!cutOutLoops(regions, loops)) return pieces;
|
|
888
|
+
return regions.map((r) => toPath(r, outRule));
|
|
889
|
+
}
|
|
890
|
+
var fillRuleOf = (path) => path.kind === "polygon" ? path.fillRule : "nonzero";
|
|
891
|
+
function resolveBoundaries(path, opts) {
|
|
892
|
+
const contours = parseContours(path);
|
|
893
|
+
if (contoursIntersect(contours, opts.flattenTolerance ?? 0.5)) {
|
|
894
|
+
return orientBoundaries(parseContours(pathUnion(path)), "nonzero");
|
|
895
|
+
}
|
|
896
|
+
return orientBoundaries(contours, fillRuleOf(path));
|
|
897
|
+
}
|
|
898
|
+
function knife(path, cut, opts, budget) {
|
|
899
|
+
const outRule = fillRuleOf(path);
|
|
900
|
+
const boundaries = resolveBoundaries(path, opts);
|
|
901
|
+
if (boundaries.length === 0) return null;
|
|
902
|
+
const crossings = findCrossings(boundaries, cut);
|
|
903
|
+
const partner = /* @__PURE__ */ new Map();
|
|
904
|
+
const chord = /* @__PURE__ */ new Map();
|
|
905
|
+
const accepted = [];
|
|
906
|
+
let deferred = false;
|
|
907
|
+
for (let k = 0; k + 1 < crossings.length; k++) {
|
|
908
|
+
const c0 = crossings[k], c1 = crossings[k + 1];
|
|
909
|
+
if (!c0.enters || c1.enters || c1.s - c0.s <= 1e-9) continue;
|
|
910
|
+
const run = splitLoops(cutBetween(cut, c0, c1)).spine;
|
|
911
|
+
if (accepted.some((other) => polylinesCross(run, other))) {
|
|
912
|
+
deferred = true;
|
|
913
|
+
continue;
|
|
914
|
+
}
|
|
915
|
+
accepted.push(run);
|
|
916
|
+
partner.set(c0, c1);
|
|
917
|
+
partner.set(c1, c0);
|
|
918
|
+
chord.set(c0, run);
|
|
919
|
+
chord.set(c1, run.slice().reverse());
|
|
920
|
+
}
|
|
921
|
+
if (partner.size === 0) return null;
|
|
922
|
+
const { loops, untouched } = traceLoops(boundaries, partner, chord);
|
|
923
|
+
const traced = assembleRegions(loops, untouched.filter((c) => signedArea(c) < 0));
|
|
924
|
+
const islands = untouched.filter((c) => signedArea(c) > 0);
|
|
925
|
+
const minArea = negligibleArea(boundaries);
|
|
926
|
+
const regions = traced.filter((r) => Math.abs(regionArea(r)) > minArea);
|
|
927
|
+
if (islands.length > 0) {
|
|
928
|
+
const leftover = untouched.filter((c) => !traced.some((r) => r.includes(c)));
|
|
929
|
+
regions.push(leftover);
|
|
930
|
+
}
|
|
931
|
+
const pieces = regions.map((r) => toPath(r, outRule));
|
|
932
|
+
if (!deferred || budget <= 1) return pieces;
|
|
933
|
+
return pieces.flatMap((p) => knife(p, cut, opts, budget - 1) ?? [p]);
|
|
934
|
+
}
|
|
935
|
+
function snipPathByPolyline(path, cut) {
|
|
936
|
+
const pts = distinctPoints(cut);
|
|
937
|
+
if (pts.length < 2) return null;
|
|
938
|
+
const rule = path.kind === "polygon" ? path.fillRule : "nonzero";
|
|
939
|
+
const runs = [];
|
|
940
|
+
const missed = [];
|
|
941
|
+
for (const sub of parseSubpaths(path, false)) {
|
|
942
|
+
const stops = snipStations(sub, pts);
|
|
943
|
+
if (stops.length === 0) {
|
|
944
|
+
missed.push(sub);
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
const segs = sub.segs;
|
|
948
|
+
if (sub.closed) {
|
|
949
|
+
for (let i = 0; i < stops.length; i++) {
|
|
950
|
+
runs.push(arcBetween(segs, stops[i], stops[(i + 1) % stops.length], stops.length === 1));
|
|
951
|
+
}
|
|
952
|
+
} else {
|
|
953
|
+
const last = segs[segs.length - 1];
|
|
954
|
+
const all = [
|
|
955
|
+
{ seg: 0, t: 0, pt: segs[0][0] },
|
|
956
|
+
...stops,
|
|
957
|
+
{ seg: segs.length - 1, t: 1, pt: last[last.length - 1] }
|
|
958
|
+
];
|
|
959
|
+
for (let i = 0; i + 1 < all.length; i++) runs.push(arcBetween(segs, all[i], all[i + 1], false));
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
if (runs.length === 0) return null;
|
|
963
|
+
const pieces = runs.filter((r) => r.length > 0).map((r) => writePath([{ segs: r, closed: false }], rule));
|
|
964
|
+
if (missed.length > 0) pieces.push(writePath(missed, rule));
|
|
965
|
+
return pieces;
|
|
966
|
+
}
|
|
967
|
+
function snipStations(sub, cut) {
|
|
968
|
+
const raw = [];
|
|
969
|
+
sub.segs.forEach((seg, si) => {
|
|
970
|
+
for (let k = 0; k + 1 < cut.length; k++) {
|
|
971
|
+
const a = cut[k];
|
|
972
|
+
const d = { x: cut[k + 1].x - a.x, y: cut[k + 1].y - a.y };
|
|
973
|
+
for (const c of signChanges(sideCoeffs(seg, a, d), true)) {
|
|
974
|
+
const pt = evalSeg(seg, c.t);
|
|
975
|
+
if (cutPosition(cut, k, pt) !== null) raw.push({ seg: si, t: c.t, pt });
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
});
|
|
979
|
+
raw.sort((x, y) => x.seg - y.seg || x.t - y.t);
|
|
980
|
+
const tol = extentOf([sub.segs]) * 1e-9;
|
|
981
|
+
const same = (p, q) => Math.abs(p.x - q.x) <= tol && Math.abs(p.y - q.y) <= tol;
|
|
982
|
+
const groups = [];
|
|
983
|
+
for (const st of raw) {
|
|
984
|
+
const g = groups[groups.length - 1];
|
|
985
|
+
if (g && same(g[0].pt, st.pt)) g.push(st);
|
|
986
|
+
else groups.push([st]);
|
|
987
|
+
}
|
|
988
|
+
if (sub.closed && groups.length > 1 && same(groups[0][0].pt, groups[groups.length - 1][0].pt)) {
|
|
989
|
+
groups[0].push(...groups.pop());
|
|
990
|
+
}
|
|
991
|
+
const first = sub.segs[0][0];
|
|
992
|
+
const lastSeg = sub.segs[sub.segs.length - 1];
|
|
993
|
+
const end = lastSeg[lastSeg.length - 1];
|
|
994
|
+
return groups.filter((g) => g.length % 2 === 1).map((g) => g[0]).filter((st) => sub.closed || !same(st.pt, first) && !same(st.pt, end));
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
export { multiPolygonToPath, pathCrop, pathCropPieces, pathDivide, pathDividePieces, pathExclude, pathIntersect, pathMerge, pathSubtract, pathToMultiPolygon, pathTrim, pathUnion, snipPathByPolyline, splitPathByPolyline, splitPathBySegment, trianglesToPath };
|
|
918
998
|
//# sourceMappingURL=index.js.map
|
|
919
999
|
//# sourceMappingURL=index.js.map
|