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 +7 -3
- package/dist/brepjs-sheetmetal.cjs +150 -45
- package/dist/brepjs-sheetmetal.js +151 -46
- package/package.json +1 -1
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.
|
|
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,
|
|
324
|
-
U-channel
|
|
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
|
|
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(
|
|
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
|
-
*
|
|
1614
|
-
*
|
|
1615
|
-
*
|
|
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
|
|
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
|
|
1621
|
-
const
|
|
1622
|
-
if (
|
|
1623
|
-
const
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
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
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
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
|
|
1637
|
-
* `thickness` apart
|
|
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
|
|
1645
|
-
if (
|
|
1646
|
-
const
|
|
1647
|
-
const
|
|
1648
|
-
|
|
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
|
|
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:
|
|
1702
|
-
center: (0, brepjs.vecScale)((0, brepjs.vecAdd)(
|
|
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
|
|
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(
|
|
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
|
-
*
|
|
1613
|
-
*
|
|
1614
|
-
*
|
|
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
|
|
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
|
|
1620
|
-
const
|
|
1621
|
-
if (
|
|
1622
|
-
const
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
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
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
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
|
|
1636
|
-
* `thickness` apart
|
|
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
|
|
1644
|
-
if (
|
|
1645
|
-
const
|
|
1646
|
-
const
|
|
1647
|
-
|
|
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
|
|
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:
|
|
1701
|
-
center: vecScale(vecAdd(
|
|
1805
|
+
normal: pa.normal,
|
|
1806
|
+
center: vecScale(vecAdd(pa.center, pb.center), .5),
|
|
1702
1807
|
area
|
|
1703
1808
|
});
|
|
1704
1809
|
}
|