@weasel-js/geom 1.13.0-rc.0 → 1.14.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/README.md +6 -2
- package/dist/3d/index.d.ts +8 -1
- package/dist/3d/index.js +25 -1
- package/dist/3d/index.js.map +1 -1
- package/dist/booleans/index.d.ts +6 -5
- package/dist/booleans/index.js +583 -73
- package/dist/booleans/index.js.map +1 -1
- package/dist/{chunk-CE7IY7TB.js → chunk-2N6OWA7E.js} +3 -3
- package/dist/{chunk-CE7IY7TB.js.map → chunk-2N6OWA7E.js.map} +1 -1
- package/dist/{chunk-Q66NA2FU.js → chunk-3HQJGI63.js} +8 -5
- package/dist/chunk-3HQJGI63.js.map +1 -0
- package/dist/{chunk-YPWFGFQX.js → chunk-3YH5XU3H.js} +125 -8
- package/dist/chunk-3YH5XU3H.js.map +1 -0
- package/dist/{chunk-UI3FUWXM.js → chunk-42TZDN5C.js} +4 -4
- package/dist/{chunk-UI3FUWXM.js.map → chunk-42TZDN5C.js.map} +1 -1
- package/dist/{chunk-B53BERKG.js → chunk-B3D6KWW3.js} +110 -8
- package/dist/chunk-B3D6KWW3.js.map +1 -0
- package/dist/{chunk-2PKPKU4P.js → chunk-EURWZUNY.js} +25 -22
- package/dist/chunk-EURWZUNY.js.map +1 -0
- package/dist/{chunk-VHXOCDZ2.js → chunk-FROQPADA.js} +6 -6
- package/dist/{chunk-VHXOCDZ2.js.map → chunk-FROQPADA.js.map} +1 -1
- package/dist/{chunk-UGE57OTX.js → chunk-GWSJQSQE.js} +11 -8
- package/dist/chunk-GWSJQSQE.js.map +1 -0
- package/dist/chunk-XXNK4N7Z.js +683 -0
- package/dist/chunk-XXNK4N7Z.js.map +1 -0
- package/dist/chunk-ZOMUTU7J.js +249 -0
- package/dist/chunk-ZOMUTU7J.js.map +1 -0
- package/dist/{curvature-PkoLduGc.d.ts → curvature-CDOI32hv.d.ts} +46 -3
- package/dist/curves/index.d.ts +2 -2
- package/dist/curves/index.js +4 -4
- package/dist/index.d.ts +172 -45
- package/dist/index.js +244 -178
- package/dist/index.js.map +1 -1
- package/dist/nd/index.d.ts +28 -1
- package/dist/nd/index.js +27 -675
- package/dist/nd/index.js.map +1 -1
- package/dist/{path-BW8d_2RY.d.ts → path-t80OoJnM.d.ts} +11 -0
- package/dist/tessellate/index.d.ts +11 -4
- package/dist/tessellate/index.js +4 -4
- package/dist/warp/index.d.ts +1 -1
- package/dist/warp/index.js +238 -64
- package/dist/warp/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-2PKPKU4P.js.map +0 -1
- package/dist/chunk-B53BERKG.js.map +0 -1
- package/dist/chunk-Q66NA2FU.js.map +0 -1
- package/dist/chunk-STGSBIOL.js +0 -152
- package/dist/chunk-STGSBIOL.js.map +0 -1
- package/dist/chunk-UGE57OTX.js.map +0 -1
- package/dist/chunk-YPWFGFQX.js.map +0 -1
package/dist/booleans/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { orientBoundaries, parseContours, contoursIntersect, assembleRegions, signedArea, negligibleArea, regionArea, toPath, parseSubpaths, writePath, signChanges, sideCoeffs, evalSeg, extentOf, keepCorners, windingAt, reverseContour, sampleContour, subSeg, endSide } from '../chunk-
|
|
2
|
-
import '../chunk-
|
|
3
|
-
import { resolveCorners, DEFAULT_FLATTEN_TOLERANCE, fillsOpenSubpaths, flattenCubic, flattenQuadratic, pointInPolygon } from '../chunk-
|
|
1
|
+
import { orientBoundaries, parseContours, contoursIntersect, assembleRegions, signedArea, negligibleArea, regionArea, toPath, parseSubpaths, writePath, signChanges, sideCoeffs, evalSeg, extentOf, keepCorners, windingAt, reverseContour, sampleContour, subSeg, endSide, intersectSegments, derivSeg } from '../chunk-3YH5XU3H.js';
|
|
2
|
+
import { nearestOnLine, nearestOnQuadratic, nearestOnCubic } from '../chunk-2N6OWA7E.js';
|
|
3
|
+
import { resolveCorners, DEFAULT_FLATTEN_TOLERANCE, fillsOpenSubpaths, flattenCubic, flattenQuadratic, pointInPolygon } from '../chunk-EURWZUNY.js';
|
|
4
4
|
import '../chunk-755Y4YZS.js';
|
|
5
|
-
import { PATH_Z, PATH_Q, elevateQuadraticToCubic, PATH_C, PATH_L, PATH_M } from '../chunk-
|
|
5
|
+
import { worldCoords, PATH_Z, PATH_Q, elevateQuadraticToCubic, PATH_C, PATH_L, PATH_M, originFor, coordsFrom } from '../chunk-B3D6KWW3.js';
|
|
6
6
|
import polygonClipping from 'polygon-clipping';
|
|
7
7
|
|
|
8
8
|
// src/booleans/adapter.ts
|
|
@@ -25,7 +25,8 @@ function pathToMultiPolygon(given, opts = {}) {
|
|
|
25
25
|
let cx = 0, cy = 0;
|
|
26
26
|
let startX = 0, startY = 0;
|
|
27
27
|
let ci = 0;
|
|
28
|
-
const { commands
|
|
28
|
+
const { commands } = path;
|
|
29
|
+
const coords = worldCoords(path);
|
|
29
30
|
const fillsOpen = fillsOpenSubpaths(path);
|
|
30
31
|
const finalizeCurrent = (closed = fillsOpen) => {
|
|
31
32
|
if (!closed) current = null;
|
|
@@ -158,25 +159,25 @@ function groupRings(rings, fillRule) {
|
|
|
158
159
|
parent[i] = best;
|
|
159
160
|
}
|
|
160
161
|
for (let i = 0; i < n; i++) if (parent[i] >= 0) children[parent[i]].push(i);
|
|
161
|
-
const
|
|
162
|
+
const filled2 = new Array(n);
|
|
162
163
|
for (let i = 0; i < n; i++) {
|
|
163
164
|
let depth = 0;
|
|
164
|
-
let
|
|
165
|
+
let winding2 = 0;
|
|
165
166
|
for (let a = i; a >= 0; a = parent[a]) {
|
|
166
|
-
|
|
167
|
+
winding2 += orient[a];
|
|
167
168
|
if (a !== i) depth++;
|
|
168
169
|
}
|
|
169
|
-
|
|
170
|
+
filled2[i] = fillRule === "evenodd" ? depth % 2 === 0 : winding2 !== 0;
|
|
170
171
|
}
|
|
171
172
|
const out = [];
|
|
172
173
|
for (let i = 0; i < n; i++) {
|
|
173
|
-
if (!
|
|
174
|
-
if (parent[i] >= 0 &&
|
|
174
|
+
if (!filled2[i]) continue;
|
|
175
|
+
if (parent[i] >= 0 && filled2[parent[i]]) continue;
|
|
175
176
|
const polygon = [rings[i]];
|
|
176
177
|
const queue = [...children[i]];
|
|
177
178
|
while (queue.length > 0) {
|
|
178
179
|
const c = queue.pop();
|
|
179
|
-
if (
|
|
180
|
+
if (filled2[c]) queue.push(...children[c]);
|
|
180
181
|
else polygon.push(rings[c]);
|
|
181
182
|
}
|
|
182
183
|
out.push(polygon);
|
|
@@ -195,7 +196,7 @@ function multiPolygonToPath(mp) {
|
|
|
195
196
|
}
|
|
196
197
|
}
|
|
197
198
|
const commands = new Uint8Array(nCmds);
|
|
198
|
-
const
|
|
199
|
+
const world = new Float64Array(nCoords);
|
|
199
200
|
let ci = 0;
|
|
200
201
|
let pi = 0;
|
|
201
202
|
for (const poly of mp) {
|
|
@@ -203,17 +204,18 @@ function multiPolygonToPath(mp) {
|
|
|
203
204
|
if (ring.length < 4) continue;
|
|
204
205
|
const unique = ring.length - 1;
|
|
205
206
|
commands[ci++] = PATH_M;
|
|
206
|
-
|
|
207
|
-
|
|
207
|
+
world[pi++] = ring[0][0];
|
|
208
|
+
world[pi++] = ring[0][1];
|
|
208
209
|
for (let k = 1; k < unique; k++) {
|
|
209
210
|
commands[ci++] = PATH_L;
|
|
210
|
-
|
|
211
|
-
|
|
211
|
+
world[pi++] = ring[k][0];
|
|
212
|
+
world[pi++] = ring[k][1];
|
|
212
213
|
}
|
|
213
214
|
commands[ci++] = PATH_Z;
|
|
214
215
|
}
|
|
215
216
|
}
|
|
216
|
-
|
|
217
|
+
const origin = originFor(world);
|
|
218
|
+
return { kind: "polygon", commands, coords: coordsFrom(origin, world), fillRule: "nonzero", ...origin ? { origin } : {} };
|
|
217
219
|
}
|
|
218
220
|
|
|
219
221
|
// src/booleans/arrangement.ts
|
|
@@ -319,15 +321,15 @@ function cutAll(segs, eps) {
|
|
|
319
321
|
}
|
|
320
322
|
function linkFaces(xs, ys, origin) {
|
|
321
323
|
const out = xs.map(() => []);
|
|
322
|
-
const
|
|
324
|
+
const angle2 = [];
|
|
323
325
|
for (let h = 0; h < origin.length; h++) {
|
|
324
326
|
const u = origin[h], v = origin[h ^ 1];
|
|
325
327
|
out[u].push(h);
|
|
326
|
-
|
|
328
|
+
angle2[h] = Math.atan2(ys[v] - ys[u], xs[v] - xs[u]);
|
|
327
329
|
}
|
|
328
330
|
const slot = [];
|
|
329
331
|
for (const list of out) {
|
|
330
|
-
list.sort((a, b) =>
|
|
332
|
+
list.sort((a, b) => angle2[a] - angle2[b]);
|
|
331
333
|
list.forEach((h, k) => {
|
|
332
334
|
slot[h] = k;
|
|
333
335
|
});
|
|
@@ -483,30 +485,530 @@ function arrangementFaces(inputs) {
|
|
|
483
485
|
return [...faces2.values()].sort((a, b) => popcount(a.mask) - popcount(b.mask) || (a.mask < b.mask ? -1 : a.mask > b.mask ? 1 : 0) || left(a) - left(b) || low(a) - low(b));
|
|
484
486
|
}
|
|
485
487
|
|
|
488
|
+
// src/booleans/curveOverlap.ts
|
|
489
|
+
function nearestOnSeg(s, p) {
|
|
490
|
+
if (s.length === 2) return nearestOnLine(p.x, p.y, s[0].x, s[0].y, s[1].x, s[1].y);
|
|
491
|
+
if (s.length === 3) return nearestOnQuadratic(p.x, p.y, s[0].x, s[0].y, s[1].x, s[1].y, s[2].x, s[2].y);
|
|
492
|
+
return nearestOnCubic(p.x, p.y, s[0].x, s[0].y, s[1].x, s[1].y, s[2].x, s[2].y, s[3].x, s[3].y);
|
|
493
|
+
}
|
|
494
|
+
var SAMPLES = [0.25, 0.5, 0.75];
|
|
495
|
+
function sharedStretch(a, b, snap) {
|
|
496
|
+
const ends = [];
|
|
497
|
+
const add = (e) => {
|
|
498
|
+
if (!ends.some((f) => Math.hypot(f.p.x - e.p.x, f.p.y - e.p.y) <= snap)) ends.push(e);
|
|
499
|
+
};
|
|
500
|
+
for (const [tb, p] of [[0, b[0]], [1, b[b.length - 1]]]) {
|
|
501
|
+
const n = nearestOnSeg(a, p);
|
|
502
|
+
if (n.dist <= snap) add({ ta: n.t, tb, p });
|
|
503
|
+
}
|
|
504
|
+
for (const [ta, p] of [[0, a[0]], [1, a[a.length - 1]]]) {
|
|
505
|
+
const n = nearestOnSeg(b, p);
|
|
506
|
+
if (n.dist <= snap) add({ ta, tb: n.t, p });
|
|
507
|
+
}
|
|
508
|
+
let best = null;
|
|
509
|
+
for (let i = 0; i < ends.length; i++) {
|
|
510
|
+
for (let j = i + 1; j < ends.length; j++) {
|
|
511
|
+
const [e, f] = ends[i].ta <= ends[j].ta ? [ends[i], ends[j]] : [ends[j], ends[i]];
|
|
512
|
+
if (best && f.ta - e.ta <= best[1].ta - best[0].ta) continue;
|
|
513
|
+
const on = SAMPLES.every((s) => nearestOnSeg(b, evalSeg(a, e.ta + (f.ta - e.ta) * s)).dist <= snap);
|
|
514
|
+
if (on) best = [e, f];
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return best;
|
|
518
|
+
}
|
|
519
|
+
function coincide(e, f, snap) {
|
|
520
|
+
const same = e.from === f.from && e.to === f.to;
|
|
521
|
+
if (!same && !(e.from === f.to && e.to === f.from)) return false;
|
|
522
|
+
return SAMPLES.every((s) => nearestOnSeg(f.seg, evalSeg(e.seg, s)).dist <= snap);
|
|
523
|
+
}
|
|
524
|
+
function mergeCoincident(edges, snap) {
|
|
525
|
+
const byEnds = /* @__PURE__ */ new Map();
|
|
526
|
+
const out = [];
|
|
527
|
+
for (const e of edges) {
|
|
528
|
+
const key = e.from < e.to ? `${e.from},${e.to}` : `${e.to},${e.from}`;
|
|
529
|
+
const list = byEnds.get(key) ?? [];
|
|
530
|
+
const twin = list.find((f) => coincide(f, e, snap));
|
|
531
|
+
if (twin) {
|
|
532
|
+
(twin.also ??= []).push({ operand: e.operand, sign: twin.from === e.from ? 1 : -1 });
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
list.push(e);
|
|
536
|
+
byEnds.set(key, list);
|
|
537
|
+
out.push(e);
|
|
538
|
+
}
|
|
539
|
+
return out;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// src/booleans/curveEdges.ts
|
|
543
|
+
var OVERLAP_HITS = 16;
|
|
544
|
+
var END_T = 1e-9;
|
|
545
|
+
var Vertices = class {
|
|
546
|
+
constructor(snap) {
|
|
547
|
+
this.snap = snap;
|
|
548
|
+
}
|
|
549
|
+
snap;
|
|
550
|
+
points = [];
|
|
551
|
+
grid = /* @__PURE__ */ new Map();
|
|
552
|
+
id(p) {
|
|
553
|
+
const gx = Math.floor(p.x / this.snap), gy = Math.floor(p.y / this.snap);
|
|
554
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
555
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
556
|
+
for (const i2 of this.grid.get(`${gx + dx},${gy + dy}`) ?? []) {
|
|
557
|
+
const q = this.points[i2];
|
|
558
|
+
if (Math.hypot(q.x - p.x, q.y - p.y) <= this.snap) return i2;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
const i = this.points.length;
|
|
563
|
+
this.points.push({ x: p.x, y: p.y });
|
|
564
|
+
const key = `${gx},${gy}`;
|
|
565
|
+
const cell = this.grid.get(key);
|
|
566
|
+
if (cell) cell.push(i);
|
|
567
|
+
else this.grid.set(key, [i]);
|
|
568
|
+
return i;
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
function refsOf(operands) {
|
|
572
|
+
const out = [];
|
|
573
|
+
operands.forEach((contours, operand) => contours.forEach((c, contour) => c.forEach((seg, index) => {
|
|
574
|
+
let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
|
|
575
|
+
for (const p of seg) {
|
|
576
|
+
x0 = Math.min(x0, p.x);
|
|
577
|
+
x1 = Math.max(x1, p.x);
|
|
578
|
+
y0 = Math.min(y0, p.y);
|
|
579
|
+
y1 = Math.max(y1, p.y);
|
|
580
|
+
}
|
|
581
|
+
out.push({ seg, operand, contour, index, count: c.length, box: [x0, y0, x1, y1] });
|
|
582
|
+
})));
|
|
583
|
+
return out;
|
|
584
|
+
}
|
|
585
|
+
var adjacent = (a, b) => {
|
|
586
|
+
if (a.operand !== b.operand || a.contour !== b.contour) return false;
|
|
587
|
+
const d = Math.abs(a.index - b.index);
|
|
588
|
+
return d === 1 || d === a.count - 1;
|
|
589
|
+
};
|
|
590
|
+
function loops(seg) {
|
|
591
|
+
if (seg.length < 4) return false;
|
|
592
|
+
const mid = evalSeg(seg, 0.5);
|
|
593
|
+
const l = subSeg(seg, 0, 0.5, seg[0], mid);
|
|
594
|
+
const r = subSeg(seg, 0.5, 1, mid, seg[3]);
|
|
595
|
+
if (!l || !r) return false;
|
|
596
|
+
return intersectSegments(l, r).some((h) => h.ta < 1 - 1e-6 || h.tb > 1e-6);
|
|
597
|
+
}
|
|
598
|
+
function cutEdges(operands, snap) {
|
|
599
|
+
const refs = refsOf(operands);
|
|
600
|
+
if (refs.some((r) => loops(r.seg))) return null;
|
|
601
|
+
const verts = new Vertices(snap);
|
|
602
|
+
const ends = refs.map(({ seg }) => [verts.id(seg[0]), verts.id(seg[seg.length - 1])]);
|
|
603
|
+
const stops = refs.map(() => []);
|
|
604
|
+
for (let i = 0; i < refs.length; i++) {
|
|
605
|
+
const a = refs[i];
|
|
606
|
+
for (let j = i + 1; j < refs.length; j++) {
|
|
607
|
+
const b = refs[j];
|
|
608
|
+
if (a.box[0] > b.box[2] + snap || b.box[0] > a.box[2] + snap || a.box[1] > b.box[3] + snap || b.box[1] > a.box[3] + snap) continue;
|
|
609
|
+
const shared = sharedStretch(a.seg, b.seg, snap);
|
|
610
|
+
if (shared) {
|
|
611
|
+
for (const end of shared) {
|
|
612
|
+
const v = verts.id(end.p);
|
|
613
|
+
stops[i].push({ t: end.ta, v });
|
|
614
|
+
stops[j].push({ t: end.tb, v });
|
|
615
|
+
}
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
const hits = intersectSegments(a.seg, b.seg);
|
|
619
|
+
if (hits.length >= OVERLAP_HITS) return null;
|
|
620
|
+
const near = adjacent(a, b);
|
|
621
|
+
for (const h of hits) {
|
|
622
|
+
if (near && (h.ta > 1 - END_T && h.tb < END_T || h.ta < END_T && h.tb > 1 - END_T)) continue;
|
|
623
|
+
const v = verts.id(h);
|
|
624
|
+
stops[i].push({ t: h.ta, v });
|
|
625
|
+
stops[j].push({ t: h.tb, v });
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
const edges = [];
|
|
630
|
+
refs.forEach((ref, i) => {
|
|
631
|
+
const { seg } = ref;
|
|
632
|
+
const list = [{ t: 0, v: ends[i][0] }, ...stops[i].sort((p, q) => p.t - q.t), { t: 1, v: ends[i][1] }];
|
|
633
|
+
const kept = [list[0]];
|
|
634
|
+
for (let k = 1; k < list.length; k++) {
|
|
635
|
+
const s = list[k];
|
|
636
|
+
if (s.v !== kept[kept.length - 1].v) kept.push(s);
|
|
637
|
+
else if (k === list.length - 1 && kept.length > 1) kept[kept.length - 1] = s;
|
|
638
|
+
}
|
|
639
|
+
const size = Math.hypot(ref.box[2] - ref.box[0], ref.box[3] - ref.box[1]);
|
|
640
|
+
if (kept.length === 1 && seg.length > 2 && size > 100 * snap) kept.push(list[list.length - 1]);
|
|
641
|
+
for (let k = 0; k + 1 < kept.length; k++) {
|
|
642
|
+
const a = kept[k], b = kept[k + 1];
|
|
643
|
+
const piece = a.t === 0 && b.t === 1 ? seg.slice() : subSeg(seg, a.t, b.t, verts.points[a.v], verts.points[b.v]);
|
|
644
|
+
if (!piece) continue;
|
|
645
|
+
piece[0] = verts.points[a.v];
|
|
646
|
+
piece[piece.length - 1] = verts.points[b.v];
|
|
647
|
+
edges.push({ seg: piece, from: a.v, to: b.v, operand: ref.operand, source: seg, t0: a.t, t1: b.t, key: i + a.t * 0.5, reversed: false });
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
return { edges: mergeCoincident(edges, snap), points: verts.points };
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// src/booleans/curveClassify.ts
|
|
654
|
+
function windingSet(contours, fillRule) {
|
|
655
|
+
const segs = contours.flat();
|
|
656
|
+
const boxes = new Float64Array(segs.length * 4);
|
|
657
|
+
segs.forEach((s, i) => {
|
|
658
|
+
let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
|
|
659
|
+
for (const p of s) {
|
|
660
|
+
x0 = Math.min(x0, p.x);
|
|
661
|
+
x1 = Math.max(x1, p.x);
|
|
662
|
+
y0 = Math.min(y0, p.y);
|
|
663
|
+
y1 = Math.max(y1, p.y);
|
|
664
|
+
}
|
|
665
|
+
boxes.set([x0, y0, x1, y1], i * 4);
|
|
666
|
+
});
|
|
667
|
+
return { segs, boxes, fillRule };
|
|
668
|
+
}
|
|
669
|
+
var RAY = { x: 1, y: 0 };
|
|
670
|
+
function winding(q, set) {
|
|
671
|
+
let w = 0;
|
|
672
|
+
const { segs, boxes } = set;
|
|
673
|
+
for (let i = 0; i < segs.length; i++) {
|
|
674
|
+
if (q.y < boxes[i * 4 + 1] || q.y > boxes[i * 4 + 3] || boxes[i * 4 + 2] < q.x) continue;
|
|
675
|
+
const s = segs[i];
|
|
676
|
+
for (const c of signChanges(sideCoeffs(s, q, RAY), true)) {
|
|
677
|
+
if (evalSeg(s, c.t).x > q.x) w += c.after > 0 ? 1 : -1;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return w;
|
|
681
|
+
}
|
|
682
|
+
var filled = (w, rule) => rule === "evenodd" ? (w & 1) !== 0 : w !== 0;
|
|
683
|
+
var reversed = (e) => ({ ...e, seg: e.seg.slice().reverse(), from: e.to, to: e.from, reversed: true });
|
|
684
|
+
function runs(e, operands) {
|
|
685
|
+
const n = new Array(operands).fill(0);
|
|
686
|
+
n[e.operand] = 1;
|
|
687
|
+
for (const { operand, sign } of e.also ?? []) n[operand] += sign;
|
|
688
|
+
return n;
|
|
689
|
+
}
|
|
690
|
+
function coverEdges(edges, sets, probe) {
|
|
691
|
+
const out = [];
|
|
692
|
+
for (const e of edges) {
|
|
693
|
+
const tm = (e.t0 + e.t1) / 2;
|
|
694
|
+
const m = evalSeg(e.source, tm);
|
|
695
|
+
let tan = derivSeg(e.source, tm);
|
|
696
|
+
let len = Math.hypot(tan.x, tan.y);
|
|
697
|
+
if (len === 0) {
|
|
698
|
+
const a = e.seg[0], b = e.seg[e.seg.length - 1];
|
|
699
|
+
tan = { x: b.x - a.x, y: b.y - a.y };
|
|
700
|
+
len = Math.hypot(tan.x, tan.y);
|
|
701
|
+
if (len === 0) continue;
|
|
702
|
+
}
|
|
703
|
+
const nx = -tan.y / len * probe, ny = tan.x / len * probe;
|
|
704
|
+
const L = { x: m.x + nx, y: m.y + ny }, R = { x: m.x - nx, y: m.y - ny };
|
|
705
|
+
const along2 = runs(e, sets.length);
|
|
706
|
+
const inL = new Array(sets.length);
|
|
707
|
+
const inR = new Array(sets.length);
|
|
708
|
+
for (let k = 0; k < sets.length; k++) {
|
|
709
|
+
const wl = winding(L, sets[k]), wr = winding(R, sets[k]);
|
|
710
|
+
if (Math.abs(wl - wr) !== Math.abs(along2[k])) return null;
|
|
711
|
+
inL[k] = filled(wl, sets[k].fillRule);
|
|
712
|
+
inR[k] = filled(wr, sets[k].fillRule);
|
|
713
|
+
}
|
|
714
|
+
out.push({ edge: e, inL, inR });
|
|
715
|
+
}
|
|
716
|
+
return out;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// src/booleans/curveTrace.ts
|
|
720
|
+
var TAU = Math.PI * 2;
|
|
721
|
+
var TIE = 1e-7;
|
|
722
|
+
function leaving(s) {
|
|
723
|
+
const a = s[0];
|
|
724
|
+
for (let i = 1; i < s.length; i++) if (s[i].x !== a.x || s[i].y !== a.y) return { x: s[i].x - a.x, y: s[i].y - a.y };
|
|
725
|
+
return { x: 0, y: 0 };
|
|
726
|
+
}
|
|
727
|
+
var backward = (s) => s.slice().reverse();
|
|
728
|
+
function along(s, dist) {
|
|
729
|
+
let len = 0;
|
|
730
|
+
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);
|
|
731
|
+
const p = evalSeg(s, len === 0 ? 0 : Math.min(1, dist / len));
|
|
732
|
+
return { x: p.x - s[0].x, y: p.y - s[0].y };
|
|
733
|
+
}
|
|
734
|
+
var angle = (d) => Math.atan2(d.y, d.x);
|
|
735
|
+
function clockwise(from, to) {
|
|
736
|
+
const d = (from - to) % TAU;
|
|
737
|
+
return d <= 0 ? d + TAU : d;
|
|
738
|
+
}
|
|
739
|
+
function chordLength(s) {
|
|
740
|
+
const a = s[0], b = s[s.length - 1];
|
|
741
|
+
return Math.hypot(b.x - a.x, b.y - a.y) || Math.hypot(s[1].x - a.x, s[1].y - a.y);
|
|
742
|
+
}
|
|
743
|
+
function nextEdge(e, out) {
|
|
744
|
+
const cands = out[e.to];
|
|
745
|
+
if (!cands || cands.length === 0) return -1;
|
|
746
|
+
if (cands.length === 1) return cands[0].key;
|
|
747
|
+
const back = backward(e.seg);
|
|
748
|
+
const turns = (dir) => {
|
|
749
|
+
const from = angle(dir(back));
|
|
750
|
+
return cands.map((c) => clockwise(from, angle(dir(c.seg))));
|
|
751
|
+
};
|
|
752
|
+
const tied = (t2) => {
|
|
753
|
+
const sorted = t2.slice().sort((x, y) => x - y);
|
|
754
|
+
return sorted[0] < TIE || sorted[sorted.length - 1] > TAU - TIE || sorted[1] - sorted[0] < TIE;
|
|
755
|
+
};
|
|
756
|
+
let t = turns(leaving);
|
|
757
|
+
if (tied(t)) {
|
|
758
|
+
const dist = 1e-3 * Math.min(chordLength(e.seg), ...cands.map((c) => chordLength(c.seg)));
|
|
759
|
+
t = turns((s) => along(s, dist));
|
|
760
|
+
if (tied(t)) return -1;
|
|
761
|
+
}
|
|
762
|
+
let best = 0;
|
|
763
|
+
for (let i = 1; i < t.length; i++) if (t[i] < t[best]) best = i;
|
|
764
|
+
return cands[best].key;
|
|
765
|
+
}
|
|
766
|
+
function joined(e, f) {
|
|
767
|
+
if (e.source !== f.source || e.reversed !== f.reversed) return null;
|
|
768
|
+
const [lo, hi] = e.reversed ? [f, e] : [e, f];
|
|
769
|
+
if (lo.t1 !== hi.t0) return null;
|
|
770
|
+
const t0 = lo.t0, t1 = hi.t1;
|
|
771
|
+
const a = lo.seg[e.reversed ? lo.seg.length - 1 : 0], b = hi.seg[e.reversed ? 0 : hi.seg.length - 1];
|
|
772
|
+
const seg = t0 === 0 && t1 === 1 ? lo.source.slice() : subSeg(lo.source, t0, t1, a, b);
|
|
773
|
+
if (!seg) return null;
|
|
774
|
+
seg[0] = a;
|
|
775
|
+
seg[seg.length - 1] = b;
|
|
776
|
+
if (e.reversed) seg.reverse();
|
|
777
|
+
return { ...e, seg, from: e.from, to: f.to, t0, t1 };
|
|
778
|
+
}
|
|
779
|
+
function rejoin(loop) {
|
|
780
|
+
const out = [];
|
|
781
|
+
for (const e of loop) {
|
|
782
|
+
const j = out.length > 0 ? joined(out[out.length - 1], e) : null;
|
|
783
|
+
if (j) out[out.length - 1] = j;
|
|
784
|
+
else out.push(e);
|
|
785
|
+
}
|
|
786
|
+
while (out.length > 1) {
|
|
787
|
+
const j = joined(out[out.length - 1], out[0]);
|
|
788
|
+
if (!j) break;
|
|
789
|
+
out.pop();
|
|
790
|
+
out[0] = j;
|
|
791
|
+
}
|
|
792
|
+
return out.map((e) => e.seg);
|
|
793
|
+
}
|
|
794
|
+
function traceLoops(edges, vertexCount) {
|
|
795
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
796
|
+
const out = Array.from({ length: vertexCount }, () => []);
|
|
797
|
+
edges.forEach((e, i) => {
|
|
798
|
+
byKey.set(e.key, i);
|
|
799
|
+
out[e.from].push(e);
|
|
800
|
+
});
|
|
801
|
+
if (byKey.size !== edges.length) return null;
|
|
802
|
+
const next = edges.map((e) => {
|
|
803
|
+
const k = nextEdge(e, out);
|
|
804
|
+
return k < 0 ? -1 : byKey.get(k);
|
|
805
|
+
});
|
|
806
|
+
if (next.includes(-1)) return null;
|
|
807
|
+
const loopOf = new Int32Array(edges.length).fill(-1);
|
|
808
|
+
const loops2 = [];
|
|
809
|
+
for (let s = 0; s < edges.length; s++) {
|
|
810
|
+
if (loopOf[s] >= 0) continue;
|
|
811
|
+
const loop = [];
|
|
812
|
+
let e = s;
|
|
813
|
+
do {
|
|
814
|
+
if (loopOf[e] >= 0) return null;
|
|
815
|
+
loopOf[e] = loops2.length;
|
|
816
|
+
loop.push(e);
|
|
817
|
+
e = next[e];
|
|
818
|
+
} while (e !== s);
|
|
819
|
+
let first = 0;
|
|
820
|
+
for (let i = 1; i < loop.length; i++) if (edges[loop[i]].key < edges[loop[first]].key) first = i;
|
|
821
|
+
loops2.push(rejoin([...loop.slice(first), ...loop.slice(0, first)].map((i) => edges[i])));
|
|
822
|
+
}
|
|
823
|
+
return loops2;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// src/booleans/curveBoolean.ts
|
|
827
|
+
var MAX_SEGMENTS = 4096;
|
|
828
|
+
function hasCurve(path) {
|
|
829
|
+
const p = resolveCorners(path);
|
|
830
|
+
return p.kind === "polygon" && p.commands.some((c) => c === PATH_C || c === PATH_Q);
|
|
831
|
+
}
|
|
832
|
+
function closingTrimmed(loop) {
|
|
833
|
+
if (loop.length < 2) return loop;
|
|
834
|
+
const last = loop[loop.length - 1];
|
|
835
|
+
return last.length === 2 ? loop.slice(0, -1) : loop;
|
|
836
|
+
}
|
|
837
|
+
function cutPaths(paths) {
|
|
838
|
+
if (!paths.some(hasCurve)) return null;
|
|
839
|
+
const operands = paths.map((p) => parseContours(p));
|
|
840
|
+
const count = operands.reduce((n, cs) => n + cs.reduce((m, c) => m + c.length, 0), 0);
|
|
841
|
+
if (count === 0 || count > MAX_SEGMENTS) return null;
|
|
842
|
+
const extent = extentOf(operands.flat()) || 1;
|
|
843
|
+
const arrangement = cutEdges(operands, extent * 1e-6);
|
|
844
|
+
if (!arrangement) return null;
|
|
845
|
+
const sets = operands.map((cs, i) => {
|
|
846
|
+
const p = paths[i];
|
|
847
|
+
return windingSet(cs, p.kind === "polygon" ? p.fillRule : "nonzero");
|
|
848
|
+
});
|
|
849
|
+
const edges = coverEdges(arrangement.edges, sets, extent * 1e-7);
|
|
850
|
+
if (!edges) return null;
|
|
851
|
+
return { edges, vertexCount: arrangement.points.length, clockwise: operands.map(clockwiseFirst) };
|
|
852
|
+
}
|
|
853
|
+
function curveRegions(cut, label) {
|
|
854
|
+
const sides = /* @__PURE__ */ new Map();
|
|
855
|
+
const add = (k, e) => {
|
|
856
|
+
const list = sides.get(k);
|
|
857
|
+
if (list) list.push(e);
|
|
858
|
+
else sides.set(k, [e]);
|
|
859
|
+
};
|
|
860
|
+
for (const { edge, inL, inR } of cut.edges) {
|
|
861
|
+
const l = label(inL), r = label(inR);
|
|
862
|
+
if (l === r) continue;
|
|
863
|
+
if (l !== -1) add(l, edge);
|
|
864
|
+
if (r !== -1) add(r, reversed(edge));
|
|
865
|
+
}
|
|
866
|
+
const out = /* @__PURE__ */ new Map();
|
|
867
|
+
for (const [k, edges] of sides) {
|
|
868
|
+
const loops2 = traceLoops(edges, cut.vertexCount);
|
|
869
|
+
if (!loops2) return null;
|
|
870
|
+
out.set(k, loops2);
|
|
871
|
+
}
|
|
872
|
+
return out;
|
|
873
|
+
}
|
|
874
|
+
function writeLoops(loops2, clockwise2) {
|
|
875
|
+
const turned = clockwise2 ? loops2.map(reversedFromFirst) : loops2;
|
|
876
|
+
return writePath(turned.map((l) => ({ segs: closingTrimmed(l), closed: true })), "nonzero");
|
|
877
|
+
}
|
|
878
|
+
function curveBoolean(paths, keep) {
|
|
879
|
+
const cut = cutPaths(paths);
|
|
880
|
+
if (!cut) return null;
|
|
881
|
+
const regions = curveRegions(cut, (inside) => keep(inside) ? 0 : -1);
|
|
882
|
+
if (!regions) return null;
|
|
883
|
+
return writeLoops(regions.get(0) ?? [], cut.clockwise[0]);
|
|
884
|
+
}
|
|
885
|
+
function clockwiseFirst(contours) {
|
|
886
|
+
let best = 0;
|
|
887
|
+
for (const c of contours) {
|
|
888
|
+
const a = signedArea(c);
|
|
889
|
+
if (Math.abs(a) > Math.abs(best)) best = a;
|
|
890
|
+
}
|
|
891
|
+
return best < 0;
|
|
892
|
+
}
|
|
893
|
+
function reversedFromFirst(loop) {
|
|
894
|
+
const rev = reverseContour(loop);
|
|
895
|
+
return [rev[rev.length - 1], ...rev.slice(0, -1)];
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// src/booleans/curvePieces.ts
|
|
899
|
+
var covering = (inside) => {
|
|
900
|
+
const s = [];
|
|
901
|
+
inside.forEach((v, i) => {
|
|
902
|
+
if (v) s.push(i);
|
|
903
|
+
});
|
|
904
|
+
return s;
|
|
905
|
+
};
|
|
906
|
+
var maskOf = (sources) => sources.reduce((m, i) => m | 1n << BigInt(i), 0n);
|
|
907
|
+
function corner(loops2) {
|
|
908
|
+
const pts = sampleContour(loops2[0]);
|
|
909
|
+
let x = Infinity, y = Infinity;
|
|
910
|
+
for (let i = 0; i < pts.length; i += 2) {
|
|
911
|
+
x = Math.min(x, pts[i]);
|
|
912
|
+
y = Math.min(y, pts[i + 1]);
|
|
913
|
+
}
|
|
914
|
+
return [x, y];
|
|
915
|
+
}
|
|
916
|
+
function connected(loops2) {
|
|
917
|
+
const outers = [], holes = [];
|
|
918
|
+
for (const l of loops2) (signedArea(l) > 0 ? outers : holes).push(l);
|
|
919
|
+
return assembleRegions(outers, holes);
|
|
920
|
+
}
|
|
921
|
+
function curveDivide(paths, split) {
|
|
922
|
+
const cut = cutPaths(paths);
|
|
923
|
+
if (!cut) return null;
|
|
924
|
+
const ids = /* @__PURE__ */ new Map();
|
|
925
|
+
const sets = [];
|
|
926
|
+
const regions = curveRegions(cut, (inside) => {
|
|
927
|
+
const s = covering(inside);
|
|
928
|
+
if (s.length === 0) return -1;
|
|
929
|
+
const key = s.join(",");
|
|
930
|
+
let id = ids.get(key);
|
|
931
|
+
if (id === void 0) {
|
|
932
|
+
id = sets.length;
|
|
933
|
+
ids.set(key, id);
|
|
934
|
+
sets.push(s);
|
|
935
|
+
}
|
|
936
|
+
return id;
|
|
937
|
+
});
|
|
938
|
+
if (!regions) return null;
|
|
939
|
+
const out = [];
|
|
940
|
+
for (const [id, loops2] of regions) {
|
|
941
|
+
const sources = sets[id];
|
|
942
|
+
const mask = maskOf(sources);
|
|
943
|
+
for (const face of split ? connected(loops2) : [loops2]) {
|
|
944
|
+
out.push({ piece: { path: writeLoops(face, cut.clockwise[sources[0]]), sources }, mask, at: corner(face) });
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
return out.sort((a, b) => a.piece.sources.length - b.piece.sources.length || (a.mask < b.mask ? -1 : a.mask > b.mask ? 1 : 0) || a.at[0] - b.at[0] || a.at[1] - b.at[1]).map((o) => o.piece);
|
|
948
|
+
}
|
|
949
|
+
var topmost = (inside) => inside.lastIndexOf(true);
|
|
950
|
+
function shown(cut) {
|
|
951
|
+
return curveRegions(cut, topmost);
|
|
952
|
+
}
|
|
953
|
+
function curveTrim(paths) {
|
|
954
|
+
const cut = cutPaths(paths);
|
|
955
|
+
const regions = cut && shown(cut);
|
|
956
|
+
if (!cut || !regions) return null;
|
|
957
|
+
return [...regions.keys()].sort((a, b) => a - b).map((i) => ({ path: writeLoops(regions.get(i), cut.clockwise[i]), sources: [i] }));
|
|
958
|
+
}
|
|
959
|
+
function curveMerge(paths, keyOf) {
|
|
960
|
+
const cut = cutPaths(paths);
|
|
961
|
+
const visible = cut && shown(cut);
|
|
962
|
+
if (!cut || !visible) return null;
|
|
963
|
+
const groupIds = /* @__PURE__ */ new Map();
|
|
964
|
+
const groupOf = paths.map((_, i) => {
|
|
965
|
+
const key = keyOf(i);
|
|
966
|
+
let g = groupIds.get(key);
|
|
967
|
+
if (g === void 0) {
|
|
968
|
+
g = groupIds.size;
|
|
969
|
+
groupIds.set(key, g);
|
|
970
|
+
}
|
|
971
|
+
return g;
|
|
972
|
+
});
|
|
973
|
+
const regions = curveRegions(cut, (inside) => {
|
|
974
|
+
const top = topmost(inside);
|
|
975
|
+
return top < 0 ? -1 : groupOf[top];
|
|
976
|
+
});
|
|
977
|
+
if (!regions) return null;
|
|
978
|
+
const pieces = [];
|
|
979
|
+
for (const [g, loops2] of regions) {
|
|
980
|
+
const sources = [...visible.keys()].filter((i) => groupOf[i] === g).sort((a, b) => a - b);
|
|
981
|
+
pieces.push({ path: writeLoops(loops2, cut.clockwise[sources[0]]), sources });
|
|
982
|
+
}
|
|
983
|
+
return pieces.sort((a, b) => a.sources[a.sources.length - 1] - b.sources[b.sources.length - 1]);
|
|
984
|
+
}
|
|
985
|
+
|
|
486
986
|
// src/booleans/ops.ts
|
|
487
987
|
function toPath2(mp, sources) {
|
|
488
988
|
return keepCorners(sources, multiPolygonToPath(mp));
|
|
489
989
|
}
|
|
490
|
-
function
|
|
990
|
+
function boolean(paths, keep, clip) {
|
|
491
991
|
if (paths.length === 0) return multiPolygonToPath([]);
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
|
|
992
|
+
const curved = curveBoolean(paths, keep);
|
|
993
|
+
if (curved) return keepCorners(paths, curved);
|
|
994
|
+
const [head, ...rest] = paths.map((p) => pathToMultiPolygon(p));
|
|
995
|
+
return toPath2(clip(head, ...rest), paths);
|
|
996
|
+
}
|
|
997
|
+
var any = (inside) => inside.includes(true);
|
|
998
|
+
var all = (inside) => !inside.includes(false);
|
|
999
|
+
var aNotB = (inside) => inside[0] && !inside[1];
|
|
1000
|
+
var odd = (inside) => inside.filter(Boolean).length % 2 === 1;
|
|
1001
|
+
function pathUnion(...paths) {
|
|
1002
|
+
return boolean(paths, any, polygonClipping.union);
|
|
495
1003
|
}
|
|
496
1004
|
function pathIntersect(...paths) {
|
|
497
|
-
|
|
498
|
-
const mps = paths.map((p) => pathToMultiPolygon(p));
|
|
499
|
-
const [head, ...rest] = mps;
|
|
500
|
-
return toPath2(polygonClipping.intersection(head, ...rest), paths);
|
|
1005
|
+
return boolean(paths, all, polygonClipping.intersection);
|
|
501
1006
|
}
|
|
502
1007
|
function pathSubtract(a, b) {
|
|
503
|
-
return
|
|
1008
|
+
return boolean([a, b], aNotB, polygonClipping.difference);
|
|
504
1009
|
}
|
|
505
1010
|
function pathExclude(...paths) {
|
|
506
|
-
|
|
507
|
-
const mps = paths.map((p) => pathToMultiPolygon(p));
|
|
508
|
-
const [head, ...rest] = mps;
|
|
509
|
-
return toPath2(polygonClipping.xor(head, ...rest), paths);
|
|
1011
|
+
return boolean(paths, odd, polygonClipping.xor);
|
|
510
1012
|
}
|
|
511
1013
|
function pathDivide(...paths) {
|
|
512
1014
|
return pathDividePieces(...paths).map((p) => p.path);
|
|
@@ -514,6 +1016,7 @@ function pathDivide(...paths) {
|
|
|
514
1016
|
function pathCrop(...paths) {
|
|
515
1017
|
return pathCropPieces(...paths).map((p) => p.path);
|
|
516
1018
|
}
|
|
1019
|
+
var cornered = (pieces, sources) => pieces.map(({ path, sources: s }) => ({ path: keepCorners(sources, path), sources: s }));
|
|
517
1020
|
function faces(paths) {
|
|
518
1021
|
const clean = paths.map((p) => {
|
|
519
1022
|
const mp = pathToMultiPolygon(p);
|
|
@@ -526,6 +1029,8 @@ function faces(paths) {
|
|
|
526
1029
|
});
|
|
527
1030
|
}
|
|
528
1031
|
function pathDividePieces(...paths) {
|
|
1032
|
+
const curved = curveDivide(paths, false);
|
|
1033
|
+
if (curved) return cornered(curved, paths);
|
|
529
1034
|
const regions = /* @__PURE__ */ new Map();
|
|
530
1035
|
for (const { polygon, sources } of faces(paths)) {
|
|
531
1036
|
const key = sources.join(",");
|
|
@@ -536,20 +1041,26 @@ function pathDividePieces(...paths) {
|
|
|
536
1041
|
return [...regions.values()].map(({ polygons, sources }) => ({ path: toPath2(polygons, paths), sources }));
|
|
537
1042
|
}
|
|
538
1043
|
function pathFacePieces(...paths) {
|
|
1044
|
+
const curved = curveDivide(paths, true);
|
|
1045
|
+
if (curved) return cornered(curved, paths);
|
|
539
1046
|
return faces(paths).map(({ polygon, sources }) => ({ path: toPath2([polygon], paths), sources }));
|
|
540
1047
|
}
|
|
541
1048
|
function pathTrim(...paths) {
|
|
1049
|
+
const curved = curveTrim(paths);
|
|
1050
|
+
if (curved) return cornered(curved, paths);
|
|
542
1051
|
const mps = paths.map((p) => pathToMultiPolygon(p));
|
|
543
1052
|
const out = [];
|
|
544
1053
|
let cover = null;
|
|
545
1054
|
for (let i = mps.length - 1; i >= 0; i--) {
|
|
546
|
-
const
|
|
547
|
-
if (
|
|
1055
|
+
const shown2 = cover ? polygonClipping.difference(mps[i], cover) : mps[i];
|
|
1056
|
+
if (shown2.length > 0) out.unshift({ path: toPath2(shown2, paths), sources: [i] });
|
|
548
1057
|
cover = cover ? polygonClipping.union(cover, mps[i]) : mps[i];
|
|
549
1058
|
}
|
|
550
1059
|
return out;
|
|
551
1060
|
}
|
|
552
1061
|
function pathMerge(paths, keyOf) {
|
|
1062
|
+
const curved = curveMerge(paths, keyOf);
|
|
1063
|
+
if (curved) return cornered(curved, paths);
|
|
553
1064
|
const groups = /* @__PURE__ */ new Map();
|
|
554
1065
|
for (const piece of pathTrim(...paths)) {
|
|
555
1066
|
const key = keyOf(piece.sources[0]);
|
|
@@ -565,12 +1076,11 @@ function pathMerge(paths, keyOf) {
|
|
|
565
1076
|
}
|
|
566
1077
|
function pathCropPieces(...paths) {
|
|
567
1078
|
if (paths.length < 2) return [];
|
|
568
|
-
const
|
|
569
|
-
const mask = mps[mps.length - 1];
|
|
1079
|
+
const mask = paths[paths.length - 1];
|
|
570
1080
|
const out = [];
|
|
571
|
-
for (let i = 0; i <
|
|
572
|
-
const clipped =
|
|
573
|
-
if (clipped.length > 0) out.push({ path:
|
|
1081
|
+
for (let i = 0; i < paths.length - 1; i++) {
|
|
1082
|
+
const clipped = pathIntersect(paths[i], mask);
|
|
1083
|
+
if (clipped.commands.length > 0) out.push({ path: clipped, sources: [i] });
|
|
574
1084
|
}
|
|
575
1085
|
return out;
|
|
576
1086
|
}
|
|
@@ -658,8 +1168,8 @@ function meshOutline(rings) {
|
|
|
658
1168
|
}
|
|
659
1169
|
}
|
|
660
1170
|
}
|
|
661
|
-
const
|
|
662
|
-
return
|
|
1171
|
+
const loops2 = walkLoops(edges).map((loop) => loop.map((k) => points.get(k)));
|
|
1172
|
+
return loops2.length === 0 ? null : bridge(loops2);
|
|
663
1173
|
}
|
|
664
1174
|
function walkLoops(edges) {
|
|
665
1175
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -669,7 +1179,7 @@ function walkLoops(edges) {
|
|
|
669
1179
|
if (!list) out.set(a, list = []);
|
|
670
1180
|
for (let i = 0; i < n; i++) list.push(b);
|
|
671
1181
|
}
|
|
672
|
-
const
|
|
1182
|
+
const loops2 = [];
|
|
673
1183
|
for (const start of out.keys()) {
|
|
674
1184
|
while ((out.get(start)?.length ?? 0) > 0) {
|
|
675
1185
|
const loop = [start];
|
|
@@ -679,13 +1189,13 @@ function walkLoops(edges) {
|
|
|
679
1189
|
const next = out.get(at);
|
|
680
1190
|
at = next.pop();
|
|
681
1191
|
}
|
|
682
|
-
|
|
1192
|
+
loops2.push(loop);
|
|
683
1193
|
}
|
|
684
1194
|
}
|
|
685
|
-
return
|
|
1195
|
+
return loops2;
|
|
686
1196
|
}
|
|
687
|
-
function bridge(
|
|
688
|
-
const entered =
|
|
1197
|
+
function bridge(loops2) {
|
|
1198
|
+
const entered = loops2.map((loop) => {
|
|
689
1199
|
let k = 0;
|
|
690
1200
|
for (let i = 1; i < loop.length; i++) if (loop[i][0] < loop[k][0] || loop[i][0] === loop[k][0] && loop[i][1] < loop[k][1]) k = i;
|
|
691
1201
|
return [...loop.slice(k), ...loop.slice(0, k)];
|
|
@@ -785,7 +1295,7 @@ function properCrossing(p0, p1, q0, q1) {
|
|
|
785
1295
|
}
|
|
786
1296
|
function splitLoops(run) {
|
|
787
1297
|
const spine = [run[0]];
|
|
788
|
-
const
|
|
1298
|
+
const loops2 = [];
|
|
789
1299
|
for (let i = 1; i < run.length; i++) {
|
|
790
1300
|
const to = run[i];
|
|
791
1301
|
for (; ; ) {
|
|
@@ -801,17 +1311,17 @@ function splitLoops(run) {
|
|
|
801
1311
|
spine.push(to);
|
|
802
1312
|
break;
|
|
803
1313
|
}
|
|
804
|
-
|
|
1314
|
+
loops2.push([hit.x, ...spine.slice(hit.j + 1)]);
|
|
805
1315
|
spine.length = hit.j + 1;
|
|
806
1316
|
spine.push(hit.x);
|
|
807
1317
|
}
|
|
808
1318
|
}
|
|
809
1319
|
const last = spine[spine.length - 1];
|
|
810
1320
|
if (spine.length >= 4 && last.x === spine[0].x && last.y === spine[0].y) {
|
|
811
|
-
|
|
812
|
-
return { spine: [], loops };
|
|
1321
|
+
loops2.push(spine.slice(0, -1));
|
|
1322
|
+
return { spine: [], loops: loops2 };
|
|
813
1323
|
}
|
|
814
|
-
return { spine, loops };
|
|
1324
|
+
return { spine, loops: loops2 };
|
|
815
1325
|
}
|
|
816
1326
|
function pointAt(cut, s) {
|
|
817
1327
|
const k = Math.min(Math.floor(s), cut.length - 2);
|
|
@@ -842,7 +1352,7 @@ function interiorLoops(cut, boundaries) {
|
|
|
842
1352
|
const last = cut.length - 1;
|
|
843
1353
|
const stops = [{ s: 0, pt: cut[0] }, ...crossings, { s: last, pt: cut[last] }];
|
|
844
1354
|
const rings = [];
|
|
845
|
-
const
|
|
1355
|
+
const loops2 = [];
|
|
846
1356
|
for (let i = 0; i + 1 < stops.length; i++) {
|
|
847
1357
|
const a = stops[i], b = stops[i + 1];
|
|
848
1358
|
if (b.s - a.s <= 1e-9) continue;
|
|
@@ -854,15 +1364,15 @@ function interiorLoops(cut, boundaries) {
|
|
|
854
1364
|
const ring = [...loop, loop[0]];
|
|
855
1365
|
if (chords.some((c) => polylinesCross(ring, c)) || rings.some((r) => polylinesCross(ring, r))) continue;
|
|
856
1366
|
rings.push(ring);
|
|
857
|
-
|
|
1367
|
+
loops2.push(loop);
|
|
858
1368
|
}
|
|
859
1369
|
}
|
|
860
|
-
return
|
|
1370
|
+
return loops2;
|
|
861
1371
|
}
|
|
862
|
-
function cutOutLoops(regions,
|
|
1372
|
+
function cutOutLoops(regions, loops2) {
|
|
863
1373
|
let cut = false;
|
|
864
1374
|
const minArea = negligibleArea(regions.flat());
|
|
865
|
-
for (const loop of
|
|
1375
|
+
for (const loop of loops2) {
|
|
866
1376
|
let ring = loop.map((p, i) => [p, loop[(i + 1) % loop.length]]);
|
|
867
1377
|
const a = signedArea(ring);
|
|
868
1378
|
if (Math.abs(a) <= minArea) continue;
|
|
@@ -892,7 +1402,7 @@ function polylinesCross(a, b) {
|
|
|
892
1402
|
}
|
|
893
1403
|
|
|
894
1404
|
// src/booleans/cutLoops.ts
|
|
895
|
-
function
|
|
1405
|
+
function traceLoops2(boundaries, partner, chord) {
|
|
896
1406
|
const byContour = /* @__PURE__ */ new Map();
|
|
897
1407
|
for (const c of partner.keys()) {
|
|
898
1408
|
const list = byContour.get(c.contour) ?? [];
|
|
@@ -913,7 +1423,7 @@ function traceLoops(boundaries, partner, chord) {
|
|
|
913
1423
|
arcFrom.set(from, { segs: arcBetween(contour, from, to, cuts.length === 1), end: to });
|
|
914
1424
|
}
|
|
915
1425
|
});
|
|
916
|
-
const
|
|
1426
|
+
const loops2 = [];
|
|
917
1427
|
const seen = /* @__PURE__ */ new Set();
|
|
918
1428
|
for (const start of arcFrom.keys()) {
|
|
919
1429
|
if (seen.has(start)) continue;
|
|
@@ -927,9 +1437,9 @@ function traceLoops(boundaries, partner, chord) {
|
|
|
927
1437
|
for (let i = 0; i + 1 < run.length; i++) loop.push([run[i], run[i + 1]]);
|
|
928
1438
|
at = partner.get(arc.end);
|
|
929
1439
|
}
|
|
930
|
-
|
|
1440
|
+
loops2.push(loop);
|
|
931
1441
|
}
|
|
932
|
-
return { loops, untouched };
|
|
1442
|
+
return { loops: loops2, untouched };
|
|
933
1443
|
}
|
|
934
1444
|
function arcBetween(contour, from, to, whole) {
|
|
935
1445
|
const out = [];
|
|
@@ -960,11 +1470,11 @@ function splitPathByPolyline(path, cut, opts = {}) {
|
|
|
960
1470
|
const boundaries = resolveBoundaries(path, opts);
|
|
961
1471
|
if (boundaries.length === 0) return null;
|
|
962
1472
|
pts = openClosedCut(pts, boundaries);
|
|
963
|
-
const
|
|
1473
|
+
const loops2 = interiorLoops(pts, boundaries);
|
|
964
1474
|
const pieces = knife(path, pts, opts, pts.length);
|
|
965
|
-
if (
|
|
1475
|
+
if (loops2.length === 0) return pieces;
|
|
966
1476
|
const regions = pieces ? pieces.map((p) => orientBoundaries(parseContours(p), outRule)) : [boundaries];
|
|
967
|
-
if (!cutOutLoops(regions,
|
|
1477
|
+
if (!cutOutLoops(regions, loops2)) return pieces;
|
|
968
1478
|
return regions.map((r) => toPath(r, outRule));
|
|
969
1479
|
}
|
|
970
1480
|
var fillRuleOf = (path) => path.kind === "polygon" ? path.fillRule : "nonzero";
|
|
@@ -999,8 +1509,8 @@ function knife(path, cut, opts, budget) {
|
|
|
999
1509
|
chord.set(c1, run.slice().reverse());
|
|
1000
1510
|
}
|
|
1001
1511
|
if (partner.size === 0) return null;
|
|
1002
|
-
const { loops, untouched } =
|
|
1003
|
-
const traced = assembleRegions(
|
|
1512
|
+
const { loops: loops2, untouched } = traceLoops2(boundaries, partner, chord);
|
|
1513
|
+
const traced = assembleRegions(loops2, untouched.filter((c) => signedArea(c) < 0));
|
|
1004
1514
|
const islands = untouched.filter((c) => signedArea(c) > 0);
|
|
1005
1515
|
const minArea = negligibleArea(boundaries);
|
|
1006
1516
|
const regions = traced.filter((r) => Math.abs(regionArea(r)) > minArea);
|
|
@@ -1016,7 +1526,7 @@ function snipPathByPolyline(path, cut) {
|
|
|
1016
1526
|
const pts = distinctPoints(cut);
|
|
1017
1527
|
if (pts.length < 2) return null;
|
|
1018
1528
|
const rule = path.kind === "polygon" ? path.fillRule : "nonzero";
|
|
1019
|
-
const
|
|
1529
|
+
const runs2 = [];
|
|
1020
1530
|
const missed = [];
|
|
1021
1531
|
for (const sub of parseSubpaths(path, false)) {
|
|
1022
1532
|
const stops = snipStations(sub, pts);
|
|
@@ -1027,20 +1537,20 @@ function snipPathByPolyline(path, cut) {
|
|
|
1027
1537
|
const segs = sub.segs;
|
|
1028
1538
|
if (sub.closed) {
|
|
1029
1539
|
for (let i = 0; i < stops.length; i++) {
|
|
1030
|
-
|
|
1540
|
+
runs2.push(arcBetween(segs, stops[i], stops[(i + 1) % stops.length], stops.length === 1));
|
|
1031
1541
|
}
|
|
1032
1542
|
} else {
|
|
1033
1543
|
const last = segs[segs.length - 1];
|
|
1034
|
-
const
|
|
1544
|
+
const all2 = [
|
|
1035
1545
|
{ seg: 0, t: 0, pt: segs[0][0] },
|
|
1036
1546
|
...stops,
|
|
1037
1547
|
{ seg: segs.length - 1, t: 1, pt: last[last.length - 1] }
|
|
1038
1548
|
];
|
|
1039
|
-
for (let i = 0; i + 1 <
|
|
1549
|
+
for (let i = 0; i + 1 < all2.length; i++) runs2.push(arcBetween(segs, all2[i], all2[i + 1], false));
|
|
1040
1550
|
}
|
|
1041
1551
|
}
|
|
1042
|
-
if (
|
|
1043
|
-
const pieces =
|
|
1552
|
+
if (runs2.length === 0) return null;
|
|
1553
|
+
const pieces = runs2.filter((r) => r.length > 0).map((r) => writePath([{ segs: r, closed: false }], rule));
|
|
1044
1554
|
if (missed.length > 0) pieces.push(writePath(missed, rule));
|
|
1045
1555
|
return pieces;
|
|
1046
1556
|
}
|