brepjs-sheetmetal 0.2.0 → 0.3.1

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.
@@ -505,7 +505,7 @@ function buildFeatureTree(graph, rootId = ROOT_FLAT_ID) {
505
505
  adjacency.get(edge.parent)?.push(edge);
506
506
  adjacency.get(edge.child)?.push(edge);
507
507
  }
508
- const visited = new Set([rootId]);
508
+ const visited = /* @__PURE__ */ new Set([rootId]);
509
509
  const treeEdges = /* @__PURE__ */ new Set();
510
510
  const bends = [];
511
511
  const queue = [rootId];
@@ -674,18 +674,20 @@ function unfold(part) {
674
674
  direction: p.direction,
675
675
  inward: [-p.frame.v[0], -p.frame.v[1]]
676
676
  })).concat(contourResult.value.bendLines).concat(hemJogResult.value.bendLines);
677
+ const pattern = {
678
+ outline: outlineResult.value,
679
+ bendLines,
680
+ holes: holesResult.value,
681
+ formCuts: formsResult.value.cuts,
682
+ formMarkers: formsResult.value.markers,
683
+ formHinges: formsResult.value.hinges,
684
+ loftedDevelopments: loftedResult.value,
685
+ developedArea: layout.developedArea
686
+ };
687
+ const report = reportFromLayout(layout);
677
688
  return (0, brepjs.ok)({
678
- pattern: {
679
- outline: outlineResult.value,
680
- bendLines,
681
- holes: holesResult.value,
682
- formCuts: formsResult.value.cuts,
683
- formMarkers: formsResult.value.markers,
684
- formHinges: formsResult.value.hinges,
685
- loftedDevelopments: loftedResult.value,
686
- developedArea: layout.developedArea
687
- },
688
- report: reportFromLayout(layout),
689
+ pattern,
690
+ report,
689
691
  warnings
690
692
  });
691
693
  }
@@ -1308,7 +1310,8 @@ function sampleFace(face, n) {
1308
1310
  for (let i = 0; i < n; i += 1) {
1309
1311
  const u = n === 1 ? .5 : i / (n - 1);
1310
1312
  for (let j = 0; j < n; j += 1) {
1311
- const point = (0, brepjs.pointOnSurface)(face, u, n === 1 ? .5 : j / (n - 1));
1313
+ const v = n === 1 ? .5 : j / (n - 1);
1314
+ const point = (0, brepjs.pointOnSurface)(face, u, v);
1312
1315
  const normal = (0, brepjs.normalAt)(face, point);
1313
1316
  const len = (0, brepjs.vecLength)(normal);
1314
1317
  if (len < GEOM_TOL) continue;
@@ -1399,7 +1402,7 @@ function axisFromNormals(normals) {
1399
1402
  }
1400
1403
  /** An orthonormal pair spanning the plane perpendicular to `axis`. */
1401
1404
  function perpBasis(axis) {
1402
- const e1 = (0, brepjs.vecNormalize)((0, brepjs.vecCross)(axis, Math.abs(axis[0]) < .9 ? [
1405
+ const seed = Math.abs(axis[0]) < .9 ? [
1403
1406
  1,
1404
1407
  0,
1405
1408
  0
@@ -1407,7 +1410,8 @@ function perpBasis(axis) {
1407
1410
  0,
1408
1411
  1,
1409
1412
  0
1410
- ]));
1413
+ ];
1414
+ const e1 = (0, brepjs.vecNormalize)((0, brepjs.vecCross)(axis, seed));
1411
1415
  return {
1412
1416
  e1,
1413
1417
  e2: (0, brepjs.vecNormalize)((0, brepjs.vecCross)(axis, e1))
@@ -1416,7 +1420,9 @@ function perpBasis(axis) {
1416
1420
  /** Perpendicular distance from `p` to the line through `o` with unit direction `d`. */
1417
1421
  function distanceToAxis(p, o, d) {
1418
1422
  const w = (0, brepjs.vecSub)(p, o);
1419
- return (0, brepjs.vecLength)((0, brepjs.vecSub)(w, (0, brepjs.vecScale)(d, (0, brepjs.vecDot)(w, d))));
1423
+ const along = (0, brepjs.vecDot)(w, d);
1424
+ const perp = (0, brepjs.vecSub)(w, (0, brepjs.vecScale)(d, along));
1425
+ return (0, brepjs.vecLength)(perp);
1420
1426
  }
1421
1427
  /** Algebraic (Kåsa) least-squares circle fit to 2D points. */
1422
1428
  function fitCircle2(pts) {
@@ -1621,7 +1627,8 @@ function detectThickness(planar) {
1621
1627
  const fb = planar[j];
1622
1628
  if (fa === void 0 || fb === void 0) continue;
1623
1629
  const na = faceNormal(fa);
1624
- if ((0, brepjs.vecDot)(na, faceNormal(fb)) > -.99) continue;
1630
+ const nb = faceNormal(fb);
1631
+ if ((0, brepjs.vecDot)(na, nb) > -.99) continue;
1625
1632
  const ca = (0, brepjs.faceCenter)(fa);
1626
1633
  const cb = (0, brepjs.faceCenter)(fb);
1627
1634
  const gap = Math.abs((0, brepjs.vecDot)((0, brepjs.vecSub)(cb, ca), na));
@@ -1650,7 +1657,8 @@ function pairFlats(planar, thickness, warnings) {
1650
1657
  if (j === i || used.has(j)) continue;
1651
1658
  const fb = planar[j];
1652
1659
  if (fb === void 0) continue;
1653
- if ((0, brepjs.vecDot)(na, faceNormal(fb)) > -.99) continue;
1660
+ const nb = faceNormal(fb);
1661
+ if ((0, brepjs.vecDot)(na, nb) > -.99) continue;
1654
1662
  const cb = (0, brepjs.faceCenter)(fb);
1655
1663
  const gap = Math.abs((0, brepjs.vecDot)((0, brepjs.vecSub)(cb, ca), na));
1656
1664
  const e = Math.abs(gap - thickness);
@@ -1819,7 +1827,7 @@ function layoutForeign(flats, graph, thickness, kFactor, warnings) {
1819
1827
  adjacency.get(e.flats[0])?.push(e);
1820
1828
  adjacency.get(e.flats[1])?.push(e);
1821
1829
  }
1822
- const visited = new Set([0]);
1830
+ const visited = /* @__PURE__ */ new Set([0]);
1823
1831
  const treeEdges = /* @__PURE__ */ new Set();
1824
1832
  const treeBends = [];
1825
1833
  const queue = [0];
@@ -1913,23 +1921,26 @@ function layoutForeign(flats, graph, thickness, kFactor, warnings) {
1913
1921
  });
1914
1922
  }
1915
1923
  const bbox = aabb2(rects);
1916
- const outlineWire = (0, brepjs.wireLoop)(buildRectOutline2(bbox));
1924
+ const outline = buildRectOutline2(bbox);
1925
+ const outlineWire = (0, brepjs.wireLoop)(outline);
1917
1926
  if (!outlineWire.ok) return outlineWire;
1927
+ const pattern = {
1928
+ outline: outlineWire.value,
1929
+ bendLines,
1930
+ holes: [],
1931
+ formCuts: [],
1932
+ formMarkers: [],
1933
+ formHinges: [],
1934
+ loftedDevelopments: [],
1935
+ developedArea
1936
+ };
1937
+ const report = {
1938
+ bends: bendReportRows,
1939
+ totalFlatSize: [bbox.x1 - bbox.x0, bbox.y1 - bbox.y0]
1940
+ };
1918
1941
  return (0, brepjs.ok)({
1919
- pattern: {
1920
- outline: outlineWire.value,
1921
- bendLines,
1922
- holes: [],
1923
- formCuts: [],
1924
- formMarkers: [],
1925
- formHinges: [],
1926
- loftedDevelopments: [],
1927
- developedArea
1928
- },
1929
- report: {
1930
- bends: bendReportRows,
1931
- totalFlatSize: [bbox.x1 - bbox.x0, bbox.y1 - bbox.y0]
1932
- }
1942
+ pattern,
1943
+ report
1933
1944
  });
1934
1945
  }
1935
1946
  /** Map a 3D in-plane point through a placed flat's affine map. */
@@ -2019,7 +2030,9 @@ function parentFootprintCentreAlong(parent, axisInPlane, axisRef) {
2019
2030
  min = Math.min(min, v);
2020
2031
  max = Math.max(max, v);
2021
2032
  }
2022
- return (0, brepjs.vecAdd)(axisRef, (0, brepjs.vecScale)(axisInPlane, (min + max) / 2 - (0, brepjs.vecDot)(axisRef, axisInPlane)));
2033
+ const mid = (min + max) / 2;
2034
+ const refAlong = (0, brepjs.vecDot)(axisRef, axisInPlane);
2035
+ return (0, brepjs.vecAdd)(axisRef, (0, brepjs.vecScale)(axisInPlane, mid - refAlong));
2023
2036
  }
2024
2037
  /** In-plane run extent of a flat along `out` (perpendicular to the bend axis). */
2025
2038
  function inPlaneRun(flat, out) {
@@ -2035,7 +2048,8 @@ function inPlaneRun(flat, out) {
2035
2048
  }
2036
2049
  /** Run direction of a child flat pointing away from the bend axis. */
2037
2050
  function runDirectionAway(flat, out, axisOrigin) {
2038
- return (0, brepjs.vecDot)((0, brepjs.vecSub)(flat.center, axisOrigin), out) >= 0 ? out : (0, brepjs.vecScale)(out, -1);
2051
+ const toCenter = (0, brepjs.vecSub)(flat.center, axisOrigin);
2052
+ return (0, brepjs.vecDot)(toCenter, out) >= 0 ? out : (0, brepjs.vecScale)(out, -1);
2039
2053
  }
2040
2054
  /**
2041
2055
  * The child corner that develops onto the strip-far near point: minimum along the
@@ -2161,7 +2175,8 @@ function bendAxialExtent(bend) {
2161
2175
  * parent flat's mid-surface, 'down' otherwise.
2162
2176
  */
2163
2177
  function bendDirection(bend, parent) {
2164
- return (0, brepjs.vecDot)((0, brepjs.vecSub)(bend.fit.axisOrigin, parent.center), parent.normal) >= 0 ? "up" : "down";
2178
+ const toAxis = (0, brepjs.vecSub)(bend.fit.axisOrigin, parent.center);
2179
+ return (0, brepjs.vecDot)(toAxis, parent.normal) >= 0 ? "up" : "down";
2165
2180
  }
2166
2181
  function faceNormal(face) {
2167
2182
  return (0, brepjs.vecNormalize)((0, brepjs.normalAt)(face, (0, brepjs.faceCenter)(face)));
@@ -2495,10 +2510,10 @@ function parentEdge(f, side) {
2495
2510
  length = f.uLen;
2496
2511
  a = o;
2497
2512
  b = uTop;
2498
- break;
2499
2513
  }
2500
2514
  const dir = (0, brepjs.vecNormalize)((0, brepjs.vecCross)(f.n, out));
2501
- const start = (0, brepjs.vecDot)((0, brepjs.vecSub)(b, a), dir) >= 0 ? a : b;
2515
+ const toB = (0, brepjs.vecSub)(b, a);
2516
+ const start = (0, brepjs.vecDot)(toB, dir) >= 0 ? a : b;
2502
2517
  return {
2503
2518
  dir,
2504
2519
  out,
@@ -2611,14 +2626,15 @@ function frameTransform$1(axisT, runT, nT, edgeStart, offset) {
2611
2626
  const applyR = (v) => (0, brepjs.vecAdd)((0, brepjs.vecAdd)((0, brepjs.vecScale)(runT, v[0]), (0, brepjs.vecScale)(axisT, v[1])), (0, brepjs.vecScale)(nT, v[2]));
2612
2627
  return {
2613
2628
  solid: (s) => {
2614
- return (0, brepjs.translate)(aa.angleDeg === 0 ? s : (0, brepjs.rotate)(s, aa.angleDeg, {
2629
+ const rotated = aa.angleDeg === 0 ? s : (0, brepjs.rotate)(s, aa.angleDeg, {
2615
2630
  at: [
2616
2631
  0,
2617
2632
  0,
2618
2633
  0
2619
2634
  ],
2620
2635
  axis: aa.axis
2621
- }), target);
2636
+ });
2637
+ return (0, brepjs.translate)(rotated, target);
2622
2638
  },
2623
2639
  point: (p) => (0, brepjs.vecAdd)(applyR(p), target),
2624
2640
  vector: applyR
@@ -2680,12 +2696,13 @@ function matrixToAxisAngle$2(m) {
2680
2696
  };
2681
2697
  }
2682
2698
  const denom = 2 * Math.sin(angle);
2699
+ const axis = [
2700
+ (m21 - m12) / denom,
2701
+ (m02 - m20) / denom,
2702
+ (m10 - m01) / denom
2703
+ ];
2683
2704
  return {
2684
- axis: (0, brepjs.vecNormalize)([
2685
- (m21 - m12) / denom,
2686
- (m02 - m20) / denom,
2687
- (m10 - m01) / denom
2688
- ]),
2705
+ axis: (0, brepjs.vecNormalize)(axis),
2689
2706
  angleDeg
2690
2707
  };
2691
2708
  }
@@ -2707,14 +2724,15 @@ function buildBendPatch$1(axisOrigin, thetaDeg, r, width, thickness, direction)
2707
2724
  });
2708
2725
  let tube = outer;
2709
2726
  if (r > 1e-9) {
2710
- const carved = (0, brepjs.cut)(outer, (0, brepjs.cylinder)(r, width, {
2727
+ const inner = (0, brepjs.cylinder)(r, width, {
2711
2728
  at: axisOrigin,
2712
2729
  axis: [
2713
2730
  0,
2714
2731
  1,
2715
2732
  0
2716
2733
  ]
2717
- }));
2734
+ });
2735
+ const carved = (0, brepjs.cut)(outer, inner);
2718
2736
  if (!carved.ok) return carved;
2719
2737
  tube = carved.value;
2720
2738
  }
@@ -2732,39 +2750,47 @@ function buildBendPatch$1(axisOrigin, thetaDeg, r, width, thickness, direction)
2732
2750
  function buildWedge$1(axisOrigin, thetaDeg, outerR, width, direction) {
2733
2751
  const span = outerR * 2 + 2;
2734
2752
  const margin = 1;
2735
- const halfA = (0, brepjs.translate)((0, brepjs.box)(span, width + 2 * margin, 2 * span), [
2753
+ const blockA = (0, brepjs.box)(span, width + 2, 2 * span);
2754
+ const halfA = (0, brepjs.translate)(blockA, [
2736
2755
  axisOrigin[0],
2737
2756
  axisOrigin[1] - margin,
2738
2757
  axisOrigin[2] - span
2739
2758
  ]);
2740
- return (0, brepjs.intersect)(halfA, (0, brepjs.rotate)(halfA, (direction === "up" ? 1 : -1) * (180 - thetaDeg), {
2759
+ const halfB = (0, brepjs.rotate)(halfA, (direction === "up" ? 1 : -1) * (180 - thetaDeg), {
2741
2760
  at: axisOrigin,
2742
2761
  axis: [
2743
2762
  0,
2744
2763
  1,
2745
2764
  0
2746
2765
  ]
2747
- }));
2766
+ });
2767
+ return (0, brepjs.intersect)(halfA, halfB);
2748
2768
  }
2749
2769
  /** The flange flat, built lying along +X past the bend, then folded to angle θ. */
2750
2770
  function buildFlangeFlat(axisOrigin, thetaDeg, width, thickness, length, direction) {
2751
- if (direction === "up") return (0, brepjs.rotate)((0, brepjs.translate)((0, brepjs.box)(length, width, thickness), [
2752
- axisOrigin[0],
2753
- axisOrigin[1],
2754
- 0
2755
- ]), -thetaDeg, {
2756
- at: axisOrigin,
2757
- axis: [
2758
- 0,
2759
- 1,
2771
+ if (direction === "up") {
2772
+ const flat = (0, brepjs.box)(length, width, thickness);
2773
+ const positioned = (0, brepjs.translate)(flat, [
2774
+ axisOrigin[0],
2775
+ axisOrigin[1],
2760
2776
  0
2761
- ]
2762
- });
2763
- return (0, brepjs.rotate)((0, brepjs.translate)((0, brepjs.box)(length, width, thickness), [
2777
+ ]);
2778
+ return (0, brepjs.rotate)(positioned, -thetaDeg, {
2779
+ at: axisOrigin,
2780
+ axis: [
2781
+ 0,
2782
+ 1,
2783
+ 0
2784
+ ]
2785
+ });
2786
+ }
2787
+ const flat = (0, brepjs.box)(length, width, thickness);
2788
+ const positioned = (0, brepjs.translate)(flat, [
2764
2789
  axisOrigin[0],
2765
2790
  axisOrigin[1],
2766
2791
  -thickness
2767
- ]), thetaDeg, {
2792
+ ]);
2793
+ return (0, brepjs.rotate)(positioned, thetaDeg, {
2768
2794
  at: axisOrigin,
2769
2795
  axis: [
2770
2796
  0,
@@ -3048,7 +3074,8 @@ function cornerPoint$1(originA, dirA, originB, dirB) {
3048
3074
  const e = (0, brepjs.vecDot)(dirB, w0);
3049
3075
  const denom = a * c - b * b;
3050
3076
  if (Math.abs(denom) < 1e-9) return originA;
3051
- return (0, brepjs.vecAdd)(originA, (0, brepjs.vecScale)(dirA, (b * e - c * d) / denom));
3077
+ const sc = (b * e - c * d) / denom;
3078
+ return (0, brepjs.vecAdd)(originA, (0, brepjs.vecScale)(dirA, sc));
3052
3079
  }
3053
3080
  /** In-plane perpendicular to `axis` pointing toward the part center (into the parent). */
3054
3081
  function inwardRun(axisOrigin, axis, center) {
@@ -3081,11 +3108,12 @@ function parentEdgeLength(part, flange) {
3081
3108
  */
3082
3109
  function slotTool3d(at, along, inward, width, depth, thickness) {
3083
3110
  const margin = thickness + 2;
3084
- const y0 = (0, brepjs.vecDot)([
3111
+ const rotPerp = [
3085
3112
  -along[1],
3086
3113
  along[0],
3087
3114
  0
3088
- ], inward) >= 0 ? 0 : -depth;
3115
+ ];
3116
+ const y0 = (0, brepjs.vecDot)(rotPerp, inward) >= 0 ? 0 : -depth;
3089
3117
  let tool = (0, brepjs.box)(width, depth, thickness + 2 * margin);
3090
3118
  tool = (0, brepjs.translate)(tool, [
3091
3119
  -width / 2,
@@ -3110,7 +3138,8 @@ function squareTool(solid, cornerXY, side) {
3110
3138
  const b = (0, brepjs.getBounds)(solid);
3111
3139
  const margin = 2;
3112
3140
  const h = side / 2;
3113
- return (0, brepjs.translate)((0, brepjs.box)(side, side, b.zMax - b.zMin + 2 * margin), [
3141
+ const tool = (0, brepjs.box)(side, side, b.zMax - b.zMin + 4);
3142
+ return (0, brepjs.translate)(tool, [
3114
3143
  cornerXY[0] - h,
3115
3144
  cornerXY[1] - h,
3116
3145
  b.zMin - margin
@@ -3603,18 +3632,20 @@ function embossForm(part, partSolid, spec, regionId, world, dev, ext) {
3603
3632
  let solid;
3604
3633
  if (spec.form === "emboss") {
3605
3634
  const base = (0, brepjs.vecAdd)(faceCentre, (0, brepjs.vecScale)(world.n, -.05));
3606
- const fused = (0, brepjs.fuse)(partSolid, (0, brepjs.cylinder)(r, spec.height + HAIR, {
3635
+ const bump = (0, brepjs.cylinder)(r, spec.height + HAIR, {
3607
3636
  at: base,
3608
3637
  axis: world.n
3609
- }));
3638
+ });
3639
+ const fused = (0, brepjs.fuse)(partSolid, bump);
3610
3640
  if (!fused.ok) return fused;
3611
3641
  solid = normalizeSolid(fused.value);
3612
3642
  } else {
3613
3643
  const top = (0, brepjs.vecAdd)(faceCentre, (0, brepjs.vecScale)(world.n, HAIR));
3614
- const cutResult = (0, brepjs.cut)(partSolid, (0, brepjs.cylinder)(r, spec.height + HAIR, {
3644
+ const tool = (0, brepjs.cylinder)(r, spec.height + HAIR, {
3615
3645
  at: top,
3616
3646
  axis: (0, brepjs.vecScale)(world.n, -1)
3617
- }));
3647
+ });
3648
+ const cutResult = (0, brepjs.cut)(partSolid, tool);
3618
3649
  if (!cutResult.ok) return cutResult;
3619
3650
  solid = normalizeSolid(cutResult.value);
3620
3651
  }
@@ -3706,7 +3737,8 @@ function buildLouverFlap(f, x0, x1, y0, width, height, thickness, sign) {
3706
3737
  const n = (0, brepjs.vecNormalize)(f.n);
3707
3738
  const v = (0, brepjs.vecNormalize)((0, brepjs.vecCross)(n, u));
3708
3739
  const hingeCorner = (0, brepjs.vecAdd)((0, brepjs.vecAdd)((0, brepjs.vecAdd)(f.origin, (0, brepjs.vecScale)(f.u, x0)), (0, brepjs.vecScale)(f.v, y0)), (0, brepjs.vecScale)(f.n, thickness));
3709
- return (0, brepjs.ok)(mapLocalToWorld(flap, u, v, n, hingeCorner));
3740
+ const placed = mapLocalToWorld(flap, u, v, n, hingeCorner);
3741
+ return (0, brepjs.ok)(placed);
3710
3742
  }
3711
3743
  /** Map a solid built in a canonical XYZ frame onto world axes (uT, vT, nT) at origin. */
3712
3744
  function mapLocalToWorld(s, uT, vT, nT, origin) {
@@ -3721,14 +3753,15 @@ function mapLocalToWorld(s, uT, vT, nT, origin) {
3721
3753
  vT[2],
3722
3754
  nT[2]
3723
3755
  ]);
3724
- return (0, brepjs.translate)(aa.angleDeg === 0 ? s : (0, brepjs.rotate)(s, aa.angleDeg, {
3756
+ const rotated = aa.angleDeg === 0 ? s : (0, brepjs.rotate)(s, aa.angleDeg, {
3725
3757
  at: [
3726
3758
  0,
3727
3759
  0,
3728
3760
  0
3729
3761
  ],
3730
3762
  axis: aa.axis
3731
- }), origin);
3763
+ });
3764
+ return (0, brepjs.translate)(rotated, origin);
3732
3765
  }
3733
3766
  /** Axis-angle of a 3×3 row-major rotation matrix (columns = target basis). */
3734
3767
  function matrixToAxisAngle$1(m) {
@@ -3786,12 +3819,13 @@ function matrixToAxisAngle$1(m) {
3786
3819
  };
3787
3820
  }
3788
3821
  const denom = 2 * Math.sin(angle);
3822
+ const axis = [
3823
+ (m21 - m12) / denom,
3824
+ (m02 - m20) / denom,
3825
+ (m10 - m01) / denom
3826
+ ];
3789
3827
  return {
3790
- axis: (0, brepjs.vecNormalize)([
3791
- (m21 - m12) / denom,
3792
- (m02 - m20) / denom,
3793
- (m10 - m01) / denom
3794
- ]),
3828
+ axis: (0, brepjs.vecNormalize)(axis),
3795
3829
  angleDeg
3796
3830
  };
3797
3831
  }
@@ -4706,7 +4740,8 @@ function cornerPoint(originA, dirA, originB, dirB) {
4706
4740
  const e = (0, brepjs.vecDot)(dirB, w0);
4707
4741
  const denom = a * c - b * b;
4708
4742
  if (Math.abs(denom) < 1e-9) return originA;
4709
- return (0, brepjs.vecAdd)(originA, (0, brepjs.vecScale)(dirA, (b * e - c * d) / denom));
4743
+ const sc = (b * e - c * d) / denom;
4744
+ return (0, brepjs.vecAdd)(originA, (0, brepjs.vecScale)(dirA, sc));
4710
4745
  }
4711
4746
  /**
4712
4747
  * Normal of the miter plane between two flanges whose bend axes are `dirA`/`dirB`.
@@ -4738,13 +4773,14 @@ function halfSpaceTool(solid, plane) {
4738
4773
  -size / 2,
4739
4774
  0
4740
4775
  ]);
4741
- return (0, brepjs.ok)((0, brepjs.translate)(alignZTo(tool, normal), plane.origin));
4776
+ const aligned = alignZTo(tool, normal);
4777
+ return (0, brepjs.ok)((0, brepjs.translate)(aligned, plane.origin));
4742
4778
  }
4743
4779
  /** Rotate a shape so its local +Z axis points along `target` (about the origin). */
4744
4780
  function alignZTo(shape, target) {
4745
4781
  const dot = (0, brepjs.vecDot)(Z_AXIS, target);
4746
- if (dot > .999999999) return shape;
4747
- if (dot < -.999999999) return (0, brepjs.rotate)(shape, 180, {
4782
+ if (dot > 1 - 1e-9) return shape;
4783
+ if (dot < -1 + 1e-9) return (0, brepjs.rotate)(shape, 180, {
4748
4784
  at: [
4749
4785
  0,
4750
4786
  0,
@@ -4757,7 +4793,8 @@ function alignZTo(shape, target) {
4757
4793
  ]
4758
4794
  });
4759
4795
  const axis = (0, brepjs.vecNormalize)((0, brepjs.vecCross)(Z_AXIS, target));
4760
- return (0, brepjs.rotate)(shape, Math.acos(Math.max(-1, Math.min(1, dot))) * 180 / Math.PI, {
4796
+ const angleDeg = Math.acos(Math.max(-1, Math.min(1, dot))) * 180 / Math.PI;
4797
+ return (0, brepjs.rotate)(shape, angleDeg, {
4761
4798
  at: [
4762
4799
  0,
4763
4800
  0,
@@ -4888,7 +4925,8 @@ function authorContourFlange(part, spec) {
4888
4925
  function buildLineLeg(base, frame, span, thickness, legLength) {
4889
4926
  const place = frameTransform(frame.run, frame.axis, frame.n, frame.origin);
4890
4927
  const canon = (0, brepjs.box)(legLength, span, thickness);
4891
- const fused = (0, brepjs.fuse)(base, place.solid(canon));
4928
+ const placed = place.solid(canon);
4929
+ const fused = (0, brepjs.fuse)(base, placed);
4892
4930
  if (!fused.ok) return fused;
4893
4931
  const nextOrigin = (0, brepjs.vecAdd)(frame.origin, (0, brepjs.vecScale)(frame.run, legLength));
4894
4932
  return (0, brepjs.ok)({
@@ -4920,7 +4958,8 @@ function buildArcBend(base, frame, span, thickness, seg) {
4920
4958
  const place = frameTransform(frame.run, frame.axis, frame.n, frame.origin);
4921
4959
  const patch = buildBendPatch(canonOrigin, seg.angleDeg, r, span, thickness, seg.direction);
4922
4960
  if (!patch.ok) return patch;
4923
- const fused = (0, brepjs.fuse)(base, place.solid(patch.value));
4961
+ const placed = place.solid(patch.value);
4962
+ const fused = (0, brepjs.fuse)(base, placed);
4924
4963
  if (!fused.ok) return fused;
4925
4964
  const theta = seg.angleDeg * Math.PI / 180;
4926
4965
  const a = -sign * theta;
@@ -4977,14 +5016,15 @@ function frameTransform(runT, axisT, nT, origin) {
4977
5016
  const applyR = (v) => (0, brepjs.vecAdd)((0, brepjs.vecAdd)((0, brepjs.vecScale)(runT, v[0]), (0, brepjs.vecScale)(axisT, v[1])), (0, brepjs.vecScale)(nT, v[2]));
4978
5017
  return {
4979
5018
  solid: (s) => {
4980
- return (0, brepjs.translate)(aa.angleDeg === 0 ? s : (0, brepjs.rotate)(s, aa.angleDeg, {
5019
+ const rotated = aa.angleDeg === 0 ? s : (0, brepjs.rotate)(s, aa.angleDeg, {
4981
5020
  at: [
4982
5021
  0,
4983
5022
  0,
4984
5023
  0
4985
5024
  ],
4986
5025
  axis: aa.axis
4987
- }), origin);
5026
+ });
5027
+ return (0, brepjs.translate)(rotated, origin);
4988
5028
  },
4989
5029
  point: (p) => (0, brepjs.vecAdd)(applyR(p), origin),
4990
5030
  vector: applyR
@@ -5045,10 +5085,10 @@ function baseEdge(part, side) {
5045
5085
  length = uLen;
5046
5086
  a = o;
5047
5087
  b = uTop;
5048
- break;
5049
5088
  }
5050
5089
  const dir = (0, brepjs.vecNormalize)((0, brepjs.vecCross)(n, out));
5051
- const start = (0, brepjs.vecDot)((0, brepjs.vecSub)(b, a), dir) >= 0 ? a : b;
5090
+ const toB = (0, brepjs.vecSub)(b, a);
5091
+ const start = (0, brepjs.vecDot)(toB, dir) >= 0 ? a : b;
5052
5092
  return {
5053
5093
  dir,
5054
5094
  out,
@@ -5095,10 +5135,10 @@ function regionEdge(frame, side) {
5095
5135
  length = frame.uLen;
5096
5136
  a = o;
5097
5137
  b = uTop;
5098
- break;
5099
5138
  }
5100
5139
  const dir = (0, brepjs.vecNormalize)((0, brepjs.vecCross)(frame.n, out));
5101
- const start = (0, brepjs.vecDot)((0, brepjs.vecSub)(b, a), dir) >= 0 ? a : b;
5140
+ const toB = (0, brepjs.vecSub)(b, a);
5141
+ const start = (0, brepjs.vecDot)(toB, dir) >= 0 ? a : b;
5102
5142
  return {
5103
5143
  dir,
5104
5144
  out,
@@ -5132,14 +5172,15 @@ function buildBendPatch(axisOrigin, thetaDeg, r, width, thickness, direction) {
5132
5172
  });
5133
5173
  let tube = outer;
5134
5174
  if (r > 1e-9) {
5135
- const carved = (0, brepjs.cut)(outer, (0, brepjs.cylinder)(r, width, {
5175
+ const inner = (0, brepjs.cylinder)(r, width, {
5136
5176
  at: axisOrigin,
5137
5177
  axis: [
5138
5178
  0,
5139
5179
  1,
5140
5180
  0
5141
5181
  ]
5142
- }));
5182
+ });
5183
+ const carved = (0, brepjs.cut)(outer, inner);
5143
5184
  if (!carved.ok) return carved;
5144
5185
  tube = carved.value;
5145
5186
  }
@@ -5150,19 +5191,21 @@ function buildBendPatch(axisOrigin, thetaDeg, r, width, thickness, direction) {
5150
5191
  function buildWedge(axisOrigin, thetaDeg, outerR, width, direction) {
5151
5192
  const span = outerR * 2 + 2;
5152
5193
  const margin = 1;
5153
- const halfA = (0, brepjs.translate)((0, brepjs.box)(span, width + 2 * margin, 2 * span), [
5194
+ const blockA = (0, brepjs.box)(span, width + 2, 2 * span);
5195
+ const halfA = (0, brepjs.translate)(blockA, [
5154
5196
  axisOrigin[0],
5155
5197
  axisOrigin[1] - margin,
5156
5198
  axisOrigin[2] - span
5157
5199
  ]);
5158
- return (0, brepjs.intersect)(halfA, (0, brepjs.rotate)(halfA, (direction === "up" ? 1 : -1) * (180 - thetaDeg), {
5200
+ const halfB = (0, brepjs.rotate)(halfA, (direction === "up" ? 1 : -1) * (180 - thetaDeg), {
5159
5201
  at: axisOrigin,
5160
5202
  axis: [
5161
5203
  0,
5162
5204
  1,
5163
5205
  0
5164
5206
  ]
5165
- }));
5207
+ });
5208
+ return (0, brepjs.intersect)(halfA, halfB);
5166
5209
  }
5167
5210
  /** Axis-angle of a 3×3 rotation matrix (row-major). */
5168
5211
  function matrixToAxisAngle(m) {
@@ -5220,12 +5263,13 @@ function matrixToAxisAngle(m) {
5220
5263
  };
5221
5264
  }
5222
5265
  const denom = 2 * Math.sin(angle);
5266
+ const axis = [
5267
+ (m21 - m12) / denom,
5268
+ (m02 - m20) / denom,
5269
+ (m10 - m01) / denom
5270
+ ];
5223
5271
  return {
5224
- axis: (0, brepjs.vecNormalize)([
5225
- (m21 - m12) / denom,
5226
- (m02 - m20) / denom,
5227
- (m10 - m01) / denom
5228
- ]),
5272
+ axis: (0, brepjs.vecNormalize)(axis),
5229
5273
  angleDeg
5230
5274
  };
5231
5275
  }
@@ -5350,10 +5394,13 @@ function buildQuadPlate(id, a0, a1, b1, b0, thickness) {
5350
5394
  }
5351
5395
  /** A flat triangle (p0,p1,p2) extruded by `thickness` along its own normal. */
5352
5396
  function buildTrianglePlate(id, p0, p1, p2, thickness) {
5397
+ const e0 = (0, brepjs.line)(p0, p1);
5398
+ const e1 = (0, brepjs.line)(p1, p2);
5399
+ const e2 = (0, brepjs.line)(p2, p0);
5353
5400
  const loopResult = (0, brepjs.wireLoop)([
5354
- (0, brepjs.line)(p0, p1),
5355
- (0, brepjs.line)(p1, p2),
5356
- (0, brepjs.line)(p2, p0)
5401
+ e0,
5402
+ e1,
5403
+ e2
5357
5404
  ]);
5358
5405
  if (!loopResult.ok) return loopResult;
5359
5406
  const loop = loopResult.value;
@@ -6136,6 +6183,30 @@ function wireToPolygon(outline) {
6136
6183
  return (0, brepjs.ok)(poly);
6137
6184
  }
6138
6185
  /**
6186
+ * Read a wire as an OPEN polyline: every edge start plus the final edge's end,
6187
+ * so the last vertex of a non-closing path (a form cut) isn't dropped the way
6188
+ * {@link wireToPolygon} drops it for closed loops.
6189
+ *
6190
+ * Arc edges are linearised to their endpoints — the same assumption
6191
+ * {@link wireToPolygon} and the DXF writer make. Form cuts are straight-sided
6192
+ * today (louver/lance slits), so this is exact; a future curved form-feature cut
6193
+ * would render as straight chords in the overlay until arc tessellation is added.
6194
+ */
6195
+ function wireToOpenPolyline(wire) {
6196
+ const edges = (0, brepjs.getEdges)(wire);
6197
+ if (edges.length === 0) return [];
6198
+ const pts = edges.map((edge) => {
6199
+ const p = (0, brepjs.curveStartPoint)(edge);
6200
+ return [p[0], p[1]];
6201
+ });
6202
+ const lastEdge = edges[edges.length - 1];
6203
+ if (lastEdge) {
6204
+ const e = (0, brepjs.curveEndPoint)(lastEdge);
6205
+ pts.push([e[0], e[1]]);
6206
+ }
6207
+ return pts;
6208
+ }
6209
+ /**
6139
6210
  * Reduce a {@link FlatPattern} to serializable 2D polylines (outline, holes, bend
6140
6211
  * lines) for a developed-pattern overlay. Best-effort: a wire that fails to read
6141
6212
  * is dropped rather than throwing, so a partial pattern still yields what it can.
@@ -6161,6 +6232,15 @@ function flatPatternToPolylines(pattern) {
6161
6232
  to: [end[0], end[1]],
6162
6233
  direction: bend.direction
6163
6234
  };
6235
+ }),
6236
+ formCuts: pattern.formCuts.map(wireToOpenPolyline).filter((p) => p.length >= 2),
6237
+ formHinges: pattern.formHinges.map((hinge) => {
6238
+ const start = (0, brepjs.curveStartPoint)(hinge);
6239
+ const end = (0, brepjs.curveEndPoint)(hinge);
6240
+ return {
6241
+ from: [start[0], start[1]],
6242
+ to: [end[0], end[1]]
6243
+ };
6164
6244
  })
6165
6245
  };
6166
6246
  }
@@ -504,7 +504,7 @@ function buildFeatureTree(graph, rootId = ROOT_FLAT_ID) {
504
504
  adjacency.get(edge.parent)?.push(edge);
505
505
  adjacency.get(edge.child)?.push(edge);
506
506
  }
507
- const visited = new Set([rootId]);
507
+ const visited = /* @__PURE__ */ new Set([rootId]);
508
508
  const treeEdges = /* @__PURE__ */ new Set();
509
509
  const bends = [];
510
510
  const queue = [rootId];
@@ -673,18 +673,20 @@ function unfold(part) {
673
673
  direction: p.direction,
674
674
  inward: [-p.frame.v[0], -p.frame.v[1]]
675
675
  })).concat(contourResult.value.bendLines).concat(hemJogResult.value.bendLines);
676
+ const pattern = {
677
+ outline: outlineResult.value,
678
+ bendLines,
679
+ holes: holesResult.value,
680
+ formCuts: formsResult.value.cuts,
681
+ formMarkers: formsResult.value.markers,
682
+ formHinges: formsResult.value.hinges,
683
+ loftedDevelopments: loftedResult.value,
684
+ developedArea: layout.developedArea
685
+ };
686
+ const report = reportFromLayout(layout);
676
687
  return ok({
677
- pattern: {
678
- outline: outlineResult.value,
679
- bendLines,
680
- holes: holesResult.value,
681
- formCuts: formsResult.value.cuts,
682
- formMarkers: formsResult.value.markers,
683
- formHinges: formsResult.value.hinges,
684
- loftedDevelopments: loftedResult.value,
685
- developedArea: layout.developedArea
686
- },
687
- report: reportFromLayout(layout),
688
+ pattern,
689
+ report,
688
690
  warnings
689
691
  });
690
692
  }
@@ -1307,7 +1309,8 @@ function sampleFace(face, n) {
1307
1309
  for (let i = 0; i < n; i += 1) {
1308
1310
  const u = n === 1 ? .5 : i / (n - 1);
1309
1311
  for (let j = 0; j < n; j += 1) {
1310
- const point = pointOnSurface(face, u, n === 1 ? .5 : j / (n - 1));
1312
+ const v = n === 1 ? .5 : j / (n - 1);
1313
+ const point = pointOnSurface(face, u, v);
1311
1314
  const normal = normalAt(face, point);
1312
1315
  const len = vecLength(normal);
1313
1316
  if (len < GEOM_TOL) continue;
@@ -1398,7 +1401,7 @@ function axisFromNormals(normals) {
1398
1401
  }
1399
1402
  /** An orthonormal pair spanning the plane perpendicular to `axis`. */
1400
1403
  function perpBasis(axis) {
1401
- const e1 = vecNormalize(vecCross(axis, Math.abs(axis[0]) < .9 ? [
1404
+ const seed = Math.abs(axis[0]) < .9 ? [
1402
1405
  1,
1403
1406
  0,
1404
1407
  0
@@ -1406,7 +1409,8 @@ function perpBasis(axis) {
1406
1409
  0,
1407
1410
  1,
1408
1411
  0
1409
- ]));
1412
+ ];
1413
+ const e1 = vecNormalize(vecCross(axis, seed));
1410
1414
  return {
1411
1415
  e1,
1412
1416
  e2: vecNormalize(vecCross(axis, e1))
@@ -1415,7 +1419,9 @@ function perpBasis(axis) {
1415
1419
  /** Perpendicular distance from `p` to the line through `o` with unit direction `d`. */
1416
1420
  function distanceToAxis(p, o, d) {
1417
1421
  const w = vecSub(p, o);
1418
- return vecLength(vecSub(w, vecScale(d, vecDot(w, d))));
1422
+ const along = vecDot(w, d);
1423
+ const perp = vecSub(w, vecScale(d, along));
1424
+ return vecLength(perp);
1419
1425
  }
1420
1426
  /** Algebraic (Kåsa) least-squares circle fit to 2D points. */
1421
1427
  function fitCircle2(pts) {
@@ -1620,7 +1626,8 @@ function detectThickness(planar) {
1620
1626
  const fb = planar[j];
1621
1627
  if (fa === void 0 || fb === void 0) continue;
1622
1628
  const na = faceNormal(fa);
1623
- if (vecDot(na, faceNormal(fb)) > -.99) continue;
1629
+ const nb = faceNormal(fb);
1630
+ if (vecDot(na, nb) > -.99) continue;
1624
1631
  const ca = faceCenter(fa);
1625
1632
  const cb = faceCenter(fb);
1626
1633
  const gap = Math.abs(vecDot(vecSub(cb, ca), na));
@@ -1649,7 +1656,8 @@ function pairFlats(planar, thickness, warnings) {
1649
1656
  if (j === i || used.has(j)) continue;
1650
1657
  const fb = planar[j];
1651
1658
  if (fb === void 0) continue;
1652
- if (vecDot(na, faceNormal(fb)) > -.99) continue;
1659
+ const nb = faceNormal(fb);
1660
+ if (vecDot(na, nb) > -.99) continue;
1653
1661
  const cb = faceCenter(fb);
1654
1662
  const gap = Math.abs(vecDot(vecSub(cb, ca), na));
1655
1663
  const e = Math.abs(gap - thickness);
@@ -1818,7 +1826,7 @@ function layoutForeign(flats, graph, thickness, kFactor, warnings) {
1818
1826
  adjacency.get(e.flats[0])?.push(e);
1819
1827
  adjacency.get(e.flats[1])?.push(e);
1820
1828
  }
1821
- const visited = new Set([0]);
1829
+ const visited = /* @__PURE__ */ new Set([0]);
1822
1830
  const treeEdges = /* @__PURE__ */ new Set();
1823
1831
  const treeBends = [];
1824
1832
  const queue = [0];
@@ -1912,23 +1920,26 @@ function layoutForeign(flats, graph, thickness, kFactor, warnings) {
1912
1920
  });
1913
1921
  }
1914
1922
  const bbox = aabb2(rects);
1915
- const outlineWire = wireLoop(buildRectOutline2(bbox));
1923
+ const outline = buildRectOutline2(bbox);
1924
+ const outlineWire = wireLoop(outline);
1916
1925
  if (!outlineWire.ok) return outlineWire;
1926
+ const pattern = {
1927
+ outline: outlineWire.value,
1928
+ bendLines,
1929
+ holes: [],
1930
+ formCuts: [],
1931
+ formMarkers: [],
1932
+ formHinges: [],
1933
+ loftedDevelopments: [],
1934
+ developedArea
1935
+ };
1936
+ const report = {
1937
+ bends: bendReportRows,
1938
+ totalFlatSize: [bbox.x1 - bbox.x0, bbox.y1 - bbox.y0]
1939
+ };
1917
1940
  return ok({
1918
- pattern: {
1919
- outline: outlineWire.value,
1920
- bendLines,
1921
- holes: [],
1922
- formCuts: [],
1923
- formMarkers: [],
1924
- formHinges: [],
1925
- loftedDevelopments: [],
1926
- developedArea
1927
- },
1928
- report: {
1929
- bends: bendReportRows,
1930
- totalFlatSize: [bbox.x1 - bbox.x0, bbox.y1 - bbox.y0]
1931
- }
1941
+ pattern,
1942
+ report
1932
1943
  });
1933
1944
  }
1934
1945
  /** Map a 3D in-plane point through a placed flat's affine map. */
@@ -2018,7 +2029,9 @@ function parentFootprintCentreAlong(parent, axisInPlane, axisRef) {
2018
2029
  min = Math.min(min, v);
2019
2030
  max = Math.max(max, v);
2020
2031
  }
2021
- return vecAdd(axisRef, vecScale(axisInPlane, (min + max) / 2 - vecDot(axisRef, axisInPlane)));
2032
+ const mid = (min + max) / 2;
2033
+ const refAlong = vecDot(axisRef, axisInPlane);
2034
+ return vecAdd(axisRef, vecScale(axisInPlane, mid - refAlong));
2022
2035
  }
2023
2036
  /** In-plane run extent of a flat along `out` (perpendicular to the bend axis). */
2024
2037
  function inPlaneRun(flat, out) {
@@ -2034,7 +2047,8 @@ function inPlaneRun(flat, out) {
2034
2047
  }
2035
2048
  /** Run direction of a child flat pointing away from the bend axis. */
2036
2049
  function runDirectionAway(flat, out, axisOrigin) {
2037
- return vecDot(vecSub(flat.center, axisOrigin), out) >= 0 ? out : vecScale(out, -1);
2050
+ const toCenter = vecSub(flat.center, axisOrigin);
2051
+ return vecDot(toCenter, out) >= 0 ? out : vecScale(out, -1);
2038
2052
  }
2039
2053
  /**
2040
2054
  * The child corner that develops onto the strip-far near point: minimum along the
@@ -2160,7 +2174,8 @@ function bendAxialExtent(bend) {
2160
2174
  * parent flat's mid-surface, 'down' otherwise.
2161
2175
  */
2162
2176
  function bendDirection(bend, parent) {
2163
- return vecDot(vecSub(bend.fit.axisOrigin, parent.center), parent.normal) >= 0 ? "up" : "down";
2177
+ const toAxis = vecSub(bend.fit.axisOrigin, parent.center);
2178
+ return vecDot(toAxis, parent.normal) >= 0 ? "up" : "down";
2164
2179
  }
2165
2180
  function faceNormal(face) {
2166
2181
  return vecNormalize(normalAt(face, faceCenter(face)));
@@ -2494,10 +2509,10 @@ function parentEdge(f, side) {
2494
2509
  length = f.uLen;
2495
2510
  a = o;
2496
2511
  b = uTop;
2497
- break;
2498
2512
  }
2499
2513
  const dir = vecNormalize(vecCross(f.n, out));
2500
- const start = vecDot(vecSub(b, a), dir) >= 0 ? a : b;
2514
+ const toB = vecSub(b, a);
2515
+ const start = vecDot(toB, dir) >= 0 ? a : b;
2501
2516
  return {
2502
2517
  dir,
2503
2518
  out,
@@ -2610,14 +2625,15 @@ function frameTransform$1(axisT, runT, nT, edgeStart, offset) {
2610
2625
  const applyR = (v) => vecAdd(vecAdd(vecScale(runT, v[0]), vecScale(axisT, v[1])), vecScale(nT, v[2]));
2611
2626
  return {
2612
2627
  solid: (s) => {
2613
- return translate(aa.angleDeg === 0 ? s : rotate(s, aa.angleDeg, {
2628
+ const rotated = aa.angleDeg === 0 ? s : rotate(s, aa.angleDeg, {
2614
2629
  at: [
2615
2630
  0,
2616
2631
  0,
2617
2632
  0
2618
2633
  ],
2619
2634
  axis: aa.axis
2620
- }), target);
2635
+ });
2636
+ return translate(rotated, target);
2621
2637
  },
2622
2638
  point: (p) => vecAdd(applyR(p), target),
2623
2639
  vector: applyR
@@ -2679,12 +2695,13 @@ function matrixToAxisAngle$2(m) {
2679
2695
  };
2680
2696
  }
2681
2697
  const denom = 2 * Math.sin(angle);
2698
+ const axis = [
2699
+ (m21 - m12) / denom,
2700
+ (m02 - m20) / denom,
2701
+ (m10 - m01) / denom
2702
+ ];
2682
2703
  return {
2683
- axis: vecNormalize([
2684
- (m21 - m12) / denom,
2685
- (m02 - m20) / denom,
2686
- (m10 - m01) / denom
2687
- ]),
2704
+ axis: vecNormalize(axis),
2688
2705
  angleDeg
2689
2706
  };
2690
2707
  }
@@ -2706,14 +2723,15 @@ function buildBendPatch$1(axisOrigin, thetaDeg, r, width, thickness, direction)
2706
2723
  });
2707
2724
  let tube = outer;
2708
2725
  if (r > 1e-9) {
2709
- const carved = cut(outer, cylinder(r, width, {
2726
+ const inner = cylinder(r, width, {
2710
2727
  at: axisOrigin,
2711
2728
  axis: [
2712
2729
  0,
2713
2730
  1,
2714
2731
  0
2715
2732
  ]
2716
- }));
2733
+ });
2734
+ const carved = cut(outer, inner);
2717
2735
  if (!carved.ok) return carved;
2718
2736
  tube = carved.value;
2719
2737
  }
@@ -2731,39 +2749,47 @@ function buildBendPatch$1(axisOrigin, thetaDeg, r, width, thickness, direction)
2731
2749
  function buildWedge$1(axisOrigin, thetaDeg, outerR, width, direction) {
2732
2750
  const span = outerR * 2 + 2;
2733
2751
  const margin = 1;
2734
- const halfA = translate(box(span, width + 2 * margin, 2 * span), [
2752
+ const blockA = box(span, width + 2, 2 * span);
2753
+ const halfA = translate(blockA, [
2735
2754
  axisOrigin[0],
2736
2755
  axisOrigin[1] - margin,
2737
2756
  axisOrigin[2] - span
2738
2757
  ]);
2739
- return intersect(halfA, rotate(halfA, (direction === "up" ? 1 : -1) * (180 - thetaDeg), {
2758
+ const halfB = rotate(halfA, (direction === "up" ? 1 : -1) * (180 - thetaDeg), {
2740
2759
  at: axisOrigin,
2741
2760
  axis: [
2742
2761
  0,
2743
2762
  1,
2744
2763
  0
2745
2764
  ]
2746
- }));
2765
+ });
2766
+ return intersect(halfA, halfB);
2747
2767
  }
2748
2768
  /** The flange flat, built lying along +X past the bend, then folded to angle θ. */
2749
2769
  function buildFlangeFlat(axisOrigin, thetaDeg, width, thickness, length, direction) {
2750
- if (direction === "up") return rotate(translate(box(length, width, thickness), [
2751
- axisOrigin[0],
2752
- axisOrigin[1],
2753
- 0
2754
- ]), -thetaDeg, {
2755
- at: axisOrigin,
2756
- axis: [
2757
- 0,
2758
- 1,
2770
+ if (direction === "up") {
2771
+ const flat = box(length, width, thickness);
2772
+ const positioned = translate(flat, [
2773
+ axisOrigin[0],
2774
+ axisOrigin[1],
2759
2775
  0
2760
- ]
2761
- });
2762
- return rotate(translate(box(length, width, thickness), [
2776
+ ]);
2777
+ return rotate(positioned, -thetaDeg, {
2778
+ at: axisOrigin,
2779
+ axis: [
2780
+ 0,
2781
+ 1,
2782
+ 0
2783
+ ]
2784
+ });
2785
+ }
2786
+ const flat = box(length, width, thickness);
2787
+ const positioned = translate(flat, [
2763
2788
  axisOrigin[0],
2764
2789
  axisOrigin[1],
2765
2790
  -thickness
2766
- ]), thetaDeg, {
2791
+ ]);
2792
+ return rotate(positioned, thetaDeg, {
2767
2793
  at: axisOrigin,
2768
2794
  axis: [
2769
2795
  0,
@@ -3047,7 +3073,8 @@ function cornerPoint$1(originA, dirA, originB, dirB) {
3047
3073
  const e = vecDot(dirB, w0);
3048
3074
  const denom = a * c - b * b;
3049
3075
  if (Math.abs(denom) < 1e-9) return originA;
3050
- return vecAdd(originA, vecScale(dirA, (b * e - c * d) / denom));
3076
+ const sc = (b * e - c * d) / denom;
3077
+ return vecAdd(originA, vecScale(dirA, sc));
3051
3078
  }
3052
3079
  /** In-plane perpendicular to `axis` pointing toward the part center (into the parent). */
3053
3080
  function inwardRun(axisOrigin, axis, center) {
@@ -3080,11 +3107,12 @@ function parentEdgeLength(part, flange) {
3080
3107
  */
3081
3108
  function slotTool3d(at, along, inward, width, depth, thickness) {
3082
3109
  const margin = thickness + 2;
3083
- const y0 = vecDot([
3110
+ const rotPerp = [
3084
3111
  -along[1],
3085
3112
  along[0],
3086
3113
  0
3087
- ], inward) >= 0 ? 0 : -depth;
3114
+ ];
3115
+ const y0 = vecDot(rotPerp, inward) >= 0 ? 0 : -depth;
3088
3116
  let tool = box(width, depth, thickness + 2 * margin);
3089
3117
  tool = translate(tool, [
3090
3118
  -width / 2,
@@ -3109,7 +3137,8 @@ function squareTool(solid, cornerXY, side) {
3109
3137
  const b = getBounds(solid);
3110
3138
  const margin = 2;
3111
3139
  const h = side / 2;
3112
- return translate(box(side, side, b.zMax - b.zMin + 2 * margin), [
3140
+ const tool = box(side, side, b.zMax - b.zMin + 4);
3141
+ return translate(tool, [
3113
3142
  cornerXY[0] - h,
3114
3143
  cornerXY[1] - h,
3115
3144
  b.zMin - margin
@@ -3602,18 +3631,20 @@ function embossForm(part, partSolid, spec, regionId, world, dev, ext) {
3602
3631
  let solid;
3603
3632
  if (spec.form === "emboss") {
3604
3633
  const base = vecAdd(faceCentre, vecScale(world.n, -.05));
3605
- const fused = fuse(partSolid, cylinder(r, spec.height + HAIR, {
3634
+ const bump = cylinder(r, spec.height + HAIR, {
3606
3635
  at: base,
3607
3636
  axis: world.n
3608
- }));
3637
+ });
3638
+ const fused = fuse(partSolid, bump);
3609
3639
  if (!fused.ok) return fused;
3610
3640
  solid = normalizeSolid(fused.value);
3611
3641
  } else {
3612
3642
  const top = vecAdd(faceCentre, vecScale(world.n, HAIR));
3613
- const cutResult = cut(partSolid, cylinder(r, spec.height + HAIR, {
3643
+ const tool = cylinder(r, spec.height + HAIR, {
3614
3644
  at: top,
3615
3645
  axis: vecScale(world.n, -1)
3616
- }));
3646
+ });
3647
+ const cutResult = cut(partSolid, tool);
3617
3648
  if (!cutResult.ok) return cutResult;
3618
3649
  solid = normalizeSolid(cutResult.value);
3619
3650
  }
@@ -3705,7 +3736,8 @@ function buildLouverFlap(f, x0, x1, y0, width, height, thickness, sign) {
3705
3736
  const n = vecNormalize(f.n);
3706
3737
  const v = vecNormalize(vecCross(n, u));
3707
3738
  const hingeCorner = vecAdd(vecAdd(vecAdd(f.origin, vecScale(f.u, x0)), vecScale(f.v, y0)), vecScale(f.n, thickness));
3708
- return ok(mapLocalToWorld(flap, u, v, n, hingeCorner));
3739
+ const placed = mapLocalToWorld(flap, u, v, n, hingeCorner);
3740
+ return ok(placed);
3709
3741
  }
3710
3742
  /** Map a solid built in a canonical XYZ frame onto world axes (uT, vT, nT) at origin. */
3711
3743
  function mapLocalToWorld(s, uT, vT, nT, origin) {
@@ -3720,14 +3752,15 @@ function mapLocalToWorld(s, uT, vT, nT, origin) {
3720
3752
  vT[2],
3721
3753
  nT[2]
3722
3754
  ]);
3723
- return translate(aa.angleDeg === 0 ? s : rotate(s, aa.angleDeg, {
3755
+ const rotated = aa.angleDeg === 0 ? s : rotate(s, aa.angleDeg, {
3724
3756
  at: [
3725
3757
  0,
3726
3758
  0,
3727
3759
  0
3728
3760
  ],
3729
3761
  axis: aa.axis
3730
- }), origin);
3762
+ });
3763
+ return translate(rotated, origin);
3731
3764
  }
3732
3765
  /** Axis-angle of a 3×3 row-major rotation matrix (columns = target basis). */
3733
3766
  function matrixToAxisAngle$1(m) {
@@ -3785,12 +3818,13 @@ function matrixToAxisAngle$1(m) {
3785
3818
  };
3786
3819
  }
3787
3820
  const denom = 2 * Math.sin(angle);
3821
+ const axis = [
3822
+ (m21 - m12) / denom,
3823
+ (m02 - m20) / denom,
3824
+ (m10 - m01) / denom
3825
+ ];
3788
3826
  return {
3789
- axis: vecNormalize([
3790
- (m21 - m12) / denom,
3791
- (m02 - m20) / denom,
3792
- (m10 - m01) / denom
3793
- ]),
3827
+ axis: vecNormalize(axis),
3794
3828
  angleDeg
3795
3829
  };
3796
3830
  }
@@ -4705,7 +4739,8 @@ function cornerPoint(originA, dirA, originB, dirB) {
4705
4739
  const e = vecDot(dirB, w0);
4706
4740
  const denom = a * c - b * b;
4707
4741
  if (Math.abs(denom) < 1e-9) return originA;
4708
- return vecAdd(originA, vecScale(dirA, (b * e - c * d) / denom));
4742
+ const sc = (b * e - c * d) / denom;
4743
+ return vecAdd(originA, vecScale(dirA, sc));
4709
4744
  }
4710
4745
  /**
4711
4746
  * Normal of the miter plane between two flanges whose bend axes are `dirA`/`dirB`.
@@ -4737,13 +4772,14 @@ function halfSpaceTool(solid, plane) {
4737
4772
  -size / 2,
4738
4773
  0
4739
4774
  ]);
4740
- return ok(translate(alignZTo(tool, normal), plane.origin));
4775
+ const aligned = alignZTo(tool, normal);
4776
+ return ok(translate(aligned, plane.origin));
4741
4777
  }
4742
4778
  /** Rotate a shape so its local +Z axis points along `target` (about the origin). */
4743
4779
  function alignZTo(shape, target) {
4744
4780
  const dot = vecDot(Z_AXIS, target);
4745
- if (dot > .999999999) return shape;
4746
- if (dot < -.999999999) return rotate(shape, 180, {
4781
+ if (dot > 1 - 1e-9) return shape;
4782
+ if (dot < -1 + 1e-9) return rotate(shape, 180, {
4747
4783
  at: [
4748
4784
  0,
4749
4785
  0,
@@ -4756,7 +4792,8 @@ function alignZTo(shape, target) {
4756
4792
  ]
4757
4793
  });
4758
4794
  const axis = vecNormalize(vecCross(Z_AXIS, target));
4759
- return rotate(shape, Math.acos(Math.max(-1, Math.min(1, dot))) * 180 / Math.PI, {
4795
+ const angleDeg = Math.acos(Math.max(-1, Math.min(1, dot))) * 180 / Math.PI;
4796
+ return rotate(shape, angleDeg, {
4760
4797
  at: [
4761
4798
  0,
4762
4799
  0,
@@ -4887,7 +4924,8 @@ function authorContourFlange(part, spec) {
4887
4924
  function buildLineLeg(base, frame, span, thickness, legLength) {
4888
4925
  const place = frameTransform(frame.run, frame.axis, frame.n, frame.origin);
4889
4926
  const canon = box(legLength, span, thickness);
4890
- const fused = fuse(base, place.solid(canon));
4927
+ const placed = place.solid(canon);
4928
+ const fused = fuse(base, placed);
4891
4929
  if (!fused.ok) return fused;
4892
4930
  const nextOrigin = vecAdd(frame.origin, vecScale(frame.run, legLength));
4893
4931
  return ok({
@@ -4919,7 +4957,8 @@ function buildArcBend(base, frame, span, thickness, seg) {
4919
4957
  const place = frameTransform(frame.run, frame.axis, frame.n, frame.origin);
4920
4958
  const patch = buildBendPatch(canonOrigin, seg.angleDeg, r, span, thickness, seg.direction);
4921
4959
  if (!patch.ok) return patch;
4922
- const fused = fuse(base, place.solid(patch.value));
4960
+ const placed = place.solid(patch.value);
4961
+ const fused = fuse(base, placed);
4923
4962
  if (!fused.ok) return fused;
4924
4963
  const theta = seg.angleDeg * Math.PI / 180;
4925
4964
  const a = -sign * theta;
@@ -4976,14 +5015,15 @@ function frameTransform(runT, axisT, nT, origin) {
4976
5015
  const applyR = (v) => vecAdd(vecAdd(vecScale(runT, v[0]), vecScale(axisT, v[1])), vecScale(nT, v[2]));
4977
5016
  return {
4978
5017
  solid: (s) => {
4979
- return translate(aa.angleDeg === 0 ? s : rotate(s, aa.angleDeg, {
5018
+ const rotated = aa.angleDeg === 0 ? s : rotate(s, aa.angleDeg, {
4980
5019
  at: [
4981
5020
  0,
4982
5021
  0,
4983
5022
  0
4984
5023
  ],
4985
5024
  axis: aa.axis
4986
- }), origin);
5025
+ });
5026
+ return translate(rotated, origin);
4987
5027
  },
4988
5028
  point: (p) => vecAdd(applyR(p), origin),
4989
5029
  vector: applyR
@@ -5044,10 +5084,10 @@ function baseEdge(part, side) {
5044
5084
  length = uLen;
5045
5085
  a = o;
5046
5086
  b = uTop;
5047
- break;
5048
5087
  }
5049
5088
  const dir = vecNormalize(vecCross(n, out));
5050
- const start = vecDot(vecSub(b, a), dir) >= 0 ? a : b;
5089
+ const toB = vecSub(b, a);
5090
+ const start = vecDot(toB, dir) >= 0 ? a : b;
5051
5091
  return {
5052
5092
  dir,
5053
5093
  out,
@@ -5094,10 +5134,10 @@ function regionEdge(frame, side) {
5094
5134
  length = frame.uLen;
5095
5135
  a = o;
5096
5136
  b = uTop;
5097
- break;
5098
5137
  }
5099
5138
  const dir = vecNormalize(vecCross(frame.n, out));
5100
- const start = vecDot(vecSub(b, a), dir) >= 0 ? a : b;
5139
+ const toB = vecSub(b, a);
5140
+ const start = vecDot(toB, dir) >= 0 ? a : b;
5101
5141
  return {
5102
5142
  dir,
5103
5143
  out,
@@ -5131,14 +5171,15 @@ function buildBendPatch(axisOrigin, thetaDeg, r, width, thickness, direction) {
5131
5171
  });
5132
5172
  let tube = outer;
5133
5173
  if (r > 1e-9) {
5134
- const carved = cut(outer, cylinder(r, width, {
5174
+ const inner = cylinder(r, width, {
5135
5175
  at: axisOrigin,
5136
5176
  axis: [
5137
5177
  0,
5138
5178
  1,
5139
5179
  0
5140
5180
  ]
5141
- }));
5181
+ });
5182
+ const carved = cut(outer, inner);
5142
5183
  if (!carved.ok) return carved;
5143
5184
  tube = carved.value;
5144
5185
  }
@@ -5149,19 +5190,21 @@ function buildBendPatch(axisOrigin, thetaDeg, r, width, thickness, direction) {
5149
5190
  function buildWedge(axisOrigin, thetaDeg, outerR, width, direction) {
5150
5191
  const span = outerR * 2 + 2;
5151
5192
  const margin = 1;
5152
- const halfA = translate(box(span, width + 2 * margin, 2 * span), [
5193
+ const blockA = box(span, width + 2, 2 * span);
5194
+ const halfA = translate(blockA, [
5153
5195
  axisOrigin[0],
5154
5196
  axisOrigin[1] - margin,
5155
5197
  axisOrigin[2] - span
5156
5198
  ]);
5157
- return intersect(halfA, rotate(halfA, (direction === "up" ? 1 : -1) * (180 - thetaDeg), {
5199
+ const halfB = rotate(halfA, (direction === "up" ? 1 : -1) * (180 - thetaDeg), {
5158
5200
  at: axisOrigin,
5159
5201
  axis: [
5160
5202
  0,
5161
5203
  1,
5162
5204
  0
5163
5205
  ]
5164
- }));
5206
+ });
5207
+ return intersect(halfA, halfB);
5165
5208
  }
5166
5209
  /** Axis-angle of a 3×3 rotation matrix (row-major). */
5167
5210
  function matrixToAxisAngle(m) {
@@ -5219,12 +5262,13 @@ function matrixToAxisAngle(m) {
5219
5262
  };
5220
5263
  }
5221
5264
  const denom = 2 * Math.sin(angle);
5265
+ const axis = [
5266
+ (m21 - m12) / denom,
5267
+ (m02 - m20) / denom,
5268
+ (m10 - m01) / denom
5269
+ ];
5222
5270
  return {
5223
- axis: vecNormalize([
5224
- (m21 - m12) / denom,
5225
- (m02 - m20) / denom,
5226
- (m10 - m01) / denom
5227
- ]),
5271
+ axis: vecNormalize(axis),
5228
5272
  angleDeg
5229
5273
  };
5230
5274
  }
@@ -5349,10 +5393,13 @@ function buildQuadPlate(id, a0, a1, b1, b0, thickness) {
5349
5393
  }
5350
5394
  /** A flat triangle (p0,p1,p2) extruded by `thickness` along its own normal. */
5351
5395
  function buildTrianglePlate(id, p0, p1, p2, thickness) {
5396
+ const e0 = line(p0, p1);
5397
+ const e1 = line(p1, p2);
5398
+ const e2 = line(p2, p0);
5352
5399
  const loopResult = wireLoop([
5353
- line(p0, p1),
5354
- line(p1, p2),
5355
- line(p2, p0)
5400
+ e0,
5401
+ e1,
5402
+ e2
5356
5403
  ]);
5357
5404
  if (!loopResult.ok) return loopResult;
5358
5405
  const loop = loopResult.value;
@@ -6135,6 +6182,30 @@ function wireToPolygon(outline) {
6135
6182
  return ok(poly);
6136
6183
  }
6137
6184
  /**
6185
+ * Read a wire as an OPEN polyline: every edge start plus the final edge's end,
6186
+ * so the last vertex of a non-closing path (a form cut) isn't dropped the way
6187
+ * {@link wireToPolygon} drops it for closed loops.
6188
+ *
6189
+ * Arc edges are linearised to their endpoints — the same assumption
6190
+ * {@link wireToPolygon} and the DXF writer make. Form cuts are straight-sided
6191
+ * today (louver/lance slits), so this is exact; a future curved form-feature cut
6192
+ * would render as straight chords in the overlay until arc tessellation is added.
6193
+ */
6194
+ function wireToOpenPolyline(wire) {
6195
+ const edges = getEdges(wire);
6196
+ if (edges.length === 0) return [];
6197
+ const pts = edges.map((edge) => {
6198
+ const p = curveStartPoint(edge);
6199
+ return [p[0], p[1]];
6200
+ });
6201
+ const lastEdge = edges[edges.length - 1];
6202
+ if (lastEdge) {
6203
+ const e = curveEndPoint(lastEdge);
6204
+ pts.push([e[0], e[1]]);
6205
+ }
6206
+ return pts;
6207
+ }
6208
+ /**
6138
6209
  * Reduce a {@link FlatPattern} to serializable 2D polylines (outline, holes, bend
6139
6210
  * lines) for a developed-pattern overlay. Best-effort: a wire that fails to read
6140
6211
  * is dropped rather than throwing, so a partial pattern still yields what it can.
@@ -6160,6 +6231,15 @@ function flatPatternToPolylines(pattern) {
6160
6231
  to: [end[0], end[1]],
6161
6232
  direction: bend.direction
6162
6233
  };
6234
+ }),
6235
+ formCuts: pattern.formCuts.map(wireToOpenPolyline).filter((p) => p.length >= 2),
6236
+ formHinges: pattern.formHinges.map((hinge) => {
6237
+ const start = curveStartPoint(hinge);
6238
+ const end = curveEndPoint(hinge);
6239
+ return {
6240
+ from: [start[0], start[1]],
6241
+ to: [end[0], end[1]]
6242
+ };
6163
6243
  })
6164
6244
  };
6165
6245
  }
@@ -24,6 +24,13 @@ export interface FlatPatternPolylines {
24
24
  holes: Polygon[];
25
25
  /** Bend lines, each with its fold direction. */
26
26
  bendLines: FlatPatternBendLine[];
27
+ /** Open form-feature cut paths in the blank (e.g. louver/lance three-sided cuts). */
28
+ formCuts: Polygon[];
29
+ /** Form-feature hinge lines (the uncut fold edge a louver/lance forms up along). */
30
+ formHinges: {
31
+ from: Pt2;
32
+ to: Pt2;
33
+ }[];
27
34
  }
28
35
  /**
29
36
  * Reduce a {@link FlatPattern} to serializable 2D polylines (outline, holes, bend
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brepjs-sheetmetal",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Sheet-metal CAD domain for brepjs — flange authoring, auto-miter, fold/unfold flat patterns",
5
5
  "keywords": [
6
6
  "sheet-metal",
@@ -43,7 +43,7 @@
43
43
  ],
44
44
  "scripts": {
45
45
  "build": "vite build",
46
- "typecheck": "tsc --noEmit",
46
+ "typecheck": "tsgo --noEmit",
47
47
  "test": "vitest run",
48
48
  "snapshot": "tsx harness/snapshot.ts",
49
49
  "lint": "eslint src tests"
@@ -53,10 +53,9 @@
53
53
  },
54
54
  "devDependencies": {
55
55
  "@eslint/js": "*",
56
- "@types/node": "25.9.2",
57
- "brepjs-opencascade": ">=0.16.0",
56
+ "@types/node": "26.2.0",
58
57
  "eslint": "*",
59
- "tsx": "4.22.4",
58
+ "tsx": "4.23.11",
60
59
  "typescript": "*",
61
60
  "typescript-eslint": "*",
62
61
  "vite": "^8.0.0",