brepjs-sheetmetal 0.4.0 → 0.4.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.
package/README.md CHANGED
@@ -300,7 +300,10 @@ authored path. It reads only the B-rep:
300
300
  other surface type (cone, sphere, spline, …) is reported `UNSUPPORTED_FACE` and skipped (the unfold never
301
301
  silently invents a flat for an unrecognised face).
302
302
  - **Pair flats**: the two large parallel planar faces of a panel, a sheet thickness apart, are the
303
- mid-surface flat. Thickness is detected as the smallest opposed-planar-pair gap.
303
+ mid-surface flat. Two antiparallel faces count as opposed only when they face each other (an interior
304
+ point of one projects inside the other), so a flange's end cap and the adjacent flange's inner face, a
305
+ bend radius apart across a corner, never pair. Thickness is detected as the smallest opposed-planar-pair
306
+ gap.
304
307
  - **Fit bends**: `fitCylinder(face)` recovers a bend's axis + radius + swept angle from sampled points and
305
308
  normals. The axis direction is the common perpendicular of the sampled normals (every cylinder normal is
306
309
  ⟂ the axis), recovered as the sign-aligned average of cross products of non-parallel normal pairs; the
@@ -320,8 +323,9 @@ non-uniform thickness, conical/spline bend faces, non-manifold / non-sheet topol
320
323
  fit a cylinder within tolerance all surface as warnings inside the `Ok` payload (or a clear `Err` when no
321
324
  sheet-metal structure is detected at all, e.g. a bare sphere). The detection is validated by an oracle:
322
325
  author a part, take only its `.solid`, run `unfoldSolid`, and confirm the detected unfold reproduces the
323
- authored unfold's developed area, bend count, flat count, and total flat bbox (single bend, L-bracket,
324
- U-channel), reading only the solid, never the feature tree.
326
+ authored unfold's developed area, bend count, flat count, and total flat bbox with no warnings (single bend,
327
+ plus the L-bracket, U-channel, and four-sided tray at thicknesses 1, 1.5, 2, and 3 against a 2 mm inner
328
+ radius), reading only the solid, never the feature tree.
325
329
 
326
330
  ## Bend tables
327
331
 
@@ -1286,6 +1286,61 @@ function reportFromLayout(layout) {
1286
1286
  totalFlatSize: [maxX - minX, maxY - minY]
1287
1287
  };
1288
1288
  }
1289
+ // \0@oxc-project+runtime@0.155.0/helpers/esm/usingCtx.js
1290
+ function _usingCtx() {
1291
+ var r = "function" == typeof SuppressedError ? SuppressedError : function(r, e) {
1292
+ var n = Error();
1293
+ return n.name = "SuppressedError", n.error = r, n.suppressed = e, n;
1294
+ }, e = {}, n = [];
1295
+ function using(r, e) {
1296
+ if (null != e) {
1297
+ if (Object(e) !== e) throw new TypeError("using declarations can only be used with objects, functions, null, or undefined.");
1298
+ if (r) var o = e[Symbol.asyncDispose || Symbol["for"]("Symbol.asyncDispose")];
1299
+ if (void 0 === o && (o = e[Symbol.dispose || Symbol["for"]("Symbol.dispose")], r)) var t = o;
1300
+ if ("function" != typeof o) throw new TypeError("Object is not disposable.");
1301
+ t && (o = function o() {
1302
+ try {
1303
+ t.call(e);
1304
+ } catch (r) {
1305
+ return Promise.reject(r);
1306
+ }
1307
+ }), n.push({
1308
+ v: e,
1309
+ d: o,
1310
+ a: r
1311
+ });
1312
+ } else r && n.push({
1313
+ d: e,
1314
+ a: r
1315
+ });
1316
+ return e;
1317
+ }
1318
+ return {
1319
+ e,
1320
+ u: using.bind(null, !1),
1321
+ a: using.bind(null, !0),
1322
+ d: function d() {
1323
+ var o, t = this.e, s = 0;
1324
+ function next() {
1325
+ for (; o = n.pop();) try {
1326
+ if (!o.a && 1 === s) return s = 0, n.push(o), Promise.resolve().then(next);
1327
+ if (o.d) {
1328
+ var r = o.d.call(o.v);
1329
+ if (o.a) return s |= 2, Promise.resolve(r).then(next, err);
1330
+ } else s |= 1;
1331
+ } catch (r) {
1332
+ return err(r);
1333
+ }
1334
+ if (1 === s) return t !== e ? Promise.reject(t) : Promise.resolve();
1335
+ if (t !== e) throw t;
1336
+ }
1337
+ function err(n) {
1338
+ return t = t !== e ? new r(n, t) : n, next();
1339
+ }
1340
+ return next();
1341
+ }
1342
+ };
1343
+ }
1289
1344
  // src/foreignUnfoldFns.ts
1290
1345
  /** Default neutral-axis K-factor for a purely geometric unfold (mid-surface). */
1291
1346
  var DEFAULT_FOREIGN_K = .5;
@@ -1596,10 +1651,11 @@ function unfoldForeignSolid(solid, opts) {
1596
1651
  message: `UNSUPPORTED_FACE: surface type '${t.value}' is not a planar panel or cylindrical bend; the unfold ignores it and may be incomplete`
1597
1652
  });
1598
1653
  }
1599
- const thicknessResult = detectThickness(planarFaces);
1654
+ const planar = planarFaces.map(describePlanar);
1655
+ const thicknessResult = detectThickness(planar);
1600
1656
  if (!thicknessResult.ok) return thicknessResult;
1601
1657
  const thickness = thicknessResult.value;
1602
- const flats = pairFlats(planarFaces, thickness, warnings);
1658
+ const flats = pairFlats(planar, thickness, warnings);
1603
1659
  if (flats.length === 0) return (0, brepjs.err)((0, brepjs.validationError)("NO_FLATS", "no planar panel pairs detected; not a recognizable sheet-metal solid"));
1604
1660
  const layout = layoutForeign(flats, buildBendGraph(flats, pairBends(cylFaces, warnings), warnings), thickness, kFactor, warnings);
1605
1661
  if (!layout.ok) return layout;
@@ -1609,59 +1665,106 @@ function unfoldForeignSolid(solid, opts) {
1609
1665
  warnings
1610
1666
  });
1611
1667
  }
1668
+ function describePlanar(face) {
1669
+ const center = (0, brepjs.faceCenter)(face);
1670
+ const n = 4;
1671
+ const candidates = [center];
1672
+ for (let i = 0; i < n; i += 1) for (let j = 0; j < n; j += 1) candidates.push((0, brepjs.pointOnSurface)(face, (i + .5) / n, (j + .5) / n));
1673
+ const interior = candidates.filter((p) => liesOnFace(face, p));
1674
+ return {
1675
+ face,
1676
+ normal: faceNormal(face),
1677
+ center,
1678
+ interior
1679
+ };
1680
+ }
1681
+ /** True if `point` (already on the face's surface) lies within the face's trimmed boundary. */
1682
+ function liesOnFace(face, point) {
1683
+ try {
1684
+ var _usingCtx$1 = _usingCtx();
1685
+ const probe = _usingCtx$1.u((0, brepjs.vertex)(point));
1686
+ const d = (0, brepjs.measureDistance)(probe, face);
1687
+ return d.ok && d.value < GEOM_TOL;
1688
+ } catch (_) {
1689
+ _usingCtx$1.e = _;
1690
+ } finally {
1691
+ _usingCtx$1.d();
1692
+ }
1693
+ }
1694
+ /** Normal-direction gap between two planar faces, or `null` unless they are antiparallel. */
1695
+ function antiparallelGap(a, b) {
1696
+ if ((0, brepjs.vecDot)(a.normal, b.normal) > -.99) return null;
1697
+ return Math.abs((0, brepjs.vecDot)((0, brepjs.vecSub)(b.center, a.center), a.normal));
1698
+ }
1612
1699
  /**
1613
- * Detect the sheet thickness as the smallest gap between an antiparallel pair of
1614
- * planar faces (the two faces of one flat panel). For a roughly-uniform-thickness
1615
- * part this gap is the same across panels; a spread beyond tolerance warns.
1700
+ * Antiparallel faces are the two sides of one panel only if they face each other:
1701
+ * an interior point of one projects inside the other. The normal-direction gap
1702
+ * alone also matches faces that merely sit on parallel planes, e.g. a flange's end
1703
+ * cap and the adjacent flange's inner face across a corner. Their gap is the bend's
1704
+ * inner radius, which passes for the sheet thickness as T approaches or exceeds R.
1705
+ */
1706
+ function facesOppose(a, b) {
1707
+ return b.interior.some((p) => liesOnFace(a.face, projectToPlane(p, a.normal, a.center))) || a.interior.some((p) => liesOnFace(b.face, projectToPlane(p, b.normal, b.center)));
1708
+ }
1709
+ /**
1710
+ * Detect the sheet thickness as the smallest gap between two antiparallel planar
1711
+ * faces that oppose each other (the two faces of one flat panel).
1616
1712
  */
1617
1713
  function detectThickness(planar) {
1618
- const gaps = [];
1714
+ const pairs = [];
1619
1715
  for (let i = 0; i < planar.length; i += 1) for (let j = i + 1; j < planar.length; j += 1) {
1620
- const fa = planar[i];
1621
- const fb = planar[j];
1622
- if (fa === void 0 || fb === void 0) continue;
1623
- const na = faceNormal(fa);
1624
- const nb = faceNormal(fb);
1625
- if ((0, brepjs.vecDot)(na, nb) > -.99) continue;
1626
- const ca = (0, brepjs.faceCenter)(fa);
1627
- const cb = (0, brepjs.faceCenter)(fb);
1628
- const gap = Math.abs((0, brepjs.vecDot)((0, brepjs.vecSub)(cb, ca), na));
1629
- if (gap > GEOM_TOL) gaps.push(gap);
1716
+ const a = planar[i];
1717
+ const b = planar[j];
1718
+ if (a === void 0 || b === void 0) continue;
1719
+ const gap = antiparallelGap(a, b);
1720
+ if (gap !== null && gap > GEOM_TOL) pairs.push({
1721
+ gap,
1722
+ a,
1723
+ b
1724
+ });
1725
+ }
1726
+ pairs.sort((p, q) => p.gap - q.gap);
1727
+ const thinnest = pairs.find((p) => facesOppose(p.a, p.b));
1728
+ if (thinnest === void 0) return (0, brepjs.err)((0, brepjs.validationError)("NO_THICKNESS", "could not detect a sheet thickness (no opposed planar pair)"));
1729
+ return (0, brepjs.ok)(thinnest.gap);
1730
+ }
1731
+ /** Unused face opposing `a` whose gap is nearest the sheet thickness, within tolerance. */
1732
+ function bestOpposedPartner(planar, i, thickness, used) {
1733
+ const a = planar[i];
1734
+ if (a === void 0) return null;
1735
+ const candidates = [];
1736
+ for (let j = 0; j < planar.length; j += 1) {
1737
+ const b = planar[j];
1738
+ if (j === i || used.has(j) || b === void 0) continue;
1739
+ const gap = antiparallelGap(a, b);
1740
+ if (gap === null) continue;
1741
+ const e = Math.abs(gap - thickness);
1742
+ if (e <= thickness * .5 + GEOM_TOL) candidates.push({
1743
+ j,
1744
+ e
1745
+ });
1630
1746
  }
1631
- if (gaps.length === 0) return (0, brepjs.err)((0, brepjs.validationError)("NO_THICKNESS", "could not detect a sheet thickness (no opposed planar pair)"));
1632
- gaps.sort((a, b) => a - b);
1633
- return (0, brepjs.ok)(gaps[0]);
1747
+ candidates.sort((p, q) => p.e - q.e);
1748
+ const match = candidates.find((c) => {
1749
+ const b = planar[c.j];
1750
+ return b !== void 0 && facesOppose(a, b);
1751
+ });
1752
+ return match === void 0 ? null : match.j;
1634
1753
  }
1635
1754
  /**
1636
- * Pair planar faces into flat panels: each panel is two parallel faces a
1637
- * `thickness` apart with overlapping footprints. Greedy nearest-match by gap.
1755
+ * Pair planar faces into flat panels: each panel is two antiparallel faces a
1756
+ * `thickness` apart that oppose each other. Greedy nearest-match by gap.
1638
1757
  */
1639
1758
  function pairFlats(planar, thickness, warnings) {
1640
1759
  const used = /* @__PURE__ */ new Set();
1641
1760
  const flats = [];
1642
1761
  for (let i = 0; i < planar.length; i += 1) {
1643
1762
  if (used.has(i)) continue;
1644
- const fa = planar[i];
1645
- if (fa === void 0) continue;
1646
- const na = faceNormal(fa);
1647
- const ca = (0, brepjs.faceCenter)(fa);
1648
- let best = -1;
1649
- let bestErr = Infinity;
1650
- for (let j = 0; j < planar.length; j += 1) {
1651
- if (j === i || used.has(j)) continue;
1652
- const fb = planar[j];
1653
- if (fb === void 0) continue;
1654
- const nb = faceNormal(fb);
1655
- if ((0, brepjs.vecDot)(na, nb) > -.99) continue;
1656
- const cb = (0, brepjs.faceCenter)(fb);
1657
- const gap = Math.abs((0, brepjs.vecDot)((0, brepjs.vecSub)(cb, ca), na));
1658
- const e = Math.abs(gap - thickness);
1659
- if (e < bestErr) {
1660
- bestErr = e;
1661
- best = j;
1662
- }
1663
- }
1664
- if (best < 0 || bestErr > thickness * .5 + GEOM_TOL) {
1763
+ const pa = planar[i];
1764
+ if (pa === void 0) continue;
1765
+ const fa = pa.face;
1766
+ const best = bestOpposedPartner(planar, i, thickness, used);
1767
+ if (best === null) {
1665
1768
  const db = (0, brepjs.getBounds)(fa);
1666
1769
  const ext = [
1667
1770
  db.xMax - db.xMin,
@@ -1675,7 +1778,9 @@ function pairFlats(planar, thickness, warnings) {
1675
1778
  });
1676
1779
  continue;
1677
1780
  }
1678
- const fb = planar[best];
1781
+ const pb = planar[best];
1782
+ if (pb === void 0) continue;
1783
+ const fb = pb.face;
1679
1784
  used.add(i);
1680
1785
  used.add(best);
1681
1786
  const areaA = (0, brepjs.measureArea)(fa);
@@ -1698,8 +1803,8 @@ function pairFlats(planar, thickness, warnings) {
1698
1803
  flats.push({
1699
1804
  id: `flat-${flats.length}`,
1700
1805
  faces: [fa, fb],
1701
- normal: na,
1702
- center: (0, brepjs.vecScale)((0, brepjs.vecAdd)(ca, (0, brepjs.faceCenter)(fb)), .5),
1806
+ normal: pa.normal,
1807
+ center: (0, brepjs.vecScale)((0, brepjs.vecAdd)(pa.center, pb.center), .5),
1703
1808
  area
1704
1809
  });
1705
1810
  }
@@ -1,4 +1,4 @@
1
- import { box, curveEndPoint, curveStartPoint, cut, cylinder, err, extrude, face, faceCenter, fuse, getBounds, getEdges, getFaces, getSolids, getSurfaceType, intersect, isErr, isOk, isPlanarWire, isSolid, isValid, line, measureArea, measureVolume, normalAt, ok, pointOnSurface, rotate, sharedEdges, translate, validationError, vecAdd, vecCross, vecDot, vecLength, vecNormalize, vecScale, vecSub, wire, wireLoop } from "brepjs";
1
+ import { box, curveEndPoint, curveStartPoint, cut, cylinder, err, extrude, face, faceCenter, fuse, getBounds, getEdges, getFaces, getSolids, getSurfaceType, intersect, isErr, isOk, isPlanarWire, isSolid, isValid, line, measureArea, measureDistance, measureVolume, normalAt, ok, pointOnSurface, rotate, sharedEdges, translate, validationError, vecAdd, vecCross, vecDot, vecLength, vecNormalize, vecScale, vecSub, vertex, wire, wireLoop } from "brepjs";
2
2
  // src/materials.ts
3
3
  var DEFAULT_K_FACTOR = .44;
4
4
  function gauge(name, thickness, kFactor = DEFAULT_K_FACTOR) {
@@ -1285,6 +1285,61 @@ function reportFromLayout(layout) {
1285
1285
  totalFlatSize: [maxX - minX, maxY - minY]
1286
1286
  };
1287
1287
  }
1288
+ // \0@oxc-project+runtime@0.155.0/helpers/esm/usingCtx.js
1289
+ function _usingCtx() {
1290
+ var r = "function" == typeof SuppressedError ? SuppressedError : function(r, e) {
1291
+ var n = Error();
1292
+ return n.name = "SuppressedError", n.error = r, n.suppressed = e, n;
1293
+ }, e = {}, n = [];
1294
+ function using(r, e) {
1295
+ if (null != e) {
1296
+ if (Object(e) !== e) throw new TypeError("using declarations can only be used with objects, functions, null, or undefined.");
1297
+ if (r) var o = e[Symbol.asyncDispose || Symbol["for"]("Symbol.asyncDispose")];
1298
+ if (void 0 === o && (o = e[Symbol.dispose || Symbol["for"]("Symbol.dispose")], r)) var t = o;
1299
+ if ("function" != typeof o) throw new TypeError("Object is not disposable.");
1300
+ t && (o = function o() {
1301
+ try {
1302
+ t.call(e);
1303
+ } catch (r) {
1304
+ return Promise.reject(r);
1305
+ }
1306
+ }), n.push({
1307
+ v: e,
1308
+ d: o,
1309
+ a: r
1310
+ });
1311
+ } else r && n.push({
1312
+ d: e,
1313
+ a: r
1314
+ });
1315
+ return e;
1316
+ }
1317
+ return {
1318
+ e,
1319
+ u: using.bind(null, !1),
1320
+ a: using.bind(null, !0),
1321
+ d: function d() {
1322
+ var o, t = this.e, s = 0;
1323
+ function next() {
1324
+ for (; o = n.pop();) try {
1325
+ if (!o.a && 1 === s) return s = 0, n.push(o), Promise.resolve().then(next);
1326
+ if (o.d) {
1327
+ var r = o.d.call(o.v);
1328
+ if (o.a) return s |= 2, Promise.resolve(r).then(next, err);
1329
+ } else s |= 1;
1330
+ } catch (r) {
1331
+ return err(r);
1332
+ }
1333
+ if (1 === s) return t !== e ? Promise.reject(t) : Promise.resolve();
1334
+ if (t !== e) throw t;
1335
+ }
1336
+ function err(n) {
1337
+ return t = t !== e ? new r(n, t) : n, next();
1338
+ }
1339
+ return next();
1340
+ }
1341
+ };
1342
+ }
1288
1343
  // src/foreignUnfoldFns.ts
1289
1344
  /** Default neutral-axis K-factor for a purely geometric unfold (mid-surface). */
1290
1345
  var DEFAULT_FOREIGN_K = .5;
@@ -1595,10 +1650,11 @@ function unfoldForeignSolid(solid, opts) {
1595
1650
  message: `UNSUPPORTED_FACE: surface type '${t.value}' is not a planar panel or cylindrical bend; the unfold ignores it and may be incomplete`
1596
1651
  });
1597
1652
  }
1598
- const thicknessResult = detectThickness(planarFaces);
1653
+ const planar = planarFaces.map(describePlanar);
1654
+ const thicknessResult = detectThickness(planar);
1599
1655
  if (!thicknessResult.ok) return thicknessResult;
1600
1656
  const thickness = thicknessResult.value;
1601
- const flats = pairFlats(planarFaces, thickness, warnings);
1657
+ const flats = pairFlats(planar, thickness, warnings);
1602
1658
  if (flats.length === 0) return err(validationError("NO_FLATS", "no planar panel pairs detected; not a recognizable sheet-metal solid"));
1603
1659
  const layout = layoutForeign(flats, buildBendGraph(flats, pairBends(cylFaces, warnings), warnings), thickness, kFactor, warnings);
1604
1660
  if (!layout.ok) return layout;
@@ -1608,59 +1664,106 @@ function unfoldForeignSolid(solid, opts) {
1608
1664
  warnings
1609
1665
  });
1610
1666
  }
1667
+ function describePlanar(face) {
1668
+ const center = faceCenter(face);
1669
+ const n = 4;
1670
+ const candidates = [center];
1671
+ for (let i = 0; i < n; i += 1) for (let j = 0; j < n; j += 1) candidates.push(pointOnSurface(face, (i + .5) / n, (j + .5) / n));
1672
+ const interior = candidates.filter((p) => liesOnFace(face, p));
1673
+ return {
1674
+ face,
1675
+ normal: faceNormal(face),
1676
+ center,
1677
+ interior
1678
+ };
1679
+ }
1680
+ /** True if `point` (already on the face's surface) lies within the face's trimmed boundary. */
1681
+ function liesOnFace(face, point) {
1682
+ try {
1683
+ var _usingCtx$1 = _usingCtx();
1684
+ const probe = _usingCtx$1.u(vertex(point));
1685
+ const d = measureDistance(probe, face);
1686
+ return d.ok && d.value < GEOM_TOL;
1687
+ } catch (_) {
1688
+ _usingCtx$1.e = _;
1689
+ } finally {
1690
+ _usingCtx$1.d();
1691
+ }
1692
+ }
1693
+ /** Normal-direction gap between two planar faces, or `null` unless they are antiparallel. */
1694
+ function antiparallelGap(a, b) {
1695
+ if (vecDot(a.normal, b.normal) > -.99) return null;
1696
+ return Math.abs(vecDot(vecSub(b.center, a.center), a.normal));
1697
+ }
1611
1698
  /**
1612
- * Detect the sheet thickness as the smallest gap between an antiparallel pair of
1613
- * planar faces (the two faces of one flat panel). For a roughly-uniform-thickness
1614
- * part this gap is the same across panels; a spread beyond tolerance warns.
1699
+ * Antiparallel faces are the two sides of one panel only if they face each other:
1700
+ * an interior point of one projects inside the other. The normal-direction gap
1701
+ * alone also matches faces that merely sit on parallel planes, e.g. a flange's end
1702
+ * cap and the adjacent flange's inner face across a corner. Their gap is the bend's
1703
+ * inner radius, which passes for the sheet thickness as T approaches or exceeds R.
1704
+ */
1705
+ function facesOppose(a, b) {
1706
+ return b.interior.some((p) => liesOnFace(a.face, projectToPlane(p, a.normal, a.center))) || a.interior.some((p) => liesOnFace(b.face, projectToPlane(p, b.normal, b.center)));
1707
+ }
1708
+ /**
1709
+ * Detect the sheet thickness as the smallest gap between two antiparallel planar
1710
+ * faces that oppose each other (the two faces of one flat panel).
1615
1711
  */
1616
1712
  function detectThickness(planar) {
1617
- const gaps = [];
1713
+ const pairs = [];
1618
1714
  for (let i = 0; i < planar.length; i += 1) for (let j = i + 1; j < planar.length; j += 1) {
1619
- const fa = planar[i];
1620
- const fb = planar[j];
1621
- if (fa === void 0 || fb === void 0) continue;
1622
- const na = faceNormal(fa);
1623
- const nb = faceNormal(fb);
1624
- if (vecDot(na, nb) > -.99) continue;
1625
- const ca = faceCenter(fa);
1626
- const cb = faceCenter(fb);
1627
- const gap = Math.abs(vecDot(vecSub(cb, ca), na));
1628
- if (gap > GEOM_TOL) gaps.push(gap);
1715
+ const a = planar[i];
1716
+ const b = planar[j];
1717
+ if (a === void 0 || b === void 0) continue;
1718
+ const gap = antiparallelGap(a, b);
1719
+ if (gap !== null && gap > GEOM_TOL) pairs.push({
1720
+ gap,
1721
+ a,
1722
+ b
1723
+ });
1724
+ }
1725
+ pairs.sort((p, q) => p.gap - q.gap);
1726
+ const thinnest = pairs.find((p) => facesOppose(p.a, p.b));
1727
+ if (thinnest === void 0) return err(validationError("NO_THICKNESS", "could not detect a sheet thickness (no opposed planar pair)"));
1728
+ return ok(thinnest.gap);
1729
+ }
1730
+ /** Unused face opposing `a` whose gap is nearest the sheet thickness, within tolerance. */
1731
+ function bestOpposedPartner(planar, i, thickness, used) {
1732
+ const a = planar[i];
1733
+ if (a === void 0) return null;
1734
+ const candidates = [];
1735
+ for (let j = 0; j < planar.length; j += 1) {
1736
+ const b = planar[j];
1737
+ if (j === i || used.has(j) || b === void 0) continue;
1738
+ const gap = antiparallelGap(a, b);
1739
+ if (gap === null) continue;
1740
+ const e = Math.abs(gap - thickness);
1741
+ if (e <= thickness * .5 + GEOM_TOL) candidates.push({
1742
+ j,
1743
+ e
1744
+ });
1629
1745
  }
1630
- if (gaps.length === 0) return err(validationError("NO_THICKNESS", "could not detect a sheet thickness (no opposed planar pair)"));
1631
- gaps.sort((a, b) => a - b);
1632
- return ok(gaps[0]);
1746
+ candidates.sort((p, q) => p.e - q.e);
1747
+ const match = candidates.find((c) => {
1748
+ const b = planar[c.j];
1749
+ return b !== void 0 && facesOppose(a, b);
1750
+ });
1751
+ return match === void 0 ? null : match.j;
1633
1752
  }
1634
1753
  /**
1635
- * Pair planar faces into flat panels: each panel is two parallel faces a
1636
- * `thickness` apart with overlapping footprints. Greedy nearest-match by gap.
1754
+ * Pair planar faces into flat panels: each panel is two antiparallel faces a
1755
+ * `thickness` apart that oppose each other. Greedy nearest-match by gap.
1637
1756
  */
1638
1757
  function pairFlats(planar, thickness, warnings) {
1639
1758
  const used = /* @__PURE__ */ new Set();
1640
1759
  const flats = [];
1641
1760
  for (let i = 0; i < planar.length; i += 1) {
1642
1761
  if (used.has(i)) continue;
1643
- const fa = planar[i];
1644
- if (fa === void 0) continue;
1645
- const na = faceNormal(fa);
1646
- const ca = faceCenter(fa);
1647
- let best = -1;
1648
- let bestErr = Infinity;
1649
- for (let j = 0; j < planar.length; j += 1) {
1650
- if (j === i || used.has(j)) continue;
1651
- const fb = planar[j];
1652
- if (fb === void 0) continue;
1653
- const nb = faceNormal(fb);
1654
- if (vecDot(na, nb) > -.99) continue;
1655
- const cb = faceCenter(fb);
1656
- const gap = Math.abs(vecDot(vecSub(cb, ca), na));
1657
- const e = Math.abs(gap - thickness);
1658
- if (e < bestErr) {
1659
- bestErr = e;
1660
- best = j;
1661
- }
1662
- }
1663
- if (best < 0 || bestErr > thickness * .5 + GEOM_TOL) {
1762
+ const pa = planar[i];
1763
+ if (pa === void 0) continue;
1764
+ const fa = pa.face;
1765
+ const best = bestOpposedPartner(planar, i, thickness, used);
1766
+ if (best === null) {
1664
1767
  const db = getBounds(fa);
1665
1768
  const ext = [
1666
1769
  db.xMax - db.xMin,
@@ -1674,7 +1777,9 @@ function pairFlats(planar, thickness, warnings) {
1674
1777
  });
1675
1778
  continue;
1676
1779
  }
1677
- const fb = planar[best];
1780
+ const pb = planar[best];
1781
+ if (pb === void 0) continue;
1782
+ const fb = pb.face;
1678
1783
  used.add(i);
1679
1784
  used.add(best);
1680
1785
  const areaA = measureArea(fa);
@@ -1697,8 +1802,8 @@ function pairFlats(planar, thickness, warnings) {
1697
1802
  flats.push({
1698
1803
  id: `flat-${flats.length}`,
1699
1804
  faces: [fa, fb],
1700
- normal: na,
1701
- center: vecScale(vecAdd(ca, faceCenter(fb)), .5),
1805
+ normal: pa.normal,
1806
+ center: vecScale(vecAdd(pa.center, pb.center), .5),
1702
1807
  area
1703
1808
  });
1704
1809
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brepjs-sheetmetal",
3
- "version": "0.4.0",
3
+ "version": "0.4.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",