@weasel-js/geom 1.9.3 → 1.9.4

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.
@@ -1,7 +1,7 @@
1
1
  import { elevateQuadraticToCubic, splitQuadraticAt, splitCubicAt } from '../chunk-URQMDR6Q.js';
2
- import { DEFAULT_FLATTEN_TOLERANCE, flattenCubic, segmentsCross, pointInPolygon } from '../chunk-RQYCDPU5.js';
3
- import '../chunk-7ROQRYOA.js';
4
- import { PATH_Z, PATH_Q, PATH_C, PATH_L, PATH_M } from '../chunk-HEZTRG2M.js';
2
+ import { DEFAULT_FLATTEN_TOLERANCE, flattenCubic, segmentsCross, pointInPolygon } from '../chunk-X6GJRIQD.js';
3
+ import { EPS } from '../chunk-N5SYTHMR.js';
4
+ import { PATH_Z, PATH_Q, PATH_C, PATH_L, PATH_M } from '../chunk-E4TJRGJD.js';
5
5
  import polygonClipping from 'polygon-clipping';
6
6
 
7
7
  // src/booleans/adapter.ts
@@ -21,6 +21,7 @@ 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
27
  const finalizeCurrent = () => {
@@ -40,14 +41,17 @@ function pathToMultiPolygon(path, opts = {}) {
40
41
  finalizeCurrent();
41
42
  cx = coords[ci];
42
43
  cy = coords[ci + 1];
44
+ startX = cx;
45
+ startY = cy;
43
46
  current = [[cx, cy]];
44
47
  ci += 2;
45
48
  break;
46
49
  }
47
50
  case PATH_L: {
51
+ current ??= [[cx, cy]];
48
52
  cx = coords[ci];
49
53
  cy = coords[ci + 1];
50
- if (current) current.push([cx, cy]);
54
+ current.push([cx, cy]);
51
55
  ci += 2;
52
56
  break;
53
57
  }
@@ -57,7 +61,8 @@ function pathToMultiPolygon(path, opts = {}) {
57
61
  const x3 = coords[ci + 4], y3 = coords[ci + 5];
58
62
  const out = [];
59
63
  flattenCubic(cx, cy, x1, y1, x2, y2, x3, y3, tolerance, out);
60
- if (current) for (let k = 0; k < out.length; k += 2) current.push([out[k], out[k + 1]]);
64
+ current ??= [[cx, cy]];
65
+ for (let k = 0; k < out.length; k += 2) current.push([out[k], out[k + 1]]);
61
66
  cx = x3;
62
67
  cy = y3;
63
68
  ci += 6;
@@ -69,7 +74,8 @@ function pathToMultiPolygon(path, opts = {}) {
69
74
  const [c1x, c1y, c2x, c2y] = elevateQuadraticToCubic(cx, cy, x1, y1, x2, y2);
70
75
  const out = [];
71
76
  flattenCubic(cx, cy, c1x, c1y, c2x, c2y, x2, y2, tolerance, out);
72
- if (current) for (let k = 0; k < out.length; k += 2) current.push([out[k], out[k + 1]]);
77
+ current ??= [[cx, cy]];
78
+ for (let k = 0; k < out.length; k += 2) current.push([out[k], out[k + 1]]);
73
79
  cx = x2;
74
80
  cy = y2;
75
81
  ci += 4;
@@ -77,6 +83,8 @@ function pathToMultiPolygon(path, opts = {}) {
77
83
  }
78
84
  case PATH_Z: {
79
85
  finalizeCurrent();
86
+ cx = startX;
87
+ cy = startY;
80
88
  break;
81
89
  }
82
90
  default:
@@ -275,261 +283,22 @@ function pathCrop(...paths) {
275
283
  }
276
284
  return out;
277
285
  }
278
-
279
- // src/booleans/splitBySegment.ts
280
- function splitPathBySegment(path, a, b, opts = {}) {
281
- return splitPathByPolyline(path, [a, b], opts);
282
- }
283
- function splitPathByPolyline(path, cut, opts = {}) {
284
- let pts = distinctPoints(cut);
285
- if (pts.length < 2) return null;
286
- const outRule = fillRuleOf(path);
287
- const boundaries = resolveBoundaries(path, opts);
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));
296
- }
297
- var fillRuleOf = (path) => path.kind === "polygon" ? path.fillRule : "nonzero";
298
- function resolveBoundaries(path, opts) {
299
- const contours = parseContours(path);
300
- if (contoursIntersect(contours, opts.flattenTolerance ?? 0.5)) {
301
- return orientBoundaries(parseContours(pathUnion(path)), "nonzero");
302
- }
303
- return orientBoundaries(contours, fillRuleOf(path));
304
- }
305
- function knife(path, cut, opts, budget) {
306
- const outRule = fillRuleOf(path);
307
- const boundaries = resolveBoundaries(path, opts);
308
- if (boundaries.length === 0) return null;
309
- const crossings = findCrossings(boundaries, cut);
310
- const partner = /* @__PURE__ */ new Map();
311
- const chord = /* @__PURE__ */ new Map();
312
- const accepted = [];
313
- let deferred = false;
314
- for (let k = 0; k + 1 < crossings.length; k++) {
315
- const c0 = crossings[k], c1 = crossings[k + 1];
316
- if (!c0.enters || c1.enters || c1.s - c0.s <= 1e-9) continue;
317
- const run = splitLoops(cutBetween(cut, c0, c1)).spine;
318
- if (accepted.some((other) => polylinesCross(run, other))) {
319
- deferred = true;
320
- continue;
321
- }
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
- }
328
- if (partner.size === 0) return null;
329
- const { loops, untouched } = traceLoops(boundaries, partner, chord);
330
- const traced = assembleRegions(loops, untouched.filter((c) => signedArea(c) < 0));
331
- const islands = untouched.filter((c) => signedArea(c) > 0);
332
- const regions = traced.filter((r) => Math.abs(regionArea(r)) > 1e-6);
333
- if (islands.length > 0) {
334
- const leftover = untouched.filter((c) => !traced.some((r) => r.includes(c)));
335
- regions.push(leftover);
336
- }
337
- const pieces = regions.map((r) => toPath(r, outRule));
338
- if (!deferred || budget <= 1) return pieces;
339
- return pieces.flatMap((p) => knife(p, cut, opts, budget - 1) ?? [p]);
340
- }
341
- function snipPathByPolyline(path, cut) {
342
- const pts = distinctPoints(cut);
343
- if (pts.length < 2) return null;
344
- const rule = path.kind === "polygon" ? path.fillRule : "nonzero";
345
- const runs = [];
346
- const missed = [];
347
- for (const sub of parseSubpaths(path, false)) {
348
- const stops = snipStations(sub, pts);
349
- if (stops.length === 0) {
350
- missed.push(sub);
351
- continue;
352
- }
353
- const segs = sub.segs;
354
- if (sub.closed) {
355
- for (let i = 0; i < stops.length; i++) {
356
- runs.push(arcBetween(segs, stops[i], stops[(i + 1) % stops.length], stops.length === 1));
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));
366
- }
367
- }
368
- if (runs.length === 0) return null;
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;
372
- }
373
- function snipStations(sub, cut) {
374
- const raw = [];
375
- sub.segs.forEach((seg, si) => {
376
- for (let k = 0; k + 1 < cut.length; k++) {
377
- const a = cut[k];
378
- const d = { x: cut[k + 1].x - a.x, y: cut[k + 1].y - a.y };
379
- for (const c of signChanges(sideCoeffs(seg, a, d), true)) {
380
- const pt = evalSeg(seg, c.t);
381
- if (cutPosition(cut, k, pt) !== null) raw.push({ seg: si, t: c.t, pt });
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]);
392
- }
393
- if (sub.closed && groups.length > 1 && same(groups[0][0].pt, groups[groups.length - 1][0].pt)) {
394
- groups[0].push(...groups.pop());
395
- }
396
- const first = sub.segs[0][0];
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));
400
- }
401
- function distinctPoints(cut) {
402
- const out = [];
403
- for (const p of cut) {
404
- const q = out[out.length - 1];
405
- if (!q || q.x !== p.x || q.y !== p.y) out.push({ x: p.x, y: p.y });
406
- }
407
- return out;
408
- }
409
- function cutBetween(cut, from, to) {
410
- const out = [from.pt];
411
- for (let v = Math.floor(from.s) + 1; v < to.s; v++) out.push(cut[v]);
412
- out.push(to.pt);
413
- return out;
414
- }
415
- function properCrossing(p0, p1, q0, q1) {
416
- const r = { x: p1.x - p0.x, y: p1.y - p0.y };
417
- const s = { x: q1.x - q0.x, y: q1.y - q0.y };
418
- const den = r.x * s.y - r.y * s.x;
419
- if (den === 0) return null;
420
- const qp = { x: q0.x - p0.x, y: q0.y - p0.y };
421
- const t = (qp.x * s.y - qp.y * s.x) / den;
422
- const u = (qp.x * r.y - qp.y * r.x) / den;
423
- const eps = 1e-9;
424
- if (t <= eps || t >= 1 - eps || u <= eps || u >= 1 - eps) return null;
425
- return { x: p0.x + t * r.x, y: p0.y + t * r.y };
426
- }
427
- function splitLoops(run) {
428
- const spine = [run[0]];
429
- const loops = [];
430
- for (let i = 1; i < run.length; i++) {
431
- const to = run[i];
432
- for (; ; ) {
433
- const from = spine[spine.length - 1];
434
- let hit = null;
435
- for (let j = 0; j + 2 < spine.length; j++) {
436
- const x = properCrossing(from, to, spine[j], spine[j + 1]);
437
- if (!x) continue;
438
- const d = Math.hypot(x.x - from.x, x.y - from.y);
439
- if (!hit || d < hit.d) hit = { j, x, d };
440
- }
441
- if (!hit) {
442
- spine.push(to);
443
- break;
444
- }
445
- loops.push([hit.x, ...spine.slice(hit.j + 1)]);
446
- spine.length = hit.j + 1;
447
- spine.push(hit.x);
448
- }
449
- }
450
- const last = spine[spine.length - 1];
451
- if (spine.length >= 4 && last.x === spine[0].x && last.y === spine[0].y) {
452
- loops.push(spine.slice(0, -1));
453
- return { spine: [], loops };
454
- }
455
- return { spine, loops };
456
- }
457
- function pointAt(cut, s) {
458
- const k = Math.min(Math.floor(s), cut.length - 2);
459
- const f = s - k;
460
- const a = cut[k], b = cut[k + 1];
461
- return { x: a.x + f * (b.x - a.x), y: a.y + f * (b.y - a.y) };
462
- }
463
- function openClosedCut(cut, boundaries) {
464
- const n = cut.length - 1;
465
- if (n < 3 || cut[0].x !== cut[n].x || cut[0].y !== cut[n].y) return cut;
466
- const crossings = findCrossings(boundaries, cut);
467
- const k = crossings.findIndex((c) => !c.enters);
468
- if (k < 0) return cut;
469
- const next = k + 1 < crossings.length ? crossings[k + 1].s : crossings[0].s + n;
470
- const s = (crossings[k].s + next) / 2 % n;
471
- const seg = Math.min(Math.floor(s), n - 1);
472
- const start = pointAt(cut, s);
473
- const ring = cut.slice(0, n);
474
- return distinctPoints([start, ...ring.slice(seg + 1), ...ring.slice(0, seg + 1), start]);
475
- }
476
- function interiorLoops(cut, boundaries) {
477
- const crossings = findCrossings(boundaries, cut);
478
- const chords = [];
479
- for (let k = 0; k + 1 < crossings.length; k++) {
480
- const c0 = crossings[k], c1 = crossings[k + 1];
481
- if (c0.enters && !c1.enters && c1.s - c0.s > 1e-9) chords.push(splitLoops(cutBetween(cut, c0, c1)).spine);
482
- }
483
- const last = cut.length - 1;
484
- const stops = [{ s: 0, pt: cut[0] }, ...crossings, { s: last, pt: cut[last] }];
485
- const rings = [];
486
- const loops = [];
487
- for (let i = 0; i + 1 < stops.length; i++) {
488
- const a = stops[i], b = stops[i + 1];
489
- if (b.s - a.s <= 1e-9) continue;
490
- if (windingAt(pointAt(cut, (a.s + b.s) / 2), boundaries) === 0) continue;
491
- const run = [a.pt];
492
- for (let v = Math.floor(a.s) + 1; v < b.s; v++) run.push(cut[v]);
493
- run.push(b.pt);
494
- for (const loop of splitLoops(distinctPoints(run)).loops) {
495
- const ring = [...loop, loop[0]];
496
- if (chords.some((c) => polylinesCross(ring, c)) || rings.some((r) => polylinesCross(ring, r))) continue;
497
- rings.push(ring);
498
- loops.push(loop);
499
- }
500
- }
501
- return loops;
502
- }
503
- function cutOutLoops(regions, loops) {
504
- let cut = false;
505
- for (const loop of loops) {
506
- let ring = loop.map((p, i) => [p, loop[(i + 1) % loop.length]]);
507
- const a = signedArea(ring);
508
- if (Math.abs(a) <= 1e-6) continue;
509
- if (a < 0) ring = reverseContour(ring);
510
- const probe = evalSeg(ring[0], 0.5);
511
- const at = regions.findIndex((r) => windingAt(probe, r) !== 0);
512
- if (at < 0) continue;
513
- const samples = sampleContour(ring);
514
- const within = (c) => {
515
- const p = evalSeg(c[0], 0.5);
516
- return pointInPolygon(samples, p.x, p.y);
517
- };
518
- const region = regions[at];
519
- regions[at] = [...region.filter((c) => !within(c)), reverseContour(ring)];
520
- regions.push([ring, ...region.filter(within)]);
521
- cut = true;
522
- }
523
- return cut;
524
- }
525
- function polylinesCross(a, b) {
526
- for (let i = 0; i + 1 < a.length; i++) {
527
- for (let j = 0; j + 1 < b.length; j++) {
528
- if (properCrossing(a[i], a[i + 1], b[j], b[j + 1])) return true;
529
- }
530
- }
531
- return false;
286
+ function trianglesToPath(vertices, indices) {
287
+ const triangles = [];
288
+ for (let i = 0; i + 2 < indices.length; i += 3) {
289
+ const a = indices[i] * 2, b = indices[i + 1] * 2, c = indices[i + 2] * 2;
290
+ const ax = vertices[a], ay = vertices[a + 1];
291
+ const bx = vertices[b], by = vertices[b + 1];
292
+ const cx = vertices[c], cy = vertices[c + 1];
293
+ if ((bx - ax) * (cy - ay) - (cx - ax) * (by - ay) === 0) continue;
294
+ triangles.push([[[[ax, ay], [bx, by], [cx, cy], [ax, ay]]]]);
295
+ }
296
+ if (triangles.length === 0) return multiPolygonToPath([]);
297
+ const [head, ...rest] = triangles;
298
+ return multiPolygonToPath(polygonClipping.union(head, ...rest));
532
299
  }
300
+
301
+ // src/booleans/contourSegments.ts
533
302
  function evalSeg(s, t) {
534
303
  const u = 1 - t;
535
304
  if (s.length === 2) return { x: u * s[0].x + t * s[1].x, y: u * s[0].y + t * s[1].y };
@@ -663,9 +432,51 @@ function endSide(f, fromStart) {
663
432
  }
664
433
  return 0;
665
434
  }
666
- function parseContours(path) {
667
- return parseSubpaths(path).map((s) => s.segs);
668
- }
435
+
436
+ // src/booleans/cutCrossings.ts
437
+ function cutPosition(cut, k, pt) {
438
+ const a = cut[k], b = cut[k + 1];
439
+ const d = { x: b.x - a.x, y: b.y - a.y };
440
+ const s = ((pt.x - a.x) * d.x + (pt.y - a.y) * d.y) / (d.x * d.x + d.y * d.y);
441
+ const eps = 1e-9;
442
+ const last = k + 2 === cut.length;
443
+ if (s < (k === 0 ? 0 : -eps)) return null;
444
+ if (last ? s > 1 : s >= 1 - eps) return null;
445
+ return k + Math.min(1, Math.max(0, s));
446
+ }
447
+ function findCrossings(boundaries, cut) {
448
+ const out = [];
449
+ for (let k = 0; k + 1 < cut.length; k++) crossingsOnSegment(boundaries, cut, k, out);
450
+ return out.sort((x, y) => x.s - y.s);
451
+ }
452
+ function crossingsOnSegment(boundaries, cut, k, out) {
453
+ const a = cut[k];
454
+ const d = { x: cut[k + 1].x - a.x, y: cut[k + 1].y - a.y };
455
+ const push = (contour, seg, t, before, after) => {
456
+ const pt = evalSeg(boundaries[contour][seg], t);
457
+ const s = cutPosition(cut, k, pt);
458
+ if (s === null) return;
459
+ out.push({ contour, seg, t, pt, s, enters: before > 0 && after < 0 });
460
+ };
461
+ boundaries.forEach((contour, ci) => {
462
+ const fs = contour.map((s) => sideCoeffs(s, a, d));
463
+ const n = contour.length;
464
+ fs.forEach((f, si) => {
465
+ for (const c of signChanges(f, false)) push(ci, si, c.t, c.before, c.after);
466
+ if (f[0] !== 0) return;
467
+ const after = endSide(f, true);
468
+ if (after === 0) return;
469
+ let before = 0;
470
+ for (let k2 = 1; k2 <= n && before === 0; k2++) before = endSide(fs[(si - k2 + n) % n], false);
471
+ if (before !== 0 && before !== after) push(ci, si, 0, before, after);
472
+ });
473
+ });
474
+ }
475
+
476
+ // src/booleans/contours.ts
477
+ function parseContours(path) {
478
+ return parseSubpaths(path).map((s) => s.segs);
479
+ }
669
480
  function parseSubpaths(path, implicitClose = true) {
670
481
  if (path.kind === "rect") {
671
482
  const { x, y, width: w, height: h } = path;
@@ -755,11 +566,7 @@ function windingAt(q, contours) {
755
566
  }
756
567
  return w;
757
568
  }
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
- };
569
+ function extentOf(contours) {
763
570
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
764
571
  for (const c of contours) for (const s of c) for (const p of s) {
765
572
  minX = Math.min(minX, p.x);
@@ -767,7 +574,18 @@ function orientBoundaries(contours, fillRule) {
767
574
  minY = Math.min(minY, p.y);
768
575
  maxY = Math.max(maxY, p.y);
769
576
  }
770
- const eps = Math.max(Math.hypot(maxX - minX, maxY - minY) * 1e-6, 1e-9);
577
+ return Math.hypot(maxX - minX, maxY - minY);
578
+ }
579
+ function negligibleArea(contours) {
580
+ const extent = extentOf(contours);
581
+ return extent * extent * EPS * EPS;
582
+ }
583
+ function orientBoundaries(contours, fillRule) {
584
+ const filled = (q) => {
585
+ const w = windingAt(q, contours);
586
+ return fillRule === "evenodd" ? (w & 1) !== 0 : w !== 0;
587
+ };
588
+ const eps = Math.max(extentOf(contours) * 1e-6, 1e-9);
771
589
  const out = [];
772
590
  for (const contour of contours) {
773
591
  let probe = contour[0];
@@ -791,44 +609,143 @@ function orientBoundaries(contours, fillRule) {
791
609
  }
792
610
  return out;
793
611
  }
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);
612
+
613
+ // src/booleans/cutPolyline.ts
614
+ function distinctPoints(cut) {
615
+ const out = [];
616
+ for (const p of cut) {
617
+ const q = out[out.length - 1];
618
+ if (!q || q.x !== p.x || q.y !== p.y) out.push({ x: p.x, y: p.y });
619
+ }
620
+ return out;
621
+ }
622
+ function cutBetween(cut, from, to) {
623
+ const out = [from.pt];
624
+ for (let v = Math.floor(from.s) + 1; v < to.s; v++) out.push(cut[v]);
625
+ out.push(to.pt);
626
+ return out;
627
+ }
628
+ function properCrossing(p0, p1, q0, q1) {
629
+ const r = { x: p1.x - p0.x, y: p1.y - p0.y };
630
+ const s = { x: q1.x - q0.x, y: q1.y - q0.y };
631
+ const den = r.x * s.y - r.y * s.x;
632
+ if (den === 0) return null;
633
+ const qp = { x: q0.x - p0.x, y: q0.y - p0.y };
634
+ const t = (qp.x * s.y - qp.y * s.x) / den;
635
+ const u = (qp.x * r.y - qp.y * r.x) / den;
798
636
  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));
637
+ if (t <= eps || t >= 1 - eps || u <= eps || u >= 1 - eps) return null;
638
+ return { x: p0.x + t * r.x, y: p0.y + t * r.y };
803
639
  }
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);
640
+ function splitLoops(run) {
641
+ const spine = [run[0]];
642
+ const loops = [];
643
+ for (let i = 1; i < run.length; i++) {
644
+ const to = run[i];
645
+ for (; ; ) {
646
+ const from = spine[spine.length - 1];
647
+ let hit = null;
648
+ for (let j = 0; j + 2 < spine.length; j++) {
649
+ const x = properCrossing(from, to, spine[j], spine[j + 1]);
650
+ if (!x) continue;
651
+ const d = Math.hypot(x.x - from.x, x.y - from.y);
652
+ if (!hit || d < hit.d) hit = { j, x, d };
653
+ }
654
+ if (!hit) {
655
+ spine.push(to);
656
+ break;
657
+ }
658
+ loops.push([hit.x, ...spine.slice(hit.j + 1)]);
659
+ spine.length = hit.j + 1;
660
+ spine.push(hit.x);
661
+ }
662
+ }
663
+ const last = spine[spine.length - 1];
664
+ if (spine.length >= 4 && last.x === spine[0].x && last.y === spine[0].y) {
665
+ loops.push(spine.slice(0, -1));
666
+ return { spine: [], loops };
667
+ }
668
+ return { spine, loops };
808
669
  }
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
- });
670
+ function pointAt(cut, s) {
671
+ const k = Math.min(Math.floor(s), cut.length - 2);
672
+ const f = s - k;
673
+ const a = cut[k], b = cut[k + 1];
674
+ return { x: a.x + f * (b.x - a.x), y: a.y + f * (b.y - a.y) };
675
+ }
676
+ function openClosedCut(cut, boundaries) {
677
+ const n = cut.length - 1;
678
+ if (n < 3 || cut[0].x !== cut[n].x || cut[0].y !== cut[n].y) return cut;
679
+ const crossings = findCrossings(boundaries, cut);
680
+ const k = crossings.findIndex((c) => !c.enters);
681
+ if (k < 0) return cut;
682
+ const next = k + 1 < crossings.length ? crossings[k + 1].s : crossings[0].s + n;
683
+ const s = (crossings[k].s + next) / 2 % n;
684
+ const seg = Math.min(Math.floor(s), n - 1);
685
+ const start = pointAt(cut, s);
686
+ const ring = cut.slice(0, n);
687
+ return distinctPoints([start, ...ring.slice(seg + 1), ...ring.slice(0, seg + 1), start]);
688
+ }
689
+ function interiorLoops(cut, boundaries) {
690
+ const crossings = findCrossings(boundaries, cut);
691
+ const chords = [];
692
+ for (let k = 0; k + 1 < crossings.length; k++) {
693
+ const c0 = crossings[k], c1 = crossings[k + 1];
694
+ if (c0.enters && !c1.enters && c1.s - c0.s > 1e-9) chords.push(splitLoops(cutBetween(cut, c0, c1)).spine);
695
+ }
696
+ const last = cut.length - 1;
697
+ const stops = [{ s: 0, pt: cut[0] }, ...crossings, { s: last, pt: cut[last] }];
698
+ const rings = [];
699
+ const loops = [];
700
+ for (let i = 0; i + 1 < stops.length; i++) {
701
+ const a = stops[i], b = stops[i + 1];
702
+ if (b.s - a.s <= 1e-9) continue;
703
+ if (windingAt(pointAt(cut, (a.s + b.s) / 2), boundaries) === 0) continue;
704
+ const run = [a.pt];
705
+ for (let v = Math.floor(a.s) + 1; v < b.s; v++) run.push(cut[v]);
706
+ run.push(b.pt);
707
+ for (const loop of splitLoops(distinctPoints(run)).loops) {
708
+ const ring = [...loop, loop[0]];
709
+ if (chords.some((c) => polylinesCross(ring, c)) || rings.some((r) => polylinesCross(ring, r))) continue;
710
+ rings.push(ring);
711
+ loops.push(loop);
712
+ }
713
+ }
714
+ return loops;
831
715
  }
716
+ function cutOutLoops(regions, loops) {
717
+ let cut = false;
718
+ const minArea = negligibleArea(regions.flat());
719
+ for (const loop of loops) {
720
+ let ring = loop.map((p, i) => [p, loop[(i + 1) % loop.length]]);
721
+ const a = signedArea(ring);
722
+ if (Math.abs(a) <= minArea) continue;
723
+ if (a < 0) ring = reverseContour(ring);
724
+ const probe = evalSeg(ring[0], 0.5);
725
+ const at = regions.findIndex((r) => windingAt(probe, r) !== 0);
726
+ if (at < 0) continue;
727
+ const samples = sampleContour(ring);
728
+ const within = (c) => {
729
+ const p = evalSeg(c[0], 0.5);
730
+ return pointInPolygon(samples, p.x, p.y);
731
+ };
732
+ const region = regions[at];
733
+ regions[at] = [...region.filter((c) => !within(c)), reverseContour(ring)];
734
+ regions.push([ring, ...region.filter(within)]);
735
+ cut = true;
736
+ }
737
+ return cut;
738
+ }
739
+ function polylinesCross(a, b) {
740
+ for (let i = 0; i + 1 < a.length; i++) {
741
+ for (let j = 0; j + 1 < b.length; j++) {
742
+ if (properCrossing(a[i], a[i + 1], b[j], b[j + 1])) return true;
743
+ }
744
+ }
745
+ return false;
746
+ }
747
+
748
+ // src/booleans/cutLoops.ts
832
749
  function traceLoops(boundaries, partner, chord) {
833
750
  const byContour = /* @__PURE__ */ new Map();
834
751
  for (const c of partner.keys()) {
@@ -885,6 +802,8 @@ function arcBetween(contour, from, to, whole) {
885
802
  add(subSeg(last, 0, to.t, last[0], to.pt));
886
803
  return out;
887
804
  }
805
+
806
+ // src/booleans/cutRegions.ts
888
807
  function assembleRegions(outers, holes) {
889
808
  const regions = outers.map((o) => ({ loops: [o], ring: sampleContour(o), area: signedArea(o) }));
890
809
  for (const h of holes) {
@@ -914,6 +833,131 @@ function writePath(subpaths, fillRule) {
914
833
  return { kind: "polygon", commands: new Uint8Array(cmds), coords: new Float32Array(xs), fillRule };
915
834
  }
916
835
 
917
- export { multiPolygonToPath, pathCrop, pathDivide, pathExclude, pathIntersect, pathSubtract, pathToMultiPolygon, pathUnion, snipPathByPolyline, splitPathByPolyline, splitPathBySegment };
836
+ // src/booleans/splitBySegment.ts
837
+ function splitPathBySegment(path, a, b, opts = {}) {
838
+ return splitPathByPolyline(path, [a, b], opts);
839
+ }
840
+ function splitPathByPolyline(path, cut, opts = {}) {
841
+ let pts = distinctPoints(cut);
842
+ if (pts.length < 2) return null;
843
+ const outRule = fillRuleOf(path);
844
+ const boundaries = resolveBoundaries(path, opts);
845
+ if (boundaries.length === 0) return null;
846
+ pts = openClosedCut(pts, boundaries);
847
+ const loops = interiorLoops(pts, boundaries);
848
+ const pieces = knife(path, pts, opts, pts.length);
849
+ if (loops.length === 0) return pieces;
850
+ const regions = pieces ? pieces.map((p) => orientBoundaries(parseContours(p), outRule)) : [boundaries];
851
+ if (!cutOutLoops(regions, loops)) return pieces;
852
+ return regions.map((r) => toPath(r, outRule));
853
+ }
854
+ var fillRuleOf = (path) => path.kind === "polygon" ? path.fillRule : "nonzero";
855
+ function resolveBoundaries(path, opts) {
856
+ const contours = parseContours(path);
857
+ if (contoursIntersect(contours, opts.flattenTolerance ?? 0.5)) {
858
+ return orientBoundaries(parseContours(pathUnion(path)), "nonzero");
859
+ }
860
+ return orientBoundaries(contours, fillRuleOf(path));
861
+ }
862
+ function knife(path, cut, opts, budget) {
863
+ const outRule = fillRuleOf(path);
864
+ const boundaries = resolveBoundaries(path, opts);
865
+ if (boundaries.length === 0) return null;
866
+ const crossings = findCrossings(boundaries, cut);
867
+ const partner = /* @__PURE__ */ new Map();
868
+ const chord = /* @__PURE__ */ new Map();
869
+ const accepted = [];
870
+ let deferred = false;
871
+ for (let k = 0; k + 1 < crossings.length; k++) {
872
+ const c0 = crossings[k], c1 = crossings[k + 1];
873
+ if (!c0.enters || c1.enters || c1.s - c0.s <= 1e-9) continue;
874
+ const run = splitLoops(cutBetween(cut, c0, c1)).spine;
875
+ if (accepted.some((other) => polylinesCross(run, other))) {
876
+ deferred = true;
877
+ continue;
878
+ }
879
+ accepted.push(run);
880
+ partner.set(c0, c1);
881
+ partner.set(c1, c0);
882
+ chord.set(c0, run);
883
+ chord.set(c1, run.slice().reverse());
884
+ }
885
+ if (partner.size === 0) return null;
886
+ const { loops, untouched } = traceLoops(boundaries, partner, chord);
887
+ const traced = assembleRegions(loops, untouched.filter((c) => signedArea(c) < 0));
888
+ const islands = untouched.filter((c) => signedArea(c) > 0);
889
+ const minArea = negligibleArea(boundaries);
890
+ const regions = traced.filter((r) => Math.abs(regionArea(r)) > minArea);
891
+ if (islands.length > 0) {
892
+ const leftover = untouched.filter((c) => !traced.some((r) => r.includes(c)));
893
+ regions.push(leftover);
894
+ }
895
+ const pieces = regions.map((r) => toPath(r, outRule));
896
+ if (!deferred || budget <= 1) return pieces;
897
+ return pieces.flatMap((p) => knife(p, cut, opts, budget - 1) ?? [p]);
898
+ }
899
+ function snipPathByPolyline(path, cut) {
900
+ const pts = distinctPoints(cut);
901
+ if (pts.length < 2) return null;
902
+ const rule = path.kind === "polygon" ? path.fillRule : "nonzero";
903
+ const runs = [];
904
+ const missed = [];
905
+ for (const sub of parseSubpaths(path, false)) {
906
+ const stops = snipStations(sub, pts);
907
+ if (stops.length === 0) {
908
+ missed.push(sub);
909
+ continue;
910
+ }
911
+ const segs = sub.segs;
912
+ if (sub.closed) {
913
+ for (let i = 0; i < stops.length; i++) {
914
+ runs.push(arcBetween(segs, stops[i], stops[(i + 1) % stops.length], stops.length === 1));
915
+ }
916
+ } else {
917
+ const last = segs[segs.length - 1];
918
+ const all = [
919
+ { seg: 0, t: 0, pt: segs[0][0] },
920
+ ...stops,
921
+ { seg: segs.length - 1, t: 1, pt: last[last.length - 1] }
922
+ ];
923
+ for (let i = 0; i + 1 < all.length; i++) runs.push(arcBetween(segs, all[i], all[i + 1], false));
924
+ }
925
+ }
926
+ if (runs.length === 0) return null;
927
+ const pieces = runs.filter((r) => r.length > 0).map((r) => writePath([{ segs: r, closed: false }], rule));
928
+ if (missed.length > 0) pieces.push(writePath(missed, rule));
929
+ return pieces;
930
+ }
931
+ function snipStations(sub, cut) {
932
+ const raw = [];
933
+ sub.segs.forEach((seg, si) => {
934
+ for (let k = 0; k + 1 < cut.length; k++) {
935
+ const a = cut[k];
936
+ const d = { x: cut[k + 1].x - a.x, y: cut[k + 1].y - a.y };
937
+ for (const c of signChanges(sideCoeffs(seg, a, d), true)) {
938
+ const pt = evalSeg(seg, c.t);
939
+ if (cutPosition(cut, k, pt) !== null) raw.push({ seg: si, t: c.t, pt });
940
+ }
941
+ }
942
+ });
943
+ raw.sort((x, y) => x.seg - y.seg || x.t - y.t);
944
+ const tol = extentOf([sub.segs]) * 1e-9;
945
+ const same = (p, q) => Math.abs(p.x - q.x) <= tol && Math.abs(p.y - q.y) <= tol;
946
+ const groups = [];
947
+ for (const st of raw) {
948
+ const g = groups[groups.length - 1];
949
+ if (g && same(g[0].pt, st.pt)) g.push(st);
950
+ else groups.push([st]);
951
+ }
952
+ if (sub.closed && groups.length > 1 && same(groups[0][0].pt, groups[groups.length - 1][0].pt)) {
953
+ groups[0].push(...groups.pop());
954
+ }
955
+ const first = sub.segs[0][0];
956
+ const lastSeg = sub.segs[sub.segs.length - 1];
957
+ const end = lastSeg[lastSeg.length - 1];
958
+ return groups.filter((g) => g.length % 2 === 1).map((g) => g[0]).filter((st) => sub.closed || !same(st.pt, first) && !same(st.pt, end));
959
+ }
960
+
961
+ export { multiPolygonToPath, pathCrop, pathDivide, pathExclude, pathIntersect, pathSubtract, pathToMultiPolygon, pathUnion, snipPathByPolyline, splitPathByPolyline, splitPathBySegment, trianglesToPath };
918
962
  //# sourceMappingURL=index.js.map
919
963
  //# sourceMappingURL=index.js.map