@weasel-js/geom 1.11.0 → 1.12.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.
@@ -60,7 +60,9 @@ declare function pathCropPieces(...paths: Path[]): BooleanPiece[];
60
60
  /**
61
61
  * Union the triangles of an indexed mesh (`vertices` as x, y pairs; `indices`
62
62
  * three per triangle) into canonical, non-overlapping rings. Overlaps count
63
- * once, and triangles with no area are skipped. The result holds only M/L/Z.
63
+ * once, triangles with no area are skipped, and a vertex lying on another
64
+ * triangle's edge joins it there rather than leaving a crack. The result
65
+ * holds only M/L/Z.
64
66
  */
65
67
  declare function trianglesToPath(vertices: ArrayLike<number>, indices: ArrayLike<number>): PolygonPath;
66
68
 
@@ -1,6 +1,6 @@
1
- import { orientBoundaries, parseContours, contoursIntersect, assembleRegions, signedArea, negligibleArea, regionArea, toPath, parseSubpaths, writePath, signChanges, sideCoeffs, evalSeg, extentOf, keepCorners, windingAt, reverseContour, sampleContour, subSeg, endSide } from '../chunk-7KLJZFBY.js';
1
+ import { orientBoundaries, parseContours, contoursIntersect, assembleRegions, signedArea, negligibleArea, regionArea, toPath, parseSubpaths, writePath, signChanges, sideCoeffs, evalSeg, extentOf, keepCorners, windingAt, reverseContour, sampleContour, subSeg, endSide } from '../chunk-YPWFGFQX.js';
2
2
  import '../chunk-CE7IY7TB.js';
3
- import { resolveCorners, DEFAULT_FLATTEN_TOLERANCE, fillsOpenSubpaths, flattenCubic, flattenQuadratic, pointInPolygon } from '../chunk-J2O2B54M.js';
3
+ import { resolveCorners, DEFAULT_FLATTEN_TOLERANCE, fillsOpenSubpaths, flattenCubic, flattenQuadratic, pointInPolygon } from '../chunk-2PKPKU4P.js';
4
4
  import '../chunk-755Y4YZS.js';
5
5
  import { PATH_Z, PATH_Q, elevateQuadraticToCubic, PATH_C, PATH_L, PATH_M } from '../chunk-B53BERKG.js';
6
6
  import polygonClipping from 'polygon-clipping';
@@ -574,19 +574,146 @@ function pathCropPieces(...paths) {
574
574
  }
575
575
  return out;
576
576
  }
577
+
578
+ // src/booleans/meshJunctions.ts
579
+ var ON_EDGE = 2 ** -20;
580
+ function junctionRings(vertices, triangles) {
581
+ const n = vertices.length / 2;
582
+ let scale = 0, minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
583
+ for (let i = 0; i < n; i++) {
584
+ const x = vertices[i * 2], y = vertices[i * 2 + 1];
585
+ scale = Math.max(scale, Math.abs(x), Math.abs(y));
586
+ minX = Math.min(minX, x);
587
+ minY = Math.min(minY, y);
588
+ maxX = Math.max(maxX, x);
589
+ maxY = Math.max(maxY, y);
590
+ }
591
+ const eps = scale * ON_EDGE;
592
+ const cell = Math.max(maxX - minX, maxY - minY, eps) / Math.max(1, Math.sqrt(n));
593
+ const grid = /* @__PURE__ */ new Map();
594
+ const key = (cx, cy) => `${cx},${cy}`;
595
+ for (const i of new Set(triangles)) {
596
+ const k = key(Math.floor((vertices[i * 2] - minX) / cell), Math.floor((vertices[i * 2 + 1] - minY) / cell));
597
+ const bucket = grid.get(k);
598
+ if (bucket) bucket.push(i);
599
+ else grid.set(k, [i]);
600
+ }
601
+ const onEdge = (p, q) => {
602
+ const px = vertices[p * 2], py = vertices[p * 2 + 1];
603
+ const dx = vertices[q * 2] - px, dy = vertices[q * 2 + 1] - py;
604
+ const len2 = dx * dx + dy * dy;
605
+ if (len2 === 0) return [];
606
+ const len = Math.sqrt(len2);
607
+ const hits = [];
608
+ const x0 = Math.floor((Math.min(px, px + dx) - eps - minX) / cell), x1 = Math.floor((Math.max(px, px + dx) + eps - minX) / cell);
609
+ const y0 = Math.floor((Math.min(py, py + dy) - eps - minY) / cell), y1 = Math.floor((Math.max(py, py + dy) + eps - minY) / cell);
610
+ for (let cx = x0; cx <= x1; cx++) {
611
+ for (let cy = y0; cy <= y1; cy++) {
612
+ for (const i of grid.get(key(cx, cy)) ?? []) {
613
+ const vx = vertices[i * 2] - px, vy = vertices[i * 2 + 1] - py;
614
+ const t = (vx * dx + vy * dy) / len2;
615
+ if (t * len <= eps || (1 - t) * len <= eps) continue;
616
+ if (Math.abs(vx * dy - vy * dx) / len <= eps) hits.push({ i, t });
617
+ }
618
+ }
619
+ }
620
+ return hits.sort((a, b) => a.t - b.t).map((h) => h.i);
621
+ };
622
+ const rings = [];
623
+ for (let k = 0; k + 2 < triangles.length; k += 3) {
624
+ const ring = [];
625
+ for (let e = 0; e < 3; e++) {
626
+ const p = triangles[k + e], q = triangles[k + (e + 1) % 3];
627
+ for (const i of [p, ...onEdge(p, q)]) ring.push([vertices[i * 2], vertices[i * 2 + 1]]);
628
+ }
629
+ ring.push(ring[0]);
630
+ rings.push(ring);
631
+ }
632
+ return rings;
633
+ }
634
+
635
+ // src/booleans/meshOutline.ts
636
+ function meshOutline(rings) {
637
+ const points = /* @__PURE__ */ new Map();
638
+ const edges = /* @__PURE__ */ new Map();
639
+ const keyOf = (p) => {
640
+ const k = `${p[0]},${p[1]}`;
641
+ if (!points.has(k)) points.set(k, p);
642
+ return k;
643
+ };
644
+ for (const ring of rings) {
645
+ const keys = ring.slice(0, -1).map(keyOf);
646
+ if (ringArea(ring) < 0) keys.reverse();
647
+ for (let i = 0; i < keys.length; i++) {
648
+ const a = keys[i], b = keys[(i + 1) % keys.length];
649
+ if (a === b) continue;
650
+ const back = `${b}|${a}`;
651
+ const n = edges.get(back);
652
+ if (n) {
653
+ if (n === 1) edges.delete(back);
654
+ else edges.set(back, n - 1);
655
+ } else {
656
+ const fwd = `${a}|${b}`;
657
+ edges.set(fwd, (edges.get(fwd) ?? 0) + 1);
658
+ }
659
+ }
660
+ }
661
+ const loops = walkLoops(edges).map((loop) => loop.map((k) => points.get(k)));
662
+ return loops.length === 0 ? null : bridge(loops);
663
+ }
664
+ function walkLoops(edges) {
665
+ const out = /* @__PURE__ */ new Map();
666
+ for (const [edge, n] of edges) {
667
+ const [a, b] = edge.split("|");
668
+ let list = out.get(a);
669
+ if (!list) out.set(a, list = []);
670
+ for (let i = 0; i < n; i++) list.push(b);
671
+ }
672
+ const loops = [];
673
+ for (const start of out.keys()) {
674
+ while ((out.get(start)?.length ?? 0) > 0) {
675
+ const loop = [start];
676
+ let at = out.get(start).pop();
677
+ while (at !== start) {
678
+ loop.push(at);
679
+ const next = out.get(at);
680
+ at = next.pop();
681
+ }
682
+ loops.push(loop);
683
+ }
684
+ }
685
+ return loops;
686
+ }
687
+ function bridge(loops) {
688
+ const entered = loops.map((loop) => {
689
+ let k = 0;
690
+ 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
+ return [...loop.slice(k), ...loop.slice(0, k)];
692
+ });
693
+ entered.sort((a, b) => a[0][0] - b[0][0] || a[0][1] - b[0][1]);
694
+ const ring = [];
695
+ for (const loop of entered) ring.push(...loop, loop[0]);
696
+ for (let i = entered.length - 2; i >= 0; i--) ring.push(entered[i][0]);
697
+ return ring;
698
+ }
699
+ function ringArea(ring) {
700
+ let a = 0;
701
+ for (let i = 0; i + 1 < ring.length; i++) a += ring[i][0] * ring[i + 1][1] - ring[i + 1][0] * ring[i][1];
702
+ return a;
703
+ }
704
+
705
+ // src/booleans/trianglesToPath.ts
577
706
  function trianglesToPath(vertices, indices) {
578
- const triangles = [];
707
+ const kept = [];
579
708
  for (let i = 0; i + 2 < indices.length; i += 3) {
580
709
  const a = indices[i] * 2, b = indices[i + 1] * 2, c = indices[i + 2] * 2;
581
710
  const ax = vertices[a], ay = vertices[a + 1];
582
- const bx = vertices[b], by = vertices[b + 1];
583
- const cx = vertices[c], cy = vertices[c + 1];
584
- if ((bx - ax) * (cy - ay) - (cx - ax) * (by - ay) === 0) continue;
585
- triangles.push([[[[ax, ay], [bx, by], [cx, cy], [ax, ay]]]]);
586
- }
587
- if (triangles.length === 0) return multiPolygonToPath([]);
588
- const [head, ...rest] = triangles;
589
- return multiPolygonToPath(polygonClipping.union(head, ...rest));
711
+ if ((vertices[b] - ax) * (vertices[c + 1] - ay) - (vertices[c] - ax) * (vertices[b + 1] - ay) === 0) continue;
712
+ kept.push(indices[i], indices[i + 1], indices[i + 2]);
713
+ }
714
+ if (kept.length === 0) return multiPolygonToPath([]);
715
+ const outline = meshOutline(junctionRings(vertices, kept));
716
+ return multiPolygonToPath(outline ? polygonClipping.union([[outline]]) : []);
590
717
  }
591
718
 
592
719
  // src/booleans/cutCrossings.ts