forgecad 0.9.6 → 0.9.7
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/dist/assets/{AdminPage-Da6hhpJx.js → AdminPage-DX0mpSZT.js} +1 -1
- package/dist/assets/{BlogPage-Bl_sKeWb.js → BlogPage-CI_P0_Pf.js} +1 -1
- package/dist/assets/{DocsPage-Blz3Tp4j.js → DocsPage-DLhIIZyJ.js} +3 -3
- package/dist/assets/{EditorApp-CuiPbtn5.js → EditorApp-BujZvuwX.js} +140 -20
- package/dist/assets/{EditorApp-DS0AIUrZ.css → EditorApp-DfFT2Dn8.css} +1 -0
- package/dist/assets/{EmbedViewer-BFG6-Ufm.js → EmbedViewer-0S0qXKog.js} +2 -2
- package/dist/assets/{LandingPageProofDriven-DB9fQd5P.js → LandingPageProofDriven-O_yMtAri.js} +1 -1
- package/dist/assets/{PricingPage-BMxYT_F0.js → PricingPage-DGkX3Ahr.js} +1 -1
- package/dist/assets/{SettingsPage-VVQNrCAg.js → SettingsPage-DBsqTB_y.js} +82 -22
- package/dist/assets/{app-Dl9ymBWC.js → app-BE2nD6Yz.js} +1056 -258
- package/dist/assets/cli/{render-CFtwKCCY.js → render-iP9qh475.js} +1533 -207
- package/dist/assets/{evalWorker-CRvbzTXm.js → evalWorker-Ds5U4xtN.js} +2178 -30
- package/dist/assets/inspectWorker-Dll4eVyD.js +12620 -0
- package/dist/assets/{manifold-DpBXFS2K.js → manifold-Bk26ViCr.js} +1 -1
- package/dist/assets/{manifold-DzZ4VRPs.js → manifold-DjYsd7A_.js} +2 -2
- package/dist/assets/{manifold-B9QSr-qP.js → manifold-sJ-axdXM.js} +1 -1
- package/dist/assets/{renderSceneState-BuAXF2jh.js → renderSceneState-Bngp5MrQ.js} +1 -1
- package/dist/assets/{reportWorker-BNWEnRg1.js → reportWorker-CU8RZ4O0.js} +2161 -30
- package/dist/assets/{distance-BEC2RjJi.js → sectionPlaneMath-BdTjyVfs.js} +2539 -1187
- package/dist/cli/render.html +1 -1
- package/dist/docs/index.html +1 -1
- package/dist/docs-raw/AI/usage.md +7 -2
- package/dist/docs-raw/CLI.md +82 -53
- package/dist/docs-raw/beta-operations.md +5 -0
- package/dist/docs-raw/coding.md +1 -1
- package/dist/docs-raw/generated/concepts.md +59 -2
- package/dist/docs-raw/generated/core.md +206 -1
- package/dist/docs-raw/generated/lib.md +17 -1
- package/dist/docs-raw/generated/viewport.md +1 -1
- package/dist/docs-raw/guides/inspection-bundles.md +36 -13
- package/dist/docs-raw/platform/auth.md +2 -0
- package/dist/docs-raw/platform/google-oauth-setup.md +4 -0
- package/dist/docs-raw/skills/forgecad-make-a-model.md +87 -8
- package/dist/docs-raw/skills/forgecad-prepare-prompt.md +14 -6
- package/dist/docs-raw/skills/forgecad-render-inspect.md +1 -1
- package/dist/docs-raw/skills/index.md +2 -2
- package/dist/index.html +1 -1
- package/dist/sitemap.xml +6 -6
- package/dist-cli/forgecad.js +7975 -4528
- package/dist-cli/forgecad.js.map +1 -1
- package/dist-skill/CONTEXT.md +260 -16
- package/dist-skill/docs/CLI.md +82 -53
- package/dist-skill/docs/generated/core.md +206 -1
- package/dist-skill/docs/generated/lib.md +17 -1
- package/dist-skill/docs/generated/viewport.md +1 -1
- package/dist-skill/docs/guides/inspection-bundles.md +36 -13
- package/dist-skill/docs-dev/CLI.md +82 -53
- package/dist-skill/docs-dev/coding.md +1 -1
- package/dist-skill/docs-dev/generated/core.md +206 -1
- package/dist-skill/docs-dev/generated/lib.md +17 -1
- package/dist-skill/docs-dev/generated/viewport.md +1 -1
- package/dist-skill/docs-dev/guides/inspection-bundles.md +36 -13
- package/dist-skill/library/forgecad-make-a-model/SKILL.md +87 -8
- package/dist-skill/library/forgecad-prepare-prompt/SKILL.md +14 -6
- package/dist-skill/library/forgecad-prepare-prompt/references/default-profiles.md +5 -3
- package/dist-skill/library/forgecad-prepare-prompt/references/master-prompt.md +7 -5
- package/dist-skill/library/forgecad-render-inspect/SKILL.md +1 -1
- package/examples/api/bolted-service-cover.forge.js +17 -0
- package/examples/api/cable-gland-anchor.forge.js +14 -0
- package/examples/api/captured-cartridge-guide.forge.js +14 -0
- package/examples/api/captured-linear-slide.forge.js +13 -0
- package/examples/api/clevis-pin-joint.forge.js +13 -0
- package/examples/api/datum-enclosure.forge.js +16 -0
- package/examples/api/hose-barb-port.forge.js +14 -0
- package/examples/api/intentional-overlap-overmold.forge.js +16 -0
- package/examples/api/knuckled-hinge-assembly.forge.js +15 -0
- package/examples/api/living-hinge-cover.forge.js +14 -0
- package/examples/api/pcb-terminal-block.forge.js +22 -0
- package/examples/api/pinned-lever-pivot-stack.forge.js +14 -0
- package/examples/api/retained-shaft-knob-stack.forge.js +15 -0
- package/examples/api/routed-tube-clip.forge.js +15 -0
- package/examples/api/seated-bearing-stack.forge.js +30 -0
- package/examples/api/snap-latch-cover.forge.js +14 -0
- package/examples/api/static-assembly-connectors.forge.js +14 -16
- package/examples/api/thumb-screw-clamp.forge.js +15 -0
- package/package.json +1 -1
|
@@ -8367,7 +8367,7 @@ function sub$7(a2, b) {
|
|
|
8367
8367
|
function cross$8(a2, b) {
|
|
8368
8368
|
return [a2[1] * b[2] - a2[2] * b[1], a2[2] * b[0] - a2[0] * b[2], a2[0] * b[1] - a2[1] * b[0]];
|
|
8369
8369
|
}
|
|
8370
|
-
function makeEdge
|
|
8370
|
+
function makeEdge(name, start, end, faceName, curve) {
|
|
8371
8371
|
return {
|
|
8372
8372
|
name,
|
|
8373
8373
|
start,
|
|
@@ -8411,10 +8411,10 @@ function buildSurfaceSheetTopology(boundaries, options = {}) {
|
|
|
8411
8411
|
surface: options.surface
|
|
8412
8412
|
});
|
|
8413
8413
|
const edges = /* @__PURE__ */ new Map();
|
|
8414
|
-
edges.set("u0", makeEdge
|
|
8415
|
-
edges.set("u1", makeEdge
|
|
8416
|
-
edges.set("v0", makeEdge
|
|
8417
|
-
edges.set("v1", makeEdge
|
|
8414
|
+
edges.set("u0", makeEdge("u0", u0Start, u0End, faceName, (_a3 = options.edgeCurves) == null ? void 0 : _a3.u0));
|
|
8415
|
+
edges.set("u1", makeEdge("u1", u1Start, u1End, faceName, (_b3 = options.edgeCurves) == null ? void 0 : _b3.u1));
|
|
8416
|
+
edges.set("v0", makeEdge("v0", v0Start, v0End, faceName, (_c2 = options.edgeCurves) == null ? void 0 : _c2.v0));
|
|
8417
|
+
edges.set("v1", makeEdge("v1", v1Start, v1End, faceName, (_d2 = options.edgeCurves) == null ? void 0 : _d2.v1));
|
|
8418
8418
|
return { faces, edges };
|
|
8419
8419
|
}
|
|
8420
8420
|
function attachSurfaceSheetTopology(shape, boundaries, options = {}) {
|
|
@@ -10605,7 +10605,7 @@ async function initManifoldWasm() {
|
|
|
10605
10605
|
if (_wasm$1) return _wasm$1;
|
|
10606
10606
|
performance.mark("manifold:start");
|
|
10607
10607
|
const Module = (await __vitePreload(async () => {
|
|
10608
|
-
const { default: __vite_default__ } = await import("./manifold-
|
|
10608
|
+
const { default: __vite_default__ } = await import("./manifold-DjYsd7A_.js");
|
|
10609
10609
|
return { default: __vite_default__ };
|
|
10610
10610
|
}, true ? [] : void 0)).default;
|
|
10611
10611
|
performance.mark("manifold:imported");
|
|
@@ -22489,7 +22489,7 @@ function marchingTetrahedra(sdfFn, bounds, edgeLength2) {
|
|
|
22489
22489
|
};
|
|
22490
22490
|
}
|
|
22491
22491
|
const EPS$8 = 1e-9;
|
|
22492
|
-
function finitePositive
|
|
22492
|
+
function finitePositive(value) {
|
|
22493
22493
|
return Number.isFinite(value) && value > EPS$8;
|
|
22494
22494
|
}
|
|
22495
22495
|
function clampNonNegative(value) {
|
|
@@ -22508,7 +22508,7 @@ function distancePreservingMatrixScale(matrix) {
|
|
|
22508
22508
|
const sx = length4(col0);
|
|
22509
22509
|
const sy = length4(col1);
|
|
22510
22510
|
const sz = length4(col2);
|
|
22511
|
-
if (!finitePositive
|
|
22511
|
+
if (!finitePositive(sx) || !finitePositive(sy) || !finitePositive(sz)) return null;
|
|
22512
22512
|
if (Math.abs(sx - sy) > EPS$8 || Math.abs(sx - sz) > EPS$8) return null;
|
|
22513
22513
|
if (Math.abs(dot2(col0, col1)) > EPS$8 || Math.abs(dot2(col0, col2)) > EPS$8 || Math.abs(dot2(col1, col2)) > EPS$8) return null;
|
|
22514
22514
|
return sx;
|
|
@@ -22525,7 +22525,7 @@ function transformStepDistanceScale(step) {
|
|
|
22525
22525
|
const sx = Math.abs(step.x);
|
|
22526
22526
|
const sy = Math.abs(step.y);
|
|
22527
22527
|
const sz = Math.abs(step.z);
|
|
22528
|
-
if (!finitePositive
|
|
22528
|
+
if (!finitePositive(sx) || !finitePositive(sy) || !finitePositive(sz)) return null;
|
|
22529
22529
|
return Math.abs(sx - sy) <= EPS$8 && Math.abs(sx - sz) <= EPS$8 ? sx : null;
|
|
22530
22530
|
}
|
|
22531
22531
|
}
|
|
@@ -22565,7 +22565,7 @@ function translatedPlan(base, z2) {
|
|
|
22565
22565
|
};
|
|
22566
22566
|
}
|
|
22567
22567
|
function offsetCylinderDimensions(plan, thickness) {
|
|
22568
|
-
if (!finitePositive
|
|
22568
|
+
if (!finitePositive(plan.height)) return null;
|
|
22569
22569
|
const radiusTop = plan.radiusTop ?? plan.radius;
|
|
22570
22570
|
const slope = (radiusTop - plan.radius) / plan.height;
|
|
22571
22571
|
const normalScale = Math.hypot(1, slope);
|
|
@@ -22574,7 +22574,7 @@ function offsetCylinderDimensions(plan, thickness) {
|
|
|
22574
22574
|
const height = zMax - zMin;
|
|
22575
22575
|
const radiusBottom = plan.radius + thickness * (normalScale - slope);
|
|
22576
22576
|
const offsetRadiusTop = radiusTop + thickness * (normalScale + slope);
|
|
22577
|
-
if (!finitePositive
|
|
22577
|
+
if (!finitePositive(height) || radiusBottom < -EPS$8 || offsetRadiusTop < -EPS$8) return null;
|
|
22578
22578
|
return {
|
|
22579
22579
|
zMin,
|
|
22580
22580
|
height,
|
|
@@ -22632,7 +22632,7 @@ function rectangleCandidatePointsFromProfile(plan) {
|
|
|
22632
22632
|
case "rect": {
|
|
22633
22633
|
const width = Math.abs(plan.width);
|
|
22634
22634
|
const height = Math.abs(plan.height);
|
|
22635
|
-
if (!finitePositive
|
|
22635
|
+
if (!finitePositive(width) || !finitePositive(height)) return null;
|
|
22636
22636
|
const halfWidth = width / 2;
|
|
22637
22637
|
const halfHeight = height / 2;
|
|
22638
22638
|
const points = [
|
|
@@ -22666,7 +22666,7 @@ function rectangleFootprintFromProfile(plan) {
|
|
|
22666
22666
|
const [xMin, xMax] = xs;
|
|
22667
22667
|
const [zMin, zMax] = zs;
|
|
22668
22668
|
if (xMin == null || xMax == null || zMin == null || zMax == null) return null;
|
|
22669
|
-
if (xMin < -EPS$8 || !finitePositive
|
|
22669
|
+
if (xMin < -EPS$8 || !finitePositive(xMax) || !finitePositive(zMax - zMin)) return null;
|
|
22670
22670
|
const hasCorner = (x2, z2) => points.some(([px2, pz2]) => sameScalar$1(px2, x2) && sameScalar$1(pz2, z2));
|
|
22671
22671
|
if (!hasCorner(xMin, zMin) || !hasCorner(xMax, zMin) || !hasCorner(xMax, zMax) || !hasCorner(xMin, zMax)) return null;
|
|
22672
22672
|
return {
|
|
@@ -22679,7 +22679,7 @@ function rectangleFootprintFromProfile(plan) {
|
|
|
22679
22679
|
function circleFootprintFromProfile(plan) {
|
|
22680
22680
|
if (plan.kind !== "circle") return null;
|
|
22681
22681
|
const radius = Math.abs(plan.radius);
|
|
22682
|
-
if (!finitePositive
|
|
22682
|
+
if (!finitePositive(radius)) return null;
|
|
22683
22683
|
const center = transformProfilePointThrough$1([0, 0], plan.transforms);
|
|
22684
22684
|
const xPoint = transformProfilePointThrough$1([1, 0], plan.transforms);
|
|
22685
22685
|
const yPoint = transformProfilePointThrough$1([0, 1], plan.transforms);
|
|
@@ -22688,7 +22688,7 @@ function circleFootprintFromProfile(plan) {
|
|
|
22688
22688
|
const xScale = Math.hypot(xAxis[0], xAxis[1]);
|
|
22689
22689
|
const yScale = Math.hypot(yAxis[0], yAxis[1]);
|
|
22690
22690
|
const dot2 = xAxis[0] * yAxis[0] + xAxis[1] * yAxis[1];
|
|
22691
|
-
if (!finitePositive
|
|
22691
|
+
if (!finitePositive(xScale) || !finitePositive(yScale)) return null;
|
|
22692
22692
|
if (Math.abs(xScale - yScale) > EPS$8 || Math.abs(dot2) > EPS$8 * xScale * yScale) return null;
|
|
22693
22693
|
return {
|
|
22694
22694
|
center,
|
|
@@ -22701,7 +22701,7 @@ function fullCircleRevolveTorusPlan(plan, minorRadiusOffset = 0) {
|
|
|
22701
22701
|
const circle2 = circleFootprintFromProfile(plan.profile);
|
|
22702
22702
|
if (!circle2 || circle2.center[0] <= EPS$8) return null;
|
|
22703
22703
|
const minorRadius = circle2.radius + minorRadiusOffset;
|
|
22704
|
-
if (!finitePositive
|
|
22704
|
+
if (!finitePositive(minorRadius) || minorRadius >= circle2.center[0] - EPS$8) return null;
|
|
22705
22705
|
return translatedPlan(
|
|
22706
22706
|
{
|
|
22707
22707
|
kind: "torus",
|
|
@@ -22748,7 +22748,7 @@ function offsetFullRectRevolvePlan(plan, thickness) {
|
|
|
22748
22748
|
const innerRadius = rectangle.innerRadius - thickness;
|
|
22749
22749
|
const outerRadius = rectangle.outerRadius + thickness;
|
|
22750
22750
|
const height = rectangle.zMax - rectangle.zMin + 2 * thickness;
|
|
22751
|
-
if (innerRadius < -EPS$8 || !finitePositive
|
|
22751
|
+
if (innerRadius < -EPS$8 || !finitePositive(outerRadius) || !finitePositive(height) || outerRadius <= innerRadius + EPS$8) return null;
|
|
22752
22752
|
const zCenter = (rectangle.zMin + rectangle.zMax) / 2;
|
|
22753
22753
|
if (innerRadius <= EPS$8) {
|
|
22754
22754
|
return translatedPlan(
|
|
@@ -22789,11 +22789,11 @@ function offsetSolidAnalyticPrimitivePlan(base, thickness) {
|
|
|
22789
22789
|
};
|
|
22790
22790
|
}
|
|
22791
22791
|
case "box": {
|
|
22792
|
-
if (!finitePositive
|
|
22792
|
+
if (!finitePositive(base.z)) return null;
|
|
22793
22793
|
const x2 = Math.abs(base.x) + 2 * thickness;
|
|
22794
22794
|
const y2 = Math.abs(base.y) + 2 * thickness;
|
|
22795
22795
|
const z2 = base.z + 2 * thickness;
|
|
22796
|
-
if (!finitePositive
|
|
22796
|
+
if (!finitePositive(x2) || !finitePositive(y2) || !finitePositive(z2)) return null;
|
|
22797
22797
|
return translatedPlan(
|
|
22798
22798
|
{
|
|
22799
22799
|
kind: "box",
|
|
@@ -22820,7 +22820,7 @@ function offsetSolidAnalyticPrimitivePlan(base, thickness) {
|
|
|
22820
22820
|
}
|
|
22821
22821
|
case "sphere": {
|
|
22822
22822
|
const radius = base.radius + thickness;
|
|
22823
|
-
if (!finitePositive
|
|
22823
|
+
if (!finitePositive(radius)) return null;
|
|
22824
22824
|
return {
|
|
22825
22825
|
kind: "sphere",
|
|
22826
22826
|
radius,
|
|
@@ -22829,7 +22829,7 @@ function offsetSolidAnalyticPrimitivePlan(base, thickness) {
|
|
|
22829
22829
|
}
|
|
22830
22830
|
case "torus": {
|
|
22831
22831
|
const minorRadius = base.minorRadius + thickness;
|
|
22832
|
-
if (!finitePositive
|
|
22832
|
+
if (!finitePositive(minorRadius) || minorRadius >= base.majorRadius - EPS$8) return null;
|
|
22833
22833
|
return {
|
|
22834
22834
|
kind: "torus",
|
|
22835
22835
|
majorRadius: base.majorRadius,
|
|
@@ -23569,18 +23569,18 @@ function faceAxes(face) {
|
|
|
23569
23569
|
}
|
|
23570
23570
|
return {};
|
|
23571
23571
|
}
|
|
23572
|
-
function edgeKey
|
|
23572
|
+
function edgeKey(start, end) {
|
|
23573
23573
|
const encode2 = (p2) => p2.map((value) => value.toFixed(9)).join(",");
|
|
23574
23574
|
const a2 = encode2(start);
|
|
23575
23575
|
const b = encode2(end);
|
|
23576
23576
|
return a2 < b ? `${a2}|${b}` : `${b}|${a2}`;
|
|
23577
23577
|
}
|
|
23578
23578
|
function faceEdgeIndex(face, start, end) {
|
|
23579
|
-
const target = edgeKey
|
|
23579
|
+
const target = edgeKey(start, end);
|
|
23580
23580
|
for (let i = 0; i < face.vertices.length; i++) {
|
|
23581
23581
|
const faceStart = face.vertices[i];
|
|
23582
23582
|
const faceEnd = face.vertices[(i + 1) % face.vertices.length];
|
|
23583
|
-
if (faceStart && faceEnd && edgeKey
|
|
23583
|
+
if (faceStart && faceEnd && edgeKey(faceStart, faceEnd) === target) return i;
|
|
23584
23584
|
}
|
|
23585
23585
|
return null;
|
|
23586
23586
|
}
|
|
@@ -23781,7 +23781,7 @@ function topologyPayloadToTopology(payload) {
|
|
|
23781
23781
|
const start = explicitVertices[explicitEdge.vertices[0]];
|
|
23782
23782
|
const end = explicitVertices[explicitEdge.vertices[1]];
|
|
23783
23783
|
if (!isVec3$2(start) || !isVec3$2(end)) continue;
|
|
23784
|
-
const key = edgeKey
|
|
23784
|
+
const key = edgeKey(start, end);
|
|
23785
23785
|
if (seenEdges.has(key)) continue;
|
|
23786
23786
|
seenEdges.set(key, edges.size);
|
|
23787
23787
|
const display = explicitEdgeDisplayName(payload, explicitEdge, explicitEdgeIndex, start, end);
|
|
@@ -23801,7 +23801,7 @@ function topologyPayloadToTopology(payload) {
|
|
|
23801
23801
|
const start = face.vertices[i];
|
|
23802
23802
|
const end = face.vertices[(i + 1) % face.vertices.length];
|
|
23803
23803
|
if (!start || !end) continue;
|
|
23804
|
-
const key = edgeKey
|
|
23804
|
+
const key = edgeKey(start, end);
|
|
23805
23805
|
if (seenEdges.has(key)) continue;
|
|
23806
23806
|
seenEdges.set(key, edges.size);
|
|
23807
23807
|
edges.set(`${face.id}:edge-${i}`, {
|
|
@@ -35897,7 +35897,7 @@ function edgesBetweenFaces(shape, faceA, faceBs) {
|
|
|
35897
35897
|
});
|
|
35898
35898
|
return coalesceEdges(result);
|
|
35899
35899
|
}
|
|
35900
|
-
const EPSILON$
|
|
35900
|
+
const EPSILON$2 = 1e-8;
|
|
35901
35901
|
function rayTriangle(ox, oy, oz, dx, dy, dz, ax, ay, az, bx, by, bz, cx, cy, cz) {
|
|
35902
35902
|
const e1x = bx - ax, e1y = by - ay, e1z = bz - az;
|
|
35903
35903
|
const e2x = cx - ax, e2y = cy - ay, e2z = cz - az;
|
|
@@ -35905,7 +35905,7 @@ function rayTriangle(ox, oy, oz, dx, dy, dz, ax, ay, az, bx, by, bz, cx, cy, cz)
|
|
|
35905
35905
|
const py2 = dz * e2x - dx * e2z;
|
|
35906
35906
|
const pz2 = dx * e2y - dy * e2x;
|
|
35907
35907
|
const det = e1x * px2 + e1y * py2 + e1z * pz2;
|
|
35908
|
-
if (det > -EPSILON$
|
|
35908
|
+
if (det > -EPSILON$2 && det < EPSILON$2) return null;
|
|
35909
35909
|
const invDet = 1 / det;
|
|
35910
35910
|
const tx = ox - ax, ty = oy - ay, tz = oz - az;
|
|
35911
35911
|
const u2 = (tx * px2 + ty * py2 + tz * pz2) * invDet;
|
|
@@ -35944,7 +35944,7 @@ function rayMeshIntersect(mesh, origin, direction2) {
|
|
|
35944
35944
|
hits.sort((a2, b) => a2.distance - b.distance);
|
|
35945
35945
|
const deduped = [];
|
|
35946
35946
|
for (const hit of hits) {
|
|
35947
|
-
if (deduped.length === 0 || Math.abs(hit.distance - deduped[deduped.length - 1].distance) > EPSILON$
|
|
35947
|
+
if (deduped.length === 0 || Math.abs(hit.distance - deduped[deduped.length - 1].distance) > EPSILON$2 * 100) {
|
|
35948
35948
|
deduped.push(hit);
|
|
35949
35949
|
}
|
|
35950
35950
|
}
|
|
@@ -49280,7 +49280,7 @@ function beltDrive(options) {
|
|
|
49280
49280
|
};
|
|
49281
49281
|
}
|
|
49282
49282
|
const GEAR_META_KEY = Symbol.for("forgecad.library.gearMeta");
|
|
49283
|
-
const EPSILON$
|
|
49283
|
+
const EPSILON$1 = 1e-9;
|
|
49284
49284
|
function clamp01$1(value) {
|
|
49285
49285
|
return Math.max(-1, Math.min(1, value));
|
|
49286
49286
|
}
|
|
@@ -49399,11 +49399,11 @@ function buildSpurGearMeta(options) {
|
|
|
49399
49399
|
const pitchRadius = options.module * options.teeth * 0.5;
|
|
49400
49400
|
const baseRadius = pitchRadius * Math.cos(options.pressureAngleRad);
|
|
49401
49401
|
const outerRadius = pitchRadius + options.addendum;
|
|
49402
|
-
const rootRadius = Math.max(EPSILON$
|
|
49403
|
-
if (!(rootRadius < outerRadius - EPSILON$
|
|
49402
|
+
const rootRadius = Math.max(EPSILON$1, pitchRadius - options.dedendum);
|
|
49403
|
+
if (!(rootRadius < outerRadius - EPSILON$1)) {
|
|
49404
49404
|
throw new Error("spurGear: invalid radii (root radius must be smaller than outer radius)");
|
|
49405
49405
|
}
|
|
49406
|
-
if (options.boreDiameter > 0 && options.boreDiameter * 0.5 >= rootRadius - EPSILON$
|
|
49406
|
+
if (options.boreDiameter > 0 && options.boreDiameter * 0.5 >= rootRadius - EPSILON$1) {
|
|
49407
49407
|
throw new Error("spurGear: bore is too large for the computed root radius");
|
|
49408
49408
|
}
|
|
49409
49409
|
return {
|
|
@@ -49426,7 +49426,7 @@ function buildSpurGearMeta(options) {
|
|
|
49426
49426
|
function createSpurToothSketch(meta2, segmentsPerTooth) {
|
|
49427
49427
|
const circularPitch = Math.PI * meta2.module;
|
|
49428
49428
|
const thicknessAtPitch = circularPitch * 0.5 - meta2.backlash;
|
|
49429
|
-
if (thicknessAtPitch <= EPSILON$
|
|
49429
|
+
if (thicknessAtPitch <= EPSILON$1) {
|
|
49430
49430
|
throw new Error("spurGear: backlash leaves no tooth thickness at pitch circle");
|
|
49431
49431
|
}
|
|
49432
49432
|
const halfThicknessAtPitch = thicknessAtPitch / (2 * meta2.pitchRadius);
|
|
@@ -49435,7 +49435,7 @@ function createSpurToothSketch(meta2, segmentsPerTooth) {
|
|
|
49435
49435
|
const arcSteps = Math.max(3, Math.ceil(segmentsPerTooth * 0.6));
|
|
49436
49436
|
const radialGap = flankStartRadius - meta2.rootRadius;
|
|
49437
49437
|
const filletRadius = Math.min(0.38 * meta2.module, radialGap * 0.9);
|
|
49438
|
-
const hasFillets = filletRadius > EPSILON$
|
|
49438
|
+
const hasFillets = filletRadius > EPSILON$1;
|
|
49439
49439
|
const leftFlank = [];
|
|
49440
49440
|
const rightFlank = [];
|
|
49441
49441
|
for (let i = 0; i <= flankSteps; i++) {
|
|
@@ -49499,7 +49499,7 @@ function spurGear(options) {
|
|
|
49499
49499
|
});
|
|
49500
49500
|
return attachGearMeta(shapeWithConnectors, meta2);
|
|
49501
49501
|
}
|
|
49502
|
-
function requirePositive$
|
|
49502
|
+
function requirePositive$8(scope, name, value) {
|
|
49503
49503
|
if (!isFinitePositive(value)) throw new Error(`${scope}: "${name}" must be > 0`);
|
|
49504
49504
|
}
|
|
49505
49505
|
function requireOptionalBore(scope, boreDiameter, maxDiameter) {
|
|
@@ -49521,8 +49521,8 @@ function cutBore$1(shape, boreDiameter) {
|
|
|
49521
49521
|
return shape.subtract(cutter);
|
|
49522
49522
|
}
|
|
49523
49523
|
function gearBodyDisk(options) {
|
|
49524
|
-
requirePositive$
|
|
49525
|
-
requirePositive$
|
|
49524
|
+
requirePositive$8("gearBodyDisk", "outerRadius", options.outerRadius);
|
|
49525
|
+
requirePositive$8("gearBodyDisk", "faceWidth", options.faceWidth);
|
|
49526
49526
|
const bore = requireOptionalBore("gearBodyDisk", options.boreDiameter, options.outerRadius * 2);
|
|
49527
49527
|
const segments = resolveSegments(options.segments);
|
|
49528
49528
|
const outer = circle2d(options.outerRadius, segments);
|
|
@@ -49530,14 +49530,14 @@ function gearBodyDisk(options) {
|
|
|
49530
49530
|
return sketchExtrude(profile, options.faceWidth);
|
|
49531
49531
|
}
|
|
49532
49532
|
function gearBodyDiskWithHub(options) {
|
|
49533
|
-
requirePositive$
|
|
49533
|
+
requirePositive$8("gearBodyDiskWithHub", "hubDiameter", options.hubDiameter);
|
|
49534
49534
|
if (options.hubDiameter >= options.outerRadius * 2) {
|
|
49535
49535
|
throw new Error('gearBodyDiskWithHub: "hubDiameter" must be smaller than the outer diameter');
|
|
49536
49536
|
}
|
|
49537
49537
|
const bore = requireOptionalBore("gearBodyDiskWithHub", options.boreDiameter, options.hubDiameter);
|
|
49538
49538
|
const base = gearBodyDisk({ ...options, boreDiameter: 0 });
|
|
49539
49539
|
const hubFaceWidth = options.hubFaceWidth ?? options.faceWidth * 1.5;
|
|
49540
|
-
requirePositive$
|
|
49540
|
+
requirePositive$8("gearBodyDiskWithHub", "hubFaceWidth", hubFaceWidth);
|
|
49541
49541
|
const hub = cylinder(hubFaceWidth, options.hubDiameter * 0.5, void 0, options.segments).translate(
|
|
49542
49542
|
0,
|
|
49543
49543
|
0,
|
|
@@ -49546,11 +49546,11 @@ function gearBodyDiskWithHub(options) {
|
|
|
49546
49546
|
return cutBore$1(base.add(hub), bore);
|
|
49547
49547
|
}
|
|
49548
49548
|
function gearBodySpoked(options) {
|
|
49549
|
-
requirePositive$
|
|
49550
|
-
requirePositive$
|
|
49551
|
-
requirePositive$
|
|
49552
|
-
requirePositive$
|
|
49553
|
-
requirePositive$
|
|
49549
|
+
requirePositive$8("gearBodySpoked", "outerRadius", options.outerRadius);
|
|
49550
|
+
requirePositive$8("gearBodySpoked", "faceWidth", options.faceWidth);
|
|
49551
|
+
requirePositive$8("gearBodySpoked", "rimWidth", options.rimWidth);
|
|
49552
|
+
requirePositive$8("gearBodySpoked", "hubDiameter", options.hubDiameter);
|
|
49553
|
+
requirePositive$8("gearBodySpoked", "spokeWidth", options.spokeWidth);
|
|
49554
49554
|
if (!Number.isInteger(options.spokeCount) || options.spokeCount < 2) {
|
|
49555
49555
|
throw new Error('gearBodySpoked: "spokeCount" must be an integer >= 2');
|
|
49556
49556
|
}
|
|
@@ -49573,12 +49573,12 @@ function gearBodySpoked(options) {
|
|
|
49573
49573
|
}
|
|
49574
49574
|
function gearBodyFromProfile(profile, options) {
|
|
49575
49575
|
if (!(profile instanceof Sketch)) throw new Error('gearBodyFromProfile: "profile" must be a Sketch');
|
|
49576
|
-
requirePositive$
|
|
49576
|
+
requirePositive$8("gearBodyFromProfile", "faceWidth", options.faceWidth);
|
|
49577
49577
|
const bore = options.boreDiameter ?? 0;
|
|
49578
49578
|
if (!Number.isFinite(bore) || bore < 0) throw new Error('gearBodyFromProfile: "boreDiameter" must be >= 0');
|
|
49579
49579
|
return cutBore$1(sketchExtrude(profile, options.faceWidth), bore);
|
|
49580
49580
|
}
|
|
49581
|
-
function requirePositive$
|
|
49581
|
+
function requirePositive$7(scope, name, value) {
|
|
49582
49582
|
if (!isFinitePositive(value)) throw new Error(`${scope}: "${name}" must be > 0`);
|
|
49583
49583
|
}
|
|
49584
49584
|
function requireFiniteAngle(scope, name, value) {
|
|
@@ -49640,7 +49640,7 @@ function buildSpurTeethRegion(options, name, faceWidth) {
|
|
|
49640
49640
|
}
|
|
49641
49641
|
function buildSolidArcRegion(options, name, faceWidth) {
|
|
49642
49642
|
const scope = "driveWheel.addSolidArcBetween";
|
|
49643
|
-
requirePositive$
|
|
49643
|
+
requirePositive$7(scope, "outerRadius", options.outerRadius);
|
|
49644
49644
|
const innerRadius = options.innerRadius ?? 0;
|
|
49645
49645
|
if (!Number.isFinite(innerRadius) || innerRadius < 0) throw new Error(`${scope}: "innerRadius" must be >= 0`);
|
|
49646
49646
|
if (innerRadius >= options.outerRadius) throw new Error(`${scope}: "innerRadius" must be smaller than "outerRadius"`);
|
|
@@ -49663,7 +49663,7 @@ function normalizedSweep(scope, fromAngleDeg, toAngleDeg) {
|
|
|
49663
49663
|
if (!Number.isFinite(toAngleDeg)) throw new Error(`${scope}: "toAngleDeg" must be finite`);
|
|
49664
49664
|
let sweep2 = toAngleDeg - fromAngleDeg;
|
|
49665
49665
|
while (sweep2 <= 0) sweep2 += 360;
|
|
49666
|
-
if (sweep2 > 360 + EPSILON$
|
|
49666
|
+
if (sweep2 > 360 + EPSILON$1) throw new Error(`${scope}: angular sweep must be <= 360 degrees`);
|
|
49667
49667
|
return Math.min(360, sweep2);
|
|
49668
49668
|
}
|
|
49669
49669
|
function buildSpurToothRegionProfile(meta2, firstTooth, toothCount, segmentsPerTooth) {
|
|
@@ -49678,7 +49678,7 @@ function buildSolidArcProfile(options, sweepDeg) {
|
|
|
49678
49678
|
const innerRadius = options.innerRadius ?? 0;
|
|
49679
49679
|
const segments = options.segments ?? Math.max(16, Math.ceil(sweepDeg / 6));
|
|
49680
49680
|
if (!Number.isInteger(segments) || segments < 4) throw new Error('driveWheel.addSolidArcBetween: "segments" must be an integer >= 4');
|
|
49681
|
-
if (Math.abs(sweepDeg - 360) < EPSILON$
|
|
49681
|
+
if (Math.abs(sweepDeg - 360) < EPSILON$1) {
|
|
49682
49682
|
const outer = circle2d(options.outerRadius, segments);
|
|
49683
49683
|
return innerRadius > 0 ? difference2d(outer, circle2d(innerRadius, segments)) : outer;
|
|
49684
49684
|
}
|
|
@@ -49706,7 +49706,7 @@ class DriveWheelBuilder {
|
|
|
49706
49706
|
__publicField(this, "boreDiameter");
|
|
49707
49707
|
__publicField(this, "regions", []);
|
|
49708
49708
|
if (options.body !== void 0 && !(options.body instanceof Shape$1)) throw new Error('driveWheel: "body" must be a Shape');
|
|
49709
|
-
if (options.faceWidth !== void 0) requirePositive$
|
|
49709
|
+
if (options.faceWidth !== void 0) requirePositive$7("driveWheel", "faceWidth", options.faceWidth);
|
|
49710
49710
|
const boreDiameter = options.boreDiameter ?? 0;
|
|
49711
49711
|
if (!Number.isFinite(boreDiameter) || boreDiameter < 0) throw new Error('driveWheel: "boreDiameter" must be >= 0');
|
|
49712
49712
|
this.body = options.body;
|
|
@@ -49741,7 +49741,7 @@ class DriveWheelBuilder {
|
|
|
49741
49741
|
if (options.innerRadius !== void 0 && (!Number.isFinite(options.innerRadius) || options.innerRadius < 0)) {
|
|
49742
49742
|
throw new Error(`${scope}: "innerRadius" must be >= 0`);
|
|
49743
49743
|
}
|
|
49744
|
-
if (options.outerRadius !== void 0) requirePositive$
|
|
49744
|
+
if (options.outerRadius !== void 0) requirePositive$7(scope, "outerRadius", options.outerRadius);
|
|
49745
49745
|
this.regions.push({
|
|
49746
49746
|
shape: shape.clone(),
|
|
49747
49747
|
meta: {
|
|
@@ -49762,7 +49762,7 @@ class DriveWheelBuilder {
|
|
|
49762
49762
|
}
|
|
49763
49763
|
const faceWidth = this.resolveBuildFaceWidth();
|
|
49764
49764
|
const firstGearRegion = (_a3 = this.regions.find((region) => region.gearMeta)) == null ? void 0 : _a3.gearMeta;
|
|
49765
|
-
if (firstGearRegion && this.boreDiameter * 0.5 >= firstGearRegion.rootRadius - EPSILON$
|
|
49765
|
+
if (firstGearRegion && this.boreDiameter * 0.5 >= firstGearRegion.rootRadius - EPSILON$1) {
|
|
49766
49766
|
throw new Error("driveWheel: bore is too large for the first spur-tooth region");
|
|
49767
49767
|
}
|
|
49768
49768
|
const body = ((_b3 = this.body) == null ? void 0 : _b3.clone()) ?? gearBodyDisk({ outerRadius: (firstGearRegion == null ? void 0 : firstGearRegion.rootRadius) ?? this.defaultBodyRadius(), faceWidth });
|
|
@@ -49807,8 +49807,8 @@ class DriveWheelBuilder {
|
|
|
49807
49807
|
resolveFaceWidth(scope, localFaceWidth) {
|
|
49808
49808
|
const faceWidth = localFaceWidth ?? this.faceWidth;
|
|
49809
49809
|
if (faceWidth === void 0) throw new Error(`${scope}: "faceWidth" is required unless driveWheel({ faceWidth }) was set`);
|
|
49810
|
-
requirePositive$
|
|
49811
|
-
if (this.faceWidth !== void 0 && localFaceWidth !== void 0 && Math.abs(this.faceWidth - localFaceWidth) > EPSILON$
|
|
49810
|
+
requirePositive$7(scope, "faceWidth", faceWidth);
|
|
49811
|
+
if (this.faceWidth !== void 0 && localFaceWidth !== void 0 && Math.abs(this.faceWidth - localFaceWidth) > EPSILON$1) {
|
|
49812
49812
|
throw new Error(`${scope}: region faceWidth must match driveWheel faceWidth`);
|
|
49813
49813
|
}
|
|
49814
49814
|
return faceWidth;
|
|
@@ -50031,7 +50031,7 @@ function normalizeRingGearOptions(options) {
|
|
|
50031
50031
|
const rimWidth = options.rimWidth ?? module * 2;
|
|
50032
50032
|
if (!isFinitePositive(rimWidth)) throw new Error('ringGear: "rimWidth" must be > 0');
|
|
50033
50033
|
const outerRadius = options.outerDiameter != null ? options.outerDiameter * 0.5 : rootRadius + rimWidth;
|
|
50034
|
-
if (!(outerRadius > rootRadius + EPSILON$
|
|
50034
|
+
if (!(outerRadius > rootRadius + EPSILON$1)) {
|
|
50035
50035
|
throw new Error("ringGear: outer diameter/rim width leaves no ring body");
|
|
50036
50036
|
}
|
|
50037
50037
|
return {
|
|
@@ -50083,8 +50083,8 @@ function ringGear(options) {
|
|
|
50083
50083
|
const baseRadius = pitchRadius * Math.cos(normalized.pressureAngleRad);
|
|
50084
50084
|
const tipRadius = pitchRadius - normalized.addendum;
|
|
50085
50085
|
const rootRadius = pitchRadius + normalized.dedendum;
|
|
50086
|
-
if (!(tipRadius > EPSILON$
|
|
50087
|
-
if (!(tipRadius < rootRadius - EPSILON$
|
|
50086
|
+
if (!(tipRadius > EPSILON$1)) throw new Error("ringGear: addendum is too large for the tooth count/module");
|
|
50087
|
+
if (!(tipRadius < rootRadius - EPSILON$1)) throw new Error("ringGear: invalid tip/root radius relationship");
|
|
50088
50088
|
const meta2 = {
|
|
50089
50089
|
kind: "ring",
|
|
50090
50090
|
module: normalized.module,
|
|
@@ -50158,13 +50158,13 @@ function rackGear(options) {
|
|
|
50158
50158
|
const baseHeight = options.baseHeight ?? module * 1.6;
|
|
50159
50159
|
if (!isFinitePositive(baseHeight)) throw new Error('rackGear: "baseHeight" must be > 0');
|
|
50160
50160
|
const thicknessAtPitch = pitch * 0.5 - backlash;
|
|
50161
|
-
if (thicknessAtPitch <= EPSILON$
|
|
50161
|
+
if (thicknessAtPitch <= EPSILON$1) throw new Error("rackGear: backlash leaves no tooth thickness");
|
|
50162
50162
|
const halfPitchThickness = thicknessAtPitch * 0.5;
|
|
50163
50163
|
const dxTip = addendum * Math.tan(pressureAngleRad);
|
|
50164
50164
|
const dxRoot = dedendum * Math.tan(pressureAngleRad);
|
|
50165
50165
|
const halfTip = halfPitchThickness - dxTip;
|
|
50166
50166
|
const halfRoot = halfPitchThickness + dxRoot;
|
|
50167
|
-
if (halfTip <= EPSILON$
|
|
50167
|
+
if (halfTip <= EPSILON$1) {
|
|
50168
50168
|
throw new Error("rackGear: tooth tip collapsed (increase module or lower pressure angle/addendum)");
|
|
50169
50169
|
}
|
|
50170
50170
|
const toothSketch = polygon([
|
|
@@ -50230,7 +50230,7 @@ function computeBevelPitchAngleDeg(teeth, mateTeeth, shaftAngleDeg) {
|
|
|
50230
50230
|
const numerator = teeth * Math.sin(shaftAngleRad);
|
|
50231
50231
|
const denominator = mateTeeth + teeth * Math.cos(shaftAngleRad);
|
|
50232
50232
|
const angle = Math.atan2(numerator, denominator);
|
|
50233
|
-
if (!(angle > EPSILON$
|
|
50233
|
+
if (!(angle > EPSILON$1 && angle < shaftAngleRad - EPSILON$1)) {
|
|
50234
50234
|
throw new Error("bevelGear: could not derive a valid pitch angle from teeth/shaft angle");
|
|
50235
50235
|
}
|
|
50236
50236
|
return angle * 180 / Math.PI;
|
|
@@ -50265,7 +50265,7 @@ function normalizeBevelGearOptions(options) {
|
|
|
50265
50265
|
}
|
|
50266
50266
|
const pitchAngleRad = pitchAngleDeg * Math.PI / 180;
|
|
50267
50267
|
const pitchRadius = spur.module * spur.teeth * 0.5;
|
|
50268
|
-
const coneDistance = pitchRadius / Math.max(EPSILON$
|
|
50268
|
+
const coneDistance = pitchRadius / Math.max(EPSILON$1, Math.sin(pitchAngleRad));
|
|
50269
50269
|
if (!isFinitePositive(coneDistance)) {
|
|
50270
50270
|
throw new Error("bevelGear: invalid cone distance");
|
|
50271
50271
|
}
|
|
@@ -50274,7 +50274,7 @@ function normalizeBevelGearOptions(options) {
|
|
|
50274
50274
|
throw new Error("bevelGear: faceWidth is too large for the selected pitch angle");
|
|
50275
50275
|
}
|
|
50276
50276
|
const topScale = smallPitchRadius / pitchRadius;
|
|
50277
|
-
if (!(topScale > EPSILON$
|
|
50277
|
+
if (!(topScale > EPSILON$1 && topScale <= 1)) {
|
|
50278
50278
|
throw new Error("bevelGear: computed top scale is invalid");
|
|
50279
50279
|
}
|
|
50280
50280
|
return {
|
|
@@ -50390,7 +50390,7 @@ function gearPair(options) {
|
|
|
50390
50390
|
const alpha = pinion.meta.pressureAngleRad;
|
|
50391
50391
|
const nominalCenterDistance = pinion.meta.pitchRadius + gear.meta.pitchRadius;
|
|
50392
50392
|
const requestedBacklash = options.backlash ?? Math.max(pinion.meta.backlash, gear.meta.backlash, 0);
|
|
50393
|
-
const autoCenterDistance = nominalCenterDistance + requestedBacklash / (2 * Math.max(EPSILON$
|
|
50393
|
+
const autoCenterDistance = nominalCenterDistance + requestedBacklash / (2 * Math.max(EPSILON$1, Math.tan(alpha)));
|
|
50394
50394
|
const centerDistance = options.centerDistance ?? autoCenterDistance;
|
|
50395
50395
|
if (!Number.isFinite(centerDistance) || centerDistance <= 0) {
|
|
50396
50396
|
throw new Error("gearPair: centerDistance must be > 0");
|
|
@@ -50419,11 +50419,11 @@ function gearPair(options) {
|
|
|
50419
50419
|
message: `Center distance ${centerDistance.toFixed(4)} exceeds addendum reach ${addendumReach.toFixed(4)} (no mesh contact)`
|
|
50420
50420
|
});
|
|
50421
50421
|
}
|
|
50422
|
-
const cosWorking = clamp01$1(baseSum / Math.max(centerDistance, EPSILON$
|
|
50422
|
+
const cosWorking = clamp01$1(baseSum / Math.max(centerDistance, EPSILON$1));
|
|
50423
50423
|
const alphaWorking = Math.acos(cosWorking);
|
|
50424
50424
|
const basePitch = Math.PI * module * Math.cos(alpha);
|
|
50425
50425
|
const pathLength = Math.sqrt(Math.max(0, pinion.meta.outerRadius ** 2 - pinion.meta.baseRadius ** 2)) + Math.sqrt(Math.max(0, gear.meta.outerRadius ** 2 - gear.meta.baseRadius ** 2)) - centerDistance * Math.sin(alphaWorking);
|
|
50426
|
-
const contactRatio = pathLength / Math.max(EPSILON$
|
|
50426
|
+
const contactRatio = pathLength / Math.max(EPSILON$1, basePitch);
|
|
50427
50427
|
if (contactRatio < 1) {
|
|
50428
50428
|
diagnostics.push({
|
|
50429
50429
|
level: "error",
|
|
@@ -50554,8 +50554,8 @@ function bevelGearPair(options) {
|
|
|
50554
50554
|
message: `Gear pitch angle (${gearMetaPitch.toFixed(2)}deg) differs from tooth-derived ${gearPitchAngleDeg.toFixed(2)}deg`
|
|
50555
50555
|
});
|
|
50556
50556
|
}
|
|
50557
|
-
const coneDistancePinion = pinion.meta.coneDistance ?? pinion.meta.pitchRadius / Math.max(EPSILON$
|
|
50558
|
-
const coneDistanceGear = gear.meta.coneDistance ?? gear.meta.pitchRadius / Math.max(EPSILON$
|
|
50557
|
+
const coneDistancePinion = pinion.meta.coneDistance ?? pinion.meta.pitchRadius / Math.max(EPSILON$1, Math.sin(pinionPitchAngleRad));
|
|
50558
|
+
const coneDistanceGear = gear.meta.coneDistance ?? gear.meta.pitchRadius / Math.max(EPSILON$1, Math.sin(gearPitchAngleRad));
|
|
50559
50559
|
const coneDistance = (coneDistancePinion + coneDistanceGear) * 0.5;
|
|
50560
50560
|
if (Math.abs(coneDistancePinion - coneDistanceGear) > pinion.meta.module * 0.5) {
|
|
50561
50561
|
diagnostics.push({
|
|
@@ -50564,8 +50564,8 @@ function bevelGearPair(options) {
|
|
|
50564
50564
|
message: `Pitch-cone distances differ: pinion=${coneDistancePinion.toFixed(3)}, gear=${coneDistanceGear.toFixed(3)}`
|
|
50565
50565
|
});
|
|
50566
50566
|
}
|
|
50567
|
-
const pinionToApex = pinion.meta.pitchRadius / Math.max(EPSILON$
|
|
50568
|
-
const gearToApex = gear.meta.pitchRadius / Math.max(EPSILON$
|
|
50567
|
+
const pinionToApex = pinion.meta.pitchRadius / Math.max(EPSILON$1, Math.tan(pinionPitchAngleRad)) - pinion.meta.faceWidth * 0.5;
|
|
50568
|
+
const gearToApex = gear.meta.pitchRadius / Math.max(EPSILON$1, Math.tan(gearPitchAngleRad)) - gear.meta.faceWidth * 0.5;
|
|
50569
50569
|
if (pinionToApex <= 0 || gearToApex <= 0) {
|
|
50570
50570
|
diagnostics.push({
|
|
50571
50571
|
level: "warn",
|
|
@@ -50960,6 +50960,1867 @@ function washer(size, options) {
|
|
|
50960
50960
|
const bore = cylinder(dims.t + 1, dims.id / 2, void 0, segs);
|
|
50961
50961
|
return outer.subtract(bore);
|
|
50962
50962
|
}
|
|
50963
|
+
function requirePositive$6(value, name) {
|
|
50964
|
+
if (!Number.isFinite(value) || value <= 0) throw new Error(`${name} must be a positive finite number`);
|
|
50965
|
+
return value;
|
|
50966
|
+
}
|
|
50967
|
+
function requireNonNegative(value, name) {
|
|
50968
|
+
if (!Number.isFinite(value) || value < 0) throw new Error(`${name} must be a non-negative finite number`);
|
|
50969
|
+
return value;
|
|
50970
|
+
}
|
|
50971
|
+
function metricWasherSizeForPin(pinDiameter) {
|
|
50972
|
+
if (pinDiameter <= 2) return "M2";
|
|
50973
|
+
if (pinDiameter <= 2.5) return "M2.5";
|
|
50974
|
+
if (pinDiameter <= 3) return "M3";
|
|
50975
|
+
if (pinDiameter <= 4) return "M4";
|
|
50976
|
+
if (pinDiameter <= 5) return "M5";
|
|
50977
|
+
if (pinDiameter <= 6) return "M6";
|
|
50978
|
+
if (pinDiameter <= 8) return "M8";
|
|
50979
|
+
return "M10";
|
|
50980
|
+
}
|
|
50981
|
+
function cylinderAlongX(length4, radius, xCenter, segments) {
|
|
50982
|
+
return cylinder(length4, radius, void 0, segments).pointAlong([1, 0, 0]).translate(xCenter - length4 / 2, 0, 0);
|
|
50983
|
+
}
|
|
50984
|
+
function tubeAlongX(length4, outerRadius, innerRadius, xCenter, segments) {
|
|
50985
|
+
return cylinderAlongX(length4, outerRadius, xCenter, segments).subtract(cylinderAlongX(length4 + 0.4, innerRadius, xCenter, segments));
|
|
50986
|
+
}
|
|
50987
|
+
function cylinderAlongY(length4, radius, yCenter, segments) {
|
|
50988
|
+
return cylinder(length4, radius, void 0, segments).pointAlong([0, 1, 0]).translate(0, yCenter - length4 / 2, 0);
|
|
50989
|
+
}
|
|
50990
|
+
function tubeAlongY(length4, outerRadius, innerRadius, yCenter, segments) {
|
|
50991
|
+
return cylinderAlongY(length4, outerRadius, yCenter, segments).subtract(cylinderAlongY(length4 + 0.4, innerRadius, yCenter, segments));
|
|
50992
|
+
}
|
|
50993
|
+
function tubeAlongZ(height, outerRadius, innerRadius, segments) {
|
|
50994
|
+
return cylinder(height, outerRadius, void 0, segments).subtract(
|
|
50995
|
+
cylinder(height + 0.4, innerRadius, void 0, segments).translate(0, 0, -0.2)
|
|
50996
|
+
);
|
|
50997
|
+
}
|
|
50998
|
+
function washerAlongX(size, xCenter, segments) {
|
|
50999
|
+
const dims = WASHER_TABLE[size];
|
|
51000
|
+
return washer(size, { segments }).pointAlong([1, 0, 0]).translate(xCenter - dims.t / 2, 0, 0);
|
|
51001
|
+
}
|
|
51002
|
+
function resolveBoltInset(raw, fallback) {
|
|
51003
|
+
if (raw === void 0) return [fallback, fallback];
|
|
51004
|
+
if (typeof raw === "number") return [requirePositive$6(raw, "boltInset"), requirePositive$6(raw, "boltInset")];
|
|
51005
|
+
if (raw.length !== 2) throw new Error("boltInset tuple must be [x, y]");
|
|
51006
|
+
return [requirePositive$6(raw[0], "boltInset[0]"), requirePositive$6(raw[1], "boltInset[1]")];
|
|
51007
|
+
}
|
|
51008
|
+
function validateBoltPositionsForServiceCover(args) {
|
|
51009
|
+
args.positions.forEach(([x2, y2], index2) => {
|
|
51010
|
+
if (!Number.isFinite(x2) || !Number.isFinite(y2)) {
|
|
51011
|
+
throw new Error(`boltedServiceCover: boltPositions[${index2}] must contain finite numbers`);
|
|
51012
|
+
}
|
|
51013
|
+
if (Math.abs(x2) + args.holeRadius >= args.coverWidth / 2 || Math.abs(y2) + args.holeRadius >= args.coverDepth / 2) {
|
|
51014
|
+
throw new Error(`boltedServiceCover: boltPositions[${index2}] is too close to the cover edge`);
|
|
51015
|
+
}
|
|
51016
|
+
const overlapsOpening = Math.abs(x2) - args.holeRadius <= args.openingWidth / 2 && Math.abs(y2) - args.holeRadius <= args.openingDepth / 2;
|
|
51017
|
+
if (overlapsOpening) {
|
|
51018
|
+
throw new Error(
|
|
51019
|
+
`boltedServiceCover: boltPositions[${index2}] lands over the service opening; decrease boltInset, increase ledgeWidth, or provide a smaller opening`
|
|
51020
|
+
);
|
|
51021
|
+
}
|
|
51022
|
+
});
|
|
51023
|
+
}
|
|
51024
|
+
function placeCutterAtPositions(cutter, positions, z2) {
|
|
51025
|
+
return union(...positions.map(([x2, y2]) => cutter.translate(x2, y2, z2)));
|
|
51026
|
+
}
|
|
51027
|
+
function boltedServiceCover(options) {
|
|
51028
|
+
const width = requirePositive$6(options.width, "width");
|
|
51029
|
+
const depth = requirePositive$6(options.depth, "depth");
|
|
51030
|
+
const coverThickness = requirePositive$6(options.coverThickness ?? 3, "coverThickness");
|
|
51031
|
+
const parentThickness = requirePositive$6(options.parentThickness ?? 8, "parentThickness");
|
|
51032
|
+
const ledgeWidth = requirePositive$6(options.ledgeWidth ?? 8, "ledgeWidth");
|
|
51033
|
+
const gasketThickness = Math.max(0, options.gasketThickness ?? 0.8);
|
|
51034
|
+
const gasketInset = Math.max(0, options.gasketInset ?? 2);
|
|
51035
|
+
const screwSize = options.screwSize ?? "M4";
|
|
51036
|
+
const segments = options.segments ?? 36;
|
|
51037
|
+
const sizeData = METRIC_HOLE_TABLE[screwSize];
|
|
51038
|
+
if (!sizeData) throw new Error(`boltedServiceCover: unsupported screwSize "${screwSize}"`);
|
|
51039
|
+
const screwLength = requirePositive$6(
|
|
51040
|
+
options.screwLength ?? parentThickness + gasketThickness + coverThickness + 4,
|
|
51041
|
+
"screwLength"
|
|
51042
|
+
);
|
|
51043
|
+
const coverFit = options.coverFit ?? "normal";
|
|
51044
|
+
const counterboreEnabled = options.counterbore ?? true;
|
|
51045
|
+
const [insetX, insetY] = resolveBoltInset(options.boltInset, Math.max(ledgeWidth * 0.65, sizeData.head * 0.75));
|
|
51046
|
+
if (insetX * 2 >= width || insetY * 2 >= depth) {
|
|
51047
|
+
throw new Error("boltedServiceCover: boltInset leaves no room for a four-corner bolt pattern");
|
|
51048
|
+
}
|
|
51049
|
+
const boltPositions = options.boltPositions ?? [
|
|
51050
|
+
[-width / 2 + insetX, -depth / 2 + insetY],
|
|
51051
|
+
[width / 2 - insetX, -depth / 2 + insetY],
|
|
51052
|
+
[-width / 2 + insetX, depth / 2 - insetY],
|
|
51053
|
+
[width / 2 - insetX, depth / 2 - insetY]
|
|
51054
|
+
];
|
|
51055
|
+
if (boltPositions.length === 0) throw new Error("boltedServiceCover: boltPositions must contain at least one point");
|
|
51056
|
+
const parentWidth = width + ledgeWidth * 2;
|
|
51057
|
+
const parentDepth = depth + ledgeWidth * 2;
|
|
51058
|
+
const openingWidth = Math.max(1, width - ledgeWidth * 2);
|
|
51059
|
+
const openingDepth = Math.max(1, depth - ledgeWidth * 2);
|
|
51060
|
+
validateBoltPositionsForServiceCover({
|
|
51061
|
+
positions: boltPositions,
|
|
51062
|
+
coverWidth: width,
|
|
51063
|
+
coverDepth: depth,
|
|
51064
|
+
openingWidth,
|
|
51065
|
+
openingDepth,
|
|
51066
|
+
holeRadius: sizeData[coverFit] / 2
|
|
51067
|
+
});
|
|
51068
|
+
const coverHole = fastenerHole({
|
|
51069
|
+
size: screwSize,
|
|
51070
|
+
fit: coverFit,
|
|
51071
|
+
depth: coverThickness + 0.6,
|
|
51072
|
+
center: true,
|
|
51073
|
+
segments,
|
|
51074
|
+
...counterboreEnabled ? { counterbore: { depth: Math.min(coverThickness * 0.6, Math.max(0.6, coverThickness - 0.4)) } } : {}
|
|
51075
|
+
});
|
|
51076
|
+
const parentTap = fastenerHole({ size: screwSize, fit: "tap", depth: parentThickness + 0.6, center: true, segments });
|
|
51077
|
+
const parentThreadEnvelope = fastenerHole({
|
|
51078
|
+
size: screwSize,
|
|
51079
|
+
fit: "close",
|
|
51080
|
+
depth: parentThickness + 0.6,
|
|
51081
|
+
center: true,
|
|
51082
|
+
segments
|
|
51083
|
+
});
|
|
51084
|
+
const openingCutter = box(openingWidth, openingDepth, parentThickness + 1).translate(0, 0, -0.5);
|
|
51085
|
+
const parentTappedPattern = placeCutterAtPositions(parentTap, boltPositions, parentThickness / 2);
|
|
51086
|
+
const parentThreadEnvelopePattern = placeCutterAtPositions(parentThreadEnvelope, boltPositions, parentThickness / 2);
|
|
51087
|
+
const parent = box(parentWidth, parentDepth, parentThickness).subtract(openingCutter).subtract(parentThreadEnvelopePattern).color("#4b5563");
|
|
51088
|
+
let coverBlank = box(width, depth, coverThickness);
|
|
51089
|
+
if (options.pullTabs ?? true) {
|
|
51090
|
+
const tabWidth = Math.min(width * 0.18, Math.max(sizeData.head * 1.6, 12));
|
|
51091
|
+
const tabDepth = Math.max(4, coverThickness * 1.4);
|
|
51092
|
+
const tabOverlap = Math.min(0.5, tabDepth * 0.25);
|
|
51093
|
+
const tabY = -depth / 2 - tabDepth / 2 + tabOverlap;
|
|
51094
|
+
const tabX = width * 0.23;
|
|
51095
|
+
coverBlank = union(
|
|
51096
|
+
coverBlank,
|
|
51097
|
+
box(tabWidth, tabDepth, coverThickness).translate(-tabX, tabY, 0),
|
|
51098
|
+
box(tabWidth, tabDepth, coverThickness).translate(tabX, tabY, 0)
|
|
51099
|
+
);
|
|
51100
|
+
}
|
|
51101
|
+
const coverClearancePattern = placeCutterAtPositions(coverHole, boltPositions, coverThickness / 2);
|
|
51102
|
+
const cover2 = coverBlank.subtract(coverClearancePattern).translate(0, 0, parentThickness + gasketThickness).color("#334155");
|
|
51103
|
+
const gasket = gasketThickness > 0 ? box(Math.max(1, width - gasketInset * 2), Math.max(1, depth - gasketInset * 2), gasketThickness).subtract(placeCutterAtPositions(coverHole, boltPositions, gasketThickness / 2)).translate(0, 0, parentThickness).color("#111827") : null;
|
|
51104
|
+
const hardware = fastenerSet(screwSize, screwLength, { washerUnderHead: false, washerUnderNut: false, fit: coverFit, segments });
|
|
51105
|
+
const screwOriginZ = parentThickness + gasketThickness + coverThickness;
|
|
51106
|
+
const screws = boltPositions.map(([x2, y2]) => hardware.bolt.translate(x2, y2, screwOriginZ).color("#94a3b8"));
|
|
51107
|
+
const parts = [
|
|
51108
|
+
{ name: "service cover parent ledge with threaded hole envelopes", shape: parent },
|
|
51109
|
+
...gasket ? [{ name: "service cover gasket seated on ledge", shape: gasket }] : [],
|
|
51110
|
+
{ name: "bolted service cover plate with fused pull tabs", shape: cover2 },
|
|
51111
|
+
...screws.map((shape, index2) => ({ name: `installed ${screwSize} cover screw ${index2 + 1}`, shape }))
|
|
51112
|
+
];
|
|
51113
|
+
return {
|
|
51114
|
+
parts,
|
|
51115
|
+
parent,
|
|
51116
|
+
cover: cover2,
|
|
51117
|
+
gasket,
|
|
51118
|
+
screws,
|
|
51119
|
+
boltPositions,
|
|
51120
|
+
cutters: {
|
|
51121
|
+
coverClearance: coverClearancePattern,
|
|
51122
|
+
parentTapped: parentTappedPattern,
|
|
51123
|
+
parentThreadEnvelope: parentThreadEnvelopePattern
|
|
51124
|
+
},
|
|
51125
|
+
dims: {
|
|
51126
|
+
width,
|
|
51127
|
+
depth,
|
|
51128
|
+
coverThickness,
|
|
51129
|
+
parentThickness,
|
|
51130
|
+
ledgeWidth,
|
|
51131
|
+
gasketThickness,
|
|
51132
|
+
screwSize,
|
|
51133
|
+
screwLength,
|
|
51134
|
+
clearanceDia: sizeData[coverFit],
|
|
51135
|
+
tapDia: sizeData.tap,
|
|
51136
|
+
threadEnvelopeDia: sizeData.close
|
|
51137
|
+
}
|
|
51138
|
+
};
|
|
51139
|
+
}
|
|
51140
|
+
function datumEnclosureAssembly(options) {
|
|
51141
|
+
const width = requirePositive$6(options.width, "width");
|
|
51142
|
+
const depth = requirePositive$6(options.depth, "depth");
|
|
51143
|
+
const height = requirePositive$6(options.height, "height");
|
|
51144
|
+
const wallThickness = requirePositive$6(options.wallThickness ?? 2.4, "wallThickness");
|
|
51145
|
+
const baseThickness = requirePositive$6(options.baseThickness ?? wallThickness, "baseThickness");
|
|
51146
|
+
const coverThickness = requirePositive$6(options.coverThickness ?? 2.4, "coverThickness");
|
|
51147
|
+
const ledgeWidth = requirePositive$6(options.ledgeWidth ?? Math.max(3.6, wallThickness * 1.35), "ledgeWidth");
|
|
51148
|
+
const gasketThickness = requireNonNegative(options.gasketThickness ?? 0.8, "gasketThickness");
|
|
51149
|
+
const faceClearance = requirePositive$6(options.faceClearance ?? 0.04, "faceClearance");
|
|
51150
|
+
const screwSize = options.screwSize ?? "M3";
|
|
51151
|
+
const coverFit = options.coverFit ?? "normal";
|
|
51152
|
+
const segments = options.segments ?? 32;
|
|
51153
|
+
const sizeData = METRIC_HOLE_TABLE[screwSize];
|
|
51154
|
+
if (!sizeData) throw new Error(`datumEnclosureAssembly: unsupported screwSize "${screwSize}"`);
|
|
51155
|
+
const innerWidth = width - wallThickness * 2;
|
|
51156
|
+
const innerDepth = depth - wallThickness * 2;
|
|
51157
|
+
if (innerWidth <= ledgeWidth * 2 + 8 || innerDepth <= ledgeWidth * 2 + 8) {
|
|
51158
|
+
throw new Error("datumEnclosureAssembly: wallThickness and ledgeWidth leave too little internal opening");
|
|
51159
|
+
}
|
|
51160
|
+
if (height <= baseThickness + coverThickness + 4) {
|
|
51161
|
+
throw new Error("datumEnclosureAssembly: height must leave room for internal ribs and standoffs");
|
|
51162
|
+
}
|
|
51163
|
+
const standoffDiameter = requirePositive$6(
|
|
51164
|
+
options.standoffDiameter ?? Math.max(sizeData.head * 1.65, sizeData.close * 2.2),
|
|
51165
|
+
"standoffDiameter"
|
|
51166
|
+
);
|
|
51167
|
+
const minInset = wallThickness + Math.max(ledgeWidth, standoffDiameter / 2 + 1.2);
|
|
51168
|
+
const [insetX, insetY] = resolveBoltInset(options.screwInset, minInset);
|
|
51169
|
+
if (insetX * 2 >= width || insetY * 2 >= depth) {
|
|
51170
|
+
throw new Error("datumEnclosureAssembly: screwInset leaves no room for the standoff datum");
|
|
51171
|
+
}
|
|
51172
|
+
const screwPositions = options.screwPositions ?? [
|
|
51173
|
+
[-width / 2 + insetX, -depth / 2 + insetY],
|
|
51174
|
+
[width / 2 - insetX, -depth / 2 + insetY],
|
|
51175
|
+
[-width / 2 + insetX, depth / 2 - insetY],
|
|
51176
|
+
[width / 2 - insetX, depth / 2 - insetY]
|
|
51177
|
+
];
|
|
51178
|
+
if (screwPositions.length === 0) throw new Error("datumEnclosureAssembly: screwPositions must contain at least one point");
|
|
51179
|
+
for (const [index2, [x2, y2]] of screwPositions.entries()) {
|
|
51180
|
+
if (!Number.isFinite(x2) || !Number.isFinite(y2)) {
|
|
51181
|
+
throw new Error(`datumEnclosureAssembly: screwPositions[${index2}] must contain finite numbers`);
|
|
51182
|
+
}
|
|
51183
|
+
if (Math.abs(x2) + standoffDiameter / 2 > innerWidth / 2 || Math.abs(y2) + standoffDiameter / 2 > innerDepth / 2) {
|
|
51184
|
+
throw new Error(`datumEnclosureAssembly: screwPositions[${index2}] does not fit inside the enclosure walls`);
|
|
51185
|
+
}
|
|
51186
|
+
}
|
|
51187
|
+
const ribHeight = requirePositive$6(options.ribHeight ?? Math.min(height * 0.24, Math.max(2.4, baseThickness * 1.4)), "ribHeight");
|
|
51188
|
+
const ribThickness = requirePositive$6(options.ribThickness ?? Math.max(1.2, wallThickness * 0.75), "ribThickness");
|
|
51189
|
+
const portWidth = requirePositive$6(options.portWidth ?? Math.min(innerWidth * 0.28, Math.max(12, width * 0.16)), "portWidth");
|
|
51190
|
+
const portHeight = requirePositive$6(options.portHeight ?? Math.min(height * 0.42, Math.max(5, height * 0.28)), "portHeight");
|
|
51191
|
+
if (portWidth >= innerWidth - ledgeWidth * 2) {
|
|
51192
|
+
throw new Error("datumEnclosureAssembly: portWidth must fit between internal ledges and standoffs");
|
|
51193
|
+
}
|
|
51194
|
+
if (portHeight >= height - baseThickness - 1) {
|
|
51195
|
+
throw new Error("datumEnclosureAssembly: portHeight must leave material above and below the service port");
|
|
51196
|
+
}
|
|
51197
|
+
const screwLength = requirePositive$6(
|
|
51198
|
+
options.screwLength ?? coverThickness + gasketThickness + Math.max(6, height * 0.45),
|
|
51199
|
+
"screwLength"
|
|
51200
|
+
);
|
|
51201
|
+
const coverHole = fastenerHole({
|
|
51202
|
+
size: screwSize,
|
|
51203
|
+
fit: coverFit,
|
|
51204
|
+
depth: coverThickness + 0.6,
|
|
51205
|
+
center: true,
|
|
51206
|
+
segments,
|
|
51207
|
+
counterbore: { depth: Math.min(coverThickness * 0.6, Math.max(0.6, coverThickness - 0.35)) }
|
|
51208
|
+
});
|
|
51209
|
+
const standoffTap = fastenerHole({ size: screwSize, fit: "tap", depth: height + 0.8, center: true, segments });
|
|
51210
|
+
const standoffThreadEnvelope = fastenerHole({ size: screwSize, fit: "close", depth: height + 0.8, center: true, segments });
|
|
51211
|
+
const coverClearance = placeCutterAtPositions(coverHole, screwPositions, coverThickness / 2);
|
|
51212
|
+
const standoffTappedPattern = placeCutterAtPositions(standoffTap, screwPositions, height / 2);
|
|
51213
|
+
const standoffThreadEnvelopePattern = placeCutterAtPositions(standoffThreadEnvelope, screwPositions, height / 2);
|
|
51214
|
+
const fuseOverlap = Math.min(0.06, Math.max(0.02, wallThickness * 0.02));
|
|
51215
|
+
const ledgeThickness = Math.min(Math.max(1.1, coverThickness * 0.45), height * 0.2);
|
|
51216
|
+
const sideX = width / 2 - wallThickness / 2;
|
|
51217
|
+
const sideY = depth / 2 - wallThickness / 2;
|
|
51218
|
+
const ledgeZ = height - ledgeThickness;
|
|
51219
|
+
const baseSolids = [
|
|
51220
|
+
box(width, depth, baseThickness),
|
|
51221
|
+
box(wallThickness, depth, height).translate(sideX, 0, 0),
|
|
51222
|
+
box(wallThickness, depth, height).translate(-sideX, 0, 0),
|
|
51223
|
+
box(width, wallThickness, height).translate(0, sideY, 0),
|
|
51224
|
+
box(width, wallThickness, height).translate(0, -sideY, 0),
|
|
51225
|
+
box(ledgeWidth, innerDepth, ledgeThickness).translate(-width / 2 + wallThickness + ledgeWidth / 2, 0, ledgeZ),
|
|
51226
|
+
box(ledgeWidth, innerDepth, ledgeThickness).translate(width / 2 - wallThickness - ledgeWidth / 2, 0, ledgeZ),
|
|
51227
|
+
box(innerWidth, ledgeWidth, ledgeThickness).translate(0, -depth / 2 + wallThickness + ledgeWidth / 2, ledgeZ),
|
|
51228
|
+
box(innerWidth, ledgeWidth, ledgeThickness).translate(0, depth / 2 - wallThickness - ledgeWidth / 2, ledgeZ),
|
|
51229
|
+
box(Math.max(1, innerWidth - standoffDiameter * 1.8), ribThickness, ribHeight + fuseOverlap).translate(
|
|
51230
|
+
0,
|
|
51231
|
+
0,
|
|
51232
|
+
baseThickness - fuseOverlap
|
|
51233
|
+
),
|
|
51234
|
+
box(ribThickness, Math.max(1, innerDepth - standoffDiameter * 1.8), ribHeight + fuseOverlap).translate(
|
|
51235
|
+
0,
|
|
51236
|
+
0,
|
|
51237
|
+
baseThickness - fuseOverlap
|
|
51238
|
+
),
|
|
51239
|
+
...screwPositions.map(
|
|
51240
|
+
([x2, y2]) => cylinder(height - baseThickness + fuseOverlap, standoffDiameter / 2, void 0, segments).translate(
|
|
51241
|
+
x2,
|
|
51242
|
+
y2,
|
|
51243
|
+
baseThickness - fuseOverlap
|
|
51244
|
+
)
|
|
51245
|
+
)
|
|
51246
|
+
];
|
|
51247
|
+
const servicePort = box(portWidth, wallThickness + 1, portHeight).translate(
|
|
51248
|
+
0,
|
|
51249
|
+
-depth / 2 + wallThickness / 2,
|
|
51250
|
+
baseThickness + Math.max(0.8, (height - baseThickness - portHeight) * 0.35)
|
|
51251
|
+
);
|
|
51252
|
+
const base = union(...baseSolids).subtract(standoffThreadEnvelopePattern).subtract(servicePort).color("#475569");
|
|
51253
|
+
const gasketFrameCutter = box(Math.max(1, width - ledgeWidth * 2), Math.max(1, depth - ledgeWidth * 2), gasketThickness + 0.6).translate(
|
|
51254
|
+
0,
|
|
51255
|
+
0,
|
|
51256
|
+
-0.3
|
|
51257
|
+
);
|
|
51258
|
+
const gasket = gasketThickness > 0 ? box(width, depth, gasketThickness).subtract(gasketFrameCutter).subtract(placeCutterAtPositions(coverHole, screwPositions, gasketThickness / 2)).translate(0, 0, height + faceClearance).color("#111827") : null;
|
|
51259
|
+
const coverZ = height + faceClearance + (gasket ? gasketThickness + faceClearance : 0);
|
|
51260
|
+
const cover2 = box(width, depth, coverThickness).subtract(coverClearance).translate(0, 0, coverZ).color("#334155");
|
|
51261
|
+
const hardware = fastenerSet(screwSize, screwLength, { washerUnderHead: false, washerUnderNut: false, fit: coverFit, segments });
|
|
51262
|
+
const screwOriginZ = coverZ + coverThickness;
|
|
51263
|
+
const screws = screwPositions.map(([x2, y2]) => hardware.bolt.translate(x2, y2, screwOriginZ).color("#94a3b8"));
|
|
51264
|
+
const parts = [
|
|
51265
|
+
{ name: "datum enclosure base tray with walls ribs standoffs and service port", shape: base },
|
|
51266
|
+
...gasket ? [{ name: "datum enclosure gasket seated on continuous ledge", shape: gasket }] : [],
|
|
51267
|
+
{ name: "datum enclosure cover plate with matched screw pattern", shape: cover2 },
|
|
51268
|
+
...screws.map((shape, index2) => ({ name: `installed ${screwSize} enclosure screw ${index2 + 1}`, shape }))
|
|
51269
|
+
];
|
|
51270
|
+
return {
|
|
51271
|
+
parts,
|
|
51272
|
+
base,
|
|
51273
|
+
cover: cover2,
|
|
51274
|
+
gasket,
|
|
51275
|
+
screws,
|
|
51276
|
+
screwPositions,
|
|
51277
|
+
cutters: {
|
|
51278
|
+
coverClearance,
|
|
51279
|
+
standoffTapped: standoffTappedPattern,
|
|
51280
|
+
standoffThreadEnvelope: standoffThreadEnvelopePattern,
|
|
51281
|
+
servicePort
|
|
51282
|
+
},
|
|
51283
|
+
dims: {
|
|
51284
|
+
width,
|
|
51285
|
+
depth,
|
|
51286
|
+
height,
|
|
51287
|
+
innerWidth,
|
|
51288
|
+
innerDepth,
|
|
51289
|
+
wallThickness,
|
|
51290
|
+
baseThickness,
|
|
51291
|
+
coverThickness,
|
|
51292
|
+
ledgeWidth,
|
|
51293
|
+
gasketThickness,
|
|
51294
|
+
faceClearance,
|
|
51295
|
+
screwSize,
|
|
51296
|
+
screwLength,
|
|
51297
|
+
standoffDiameter,
|
|
51298
|
+
ribHeight,
|
|
51299
|
+
ribThickness,
|
|
51300
|
+
portWidth,
|
|
51301
|
+
portHeight,
|
|
51302
|
+
clearanceDia: sizeData[coverFit],
|
|
51303
|
+
tapDia: sizeData.tap,
|
|
51304
|
+
threadEnvelopeDia: sizeData.close
|
|
51305
|
+
}
|
|
51306
|
+
};
|
|
51307
|
+
}
|
|
51308
|
+
function snapLatchCoverAssembly(options) {
|
|
51309
|
+
const width = requirePositive$6(options.width, "width");
|
|
51310
|
+
const depth = requirePositive$6(options.depth, "depth");
|
|
51311
|
+
const coverThickness = requirePositive$6(options.coverThickness ?? 2.4, "coverThickness");
|
|
51312
|
+
const parentThickness = requirePositive$6(options.parentThickness ?? 6, "parentThickness");
|
|
51313
|
+
const ledgeWidth = requirePositive$6(options.ledgeWidth ?? 8, "ledgeWidth");
|
|
51314
|
+
const runningClearance = requirePositive$6(options.runningClearance ?? 0.25, "runningClearance");
|
|
51315
|
+
const faceClearance = requirePositive$6(options.faceClearance ?? 0.04, "faceClearance");
|
|
51316
|
+
const latchWidth = requirePositive$6(options.latchWidth ?? Math.min(width * 0.22, Math.max(12, width * 0.16)), "latchWidth");
|
|
51317
|
+
const latchThickness = requirePositive$6(options.latchThickness ?? 1.6, "latchThickness");
|
|
51318
|
+
const hookThrow = requirePositive$6(options.hookThrow ?? 3.2, "hookThrow");
|
|
51319
|
+
const hookThickness = requirePositive$6(options.hookThickness ?? 1.6, "hookThickness");
|
|
51320
|
+
const openingWidth = width - ledgeWidth * 2;
|
|
51321
|
+
const openingDepth = depth - ledgeWidth * 2;
|
|
51322
|
+
if (openingWidth <= Math.max(8, latchWidth * 0.8) || openingDepth <= 8) {
|
|
51323
|
+
throw new Error("snapLatchCoverAssembly: ledgeWidth leaves too little service opening under the cover");
|
|
51324
|
+
}
|
|
51325
|
+
if (latchWidth >= openingWidth) {
|
|
51326
|
+
throw new Error("snapLatchCoverAssembly: latchWidth must fit along the receiver opening");
|
|
51327
|
+
}
|
|
51328
|
+
if (latchThickness + runningClearance * 2 >= ledgeWidth) {
|
|
51329
|
+
throw new Error("snapLatchCoverAssembly: latchThickness and clearance must fit inside the receiver ledge");
|
|
51330
|
+
}
|
|
51331
|
+
if (hookThrow + latchThickness / 2 + runningClearance >= ledgeWidth * 1.5) {
|
|
51332
|
+
throw new Error("snapLatchCoverAssembly: hookThrow is too large for the available underside catch land");
|
|
51333
|
+
}
|
|
51334
|
+
const parentWidth = width + ledgeWidth * 2;
|
|
51335
|
+
const parentDepth = depth + ledgeWidth * 2;
|
|
51336
|
+
const fuseOverlap = Math.min(0.04, faceClearance * 0.7);
|
|
51337
|
+
const hookClearance = Math.min(0.08, runningClearance * 0.32);
|
|
51338
|
+
const coverMinZ = parentThickness + faceClearance;
|
|
51339
|
+
const stemMinZ = -hookClearance - hookThickness;
|
|
51340
|
+
const stemHeight = coverMinZ + fuseOverlap - stemMinZ;
|
|
51341
|
+
const slotY = openingDepth / 2 + ledgeWidth / 2;
|
|
51342
|
+
const latchWindow = (sign2) => box(latchWidth + runningClearance * 2, latchThickness + runningClearance * 2, parentThickness + 0.8).translate(
|
|
51343
|
+
0,
|
|
51344
|
+
sign2 * slotY,
|
|
51345
|
+
-0.4
|
|
51346
|
+
);
|
|
51347
|
+
const latchWindows = union(latchWindow(1), latchWindow(-1));
|
|
51348
|
+
const serviceOpening = box(openingWidth, openingDepth, parentThickness + 1).translate(0, 0, -0.5);
|
|
51349
|
+
const parent = box(parentWidth, parentDepth, parentThickness).subtract(serviceOpening).subtract(latchWindows).color("#475569");
|
|
51350
|
+
const coverPlate = box(width, depth, coverThickness).translate(0, 0, coverMinZ);
|
|
51351
|
+
const snapHook = (sign2) => {
|
|
51352
|
+
const y2 = sign2 * slotY;
|
|
51353
|
+
const stem = box(latchWidth, latchThickness, stemHeight).translate(0, y2, stemMinZ);
|
|
51354
|
+
const barb = box(latchWidth, latchThickness + hookThrow, hookThickness).translate(
|
|
51355
|
+
0,
|
|
51356
|
+
y2 + sign2 * (hookThrow / 2),
|
|
51357
|
+
stemMinZ
|
|
51358
|
+
);
|
|
51359
|
+
const rootRib = box(latchWidth, Math.max(latchThickness, hookThrow * 0.55), coverThickness * 0.65).translate(
|
|
51360
|
+
0,
|
|
51361
|
+
y2 - sign2 * (ledgeWidth * 0.18),
|
|
51362
|
+
coverMinZ
|
|
51363
|
+
);
|
|
51364
|
+
return union(stem, barb, rootRib);
|
|
51365
|
+
};
|
|
51366
|
+
const cover2 = union(coverPlate, snapHook(1), snapHook(-1)).color("#111827");
|
|
51367
|
+
const parts = [
|
|
51368
|
+
{ name: "snap cover receiver frame with latch windows and catch lands", shape: parent },
|
|
51369
|
+
{ name: "one-piece snap cover with fused hooks and underside barbs", shape: cover2 }
|
|
51370
|
+
];
|
|
51371
|
+
return {
|
|
51372
|
+
parts,
|
|
51373
|
+
parent,
|
|
51374
|
+
cover: cover2,
|
|
51375
|
+
cutters: {
|
|
51376
|
+
serviceOpening,
|
|
51377
|
+
latchWindows
|
|
51378
|
+
},
|
|
51379
|
+
dims: {
|
|
51380
|
+
width,
|
|
51381
|
+
depth,
|
|
51382
|
+
parentWidth,
|
|
51383
|
+
parentDepth,
|
|
51384
|
+
openingWidth,
|
|
51385
|
+
openingDepth,
|
|
51386
|
+
coverThickness,
|
|
51387
|
+
parentThickness,
|
|
51388
|
+
ledgeWidth,
|
|
51389
|
+
latchWidth,
|
|
51390
|
+
latchThickness,
|
|
51391
|
+
hookThrow,
|
|
51392
|
+
hookThickness,
|
|
51393
|
+
runningClearance,
|
|
51394
|
+
faceClearance
|
|
51395
|
+
}
|
|
51396
|
+
};
|
|
51397
|
+
}
|
|
51398
|
+
function pinnedLeverAssembly(options) {
|
|
51399
|
+
const armLength = requirePositive$6(options.armLength, "armLength");
|
|
51400
|
+
const armWidth = requirePositive$6(options.armWidth ?? 10, "armWidth");
|
|
51401
|
+
const leverThickness = requirePositive$6(options.leverThickness ?? 5, "leverThickness");
|
|
51402
|
+
const pinDiameter = requirePositive$6(options.pinDiameter ?? 5, "pinDiameter");
|
|
51403
|
+
const pinClearance = requireNonNegative(options.pinClearance ?? 0.25, "pinClearance");
|
|
51404
|
+
const boreDiameter = pinDiameter + pinClearance;
|
|
51405
|
+
const hubRadius = requirePositive$6(options.hubRadius ?? Math.max(armWidth * 0.85, pinDiameter * 1.8), "hubRadius");
|
|
51406
|
+
const supportThickness = requirePositive$6(options.supportThickness ?? Math.max(6, pinDiameter * 1.4), "supportThickness");
|
|
51407
|
+
const supportWidth = requirePositive$6(options.supportWidth ?? hubRadius * 2 + 18, "supportWidth");
|
|
51408
|
+
const supportDepth = requirePositive$6(options.supportDepth ?? Math.max(armWidth + 18, hubRadius * 2 + 10), "supportDepth");
|
|
51409
|
+
const washerSize = options.washerSize ?? metricWasherSizeForPin(pinDiameter);
|
|
51410
|
+
const washerDims = WASHER_TABLE[washerSize];
|
|
51411
|
+
if (!washerDims) throw new Error(`pinnedLeverAssembly: unsupported washerSize "${washerSize}"`);
|
|
51412
|
+
if (washerDims.id <= pinDiameter) {
|
|
51413
|
+
throw new Error(`pinnedLeverAssembly: ${washerSize} washer inner diameter is too small for a ${pinDiameter} mm pin`);
|
|
51414
|
+
}
|
|
51415
|
+
if (hubRadius <= boreDiameter / 2 + Math.max(1, pinDiameter * 0.25)) {
|
|
51416
|
+
throw new Error("pinnedLeverAssembly: hubRadius leaves too little material around the pivot bore");
|
|
51417
|
+
}
|
|
51418
|
+
if (supportWidth <= boreDiameter + 4 || supportDepth <= boreDiameter + 4) {
|
|
51419
|
+
throw new Error("pinnedLeverAssembly: support dimensions leave too little material around the pivot bore");
|
|
51420
|
+
}
|
|
51421
|
+
const segments = options.segments ?? 40;
|
|
51422
|
+
const gripLength = requirePositive$6(options.gripLength ?? Math.min(armLength * 0.32, Math.max(16, armWidth * 2.4)), "gripLength");
|
|
51423
|
+
const gripWidth = requirePositive$6(options.gripWidth ?? armWidth * 1.55, "gripWidth");
|
|
51424
|
+
if (gripLength >= armLength) throw new Error("pinnedLeverAssembly: gripLength must be shorter than armLength");
|
|
51425
|
+
const armOverlap = Math.min(hubRadius * 0.65, armLength * 0.25);
|
|
51426
|
+
const armStartX = hubRadius - armOverlap;
|
|
51427
|
+
const armCenterX = armStartX + armLength / 2;
|
|
51428
|
+
const gripCenterX = armStartX + armLength - gripLength / 2;
|
|
51429
|
+
const runningClearance = 0.03;
|
|
51430
|
+
const lowerWasherZ = supportThickness + runningClearance;
|
|
51431
|
+
const leverZ = lowerWasherZ + washerDims.t + runningClearance;
|
|
51432
|
+
const upperWasherZ = leverZ + leverThickness + runningClearance;
|
|
51433
|
+
const stackHeight = upperWasherZ + washerDims.t;
|
|
51434
|
+
const pinHeadThickness = Math.max(washerDims.t, pinDiameter * 0.35);
|
|
51435
|
+
const pinHeadRadius = Math.max(washerDims.od * 0.42, pinDiameter * 0.8);
|
|
51436
|
+
const supportBore = cylinder(supportThickness + 1, boreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
|
|
51437
|
+
let supportBlank = box(supportWidth, supportDepth, supportThickness);
|
|
51438
|
+
if (options.stopBlock ?? true) {
|
|
51439
|
+
const stopLength = Math.min(armLength * 0.22, Math.max(10, armWidth * 1.4));
|
|
51440
|
+
const stopWidth = Math.max(4, pinDiameter * 0.7);
|
|
51441
|
+
const stopHeight = supportThickness;
|
|
51442
|
+
const stopX = hubRadius + stopLength / 2;
|
|
51443
|
+
const stopY = armWidth / 2 + stopWidth / 2 + runningClearance;
|
|
51444
|
+
supportBlank = union(supportBlank, box(stopLength, stopWidth, stopHeight).translate(stopX, stopY, 0));
|
|
51445
|
+
}
|
|
51446
|
+
const support = supportBlank.subtract(supportBore).color("#475569");
|
|
51447
|
+
const hub = cylinder(leverThickness, hubRadius, void 0, segments);
|
|
51448
|
+
const arm = box(armLength, armWidth, leverThickness).translate(armCenterX, 0, 0);
|
|
51449
|
+
const grip = box(gripLength, gripWidth, leverThickness).translate(gripCenterX, 0, 0);
|
|
51450
|
+
const leverSolids = [hub, arm, grip];
|
|
51451
|
+
if (options.detentBoss ?? true) {
|
|
51452
|
+
const bossRadius = Math.min(armWidth * 0.42, hubRadius * 0.42);
|
|
51453
|
+
const bossX = hubRadius + Math.min(armLength * 0.22, armWidth * 2);
|
|
51454
|
+
const bossY = -armWidth / 2 - bossRadius * 0.45;
|
|
51455
|
+
leverSolids.push(cylinder(leverThickness, bossRadius, void 0, segments).translate(bossX, bossY, 0));
|
|
51456
|
+
}
|
|
51457
|
+
const leverBore = cylinder(leverThickness + 1, boreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
|
|
51458
|
+
const lever = union(...leverSolids).subtract(leverBore).translate(0, 0, leverZ).color("#7f1d1d");
|
|
51459
|
+
const lowerWasher = washer(washerSize, { segments }).translate(0, 0, lowerWasherZ).color("#94a3b8");
|
|
51460
|
+
const upperWasher = washer(washerSize, { segments }).translate(0, 0, upperWasherZ).color("#94a3b8");
|
|
51461
|
+
const shaft = cylinder(stackHeight, pinDiameter / 2, void 0, segments);
|
|
51462
|
+
const lowerRetainer = cylinder(pinHeadThickness, pinHeadRadius, void 0, segments).translate(0, 0, -pinHeadThickness - runningClearance);
|
|
51463
|
+
const upperHead = cylinder(pinHeadThickness, pinHeadRadius, void 0, segments).translate(0, 0, stackHeight + runningClearance);
|
|
51464
|
+
const pin = union(shaft, lowerRetainer, upperHead).color("#cbd5e1");
|
|
51465
|
+
const pivotBore = cylinder(stackHeight + 1, boreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
|
|
51466
|
+
const parts = [
|
|
51467
|
+
{ name: "pivot support block with bearing bore and low stop land", shape: support },
|
|
51468
|
+
{ name: "lower thrust washer under pinned lever", shape: lowerWasher },
|
|
51469
|
+
{ name: "fused pinned lever with hub arm grip and detent boss", shape: lever },
|
|
51470
|
+
{ name: "upper thrust washer over pinned lever", shape: upperWasher },
|
|
51471
|
+
{ name: "retained pivot pin through lever stack", shape: pin }
|
|
51472
|
+
];
|
|
51473
|
+
return {
|
|
51474
|
+
parts,
|
|
51475
|
+
support,
|
|
51476
|
+
lever,
|
|
51477
|
+
pin,
|
|
51478
|
+
washers: {
|
|
51479
|
+
lower: lowerWasher,
|
|
51480
|
+
upper: upperWasher
|
|
51481
|
+
},
|
|
51482
|
+
cutters: {
|
|
51483
|
+
pivotBore
|
|
51484
|
+
},
|
|
51485
|
+
dims: {
|
|
51486
|
+
armLength,
|
|
51487
|
+
armWidth,
|
|
51488
|
+
leverThickness,
|
|
51489
|
+
hubRadius,
|
|
51490
|
+
pinDiameter,
|
|
51491
|
+
boreDiameter,
|
|
51492
|
+
supportWidth,
|
|
51493
|
+
supportDepth,
|
|
51494
|
+
supportThickness,
|
|
51495
|
+
washerSize,
|
|
51496
|
+
washerThickness: washerDims.t,
|
|
51497
|
+
stackHeight
|
|
51498
|
+
}
|
|
51499
|
+
};
|
|
51500
|
+
}
|
|
51501
|
+
function retainedShaftAssembly(options) {
|
|
51502
|
+
const supportSpacing = requirePositive$6(options.supportSpacing, "supportSpacing");
|
|
51503
|
+
const shaftDiameter = requirePositive$6(options.shaftDiameter ?? 8, "shaftDiameter");
|
|
51504
|
+
const boreClearance = requireNonNegative(options.boreClearance ?? 0.35, "boreClearance");
|
|
51505
|
+
const boreDiameter = shaftDiameter + boreClearance;
|
|
51506
|
+
const supportThickness = requirePositive$6(options.supportThickness ?? Math.max(5, shaftDiameter * 0.75), "supportThickness");
|
|
51507
|
+
const washerSize = options.washerSize ?? metricWasherSizeForPin(shaftDiameter);
|
|
51508
|
+
const washerDims = WASHER_TABLE[washerSize];
|
|
51509
|
+
if (!washerDims) throw new Error(`retainedShaftAssembly: unsupported washerSize "${washerSize}"`);
|
|
51510
|
+
if (washerDims.id <= shaftDiameter) {
|
|
51511
|
+
throw new Error(`retainedShaftAssembly: ${washerSize} washer inner diameter is too small for a ${shaftDiameter} mm shaft`);
|
|
51512
|
+
}
|
|
51513
|
+
const knobDiameter = requirePositive$6(options.knobDiameter ?? shaftDiameter * 3, "knobDiameter");
|
|
51514
|
+
const knobThickness = requirePositive$6(options.knobThickness ?? Math.max(8, shaftDiameter), "knobThickness");
|
|
51515
|
+
const retainerThickness = requirePositive$6(
|
|
51516
|
+
options.retainerThickness ?? Math.max(washerDims.t, shaftDiameter * 0.35),
|
|
51517
|
+
"retainerThickness"
|
|
51518
|
+
);
|
|
51519
|
+
const runningClearance = requireNonNegative(options.runningClearance ?? 0.05, "runningClearance");
|
|
51520
|
+
const supportWidth = requirePositive$6(options.supportWidth ?? Math.max(28, knobDiameter * 1.25), "supportWidth");
|
|
51521
|
+
const supportHeight = requirePositive$6(options.supportHeight ?? Math.max(34, knobDiameter * 1.45), "supportHeight");
|
|
51522
|
+
const segments = options.segments ?? 40;
|
|
51523
|
+
if (supportSpacing <= supportThickness) {
|
|
51524
|
+
throw new Error("retainedShaftAssembly: supportSpacing must leave a gap between support cheeks");
|
|
51525
|
+
}
|
|
51526
|
+
if (supportWidth <= boreDiameter + 4 || supportHeight <= boreDiameter + 4) {
|
|
51527
|
+
throw new Error("retainedShaftAssembly: support dimensions leave too little material around the shaft bore");
|
|
51528
|
+
}
|
|
51529
|
+
const leftSupportX = -supportSpacing / 2;
|
|
51530
|
+
const rightSupportX = supportSpacing / 2;
|
|
51531
|
+
const leftOuterFaceX = leftSupportX - supportThickness / 2;
|
|
51532
|
+
const rightOuterFaceX = rightSupportX + supportThickness / 2;
|
|
51533
|
+
const leftWasherX = leftOuterFaceX - runningClearance - washerDims.t / 2;
|
|
51534
|
+
const rightWasherX = rightOuterFaceX + runningClearance + washerDims.t / 2;
|
|
51535
|
+
const leftKnobX = leftOuterFaceX - runningClearance * 2 - washerDims.t - knobThickness / 2;
|
|
51536
|
+
const rightKnobX = rightOuterFaceX + runningClearance * 2 + washerDims.t + knobThickness / 2;
|
|
51537
|
+
const leftStackOuterX = leftKnobX - knobThickness / 2;
|
|
51538
|
+
const rightStackOuterX = rightKnobX + knobThickness / 2;
|
|
51539
|
+
const minimumShaftLength = rightStackOuterX - leftStackOuterX + retainerThickness * 2 + runningClearance * 2;
|
|
51540
|
+
const shaftLength = requirePositive$6(options.shaftLength ?? minimumShaftLength, "shaftLength");
|
|
51541
|
+
if (shaftLength < minimumShaftLength) {
|
|
51542
|
+
throw new Error("retainedShaftAssembly: shaftLength is too short to retain both supports, washers, and knobs");
|
|
51543
|
+
}
|
|
51544
|
+
const supportBore = cylinderAlongX(supportThickness + 1, boreDiameter / 2, 0, segments);
|
|
51545
|
+
const makeSupport = (x2) => box(supportThickness, supportWidth, supportHeight).translate(x2, 0, -supportHeight / 2).subtract(supportBore.translate(x2, 0, 0)).color("#334155");
|
|
51546
|
+
const knobBore = cylinder(knobThickness + 1, boreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
|
|
51547
|
+
const makeKnob = (x2) => cylinder(knobThickness, knobDiameter / 2, void 0, 18).subtract(knobBore).pointAlong([1, 0, 0]).translate(x2 - knobThickness / 2, 0, 0).color("#111827");
|
|
51548
|
+
const retainerRadius = Math.max(shaftDiameter * 0.85, knobDiameter * 0.36);
|
|
51549
|
+
const shaftCore = cylinderAlongX(shaftLength, shaftDiameter / 2, 0, segments);
|
|
51550
|
+
const leftRetainer = cylinderAlongX(retainerThickness, retainerRadius, -shaftLength / 2 + retainerThickness / 2, segments);
|
|
51551
|
+
const rightRetainer = cylinderAlongX(retainerThickness, retainerRadius, shaftLength / 2 - retainerThickness / 2, segments);
|
|
51552
|
+
const shaft = union(shaftCore, leftRetainer, rightRetainer).color("#cbd5e1");
|
|
51553
|
+
const leftSupport = makeSupport(leftSupportX);
|
|
51554
|
+
const rightSupport = makeSupport(rightSupportX);
|
|
51555
|
+
const leftWasher = washerAlongX(washerSize, leftWasherX, segments).color("#94a3b8");
|
|
51556
|
+
const rightWasher = washerAlongX(washerSize, rightWasherX, segments).color("#94a3b8");
|
|
51557
|
+
const leftKnob = makeKnob(leftKnobX);
|
|
51558
|
+
const rightKnob = makeKnob(rightKnobX);
|
|
51559
|
+
const shaftBore = cylinderAlongX(supportThickness + knobThickness + 2, boreDiameter / 2, 0, segments);
|
|
51560
|
+
const parts = [
|
|
51561
|
+
{ name: "left bored support cheek for retained shaft", shape: leftSupport },
|
|
51562
|
+
{ name: "right bored support cheek for retained shaft", shape: rightSupport },
|
|
51563
|
+
{ name: "retained through shaft with end heads", shape: shaft },
|
|
51564
|
+
{ name: `left ${washerSize} thrust washer on shaft`, shape: leftWasher },
|
|
51565
|
+
{ name: `right ${washerSize} thrust washer on shaft`, shape: rightWasher },
|
|
51566
|
+
{ name: "left retained hand knob with shaft bore", shape: leftKnob },
|
|
51567
|
+
{ name: "right retained hand knob with shaft bore", shape: rightKnob }
|
|
51568
|
+
];
|
|
51569
|
+
return {
|
|
51570
|
+
parts,
|
|
51571
|
+
supports: {
|
|
51572
|
+
left: leftSupport,
|
|
51573
|
+
right: rightSupport
|
|
51574
|
+
},
|
|
51575
|
+
shaft,
|
|
51576
|
+
washers: {
|
|
51577
|
+
left: leftWasher,
|
|
51578
|
+
right: rightWasher
|
|
51579
|
+
},
|
|
51580
|
+
knobs: {
|
|
51581
|
+
left: leftKnob,
|
|
51582
|
+
right: rightKnob
|
|
51583
|
+
},
|
|
51584
|
+
cutters: {
|
|
51585
|
+
shaftBore
|
|
51586
|
+
},
|
|
51587
|
+
dims: {
|
|
51588
|
+
supportSpacing,
|
|
51589
|
+
supportThickness,
|
|
51590
|
+
supportWidth,
|
|
51591
|
+
supportHeight,
|
|
51592
|
+
shaftDiameter,
|
|
51593
|
+
shaftLength,
|
|
51594
|
+
boreDiameter,
|
|
51595
|
+
washerSize,
|
|
51596
|
+
washerThickness: washerDims.t,
|
|
51597
|
+
knobDiameter,
|
|
51598
|
+
knobThickness,
|
|
51599
|
+
retainerThickness,
|
|
51600
|
+
runningClearance
|
|
51601
|
+
}
|
|
51602
|
+
};
|
|
51603
|
+
}
|
|
51604
|
+
function capturedLinearSlide(options) {
|
|
51605
|
+
const length4 = requirePositive$6(options.length, "length");
|
|
51606
|
+
const railWidth = requirePositive$6(options.railWidth ?? 38, "railWidth");
|
|
51607
|
+
const baseThickness = requirePositive$6(options.baseThickness ?? 2.4, "baseThickness");
|
|
51608
|
+
const wallThickness = requirePositive$6(options.wallThickness ?? 2, "wallThickness");
|
|
51609
|
+
const wallHeight = requirePositive$6(options.wallHeight ?? 9, "wallHeight");
|
|
51610
|
+
const lipWidth = requirePositive$6(options.lipWidth ?? 4, "lipWidth");
|
|
51611
|
+
const lipThickness = requirePositive$6(options.lipThickness ?? 1.8, "lipThickness");
|
|
51612
|
+
const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
|
|
51613
|
+
const endStopLength = requirePositive$6(options.endStopLength ?? 6, "endStopLength");
|
|
51614
|
+
const carriageLength = requirePositive$6(options.carriageLength ?? length4 * 0.32, "carriageLength");
|
|
51615
|
+
const innerWidth = railWidth - wallThickness * 2;
|
|
51616
|
+
const throatWidth = innerWidth - lipWidth * 2;
|
|
51617
|
+
if (innerWidth <= 0) throw new Error("capturedLinearSlide: wallThickness leaves no inner rail width");
|
|
51618
|
+
if (throatWidth <= 0) throw new Error("capturedLinearSlide: lipWidth closes the rail throat");
|
|
51619
|
+
const carriageWidth = requirePositive$6(options.carriageWidth ?? innerWidth - runningClearance * 2, "carriageWidth");
|
|
51620
|
+
const carriageThickness = requirePositive$6(options.carriageThickness ?? 4, "carriageThickness");
|
|
51621
|
+
if (carriageWidth >= innerWidth - runningClearance) {
|
|
51622
|
+
throw new Error("capturedLinearSlide: carriageWidth leaves too little side clearance inside the rail");
|
|
51623
|
+
}
|
|
51624
|
+
if (carriageWidth <= throatWidth + runningClearance) {
|
|
51625
|
+
throw new Error("capturedLinearSlide: carriageWidth must be wider than the lip throat so the rail actually captures it");
|
|
51626
|
+
}
|
|
51627
|
+
if (carriageThickness + runningClearance * 2 >= wallHeight) {
|
|
51628
|
+
throw new Error("capturedLinearSlide: carriage is too tall to clear the return lips");
|
|
51629
|
+
}
|
|
51630
|
+
const maxTravel = length4 - endStopLength * 2 - carriageLength;
|
|
51631
|
+
if (maxTravel <= 0) {
|
|
51632
|
+
throw new Error("capturedLinearSlide: rail length, end stops, and carriage length leave no travel");
|
|
51633
|
+
}
|
|
51634
|
+
const travel = options.travel ?? maxTravel / 2;
|
|
51635
|
+
if (!Number.isFinite(travel) || travel < 0 || travel > maxTravel) {
|
|
51636
|
+
throw new Error(`capturedLinearSlide: travel must be between 0 and ${maxTravel}`);
|
|
51637
|
+
}
|
|
51638
|
+
const carriageCenterX = -maxTravel / 2 + travel;
|
|
51639
|
+
const fuseOverlap = Math.min(0.04, runningClearance * 0.1);
|
|
51640
|
+
const sideY = railWidth / 2 - wallThickness / 2;
|
|
51641
|
+
const lipY = railWidth / 2 - wallThickness - lipWidth / 2 + fuseOverlap / 2;
|
|
51642
|
+
const stopZ = baseThickness - fuseOverlap;
|
|
51643
|
+
const rail2 = union(
|
|
51644
|
+
box(length4, railWidth, baseThickness),
|
|
51645
|
+
box(length4, wallThickness, wallHeight + fuseOverlap).translate(0, sideY, baseThickness - fuseOverlap),
|
|
51646
|
+
box(length4, wallThickness, wallHeight + fuseOverlap).translate(0, -sideY, baseThickness - fuseOverlap),
|
|
51647
|
+
box(length4, lipWidth, lipThickness + fuseOverlap).translate(0, lipY, baseThickness + wallHeight - fuseOverlap),
|
|
51648
|
+
box(length4, lipWidth, lipThickness + fuseOverlap).translate(0, -lipY, baseThickness + wallHeight - fuseOverlap),
|
|
51649
|
+
box(endStopLength, throatWidth, carriageThickness + fuseOverlap).translate(-length4 / 2 + endStopLength / 2, 0, stopZ),
|
|
51650
|
+
box(endStopLength, throatWidth, carriageThickness + fuseOverlap).translate(length4 / 2 - endStopLength / 2, 0, stopZ)
|
|
51651
|
+
).color("#475569");
|
|
51652
|
+
const carriage = union(
|
|
51653
|
+
box(carriageLength, carriageWidth, carriageThickness),
|
|
51654
|
+
box(carriageLength * 0.78, throatWidth - runningClearance * 2, Math.max(1, carriageThickness * 0.38)).translate(
|
|
51655
|
+
0,
|
|
51656
|
+
0,
|
|
51657
|
+
carriageThickness
|
|
51658
|
+
)
|
|
51659
|
+
).translate(carriageCenterX, 0, baseThickness + runningClearance).color("#111827");
|
|
51660
|
+
const parts = [
|
|
51661
|
+
{ name: "captured linear rail with return lips and end stops", shape: rail2 },
|
|
51662
|
+
{ name: "sliding carriage captured under rail lips", shape: carriage }
|
|
51663
|
+
];
|
|
51664
|
+
return {
|
|
51665
|
+
parts,
|
|
51666
|
+
rail: rail2,
|
|
51667
|
+
carriage,
|
|
51668
|
+
dims: {
|
|
51669
|
+
length: length4,
|
|
51670
|
+
railWidth,
|
|
51671
|
+
innerWidth,
|
|
51672
|
+
throatWidth,
|
|
51673
|
+
baseThickness,
|
|
51674
|
+
wallThickness,
|
|
51675
|
+
wallHeight,
|
|
51676
|
+
lipWidth,
|
|
51677
|
+
lipThickness,
|
|
51678
|
+
carriageLength,
|
|
51679
|
+
carriageWidth,
|
|
51680
|
+
carriageThickness,
|
|
51681
|
+
endStopLength,
|
|
51682
|
+
runningClearance,
|
|
51683
|
+
maxTravel,
|
|
51684
|
+
travel,
|
|
51685
|
+
carriageCenterX
|
|
51686
|
+
}
|
|
51687
|
+
};
|
|
51688
|
+
}
|
|
51689
|
+
function capturedCartridgeGuideAssembly(options) {
|
|
51690
|
+
const length4 = requirePositive$6(options.length, "length");
|
|
51691
|
+
const guideWidth = requirePositive$6(options.guideWidth ?? 42, "guideWidth");
|
|
51692
|
+
const baseThickness = requirePositive$6(options.baseThickness ?? 3, "baseThickness");
|
|
51693
|
+
const wallThickness = requirePositive$6(options.wallThickness ?? 2.5, "wallThickness");
|
|
51694
|
+
const wallHeight = requirePositive$6(options.wallHeight ?? 12, "wallHeight");
|
|
51695
|
+
const lipWidth = requirePositive$6(options.lipWidth ?? 4, "lipWidth");
|
|
51696
|
+
const lipThickness = requirePositive$6(options.lipThickness ?? 2, "lipThickness");
|
|
51697
|
+
const rearStopLength = requirePositive$6(options.rearStopLength ?? 7, "rearStopLength");
|
|
51698
|
+
const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
|
|
51699
|
+
const cartridgeLength = requirePositive$6(options.cartridgeLength ?? length4 * 0.58, "cartridgeLength");
|
|
51700
|
+
const cartridgeHeight = requirePositive$6(options.cartridgeHeight ?? 10, "cartridgeHeight");
|
|
51701
|
+
const flangeThickness = requirePositive$6(options.flangeThickness ?? 3, "flangeThickness");
|
|
51702
|
+
const pullTabLength = requirePositive$6(options.pullTabLength ?? 10, "pullTabLength");
|
|
51703
|
+
const innerWidth = guideWidth - wallThickness * 2;
|
|
51704
|
+
const throatWidth = innerWidth - lipWidth * 2;
|
|
51705
|
+
if (innerWidth <= 0) throw new Error("capturedCartridgeGuideAssembly: wallThickness leaves no inner guide width");
|
|
51706
|
+
if (throatWidth <= 0) throw new Error("capturedCartridgeGuideAssembly: lipWidth closes the guide throat");
|
|
51707
|
+
if (wallHeight <= lipThickness + flangeThickness + runningClearance * 2) {
|
|
51708
|
+
throw new Error("capturedCartridgeGuideAssembly: wallHeight leaves too little vertical capture clearance");
|
|
51709
|
+
}
|
|
51710
|
+
const cartridgeWidth = requirePositive$6(options.cartridgeWidth ?? innerWidth - runningClearance * 2, "cartridgeWidth");
|
|
51711
|
+
const cartridgeBodyWidth = throatWidth - runningClearance * 2;
|
|
51712
|
+
if (cartridgeBodyWidth <= 0) {
|
|
51713
|
+
throw new Error("capturedCartridgeGuideAssembly: throatWidth and runningClearance leave no cartridge body width");
|
|
51714
|
+
}
|
|
51715
|
+
if (cartridgeWidth >= innerWidth - runningClearance) {
|
|
51716
|
+
throw new Error("capturedCartridgeGuideAssembly: cartridgeWidth leaves too little side clearance inside the guide");
|
|
51717
|
+
}
|
|
51718
|
+
if (cartridgeWidth <= throatWidth + runningClearance) {
|
|
51719
|
+
throw new Error("capturedCartridgeGuideAssembly: cartridge flange must be wider than the guide throat so the cartridge is captured");
|
|
51720
|
+
}
|
|
51721
|
+
const maxInsertion = length4 - rearStopLength - cartridgeLength;
|
|
51722
|
+
if (maxInsertion <= 0) {
|
|
51723
|
+
throw new Error("capturedCartridgeGuideAssembly: length, rearStopLength, and cartridgeLength leave no insertion travel");
|
|
51724
|
+
}
|
|
51725
|
+
const insertion = options.insertion ?? maxInsertion * 0.4;
|
|
51726
|
+
if (!Number.isFinite(insertion) || insertion < 0 || insertion > maxInsertion) {
|
|
51727
|
+
throw new Error(`capturedCartridgeGuideAssembly: insertion must be between 0 and ${maxInsertion}`);
|
|
51728
|
+
}
|
|
51729
|
+
const cartridgeCenterX = -length4 / 2 + cartridgeLength / 2 + insertion;
|
|
51730
|
+
const fuseOverlap = Math.min(0.04, runningClearance * 0.1);
|
|
51731
|
+
const sideY = guideWidth / 2 - wallThickness / 2;
|
|
51732
|
+
const lipY = guideWidth / 2 - wallThickness - lipWidth / 2 + fuseOverlap / 2;
|
|
51733
|
+
const guide = union(
|
|
51734
|
+
box(length4, guideWidth, baseThickness),
|
|
51735
|
+
box(length4, wallThickness, wallHeight + fuseOverlap).translate(0, sideY, baseThickness - fuseOverlap),
|
|
51736
|
+
box(length4, wallThickness, wallHeight + fuseOverlap).translate(0, -sideY, baseThickness - fuseOverlap),
|
|
51737
|
+
box(length4, lipWidth, lipThickness + fuseOverlap).translate(0, lipY, baseThickness + wallHeight - fuseOverlap),
|
|
51738
|
+
box(length4, lipWidth, lipThickness + fuseOverlap).translate(0, -lipY, baseThickness + wallHeight - fuseOverlap),
|
|
51739
|
+
box(rearStopLength, throatWidth, Math.max(flangeThickness + runningClearance, 4)).translate(
|
|
51740
|
+
length4 / 2 - rearStopLength / 2,
|
|
51741
|
+
0,
|
|
51742
|
+
baseThickness - fuseOverlap
|
|
51743
|
+
)
|
|
51744
|
+
).color("#475569");
|
|
51745
|
+
const flangeZ = baseThickness + runningClearance;
|
|
51746
|
+
const bodyHeight = Math.max(1, cartridgeHeight - flangeThickness);
|
|
51747
|
+
const bodyZ = flangeZ + flangeThickness;
|
|
51748
|
+
const tabOverlap = Math.min(0.6, pullTabLength * 0.15);
|
|
51749
|
+
const pullTabX = cartridgeCenterX - cartridgeLength / 2 - pullTabLength / 2 + tabOverlap;
|
|
51750
|
+
const pullTabWidth = Math.max(cartridgeBodyWidth * 0.55, 12);
|
|
51751
|
+
const cartridge = union(
|
|
51752
|
+
box(cartridgeLength, cartridgeWidth, flangeThickness).translate(cartridgeCenterX, 0, flangeZ),
|
|
51753
|
+
box(cartridgeLength * 0.88, cartridgeBodyWidth, bodyHeight).translate(cartridgeCenterX, 0, bodyZ),
|
|
51754
|
+
box(pullTabLength, pullTabWidth, Math.max(flangeThickness, 3)).translate(pullTabX, 0, flangeZ)
|
|
51755
|
+
).color("#111827");
|
|
51756
|
+
const parts = [
|
|
51757
|
+
{ name: "captured cartridge guide with return lips and rear stop", shape: guide },
|
|
51758
|
+
{ name: "removable cartridge with captured flange and pull tab", shape: cartridge }
|
|
51759
|
+
];
|
|
51760
|
+
return {
|
|
51761
|
+
parts,
|
|
51762
|
+
guide,
|
|
51763
|
+
cartridge,
|
|
51764
|
+
dims: {
|
|
51765
|
+
length: length4,
|
|
51766
|
+
guideWidth,
|
|
51767
|
+
innerWidth,
|
|
51768
|
+
throatWidth,
|
|
51769
|
+
baseThickness,
|
|
51770
|
+
wallThickness,
|
|
51771
|
+
wallHeight,
|
|
51772
|
+
lipWidth,
|
|
51773
|
+
lipThickness,
|
|
51774
|
+
rearStopLength,
|
|
51775
|
+
cartridgeLength,
|
|
51776
|
+
cartridgeWidth,
|
|
51777
|
+
cartridgeBodyWidth,
|
|
51778
|
+
cartridgeHeight,
|
|
51779
|
+
flangeThickness,
|
|
51780
|
+
pullTabLength,
|
|
51781
|
+
runningClearance,
|
|
51782
|
+
maxInsertion,
|
|
51783
|
+
insertion,
|
|
51784
|
+
cartridgeCenterX
|
|
51785
|
+
}
|
|
51786
|
+
};
|
|
51787
|
+
}
|
|
51788
|
+
function livingHingeCoverAssembly(options) {
|
|
51789
|
+
const width = requirePositive$6(options.width, "width");
|
|
51790
|
+
const coverDepth = requirePositive$6(options.coverDepth ?? 42, "coverDepth");
|
|
51791
|
+
const fixedLeafDepth = requirePositive$6(options.fixedLeafDepth ?? 18, "fixedLeafDepth");
|
|
51792
|
+
const leafThickness = requirePositive$6(options.leafThickness ?? 2, "leafThickness");
|
|
51793
|
+
const hingeWebWidth = requirePositive$6(options.hingeWebWidth ?? 3.2, "hingeWebWidth");
|
|
51794
|
+
const hingeWebThickness = requirePositive$6(options.hingeWebThickness ?? 0.45, "hingeWebThickness");
|
|
51795
|
+
const pullLipDepth = requirePositive$6(options.pullLipDepth ?? 5, "pullLipDepth");
|
|
51796
|
+
const snapBarbWidth = requirePositive$6(options.snapBarbWidth ?? width * 0.35, "snapBarbWidth");
|
|
51797
|
+
const snapBarbDepth = requirePositive$6(options.snapBarbDepth ?? 2.4, "snapBarbDepth");
|
|
51798
|
+
const snapBarbHeight = requirePositive$6(options.snapBarbHeight ?? 1.4, "snapBarbHeight");
|
|
51799
|
+
const catchLandDepth = requirePositive$6(options.catchLandDepth ?? 2.4, "catchLandDepth");
|
|
51800
|
+
if (hingeWebThickness >= leafThickness * 0.55) {
|
|
51801
|
+
throw new Error("livingHingeCoverAssembly: hingeWebThickness must be much thinner than the rigid leaves");
|
|
51802
|
+
}
|
|
51803
|
+
if (hingeWebWidth >= Math.min(coverDepth, fixedLeafDepth) * 0.45) {
|
|
51804
|
+
throw new Error("livingHingeCoverAssembly: hingeWebWidth is too wide for the selected leaves");
|
|
51805
|
+
}
|
|
51806
|
+
if (snapBarbWidth >= width - 2) {
|
|
51807
|
+
throw new Error("livingHingeCoverAssembly: snapBarbWidth must leave side material on the cover leaf");
|
|
51808
|
+
}
|
|
51809
|
+
const fuseOverlap = Math.min(0.04, hingeWebWidth * 0.02);
|
|
51810
|
+
const fixedCenterY = -hingeWebWidth / 2 - fixedLeafDepth / 2 + fuseOverlap / 2;
|
|
51811
|
+
const coverCenterY = hingeWebWidth / 2 + coverDepth / 2 - fuseOverlap / 2;
|
|
51812
|
+
const fixedLeaf = box(width, fixedLeafDepth + fuseOverlap, leafThickness).translate(0, fixedCenterY, 0);
|
|
51813
|
+
const movingLeaf = box(width, coverDepth + fuseOverlap, leafThickness).translate(0, coverCenterY, 0);
|
|
51814
|
+
const hingeWeb = box(width, hingeWebWidth + fuseOverlap * 2, hingeWebThickness).translate(0, 0, 0);
|
|
51815
|
+
const pullLip = box(width * 0.92, pullLipDepth, leafThickness).translate(0, coverCenterY + coverDepth / 2 + pullLipDepth / 2 - fuseOverlap, 0);
|
|
51816
|
+
const snapBarb = box(snapBarbWidth, snapBarbDepth, snapBarbHeight).translate(
|
|
51817
|
+
0,
|
|
51818
|
+
coverCenterY + coverDepth / 2 - snapBarbDepth / 2,
|
|
51819
|
+
leafThickness
|
|
51820
|
+
);
|
|
51821
|
+
const catchLand = box(width * 0.55, catchLandDepth, Math.max(0.8, leafThickness * 0.45)).translate(
|
|
51822
|
+
0,
|
|
51823
|
+
fixedCenterY - fixedLeafDepth / 2 + catchLandDepth / 2,
|
|
51824
|
+
leafThickness
|
|
51825
|
+
);
|
|
51826
|
+
const cover2 = union(fixedLeaf, movingLeaf, hingeWeb, pullLip, snapBarb, catchLand).color("#0f766e");
|
|
51827
|
+
const overallDepth = fixedLeafDepth + hingeWebWidth + coverDepth + pullLipDepth;
|
|
51828
|
+
const flexRatio = leafThickness / hingeWebThickness;
|
|
51829
|
+
return {
|
|
51830
|
+
parts: [{ name: "one-piece molded living hinge cover with snap barb", shape: cover2 }],
|
|
51831
|
+
cover: cover2,
|
|
51832
|
+
fixedLeaf,
|
|
51833
|
+
movingLeaf,
|
|
51834
|
+
hingeWeb,
|
|
51835
|
+
snapBarb,
|
|
51836
|
+
catchLand,
|
|
51837
|
+
dims: {
|
|
51838
|
+
width,
|
|
51839
|
+
coverDepth,
|
|
51840
|
+
fixedLeafDepth,
|
|
51841
|
+
leafThickness,
|
|
51842
|
+
hingeWebWidth,
|
|
51843
|
+
hingeWebThickness,
|
|
51844
|
+
pullLipDepth,
|
|
51845
|
+
snapBarbWidth,
|
|
51846
|
+
snapBarbDepth,
|
|
51847
|
+
snapBarbHeight,
|
|
51848
|
+
catchLandDepth,
|
|
51849
|
+
flexRatio,
|
|
51850
|
+
overallDepth
|
|
51851
|
+
}
|
|
51852
|
+
};
|
|
51853
|
+
}
|
|
51854
|
+
function knuckledHingeAssembly(options) {
|
|
51855
|
+
const length4 = requirePositive$6(options.length, "length");
|
|
51856
|
+
const leafLength = requirePositive$6(options.leafLength ?? 36, "leafLength");
|
|
51857
|
+
const leafThickness = requirePositive$6(options.leafThickness ?? 1.6, "leafThickness");
|
|
51858
|
+
const barrelOuterRadius = requirePositive$6(options.barrelOuterRadius ?? 3, "barrelOuterRadius");
|
|
51859
|
+
const pinDiameter = requirePositive$6(options.pinDiameter ?? 2, "pinDiameter");
|
|
51860
|
+
const pinClearance = requireNonNegative(options.pinClearance ?? 0.25, "pinClearance");
|
|
51861
|
+
const boreDiameter = pinDiameter + pinClearance;
|
|
51862
|
+
const knuckleGap = requireNonNegative(options.knuckleGap ?? 0.45, "knuckleGap");
|
|
51863
|
+
const openAngleDeg = Number.isFinite(options.openAngleDeg ?? 35) ? options.openAngleDeg ?? 35 : 35;
|
|
51864
|
+
const retainerThickness = requirePositive$6(
|
|
51865
|
+
options.retainerThickness ?? Math.max(leafThickness, pinDiameter * 0.7),
|
|
51866
|
+
"retainerThickness"
|
|
51867
|
+
);
|
|
51868
|
+
const segments = options.segments ?? 36;
|
|
51869
|
+
const knuckleCount = options.knuckleCount ?? 5;
|
|
51870
|
+
if (!Number.isInteger(knuckleCount) || knuckleCount < 3 || knuckleCount % 2 === 0) {
|
|
51871
|
+
throw new Error("knuckledHingeAssembly: knuckleCount must be an odd integer >= 3");
|
|
51872
|
+
}
|
|
51873
|
+
if (barrelOuterRadius <= boreDiameter / 2 + Math.max(0.35, pinDiameter * 0.18)) {
|
|
51874
|
+
throw new Error("knuckledHingeAssembly: barrelOuterRadius leaves too little wall around the pin bore");
|
|
51875
|
+
}
|
|
51876
|
+
const knuckleLength = (length4 - knuckleGap * (knuckleCount - 1)) / knuckleCount;
|
|
51877
|
+
if (knuckleLength <= pinDiameter * 1.4) {
|
|
51878
|
+
throw new Error("knuckledHingeAssembly: length, knuckleCount, and knuckleGap make knuckles too short");
|
|
51879
|
+
}
|
|
51880
|
+
const leafRootClearance = Math.max(0.12, Math.min(knuckleGap * 0.35, 0.35));
|
|
51881
|
+
const barrelLeafOverlap = Math.min(barrelOuterRadius * 0.18, leafThickness * 0.35);
|
|
51882
|
+
const bridgeDepth = leafRootClearance + barrelLeafOverlap + 0.2;
|
|
51883
|
+
const fixedLeafPlate = box(length4, leafLength, leafThickness).translate(
|
|
51884
|
+
0,
|
|
51885
|
+
barrelOuterRadius + leafRootClearance + leafLength / 2,
|
|
51886
|
+
-leafThickness / 2
|
|
51887
|
+
);
|
|
51888
|
+
const movingLeafPlate = box(length4, leafLength, leafThickness).translate(
|
|
51889
|
+
0,
|
|
51890
|
+
-barrelOuterRadius - leafRootClearance - leafLength / 2,
|
|
51891
|
+
-leafThickness / 2
|
|
51892
|
+
);
|
|
51893
|
+
const fixedKnuckles = [];
|
|
51894
|
+
const movingKnuckles = [];
|
|
51895
|
+
const fixedBridges = [];
|
|
51896
|
+
const movingBridges = [];
|
|
51897
|
+
for (let index2 = 0; index2 < knuckleCount; index2 += 1) {
|
|
51898
|
+
const xStart = -length4 / 2 + index2 * (knuckleLength + knuckleGap);
|
|
51899
|
+
const xCenter = xStart + knuckleLength / 2;
|
|
51900
|
+
const knuckle = tubeAlongX(knuckleLength, barrelOuterRadius, boreDiameter / 2, xCenter, segments);
|
|
51901
|
+
if (index2 % 2 === 0) {
|
|
51902
|
+
fixedKnuckles.push(knuckle);
|
|
51903
|
+
fixedBridges.push(
|
|
51904
|
+
box(knuckleLength, bridgeDepth, leafThickness).translate(
|
|
51905
|
+
xCenter,
|
|
51906
|
+
barrelOuterRadius - barrelLeafOverlap + bridgeDepth / 2,
|
|
51907
|
+
-leafThickness / 2
|
|
51908
|
+
)
|
|
51909
|
+
);
|
|
51910
|
+
} else {
|
|
51911
|
+
movingKnuckles.push(knuckle);
|
|
51912
|
+
movingBridges.push(
|
|
51913
|
+
box(knuckleLength, bridgeDepth, leafThickness).translate(
|
|
51914
|
+
xCenter,
|
|
51915
|
+
-barrelOuterRadius + barrelLeafOverlap - bridgeDepth / 2,
|
|
51916
|
+
-leafThickness / 2
|
|
51917
|
+
)
|
|
51918
|
+
);
|
|
51919
|
+
}
|
|
51920
|
+
}
|
|
51921
|
+
const fixedLeaf = union(fixedLeafPlate, ...fixedKnuckles, ...fixedBridges).color("#475569");
|
|
51922
|
+
const movingLeaf = union(movingLeafPlate, ...movingKnuckles, ...movingBridges).rotateX(openAngleDeg).color("#111827");
|
|
51923
|
+
const pinCore = cylinderAlongX(length4 + retainerThickness * 2, pinDiameter / 2, 0, segments);
|
|
51924
|
+
const retainerRadius = Math.max(barrelOuterRadius * 0.85, pinDiameter);
|
|
51925
|
+
const leftHead = cylinderAlongX(retainerThickness, retainerRadius, -length4 / 2 - retainerThickness / 2, segments);
|
|
51926
|
+
const rightHead = cylinderAlongX(retainerThickness, retainerRadius, length4 / 2 + retainerThickness / 2, segments);
|
|
51927
|
+
const pin = union(pinCore, leftHead, rightHead).color("#cbd5e1");
|
|
51928
|
+
const pinBore = cylinderAlongX(length4 + retainerThickness * 2, boreDiameter / 2, 0, segments);
|
|
51929
|
+
const parts = [
|
|
51930
|
+
{ name: "fixed hinge leaf with alternating knuckles", shape: fixedLeaf },
|
|
51931
|
+
{ name: "moving hinge leaf with alternating knuckles", shape: movingLeaf },
|
|
51932
|
+
{ name: "retained hinge pin through knuckle stack", shape: pin }
|
|
51933
|
+
];
|
|
51934
|
+
return {
|
|
51935
|
+
parts,
|
|
51936
|
+
fixedLeaf,
|
|
51937
|
+
movingLeaf,
|
|
51938
|
+
pin,
|
|
51939
|
+
cutters: {
|
|
51940
|
+
pinBore
|
|
51941
|
+
},
|
|
51942
|
+
dims: {
|
|
51943
|
+
length: length4,
|
|
51944
|
+
leafLength,
|
|
51945
|
+
leafThickness,
|
|
51946
|
+
barrelOuterRadius,
|
|
51947
|
+
pinDiameter,
|
|
51948
|
+
boreDiameter,
|
|
51949
|
+
knuckleGap,
|
|
51950
|
+
knuckleCount,
|
|
51951
|
+
knuckleLength,
|
|
51952
|
+
openAngleDeg,
|
|
51953
|
+
retainerThickness
|
|
51954
|
+
}
|
|
51955
|
+
};
|
|
51956
|
+
}
|
|
51957
|
+
function clevisPinJointAssembly(options = {}) {
|
|
51958
|
+
const pinDiameter = requirePositive$6(options.pinDiameter ?? 4, "pinDiameter");
|
|
51959
|
+
const pinClearance = requireNonNegative(options.pinClearance ?? 0.3, "pinClearance");
|
|
51960
|
+
const boreDiameter = pinDiameter + pinClearance;
|
|
51961
|
+
const linkThickness = requirePositive$6(options.linkThickness ?? Math.max(5, pinDiameter * 1.5), "linkThickness");
|
|
51962
|
+
const earThickness = requirePositive$6(options.earThickness ?? Math.max(3.5, pinDiameter), "earThickness");
|
|
51963
|
+
const runningClearance = requireNonNegative(options.runningClearance ?? 0.25, "runningClearance");
|
|
51964
|
+
const linkArmWidth = requirePositive$6(options.linkArmWidth ?? pinDiameter * 2.4, "linkArmWidth");
|
|
51965
|
+
const eyeOuterRadius = requirePositive$6(
|
|
51966
|
+
options.eyeOuterRadius ?? Math.max(pinDiameter * 1.8, linkArmWidth / 2 + 1.4),
|
|
51967
|
+
"eyeOuterRadius"
|
|
51968
|
+
);
|
|
51969
|
+
const earLength = requirePositive$6(options.earLength ?? Math.max(eyeOuterRadius * 2.55, pinDiameter * 4.2), "earLength");
|
|
51970
|
+
const earHeight = requirePositive$6(options.earHeight ?? Math.max(eyeOuterRadius * 2.25, pinDiameter * 4.4), "earHeight");
|
|
51971
|
+
const linkArmLength = requirePositive$6(options.linkArmLength ?? 34, "linkArmLength");
|
|
51972
|
+
const retainerThickness = requirePositive$6(
|
|
51973
|
+
options.retainerThickness ?? Math.max(1.2, pinDiameter * 0.35),
|
|
51974
|
+
"retainerThickness"
|
|
51975
|
+
);
|
|
51976
|
+
const segments = options.segments ?? 40;
|
|
51977
|
+
if (eyeOuterRadius <= boreDiameter / 2 + Math.max(0.8, pinDiameter * 0.25)) {
|
|
51978
|
+
throw new Error("clevisPinJointAssembly: eyeOuterRadius leaves too little material around the pin bore");
|
|
51979
|
+
}
|
|
51980
|
+
if (earHeight <= boreDiameter + Math.max(3, pinDiameter)) {
|
|
51981
|
+
throw new Error("clevisPinJointAssembly: earHeight leaves too little material around the pin bore");
|
|
51982
|
+
}
|
|
51983
|
+
if (earLength / 2 <= eyeOuterRadius + runningClearance) {
|
|
51984
|
+
throw new Error("clevisPinJointAssembly: earLength must extend behind the link eye for a rear clevis bridge");
|
|
51985
|
+
}
|
|
51986
|
+
const clevisGap = linkThickness + runningClearance * 2;
|
|
51987
|
+
const earCenterY = clevisGap / 2 + earThickness / 2;
|
|
51988
|
+
const totalStackY = clevisGap + earThickness * 2;
|
|
51989
|
+
const pinLength = totalStackY + retainerThickness * 2 + runningClearance * 2;
|
|
51990
|
+
const bridgeClearX = -eyeOuterRadius - runningClearance;
|
|
51991
|
+
const bridgeLength = Math.max(pinDiameter * 2.2, 4);
|
|
51992
|
+
const bridgeHeight = Math.min(earHeight * 0.48, Math.max(pinDiameter * 1.4, eyeOuterRadius * 0.75));
|
|
51993
|
+
const bridgeCenterX = bridgeClearX - bridgeLength / 2;
|
|
51994
|
+
const bridgeCenterZ = -earHeight / 2 + bridgeHeight / 2;
|
|
51995
|
+
const pinBore = cylinderAlongY(totalStackY + 0.8, boreDiameter / 2, 0, segments);
|
|
51996
|
+
const clevisBlank = union(
|
|
51997
|
+
box(earLength, earThickness, earHeight).translate(0, earCenterY, -earHeight / 2),
|
|
51998
|
+
box(earLength, earThickness, earHeight).translate(0, -earCenterY, -earHeight / 2),
|
|
51999
|
+
box(bridgeLength, totalStackY, bridgeHeight).translate(bridgeCenterX, 0, bridgeCenterZ)
|
|
52000
|
+
);
|
|
52001
|
+
const clevis = clevisBlank.subtract(pinBore).color("#475569");
|
|
52002
|
+
const eye = tubeAlongY(linkThickness, eyeOuterRadius, boreDiameter / 2, 0, segments);
|
|
52003
|
+
const armOverlap = Math.min(eyeOuterRadius * 0.65, linkArmLength * 0.25);
|
|
52004
|
+
const armCenterX = eyeOuterRadius - armOverlap + linkArmLength / 2;
|
|
52005
|
+
const linkArm = box(linkArmLength, linkThickness, linkArmWidth).translate(armCenterX, 0, -linkArmWidth / 2);
|
|
52006
|
+
const link = union(eye, linkArm).color("#111827");
|
|
52007
|
+
const pinCore = cylinderAlongY(pinLength, pinDiameter / 2, 0, segments);
|
|
52008
|
+
const headRadius = Math.max(pinDiameter * 0.9, boreDiameter / 2 + 0.8);
|
|
52009
|
+
const headY = totalStackY / 2 + runningClearance + retainerThickness / 2;
|
|
52010
|
+
const headA = cylinderAlongY(retainerThickness, headRadius, headY, segments);
|
|
52011
|
+
const headB = cylinderAlongY(retainerThickness, headRadius, -headY, segments);
|
|
52012
|
+
const pin = union(pinCore, headA, headB).color("#cbd5e1");
|
|
52013
|
+
const cutter = cylinderAlongY(pinLength + 1, boreDiameter / 2, 0, segments);
|
|
52014
|
+
const parts = [
|
|
52015
|
+
{ name: "bored clevis yoke with rear bridge", shape: clevis },
|
|
52016
|
+
{ name: "center link eye captured in clevis", shape: link },
|
|
52017
|
+
{ name: "retained clevis pin through link eye", shape: pin }
|
|
52018
|
+
];
|
|
52019
|
+
return {
|
|
52020
|
+
parts,
|
|
52021
|
+
clevis,
|
|
52022
|
+
link,
|
|
52023
|
+
pin,
|
|
52024
|
+
cutters: {
|
|
52025
|
+
pinBore: cutter
|
|
52026
|
+
},
|
|
52027
|
+
dims: {
|
|
52028
|
+
pinDiameter,
|
|
52029
|
+
boreDiameter,
|
|
52030
|
+
linkThickness,
|
|
52031
|
+
earThickness,
|
|
52032
|
+
runningClearance,
|
|
52033
|
+
earLength,
|
|
52034
|
+
earHeight,
|
|
52035
|
+
linkArmLength,
|
|
52036
|
+
linkArmWidth,
|
|
52037
|
+
eyeOuterRadius,
|
|
52038
|
+
retainerThickness,
|
|
52039
|
+
pinLength,
|
|
52040
|
+
clevisGap
|
|
52041
|
+
}
|
|
52042
|
+
};
|
|
52043
|
+
}
|
|
52044
|
+
function seatedBearingAssembly(options) {
|
|
52045
|
+
const bearingOuterDiameter = requirePositive$6(options.bearingOuterDiameter, "bearingOuterDiameter");
|
|
52046
|
+
const bearingInnerDiameter = requirePositive$6(options.bearingInnerDiameter, "bearingInnerDiameter");
|
|
52047
|
+
const bearingWidth = requirePositive$6(options.bearingWidth, "bearingWidth");
|
|
52048
|
+
const shaftDiameter = requirePositive$6(options.shaftDiameter ?? Math.max(1, bearingInnerDiameter - 0.4), "shaftDiameter");
|
|
52049
|
+
const pocketClearance = requireNonNegative(options.pocketClearance ?? 0.2, "pocketClearance");
|
|
52050
|
+
const shaftClearance = requireNonNegative(options.shaftClearance ?? 0.35, "shaftClearance");
|
|
52051
|
+
const runningClearance = requireNonNegative(options.runningClearance ?? 0.05, "runningClearance");
|
|
52052
|
+
const housingThickness = requirePositive$6(options.housingThickness ?? bearingWidth + 5, "housingThickness");
|
|
52053
|
+
const bossHeight = requirePositive$6(options.bossHeight ?? Math.max(2, bearingWidth * 0.45), "bossHeight");
|
|
52054
|
+
const bossOuterDiameter = requirePositive$6(
|
|
52055
|
+
options.bossOuterDiameter ?? bearingOuterDiameter + Math.max(8, bearingOuterDiameter * 0.36),
|
|
52056
|
+
"bossOuterDiameter"
|
|
52057
|
+
);
|
|
52058
|
+
const housingWidth = requirePositive$6(options.housingWidth ?? Math.max(bossOuterDiameter + 12, bearingOuterDiameter * 2.1), "housingWidth");
|
|
52059
|
+
const housingDepth = requirePositive$6(options.housingDepth ?? Math.max(bossOuterDiameter + 12, bearingOuterDiameter * 1.8), "housingDepth");
|
|
52060
|
+
const shaftOverhang = requirePositive$6(options.shaftOverhang ?? Math.max(8, bearingOuterDiameter * 0.45), "shaftOverhang");
|
|
52061
|
+
const shoulderDiameter = requirePositive$6(options.shoulderDiameter ?? Math.max(shaftDiameter * 1.65, bearingInnerDiameter + 2), "shoulderDiameter");
|
|
52062
|
+
const shoulderThickness = requirePositive$6(options.shoulderThickness ?? Math.max(1.5, shaftDiameter * 0.32), "shoulderThickness");
|
|
52063
|
+
const segments = options.segments ?? 48;
|
|
52064
|
+
if (bearingOuterDiameter <= bearingInnerDiameter + Math.max(1, bearingOuterDiameter * 0.08)) {
|
|
52065
|
+
throw new Error("seatedBearingAssembly: bearingOuterDiameter leaves too little bearing wall around the bore");
|
|
52066
|
+
}
|
|
52067
|
+
if (shaftDiameter + shaftClearance >= bearingInnerDiameter) {
|
|
52068
|
+
throw new Error("seatedBearingAssembly: shaftDiameter plus shaftClearance must fit inside the bearing bore");
|
|
52069
|
+
}
|
|
52070
|
+
if (shoulderDiameter >= bearingOuterDiameter - runningClearance * 2) {
|
|
52071
|
+
throw new Error("seatedBearingAssembly: shoulderDiameter must stay smaller than the bearing outer race");
|
|
52072
|
+
}
|
|
52073
|
+
const pocketDiameter = bearingOuterDiameter + pocketClearance;
|
|
52074
|
+
const shaftBoreDiameter = shaftDiameter + shaftClearance;
|
|
52075
|
+
const totalHousingHeight = housingThickness + bossHeight;
|
|
52076
|
+
const pocketDepth = bearingWidth + runningClearance * 2;
|
|
52077
|
+
if (pocketDepth >= totalHousingHeight - runningClearance) {
|
|
52078
|
+
throw new Error("seatedBearingAssembly: housingThickness and bossHeight must leave a shoulder below the bearing pocket");
|
|
52079
|
+
}
|
|
52080
|
+
if (bossOuterDiameter <= pocketDiameter + Math.max(2, bearingOuterDiameter * 0.12)) {
|
|
52081
|
+
throw new Error("seatedBearingAssembly: bossOuterDiameter leaves too little wall around the bearing pocket");
|
|
52082
|
+
}
|
|
52083
|
+
if (housingWidth <= pocketDiameter + 6 || housingDepth <= pocketDiameter + 6) {
|
|
52084
|
+
throw new Error("seatedBearingAssembly: housing dimensions leave too little material around the bearing pocket");
|
|
52085
|
+
}
|
|
52086
|
+
if (shoulderThickness * 2 + runningClearance * 2 >= shaftOverhang) {
|
|
52087
|
+
throw new Error("seatedBearingAssembly: shaftOverhang must leave room for retaining collars outside the housing");
|
|
52088
|
+
}
|
|
52089
|
+
const pocketBottomZ = totalHousingHeight - pocketDepth;
|
|
52090
|
+
const bearingZ = pocketBottomZ + runningClearance;
|
|
52091
|
+
const lowerShoulderZ = -runningClearance - shoulderThickness;
|
|
52092
|
+
const upperShoulderZ = totalHousingHeight + runningClearance;
|
|
52093
|
+
const shaftLength = totalHousingHeight + shaftOverhang * 2;
|
|
52094
|
+
const bossFuseOverlap = Math.min(0.08, Math.max(0.02, bossHeight * 0.03));
|
|
52095
|
+
const bearingPocket = cylinder(pocketDepth + 0.4, pocketDiameter / 2, void 0, segments).translate(0, 0, pocketBottomZ - 0.2);
|
|
52096
|
+
const shaftBore = cylinder(totalHousingHeight + 1, shaftBoreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
|
|
52097
|
+
const housingBase = box(housingWidth, housingDepth, housingThickness).subtract(bearingPocket).subtract(shaftBore);
|
|
52098
|
+
const housingBoss = cylinder(bossHeight + bossFuseOverlap, bossOuterDiameter / 2, void 0, segments).translate(
|
|
52099
|
+
0,
|
|
52100
|
+
0,
|
|
52101
|
+
housingThickness - bossFuseOverlap
|
|
52102
|
+
).subtract(bearingPocket);
|
|
52103
|
+
const housing = union(housingBase, housingBoss).color("#475569");
|
|
52104
|
+
const bearingRing = tubeAlongZ(bearingWidth, bearingOuterDiameter / 2, bearingInnerDiameter / 2, segments);
|
|
52105
|
+
const shieldInset = Math.min(bearingWidth * 0.18, 0.7);
|
|
52106
|
+
const shieldOuterRadius = bearingOuterDiameter / 2 - Math.max(0.45, (bearingOuterDiameter - bearingInnerDiameter) * 0.08);
|
|
52107
|
+
const shieldInnerRadius = bearingInnerDiameter / 2 + Math.max(0.2, (bearingOuterDiameter - bearingInnerDiameter) * 0.035);
|
|
52108
|
+
const bearingShield = shieldOuterRadius > shieldInnerRadius + 0.2 ? union(
|
|
52109
|
+
tubeAlongZ(Math.min(0.35, bearingWidth * 0.08), shieldOuterRadius, shieldInnerRadius, segments).translate(0, 0, shieldInset),
|
|
52110
|
+
tubeAlongZ(Math.min(0.35, bearingWidth * 0.08), shieldOuterRadius, shieldInnerRadius, segments).translate(
|
|
52111
|
+
0,
|
|
52112
|
+
0,
|
|
52113
|
+
bearingWidth - shieldInset - Math.min(0.35, bearingWidth * 0.08)
|
|
52114
|
+
)
|
|
52115
|
+
) : null;
|
|
52116
|
+
const bearing = (bearingShield ? union(bearingRing, bearingShield) : bearingRing).translate(0, 0, bearingZ).color("#111827");
|
|
52117
|
+
const shaftCore = cylinder(shaftLength, shaftDiameter / 2, void 0, segments).translate(0, 0, -shaftOverhang);
|
|
52118
|
+
const lowerShoulder = cylinder(shoulderThickness, shoulderDiameter / 2, void 0, segments).translate(0, 0, lowerShoulderZ);
|
|
52119
|
+
const upperShoulder = cylinder(shoulderThickness, shoulderDiameter / 2, void 0, segments).translate(0, 0, upperShoulderZ);
|
|
52120
|
+
const shaft = union(shaftCore, lowerShoulder, upperShoulder).color("#cbd5e1");
|
|
52121
|
+
const parts = [
|
|
52122
|
+
{ name: "bearing housing with counterbore pocket and shoulder", shape: housing },
|
|
52123
|
+
{ name: "purchased radial bearing seated in counterbore", shape: bearing },
|
|
52124
|
+
{ name: "shaft through bearing bore with retaining collars", shape: shaft }
|
|
52125
|
+
];
|
|
52126
|
+
return {
|
|
52127
|
+
parts,
|
|
52128
|
+
housing,
|
|
52129
|
+
bearing,
|
|
52130
|
+
shaft,
|
|
52131
|
+
cutters: {
|
|
52132
|
+
bearingPocket,
|
|
52133
|
+
shaftBore
|
|
52134
|
+
},
|
|
52135
|
+
dims: {
|
|
52136
|
+
bearingOuterDiameter,
|
|
52137
|
+
bearingInnerDiameter,
|
|
52138
|
+
bearingWidth,
|
|
52139
|
+
shaftDiameter,
|
|
52140
|
+
housingWidth,
|
|
52141
|
+
housingDepth,
|
|
52142
|
+
housingThickness,
|
|
52143
|
+
bossOuterDiameter,
|
|
52144
|
+
bossHeight,
|
|
52145
|
+
totalHousingHeight,
|
|
52146
|
+
pocketDiameter,
|
|
52147
|
+
pocketDepth,
|
|
52148
|
+
shaftBoreDiameter,
|
|
52149
|
+
runningClearance,
|
|
52150
|
+
shaftLength,
|
|
52151
|
+
shoulderDiameter,
|
|
52152
|
+
shoulderThickness
|
|
52153
|
+
}
|
|
52154
|
+
};
|
|
52155
|
+
}
|
|
52156
|
+
function cableGlandAnchorAssembly(options) {
|
|
52157
|
+
const cableDiameter = requirePositive$6(options.cableDiameter, "cableDiameter");
|
|
52158
|
+
const panelThickness = requirePositive$6(options.panelThickness ?? 3, "panelThickness");
|
|
52159
|
+
const panelWidth = requirePositive$6(options.panelWidth ?? Math.max(54, cableDiameter * 7), "panelWidth");
|
|
52160
|
+
const panelHeight = requirePositive$6(options.panelHeight ?? Math.max(38, cableDiameter * 5), "panelHeight");
|
|
52161
|
+
const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
|
|
52162
|
+
const panelHoleClearance = requirePositive$6(options.panelHoleClearance ?? 0.25, "panelHoleClearance");
|
|
52163
|
+
const cableBoreDiameter = cableDiameter + runningClearance * 2;
|
|
52164
|
+
const glandOuterDiameter = requirePositive$6(options.glandOuterDiameter ?? cableDiameter + Math.max(6, cableDiameter * 0.9), "glandOuterDiameter");
|
|
52165
|
+
const nutOuterDiameter = requirePositive$6(options.nutOuterDiameter ?? glandOuterDiameter + Math.max(6, cableDiameter * 0.8), "nutOuterDiameter");
|
|
52166
|
+
const nutThickness = requirePositive$6(options.nutThickness ?? Math.max(4, cableDiameter * 0.8), "nutThickness");
|
|
52167
|
+
const flangeDiameter = requirePositive$6(options.flangeDiameter ?? glandOuterDiameter + Math.max(5, cableDiameter * 0.7), "flangeDiameter");
|
|
52168
|
+
const flangeThickness = requirePositive$6(options.flangeThickness ?? Math.max(2, panelThickness * 0.45), "flangeThickness");
|
|
52169
|
+
const minGlandLength = panelThickness + nutThickness + flangeThickness + runningClearance * 4;
|
|
52170
|
+
const glandLength = requirePositive$6(options.glandLength ?? minGlandLength + Math.max(8, cableDiameter), "glandLength");
|
|
52171
|
+
const cableLength = requirePositive$6(options.cableLength ?? glandLength + Math.max(36, cableDiameter * 5), "cableLength");
|
|
52172
|
+
const segments = options.segments ?? 40;
|
|
52173
|
+
if (glandOuterDiameter <= cableBoreDiameter + Math.max(1.2, cableDiameter * 0.18)) {
|
|
52174
|
+
throw new Error("cableGlandAnchorAssembly: glandOuterDiameter leaves too little wall around the cable bore");
|
|
52175
|
+
}
|
|
52176
|
+
if (nutOuterDiameter <= glandOuterDiameter + Math.max(1.5, cableDiameter * 0.2)) {
|
|
52177
|
+
throw new Error("cableGlandAnchorAssembly: nutOuterDiameter must leave material around the gland body");
|
|
52178
|
+
}
|
|
52179
|
+
if (flangeDiameter <= glandOuterDiameter + Math.max(1.2, cableDiameter * 0.16)) {
|
|
52180
|
+
throw new Error("cableGlandAnchorAssembly: flangeDiameter must be larger than the gland body");
|
|
52181
|
+
}
|
|
52182
|
+
if (panelWidth <= flangeDiameter + 8 || panelHeight <= flangeDiameter + 8) {
|
|
52183
|
+
throw new Error("cableGlandAnchorAssembly: panel dimensions leave too little material around the gland hole");
|
|
52184
|
+
}
|
|
52185
|
+
if (glandLength <= minGlandLength) {
|
|
52186
|
+
throw new Error("cableGlandAnchorAssembly: glandLength must span the panel, flange, compression nut, and clearances");
|
|
52187
|
+
}
|
|
52188
|
+
if (cableLength <= glandLength + runningClearance * 2) {
|
|
52189
|
+
throw new Error("cableGlandAnchorAssembly: cableLength must extend beyond the gland body");
|
|
52190
|
+
}
|
|
52191
|
+
const panelHoleDiameter = glandOuterDiameter + panelHoleClearance * 2;
|
|
52192
|
+
const glandOuterRadius = glandOuterDiameter / 2;
|
|
52193
|
+
const cableBoreRadius = cableBoreDiameter / 2;
|
|
52194
|
+
const faceClearance = Math.min(0.05, runningClearance * 0.15);
|
|
52195
|
+
const flangePocketDepth = Math.min(Math.max(0.35, panelThickness * 0.18), panelThickness * 0.4, flangeThickness * 0.55);
|
|
52196
|
+
const panelHole = cylinderAlongX(panelThickness + 0.8, panelHoleDiameter / 2, 0, segments);
|
|
52197
|
+
const flangeSeatPocket = cylinderAlongX(
|
|
52198
|
+
flangePocketDepth + 0.2,
|
|
52199
|
+
flangeDiameter / 2 + panelHoleClearance,
|
|
52200
|
+
panelThickness / 2 - flangePocketDepth / 2,
|
|
52201
|
+
segments
|
|
52202
|
+
);
|
|
52203
|
+
const cableBore = cylinderAlongX(glandLength + 0.8, cableBoreRadius, 0, segments);
|
|
52204
|
+
const panel = box(panelThickness, panelWidth, panelHeight).translate(0, 0, -panelHeight / 2).subtract(panelHole).subtract(flangeSeatPocket).color("#475569");
|
|
52205
|
+
const glandBody = tubeAlongX(glandLength, glandOuterRadius, cableBoreRadius, 0, segments);
|
|
52206
|
+
const flangeCenterX = panelThickness / 2 - flangePocketDepth + faceClearance + flangeThickness / 2;
|
|
52207
|
+
const flange = tubeAlongX(flangeThickness, flangeDiameter / 2, cableBoreRadius, flangeCenterX, segments);
|
|
52208
|
+
const gland = union(glandBody, flange).color("#94a3b8");
|
|
52209
|
+
const nutInnerRadius = glandOuterRadius + Math.min(0.12, runningClearance * 0.4);
|
|
52210
|
+
const nutCenterX = -panelThickness / 2 - faceClearance - nutThickness / 2;
|
|
52211
|
+
const compressionNut = tubeAlongX(nutThickness, nutOuterDiameter / 2, nutInnerRadius, nutCenterX, segments).color("#cbd5e1");
|
|
52212
|
+
const cable = cylinderAlongX(cableLength, cableDiameter / 2, 0, segments).color("#111827");
|
|
52213
|
+
const parts = [
|
|
52214
|
+
{ name: "panel with gland clearance hole", shape: panel },
|
|
52215
|
+
{ name: "hollow cable gland body with panel flange", shape: gland },
|
|
52216
|
+
{ name: "compression nut around gland body", shape: compressionNut },
|
|
52217
|
+
{ name: "routed cable through gland bore", shape: cable }
|
|
52218
|
+
];
|
|
52219
|
+
return {
|
|
52220
|
+
parts,
|
|
52221
|
+
panel,
|
|
52222
|
+
gland,
|
|
52223
|
+
compressionNut,
|
|
52224
|
+
cable,
|
|
52225
|
+
cutters: {
|
|
52226
|
+
panelHole,
|
|
52227
|
+
flangeSeatPocket,
|
|
52228
|
+
cableBore
|
|
52229
|
+
},
|
|
52230
|
+
dims: {
|
|
52231
|
+
cableDiameter,
|
|
52232
|
+
cableBoreDiameter,
|
|
52233
|
+
panelThickness,
|
|
52234
|
+
panelWidth,
|
|
52235
|
+
panelHeight,
|
|
52236
|
+
glandOuterDiameter,
|
|
52237
|
+
glandLength,
|
|
52238
|
+
nutOuterDiameter,
|
|
52239
|
+
nutThickness,
|
|
52240
|
+
flangeDiameter,
|
|
52241
|
+
flangeThickness,
|
|
52242
|
+
runningClearance,
|
|
52243
|
+
faceClearance,
|
|
52244
|
+
flangePocketDepth,
|
|
52245
|
+
panelHoleDiameter,
|
|
52246
|
+
cableLength
|
|
52247
|
+
}
|
|
52248
|
+
};
|
|
52249
|
+
}
|
|
52250
|
+
function hoseBarbPortAssembly(options) {
|
|
52251
|
+
const hoseInnerDiameter = requirePositive$6(options.hoseInnerDiameter, "hoseInnerDiameter");
|
|
52252
|
+
const runningClearance = requirePositive$6(options.runningClearance ?? 0.18, "runningClearance");
|
|
52253
|
+
const faceClearance = requirePositive$6(options.faceClearance ?? 0.04, "faceClearance");
|
|
52254
|
+
const barbRootDiameter = requirePositive$6(
|
|
52255
|
+
options.barbRootDiameter ?? Math.max(1, hoseInnerDiameter - Math.max(0.25, hoseInnerDiameter * 0.06)),
|
|
52256
|
+
"barbRootDiameter"
|
|
52257
|
+
);
|
|
52258
|
+
const barbPeakDiameter = requirePositive$6(
|
|
52259
|
+
options.barbPeakDiameter ?? hoseInnerDiameter + Math.max(0.65, hoseInnerDiameter * 0.12),
|
|
52260
|
+
"barbPeakDiameter"
|
|
52261
|
+
);
|
|
52262
|
+
const installedHoseBoreDiameter = barbPeakDiameter + runningClearance * 2;
|
|
52263
|
+
const hoseOuterDiameter = requirePositive$6(
|
|
52264
|
+
options.hoseOuterDiameter ?? Math.max(installedHoseBoreDiameter + 2.4, hoseInnerDiameter + Math.max(3, hoseInnerDiameter * 0.55)),
|
|
52265
|
+
"hoseOuterDiameter"
|
|
52266
|
+
);
|
|
52267
|
+
const fluidBoreDiameter = requirePositive$6(options.fluidBoreDiameter ?? hoseInnerDiameter * 0.65, "fluidBoreDiameter");
|
|
52268
|
+
const blockThickness = requirePositive$6(options.blockThickness ?? Math.max(7, hoseInnerDiameter * 1.2), "blockThickness");
|
|
52269
|
+
const barbCount = options.barbCount ?? 3;
|
|
52270
|
+
const barbLength = requirePositive$6(options.barbLength ?? Math.max(2.6, hoseInnerDiameter * 0.55), "barbLength");
|
|
52271
|
+
const barbStackLength = barbCount * barbLength;
|
|
52272
|
+
const shoulderDiameter = requirePositive$6(
|
|
52273
|
+
options.shoulderDiameter ?? barbPeakDiameter + Math.max(4, hoseInnerDiameter * 0.65),
|
|
52274
|
+
"shoulderDiameter"
|
|
52275
|
+
);
|
|
52276
|
+
const shoulderThickness = requirePositive$6(options.shoulderThickness ?? Math.max(2, hoseInnerDiameter * 0.35), "shoulderThickness");
|
|
52277
|
+
const bossDiameter = requirePositive$6(options.bossDiameter ?? shoulderDiameter + Math.max(4, hoseInnerDiameter * 0.6), "bossDiameter");
|
|
52278
|
+
const bossHeight = requirePositive$6(options.bossHeight ?? Math.max(2.4, hoseInnerDiameter * 0.45), "bossHeight");
|
|
52279
|
+
const blockWidth = requirePositive$6(options.blockWidth ?? bossDiameter + Math.max(14, hoseInnerDiameter * 2.4), "blockWidth");
|
|
52280
|
+
const blockHeight = requirePositive$6(options.blockHeight ?? bossDiameter + Math.max(12, hoseInnerDiameter * 2.1), "blockHeight");
|
|
52281
|
+
const hoseLength = requirePositive$6(options.hoseLength ?? barbStackLength + Math.max(32, hoseInnerDiameter * 5), "hoseLength");
|
|
52282
|
+
const clampWidth = requirePositive$6(options.clampWidth ?? Math.max(4, hoseOuterDiameter * 0.45), "clampWidth");
|
|
52283
|
+
const clampThickness = requirePositive$6(options.clampThickness ?? 0.9, "clampThickness");
|
|
52284
|
+
const segments = options.segments ?? 40;
|
|
52285
|
+
if (!Number.isInteger(barbCount) || barbCount < 1 || barbCount > 8) {
|
|
52286
|
+
throw new Error("hoseBarbPortAssembly: barbCount must be an integer from 1 to 8");
|
|
52287
|
+
}
|
|
52288
|
+
if (barbPeakDiameter <= hoseInnerDiameter) {
|
|
52289
|
+
throw new Error("hoseBarbPortAssembly: barbPeakDiameter must exceed hoseInnerDiameter so the barb retains the hose");
|
|
52290
|
+
}
|
|
52291
|
+
if (barbRootDiameter >= barbPeakDiameter - Math.max(0.25, hoseInnerDiameter * 0.04)) {
|
|
52292
|
+
throw new Error("hoseBarbPortAssembly: barbRootDiameter must leave a visible barb rise");
|
|
52293
|
+
}
|
|
52294
|
+
if (fluidBoreDiameter >= barbRootDiameter - Math.max(0.8, hoseInnerDiameter * 0.12)) {
|
|
52295
|
+
throw new Error("hoseBarbPortAssembly: fluidBoreDiameter leaves too little wall in the barb fitting");
|
|
52296
|
+
}
|
|
52297
|
+
if (hoseOuterDiameter <= installedHoseBoreDiameter + Math.max(1.2, hoseInnerDiameter * 0.16)) {
|
|
52298
|
+
throw new Error("hoseBarbPortAssembly: hoseOuterDiameter leaves too little hose wall around the installed barb envelope");
|
|
52299
|
+
}
|
|
52300
|
+
if (shoulderDiameter <= barbPeakDiameter + Math.max(1.5, hoseInnerDiameter * 0.2)) {
|
|
52301
|
+
throw new Error("hoseBarbPortAssembly: shoulderDiameter must be larger than the barb peaks");
|
|
52302
|
+
}
|
|
52303
|
+
if (bossDiameter <= shoulderDiameter + Math.max(1.5, hoseInnerDiameter * 0.2)) {
|
|
52304
|
+
throw new Error("hoseBarbPortAssembly: bossDiameter must leave material around the shoulder seat");
|
|
52305
|
+
}
|
|
52306
|
+
if (blockWidth <= bossDiameter + 8 || blockHeight <= bossDiameter + 8) {
|
|
52307
|
+
throw new Error("hoseBarbPortAssembly: receiver block dimensions leave too little material around the port boss");
|
|
52308
|
+
}
|
|
52309
|
+
const portBoreDiameter = barbRootDiameter + runningClearance * 2;
|
|
52310
|
+
const portBore = cylinderAlongX(blockThickness + bossHeight + 0.8, portBoreDiameter / 2, bossHeight / 2, segments);
|
|
52311
|
+
const fuseOverlap = Math.min(0.04, faceClearance * 0.7);
|
|
52312
|
+
const bossCenterX = blockThickness / 2 + bossHeight / 2 - fuseOverlap;
|
|
52313
|
+
const receiver = union(
|
|
52314
|
+
box(blockThickness, blockWidth, blockHeight).translate(0, 0, -blockHeight / 2),
|
|
52315
|
+
cylinderAlongX(bossHeight + fuseOverlap, bossDiameter / 2, bossCenterX, segments)
|
|
52316
|
+
).subtract(portBore).color("#475569");
|
|
52317
|
+
const bossFaceX = blockThickness / 2 + bossHeight;
|
|
52318
|
+
const shoulderCenterX = bossFaceX + faceClearance + shoulderThickness / 2;
|
|
52319
|
+
const barbStartX = shoulderCenterX + shoulderThickness / 2;
|
|
52320
|
+
const fittingStartX = -blockThickness / 2 - runningClearance;
|
|
52321
|
+
const fittingEndX = barbStartX + barbStackLength;
|
|
52322
|
+
const fittingCore = tubeAlongX(fittingEndX - fittingStartX, barbRootDiameter / 2, fluidBoreDiameter / 2, (fittingStartX + fittingEndX) / 2, segments);
|
|
52323
|
+
const shoulder = tubeAlongX(shoulderThickness, shoulderDiameter / 2, fluidBoreDiameter / 2, shoulderCenterX, segments);
|
|
52324
|
+
const barbSolids = [];
|
|
52325
|
+
const ridgeLength = Math.max(0.8, Math.min(barbLength * 0.45, hoseInnerDiameter * 0.28));
|
|
52326
|
+
for (let index2 = 0; index2 < barbCount; index2 += 1) {
|
|
52327
|
+
const startX = barbStartX + index2 * barbLength;
|
|
52328
|
+
const ridgeCenterX = startX + barbLength - ridgeLength / 2;
|
|
52329
|
+
barbSolids.push(tubeAlongX(ridgeLength, barbPeakDiameter / 2, fluidBoreDiameter / 2, ridgeCenterX, segments));
|
|
52330
|
+
}
|
|
52331
|
+
const fitting = union(fittingCore, shoulder, ...barbSolids).color("#94a3b8");
|
|
52332
|
+
const hoseStartX = barbStartX + faceClearance;
|
|
52333
|
+
const hoseCenterX = hoseStartX + hoseLength / 2;
|
|
52334
|
+
const installedHoseBore = cylinderAlongX(hoseLength + 0.8, installedHoseBoreDiameter / 2, hoseCenterX, segments);
|
|
52335
|
+
const hose = tubeAlongX(hoseLength, hoseOuterDiameter / 2, installedHoseBoreDiameter / 2, hoseCenterX, segments).color("#111827");
|
|
52336
|
+
const clampCenterX = barbStartX + Math.min(barbStackLength * 0.55, Math.max(barbLength, clampWidth));
|
|
52337
|
+
const clamp2 = tubeAlongX(
|
|
52338
|
+
clampWidth,
|
|
52339
|
+
hoseOuterDiameter / 2 + clampThickness,
|
|
52340
|
+
hoseOuterDiameter / 2 + Math.min(0.08, runningClearance * 0.45),
|
|
52341
|
+
clampCenterX,
|
|
52342
|
+
segments
|
|
52343
|
+
).color("#cbd5e1");
|
|
52344
|
+
const parts = [
|
|
52345
|
+
{ name: "bored pump or filter body with raised hose-port boss", shape: receiver },
|
|
52346
|
+
{ name: "hollow hose barb fitting with shoulder and retention ridges", shape: fitting },
|
|
52347
|
+
{ name: "installed flexible hose over barb tail", shape: hose },
|
|
52348
|
+
{ name: "clamp band over hose and barb ridges", shape: clamp2 }
|
|
52349
|
+
];
|
|
52350
|
+
return {
|
|
52351
|
+
parts,
|
|
52352
|
+
receiver,
|
|
52353
|
+
fitting,
|
|
52354
|
+
hose,
|
|
52355
|
+
clamp: clamp2,
|
|
52356
|
+
cutters: {
|
|
52357
|
+
portBore,
|
|
52358
|
+
installedHoseBore
|
|
52359
|
+
},
|
|
52360
|
+
dims: {
|
|
52361
|
+
hoseInnerDiameter,
|
|
52362
|
+
hoseOuterDiameter,
|
|
52363
|
+
installedHoseBoreDiameter,
|
|
52364
|
+
blockThickness,
|
|
52365
|
+
blockWidth,
|
|
52366
|
+
blockHeight,
|
|
52367
|
+
bossDiameter,
|
|
52368
|
+
bossHeight,
|
|
52369
|
+
fluidBoreDiameter,
|
|
52370
|
+
barbRootDiameter,
|
|
52371
|
+
barbPeakDiameter,
|
|
52372
|
+
barbCount,
|
|
52373
|
+
barbLength,
|
|
52374
|
+
barbStackLength,
|
|
52375
|
+
shoulderDiameter,
|
|
52376
|
+
shoulderThickness,
|
|
52377
|
+
hoseLength,
|
|
52378
|
+
clampWidth,
|
|
52379
|
+
clampThickness,
|
|
52380
|
+
runningClearance,
|
|
52381
|
+
faceClearance
|
|
52382
|
+
}
|
|
52383
|
+
};
|
|
52384
|
+
}
|
|
52385
|
+
function routedTubeClipAssembly(options) {
|
|
52386
|
+
const tubeDiameter = requirePositive$6(options.tubeDiameter, "tubeDiameter");
|
|
52387
|
+
const tubeLength = requirePositive$6(options.tubeLength ?? 120, "tubeLength");
|
|
52388
|
+
const panelThickness = requirePositive$6(options.panelThickness ?? 3, "panelThickness");
|
|
52389
|
+
const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
|
|
52390
|
+
const screwSize = options.screwSize ?? "M3";
|
|
52391
|
+
const segments = options.segments ?? 32;
|
|
52392
|
+
const sizeData = METRIC_HOLE_TABLE[screwSize];
|
|
52393
|
+
if (!sizeData) throw new Error(`routedTubeClipAssembly: unsupported screwSize "${screwSize}"`);
|
|
52394
|
+
const clipCount = options.clipCount ?? 3;
|
|
52395
|
+
if (!Number.isInteger(clipCount) || clipCount < 1 || clipCount > 8) {
|
|
52396
|
+
throw new Error("routedTubeClipAssembly: clipCount must be an integer from 1 to 8");
|
|
52397
|
+
}
|
|
52398
|
+
const screwDiameter = parseFloat(screwSize.replace("M", ""));
|
|
52399
|
+
const screwHeadDiameter = sizeData.head;
|
|
52400
|
+
const tubeBoreDiameter = tubeDiameter + runningClearance * 2;
|
|
52401
|
+
const clipWallThickness = requirePositive$6(
|
|
52402
|
+
options.clipWallThickness ?? Math.max(screwHeadDiameter + 1.2, tubeDiameter * 0.45, 5),
|
|
52403
|
+
"clipWallThickness"
|
|
52404
|
+
);
|
|
52405
|
+
const clipWidth = requirePositive$6(options.clipWidth ?? Math.max(screwHeadDiameter + 3, tubeDiameter * 1.4, 10), "clipWidth");
|
|
52406
|
+
const clipDepth = tubeBoreDiameter + clipWallThickness * 2;
|
|
52407
|
+
const bottomWall = Math.max(1.2, clipWallThickness * 0.35);
|
|
52408
|
+
const topWall = Math.max(2, clipWallThickness * 0.45);
|
|
52409
|
+
const clipHeight = bottomWall + tubeBoreDiameter + topWall;
|
|
52410
|
+
const tubeCenterZ = panelThickness + bottomWall + tubeBoreDiameter / 2;
|
|
52411
|
+
const panelLength = requirePositive$6(options.panelLength ?? tubeLength + 24, "panelLength");
|
|
52412
|
+
const panelWidth = requirePositive$6(options.panelWidth ?? clipDepth + Math.max(14, screwHeadDiameter * 2), "panelWidth");
|
|
52413
|
+
if (tubeLength <= clipWidth + 8) {
|
|
52414
|
+
throw new Error("routedTubeClipAssembly: tubeLength must leave visible tube beyond the clip body");
|
|
52415
|
+
}
|
|
52416
|
+
const defaultSpacing = clipCount === 1 ? 0 : Math.max(clipWidth + 8, (tubeLength - clipWidth * 2) / (clipCount - 1));
|
|
52417
|
+
const clipSpacing = options.clipSpacing === void 0 ? defaultSpacing : requirePositive$6(options.clipSpacing, "clipSpacing");
|
|
52418
|
+
const clipCenters = Array.from({ length: clipCount }, (_2, index2) => (index2 - (clipCount - 1) / 2) * clipSpacing);
|
|
52419
|
+
const maxClipExtent = Math.max(...clipCenters.map((x2) => Math.abs(x2) + clipWidth / 2));
|
|
52420
|
+
if (maxClipExtent > tubeLength / 2 - 2) {
|
|
52421
|
+
throw new Error("routedTubeClipAssembly: clipSpacing places a clip beyond the routed tube length");
|
|
52422
|
+
}
|
|
52423
|
+
if (maxClipExtent > panelLength / 2 - 2) {
|
|
52424
|
+
throw new Error("routedTubeClipAssembly: panelLength is too short for the clip pattern");
|
|
52425
|
+
}
|
|
52426
|
+
const boreRadius = tubeBoreDiameter / 2;
|
|
52427
|
+
const screwY = boreRadius + clipWallThickness / 2;
|
|
52428
|
+
if (screwY + screwHeadDiameter / 2 > clipDepth / 2 - 0.2) {
|
|
52429
|
+
throw new Error("routedTubeClipAssembly: clipWallThickness leaves too little land for screw heads");
|
|
52430
|
+
}
|
|
52431
|
+
if (clipDepth > panelWidth - Math.max(4, screwHeadDiameter * 0.5)) {
|
|
52432
|
+
throw new Error("routedTubeClipAssembly: panelWidth leaves too little material beside the clips");
|
|
52433
|
+
}
|
|
52434
|
+
const screwPositions = clipCenters.flatMap((x2) => [
|
|
52435
|
+
[x2, -screwY],
|
|
52436
|
+
[x2, screwY]
|
|
52437
|
+
]);
|
|
52438
|
+
const screwClearanceDiameter = Math.max(sizeData.loose, screwDiameter + 0.8);
|
|
52439
|
+
const panelThreadEnvelopeDiameter = screwClearanceDiameter;
|
|
52440
|
+
const clipTopZ = panelThickness + clipHeight;
|
|
52441
|
+
const clipTubeBores = union(
|
|
52442
|
+
...clipCenters.map((x2) => cylinderAlongX(clipWidth + 0.8, boreRadius, x2, segments).translate(0, 0, tubeCenterZ))
|
|
52443
|
+
);
|
|
52444
|
+
const clipScrewClearances = union(
|
|
52445
|
+
...screwPositions.map(([x2, y2]) => cylinder(clipHeight + 0.8, screwClearanceDiameter / 2, void 0, segments).translate(x2, y2, panelThickness - 0.4))
|
|
52446
|
+
);
|
|
52447
|
+
const panelThreadEnvelopes = union(
|
|
52448
|
+
...screwPositions.map(([x2, y2]) => cylinder(panelThickness + 0.8, panelThreadEnvelopeDiameter / 2, void 0, segments).translate(x2, y2, -0.4))
|
|
52449
|
+
);
|
|
52450
|
+
const panel = box(panelLength, panelWidth, panelThickness).subtract(panelThreadEnvelopes).color("#475569");
|
|
52451
|
+
const tube2 = cylinderAlongX(tubeLength, tubeDiameter / 2, 0, segments).translate(0, 0, tubeCenterZ).color("#0f172a");
|
|
52452
|
+
const clips = clipCenters.map((x2) => {
|
|
52453
|
+
const body = box(clipWidth, clipDepth, clipHeight).translate(x2, 0, panelThickness);
|
|
52454
|
+
const tubeBore = cylinderAlongX(clipWidth + 0.8, boreRadius, x2, segments).translate(0, 0, tubeCenterZ);
|
|
52455
|
+
const screwHoles = union(
|
|
52456
|
+
cylinder(clipHeight + 0.8, screwClearanceDiameter / 2, void 0, segments).translate(x2, -screwY, panelThickness - 0.4),
|
|
52457
|
+
cylinder(clipHeight + 0.8, screwClearanceDiameter / 2, void 0, segments).translate(x2, screwY, panelThickness - 0.4)
|
|
52458
|
+
);
|
|
52459
|
+
return body.subtract(tubeBore).subtract(screwHoles).color("#94a3b8");
|
|
52460
|
+
});
|
|
52461
|
+
const screwLength = clipHeight + panelThickness * 0.65;
|
|
52462
|
+
const screwHeadHeight = Math.max(1.2, screwDiameter * 0.55);
|
|
52463
|
+
const screwBlank = union(
|
|
52464
|
+
cylinder(screwLength, screwDiameter / 2, void 0, segments).translate(0, 0, clipTopZ - screwLength),
|
|
52465
|
+
cylinder(screwHeadHeight, screwHeadDiameter / 2, void 0, segments).translate(0, 0, clipTopZ)
|
|
52466
|
+
).color("#cbd5e1");
|
|
52467
|
+
const screws = screwPositions.map(([x2, y2]) => screwBlank.translate(x2, y2, 0));
|
|
52468
|
+
const parts = [
|
|
52469
|
+
{ name: "panel with tube-clip screw receiving holes", shape: panel },
|
|
52470
|
+
{ name: "routed flexible tube through retained clip bores", shape: tube2 },
|
|
52471
|
+
...clips.map((shape, index2) => ({ name: `saddle tube clip ${index2 + 1} with through-bore`, shape })),
|
|
52472
|
+
...screws.map((shape, index2) => ({ name: `installed ${screwSize} tube clip screw ${index2 + 1}`, shape }))
|
|
52473
|
+
];
|
|
52474
|
+
return {
|
|
52475
|
+
parts,
|
|
52476
|
+
panel,
|
|
52477
|
+
tube: tube2,
|
|
52478
|
+
clips,
|
|
52479
|
+
screws,
|
|
52480
|
+
clipCenters,
|
|
52481
|
+
screwPositions,
|
|
52482
|
+
cutters: {
|
|
52483
|
+
clipTubeBores,
|
|
52484
|
+
clipScrewClearances,
|
|
52485
|
+
panelThreadEnvelopes
|
|
52486
|
+
},
|
|
52487
|
+
dims: {
|
|
52488
|
+
tubeDiameter,
|
|
52489
|
+
tubeLength,
|
|
52490
|
+
tubeBoreDiameter,
|
|
52491
|
+
panelLength,
|
|
52492
|
+
panelWidth,
|
|
52493
|
+
panelThickness,
|
|
52494
|
+
clipCount,
|
|
52495
|
+
clipWidth,
|
|
52496
|
+
clipDepth,
|
|
52497
|
+
clipHeight,
|
|
52498
|
+
clipWallThickness,
|
|
52499
|
+
tubeCenterZ,
|
|
52500
|
+
screwSize,
|
|
52501
|
+
screwDiameter,
|
|
52502
|
+
screwHeadDiameter,
|
|
52503
|
+
screwLength,
|
|
52504
|
+
screwClearanceDiameter,
|
|
52505
|
+
panelThreadEnvelopeDiameter,
|
|
52506
|
+
runningClearance
|
|
52507
|
+
}
|
|
52508
|
+
};
|
|
52509
|
+
}
|
|
52510
|
+
function pcbTerminalBlockAssembly(options = {}) {
|
|
52511
|
+
const terminalCount = options.terminalCount ?? 4;
|
|
52512
|
+
if (!Number.isInteger(terminalCount) || terminalCount < 1 || terminalCount > 24) {
|
|
52513
|
+
throw new Error("pcbTerminalBlockAssembly: terminalCount must be an integer from 1 to 24");
|
|
52514
|
+
}
|
|
52515
|
+
const terminalPitch = requirePositive$6(options.terminalPitch ?? 5.08, "terminalPitch");
|
|
52516
|
+
const terminalBlockWidth = terminalPitch * terminalCount + 3;
|
|
52517
|
+
const boardWidth = requirePositive$6(options.boardWidth ?? Math.max(50, terminalBlockWidth + 28), "boardWidth");
|
|
52518
|
+
const boardDepth = requirePositive$6(options.boardDepth ?? 38, "boardDepth");
|
|
52519
|
+
const boardThickness = requirePositive$6(options.boardThickness ?? 1.6, "boardThickness");
|
|
52520
|
+
const backplateThickness = requirePositive$6(options.backplateThickness ?? 3, "backplateThickness");
|
|
52521
|
+
const backplateMargin = requirePositive$6(options.backplateMargin ?? 5, "backplateMargin");
|
|
52522
|
+
const standoffHeight = requirePositive$6(options.standoffHeight ?? 6, "standoffHeight");
|
|
52523
|
+
const screwSize = options.screwSize ?? "M3";
|
|
52524
|
+
const segments = options.segments ?? 28;
|
|
52525
|
+
const sizeData = METRIC_HOLE_TABLE[screwSize];
|
|
52526
|
+
if (!sizeData) throw new Error(`pcbTerminalBlockAssembly: unsupported screwSize "${screwSize}"`);
|
|
52527
|
+
const screwDiameter = parseFloat(screwSize.replace("M", ""));
|
|
52528
|
+
const screwHeadDiameter = sizeData.head;
|
|
52529
|
+
const screwHeadHeight = Math.max(1.2, screwDiameter * 0.55);
|
|
52530
|
+
const standoffDiameter = requirePositive$6(
|
|
52531
|
+
options.standoffDiameter ?? Math.max(screwHeadDiameter * 1.45, sizeData.normal + 3),
|
|
52532
|
+
"standoffDiameter"
|
|
52533
|
+
);
|
|
52534
|
+
const [mountInsetX, mountInsetY] = resolveBoltInset(
|
|
52535
|
+
options.mountingInset,
|
|
52536
|
+
Math.max(standoffDiameter / 2 + 1.2, screwHeadDiameter * 0.75)
|
|
52537
|
+
);
|
|
52538
|
+
if (mountInsetX * 2 >= boardWidth || mountInsetY * 2 >= boardDepth) {
|
|
52539
|
+
throw new Error("pcbTerminalBlockAssembly: mountingInset leaves no room for the PCB mounting pattern");
|
|
52540
|
+
}
|
|
52541
|
+
const terminalBlockDepth = requirePositive$6(options.terminalBlockDepth ?? 10, "terminalBlockDepth");
|
|
52542
|
+
const terminalBlockHeight = requirePositive$6(options.terminalBlockHeight ?? 9, "terminalBlockHeight");
|
|
52543
|
+
const terminalEdgeInset = requirePositive$6(options.terminalEdgeInset ?? 5, "terminalEdgeInset");
|
|
52544
|
+
const pinDiameter = requirePositive$6(options.pinDiameter ?? 0.9, "pinDiameter");
|
|
52545
|
+
const pinClearance = requirePositive$6(options.pinClearance ?? 0.25, "pinClearance");
|
|
52546
|
+
const pinTailLength = requireNonNegative(options.pinTailLength ?? 0, "pinTailLength");
|
|
52547
|
+
const wirePortDiameter = requirePositive$6(options.wirePortDiameter ?? 2.6, "wirePortDiameter");
|
|
52548
|
+
const pinHoleDiameter = pinDiameter + pinClearance;
|
|
52549
|
+
const terminalCenterY = -boardDepth / 2 + terminalEdgeInset + terminalBlockDepth / 2;
|
|
52550
|
+
const pinY = terminalCenterY + terminalBlockDepth * 0.24;
|
|
52551
|
+
const firstPinX = -((terminalCount - 1) * terminalPitch) / 2;
|
|
52552
|
+
const pinPositions = Array.from({ length: terminalCount }, (_2, index2) => [firstPinX + index2 * terminalPitch, pinY]);
|
|
52553
|
+
const mountingPositions = [
|
|
52554
|
+
[-boardWidth / 2 + mountInsetX, -boardDepth / 2 + mountInsetY],
|
|
52555
|
+
[boardWidth / 2 - mountInsetX, -boardDepth / 2 + mountInsetY],
|
|
52556
|
+
[-boardWidth / 2 + mountInsetX, boardDepth / 2 - mountInsetY],
|
|
52557
|
+
[boardWidth / 2 - mountInsetX, boardDepth / 2 - mountInsetY]
|
|
52558
|
+
];
|
|
52559
|
+
if (terminalBlockWidth >= boardWidth - mountInsetX * 2) {
|
|
52560
|
+
throw new Error("pcbTerminalBlockAssembly: terminal block is too wide for the PCB mounting pattern");
|
|
52561
|
+
}
|
|
52562
|
+
if (terminalEdgeInset + terminalBlockDepth >= boardDepth - mountInsetY * 2) {
|
|
52563
|
+
throw new Error("pcbTerminalBlockAssembly: terminal block depth collides with the rear mounting datum");
|
|
52564
|
+
}
|
|
52565
|
+
if (pinHoleDiameter >= terminalPitch * 0.55) {
|
|
52566
|
+
throw new Error("pcbTerminalBlockAssembly: pinDiameter and pinClearance leave too little PCB web between terminal holes");
|
|
52567
|
+
}
|
|
52568
|
+
if (wirePortDiameter >= Math.min(terminalPitch * 0.72, terminalBlockHeight * 0.65)) {
|
|
52569
|
+
throw new Error("pcbTerminalBlockAssembly: wirePortDiameter is too large for the terminal pitch or body height");
|
|
52570
|
+
}
|
|
52571
|
+
for (const [index2, [x2, y2]] of [...mountingPositions, ...pinPositions].entries()) {
|
|
52572
|
+
if (!Number.isFinite(x2) || !Number.isFinite(y2)) {
|
|
52573
|
+
throw new Error(`pcbTerminalBlockAssembly: generated datum position ${index2} is not finite`);
|
|
52574
|
+
}
|
|
52575
|
+
}
|
|
52576
|
+
const backplateWidth = boardWidth + backplateMargin * 2;
|
|
52577
|
+
const backplateDepth = boardDepth + backplateMargin * 2;
|
|
52578
|
+
const boardBottomZ = backplateThickness + standoffHeight;
|
|
52579
|
+
const boardTopZ = boardBottomZ + boardThickness;
|
|
52580
|
+
const standoffOverlap = Math.min(0.08, standoffHeight * 0.03);
|
|
52581
|
+
const standoffThreadEnvelopeDiameter = Math.max(sizeData.loose, screwDiameter + 1);
|
|
52582
|
+
const standoffThreadEnvelope = cylinder(standoffHeight + 0.8, standoffThreadEnvelopeDiameter / 2, void 0, segments).translate(
|
|
52583
|
+
0,
|
|
52584
|
+
0,
|
|
52585
|
+
backplateThickness - 0.4
|
|
52586
|
+
);
|
|
52587
|
+
const standoffThreadEnvelopes = union(...mountingPositions.map(([x2, y2]) => standoffThreadEnvelope.translate(x2, y2, 0)));
|
|
52588
|
+
const standoff = cylinder(standoffHeight + standoffOverlap, standoffDiameter / 2, void 0, segments).translate(0, 0, backplateThickness - standoffOverlap).subtract(standoffThreadEnvelope);
|
|
52589
|
+
const standoffs = union(...mountingPositions.map(([x2, y2]) => standoff.translate(x2, y2, 0)));
|
|
52590
|
+
const backplate = union(box(backplateWidth, backplateDepth, backplateThickness), standoffs).color("#475569");
|
|
52591
|
+
const boardMountingHoleDiameter = sizeData.normal;
|
|
52592
|
+
const boardMountHole = cylinder(boardThickness + 0.8, boardMountingHoleDiameter / 2, void 0, segments).translate(
|
|
52593
|
+
0,
|
|
52594
|
+
0,
|
|
52595
|
+
boardBottomZ - 0.4
|
|
52596
|
+
);
|
|
52597
|
+
const pcbMountingHoles = union(...mountingPositions.map(([x2, y2]) => boardMountHole.translate(x2, y2, 0)));
|
|
52598
|
+
const pinHole = cylinder(boardThickness + 0.8, pinHoleDiameter / 2, void 0, segments).translate(0, 0, boardBottomZ - 0.4);
|
|
52599
|
+
const pcbPinHoles = union(...pinPositions.map(([x2, y2]) => pinHole.translate(x2, y2, 0)));
|
|
52600
|
+
const pcb = box(boardWidth, boardDepth, boardThickness).translate(0, 0, boardBottomZ).subtract(pcbMountingHoles).subtract(pcbPinHoles).color("#166534");
|
|
52601
|
+
const terminalBodyBlank = box(terminalBlockWidth, terminalBlockDepth, terminalBlockHeight).translate(0, terminalCenterY, boardTopZ);
|
|
52602
|
+
const wirePort = cylinderAlongY(terminalBlockDepth + 0.8, wirePortDiameter / 2, terminalCenterY, segments).translate(
|
|
52603
|
+
0,
|
|
52604
|
+
0,
|
|
52605
|
+
boardTopZ + terminalBlockHeight * 0.42
|
|
52606
|
+
);
|
|
52607
|
+
const wirePorts = union(...pinPositions.map(([x2]) => wirePort.translate(x2, 0, 0)));
|
|
52608
|
+
const clampScrewPockets = union(
|
|
52609
|
+
...pinPositions.map(
|
|
52610
|
+
([x2]) => cylinder(Math.max(0.6, terminalBlockHeight * 0.22), Math.min(terminalPitch * 0.22, wirePortDiameter * 0.42), void 0, segments).translate(
|
|
52611
|
+
x2,
|
|
52612
|
+
terminalCenterY + terminalBlockDepth * 0.12,
|
|
52613
|
+
boardTopZ + terminalBlockHeight * 0.76
|
|
52614
|
+
)
|
|
52615
|
+
)
|
|
52616
|
+
);
|
|
52617
|
+
const pinLength = boardThickness + pinTailLength + Math.min(0.6, terminalBlockHeight * 0.08);
|
|
52618
|
+
const pinStartZ = boardBottomZ - pinTailLength;
|
|
52619
|
+
const pins = union(...pinPositions.map(([x2, y2]) => cylinder(pinLength, pinDiameter / 2, void 0, segments).translate(x2, y2, pinStartZ)));
|
|
52620
|
+
const terminalBlock = union(terminalBodyBlank.subtract(wirePorts).subtract(clampScrewPockets), pins).color("#16a34a");
|
|
52621
|
+
const screwShaftLength = boardThickness + standoffHeight * 0.85;
|
|
52622
|
+
const mountingHardware = fastenerSet(screwSize, screwShaftLength, {
|
|
52623
|
+
washerUnderHead: false,
|
|
52624
|
+
washerUnderNut: false,
|
|
52625
|
+
fit: "normal",
|
|
52626
|
+
segments
|
|
52627
|
+
});
|
|
52628
|
+
const screws = mountingPositions.map(([x2, y2]) => mountingHardware.bolt.translate(x2, y2, boardTopZ).color("#cbd5e1"));
|
|
52629
|
+
const parts = [
|
|
52630
|
+
{ name: "electronics backplate with fused PCB standoffs", shape: backplate },
|
|
52631
|
+
{ name: "PCB with mounting holes and terminal pin clearances", shape: pcb },
|
|
52632
|
+
{ name: "seated purchased terminal block with through-board pins", shape: terminalBlock },
|
|
52633
|
+
...screws.map((shape, index2) => ({ name: `installed ${screwSize} PCB mounting screw ${index2 + 1}`, shape }))
|
|
52634
|
+
];
|
|
52635
|
+
return {
|
|
52636
|
+
parts,
|
|
52637
|
+
backplate,
|
|
52638
|
+
pcb,
|
|
52639
|
+
terminalBlock,
|
|
52640
|
+
screws,
|
|
52641
|
+
mountingPositions,
|
|
52642
|
+
pinPositions,
|
|
52643
|
+
cutters: {
|
|
52644
|
+
pcbMountingHoles,
|
|
52645
|
+
pcbPinHoles,
|
|
52646
|
+
standoffThreadEnvelopes
|
|
52647
|
+
},
|
|
52648
|
+
dims: {
|
|
52649
|
+
terminalCount,
|
|
52650
|
+
terminalPitch,
|
|
52651
|
+
boardWidth,
|
|
52652
|
+
boardDepth,
|
|
52653
|
+
boardThickness,
|
|
52654
|
+
backplateWidth,
|
|
52655
|
+
backplateDepth,
|
|
52656
|
+
backplateThickness,
|
|
52657
|
+
standoffHeight,
|
|
52658
|
+
standoffDiameter,
|
|
52659
|
+
screwSize,
|
|
52660
|
+
screwDiameter,
|
|
52661
|
+
screwHeadDiameter,
|
|
52662
|
+
screwHeadHeight,
|
|
52663
|
+
screwShaftLength,
|
|
52664
|
+
boardMountingHoleDiameter,
|
|
52665
|
+
standoffThreadEnvelopeDiameter,
|
|
52666
|
+
terminalBlockWidth,
|
|
52667
|
+
terminalBlockDepth,
|
|
52668
|
+
terminalBlockHeight,
|
|
52669
|
+
terminalEdgeInset,
|
|
52670
|
+
pinDiameter,
|
|
52671
|
+
pinClearance,
|
|
52672
|
+
pinHoleDiameter,
|
|
52673
|
+
pinTailLength,
|
|
52674
|
+
wirePortDiameter
|
|
52675
|
+
}
|
|
52676
|
+
};
|
|
52677
|
+
}
|
|
52678
|
+
function thumbScrewClampAssembly(options = {}) {
|
|
52679
|
+
const screwSize = options.screwSize ?? "M6";
|
|
52680
|
+
const segments = options.segments ?? 36;
|
|
52681
|
+
const sizeData = METRIC_HOLE_TABLE[screwSize];
|
|
52682
|
+
if (!sizeData) throw new Error(`thumbScrewClampAssembly: unsupported screwSize "${screwSize}"`);
|
|
52683
|
+
const screwDiameter = parseFloat(screwSize.replace("M", ""));
|
|
52684
|
+
const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
|
|
52685
|
+
const faceClearance = requireNonNegative(options.faceClearance ?? 0, "faceClearance");
|
|
52686
|
+
const threadEnvelopeDiameter = Math.max(sizeData.normal, screwDiameter + runningClearance * 2);
|
|
52687
|
+
const pressurePadDiameter = requirePositive$6(
|
|
52688
|
+
options.pressurePadDiameter ?? Math.max(screwDiameter * 3.2, 18),
|
|
52689
|
+
"pressurePadDiameter"
|
|
52690
|
+
);
|
|
52691
|
+
const pressurePadThickness = requirePositive$6(
|
|
52692
|
+
options.pressurePadThickness ?? Math.max(screwDiameter * 0.72, 4),
|
|
52693
|
+
"pressurePadThickness"
|
|
52694
|
+
);
|
|
52695
|
+
const knobDiameter = requirePositive$6(options.knobDiameter ?? Math.max(screwDiameter * 4.2, 24), "knobDiameter");
|
|
52696
|
+
const knobThickness = requirePositive$6(options.knobThickness ?? Math.max(screwDiameter * 0.9, 7), "knobThickness");
|
|
52697
|
+
const workpieceThickness = requirePositive$6(options.workpieceThickness ?? 18, "workpieceThickness");
|
|
52698
|
+
const workpieceDepth = requirePositive$6(options.workpieceDepth ?? Math.max(46, pressurePadDiameter * 1.5), "workpieceDepth");
|
|
52699
|
+
const workpieceHeight = requirePositive$6(options.workpieceHeight ?? Math.max(pressurePadDiameter * 1.35, 24), "workpieceHeight");
|
|
52700
|
+
const frameDepth = requirePositive$6(
|
|
52701
|
+
options.frameDepth ?? Math.max(workpieceDepth + 12, pressurePadDiameter + 16),
|
|
52702
|
+
"frameDepth"
|
|
52703
|
+
);
|
|
52704
|
+
const baseThickness = requirePositive$6(options.baseThickness ?? Math.max(screwDiameter, 6), "baseThickness");
|
|
52705
|
+
const jawThickness = requirePositive$6(options.jawThickness ?? Math.max(screwDiameter * 1.35, 9), "jawThickness");
|
|
52706
|
+
const supportThickness = requirePositive$6(
|
|
52707
|
+
options.supportThickness ?? Math.max(screwDiameter * 1.8, 12),
|
|
52708
|
+
"supportThickness"
|
|
52709
|
+
);
|
|
52710
|
+
const bossLength = requirePositive$6(options.bossLength ?? Math.max(screwDiameter * 1.1, 8), "bossLength");
|
|
52711
|
+
const bossDiameter = requirePositive$6(options.bossDiameter ?? Math.max(threadEnvelopeDiameter + 5, screwDiameter * 2.5), "bossDiameter");
|
|
52712
|
+
const exposedScrewLength = requirePositive$6(
|
|
52713
|
+
options.exposedScrewLength ?? Math.max(pressurePadDiameter * 0.45, screwDiameter * 2.2),
|
|
52714
|
+
"exposedScrewLength"
|
|
52715
|
+
);
|
|
52716
|
+
const screwCenterZ = baseThickness + Math.max(workpieceHeight * 0.52, pressurePadDiameter * 0.68);
|
|
52717
|
+
const frameHeight = requirePositive$6(
|
|
52718
|
+
options.frameHeight ?? screwCenterZ - baseThickness + pressurePadDiameter / 2 + Math.max(baseThickness, 7),
|
|
52719
|
+
"frameHeight"
|
|
52720
|
+
);
|
|
52721
|
+
if (workpieceDepth > frameDepth - 6) {
|
|
52722
|
+
throw new Error("thumbScrewClampAssembly: frameDepth must leave side material around the clamped workpiece");
|
|
52723
|
+
}
|
|
52724
|
+
if (pressurePadDiameter > frameDepth - 4) {
|
|
52725
|
+
throw new Error("thumbScrewClampAssembly: pressurePadDiameter is too large for the frame depth");
|
|
52726
|
+
}
|
|
52727
|
+
if (bossDiameter > frameDepth - 4) {
|
|
52728
|
+
throw new Error("thumbScrewClampAssembly: bossDiameter is too large for the frame depth");
|
|
52729
|
+
}
|
|
52730
|
+
if (screwCenterZ - pressurePadDiameter / 2 <= baseThickness + 0.5) {
|
|
52731
|
+
throw new Error("thumbScrewClampAssembly: pressure pad collides with the base bridge");
|
|
52732
|
+
}
|
|
52733
|
+
if (baseThickness + frameHeight - screwCenterZ <= pressurePadDiameter / 2 + 2) {
|
|
52734
|
+
throw new Error("thumbScrewClampAssembly: frameHeight leaves too little material above the screw axis");
|
|
52735
|
+
}
|
|
52736
|
+
if (threadEnvelopeDiameter + 4 > Math.min(frameDepth, frameHeight)) {
|
|
52737
|
+
throw new Error("thumbScrewClampAssembly: threaded boss bore leaves too little surrounding frame material");
|
|
52738
|
+
}
|
|
52739
|
+
const workpieceLeftFaceX = -workpieceThickness / 2;
|
|
52740
|
+
const workpieceRightFaceX = workpieceThickness / 2;
|
|
52741
|
+
const anvilOverlap = Math.min(0.35, pressurePadThickness * 0.18);
|
|
52742
|
+
const anvilPadCenterX = workpieceLeftFaceX - faceClearance - pressurePadThickness / 2;
|
|
52743
|
+
const pressurePadCenterX = workpieceRightFaceX + faceClearance + pressurePadThickness / 2;
|
|
52744
|
+
const fixedJawRightFaceX = anvilPadCenterX - pressurePadThickness / 2 + anvilOverlap;
|
|
52745
|
+
const fixedJawCenterX = fixedJawRightFaceX - jawThickness / 2;
|
|
52746
|
+
const pressurePadRightFaceX = pressurePadCenterX + pressurePadThickness / 2;
|
|
52747
|
+
const supportInnerFaceX = pressurePadRightFaceX + exposedScrewLength;
|
|
52748
|
+
const supportCenterX = supportInnerFaceX + supportThickness / 2;
|
|
52749
|
+
const supportOuterFaceX = supportInnerFaceX + supportThickness;
|
|
52750
|
+
const frameLeftFaceX = fixedJawCenterX - jawThickness / 2;
|
|
52751
|
+
const frameRightFaceX = supportOuterFaceX;
|
|
52752
|
+
const baseLength = frameRightFaceX - frameLeftFaceX;
|
|
52753
|
+
if (baseLength <= 0 || !Number.isFinite(baseLength)) {
|
|
52754
|
+
throw new Error("thumbScrewClampAssembly: generated clamp frame length is invalid");
|
|
52755
|
+
}
|
|
52756
|
+
const bossCenterX = supportInnerFaceX + (supportThickness + bossLength) / 2;
|
|
52757
|
+
const threadedBossBore = cylinderAlongX(supportThickness + bossLength + 1, threadEnvelopeDiameter / 2, bossCenterX, segments).translate(
|
|
52758
|
+
0,
|
|
52759
|
+
0,
|
|
52760
|
+
screwCenterZ
|
|
52761
|
+
);
|
|
52762
|
+
const frameOverlap = Math.min(0.12, baseThickness * 0.04);
|
|
52763
|
+
const base = box(baseLength, frameDepth, baseThickness).translate((frameLeftFaceX + frameRightFaceX) / 2, 0, 0);
|
|
52764
|
+
const fixedJaw = box(jawThickness, frameDepth, frameHeight + frameOverlap).translate(fixedJawCenterX, 0, baseThickness - frameOverlap);
|
|
52765
|
+
const support = box(supportThickness, frameDepth, frameHeight + frameOverlap).translate(supportCenterX, 0, baseThickness - frameOverlap);
|
|
52766
|
+
const boss2 = cylinderAlongX(supportThickness + bossLength, bossDiameter / 2, bossCenterX, segments).translate(0, 0, screwCenterZ);
|
|
52767
|
+
const anvilPad = cylinderAlongX(pressurePadThickness, pressurePadDiameter / 2, anvilPadCenterX, segments).translate(0, 0, screwCenterZ);
|
|
52768
|
+
const frame = union(base, fixedJaw, support, boss2, anvilPad).subtract(threadedBossBore).color("#475569");
|
|
52769
|
+
const workpieceBottomZ = screwCenterZ - workpieceHeight / 2;
|
|
52770
|
+
const workpiece = box(workpieceThickness, workpieceDepth, workpieceHeight).translate(0, 0, workpieceBottomZ).color("#a16207");
|
|
52771
|
+
const pressurePad = cylinderAlongX(pressurePadThickness, pressurePadDiameter / 2, pressurePadCenterX, segments).translate(0, 0, screwCenterZ);
|
|
52772
|
+
const knobCenterX = supportOuterFaceX + bossLength + runningClearance + knobThickness / 2;
|
|
52773
|
+
const knob = cylinderAlongX(knobThickness, knobDiameter / 2, knobCenterX, segments).translate(0, 0, screwCenterZ);
|
|
52774
|
+
const shaftLeftX = pressurePadRightFaceX - Math.min(pressurePadThickness * 0.45, screwDiameter * 0.45);
|
|
52775
|
+
const shaftRightX = knobCenterX + knobThickness / 2;
|
|
52776
|
+
const shaftLength = shaftRightX - shaftLeftX;
|
|
52777
|
+
if (shaftLength <= supportThickness + bossLength) {
|
|
52778
|
+
throw new Error("thumbScrewClampAssembly: generated screw length is too short for the threaded support");
|
|
52779
|
+
}
|
|
52780
|
+
const shaft = cylinderAlongX(shaftLength, screwDiameter / 2, (shaftLeftX + shaftRightX) / 2, segments).translate(0, 0, screwCenterZ);
|
|
52781
|
+
const clampScrew = union(shaft, pressurePad, knob).color("#cbd5e1");
|
|
52782
|
+
const workpieceEnvelope = box(workpieceThickness, workpieceDepth, workpieceHeight).translate(0, 0, workpieceBottomZ);
|
|
52783
|
+
return {
|
|
52784
|
+
parts: [
|
|
52785
|
+
{ name: "thumb-screw clamp frame with fixed anvil and threaded boss", shape: frame },
|
|
52786
|
+
{ name: "representative clamped workpiece between pads", shape: workpiece },
|
|
52787
|
+
{ name: "installed thumb screw with captive pressure pad and hand knob", shape: clampScrew }
|
|
52788
|
+
],
|
|
52789
|
+
frame,
|
|
52790
|
+
workpiece,
|
|
52791
|
+
clampScrew,
|
|
52792
|
+
cutters: {
|
|
52793
|
+
threadedBossBore,
|
|
52794
|
+
workpieceEnvelope
|
|
52795
|
+
},
|
|
52796
|
+
dims: {
|
|
52797
|
+
screwSize,
|
|
52798
|
+
screwDiameter,
|
|
52799
|
+
threadEnvelopeDiameter,
|
|
52800
|
+
workpieceThickness,
|
|
52801
|
+
workpieceDepth,
|
|
52802
|
+
workpieceHeight,
|
|
52803
|
+
frameDepth,
|
|
52804
|
+
frameHeight,
|
|
52805
|
+
baseThickness,
|
|
52806
|
+
jawThickness,
|
|
52807
|
+
supportThickness,
|
|
52808
|
+
bossLength,
|
|
52809
|
+
bossDiameter,
|
|
52810
|
+
exposedScrewLength,
|
|
52811
|
+
pressurePadDiameter,
|
|
52812
|
+
pressurePadThickness,
|
|
52813
|
+
knobDiameter,
|
|
52814
|
+
knobThickness,
|
|
52815
|
+
screwCenterZ,
|
|
52816
|
+
fixedAnvilFaceX: workpieceLeftFaceX - faceClearance,
|
|
52817
|
+
pressurePadFaceX: workpieceRightFaceX + faceClearance,
|
|
52818
|
+
supportInnerFaceX,
|
|
52819
|
+
runningClearance,
|
|
52820
|
+
faceClearance
|
|
52821
|
+
}
|
|
52822
|
+
};
|
|
52823
|
+
}
|
|
50963
52824
|
function fastenerSet(size, boltLength, options) {
|
|
50964
52825
|
const sizeData = METRIC_HOLE_TABLE[size];
|
|
50965
52826
|
if (!sizeData) throw new Error(`fastenerSet: unsupported size "${size}"`);
|
|
@@ -51020,6 +52881,22 @@ const partLibrary = {
|
|
|
51020
52881
|
nut,
|
|
51021
52882
|
washer,
|
|
51022
52883
|
fastenerSet,
|
|
52884
|
+
boltedServiceCover,
|
|
52885
|
+
datumEnclosureAssembly,
|
|
52886
|
+
snapLatchCoverAssembly,
|
|
52887
|
+
pinnedLeverAssembly,
|
|
52888
|
+
retainedShaftAssembly,
|
|
52889
|
+
capturedLinearSlide,
|
|
52890
|
+
capturedCartridgeGuideAssembly,
|
|
52891
|
+
livingHingeCoverAssembly,
|
|
52892
|
+
knuckledHingeAssembly,
|
|
52893
|
+
clevisPinJointAssembly,
|
|
52894
|
+
seatedBearingAssembly,
|
|
52895
|
+
cableGlandAnchorAssembly,
|
|
52896
|
+
hoseBarbPortAssembly,
|
|
52897
|
+
routedTubeClipAssembly,
|
|
52898
|
+
pcbTerminalBlockAssembly,
|
|
52899
|
+
thumbScrewClampAssembly,
|
|
51023
52900
|
pipeRoute,
|
|
51024
52901
|
elbow,
|
|
51025
52902
|
beltDrive,
|
|
@@ -51360,11 +53237,11 @@ function inverseLerp(x2, y2, value) {
|
|
|
51360
53237
|
return 0;
|
|
51361
53238
|
}
|
|
51362
53239
|
}
|
|
51363
|
-
function lerp$
|
|
53240
|
+
function lerp$5(x2, y2, t) {
|
|
51364
53241
|
return (1 - t) * x2 + t * y2;
|
|
51365
53242
|
}
|
|
51366
53243
|
function damp(x2, y2, lambda, dt) {
|
|
51367
|
-
return lerp$
|
|
53244
|
+
return lerp$5(x2, y2, 1 - Math.exp(-lambda * dt));
|
|
51368
53245
|
}
|
|
51369
53246
|
function pingpong(x2, length4 = 1) {
|
|
51370
53247
|
return length4 - Math.abs(euclideanModulo(x2, length4 * 2) - length4);
|
|
@@ -51557,7 +53434,7 @@ const MathUtils = {
|
|
|
51557
53434
|
* @param {number} t - The interpolation factor in the closed interval `[0, 1]`.
|
|
51558
53435
|
* @return {number} The interpolated value.
|
|
51559
53436
|
*/
|
|
51560
|
-
lerp: lerp$
|
|
53437
|
+
lerp: lerp$5,
|
|
51561
53438
|
/**
|
|
51562
53439
|
* Smoothly interpolate a number from `x` to `y` in a spring-like manner using a delta
|
|
51563
53440
|
* time to maintain frame rate independent movement. For details, see
|
|
@@ -60511,9 +62388,9 @@ class Color {
|
|
|
60511
62388
|
lerpHSL(color, alpha) {
|
|
60512
62389
|
this.getHSL(_hslA);
|
|
60513
62390
|
color.getHSL(_hslB);
|
|
60514
|
-
const h = lerp$
|
|
60515
|
-
const s = lerp$
|
|
60516
|
-
const l = lerp$
|
|
62391
|
+
const h = lerp$5(_hslA.h, _hslB.h, alpha);
|
|
62392
|
+
const s = lerp$5(_hslA.s, _hslB.s, alpha);
|
|
62393
|
+
const l = lerp$5(_hslA.l, _hslB.l, alpha);
|
|
60517
62394
|
this.setHSL(h, s, l);
|
|
60518
62395
|
return this;
|
|
60519
62396
|
}
|
|
@@ -94689,7 +96566,7 @@ function scale$1(v, s) {
|
|
|
94689
96566
|
function dot$2(a2, b) {
|
|
94690
96567
|
return a2[0] * b[0] + a2[1] * b[1] + a2[2] * b[2];
|
|
94691
96568
|
}
|
|
94692
|
-
function lerp$
|
|
96569
|
+
function lerp$4(a2, b, t) {
|
|
94693
96570
|
return a2 + (b - a2) * t;
|
|
94694
96571
|
}
|
|
94695
96572
|
function frameMatrix$1(x2, y2, z2, p2) {
|
|
@@ -94765,9 +96642,9 @@ function interpolateQuery(a2, b, t) {
|
|
|
94765
96642
|
}
|
|
94766
96643
|
return {
|
|
94767
96644
|
side: sideA,
|
|
94768
|
-
u: lerp$
|
|
94769
|
-
v: lerp$
|
|
94770
|
-
offset: lerp$
|
|
96645
|
+
u: lerp$4(a2.u ?? 0.5, b.u ?? 0.5, t),
|
|
96646
|
+
v: lerp$4(a2.v ?? 0.5, b.v ?? 0.5, t),
|
|
96647
|
+
offset: lerp$4(a2.offset ?? 0, b.offset ?? 0, t)
|
|
94771
96648
|
};
|
|
94772
96649
|
}
|
|
94773
96650
|
function resolvePathQueries(points) {
|
|
@@ -94892,7 +96769,7 @@ class ProductSkin {
|
|
|
94892
96769
|
}
|
|
94893
96770
|
/** Interpolate center, width, and depth at a normalized v or absolute axis value. */
|
|
94894
96771
|
stationAt(vOrAxis) {
|
|
94895
|
-
const axisValue = vOrAxis >= 0 && vOrAxis <= 1 ? lerp$
|
|
96772
|
+
const axisValue = vOrAxis >= 0 && vOrAxis <= 1 ? lerp$4(this.axisMin, this.axisMax, vOrAxis) : clamp$5(vOrAxis, this.axisMin, this.axisMax);
|
|
94896
96773
|
const sorted = this.stations;
|
|
94897
96774
|
for (let index2 = 0; index2 < sorted.length - 1; index2 += 1) {
|
|
94898
96775
|
const a2 = sorted[index2];
|
|
@@ -94904,12 +96781,12 @@ class ProductSkin {
|
|
|
94904
96781
|
const t = clamp$5((axisValue - aAxis) / span, 0, 1);
|
|
94905
96782
|
return {
|
|
94906
96783
|
axisValue,
|
|
94907
|
-
center: [lerp$
|
|
94908
|
-
width: lerp$
|
|
94909
|
-
depth: lerp$
|
|
96784
|
+
center: [lerp$4(a2.center[0], b.center[0], t), lerp$4(a2.center[1], b.center[1], t), lerp$4(a2.center[2], b.center[2], t)],
|
|
96785
|
+
width: lerp$4(a2.profile.width, b.profile.width, t),
|
|
96786
|
+
depth: lerp$4(a2.profile.depth, b.profile.depth, t),
|
|
94910
96787
|
dWidth: (b.profile.width - a2.profile.width) / span,
|
|
94911
96788
|
dDepth: (b.profile.depth - a2.profile.depth) / span,
|
|
94912
|
-
exponent: lerp$
|
|
96789
|
+
exponent: lerp$4(profileExponent(a2), profileExponent(b), t),
|
|
94913
96790
|
kind: a2.profile.kind === b.profile.kind ? a2.profile.kind : "custom"
|
|
94914
96791
|
};
|
|
94915
96792
|
}
|
|
@@ -95742,7 +97619,7 @@ function requirePositive$3(value, label) {
|
|
|
95742
97619
|
function clamp$4(value, min2, max2) {
|
|
95743
97620
|
return Math.max(min2, Math.min(max2, value));
|
|
95744
97621
|
}
|
|
95745
|
-
function lerp$
|
|
97622
|
+
function lerp$3(a2, b, t) {
|
|
95746
97623
|
return a2 + (b - a2) * t;
|
|
95747
97624
|
}
|
|
95748
97625
|
function add(a2, b) {
|
|
@@ -95792,19 +97669,19 @@ function transformLocal(point2, tangentAcross, normal, tangentAlong, x2, y2, z2
|
|
|
95792
97669
|
function interpolateCylinder(a2, b, t, mode) {
|
|
95793
97670
|
let delta = b.angle - a2.angle;
|
|
95794
97671
|
if (mode === "shortest" && Math.abs(delta) > 180) delta -= Math.sign(delta) * 360;
|
|
95795
|
-
return { kind: "cylinder", angle: a2.angle + delta * t, z: lerp$
|
|
97672
|
+
return { kind: "cylinder", angle: a2.angle + delta * t, z: lerp$3(a2.z, b.z, t), offset: lerp$3(a2.offset ?? 0, b.offset ?? 0, t) };
|
|
95796
97673
|
}
|
|
95797
97674
|
function interpolatePlane(a2, b, t) {
|
|
95798
|
-
return { kind: "plane", x: lerp$
|
|
97675
|
+
return { kind: "plane", x: lerp$3(a2.x, b.x, t), y: lerp$3(a2.y, b.y, t), offset: lerp$3(a2.offset ?? 0, b.offset ?? 0, t) };
|
|
95799
97676
|
}
|
|
95800
97677
|
function interpolateProductSkin(a2, b, t) {
|
|
95801
97678
|
if ((a2.side ?? b.side) !== (b.side ?? a2.side)) throw new Error("SurfacePath on ProductSkin currently supports one side per path; split side transitions into separate members.");
|
|
95802
97679
|
return {
|
|
95803
97680
|
kind: "productSkin",
|
|
95804
97681
|
side: a2.side ?? b.side,
|
|
95805
|
-
u: lerp$
|
|
95806
|
-
v: lerp$
|
|
95807
|
-
offset: lerp$
|
|
97682
|
+
u: lerp$3(a2.u ?? 0.5, b.u ?? 0.5, t),
|
|
97683
|
+
v: lerp$3(a2.v ?? 0.5, b.v ?? 0.5, t),
|
|
97684
|
+
offset: lerp$3(a2.offset ?? 0, b.offset ?? 0, t)
|
|
95808
97685
|
};
|
|
95809
97686
|
}
|
|
95810
97687
|
class SurfacePath {
|
|
@@ -96902,7 +98779,7 @@ function counterboresForPlate(spec2, width, height, thickness, diagnostics) {
|
|
|
96902
98779
|
function minWidthAcrossAlongRange(widthAtT, length4, minAlong, maxAlong) {
|
|
96903
98780
|
let minWidth = Number.POSITIVE_INFINITY;
|
|
96904
98781
|
for (let index2 = 0; index2 <= 8; index2 += 1) {
|
|
96905
|
-
const along = lerp$
|
|
98782
|
+
const along = lerp$3(minAlong, maxAlong, index2 / 8);
|
|
96906
98783
|
const t = Math.max(0, Math.min(1, (along + length4 / 2) / Math.max(length4, 1e-8)));
|
|
96907
98784
|
minWidth = Math.min(minWidth, widthAtT(t));
|
|
96908
98785
|
}
|
|
@@ -97202,7 +99079,7 @@ function pathParameterAtDistance(samples, distance2) {
|
|
|
97202
99079
|
const segmentLength = Math.hypot(b.point[0] - a2.point[0], b.point[1] - a2.point[1], b.point[2] - a2.point[2]);
|
|
97203
99080
|
if (traveled + segmentLength >= distance2) {
|
|
97204
99081
|
const localT = segmentLength <= 1e-8 ? 0 : (distance2 - traveled) / segmentLength;
|
|
97205
|
-
return lerp$
|
|
99082
|
+
return lerp$3(a2.t, b.t, localT);
|
|
97206
99083
|
}
|
|
97207
99084
|
traveled += segmentLength;
|
|
97208
99085
|
}
|
|
@@ -97255,7 +99132,7 @@ function compileBandFootprintMesh(path2, input) {
|
|
|
97255
99132
|
const width = input.widthAt(t);
|
|
97256
99133
|
const along = distance2 - length4 / 2;
|
|
97257
99134
|
for (let acrossIndex = 0; acrossIndex <= acrossSegments; acrossIndex += 1) {
|
|
97258
|
-
const across = lerp$
|
|
99135
|
+
const across = lerp$3(-width / 2, width / 2, acrossIndex / acrossSegments);
|
|
97259
99136
|
mesh.vertices.push(pointAtProfile([across, along], false));
|
|
97260
99137
|
}
|
|
97261
99138
|
}
|
|
@@ -97265,7 +99142,7 @@ function compileBandFootprintMesh(path2, input) {
|
|
|
97265
99142
|
const width = input.widthAt(t);
|
|
97266
99143
|
const along = distance2 - length4 / 2;
|
|
97267
99144
|
for (let acrossIndex = 0; acrossIndex <= acrossSegments; acrossIndex += 1) {
|
|
97268
|
-
const across = lerp$
|
|
99145
|
+
const across = lerp$3(-width / 2, width / 2, acrossIndex / acrossSegments);
|
|
97269
99146
|
mesh.vertices.push(pointAtProfile([across, along], true));
|
|
97270
99147
|
}
|
|
97271
99148
|
}
|
|
@@ -97277,7 +99154,7 @@ function compileBandFootprintMesh(path2, input) {
|
|
|
97277
99154
|
const width = input.widthAt(t);
|
|
97278
99155
|
const along = distance2 - length4 / 2;
|
|
97279
99156
|
for (let acrossIndex = 0; acrossIndex < acrossSegments; acrossIndex += 1) {
|
|
97280
|
-
const across = lerp$
|
|
99157
|
+
const across = lerp$3(-width / 2, width / 2, (acrossIndex + 0.5) / acrossSegments);
|
|
97281
99158
|
filled[alongIndex][acrossIndex] = !holes.some((hole2) => pointInProfileLoop([across, along], hole2));
|
|
97282
99159
|
}
|
|
97283
99160
|
}
|
|
@@ -99477,7 +101354,7 @@ function midpoint$1(a2, b) {
|
|
|
99477
101354
|
requireVec3$1(b, "b");
|
|
99478
101355
|
return [(a2[0] + b[0]) / 2, (a2[1] + b[1]) / 2, (a2[2] + b[2]) / 2];
|
|
99479
101356
|
}
|
|
99480
|
-
function lerp$
|
|
101357
|
+
function lerp$2(a2, b, t) {
|
|
99481
101358
|
requireVec3$1(a2, "a");
|
|
99482
101359
|
requireVec3$1(b, "b");
|
|
99483
101360
|
requireFiniteNumber(t, "t");
|
|
@@ -99507,7 +101384,7 @@ const Points2 = {
|
|
|
99507
101384
|
/** Center point between two 3D points. */
|
|
99508
101385
|
midpoint: midpoint$1,
|
|
99509
101386
|
/** Linearly interpolate between two 3D points. t=0 returns a, t=1 returns b. */
|
|
99510
|
-
lerp: lerp$
|
|
101387
|
+
lerp: lerp$2,
|
|
99511
101388
|
/** Unit direction vector from a to b. Throws if a and b are the same point. */
|
|
99512
101389
|
direction,
|
|
99513
101390
|
/** Move a point along a direction vector by a given amount. */
|
|
@@ -107406,7 +109283,7 @@ function chamferTrackedEdge(shape, edge, size, quadrant = [-1, -1]) {
|
|
|
107406
109283
|
);
|
|
107407
109284
|
return buildEdgeFeatureResult(target, plan, "chamfer");
|
|
107408
109285
|
}
|
|
107409
|
-
const EPSILON
|
|
109286
|
+
const EPSILON = 1e-8;
|
|
107410
109287
|
function toTuple(point2) {
|
|
107411
109288
|
return Array.isArray(point2) ? [point2[0], point2[1]] : [point2.x, point2.y];
|
|
107412
109289
|
}
|
|
@@ -107417,7 +109294,7 @@ function distance(a2, b) {
|
|
|
107417
109294
|
}
|
|
107418
109295
|
function normalize(vx, vy) {
|
|
107419
109296
|
const len2 = Math.hypot(vx, vy);
|
|
107420
|
-
if (len2 <= EPSILON
|
|
109297
|
+
if (len2 <= EPSILON) throw new Error("filletCorners requires non-degenerate edges");
|
|
107421
109298
|
return [vx / len2, vy / len2];
|
|
107422
109299
|
}
|
|
107423
109300
|
function clamp$3(value, min2, max2) {
|
|
@@ -107454,19 +109331,19 @@ function buildCornerGeometry(points, spec2, winding) {
|
|
|
107454
109331
|
const [inDirX, inDirY] = normalize(current[0] - prev[0], current[1] - prev[1]);
|
|
107455
109332
|
const [outDirX, outDirY] = normalize(next[0] - current[0], next[1] - current[1]);
|
|
107456
109333
|
const turn = inDirX * outDirY - inDirY * outDirX;
|
|
107457
|
-
const isConvex = turn * winding > EPSILON
|
|
107458
|
-
const isConcave = turn * winding < -EPSILON
|
|
109334
|
+
const isConvex = turn * winding > EPSILON;
|
|
109335
|
+
const isConcave = turn * winding < -EPSILON;
|
|
107459
109336
|
if (!isConvex && !isConcave) {
|
|
107460
109337
|
throw new Error(`filletCorners corner ${spec2.index} is collinear; cannot fillet a straight edge`);
|
|
107461
109338
|
}
|
|
107462
109339
|
const toPrev = [-inDirX, -inDirY];
|
|
107463
109340
|
const toNext = [outDirX, outDirY];
|
|
107464
109341
|
const interiorAngle = Math.acos(clamp$3(toPrev[0] * toNext[0] + toPrev[1] * toNext[1], -1, 1));
|
|
107465
|
-
if (interiorAngle <= EPSILON
|
|
109342
|
+
if (interiorAngle <= EPSILON || interiorAngle >= Math.PI - EPSILON) {
|
|
107466
109343
|
throw new Error(`filletCorners corner ${spec2.index} has an unsupported angle`);
|
|
107467
109344
|
}
|
|
107468
109345
|
const tangentDistance = spec2.radius / Math.tan(interiorAngle / 2);
|
|
107469
|
-
if (tangentDistance >= inLength - EPSILON
|
|
109346
|
+
if (tangentDistance >= inLength - EPSILON || tangentDistance >= outLength - EPSILON) {
|
|
107470
109347
|
const maxRadius = Math.min(inLength, outLength) * Math.tan(interiorAngle / 2);
|
|
107471
109348
|
throw new Error(`filletCorners radius ${spec2.radius} is too large for corner ${spec2.index}; max is ${maxRadius.toFixed(3)}`);
|
|
107472
109349
|
}
|
|
@@ -107496,7 +109373,7 @@ function filletCorners(points, corners) {
|
|
|
107496
109373
|
if (corners.length === 0) return polygon(points);
|
|
107497
109374
|
const tuples = points.map(toTuple);
|
|
107498
109375
|
const area2 = signedArea$3(tuples);
|
|
107499
|
-
if (Math.abs(area2) <= EPSILON
|
|
109376
|
+
if (Math.abs(area2) <= EPSILON) throw new Error("filletCorners requires a non-degenerate polygon");
|
|
107500
109377
|
const winding = Math.sign(area2);
|
|
107501
109378
|
const geometryByIndex = /* @__PURE__ */ new Map();
|
|
107502
109379
|
for (const spec2 of corners) {
|
|
@@ -107510,7 +109387,7 @@ function filletCorners(points, corners) {
|
|
|
107510
109387
|
const edgeLength2 = distance(tuples[i], tuples[nextIndex]);
|
|
107511
109388
|
const exitDistance = ((_a3 = geometryByIndex.get(i)) == null ? void 0 : _a3.tangentDistance) ?? 0;
|
|
107512
109389
|
const entryDistance = ((_b3 = geometryByIndex.get(nextIndex)) == null ? void 0 : _b3.tangentDistance) ?? 0;
|
|
107513
|
-
if (exitDistance + entryDistance >= edgeLength2 - EPSILON
|
|
109390
|
+
if (exitDistance + entryDistance >= edgeLength2 - EPSILON) {
|
|
107514
109391
|
throw new Error(`filletCorners adjacent fillets overlap on edge ${i} -> ${nextIndex}; reduce one of the radii`);
|
|
107515
109392
|
}
|
|
107516
109393
|
}
|
|
@@ -120043,7 +121920,7 @@ function railCrossAt(rail2, position) {
|
|
|
120043
121920
|
const b = points[index2 + 1];
|
|
120044
121921
|
if (position >= a2.position - LOFT_GUIDE_EPS && position <= b.position + LOFT_GUIDE_EPS) {
|
|
120045
121922
|
const t = (position - a2.position) / (b.position - a2.position);
|
|
120046
|
-
return [lerp$
|
|
121923
|
+
return [lerp$1(a2.cross[0], b.cross[0], t), lerp$1(a2.cross[1], b.cross[1], t)];
|
|
120047
121924
|
}
|
|
120048
121925
|
}
|
|
120049
121926
|
throw new Error("Loft guide rail does not cover requested station position");
|
|
@@ -120081,7 +121958,7 @@ function crossPointForAxis(axis, point2) {
|
|
|
120081
121958
|
if (axis === "Y") return [point2[0], -point2[2]];
|
|
120082
121959
|
return [point2[0], point2[1]];
|
|
120083
121960
|
}
|
|
120084
|
-
function lerp$
|
|
121961
|
+
function lerp$1(a2, b, t) {
|
|
120085
121962
|
return a2 + (b - a2) * t;
|
|
120086
121963
|
}
|
|
120087
121964
|
function loftWithGuideRails(stations, rails, options = {}) {
|
|
@@ -120205,13 +122082,13 @@ function sketchBounds(profile) {
|
|
|
120205
122082
|
}
|
|
120206
122083
|
function lerpBounds(a2, b, t) {
|
|
120207
122084
|
return {
|
|
120208
|
-
minX: lerp
|
|
120209
|
-
maxX: lerp
|
|
120210
|
-
minY: lerp
|
|
120211
|
-
maxY: lerp
|
|
122085
|
+
minX: lerp(a2.minX, b.minX, t),
|
|
122086
|
+
maxX: lerp(a2.maxX, b.maxX, t),
|
|
122087
|
+
minY: lerp(a2.minY, b.minY, t),
|
|
122088
|
+
maxY: lerp(a2.maxY, b.maxY, t)
|
|
120212
122089
|
};
|
|
120213
122090
|
}
|
|
120214
|
-
function lerp
|
|
122091
|
+
function lerp(a2, b, t) {
|
|
120215
122092
|
return a2 + (b - a2) * t;
|
|
120216
122093
|
}
|
|
120217
122094
|
function mapLoftPath2D(path2, label, mapper) {
|
|
@@ -120975,7 +122852,7 @@ function resolveCornerYSelection(edges, options = {}) {
|
|
|
120975
122852
|
for (const candidate of candidates) {
|
|
120976
122853
|
const selectedEndpoints = [];
|
|
120977
122854
|
let totalGap = 0;
|
|
120978
|
-
let
|
|
122855
|
+
let maxGap = 0;
|
|
120979
122856
|
for (const edge of edges) {
|
|
120980
122857
|
const start = edgeEndpoint(edge, "start");
|
|
120981
122858
|
const end = edgeEndpoint(edge, "end");
|
|
@@ -120985,10 +122862,10 @@ function resolveCornerYSelection(edges, options = {}) {
|
|
|
120985
122862
|
const gap = Math.min(startGap, endGap);
|
|
120986
122863
|
selectedEndpoints.push(selected);
|
|
120987
122864
|
totalGap += gap;
|
|
120988
|
-
|
|
122865
|
+
maxGap = Math.max(maxGap, gap);
|
|
120989
122866
|
}
|
|
120990
|
-
if (
|
|
120991
|
-
best = { totalGap, maxGap
|
|
122867
|
+
if (maxGap <= cornerTolerance && (!best || totalGap < best.totalGap)) {
|
|
122868
|
+
best = { totalGap, maxGap, selectedEndpoints };
|
|
120992
122869
|
}
|
|
120993
122870
|
}
|
|
120994
122871
|
if (!best) {
|
|
@@ -125424,10 +127301,14 @@ function spec(name, checkFn) {
|
|
|
125424
127301
|
};
|
|
125425
127302
|
}
|
|
125426
127303
|
let _collected = [];
|
|
127304
|
+
let _collisionAllowances = [];
|
|
127305
|
+
let _physicalComponentExpectations = [];
|
|
125427
127306
|
let _counter = 0;
|
|
125428
127307
|
let _activeGroup = null;
|
|
125429
127308
|
function resetVerifications() {
|
|
125430
127309
|
_collected = [];
|
|
127310
|
+
_collisionAllowances = [];
|
|
127311
|
+
_physicalComponentExpectations = [];
|
|
125431
127312
|
_counter = 0;
|
|
125432
127313
|
}
|
|
125433
127314
|
function getCollectedVerifications() {
|
|
@@ -125461,15 +127342,35 @@ function push(result) {
|
|
|
125461
127342
|
function roundNum(n, digits = 4) {
|
|
125462
127343
|
return Number.isFinite(n) ? n.toFixed(digits).replace(/\.?0+$/, "") : String(n);
|
|
125463
127344
|
}
|
|
127345
|
+
function meshDerivedManifoldBackend(shape) {
|
|
127346
|
+
const mesh = getShapeRuntimeBackend(shape).getMesh();
|
|
127347
|
+
return reconstructBackendFromMesh({
|
|
127348
|
+
numProp: mesh.numProp,
|
|
127349
|
+
triVerts: mesh.triVerts,
|
|
127350
|
+
vertProperties: mesh.vertProperties,
|
|
127351
|
+
mergeFromVert: mesh.mergeFromVert ?? new Uint32Array(),
|
|
127352
|
+
mergeToVert: mesh.mergeToVert ?? new Uint32Array()
|
|
127353
|
+
});
|
|
127354
|
+
}
|
|
127355
|
+
function backendForMinGap(shape) {
|
|
127356
|
+
const backend = getShapeRuntimeBackend(shape);
|
|
127357
|
+
if (isManifoldCapableBackend(backend)) return { backend, method: "exact", dispose: false };
|
|
127358
|
+
return { backend: meshDerivedManifoldBackend(shape), method: "mesh-derived", dispose: true };
|
|
127359
|
+
}
|
|
125464
127360
|
function computeMinGap(a2, b, searchLength) {
|
|
125465
|
-
const backendA =
|
|
125466
|
-
const backendB =
|
|
125467
|
-
|
|
125468
|
-
|
|
127361
|
+
const backendA = backendForMinGap(a2);
|
|
127362
|
+
const backendB = backendForMinGap(b);
|
|
127363
|
+
try {
|
|
127364
|
+
const manifoldA = requireManifoldShapeBackend(backendA.backend, "verification.minGap");
|
|
127365
|
+
const manifoldB = requireManifoldShapeBackend(backendB.backend, "verification.minGap");
|
|
127366
|
+
return {
|
|
127367
|
+
gap: manifoldA.minGap(manifoldB, searchLength),
|
|
127368
|
+
method: backendA.method === "exact" && backendB.method === "exact" ? "exact" : "mesh-derived"
|
|
127369
|
+
};
|
|
127370
|
+
} finally {
|
|
127371
|
+
if (backendA.dispose) disposeShapeBackend(backendA.backend);
|
|
127372
|
+
if (backendB.dispose) disposeShapeBackend(backendB.backend);
|
|
125469
127373
|
}
|
|
125470
|
-
const manifoldA = backendA.requireManifold("verification.minGap");
|
|
125471
|
-
const manifoldB = requireManifoldShapeBackend(backendB, "verification.minGap");
|
|
125472
|
-
return manifoldA.minGap(manifoldB, searchLength);
|
|
125473
127374
|
}
|
|
125474
127375
|
function vec3Dot(a2, b) {
|
|
125475
127376
|
return a2[0] * b[0] + a2[1] * b[1] + a2[2] * b[2];
|
|
@@ -125604,9 +127505,144 @@ const verify = {
|
|
|
125604
127505
|
actual: `${roundNum(d2, 3)} mm`
|
|
125605
127506
|
});
|
|
125606
127507
|
} catch (e) {
|
|
125607
|
-
push({
|
|
127508
|
+
push({
|
|
127509
|
+
id: nextId(),
|
|
127510
|
+
label,
|
|
127511
|
+
kind: "interface",
|
|
127512
|
+
status: "fail",
|
|
127513
|
+
message: `Error: ${e instanceof Error ? e.message : String(e)}`,
|
|
127514
|
+
line: line2
|
|
127515
|
+
});
|
|
125608
127516
|
}
|
|
125609
127517
|
},
|
|
127518
|
+
/**
|
|
127519
|
+
* Check the distance between two named connectors on a shape or group.
|
|
127520
|
+
*
|
|
127521
|
+
* Use this when connectors + `matchTo()` define a static assembly interface.
|
|
127522
|
+
* It proves the mate at runtime, unlike a plain source-level connector
|
|
127523
|
+
* declaration. The common case is `expected = 0`, meaning the two connector
|
|
127524
|
+
* origins should coincide after placement.
|
|
127525
|
+
*
|
|
127526
|
+
* **Example**
|
|
127527
|
+
*
|
|
127528
|
+
* ```ts
|
|
127529
|
+
* verify.connectorDistance("leg is seated", bench, "Rail.leg_0", "Leg0.head", 0, 0.01);
|
|
127530
|
+
* ```
|
|
127531
|
+
*/
|
|
127532
|
+
connectorDistance(label, target, connectorA, connectorB, expected = 0, tolerance = 0.01) {
|
|
127533
|
+
const line2 = captureSourceLine();
|
|
127534
|
+
try {
|
|
127535
|
+
const actual = target.connectorDistance(connectorA, connectorB);
|
|
127536
|
+
const diff = Math.abs(actual - expected);
|
|
127537
|
+
const passed = diff <= Math.abs(tolerance);
|
|
127538
|
+
push({
|
|
127539
|
+
id: nextId(),
|
|
127540
|
+
label,
|
|
127541
|
+
kind: "interface",
|
|
127542
|
+
status: passed ? "pass" : "fail",
|
|
127543
|
+
message: passed ? `Connector distance ${roundNum(actual, 4)} mm ≈ ${roundNum(expected, 4)} mm` : `Connector distance ${roundNum(actual, 4)} mm is outside ${roundNum(expected, 4)} ± ${roundNum(tolerance, 4)} mm`,
|
|
127544
|
+
line: passed ? void 0 : line2,
|
|
127545
|
+
expected: `${roundNum(expected, 4)} ± ${roundNum(tolerance, 4)} mm`,
|
|
127546
|
+
actual: `${roundNum(actual, 4)} mm`
|
|
127547
|
+
});
|
|
127548
|
+
} catch (e) {
|
|
127549
|
+
push({
|
|
127550
|
+
id: nextId(),
|
|
127551
|
+
label,
|
|
127552
|
+
kind: "interface",
|
|
127553
|
+
status: "fail",
|
|
127554
|
+
message: `Error: ${e instanceof Error ? e.message : String(e)}`,
|
|
127555
|
+
line: line2
|
|
127556
|
+
});
|
|
127557
|
+
}
|
|
127558
|
+
},
|
|
127559
|
+
/**
|
|
127560
|
+
* Declare the expected physical connectivity component count for the returned visible model.
|
|
127561
|
+
*
|
|
127562
|
+
* **Details**
|
|
127563
|
+
*
|
|
127564
|
+
* Use this for generated mechanical models that should have a clear component graph:
|
|
127565
|
+
* one connected fixture, a purchased part plus a removable cartridge, a root assembly plus
|
|
127566
|
+
* named intentional ghosts, and so on. `forgecad inspect mechanical-integrity` resolves the returned
|
|
127567
|
+
* visible objects with the same physical-connectivity analysis used in the quality gate and
|
|
127568
|
+
* fails if the actual component count differs.
|
|
127569
|
+
*
|
|
127570
|
+
* This catches the common generated-CAD failure where a script returns a visually plausible
|
|
127571
|
+
* artifact but the handle, screw, washer, cover, or terminal block is actually a separate island.
|
|
127572
|
+
*
|
|
127573
|
+
* **Example**
|
|
127574
|
+
*
|
|
127575
|
+
* ```ts
|
|
127576
|
+
* verify.physicalComponentCount("vise is one connected installed assembly", 1);
|
|
127577
|
+
* ```
|
|
127578
|
+
*/
|
|
127579
|
+
physicalComponentCount(label, expected) {
|
|
127580
|
+
const line2 = captureSourceLine();
|
|
127581
|
+
const id = nextId();
|
|
127582
|
+
if (!Number.isInteger(expected) || expected < 0) {
|
|
127583
|
+
push({
|
|
127584
|
+
id,
|
|
127585
|
+
label,
|
|
127586
|
+
kind: "interface",
|
|
127587
|
+
status: "fail",
|
|
127588
|
+
message: "Expected physical component count must be a non-negative integer",
|
|
127589
|
+
line: line2
|
|
127590
|
+
});
|
|
127591
|
+
return;
|
|
127592
|
+
}
|
|
127593
|
+
_physicalComponentExpectations.push({ id, label, expected, line: line2 });
|
|
127594
|
+
push({
|
|
127595
|
+
id,
|
|
127596
|
+
label,
|
|
127597
|
+
kind: "interface",
|
|
127598
|
+
status: "pass",
|
|
127599
|
+
message: `Expected ${expected} physical component(s); checked by mechanical-integrity connectivity`
|
|
127600
|
+
});
|
|
127601
|
+
},
|
|
127602
|
+
/**
|
|
127603
|
+
* Declare that two visible objects intentionally overlap because the overlap is real manufacturing intent.
|
|
127604
|
+
*
|
|
127605
|
+
* **Details**
|
|
127606
|
+
*
|
|
127607
|
+
* Use this only for overlaps that a mechanical reviewer would accept as actual matter sharing volume:
|
|
127608
|
+
* welded/fused regions, overmolded inserts, potted electronics, cast-in hardware, or deliberately
|
|
127609
|
+
* bonded laminations. This is not a shortcut for screws without holes, shafts without bores, covers
|
|
127610
|
+
* without pockets, or parts placed with collision as a positioning hack.
|
|
127611
|
+
*
|
|
127612
|
+
* `forgecad inspect mechanical-integrity --collisions` only honors this declaration when both shapes are
|
|
127613
|
+
* returned as visible objects and the exact collision report finds that same object pair. Unused or
|
|
127614
|
+
* non-visible declarations fail the quality gate so annotations cannot hide unrelated collisions.
|
|
127615
|
+
*
|
|
127616
|
+
* **Example**
|
|
127617
|
+
*
|
|
127618
|
+
* ```ts
|
|
127619
|
+
* verify.intentionalOverlap("rubber grip is overmolded on handle", rubberGrip, handleCore, "overmolded insert");
|
|
127620
|
+
* ```
|
|
127621
|
+
*/
|
|
127622
|
+
intentionalOverlap(label, a2, b, reason) {
|
|
127623
|
+
const line2 = captureSourceLine();
|
|
127624
|
+
const id = nextId();
|
|
127625
|
+
const trimmedReason = String(reason ?? "").trim();
|
|
127626
|
+
if (trimmedReason.length === 0) {
|
|
127627
|
+
push({
|
|
127628
|
+
id,
|
|
127629
|
+
label,
|
|
127630
|
+
kind: "interface",
|
|
127631
|
+
status: "fail",
|
|
127632
|
+
message: "Intentional overlap requires a manufacturing reason",
|
|
127633
|
+
line: line2
|
|
127634
|
+
});
|
|
127635
|
+
return;
|
|
127636
|
+
}
|
|
127637
|
+
_collisionAllowances.push({ id, label, reason: trimmedReason, a: a2, b, line: line2 });
|
|
127638
|
+
push({
|
|
127639
|
+
id,
|
|
127640
|
+
label,
|
|
127641
|
+
kind: "interface",
|
|
127642
|
+
status: "pass",
|
|
127643
|
+
message: `Intentional overlap declared: ${trimmedReason}`
|
|
127644
|
+
});
|
|
127645
|
+
},
|
|
125610
127646
|
/**
|
|
125611
127647
|
* Check that two shapes do not collide (minGap > 0).
|
|
125612
127648
|
*
|
|
@@ -125615,19 +127651,28 @@ const verify = {
|
|
|
125615
127651
|
notColliding(label, a2, b, searchLength = 1) {
|
|
125616
127652
|
const line2 = captureSourceLine();
|
|
125617
127653
|
try {
|
|
125618
|
-
const gap = computeMinGap(a2, b, searchLength);
|
|
127654
|
+
const { gap, method } = computeMinGap(a2, b, searchLength);
|
|
127655
|
+
const methodLabel = method === "exact" ? "exact min gap" : "mesh-derived min gap";
|
|
125619
127656
|
const passed = gap > 0;
|
|
125620
127657
|
push({
|
|
125621
127658
|
id: nextId(),
|
|
125622
127659
|
label,
|
|
127660
|
+
kind: "interface",
|
|
125623
127661
|
status: passed ? "pass" : "fail",
|
|
125624
|
-
message: passed ? `No collision (
|
|
127662
|
+
message: passed ? `No collision (${methodLabel} ${roundNum(gap, 3)} mm)` : `Shapes are colliding (${methodLabel} ${roundNum(gap, 3)} mm ≤ 0)`,
|
|
125625
127663
|
line: passed ? void 0 : line2,
|
|
125626
127664
|
expected: "> 0 mm",
|
|
125627
127665
|
actual: `${roundNum(gap, 3)} mm`
|
|
125628
127666
|
});
|
|
125629
127667
|
} catch (e) {
|
|
125630
|
-
push({
|
|
127668
|
+
push({
|
|
127669
|
+
id: nextId(),
|
|
127670
|
+
label,
|
|
127671
|
+
kind: "interface",
|
|
127672
|
+
status: "fail",
|
|
127673
|
+
message: `Error: ${e instanceof Error ? e.message : String(e)}`,
|
|
127674
|
+
line: line2
|
|
127675
|
+
});
|
|
125631
127676
|
}
|
|
125632
127677
|
},
|
|
125633
127678
|
/**
|
|
@@ -125636,13 +127681,15 @@ const verify = {
|
|
|
125636
127681
|
minClearance(label, a2, b, minGap, searchLength = 10) {
|
|
125637
127682
|
const line2 = captureSourceLine();
|
|
125638
127683
|
try {
|
|
125639
|
-
const gap = computeMinGap(a2, b, searchLength);
|
|
127684
|
+
const { gap, method } = computeMinGap(a2, b, searchLength);
|
|
127685
|
+
const methodLabel = method === "exact" ? "exact gap" : "mesh-derived gap";
|
|
125640
127686
|
const passed = gap >= minGap;
|
|
125641
127687
|
push({
|
|
125642
127688
|
id: nextId(),
|
|
125643
127689
|
label,
|
|
127690
|
+
kind: "interface",
|
|
125644
127691
|
status: passed ? "pass" : "fail",
|
|
125645
|
-
message: passed ?
|
|
127692
|
+
message: passed ? `${methodLabel} ${roundNum(gap, 3)} mm ≥ ${roundNum(minGap, 3)} mm` : `${methodLabel} ${roundNum(gap, 3)} mm < required ${roundNum(minGap, 3)} mm`,
|
|
125646
127693
|
line: passed ? void 0 : line2,
|
|
125647
127694
|
expected: `≥ ${roundNum(minGap, 3)} mm`,
|
|
125648
127695
|
actual: `${roundNum(gap, 3)} mm`
|
|
@@ -125651,6 +127698,90 @@ const verify = {
|
|
|
125651
127698
|
push({ id: nextId(), label, status: "fail", message: `Error: ${e instanceof Error ? e.message : String(e)}`, line: line2 });
|
|
125652
127699
|
}
|
|
125653
127700
|
},
|
|
127701
|
+
/**
|
|
127702
|
+
* Check that the clearance gap between two shapes is inside an allowed range.
|
|
127703
|
+
*
|
|
127704
|
+
* **Details**
|
|
127705
|
+
*
|
|
127706
|
+
* Use this for seated and retained interfaces where a part must be close
|
|
127707
|
+
* enough to be mechanically accountable, but must not collide beyond the
|
|
127708
|
+
* allowed minimum. It catches both failure modes that make generated CAD look
|
|
127709
|
+
* fake: parts floating away from their receiver, and parts intersecting their
|
|
127710
|
+
* receiver because the pocket, bore, or running clearance was not modeled.
|
|
127711
|
+
*
|
|
127712
|
+
* For contact, use a narrow range such as `[-0.01, 0.05]` to tolerate tiny
|
|
127713
|
+
* numerical noise. For a running fit, use the intended clearance band.
|
|
127714
|
+
*
|
|
127715
|
+
* Manifold-backed shapes use exact min-gap distance. Other backends use a
|
|
127716
|
+
* mesh-derived min-gap check and say so in the verification message; keep
|
|
127717
|
+
* `forgecad inspect mechanical-integrity --collisions` in the acceptance gate for
|
|
127718
|
+
* positive-volume interference.
|
|
127719
|
+
*
|
|
127720
|
+
* **Example**
|
|
127721
|
+
*
|
|
127722
|
+
* ```ts
|
|
127723
|
+
* verify.clearanceBetween("cover is seated on gasket", cover, gasket, -0.01, 0.05);
|
|
127724
|
+
* verify.clearanceBetween("carriage runs inside rail", carriage, rail, 0.2, 0.5);
|
|
127725
|
+
* ```
|
|
127726
|
+
*/
|
|
127727
|
+
clearanceBetween(label, a2, b, minGap, maxGap, searchLength) {
|
|
127728
|
+
const line2 = captureSourceLine();
|
|
127729
|
+
try {
|
|
127730
|
+
if (!Number.isFinite(minGap) || !Number.isFinite(maxGap)) {
|
|
127731
|
+
push({
|
|
127732
|
+
id: nextId(),
|
|
127733
|
+
label,
|
|
127734
|
+
kind: "interface",
|
|
127735
|
+
status: "fail",
|
|
127736
|
+
message: "Clearance range must use finite numbers",
|
|
127737
|
+
line: line2
|
|
127738
|
+
});
|
|
127739
|
+
return;
|
|
127740
|
+
}
|
|
127741
|
+
if (maxGap < minGap) {
|
|
127742
|
+
push({
|
|
127743
|
+
id: nextId(),
|
|
127744
|
+
label,
|
|
127745
|
+
kind: "interface",
|
|
127746
|
+
status: "fail",
|
|
127747
|
+
message: `Clearance max ${roundNum(maxGap, 3)} mm is smaller than min ${roundNum(minGap, 3)} mm`,
|
|
127748
|
+
line: line2
|
|
127749
|
+
});
|
|
127750
|
+
return;
|
|
127751
|
+
}
|
|
127752
|
+
const search = searchLength ?? Math.max(10, Math.abs(maxGap) * 2 + 1);
|
|
127753
|
+
const { gap, method } = computeMinGap(a2, b, search);
|
|
127754
|
+
const methodLabel = method === "exact" ? "exact gap" : "mesh-derived gap";
|
|
127755
|
+
const passed = gap >= minGap && gap <= maxGap;
|
|
127756
|
+
let message;
|
|
127757
|
+
if (passed) {
|
|
127758
|
+
message = `${methodLabel} ${roundNum(gap, 3)} mm in [${roundNum(minGap, 3)}, ${roundNum(maxGap, 3)}] mm`;
|
|
127759
|
+
} else if (gap < minGap) {
|
|
127760
|
+
message = `${methodLabel} ${roundNum(gap, 3)} mm < allowed minimum ${roundNum(minGap, 3)} mm`;
|
|
127761
|
+
} else {
|
|
127762
|
+
message = `${methodLabel} ${roundNum(gap, 3)} mm > allowed maximum ${roundNum(maxGap, 3)} mm`;
|
|
127763
|
+
}
|
|
127764
|
+
push({
|
|
127765
|
+
id: nextId(),
|
|
127766
|
+
label,
|
|
127767
|
+
kind: "interface",
|
|
127768
|
+
status: passed ? "pass" : "fail",
|
|
127769
|
+
message,
|
|
127770
|
+
line: passed ? void 0 : line2,
|
|
127771
|
+
expected: `[${roundNum(minGap, 3)}, ${roundNum(maxGap, 3)}] mm`,
|
|
127772
|
+
actual: `${roundNum(gap, 3)} mm`
|
|
127773
|
+
});
|
|
127774
|
+
} catch (e) {
|
|
127775
|
+
push({
|
|
127776
|
+
id: nextId(),
|
|
127777
|
+
label,
|
|
127778
|
+
kind: "interface",
|
|
127779
|
+
status: "fail",
|
|
127780
|
+
message: `Error: ${e instanceof Error ? e.message : String(e)}`,
|
|
127781
|
+
line: line2
|
|
127782
|
+
});
|
|
127783
|
+
}
|
|
127784
|
+
},
|
|
125654
127785
|
/**
|
|
125655
127786
|
* Check that two face normals are parallel (within toleranceDeg degrees).
|
|
125656
127787
|
*/
|
|
@@ -339418,10 +341549,149 @@ function runScript(code, fileName = "main.forge.js", allFiles = {}, options = {}
|
|
|
339418
341549
|
}
|
|
339419
341550
|
}
|
|
339420
341551
|
}
|
|
341552
|
+
const DEFAULT_LEAF_SIZE = 8;
|
|
341553
|
+
function cloneVec3$1(value) {
|
|
341554
|
+
return [value[0], value[1], value[2]];
|
|
341555
|
+
}
|
|
341556
|
+
function emptyBounds() {
|
|
341557
|
+
return {
|
|
341558
|
+
min: [Infinity, Infinity, Infinity],
|
|
341559
|
+
max: [-Infinity, -Infinity, -Infinity]
|
|
341560
|
+
};
|
|
341561
|
+
}
|
|
341562
|
+
function expandBounds(bounds, entry) {
|
|
341563
|
+
for (let axis = 0; axis < 3; axis += 1) {
|
|
341564
|
+
bounds.min[axis] = Math.min(bounds.min[axis], entry.min[axis]);
|
|
341565
|
+
bounds.max[axis] = Math.max(bounds.max[axis], entry.max[axis]);
|
|
341566
|
+
}
|
|
341567
|
+
}
|
|
341568
|
+
function boundsFor(entries, indexes) {
|
|
341569
|
+
const bounds = emptyBounds();
|
|
341570
|
+
for (const index2 of indexes) expandBounds(bounds, entries[index2]);
|
|
341571
|
+
return bounds;
|
|
341572
|
+
}
|
|
341573
|
+
function longestAxis(bounds) {
|
|
341574
|
+
const sizes = [bounds.max[0] - bounds.min[0], bounds.max[1] - bounds.min[1], bounds.max[2] - bounds.min[2]];
|
|
341575
|
+
if (sizes[1] > sizes[0] && sizes[1] >= sizes[2]) return 1;
|
|
341576
|
+
if (sizes[2] > sizes[0] && sizes[2] > sizes[1]) return 2;
|
|
341577
|
+
return 0;
|
|
341578
|
+
}
|
|
341579
|
+
function centerOnAxis(entry, axis) {
|
|
341580
|
+
return (entry.min[axis] + entry.max[axis]) * 0.5;
|
|
341581
|
+
}
|
|
341582
|
+
function nodeVolume(node) {
|
|
341583
|
+
return Math.max(0, node.max[0] - node.min[0]) * Math.max(0, node.max[1] - node.min[1]) * Math.max(0, node.max[2] - node.min[2]);
|
|
341584
|
+
}
|
|
341585
|
+
function aabbOverlaps(a2, b, padding = 0) {
|
|
341586
|
+
if (padding <= 0) {
|
|
341587
|
+
return [0, 1, 2].every((axis) => a2.min[axis] < b.max[axis] && a2.max[axis] > b.min[axis]);
|
|
341588
|
+
}
|
|
341589
|
+
return [0, 1, 2].every((axis) => a2.min[axis] <= b.max[axis] + padding && a2.max[axis] + padding >= b.min[axis]);
|
|
341590
|
+
}
|
|
341591
|
+
function aabbGaps(a2, b) {
|
|
341592
|
+
return [
|
|
341593
|
+
Math.max(0, b.min[0] - a2.max[0], a2.min[0] - b.max[0]),
|
|
341594
|
+
Math.max(0, b.min[1] - a2.max[1], a2.min[1] - b.max[1]),
|
|
341595
|
+
Math.max(0, b.min[2] - a2.max[2], a2.min[2] - b.max[2])
|
|
341596
|
+
];
|
|
341597
|
+
}
|
|
341598
|
+
function aabbInteriorOverlaps(a2, b) {
|
|
341599
|
+
return [0, 1, 2].every((axis) => Math.min(a2.max[axis], b.max[axis]) - Math.max(a2.min[axis], b.min[axis]) > 0);
|
|
341600
|
+
}
|
|
341601
|
+
function aabbOverlapVolume(a2, b) {
|
|
341602
|
+
let volume = 1;
|
|
341603
|
+
for (const axis of [0, 1, 2]) {
|
|
341604
|
+
const overlap = Math.min(a2.max[axis], b.max[axis]) - Math.max(a2.min[axis], b.min[axis]);
|
|
341605
|
+
if (overlap <= 0) return 0;
|
|
341606
|
+
volume *= overlap;
|
|
341607
|
+
}
|
|
341608
|
+
return volume;
|
|
341609
|
+
}
|
|
341610
|
+
class AabbSpatialIndex {
|
|
341611
|
+
constructor(entries, leafSize = DEFAULT_LEAF_SIZE) {
|
|
341612
|
+
__publicField(this, "root");
|
|
341613
|
+
__publicField(this, "nodeCount");
|
|
341614
|
+
this.entries = entries;
|
|
341615
|
+
this.leafSize = leafSize;
|
|
341616
|
+
let nodeCount = 0;
|
|
341617
|
+
const build = (indexes) => {
|
|
341618
|
+
nodeCount += 1;
|
|
341619
|
+
const bounds = boundsFor(entries, indexes);
|
|
341620
|
+
if (indexes.length <= this.leafSize) {
|
|
341621
|
+
return { min: cloneVec3$1(bounds.min), max: cloneVec3$1(bounds.max), left: null, right: null, indexes };
|
|
341622
|
+
}
|
|
341623
|
+
const axis = longestAxis(bounds);
|
|
341624
|
+
indexes.sort((a2, b) => centerOnAxis(entries[a2], axis) - centerOnAxis(entries[b], axis) || a2 - b);
|
|
341625
|
+
const mid = Math.max(1, Math.floor(indexes.length / 2));
|
|
341626
|
+
return {
|
|
341627
|
+
min: cloneVec3$1(bounds.min),
|
|
341628
|
+
max: cloneVec3$1(bounds.max),
|
|
341629
|
+
left: build(indexes.slice(0, mid)),
|
|
341630
|
+
right: build(indexes.slice(mid)),
|
|
341631
|
+
indexes: null
|
|
341632
|
+
};
|
|
341633
|
+
};
|
|
341634
|
+
this.root = entries.length > 0 ? build(entries.map((_2, index2) => index2)) : null;
|
|
341635
|
+
this.nodeCount = nodeCount;
|
|
341636
|
+
}
|
|
341637
|
+
overlapPairs(options = {}) {
|
|
341638
|
+
const padding = Math.max(0, options.padding ?? 0);
|
|
341639
|
+
const pairs = [];
|
|
341640
|
+
let nodePairChecks = 0;
|
|
341641
|
+
let leafPairChecks = 0;
|
|
341642
|
+
const addLeafPair = (sourceIndex, targetIndex) => {
|
|
341643
|
+
var _a3;
|
|
341644
|
+
const source = this.entries[sourceIndex];
|
|
341645
|
+
const target = this.entries[targetIndex];
|
|
341646
|
+
leafPairChecks += 1;
|
|
341647
|
+
if ((_a3 = options.skipPair) == null ? void 0 : _a3.call(options, source, target, sourceIndex, targetIndex)) return;
|
|
341648
|
+
if (!aabbOverlaps(source, target, padding)) return;
|
|
341649
|
+
pairs.push({ sourceIndex, targetIndex });
|
|
341650
|
+
};
|
|
341651
|
+
const between = (a2, b) => {
|
|
341652
|
+
nodePairChecks += 1;
|
|
341653
|
+
if (!aabbOverlaps(a2, b, padding)) return;
|
|
341654
|
+
if (a2.indexes && b.indexes) {
|
|
341655
|
+
for (const sourceIndex of a2.indexes) {
|
|
341656
|
+
for (const targetIndex of b.indexes) addLeafPair(Math.min(sourceIndex, targetIndex), Math.max(sourceIndex, targetIndex));
|
|
341657
|
+
}
|
|
341658
|
+
return;
|
|
341659
|
+
}
|
|
341660
|
+
if (!b.indexes && (a2.indexes || nodeVolume(a2) <= nodeVolume(b))) {
|
|
341661
|
+
between(a2, b.left);
|
|
341662
|
+
between(a2, b.right);
|
|
341663
|
+
return;
|
|
341664
|
+
}
|
|
341665
|
+
between(a2.left, b);
|
|
341666
|
+
between(a2.right, b);
|
|
341667
|
+
};
|
|
341668
|
+
const within = (node) => {
|
|
341669
|
+
if (node.indexes) {
|
|
341670
|
+
for (let i = 0; i < node.indexes.length; i += 1) {
|
|
341671
|
+
for (let j = i + 1; j < node.indexes.length; j += 1) addLeafPair(node.indexes[i], node.indexes[j]);
|
|
341672
|
+
}
|
|
341673
|
+
return;
|
|
341674
|
+
}
|
|
341675
|
+
within(node.left);
|
|
341676
|
+
within(node.right);
|
|
341677
|
+
between(node.left, node.right);
|
|
341678
|
+
};
|
|
341679
|
+
if (this.root) within(this.root);
|
|
341680
|
+
return {
|
|
341681
|
+
pairs,
|
|
341682
|
+
stats: {
|
|
341683
|
+
objectCount: this.entries.length,
|
|
341684
|
+
nodeCount: this.nodeCount,
|
|
341685
|
+
nodePairChecks,
|
|
341686
|
+
leafPairChecks
|
|
341687
|
+
}
|
|
341688
|
+
};
|
|
341689
|
+
}
|
|
341690
|
+
}
|
|
339421
341691
|
const DEFAULT_COLLISION_INSPECTION_OPTIONS = {
|
|
339422
341692
|
minOverlapVolume: 0.1
|
|
339423
341693
|
};
|
|
339424
|
-
function cloneVec3
|
|
341694
|
+
function cloneVec3(value) {
|
|
339425
341695
|
return [value[0], value[1], value[2]];
|
|
339426
341696
|
}
|
|
339427
341697
|
function isIdentityTransform(matrix) {
|
|
@@ -339463,8 +341733,8 @@ function prepareEntry(entry) {
|
|
|
339463
341733
|
if (isIdentityTransform(entry.transform)) {
|
|
339464
341734
|
return {
|
|
339465
341735
|
...entry,
|
|
339466
|
-
min: cloneVec3
|
|
339467
|
-
max: cloneVec3
|
|
341736
|
+
min: cloneVec3(entry.min),
|
|
341737
|
+
max: cloneVec3(entry.max)
|
|
339468
341738
|
};
|
|
339469
341739
|
}
|
|
339470
341740
|
const bbox = transformBBox(entry.min, entry.max, entry.transform);
|
|
@@ -339475,56 +341745,26 @@ function prepareEntry(entry) {
|
|
|
339475
341745
|
max: bbox.max
|
|
339476
341746
|
};
|
|
339477
341747
|
}
|
|
339478
|
-
function bboxOverlaps(a2, b) {
|
|
339479
|
-
return [0, 1, 2].every((axis) => a2.min[axis] < b.max[axis] && a2.max[axis] > b.min[axis]);
|
|
339480
|
-
}
|
|
339481
341748
|
function collisionId(a2, b) {
|
|
339482
341749
|
return `${a2.id}__${b.id}`;
|
|
339483
341750
|
}
|
|
339484
|
-
function
|
|
339485
|
-
|
|
339486
|
-
|
|
339487
|
-
|
|
339488
|
-
let count = 0;
|
|
339489
|
-
for (let seen2 = 0; seen2 < ordered.length; seen2 += 1) {
|
|
339490
|
-
const currentMin = ordered[seen2].min;
|
|
339491
|
-
while (expired < seen2 && endValues[expired] <= currentMin) expired += 1;
|
|
339492
|
-
count += seen2 - expired;
|
|
339493
|
-
}
|
|
339494
|
-
return count;
|
|
341751
|
+
function shouldSkipPair(a2, b, options) {
|
|
341752
|
+
if (options.skipIntraGroup && a2.groupName && a2.groupName === b.groupName) return true;
|
|
341753
|
+
if (options.skipMockPairs && a2.mock && b.mock) return true;
|
|
341754
|
+
return false;
|
|
339495
341755
|
}
|
|
339496
|
-
function
|
|
339497
|
-
|
|
339498
|
-
|
|
339499
|
-
|
|
339500
|
-
|
|
339501
|
-
|
|
339502
|
-
|
|
339503
|
-
|
|
339504
|
-
|
|
339505
|
-
}
|
|
339506
|
-
|
|
339507
|
-
}
|
|
339508
|
-
function collectCandidatePairs$1(entries) {
|
|
339509
|
-
if (entries.length < 2) return { pairs: [], bboxPairChecks: 0 };
|
|
339510
|
-
const axis = chooseSweepAxis$1(entries);
|
|
339511
|
-
const ordered = entries.map((entry, index2) => ({ entry, index: index2 })).sort((a2, b) => a2.entry.min[axis] - b.entry.min[axis] || a2.entry.max[axis] - b.entry.max[axis] || a2.index - b.index);
|
|
339512
|
-
let active = [];
|
|
339513
|
-
const pairs = [];
|
|
339514
|
-
let bboxPairChecks = 0;
|
|
339515
|
-
for (const current of ordered) {
|
|
339516
|
-
active = active.filter((candidate) => candidate.entry.max[axis] > current.entry.min[axis]);
|
|
339517
|
-
for (const candidate of active) {
|
|
339518
|
-
bboxPairChecks += 1;
|
|
339519
|
-
if (!bboxOverlaps(candidate.entry, current.entry)) continue;
|
|
339520
|
-
const sourceIndex = Math.min(candidate.index, current.index);
|
|
339521
|
-
const targetIndex = Math.max(candidate.index, current.index);
|
|
339522
|
-
pairs.push({ sourceIndex, targetIndex });
|
|
339523
|
-
}
|
|
339524
|
-
active.push(current);
|
|
339525
|
-
}
|
|
339526
|
-
pairs.sort((a2, b) => a2.sourceIndex - b.sourceIndex || a2.targetIndex - b.targetIndex);
|
|
339527
|
-
return { pairs, bboxPairChecks };
|
|
341756
|
+
function collectCandidatePairs(entries, options) {
|
|
341757
|
+
const index2 = new AabbSpatialIndex(entries, 1);
|
|
341758
|
+
const result = index2.overlapPairs({
|
|
341759
|
+
padding: options.bboxPadding,
|
|
341760
|
+
skipPair: (a2, b) => shouldSkipPair(a2, b, options)
|
|
341761
|
+
});
|
|
341762
|
+
const pairs = result.pairs.map((pair) => ({
|
|
341763
|
+
...pair,
|
|
341764
|
+
bboxOverlapVolume: aabbOverlapVolume(entries[pair.sourceIndex], entries[pair.targetIndex])
|
|
341765
|
+
}));
|
|
341766
|
+
pairs.sort((a2, b) => b.bboxOverlapVolume - a2.bboxOverlapVolume || a2.sourceIndex - b.sourceIndex || a2.targetIndex - b.targetIndex);
|
|
341767
|
+
return { pairs, bboxPairChecks: result.stats.leafPairChecks };
|
|
339528
341768
|
}
|
|
339529
341769
|
function serializeCollisionFinding(finding) {
|
|
339530
341770
|
return {
|
|
@@ -339540,14 +341780,32 @@ function serializeCollisionFinding(finding) {
|
|
|
339540
341780
|
};
|
|
339541
341781
|
}
|
|
339542
341782
|
function analyzeCollisionIntersections(entries, rawOptions = {}) {
|
|
341783
|
+
const maxCandidatePairs = rawOptions.maxCandidatePairs === void 0 ? null : Math.max(0, Math.trunc(rawOptions.maxCandidatePairs));
|
|
341784
|
+
const maxElapsedMs = rawOptions.maxElapsedMs === void 0 ? null : Math.max(0, rawOptions.maxElapsedMs);
|
|
339543
341785
|
const options = {
|
|
339544
|
-
minOverlapVolume: rawOptions.minOverlapVolume ?? DEFAULT_COLLISION_INSPECTION_OPTIONS.minOverlapVolume
|
|
341786
|
+
minOverlapVolume: rawOptions.minOverlapVolume ?? DEFAULT_COLLISION_INSPECTION_OPTIONS.minOverlapVolume,
|
|
341787
|
+
maxCandidatePairs,
|
|
341788
|
+
maxElapsedMs,
|
|
341789
|
+
bboxPadding: Math.max(0, rawOptions.bboxPadding ?? 0),
|
|
341790
|
+
skipIntraGroup: rawOptions.skipIntraGroup === true,
|
|
341791
|
+
skipMockPairs: rawOptions.skipMockPairs === true,
|
|
341792
|
+
includeBBoxCandidates: rawOptions.includeBBoxCandidates !== false
|
|
339545
341793
|
};
|
|
339546
341794
|
const warnings = [];
|
|
339547
341795
|
const collisions = [];
|
|
339548
341796
|
const preparedEntries = entries.map((entry) => prepareEntry(entry));
|
|
339549
|
-
const { pairs: candidatePairs, bboxPairChecks } = collectCandidatePairs
|
|
339550
|
-
|
|
341797
|
+
const { pairs: candidatePairs, bboxPairChecks } = collectCandidatePairs(preparedEntries, options);
|
|
341798
|
+
const limitedCandidatePairs = options.maxCandidatePairs === null ? candidatePairs : candidatePairs.slice(0, options.maxCandidatePairs);
|
|
341799
|
+
const pairLimitSkippedPairCount = candidatePairs.length - limitedCandidatePairs.length;
|
|
341800
|
+
let testedPairCount = 0;
|
|
341801
|
+
let timeBudgetStopped = false;
|
|
341802
|
+
const exactStarted = performance.now();
|
|
341803
|
+
for (const pair of limitedCandidatePairs) {
|
|
341804
|
+
if (options.maxElapsedMs !== null && (options.maxElapsedMs <= 0 || performance.now() - exactStarted >= options.maxElapsedMs)) {
|
|
341805
|
+
timeBudgetStopped = true;
|
|
341806
|
+
break;
|
|
341807
|
+
}
|
|
341808
|
+
testedPairCount += 1;
|
|
339551
341809
|
const a2 = preparedEntries[pair.sourceIndex];
|
|
339552
341810
|
const b = preparedEntries[pair.targetIndex];
|
|
339553
341811
|
try {
|
|
@@ -339572,6 +341830,40 @@ function analyzeCollisionIntersections(entries, rawOptions = {}) {
|
|
|
339572
341830
|
warnings.push(`Could not boolean-test ${a2.name} against ${b.name}: ${message}`);
|
|
339573
341831
|
}
|
|
339574
341832
|
}
|
|
341833
|
+
const exactCheckMs = performance.now() - exactStarted;
|
|
341834
|
+
const timeBudgetSkippedPairCount = timeBudgetStopped ? limitedCandidatePairs.length - testedPairCount : 0;
|
|
341835
|
+
const skippedPairCount = pairLimitSkippedPairCount + timeBudgetSkippedPairCount;
|
|
341836
|
+
const bboxCandidates = options.includeBBoxCandidates ? candidatePairs.map((pair, index2) => {
|
|
341837
|
+
const source = preparedEntries[pair.sourceIndex];
|
|
341838
|
+
const target = preparedEntries[pair.targetIndex];
|
|
341839
|
+
const exactChecked = index2 < testedPairCount;
|
|
341840
|
+
let skippedBy;
|
|
341841
|
+
if (!exactChecked) {
|
|
341842
|
+
skippedBy = index2 >= limitedCandidatePairs.length ? "pair-limit" : "time-budget";
|
|
341843
|
+
}
|
|
341844
|
+
return {
|
|
341845
|
+
index: index2 + 1,
|
|
341846
|
+
sourceIndex: pair.sourceIndex,
|
|
341847
|
+
targetIndex: pair.targetIndex,
|
|
341848
|
+
sourceId: source.id,
|
|
341849
|
+
targetId: target.id,
|
|
341850
|
+
sourceName: source.name,
|
|
341851
|
+
targetName: target.name,
|
|
341852
|
+
bboxOverlapVolume: pair.bboxOverlapVolume,
|
|
341853
|
+
exactChecked,
|
|
341854
|
+
...skippedBy ? { skippedBy } : {}
|
|
341855
|
+
};
|
|
341856
|
+
}) : [];
|
|
341857
|
+
if (pairLimitSkippedPairCount > 0) {
|
|
341858
|
+
warnings.push(
|
|
341859
|
+
`Collision check candidate set was limited to ${limitedCandidatePairs.length}/${candidatePairs.length} bbox-overlapping object pair(s); ${pairLimitSkippedPairCount} lower-priority pair(s) were skipped by maxCandidatePairs=${options.maxCandidatePairs}. Increase the collision pair limit for exhaustive checking.`
|
|
341860
|
+
);
|
|
341861
|
+
}
|
|
341862
|
+
if (timeBudgetSkippedPairCount > 0) {
|
|
341863
|
+
warnings.push(
|
|
341864
|
+
`Collision check stopped after ${exactCheckMs.toFixed(0)} ms; tested ${testedPairCount}/${limitedCandidatePairs.length} selected bbox-overlapping object pair(s), skipping ${timeBudgetSkippedPairCount} by maxElapsedMs=${options.maxElapsedMs}. Increase the collision time budget for a deeper check.`
|
|
341865
|
+
);
|
|
341866
|
+
}
|
|
339575
341867
|
const objects = preparedEntries.map((entry, index2) => ({
|
|
339576
341868
|
index: index2,
|
|
339577
341869
|
id: entry.id,
|
|
@@ -339580,482 +341872,33 @@ function analyzeCollisionIntersections(entries, rawOptions = {}) {
|
|
|
339580
341872
|
treePath: entry.treePath,
|
|
339581
341873
|
mock: entry.mock === true,
|
|
339582
341874
|
bbox: {
|
|
339583
|
-
min: cloneVec3
|
|
339584
|
-
max: cloneVec3
|
|
341875
|
+
min: cloneVec3(entry.min),
|
|
341876
|
+
max: cloneVec3(entry.max)
|
|
339585
341877
|
}
|
|
339586
341878
|
}));
|
|
339587
341879
|
return {
|
|
339588
341880
|
method: "boolean-intersection",
|
|
339589
341881
|
options,
|
|
339590
341882
|
broadphase: {
|
|
339591
|
-
method: "
|
|
341883
|
+
method: "aabb-bvh",
|
|
339592
341884
|
allPairCount: preparedEntries.length * (preparedEntries.length - 1) / 2,
|
|
339593
341885
|
bboxPairChecks,
|
|
339594
|
-
booleanPairChecks:
|
|
341886
|
+
booleanPairChecks: testedPairCount
|
|
339595
341887
|
},
|
|
339596
341888
|
objectCount: objects.length,
|
|
341889
|
+
candidatePairCount: candidatePairs.length,
|
|
341890
|
+
testedPairCount,
|
|
341891
|
+
skippedPairCount,
|
|
341892
|
+
pairLimitSkippedPairCount,
|
|
341893
|
+
timeBudgetSkippedPairCount,
|
|
341894
|
+
exactCheckMs,
|
|
339597
341895
|
collisionCount: collisions.length,
|
|
341896
|
+
bboxCandidates,
|
|
339598
341897
|
objects,
|
|
339599
341898
|
collisions,
|
|
339600
341899
|
warnings
|
|
339601
341900
|
};
|
|
339602
341901
|
}
|
|
339603
|
-
const DEFAULT_ROUGHNESS_INSPECTION_OPTIONS = {
|
|
339604
|
-
smoothAngleDeg: 5,
|
|
339605
|
-
sharpAngleDeg: 30,
|
|
339606
|
-
harshAngleDeg: 90
|
|
339607
|
-
};
|
|
339608
|
-
const ROUGHNESS_COLORS = {
|
|
339609
|
-
smooth: [62, 72, 84],
|
|
339610
|
-
moderate: [255, 214, 0],
|
|
339611
|
-
sharp: [255, 124, 34],
|
|
339612
|
-
harsh: [255, 42, 96]
|
|
339613
|
-
};
|
|
339614
|
-
function resolveRoughnessInspectionOptions(raw = {}) {
|
|
339615
|
-
const options = {
|
|
339616
|
-
smoothAngleDeg: raw.smoothAngleDeg ?? DEFAULT_ROUGHNESS_INSPECTION_OPTIONS.smoothAngleDeg,
|
|
339617
|
-
sharpAngleDeg: raw.sharpAngleDeg ?? DEFAULT_ROUGHNESS_INSPECTION_OPTIONS.sharpAngleDeg,
|
|
339618
|
-
harshAngleDeg: raw.harshAngleDeg ?? DEFAULT_ROUGHNESS_INSPECTION_OPTIONS.harshAngleDeg
|
|
339619
|
-
};
|
|
339620
|
-
if (!Number.isFinite(options.smoothAngleDeg) || options.smoothAngleDeg < 0) {
|
|
339621
|
-
throw new Error(`smoothAngleDeg must be a finite non-negative angle (got ${options.smoothAngleDeg}).`);
|
|
339622
|
-
}
|
|
339623
|
-
if (!Number.isFinite(options.sharpAngleDeg) || options.sharpAngleDeg <= options.smoothAngleDeg) {
|
|
339624
|
-
throw new Error(`sharpAngleDeg must be greater than smoothAngleDeg (got ${options.sharpAngleDeg}).`);
|
|
339625
|
-
}
|
|
339626
|
-
if (!Number.isFinite(options.harshAngleDeg) || options.harshAngleDeg <= options.sharpAngleDeg || options.harshAngleDeg > 180) {
|
|
339627
|
-
throw new Error(`harshAngleDeg must be greater than sharpAngleDeg and <= 180 (got ${options.harshAngleDeg}).`);
|
|
339628
|
-
}
|
|
339629
|
-
return options;
|
|
339630
|
-
}
|
|
339631
|
-
function roughnessClassForAngle(angleDeg, options) {
|
|
339632
|
-
if (angleDeg >= options.harshAngleDeg) return "harsh";
|
|
339633
|
-
if (angleDeg >= options.sharpAngleDeg) return "sharp";
|
|
339634
|
-
if (angleDeg >= options.smoothAngleDeg) return "moderate";
|
|
339635
|
-
return "smooth";
|
|
339636
|
-
}
|
|
339637
|
-
function roughnessScoreForAngle(angleDeg, options) {
|
|
339638
|
-
if (angleDeg < options.sharpAngleDeg) return 0;
|
|
339639
|
-
if (angleDeg < options.harshAngleDeg) {
|
|
339640
|
-
return MathUtils.lerp(0.48, 0.82, (angleDeg - options.sharpAngleDeg) / (options.harshAngleDeg - options.sharpAngleDeg));
|
|
339641
|
-
}
|
|
339642
|
-
return 1;
|
|
339643
|
-
}
|
|
339644
|
-
function roughnessColorForAngle(angleDeg, options) {
|
|
339645
|
-
const cls = roughnessClassForAngle(angleDeg, options);
|
|
339646
|
-
if (cls === "smooth" || cls === "harsh") return ROUGHNESS_COLORS[cls];
|
|
339647
|
-
if (cls === "moderate") {
|
|
339648
|
-
return lerpRgb(
|
|
339649
|
-
ROUGHNESS_COLORS.moderate,
|
|
339650
|
-
ROUGHNESS_COLORS.sharp,
|
|
339651
|
-
(angleDeg - options.smoothAngleDeg) / (options.sharpAngleDeg - options.smoothAngleDeg)
|
|
339652
|
-
);
|
|
339653
|
-
}
|
|
339654
|
-
return lerpRgb(
|
|
339655
|
-
ROUGHNESS_COLORS.sharp,
|
|
339656
|
-
ROUGHNESS_COLORS.harsh,
|
|
339657
|
-
(angleDeg - options.sharpAngleDeg) / (options.harshAngleDeg - options.sharpAngleDeg)
|
|
339658
|
-
);
|
|
339659
|
-
}
|
|
339660
|
-
function lerpRgb(a2, b, t) {
|
|
339661
|
-
const clamped = MathUtils.clamp(t, 0, 1);
|
|
339662
|
-
return [
|
|
339663
|
-
Math.round(MathUtils.lerp(a2[0], b[0], clamped)),
|
|
339664
|
-
Math.round(MathUtils.lerp(a2[1], b[1], clamped)),
|
|
339665
|
-
Math.round(MathUtils.lerp(a2[2], b[2], clamped))
|
|
339666
|
-
];
|
|
339667
|
-
}
|
|
339668
|
-
function emptyRoughnessSummary() {
|
|
339669
|
-
return {
|
|
339670
|
-
triangleCount: 0,
|
|
339671
|
-
edgeCount: 0,
|
|
339672
|
-
boundaryEdgeCount: 0,
|
|
339673
|
-
nonManifoldEdgeCount: 0,
|
|
339674
|
-
smoothAreaPercent: 0,
|
|
339675
|
-
moderateAreaPercent: 0,
|
|
339676
|
-
sharpAreaPercent: 0,
|
|
339677
|
-
harshAreaPercent: 0,
|
|
339678
|
-
roughAreaPercent: 0,
|
|
339679
|
-
meanAngleDeg: null,
|
|
339680
|
-
p50AngleDeg: null,
|
|
339681
|
-
p90AngleDeg: null,
|
|
339682
|
-
p95AngleDeg: null,
|
|
339683
|
-
p99AngleDeg: null,
|
|
339684
|
-
maxAngleDeg: null,
|
|
339685
|
-
qualityScore: 0
|
|
339686
|
-
};
|
|
339687
|
-
}
|
|
339688
|
-
function summarizeRoughnessTriangles(triangles, edgeAngles, edgeCount, boundaryEdgeCount, nonManifoldEdgeCount, options) {
|
|
339689
|
-
const areaByClass = {
|
|
339690
|
-
smooth: 0,
|
|
339691
|
-
moderate: 0,
|
|
339692
|
-
sharp: 0,
|
|
339693
|
-
harsh: 0
|
|
339694
|
-
};
|
|
339695
|
-
let totalArea = 0;
|
|
339696
|
-
for (const tri of triangles) {
|
|
339697
|
-
const area2 = Number.isFinite(tri.area) ? tri.area : 0;
|
|
339698
|
-
totalArea += area2;
|
|
339699
|
-
areaByClass[roughnessClassForAngle(tri.maxAngleDeg, options)] += area2;
|
|
339700
|
-
}
|
|
339701
|
-
const sortedAngles = [...edgeAngles].sort((lhs, rhs) => lhs - rhs);
|
|
339702
|
-
const meanAngleDeg = sortedAngles.length > 0 ? Number((sortedAngles.reduce((sum2, angle) => sum2 + angle, 0) / sortedAngles.length).toFixed(2)) : null;
|
|
339703
|
-
const qualityScore = totalArea > 0 ? Math.round(
|
|
339704
|
-
MathUtils.clamp(
|
|
339705
|
-
100 * (areaByClass.smooth + areaByClass.moderate * 0.9) / totalArea - 50 * areaByClass.harsh / totalArea,
|
|
339706
|
-
0,
|
|
339707
|
-
100
|
|
339708
|
-
)
|
|
339709
|
-
) : 0;
|
|
339710
|
-
const safePercent = (value) => totalArea > 0 ? Number((value / totalArea * 100).toFixed(2)) : 0;
|
|
339711
|
-
return {
|
|
339712
|
-
triangleCount: triangles.length,
|
|
339713
|
-
edgeCount,
|
|
339714
|
-
boundaryEdgeCount,
|
|
339715
|
-
nonManifoldEdgeCount,
|
|
339716
|
-
smoothAreaPercent: safePercent(areaByClass.smooth),
|
|
339717
|
-
moderateAreaPercent: safePercent(areaByClass.moderate),
|
|
339718
|
-
sharpAreaPercent: safePercent(areaByClass.sharp),
|
|
339719
|
-
harshAreaPercent: safePercent(areaByClass.harsh),
|
|
339720
|
-
roughAreaPercent: safePercent(areaByClass.sharp + areaByClass.harsh),
|
|
339721
|
-
meanAngleDeg,
|
|
339722
|
-
p50AngleDeg: percentile(sortedAngles, 0.5),
|
|
339723
|
-
p90AngleDeg: percentile(sortedAngles, 0.9),
|
|
339724
|
-
p95AngleDeg: percentile(sortedAngles, 0.95),
|
|
339725
|
-
p99AngleDeg: percentile(sortedAngles, 0.99),
|
|
339726
|
-
maxAngleDeg: sortedAngles.length > 0 ? Number(sortedAngles[sortedAngles.length - 1].toFixed(2)) : null,
|
|
339727
|
-
qualityScore
|
|
339728
|
-
};
|
|
339729
|
-
}
|
|
339730
|
-
function percentile(sorted, q) {
|
|
339731
|
-
if (sorted.length === 0) return null;
|
|
339732
|
-
const index2 = MathUtils.clamp(Math.floor(sorted.length * q), 0, sorted.length - 1);
|
|
339733
|
-
return Number(sorted[index2].toFixed(2));
|
|
339734
|
-
}
|
|
339735
|
-
const DEG_PER_RAD = 180 / Math.PI;
|
|
339736
|
-
function analyzeRoughnessGeometry(sourceGeometry, rawOptions = {}) {
|
|
339737
|
-
const options = resolveRoughnessInspectionOptions(rawOptions);
|
|
339738
|
-
const geometry = sourceGeometry.index ? sourceGeometry.toNonIndexed() : sourceGeometry.clone();
|
|
339739
|
-
const position = geometry.getAttribute("position");
|
|
339740
|
-
const warnings = [];
|
|
339741
|
-
if (!position || position.count < 3) {
|
|
339742
|
-
return { geometry, summary: emptyRoughnessSummary(), warnings: ["No triangle geometry."] };
|
|
339743
|
-
}
|
|
339744
|
-
const triangleCount = Math.floor(position.count / 3);
|
|
339745
|
-
const normals = new Array(triangleCount);
|
|
339746
|
-
const triangles = new Array(triangleCount);
|
|
339747
|
-
const edges = /* @__PURE__ */ new Map();
|
|
339748
|
-
const colors = new Float32Array(position.count * 3);
|
|
339749
|
-
const scores = new Float32Array(position.count);
|
|
339750
|
-
const a2 = new Vector3();
|
|
339751
|
-
const b = new Vector3();
|
|
339752
|
-
const c2 = new Vector3();
|
|
339753
|
-
const ac = new Vector3();
|
|
339754
|
-
const normal = new Vector3();
|
|
339755
|
-
const bbox = new Box3().setFromBufferAttribute(position);
|
|
339756
|
-
const snap = Math.max(1e-6, bbox.getSize(new Vector3()).length() * 1e-8);
|
|
339757
|
-
for (let tri = 0; tri < triangleCount; tri += 1) {
|
|
339758
|
-
const offset2 = tri * 3;
|
|
339759
|
-
readVertex(position, offset2, a2);
|
|
339760
|
-
readVertex(position, offset2 + 1, b);
|
|
339761
|
-
readVertex(position, offset2 + 2, c2);
|
|
339762
|
-
normal.subVectors(b, a2).cross(ac.subVectors(c2, a2));
|
|
339763
|
-
const areaTwice = normal.length();
|
|
339764
|
-
triangles[tri] = { area: areaTwice * 0.5, maxAngleDeg: 0 };
|
|
339765
|
-
normals[tri] = areaTwice > 1e-12 ? normal.multiplyScalar(1 / areaTwice).clone() : new Vector3(0, 0, 1);
|
|
339766
|
-
const keys = [vertexKey(a2, snap), vertexKey(b, snap), vertexKey(c2, snap)];
|
|
339767
|
-
for (let edge = 0; edge < 3; edge += 1) {
|
|
339768
|
-
const key = edgeKey(keys[edge], keys[(edge + 1) % 3]);
|
|
339769
|
-
let record = edges.get(key);
|
|
339770
|
-
if (!record) {
|
|
339771
|
-
record = { triangles: [] };
|
|
339772
|
-
edges.set(key, record);
|
|
339773
|
-
}
|
|
339774
|
-
record.triangles.push(tri);
|
|
339775
|
-
}
|
|
339776
|
-
}
|
|
339777
|
-
const { boundaryEdgeCount, nonManifoldEdgeCount, edgeAngles } = markTriangleRoughness(edges, triangles, normals);
|
|
339778
|
-
if (boundaryEdgeCount > 0) warnings.push(`${boundaryEdgeCount} boundary edge(s) were treated as harsh roughness.`);
|
|
339779
|
-
if (nonManifoldEdgeCount > 0) warnings.push(`${nonManifoldEdgeCount} non-manifold edge(s) were treated as harsh roughness.`);
|
|
339780
|
-
for (let tri = 0; tri < triangleCount; tri += 1) {
|
|
339781
|
-
const { maxAngleDeg } = triangles[tri];
|
|
339782
|
-
const color = roughnessColorForAngle(maxAngleDeg, options);
|
|
339783
|
-
const score = roughnessScoreForAngle(maxAngleDeg, options);
|
|
339784
|
-
const offset2 = tri * 3;
|
|
339785
|
-
for (let vertex2 = 0; vertex2 < 3; vertex2 += 1) {
|
|
339786
|
-
const colorOffset = (offset2 + vertex2) * 3;
|
|
339787
|
-
colors[colorOffset] = color[0] / 255;
|
|
339788
|
-
colors[colorOffset + 1] = color[1] / 255;
|
|
339789
|
-
colors[colorOffset + 2] = color[2] / 255;
|
|
339790
|
-
scores[offset2 + vertex2] = score;
|
|
339791
|
-
}
|
|
339792
|
-
}
|
|
339793
|
-
geometry.setAttribute("color", new BufferAttribute(colors, 3));
|
|
339794
|
-
geometry.setAttribute("roughnessScore", new BufferAttribute(scores, 1));
|
|
339795
|
-
geometry.computeBoundingBox();
|
|
339796
|
-
return {
|
|
339797
|
-
geometry,
|
|
339798
|
-
summary: summarizeRoughnessTriangles(triangles, edgeAngles, edges.size, boundaryEdgeCount, nonManifoldEdgeCount, options),
|
|
339799
|
-
warnings
|
|
339800
|
-
};
|
|
339801
|
-
}
|
|
339802
|
-
function markTriangleRoughness(edges, triangles, normals) {
|
|
339803
|
-
const edgeAngles = [];
|
|
339804
|
-
let boundaryEdgeCount = 0;
|
|
339805
|
-
let nonManifoldEdgeCount = 0;
|
|
339806
|
-
for (const edge of edges.values()) {
|
|
339807
|
-
if (edge.triangles.length === 1) {
|
|
339808
|
-
boundaryEdgeCount += 1;
|
|
339809
|
-
markTriangles(edge.triangles, triangles, 180);
|
|
339810
|
-
edgeAngles.push(180);
|
|
339811
|
-
continue;
|
|
339812
|
-
}
|
|
339813
|
-
if (edge.triangles.length > 2) {
|
|
339814
|
-
nonManifoldEdgeCount += 1;
|
|
339815
|
-
markTriangles(edge.triangles, triangles, 180);
|
|
339816
|
-
edgeAngles.push(180);
|
|
339817
|
-
continue;
|
|
339818
|
-
}
|
|
339819
|
-
const [first, second] = edge.triangles;
|
|
339820
|
-
const dot2 = MathUtils.clamp(normals[first].dot(normals[second]), -1, 1);
|
|
339821
|
-
const angleDeg = Math.acos(dot2) * DEG_PER_RAD;
|
|
339822
|
-
markTriangles(edge.triangles, triangles, angleDeg);
|
|
339823
|
-
edgeAngles.push(angleDeg);
|
|
339824
|
-
}
|
|
339825
|
-
return { boundaryEdgeCount, nonManifoldEdgeCount, edgeAngles };
|
|
339826
|
-
}
|
|
339827
|
-
function readVertex(position, index2, target) {
|
|
339828
|
-
target.set(position.getX(index2), position.getY(index2), position.getZ(index2));
|
|
339829
|
-
}
|
|
339830
|
-
function vertexKey(point2, snap) {
|
|
339831
|
-
return `${Math.round(point2.x / snap)},${Math.round(point2.y / snap)},${Math.round(point2.z / snap)}`;
|
|
339832
|
-
}
|
|
339833
|
-
function edgeKey(a2, b) {
|
|
339834
|
-
return a2 < b ? `${a2}|${b}` : `${b}|${a2}`;
|
|
339835
|
-
}
|
|
339836
|
-
function markTriangles(indices, triangles, angleDeg) {
|
|
339837
|
-
for (const index2 of indices) {
|
|
339838
|
-
triangles[index2].maxAngleDeg = Math.max(triangles[index2].maxAngleDeg, angleDeg);
|
|
339839
|
-
}
|
|
339840
|
-
}
|
|
339841
|
-
const DEFAULT_THICKNESS_INSPECTION_OPTIONS = {
|
|
339842
|
-
minThickness: 1.2,
|
|
339843
|
-
warnThickness: 2,
|
|
339844
|
-
maxThickness: 6,
|
|
339845
|
-
maxSamplesPerObject: 5e3
|
|
339846
|
-
};
|
|
339847
|
-
const THICKNESS_COLORS = {
|
|
339848
|
-
critical: [255, 28, 28],
|
|
339849
|
-
warning: [255, 150, 0],
|
|
339850
|
-
ok: [60, 220, 90],
|
|
339851
|
-
thick: [70, 145, 255],
|
|
339852
|
-
unknown: [90, 90, 90]
|
|
339853
|
-
};
|
|
339854
|
-
function finitePositive(value, fallback, label) {
|
|
339855
|
-
if (value === void 0) return fallback;
|
|
339856
|
-
if (!Number.isFinite(value) || value <= 0) {
|
|
339857
|
-
throw new Error(`${label} must be a positive finite number.`);
|
|
339858
|
-
}
|
|
339859
|
-
return value;
|
|
339860
|
-
}
|
|
339861
|
-
function resolveThicknessInspectionOptions(raw = {}) {
|
|
339862
|
-
const minThickness = finitePositive(raw.minThickness, DEFAULT_THICKNESS_INSPECTION_OPTIONS.minThickness, "minThickness");
|
|
339863
|
-
const warnThickness = finitePositive(raw.warnThickness, DEFAULT_THICKNESS_INSPECTION_OPTIONS.warnThickness, "warnThickness");
|
|
339864
|
-
const maxThickness = finitePositive(raw.maxThickness, DEFAULT_THICKNESS_INSPECTION_OPTIONS.maxThickness, "maxThickness");
|
|
339865
|
-
const maxSamplesPerObject = finitePositive(
|
|
339866
|
-
raw.maxSamplesPerObject,
|
|
339867
|
-
DEFAULT_THICKNESS_INSPECTION_OPTIONS.maxSamplesPerObject,
|
|
339868
|
-
"maxSamplesPerObject"
|
|
339869
|
-
);
|
|
339870
|
-
if (minThickness > warnThickness) {
|
|
339871
|
-
throw new Error("minThickness must be less than or equal to warnThickness.");
|
|
339872
|
-
}
|
|
339873
|
-
if (warnThickness > maxThickness) {
|
|
339874
|
-
throw new Error("warnThickness must be less than or equal to maxThickness.");
|
|
339875
|
-
}
|
|
339876
|
-
return {
|
|
339877
|
-
minThickness,
|
|
339878
|
-
warnThickness,
|
|
339879
|
-
maxThickness,
|
|
339880
|
-
maxSamplesPerObject: Math.max(1, Math.floor(maxSamplesPerObject))
|
|
339881
|
-
};
|
|
339882
|
-
}
|
|
339883
|
-
function lerp(a2, b, t) {
|
|
339884
|
-
return a2 + (b - a2) * Math.max(0, Math.min(1, t));
|
|
339885
|
-
}
|
|
339886
|
-
function lerpColor(a2, b, t) {
|
|
339887
|
-
return [Math.round(lerp(a2[0], b[0], t)), Math.round(lerp(a2[1], b[1], t)), Math.round(lerp(a2[2], b[2], t))];
|
|
339888
|
-
}
|
|
339889
|
-
function thicknessClass(thickness, options) {
|
|
339890
|
-
if (thickness == null || !Number.isFinite(thickness) || thickness <= 0) return "unknown";
|
|
339891
|
-
if (thickness <= options.minThickness) return "critical";
|
|
339892
|
-
if (thickness <= options.warnThickness) return "warning";
|
|
339893
|
-
if (thickness <= options.maxThickness) return "ok";
|
|
339894
|
-
return "thick";
|
|
339895
|
-
}
|
|
339896
|
-
function thicknessColor(thickness, options) {
|
|
339897
|
-
const cls = thicknessClass(thickness, options);
|
|
339898
|
-
if (cls === "unknown") return THICKNESS_COLORS.unknown;
|
|
339899
|
-
if (cls === "critical") return THICKNESS_COLORS.critical;
|
|
339900
|
-
if (cls === "warning") {
|
|
339901
|
-
const span = Math.max(1e-9, options.warnThickness - options.minThickness);
|
|
339902
|
-
return lerpColor(THICKNESS_COLORS.critical, THICKNESS_COLORS.warning, ((thickness ?? 0) - options.minThickness) / span);
|
|
339903
|
-
}
|
|
339904
|
-
if (cls === "ok") {
|
|
339905
|
-
const span = Math.max(1e-9, options.maxThickness - options.warnThickness);
|
|
339906
|
-
return lerpColor(THICKNESS_COLORS.ok, THICKNESS_COLORS.thick, ((thickness ?? 0) - options.warnThickness) / span);
|
|
339907
|
-
}
|
|
339908
|
-
return THICKNESS_COLORS.thick;
|
|
339909
|
-
}
|
|
339910
|
-
function sampleArea(sample) {
|
|
339911
|
-
const area2 = sample.area ?? 1;
|
|
339912
|
-
return Number.isFinite(area2) && area2 > 0 ? area2 : 1;
|
|
339913
|
-
}
|
|
339914
|
-
function weightedQuantile(samples, q) {
|
|
339915
|
-
if (samples.length === 0) return null;
|
|
339916
|
-
const sorted = [...samples].sort((a2, b) => a2.thickness - b.thickness);
|
|
339917
|
-
const totalArea = sorted.reduce((sum2, sample) => sum2 + sample.area, 0);
|
|
339918
|
-
const target = totalArea * Math.max(0, Math.min(1, q));
|
|
339919
|
-
let cumulative = 0;
|
|
339920
|
-
for (const sample of sorted) {
|
|
339921
|
-
cumulative += sample.area;
|
|
339922
|
-
if (cumulative >= target) return sample.thickness;
|
|
339923
|
-
}
|
|
339924
|
-
return sorted[sorted.length - 1].thickness;
|
|
339925
|
-
}
|
|
339926
|
-
function percent(part, total) {
|
|
339927
|
-
if (total <= 0) return 0;
|
|
339928
|
-
return part / total * 100;
|
|
339929
|
-
}
|
|
339930
|
-
function summarizeThicknessSamples(samples, options) {
|
|
339931
|
-
const resolved = [];
|
|
339932
|
-
let totalArea = 0;
|
|
339933
|
-
let resolvedArea = 0;
|
|
339934
|
-
let unresolvedArea = 0;
|
|
339935
|
-
let criticalArea = 0;
|
|
339936
|
-
let warningArea = 0;
|
|
339937
|
-
let weightedSum = 0;
|
|
339938
|
-
for (const sample of samples) {
|
|
339939
|
-
const area2 = sampleArea(sample);
|
|
339940
|
-
totalArea += area2;
|
|
339941
|
-
const value = sample.thickness;
|
|
339942
|
-
if (value == null || !Number.isFinite(value) || value <= 0) {
|
|
339943
|
-
unresolvedArea += area2;
|
|
339944
|
-
continue;
|
|
339945
|
-
}
|
|
339946
|
-
resolved.push({ thickness: value, area: area2 });
|
|
339947
|
-
resolvedArea += area2;
|
|
339948
|
-
weightedSum += value * area2;
|
|
339949
|
-
if (value <= options.minThickness) {
|
|
339950
|
-
criticalArea += area2;
|
|
339951
|
-
} else if (value <= options.warnThickness) {
|
|
339952
|
-
warningArea += area2;
|
|
339953
|
-
}
|
|
339954
|
-
}
|
|
339955
|
-
const values = resolved.map((sample) => sample.thickness);
|
|
339956
|
-
return {
|
|
339957
|
-
sampleCount: samples.length,
|
|
339958
|
-
resolvedCount: resolved.length,
|
|
339959
|
-
unresolvedCount: samples.length - resolved.length,
|
|
339960
|
-
minThickness: values.length > 0 ? Math.min(...values) : null,
|
|
339961
|
-
p05Thickness: weightedQuantile(resolved, 0.05),
|
|
339962
|
-
medianThickness: weightedQuantile(resolved, 0.5),
|
|
339963
|
-
meanThickness: resolvedArea > 0 ? weightedSum / resolvedArea : null,
|
|
339964
|
-
maxThickness: values.length > 0 ? Math.max(...values) : null,
|
|
339965
|
-
criticalAreaPercent: percent(criticalArea, resolvedArea),
|
|
339966
|
-
warningAreaPercent: percent(warningArea, resolvedArea),
|
|
339967
|
-
belowWarnAreaPercent: percent(criticalArea + warningArea, resolvedArea),
|
|
339968
|
-
unresolvedAreaPercent: percent(unresolvedArea, totalArea)
|
|
339969
|
-
};
|
|
339970
|
-
}
|
|
339971
|
-
function cloneGeometryForFaceColors(geometry) {
|
|
339972
|
-
return geometry.index ? geometry.toNonIndexed() : geometry.clone();
|
|
339973
|
-
}
|
|
339974
|
-
function geometryMaxDimension(geometry) {
|
|
339975
|
-
geometry.computeBoundingBox();
|
|
339976
|
-
const box2 = geometry.boundingBox;
|
|
339977
|
-
if (!box2) return 1;
|
|
339978
|
-
const size = new Vector3();
|
|
339979
|
-
box2.getSize(size);
|
|
339980
|
-
return Math.max(1, size.x, size.y, size.z);
|
|
339981
|
-
}
|
|
339982
|
-
function firstOppositeSurfaceDistance(raycaster, mesh, point2, direction2, epsilon2, far) {
|
|
339983
|
-
const origin = point2.clone().addScaledVector(direction2, epsilon2);
|
|
339984
|
-
raycaster.set(origin, direction2);
|
|
339985
|
-
raycaster.near = epsilon2;
|
|
339986
|
-
raycaster.far = far;
|
|
339987
|
-
const hit = raycaster.intersectObject(mesh, false).find((entry) => entry.distance > epsilon2);
|
|
339988
|
-
return hit ? hit.distance + epsilon2 : null;
|
|
339989
|
-
}
|
|
339990
|
-
function triangleThickness(raycaster, mesh, centroid, normal, epsilon2, far) {
|
|
339991
|
-
const forward = firstOppositeSurfaceDistance(raycaster, mesh, centroid, normal, epsilon2, far);
|
|
339992
|
-
const backward = firstOppositeSurfaceDistance(raycaster, mesh, centroid, normal.clone().negate(), epsilon2, far);
|
|
339993
|
-
if (forward == null) return backward;
|
|
339994
|
-
if (backward == null) return forward;
|
|
339995
|
-
return Math.min(forward, backward);
|
|
339996
|
-
}
|
|
339997
|
-
function analyzeThicknessGeometry(sourceGeometry, rawOptions = {}) {
|
|
339998
|
-
const options = resolveThicknessInspectionOptions(rawOptions);
|
|
339999
|
-
const geometry = cloneGeometryForFaceColors(sourceGeometry);
|
|
340000
|
-
const position = geometry.getAttribute("position");
|
|
340001
|
-
if (!position || position.count < 3) {
|
|
340002
|
-
return { geometry, samples: [], triangleCount: 0, sampledTriangleCount: 0, sampleStride: 1, warnings: ["No triangle geometry."] };
|
|
340003
|
-
}
|
|
340004
|
-
const triangleCount = Math.floor(position.count / 3);
|
|
340005
|
-
const sampleStride = Math.max(1, Math.ceil(triangleCount / options.maxSamplesPerObject));
|
|
340006
|
-
const maxDim = geometryMaxDimension(geometry);
|
|
340007
|
-
const epsilon2 = Math.max(1e-4, maxDim * 1e-6);
|
|
340008
|
-
const far = Math.max(maxDim * 4, options.maxThickness * 4, 1);
|
|
340009
|
-
const colors = new Float32Array(position.count * 3);
|
|
340010
|
-
const samples = [];
|
|
340011
|
-
const warnings = [];
|
|
340012
|
-
const rayMaterial = new MeshBasicMaterial({ side: DoubleSide });
|
|
340013
|
-
const rayMesh = new Mesh(geometry, rayMaterial);
|
|
340014
|
-
const raycaster = new Raycaster();
|
|
340015
|
-
if (sampleStride > 1) {
|
|
340016
|
-
warnings.push(`Triangle sampling stride ${sampleStride}; increase --thickness-samples for denser analysis.`);
|
|
340017
|
-
}
|
|
340018
|
-
const a2 = new Vector3();
|
|
340019
|
-
const b = new Vector3();
|
|
340020
|
-
const c2 = new Vector3();
|
|
340021
|
-
const ac = new Vector3();
|
|
340022
|
-
const normal = new Vector3();
|
|
340023
|
-
const centroid = new Vector3();
|
|
340024
|
-
let sampledTriangleCount = 0;
|
|
340025
|
-
let lastThickness = null;
|
|
340026
|
-
for (let tri = 0; tri < triangleCount; tri += 1) {
|
|
340027
|
-
const offset2 = tri * 3;
|
|
340028
|
-
a2.fromBufferAttribute(position, offset2);
|
|
340029
|
-
b.fromBufferAttribute(position, offset2 + 1);
|
|
340030
|
-
c2.fromBufferAttribute(position, offset2 + 2);
|
|
340031
|
-
normal.subVectors(b, a2).cross(ac.subVectors(c2, a2));
|
|
340032
|
-
const areaTwice = normal.length();
|
|
340033
|
-
const area2 = areaTwice * 0.5;
|
|
340034
|
-
let thickness = lastThickness;
|
|
340035
|
-
if (tri % sampleStride === 0) {
|
|
340036
|
-
sampledTriangleCount += 1;
|
|
340037
|
-
if (areaTwice <= 1e-12) {
|
|
340038
|
-
thickness = null;
|
|
340039
|
-
} else {
|
|
340040
|
-
normal.multiplyScalar(1 / areaTwice);
|
|
340041
|
-
centroid.copy(a2).add(b).add(c2).multiplyScalar(1 / 3);
|
|
340042
|
-
thickness = triangleThickness(raycaster, rayMesh, centroid, normal, epsilon2, far);
|
|
340043
|
-
}
|
|
340044
|
-
lastThickness = thickness;
|
|
340045
|
-
samples.push({ thickness, area: area2 });
|
|
340046
|
-
}
|
|
340047
|
-
const color = thicknessColor(thickness, options);
|
|
340048
|
-
for (let vertex2 = 0; vertex2 < 3; vertex2 += 1) {
|
|
340049
|
-
const colorOffset = (offset2 + vertex2) * 3;
|
|
340050
|
-
colors[colorOffset] = color[0] / 255;
|
|
340051
|
-
colors[colorOffset + 1] = color[1] / 255;
|
|
340052
|
-
colors[colorOffset + 2] = color[2] / 255;
|
|
340053
|
-
}
|
|
340054
|
-
}
|
|
340055
|
-
geometry.setAttribute("color", new BufferAttribute(colors, 3));
|
|
340056
|
-
rayMaterial.dispose();
|
|
340057
|
-
return { geometry, samples, triangleCount, sampledTriangleCount, sampleStride, warnings };
|
|
340058
|
-
}
|
|
340059
341902
|
const VECTOR_KEYS = /* @__PURE__ */ new Set(["pos", "position", "target", "lookat", "aim", "up"]);
|
|
340060
341903
|
const roundNumber = (value, digits) => {
|
|
340061
341904
|
const scale2 = 10 ** digits;
|
|
@@ -340227,494 +342070,6 @@ function localAabbPlaneRelation(min2, max2, plane, eps = 1e-7) {
|
|
|
340227
342070
|
if (minDistance > -eps) return "positive";
|
|
340228
342071
|
return "crossing";
|
|
340229
342072
|
}
|
|
340230
|
-
const DEFAULT_PHYSICAL_CONNECTIVITY_OPTIONS = {
|
|
340231
|
-
contactTolerance: 0.05,
|
|
340232
|
-
minOverlapVolume: 0.1,
|
|
340233
|
-
exactGeometry: false
|
|
340234
|
-
};
|
|
340235
|
-
const AXIS_NAMES = ["x", "y", "z"];
|
|
340236
|
-
class UnionFind {
|
|
340237
|
-
constructor(size) {
|
|
340238
|
-
__publicField(this, "parent");
|
|
340239
|
-
__publicField(this, "rank");
|
|
340240
|
-
this.parent = Array.from({ length: size }, (_2, index2) => index2);
|
|
340241
|
-
this.rank = Array.from({ length: size }, () => 0);
|
|
340242
|
-
}
|
|
340243
|
-
find(value) {
|
|
340244
|
-
const parent = this.parent[value];
|
|
340245
|
-
if (parent === value) return value;
|
|
340246
|
-
const root = this.find(parent);
|
|
340247
|
-
this.parent[value] = root;
|
|
340248
|
-
return root;
|
|
340249
|
-
}
|
|
340250
|
-
union(a2, b) {
|
|
340251
|
-
const rootA = this.find(a2);
|
|
340252
|
-
const rootB = this.find(b);
|
|
340253
|
-
if (rootA === rootB) return;
|
|
340254
|
-
if (this.rank[rootA] < this.rank[rootB]) {
|
|
340255
|
-
this.parent[rootA] = rootB;
|
|
340256
|
-
return;
|
|
340257
|
-
}
|
|
340258
|
-
if (this.rank[rootA] > this.rank[rootB]) {
|
|
340259
|
-
this.parent[rootB] = rootA;
|
|
340260
|
-
return;
|
|
340261
|
-
}
|
|
340262
|
-
this.parent[rootB] = rootA;
|
|
340263
|
-
this.rank[rootA] += 1;
|
|
340264
|
-
}
|
|
340265
|
-
}
|
|
340266
|
-
function cloneVec3(value) {
|
|
340267
|
-
return [value[0], value[1], value[2]];
|
|
340268
|
-
}
|
|
340269
|
-
function emptyBBox() {
|
|
340270
|
-
return {
|
|
340271
|
-
min: [Infinity, Infinity, Infinity],
|
|
340272
|
-
max: [-Infinity, -Infinity, -Infinity]
|
|
340273
|
-
};
|
|
340274
|
-
}
|
|
340275
|
-
function expandBBox(target, min2, max2) {
|
|
340276
|
-
for (let axis = 0; axis < 3; axis += 1) {
|
|
340277
|
-
target.min[axis] = Math.min(target.min[axis], min2[axis]);
|
|
340278
|
-
target.max[axis] = Math.max(target.max[axis], max2[axis]);
|
|
340279
|
-
}
|
|
340280
|
-
}
|
|
340281
|
-
function intervalGap$1(aMin, aMax, bMin, bMax) {
|
|
340282
|
-
if (aMax < bMin) return bMin - aMax;
|
|
340283
|
-
if (bMax < aMin) return aMin - bMax;
|
|
340284
|
-
return 0;
|
|
340285
|
-
}
|
|
340286
|
-
function nearestBoundaryGap(a2, b, axis) {
|
|
340287
|
-
return Math.min(Math.abs(a2.max[axis] - b.min[axis]), Math.abs(b.max[axis] - a2.min[axis]));
|
|
340288
|
-
}
|
|
340289
|
-
function bboxGaps(a2, b) {
|
|
340290
|
-
return [
|
|
340291
|
-
intervalGap$1(a2.min[0], a2.max[0], b.min[0], b.max[0]),
|
|
340292
|
-
intervalGap$1(a2.min[1], a2.max[1], b.min[1], b.max[1]),
|
|
340293
|
-
intervalGap$1(a2.min[2], a2.max[2], b.min[2], b.max[2])
|
|
340294
|
-
];
|
|
340295
|
-
}
|
|
340296
|
-
function maxGap(gaps) {
|
|
340297
|
-
return Math.max(gaps[0], gaps[1], gaps[2]);
|
|
340298
|
-
}
|
|
340299
|
-
function hasPositiveGap(gaps) {
|
|
340300
|
-
return gaps[0] > 0 || gaps[1] > 0 || gaps[2] > 0;
|
|
340301
|
-
}
|
|
340302
|
-
function bboxInteriorOverlaps(a2, b) {
|
|
340303
|
-
for (let axis = 0; axis < 3; axis += 1) {
|
|
340304
|
-
if (Math.min(a2.max[axis], b.max[axis]) - Math.max(a2.min[axis], b.min[axis]) <= 0) return false;
|
|
340305
|
-
}
|
|
340306
|
-
return true;
|
|
340307
|
-
}
|
|
340308
|
-
function bboxOverlapVolume(a2, b) {
|
|
340309
|
-
let volume = 1;
|
|
340310
|
-
for (let axis = 0; axis < 3; axis += 1) {
|
|
340311
|
-
volume *= Math.max(0, Math.min(a2.max[axis], b.max[axis]) - Math.max(a2.min[axis], b.min[axis]));
|
|
340312
|
-
}
|
|
340313
|
-
return volume;
|
|
340314
|
-
}
|
|
340315
|
-
function estimateSweepPairCount(entries, axis, tolerance) {
|
|
340316
|
-
const ordered = entries.map((entry) => ({ min: entry.min[axis], max: entry.max[axis] })).sort((a2, b) => a2.min - b.min || a2.max - b.max);
|
|
340317
|
-
const endValues = ordered.map((entry) => entry.max + tolerance).sort((a2, b) => a2 - b);
|
|
340318
|
-
let expired = 0;
|
|
340319
|
-
let count = 0;
|
|
340320
|
-
for (let seen2 = 0; seen2 < ordered.length; seen2 += 1) {
|
|
340321
|
-
const currentMin = ordered[seen2].min;
|
|
340322
|
-
while (expired < seen2 && endValues[expired] < currentMin) expired += 1;
|
|
340323
|
-
count += seen2 - expired;
|
|
340324
|
-
}
|
|
340325
|
-
return count;
|
|
340326
|
-
}
|
|
340327
|
-
function chooseSweepAxis(entries, tolerance) {
|
|
340328
|
-
let bestAxis = 0;
|
|
340329
|
-
let bestCount = estimateSweepPairCount(entries, bestAxis, tolerance);
|
|
340330
|
-
for (const axis of [1, 2]) {
|
|
340331
|
-
const count = estimateSweepPairCount(entries, axis, tolerance);
|
|
340332
|
-
if (count < bestCount) {
|
|
340333
|
-
bestAxis = axis;
|
|
340334
|
-
bestCount = count;
|
|
340335
|
-
}
|
|
340336
|
-
}
|
|
340337
|
-
return bestAxis;
|
|
340338
|
-
}
|
|
340339
|
-
function collectCandidatePairs(entries, tolerance) {
|
|
340340
|
-
if (entries.length < 2) return [];
|
|
340341
|
-
const axis = chooseSweepAxis(entries, tolerance);
|
|
340342
|
-
const ordered = entries.map((entry, index2) => ({ entry, index: index2 })).sort((a2, b) => a2.entry.min[axis] - b.entry.min[axis] || a2.entry.max[axis] - b.entry.max[axis] || a2.index - b.index);
|
|
340343
|
-
let active = [];
|
|
340344
|
-
const pairs = [];
|
|
340345
|
-
for (const current of ordered) {
|
|
340346
|
-
active = active.filter((candidate) => candidate.entry.max[axis] + tolerance >= current.entry.min[axis]);
|
|
340347
|
-
for (const candidate of active) {
|
|
340348
|
-
const gaps = bboxGaps(candidate.entry, current.entry);
|
|
340349
|
-
if (maxGap(gaps) > tolerance) continue;
|
|
340350
|
-
const sourceIndex = Math.min(candidate.index, current.index);
|
|
340351
|
-
const targetIndex = Math.max(candidate.index, current.index);
|
|
340352
|
-
pairs.push({ sourceIndex, targetIndex, gaps });
|
|
340353
|
-
}
|
|
340354
|
-
active.push(current);
|
|
340355
|
-
}
|
|
340356
|
-
pairs.sort((a2, b) => a2.sourceIndex - b.sourceIndex || a2.targetIndex - b.targetIndex);
|
|
340357
|
-
return pairs;
|
|
340358
|
-
}
|
|
340359
|
-
function contactFromBBoxes(a2, b, tolerance) {
|
|
340360
|
-
const gaps = bboxGaps(a2, b);
|
|
340361
|
-
const largestGap = maxGap(gaps);
|
|
340362
|
-
if (largestGap > tolerance) return { touching: false, gap: largestGap };
|
|
340363
|
-
const separatedAxes = gaps.map((gap, axis) => ({ gap, axis })).filter((entry) => entry.gap > 0);
|
|
340364
|
-
if (separatedAxes.length > 0) {
|
|
340365
|
-
const nearest2 = separatedAxes.reduce((best, entry) => entry.gap > best.gap ? entry : best, separatedAxes[0]);
|
|
340366
|
-
return { touching: true, gap: nearest2.gap, axis: AXIS_NAMES[nearest2.axis] };
|
|
340367
|
-
}
|
|
340368
|
-
const boundaryAxes = AXIS_NAMES.map((axisName, axis) => ({
|
|
340369
|
-
axis,
|
|
340370
|
-
axisName,
|
|
340371
|
-
gap: nearestBoundaryGap(a2, b, axis)
|
|
340372
|
-
})).filter((entry) => entry.gap <= tolerance);
|
|
340373
|
-
if (boundaryAxes.length === 0) return { touching: false, gap: 0 };
|
|
340374
|
-
const nearest = boundaryAxes.reduce((best, entry) => entry.gap < best.gap ? entry : best, boundaryAxes[0]);
|
|
340375
|
-
return { touching: true, gap: nearest.gap, axis: nearest.axisName };
|
|
340376
|
-
}
|
|
340377
|
-
function intersectionVolume(a2, b) {
|
|
340378
|
-
try {
|
|
340379
|
-
const hit = a2.shape.intersect(b.shape);
|
|
340380
|
-
if (hit.isEmpty()) return { volume: 0 };
|
|
340381
|
-
const volume = hit.volume();
|
|
340382
|
-
return { volume: Number.isFinite(volume) ? volume : 0 };
|
|
340383
|
-
} catch (err2) {
|
|
340384
|
-
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
340385
|
-
return { volume: null, warning: `Could not boolean-test ${a2.name} against ${b.name}: ${message}` };
|
|
340386
|
-
}
|
|
340387
|
-
}
|
|
340388
|
-
function bodyCountForEntry(entry) {
|
|
340389
|
-
if (typeof entry.bodyCount === "number" && Number.isFinite(entry.bodyCount)) {
|
|
340390
|
-
return Math.max(0, Math.round(entry.bodyCount));
|
|
340391
|
-
}
|
|
340392
|
-
return 1;
|
|
340393
|
-
}
|
|
340394
|
-
function makeEdge(entries, sourceIndex, targetIndex, edge) {
|
|
340395
|
-
const source = entries[sourceIndex];
|
|
340396
|
-
const target = entries[targetIndex];
|
|
340397
|
-
return {
|
|
340398
|
-
sourceIndex,
|
|
340399
|
-
targetIndex,
|
|
340400
|
-
sourceId: source.id,
|
|
340401
|
-
targetId: target.id,
|
|
340402
|
-
sourceName: source.name,
|
|
340403
|
-
targetName: target.name,
|
|
340404
|
-
...edge
|
|
340405
|
-
};
|
|
340406
|
-
}
|
|
340407
|
-
function analyzePhysicalConnectivity(entries, rawOptions = {}) {
|
|
340408
|
-
const options = {
|
|
340409
|
-
contactTolerance: rawOptions.contactTolerance ?? DEFAULT_PHYSICAL_CONNECTIVITY_OPTIONS.contactTolerance,
|
|
340410
|
-
minOverlapVolume: rawOptions.minOverlapVolume ?? DEFAULT_PHYSICAL_CONNECTIVITY_OPTIONS.minOverlapVolume,
|
|
340411
|
-
exactGeometry: rawOptions.exactGeometry ?? DEFAULT_PHYSICAL_CONNECTIVITY_OPTIONS.exactGeometry
|
|
340412
|
-
};
|
|
340413
|
-
const warnings = [];
|
|
340414
|
-
const edges = [];
|
|
340415
|
-
const unionFind = new UnionFind(entries.length);
|
|
340416
|
-
for (const pair of collectCandidatePairs(entries, options.contactTolerance)) {
|
|
340417
|
-
const i = pair.sourceIndex;
|
|
340418
|
-
const j = pair.targetIndex;
|
|
340419
|
-
const a2 = entries[i];
|
|
340420
|
-
const b = entries[j];
|
|
340421
|
-
const bboxOverlaps2 = !hasPositiveGap(pair.gaps) && bboxInteriorOverlaps(a2, b);
|
|
340422
|
-
if (options.exactGeometry && bboxOverlaps2) {
|
|
340423
|
-
const overlap = intersectionVolume(a2, b);
|
|
340424
|
-
if (overlap.warning) warnings.push(overlap.warning);
|
|
340425
|
-
if (overlap.volume != null && overlap.volume > options.minOverlapVolume) {
|
|
340426
|
-
unionFind.union(i, j);
|
|
340427
|
-
edges.push(
|
|
340428
|
-
makeEdge(entries, i, j, {
|
|
340429
|
-
kind: "overlap",
|
|
340430
|
-
method: "boolean-intersection",
|
|
340431
|
-
gap: 0,
|
|
340432
|
-
overlapVolume: overlap.volume
|
|
340433
|
-
})
|
|
340434
|
-
);
|
|
340435
|
-
continue;
|
|
340436
|
-
}
|
|
340437
|
-
if (overlap.volume != null && overlap.volume > 0) {
|
|
340438
|
-
unionFind.union(i, j);
|
|
340439
|
-
edges.push(
|
|
340440
|
-
makeEdge(entries, i, j, {
|
|
340441
|
-
kind: "touching",
|
|
340442
|
-
method: "boolean-intersection",
|
|
340443
|
-
gap: 0,
|
|
340444
|
-
overlapVolume: overlap.volume
|
|
340445
|
-
})
|
|
340446
|
-
);
|
|
340447
|
-
continue;
|
|
340448
|
-
}
|
|
340449
|
-
}
|
|
340450
|
-
if (bboxOverlaps2) {
|
|
340451
|
-
unionFind.union(i, j);
|
|
340452
|
-
edges.push(
|
|
340453
|
-
makeEdge(entries, i, j, {
|
|
340454
|
-
kind: "overlap",
|
|
340455
|
-
method: "bbox-overlap",
|
|
340456
|
-
gap: 0,
|
|
340457
|
-
overlapVolume: bboxOverlapVolume(a2, b)
|
|
340458
|
-
})
|
|
340459
|
-
);
|
|
340460
|
-
} else {
|
|
340461
|
-
const contact = contactFromBBoxes(a2, b, options.contactTolerance);
|
|
340462
|
-
if (!contact.touching) continue;
|
|
340463
|
-
unionFind.union(i, j);
|
|
340464
|
-
edges.push(
|
|
340465
|
-
makeEdge(entries, i, j, {
|
|
340466
|
-
kind: "touching",
|
|
340467
|
-
method: "bbox-contact",
|
|
340468
|
-
gap: contact.gap,
|
|
340469
|
-
axis: contact.axis
|
|
340470
|
-
})
|
|
340471
|
-
);
|
|
340472
|
-
}
|
|
340473
|
-
}
|
|
340474
|
-
const objects = entries.map((entry, index2) => ({
|
|
340475
|
-
index: index2,
|
|
340476
|
-
id: entry.id,
|
|
340477
|
-
name: entry.name,
|
|
340478
|
-
groupName: entry.groupName,
|
|
340479
|
-
treePath: entry.treePath,
|
|
340480
|
-
mock: entry.mock === true,
|
|
340481
|
-
bodyCount: bodyCountForEntry(entry),
|
|
340482
|
-
bbox: {
|
|
340483
|
-
min: cloneVec3(entry.min),
|
|
340484
|
-
max: cloneVec3(entry.max)
|
|
340485
|
-
},
|
|
340486
|
-
componentIndex: 0
|
|
340487
|
-
}));
|
|
340488
|
-
const componentByRoot = /* @__PURE__ */ new Map();
|
|
340489
|
-
const rootToComponentIndex = /* @__PURE__ */ new Map();
|
|
340490
|
-
for (let objectIndex = 0; objectIndex < objects.length; objectIndex += 1) {
|
|
340491
|
-
const root = unionFind.find(objectIndex);
|
|
340492
|
-
let component = componentByRoot.get(root);
|
|
340493
|
-
if (!component) {
|
|
340494
|
-
component = {
|
|
340495
|
-
index: componentByRoot.size + 1,
|
|
340496
|
-
objectIndexes: [],
|
|
340497
|
-
objectIds: [],
|
|
340498
|
-
objectNames: [],
|
|
340499
|
-
objectCount: 0,
|
|
340500
|
-
bodyCount: 0,
|
|
340501
|
-
bbox: emptyBBox()
|
|
340502
|
-
};
|
|
340503
|
-
componentByRoot.set(root, component);
|
|
340504
|
-
rootToComponentIndex.set(root, component.index);
|
|
340505
|
-
}
|
|
340506
|
-
const object = objects[objectIndex];
|
|
340507
|
-
object.componentIndex = rootToComponentIndex.get(root) ?? component.index;
|
|
340508
|
-
component.objectIndexes.push(object.index);
|
|
340509
|
-
component.objectIds.push(object.id);
|
|
340510
|
-
component.objectNames.push(object.name);
|
|
340511
|
-
component.objectCount += 1;
|
|
340512
|
-
component.bodyCount += object.bodyCount;
|
|
340513
|
-
expandBBox(component.bbox, object.bbox.min, object.bbox.max);
|
|
340514
|
-
}
|
|
340515
|
-
const components = [...componentByRoot.values()];
|
|
340516
|
-
return {
|
|
340517
|
-
method: options.exactGeometry ? "boolean-overlap-plus-bbox-contact" : "bbox-neighborhood",
|
|
340518
|
-
options,
|
|
340519
|
-
objectCount: objects.length,
|
|
340520
|
-
componentCount: components.length,
|
|
340521
|
-
objects,
|
|
340522
|
-
components,
|
|
340523
|
-
edges,
|
|
340524
|
-
warnings
|
|
340525
|
-
};
|
|
340526
|
-
}
|
|
340527
|
-
const EPSILON = 1e-9;
|
|
340528
|
-
function intervalGap(aMin, aMax, bMin, bMax) {
|
|
340529
|
-
if (aMax < bMin) return bMin - aMax;
|
|
340530
|
-
if (bMax < aMin) return aMin - bMax;
|
|
340531
|
-
return 0;
|
|
340532
|
-
}
|
|
340533
|
-
function bboxGap(a2, b) {
|
|
340534
|
-
const axisGaps = [
|
|
340535
|
-
intervalGap(a2.bbox.min[0], a2.bbox.max[0], b.bbox.min[0], b.bbox.max[0]),
|
|
340536
|
-
intervalGap(a2.bbox.min[1], a2.bbox.max[1], b.bbox.min[1], b.bbox.max[1]),
|
|
340537
|
-
intervalGap(a2.bbox.min[2], a2.bbox.max[2], b.bbox.min[2], b.bbox.max[2])
|
|
340538
|
-
];
|
|
340539
|
-
const gap = Math.sqrt(axisGaps[0] ** 2 + axisGaps[1] ** 2 + axisGaps[2] ** 2);
|
|
340540
|
-
return { gap, axisGaps };
|
|
340541
|
-
}
|
|
340542
|
-
function bboxVolume(component) {
|
|
340543
|
-
const dx = Math.max(0, component.bbox.max[0] - component.bbox.min[0]);
|
|
340544
|
-
const dy = Math.max(0, component.bbox.max[1] - component.bbox.min[1]);
|
|
340545
|
-
const dz = Math.max(0, component.bbox.max[2] - component.bbox.min[2]);
|
|
340546
|
-
return dx * dy * dz;
|
|
340547
|
-
}
|
|
340548
|
-
function compareDefaultRoot(a2, b) {
|
|
340549
|
-
if (a2.bodyCount !== b.bodyCount) return a2.bodyCount - b.bodyCount;
|
|
340550
|
-
if (a2.objectCount !== b.objectCount) return a2.objectCount - b.objectCount;
|
|
340551
|
-
const volumeDelta = bboxVolume(a2) - bboxVolume(b);
|
|
340552
|
-
if (Math.abs(volumeDelta) > EPSILON) return volumeDelta;
|
|
340553
|
-
return b.index - a2.index;
|
|
340554
|
-
}
|
|
340555
|
-
function defaultRootComponentIndex(components) {
|
|
340556
|
-
if (components.length === 0) return null;
|
|
340557
|
-
return components.reduce((best, component) => compareDefaultRoot(component, best) > 0 ? component : best, components[0]).index;
|
|
340558
|
-
}
|
|
340559
|
-
function componentPositionByIndex(components) {
|
|
340560
|
-
return new Map(components.map((component, position) => [component.index, position]));
|
|
340561
|
-
}
|
|
340562
|
-
function computeNearestComponents(components) {
|
|
340563
|
-
const nearest = components.map(() => ({ nearestGap: null, nearestComponentIndex: null }));
|
|
340564
|
-
for (let sourcePosition = 0; sourcePosition < components.length; sourcePosition += 1) {
|
|
340565
|
-
for (let targetPosition = sourcePosition + 1; targetPosition < components.length; targetPosition += 1) {
|
|
340566
|
-
const sourceComponent = components[sourcePosition];
|
|
340567
|
-
const targetComponent = components[targetPosition];
|
|
340568
|
-
const edge = bboxGap(sourceComponent, targetComponent);
|
|
340569
|
-
if (!Number.isFinite(edge.gap)) continue;
|
|
340570
|
-
const source = nearest[sourcePosition];
|
|
340571
|
-
if (source.nearestGap == null || edge.gap < source.nearestGap - EPSILON || Math.abs(edge.gap - source.nearestGap) <= EPSILON && targetComponent.index < (source.nearestComponentIndex ?? Infinity)) {
|
|
340572
|
-
source.nearestGap = edge.gap;
|
|
340573
|
-
source.nearestComponentIndex = targetComponent.index;
|
|
340574
|
-
}
|
|
340575
|
-
const target = nearest[targetPosition];
|
|
340576
|
-
if (target.nearestGap == null || edge.gap < target.nearestGap - EPSILON || Math.abs(edge.gap - target.nearestGap) <= EPSILON && sourceComponent.index < (target.nearestComponentIndex ?? Infinity)) {
|
|
340577
|
-
target.nearestGap = edge.gap;
|
|
340578
|
-
target.nearestComponentIndex = sourceComponent.index;
|
|
340579
|
-
}
|
|
340580
|
-
}
|
|
340581
|
-
}
|
|
340582
|
-
return nearest;
|
|
340583
|
-
}
|
|
340584
|
-
function computeRootDistances(components, rootComponentIndex) {
|
|
340585
|
-
if (rootComponentIndex == null) return [];
|
|
340586
|
-
const positions = componentPositionByIndex(components);
|
|
340587
|
-
const rootPosition = positions.get(rootComponentIndex);
|
|
340588
|
-
if (rootPosition == null) {
|
|
340589
|
-
throw new Error(`rootComponentIndex ${rootComponentIndex} does not match any physical component`);
|
|
340590
|
-
}
|
|
340591
|
-
const visited = components.map(() => false);
|
|
340592
|
-
const distances = components.map(() => Infinity);
|
|
340593
|
-
const parents = components.map(() => null);
|
|
340594
|
-
const parentGaps = components.map(() => null);
|
|
340595
|
-
distances[rootPosition] = 0;
|
|
340596
|
-
for (; ; ) {
|
|
340597
|
-
let current = -1;
|
|
340598
|
-
for (let i = 0; i < components.length; i += 1) {
|
|
340599
|
-
if (visited[i]) continue;
|
|
340600
|
-
if (current === -1 || distances[i] < distances[current] - EPSILON || Math.abs(distances[i] - distances[current]) <= EPSILON && components[i].index < components[current].index) {
|
|
340601
|
-
current = i;
|
|
340602
|
-
}
|
|
340603
|
-
}
|
|
340604
|
-
if (current === -1 || !Number.isFinite(distances[current])) break;
|
|
340605
|
-
visited[current] = true;
|
|
340606
|
-
for (let targetPosition = 0; targetPosition < components.length; targetPosition += 1) {
|
|
340607
|
-
if (visited[targetPosition] || targetPosition === current) continue;
|
|
340608
|
-
const edge = bboxGap(components[current], components[targetPosition]);
|
|
340609
|
-
if (!Number.isFinite(edge.gap)) continue;
|
|
340610
|
-
const nextDistance = distances[current] + edge.gap;
|
|
340611
|
-
if (nextDistance < distances[targetPosition] - EPSILON || Math.abs(nextDistance - distances[targetPosition]) <= EPSILON && components[current].index < (parents[targetPosition] ?? Infinity)) {
|
|
340612
|
-
distances[targetPosition] = nextDistance;
|
|
340613
|
-
parents[targetPosition] = components[current].index;
|
|
340614
|
-
parentGaps[targetPosition] = edge.gap;
|
|
340615
|
-
}
|
|
340616
|
-
}
|
|
340617
|
-
}
|
|
340618
|
-
return components.map((_2, position) => ({
|
|
340619
|
-
rootDistance: distances[position],
|
|
340620
|
-
parentComponentIndex: parents[position],
|
|
340621
|
-
parentGap: parentGaps[position]
|
|
340622
|
-
}));
|
|
340623
|
-
}
|
|
340624
|
-
function makeGapEdge(source, target) {
|
|
340625
|
-
const gap = bboxGap(source, target);
|
|
340626
|
-
return {
|
|
340627
|
-
sourceComponentIndex: source.index,
|
|
340628
|
-
targetComponentIndex: target.index,
|
|
340629
|
-
sourceObjectNames: [...source.objectNames],
|
|
340630
|
-
targetObjectNames: [...target.objectNames],
|
|
340631
|
-
gap: gap.gap,
|
|
340632
|
-
axisGaps: gap.axisGaps
|
|
340633
|
-
};
|
|
340634
|
-
}
|
|
340635
|
-
function compactGapEdges(components, nearest, rooted) {
|
|
340636
|
-
const componentByIndex = new Map(components.map((component) => [component.index, component]));
|
|
340637
|
-
const seen2 = /* @__PURE__ */ new Set();
|
|
340638
|
-
const edges = [];
|
|
340639
|
-
const add2 = (sourceIndex, targetIndex) => {
|
|
340640
|
-
if (targetIndex == null || sourceIndex === targetIndex) return;
|
|
340641
|
-
const source = componentByIndex.get(Math.min(sourceIndex, targetIndex));
|
|
340642
|
-
const target = componentByIndex.get(Math.max(sourceIndex, targetIndex));
|
|
340643
|
-
if (!source || !target) return;
|
|
340644
|
-
const key = `${source.index}:${target.index}`;
|
|
340645
|
-
if (seen2.has(key)) return;
|
|
340646
|
-
seen2.add(key);
|
|
340647
|
-
edges.push(makeGapEdge(source, target));
|
|
340648
|
-
};
|
|
340649
|
-
components.forEach((component, position) => {
|
|
340650
|
-
var _a3, _b3;
|
|
340651
|
-
add2(component.index, ((_a3 = nearest[position]) == null ? void 0 : _a3.nearestComponentIndex) ?? null);
|
|
340652
|
-
add2(component.index, ((_b3 = rooted[position]) == null ? void 0 : _b3.parentComponentIndex) ?? null);
|
|
340653
|
-
});
|
|
340654
|
-
return edges.sort((a2, b) => a2.sourceComponentIndex - b.sourceComponentIndex || a2.targetComponentIndex - b.targetComponentIndex);
|
|
340655
|
-
}
|
|
340656
|
-
function analyzeDistanceInspection(entries, rawOptions = {}) {
|
|
340657
|
-
const connectivity = analyzePhysicalConnectivity(entries, rawOptions);
|
|
340658
|
-
const rootComponentIndex = rawOptions.rootComponentIndex ?? defaultRootComponentIndex(connectivity.components);
|
|
340659
|
-
const nearest = computeNearestComponents(connectivity.components);
|
|
340660
|
-
const rooted = computeRootDistances(connectivity.components, rootComponentIndex);
|
|
340661
|
-
const gapEdges = compactGapEdges(connectivity.components, nearest, rooted);
|
|
340662
|
-
const componentByIndex = /* @__PURE__ */ new Map();
|
|
340663
|
-
const components = connectivity.components.map((component, position) => {
|
|
340664
|
-
var _a3, _b3;
|
|
340665
|
-
const rootData = rooted[position] ?? {
|
|
340666
|
-
rootDistance: rootComponentIndex === component.index ? 0 : Infinity,
|
|
340667
|
-
parentComponentIndex: null,
|
|
340668
|
-
parentGap: null
|
|
340669
|
-
};
|
|
340670
|
-
const decorated = {
|
|
340671
|
-
...component,
|
|
340672
|
-
isRoot: component.index === rootComponentIndex,
|
|
340673
|
-
rootDistance: rootData.rootDistance,
|
|
340674
|
-
nearestGap: ((_a3 = nearest[position]) == null ? void 0 : _a3.nearestGap) ?? null,
|
|
340675
|
-
nearestComponentIndex: ((_b3 = nearest[position]) == null ? void 0 : _b3.nearestComponentIndex) ?? null,
|
|
340676
|
-
parentComponentIndex: rootData.parentComponentIndex,
|
|
340677
|
-
parentGap: rootData.parentGap
|
|
340678
|
-
};
|
|
340679
|
-
componentByIndex.set(component.index, decorated);
|
|
340680
|
-
return decorated;
|
|
340681
|
-
});
|
|
340682
|
-
const objects = connectivity.objects.map((object) => {
|
|
340683
|
-
const component = componentByIndex.get(object.componentIndex);
|
|
340684
|
-
return {
|
|
340685
|
-
...object,
|
|
340686
|
-
rootDistance: (component == null ? void 0 : component.rootDistance) ?? Infinity,
|
|
340687
|
-
nearestGap: (component == null ? void 0 : component.nearestGap) ?? null,
|
|
340688
|
-
nearestComponentIndex: (component == null ? void 0 : component.nearestComponentIndex) ?? null,
|
|
340689
|
-
parentComponentIndex: (component == null ? void 0 : component.parentComponentIndex) ?? null,
|
|
340690
|
-
parentGap: (component == null ? void 0 : component.parentGap) ?? null
|
|
340691
|
-
};
|
|
340692
|
-
});
|
|
340693
|
-
const finiteDistances = components.map((component) => component.rootDistance).filter(Number.isFinite);
|
|
340694
|
-
const maxRootDistance = finiteDistances.length > 0 ? Math.max(...finiteDistances) : 0;
|
|
340695
|
-
return {
|
|
340696
|
-
method: "physical-component-bbox-gap-graph",
|
|
340697
|
-
distanceMethod: "axis-aligned-bbox-gap",
|
|
340698
|
-
options: {
|
|
340699
|
-
contactTolerance: connectivity.options.contactTolerance,
|
|
340700
|
-
minOverlapVolume: connectivity.options.minOverlapVolume,
|
|
340701
|
-
rootComponentIndex
|
|
340702
|
-
},
|
|
340703
|
-
objectCount: connectivity.objectCount,
|
|
340704
|
-
componentCount: connectivity.componentCount,
|
|
340705
|
-
rootComponentIndex,
|
|
340706
|
-
maxRootDistance,
|
|
340707
|
-
gapEdgeCount: connectivity.components.length * (connectivity.components.length - 1) / 2,
|
|
340708
|
-
objects,
|
|
340709
|
-
components,
|
|
340710
|
-
gapEdges,
|
|
340711
|
-
connectivity: {
|
|
340712
|
-
method: connectivity.method,
|
|
340713
|
-
edges: connectivity.edges
|
|
340714
|
-
},
|
|
340715
|
-
warnings: [...connectivity.warnings]
|
|
340716
|
-
};
|
|
340717
|
-
}
|
|
340718
342073
|
export {
|
|
340719
342074
|
Matrix4 as $,
|
|
340720
342075
|
ACESFilmicToneMapping as A,
|
|
@@ -340810,7 +342165,7 @@ export {
|
|
|
340810
342165
|
DEFAULT_ACTIVE_BACKEND as ay,
|
|
340811
342166
|
isConstraintSketch as az,
|
|
340812
342167
|
PCFShadowMap as b,
|
|
340813
|
-
|
|
342168
|
+
initKernelManifoldOnly as b$,
|
|
340814
342169
|
PointLight as b0,
|
|
340815
342170
|
DirectionalLight as b1,
|
|
340816
342171
|
analyzeCollisionIntersections as b2,
|
|
@@ -340821,64 +342176,61 @@ export {
|
|
|
340821
342176
|
runScript as b7,
|
|
340822
342177
|
MeshPhysicalMaterial as b8,
|
|
340823
342178
|
LineSegments as b9,
|
|
340824
|
-
|
|
340825
|
-
|
|
340826
|
-
|
|
340827
|
-
|
|
340828
|
-
|
|
340829
|
-
|
|
340830
|
-
|
|
340831
|
-
|
|
340832
|
-
|
|
340833
|
-
|
|
340834
|
-
|
|
340835
|
-
|
|
340836
|
-
|
|
340837
|
-
|
|
340838
|
-
|
|
340839
|
-
|
|
342179
|
+
findJointAnimationClip as bA,
|
|
342180
|
+
resolveJointAnimation as bB,
|
|
342181
|
+
resolveJointViewValues as bC,
|
|
342182
|
+
getShapePorts as bD,
|
|
342183
|
+
getShapeUsedPorts as bE,
|
|
342184
|
+
DEFAULT_VIEW_CONFIG as bF,
|
|
342185
|
+
getKernelFaceNameForTriangle as bG,
|
|
342186
|
+
initKernel as bH,
|
|
342187
|
+
initSolverWasm as bI,
|
|
342188
|
+
BoxGeometry as bJ,
|
|
342189
|
+
localAabbPlaneRelation as bK,
|
|
342190
|
+
ShapeUtils as bL,
|
|
342191
|
+
aabbInteriorOverlaps as bM,
|
|
342192
|
+
aabbOverlapVolume as bN,
|
|
342193
|
+
AabbSpatialIndex as bO,
|
|
342194
|
+
aabbGaps as bP,
|
|
340840
342195
|
Group as bQ,
|
|
340841
|
-
|
|
340842
|
-
|
|
340843
|
-
|
|
340844
|
-
|
|
340845
|
-
|
|
340846
|
-
|
|
340847
|
-
|
|
340848
|
-
|
|
340849
|
-
|
|
340850
|
-
|
|
340851
|
-
|
|
340852
|
-
|
|
340853
|
-
|
|
340854
|
-
|
|
340855
|
-
|
|
340856
|
-
|
|
340857
|
-
|
|
340858
|
-
|
|
340859
|
-
|
|
340860
|
-
|
|
340861
|
-
|
|
340862
|
-
|
|
340863
|
-
|
|
340864
|
-
|
|
340865
|
-
|
|
340866
|
-
|
|
340867
|
-
|
|
340868
|
-
|
|
340869
|
-
|
|
340870
|
-
|
|
340871
|
-
|
|
340872
|
-
|
|
340873
|
-
|
|
340874
|
-
|
|
340875
|
-
|
|
340876
|
-
|
|
342196
|
+
intersectWithPlane as bR,
|
|
342197
|
+
parseCameraCliSpec as bS,
|
|
342198
|
+
PMREMGenerator as bT,
|
|
342199
|
+
PointsMaterial as bU,
|
|
342200
|
+
Points$1 as bV,
|
|
342201
|
+
serializeCollisionFinding as bW,
|
|
342202
|
+
worldAuthorPlaneToLocal as bX,
|
|
342203
|
+
generateCuttingLayoutPdf as bY,
|
|
342204
|
+
getCameraForwardVector as bZ,
|
|
342205
|
+
RENDER_STYLE_OPTIONS as b_,
|
|
342206
|
+
getRenderStylePreset as ba,
|
|
342207
|
+
AdditiveBlending as bb,
|
|
342208
|
+
CatmullRomCurve3 as bc,
|
|
342209
|
+
TubeGeometry as bd,
|
|
342210
|
+
MeshStandardMaterial as be,
|
|
342211
|
+
compileSdfNode3 as bf,
|
|
342212
|
+
buildSdfRaymarchFragmentShader as bg,
|
|
342213
|
+
SDF_RAYMARCH_PROXY_VERTEX_SHADER as bh,
|
|
342214
|
+
Shape2 as bi,
|
|
342215
|
+
ShapeGeometry as bj,
|
|
342216
|
+
ShaderLib as bk,
|
|
342217
|
+
CylinderGeometry as bl,
|
|
342218
|
+
parseViewportCameraState as bm,
|
|
342219
|
+
createResolvedExplodeConfig as bn,
|
|
342220
|
+
explodeBoundsCenter as bo,
|
|
342221
|
+
explodeMergeBounds as bp,
|
|
342222
|
+
resolveExplodeDirective as bq,
|
|
342223
|
+
computeExplodeMotion as br,
|
|
342224
|
+
getSketchWorldMatrix as bs,
|
|
342225
|
+
explodeAdd as bt,
|
|
342226
|
+
hasExplodeOverride as bu,
|
|
342227
|
+
resolveExplodeLocalFanDirection as bv,
|
|
342228
|
+
explodeMul as bw,
|
|
342229
|
+
explodeLeafFanStage as bx,
|
|
342230
|
+
normalizeCutPlane as by,
|
|
342231
|
+
toClippingPlane as bz,
|
|
340877
342232
|
SRGBColorSpace as c,
|
|
340878
|
-
|
|
340879
|
-
RENDER_STYLE_OPTIONS as c1,
|
|
340880
|
-
initKernelManifoldOnly as c2,
|
|
340881
|
-
__viteBrowserExternal$1 as c3,
|
|
342233
|
+
__viteBrowserExternal$1 as c0,
|
|
340882
342234
|
Layers as d,
|
|
340883
342235
|
Color as e,
|
|
340884
342236
|
RGBAFormat as f,
|