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
|
@@ -10526,7 +10526,7 @@ let _wasm$1 = null;
|
|
|
10526
10526
|
async function initManifoldWasm() {
|
|
10527
10527
|
if (_wasm$1) return _wasm$1;
|
|
10528
10528
|
performance.mark("manifold:start");
|
|
10529
|
-
const Module = (await import("./manifold-
|
|
10529
|
+
const Module = (await import("./manifold-sJ-axdXM.js")).default;
|
|
10530
10530
|
performance.mark("manifold:imported");
|
|
10531
10531
|
const wasm = await Module();
|
|
10532
10532
|
wasm.setup();
|
|
@@ -10658,6 +10658,23 @@ let ManifoldShapeBackend = _ManifoldShapeBackend;
|
|
|
10658
10658
|
function wrapManifoldShapeBackend(manifold) {
|
|
10659
10659
|
return new ManifoldShapeBackend(manifold);
|
|
10660
10660
|
}
|
|
10661
|
+
function reconstructBackendFromMesh(mesh) {
|
|
10662
|
+
const wasm = getManifoldWasm();
|
|
10663
|
+
const wasmMesh = new wasm.Mesh({
|
|
10664
|
+
numProp: mesh.numProp,
|
|
10665
|
+
triVerts: mesh.triVerts,
|
|
10666
|
+
vertProperties: mesh.vertProperties,
|
|
10667
|
+
mergeFromVert: mesh.mergeFromVert.length > 0 ? mesh.mergeFromVert : void 0,
|
|
10668
|
+
mergeToVert: mesh.mergeToVert.length > 0 ? mesh.mergeToVert : void 0
|
|
10669
|
+
});
|
|
10670
|
+
let manifold;
|
|
10671
|
+
try {
|
|
10672
|
+
manifold = new wasm.Manifold(wasmMesh);
|
|
10673
|
+
} catch {
|
|
10674
|
+
manifold = wasm.Manifold.cube([0, 0, 0]);
|
|
10675
|
+
}
|
|
10676
|
+
return new ManifoldShapeBackend(manifold);
|
|
10677
|
+
}
|
|
10661
10678
|
function requireManifoldShapeBackend(backend, apiName = "requireManifoldShapeBackend()") {
|
|
10662
10679
|
if (isManifoldCapableBackend(backend)) {
|
|
10663
10680
|
return backend.requireManifold(apiName);
|
|
@@ -49570,7 +49587,7 @@ function spurGear(options) {
|
|
|
49570
49587
|
});
|
|
49571
49588
|
return attachGearMeta(shapeWithConnectors, meta2);
|
|
49572
49589
|
}
|
|
49573
|
-
function requirePositive$
|
|
49590
|
+
function requirePositive$8(scope, name, value) {
|
|
49574
49591
|
if (!isFinitePositive(value)) throw new Error(`${scope}: "${name}" must be > 0`);
|
|
49575
49592
|
}
|
|
49576
49593
|
function requireOptionalBore(scope, boreDiameter, maxDiameter) {
|
|
@@ -49592,8 +49609,8 @@ function cutBore$1(shape, boreDiameter) {
|
|
|
49592
49609
|
return shape.subtract(cutter);
|
|
49593
49610
|
}
|
|
49594
49611
|
function gearBodyDisk(options) {
|
|
49595
|
-
requirePositive$
|
|
49596
|
-
requirePositive$
|
|
49612
|
+
requirePositive$8("gearBodyDisk", "outerRadius", options.outerRadius);
|
|
49613
|
+
requirePositive$8("gearBodyDisk", "faceWidth", options.faceWidth);
|
|
49597
49614
|
const bore = requireOptionalBore("gearBodyDisk", options.boreDiameter, options.outerRadius * 2);
|
|
49598
49615
|
const segments = resolveSegments(options.segments);
|
|
49599
49616
|
const outer = circle2d(options.outerRadius, segments);
|
|
@@ -49601,14 +49618,14 @@ function gearBodyDisk(options) {
|
|
|
49601
49618
|
return sketchExtrude(profile, options.faceWidth);
|
|
49602
49619
|
}
|
|
49603
49620
|
function gearBodyDiskWithHub(options) {
|
|
49604
|
-
requirePositive$
|
|
49621
|
+
requirePositive$8("gearBodyDiskWithHub", "hubDiameter", options.hubDiameter);
|
|
49605
49622
|
if (options.hubDiameter >= options.outerRadius * 2) {
|
|
49606
49623
|
throw new Error('gearBodyDiskWithHub: "hubDiameter" must be smaller than the outer diameter');
|
|
49607
49624
|
}
|
|
49608
49625
|
const bore = requireOptionalBore("gearBodyDiskWithHub", options.boreDiameter, options.hubDiameter);
|
|
49609
49626
|
const base = gearBodyDisk({ ...options, boreDiameter: 0 });
|
|
49610
49627
|
const hubFaceWidth = options.hubFaceWidth ?? options.faceWidth * 1.5;
|
|
49611
|
-
requirePositive$
|
|
49628
|
+
requirePositive$8("gearBodyDiskWithHub", "hubFaceWidth", hubFaceWidth);
|
|
49612
49629
|
const hub = cylinder(hubFaceWidth, options.hubDiameter * 0.5, void 0, options.segments).translate(
|
|
49613
49630
|
0,
|
|
49614
49631
|
0,
|
|
@@ -49617,11 +49634,11 @@ function gearBodyDiskWithHub(options) {
|
|
|
49617
49634
|
return cutBore$1(base.add(hub), bore);
|
|
49618
49635
|
}
|
|
49619
49636
|
function gearBodySpoked(options) {
|
|
49620
|
-
requirePositive$
|
|
49621
|
-
requirePositive$
|
|
49622
|
-
requirePositive$
|
|
49623
|
-
requirePositive$
|
|
49624
|
-
requirePositive$
|
|
49637
|
+
requirePositive$8("gearBodySpoked", "outerRadius", options.outerRadius);
|
|
49638
|
+
requirePositive$8("gearBodySpoked", "faceWidth", options.faceWidth);
|
|
49639
|
+
requirePositive$8("gearBodySpoked", "rimWidth", options.rimWidth);
|
|
49640
|
+
requirePositive$8("gearBodySpoked", "hubDiameter", options.hubDiameter);
|
|
49641
|
+
requirePositive$8("gearBodySpoked", "spokeWidth", options.spokeWidth);
|
|
49625
49642
|
if (!Number.isInteger(options.spokeCount) || options.spokeCount < 2) {
|
|
49626
49643
|
throw new Error('gearBodySpoked: "spokeCount" must be an integer >= 2');
|
|
49627
49644
|
}
|
|
@@ -49644,12 +49661,12 @@ function gearBodySpoked(options) {
|
|
|
49644
49661
|
}
|
|
49645
49662
|
function gearBodyFromProfile(profile, options) {
|
|
49646
49663
|
if (!(profile instanceof Sketch)) throw new Error('gearBodyFromProfile: "profile" must be a Sketch');
|
|
49647
|
-
requirePositive$
|
|
49664
|
+
requirePositive$8("gearBodyFromProfile", "faceWidth", options.faceWidth);
|
|
49648
49665
|
const bore = options.boreDiameter ?? 0;
|
|
49649
49666
|
if (!Number.isFinite(bore) || bore < 0) throw new Error('gearBodyFromProfile: "boreDiameter" must be >= 0');
|
|
49650
49667
|
return cutBore$1(sketchExtrude(profile, options.faceWidth), bore);
|
|
49651
49668
|
}
|
|
49652
|
-
function requirePositive$
|
|
49669
|
+
function requirePositive$7(scope, name, value) {
|
|
49653
49670
|
if (!isFinitePositive(value)) throw new Error(`${scope}: "${name}" must be > 0`);
|
|
49654
49671
|
}
|
|
49655
49672
|
function requireFiniteAngle(scope, name, value) {
|
|
@@ -49711,7 +49728,7 @@ function buildSpurTeethRegion(options, name, faceWidth) {
|
|
|
49711
49728
|
}
|
|
49712
49729
|
function buildSolidArcRegion(options, name, faceWidth) {
|
|
49713
49730
|
const scope = "driveWheel.addSolidArcBetween";
|
|
49714
|
-
requirePositive$
|
|
49731
|
+
requirePositive$7(scope, "outerRadius", options.outerRadius);
|
|
49715
49732
|
const innerRadius = options.innerRadius ?? 0;
|
|
49716
49733
|
if (!Number.isFinite(innerRadius) || innerRadius < 0) throw new Error(`${scope}: "innerRadius" must be >= 0`);
|
|
49717
49734
|
if (innerRadius >= options.outerRadius) throw new Error(`${scope}: "innerRadius" must be smaller than "outerRadius"`);
|
|
@@ -49777,7 +49794,7 @@ class DriveWheelBuilder {
|
|
|
49777
49794
|
__publicField(this, "boreDiameter");
|
|
49778
49795
|
__publicField(this, "regions", []);
|
|
49779
49796
|
if (options.body !== void 0 && !(options.body instanceof Shape)) throw new Error('driveWheel: "body" must be a Shape');
|
|
49780
|
-
if (options.faceWidth !== void 0) requirePositive$
|
|
49797
|
+
if (options.faceWidth !== void 0) requirePositive$7("driveWheel", "faceWidth", options.faceWidth);
|
|
49781
49798
|
const boreDiameter = options.boreDiameter ?? 0;
|
|
49782
49799
|
if (!Number.isFinite(boreDiameter) || boreDiameter < 0) throw new Error('driveWheel: "boreDiameter" must be >= 0');
|
|
49783
49800
|
this.body = options.body;
|
|
@@ -49812,7 +49829,7 @@ class DriveWheelBuilder {
|
|
|
49812
49829
|
if (options.innerRadius !== void 0 && (!Number.isFinite(options.innerRadius) || options.innerRadius < 0)) {
|
|
49813
49830
|
throw new Error(`${scope}: "innerRadius" must be >= 0`);
|
|
49814
49831
|
}
|
|
49815
|
-
if (options.outerRadius !== void 0) requirePositive$
|
|
49832
|
+
if (options.outerRadius !== void 0) requirePositive$7(scope, "outerRadius", options.outerRadius);
|
|
49816
49833
|
this.regions.push({
|
|
49817
49834
|
shape: shape.clone(),
|
|
49818
49835
|
meta: {
|
|
@@ -49878,7 +49895,7 @@ class DriveWheelBuilder {
|
|
|
49878
49895
|
resolveFaceWidth(scope, localFaceWidth) {
|
|
49879
49896
|
const faceWidth = localFaceWidth ?? this.faceWidth;
|
|
49880
49897
|
if (faceWidth === void 0) throw new Error(`${scope}: "faceWidth" is required unless driveWheel({ faceWidth }) was set`);
|
|
49881
|
-
requirePositive$
|
|
49898
|
+
requirePositive$7(scope, "faceWidth", faceWidth);
|
|
49882
49899
|
if (this.faceWidth !== void 0 && localFaceWidth !== void 0 && Math.abs(this.faceWidth - localFaceWidth) > EPSILON$1) {
|
|
49883
49900
|
throw new Error(`${scope}: region faceWidth must match driveWheel faceWidth`);
|
|
49884
49901
|
}
|
|
@@ -51031,6 +51048,1867 @@ function washer(size, options) {
|
|
|
51031
51048
|
const bore = cylinder(dims.t + 1, dims.id / 2, void 0, segs);
|
|
51032
51049
|
return outer.subtract(bore);
|
|
51033
51050
|
}
|
|
51051
|
+
function requirePositive$6(value, name) {
|
|
51052
|
+
if (!Number.isFinite(value) || value <= 0) throw new Error(`${name} must be a positive finite number`);
|
|
51053
|
+
return value;
|
|
51054
|
+
}
|
|
51055
|
+
function requireNonNegative(value, name) {
|
|
51056
|
+
if (!Number.isFinite(value) || value < 0) throw new Error(`${name} must be a non-negative finite number`);
|
|
51057
|
+
return value;
|
|
51058
|
+
}
|
|
51059
|
+
function metricWasherSizeForPin(pinDiameter) {
|
|
51060
|
+
if (pinDiameter <= 2) return "M2";
|
|
51061
|
+
if (pinDiameter <= 2.5) return "M2.5";
|
|
51062
|
+
if (pinDiameter <= 3) return "M3";
|
|
51063
|
+
if (pinDiameter <= 4) return "M4";
|
|
51064
|
+
if (pinDiameter <= 5) return "M5";
|
|
51065
|
+
if (pinDiameter <= 6) return "M6";
|
|
51066
|
+
if (pinDiameter <= 8) return "M8";
|
|
51067
|
+
return "M10";
|
|
51068
|
+
}
|
|
51069
|
+
function cylinderAlongX(length4, radius, xCenter, segments) {
|
|
51070
|
+
return cylinder(length4, radius, void 0, segments).pointAlong([1, 0, 0]).translate(xCenter - length4 / 2, 0, 0);
|
|
51071
|
+
}
|
|
51072
|
+
function tubeAlongX(length4, outerRadius, innerRadius, xCenter, segments) {
|
|
51073
|
+
return cylinderAlongX(length4, outerRadius, xCenter, segments).subtract(cylinderAlongX(length4 + 0.4, innerRadius, xCenter, segments));
|
|
51074
|
+
}
|
|
51075
|
+
function cylinderAlongY(length4, radius, yCenter, segments) {
|
|
51076
|
+
return cylinder(length4, radius, void 0, segments).pointAlong([0, 1, 0]).translate(0, yCenter - length4 / 2, 0);
|
|
51077
|
+
}
|
|
51078
|
+
function tubeAlongY(length4, outerRadius, innerRadius, yCenter, segments) {
|
|
51079
|
+
return cylinderAlongY(length4, outerRadius, yCenter, segments).subtract(cylinderAlongY(length4 + 0.4, innerRadius, yCenter, segments));
|
|
51080
|
+
}
|
|
51081
|
+
function tubeAlongZ(height, outerRadius, innerRadius, segments) {
|
|
51082
|
+
return cylinder(height, outerRadius, void 0, segments).subtract(
|
|
51083
|
+
cylinder(height + 0.4, innerRadius, void 0, segments).translate(0, 0, -0.2)
|
|
51084
|
+
);
|
|
51085
|
+
}
|
|
51086
|
+
function washerAlongX(size, xCenter, segments) {
|
|
51087
|
+
const dims = WASHER_TABLE[size];
|
|
51088
|
+
return washer(size, { segments }).pointAlong([1, 0, 0]).translate(xCenter - dims.t / 2, 0, 0);
|
|
51089
|
+
}
|
|
51090
|
+
function resolveBoltInset(raw, fallback) {
|
|
51091
|
+
if (raw === void 0) return [fallback, fallback];
|
|
51092
|
+
if (typeof raw === "number") return [requirePositive$6(raw, "boltInset"), requirePositive$6(raw, "boltInset")];
|
|
51093
|
+
if (raw.length !== 2) throw new Error("boltInset tuple must be [x, y]");
|
|
51094
|
+
return [requirePositive$6(raw[0], "boltInset[0]"), requirePositive$6(raw[1], "boltInset[1]")];
|
|
51095
|
+
}
|
|
51096
|
+
function validateBoltPositionsForServiceCover(args) {
|
|
51097
|
+
args.positions.forEach(([x2, y2], index2) => {
|
|
51098
|
+
if (!Number.isFinite(x2) || !Number.isFinite(y2)) {
|
|
51099
|
+
throw new Error(`boltedServiceCover: boltPositions[${index2}] must contain finite numbers`);
|
|
51100
|
+
}
|
|
51101
|
+
if (Math.abs(x2) + args.holeRadius >= args.coverWidth / 2 || Math.abs(y2) + args.holeRadius >= args.coverDepth / 2) {
|
|
51102
|
+
throw new Error(`boltedServiceCover: boltPositions[${index2}] is too close to the cover edge`);
|
|
51103
|
+
}
|
|
51104
|
+
const overlapsOpening = Math.abs(x2) - args.holeRadius <= args.openingWidth / 2 && Math.abs(y2) - args.holeRadius <= args.openingDepth / 2;
|
|
51105
|
+
if (overlapsOpening) {
|
|
51106
|
+
throw new Error(
|
|
51107
|
+
`boltedServiceCover: boltPositions[${index2}] lands over the service opening; decrease boltInset, increase ledgeWidth, or provide a smaller opening`
|
|
51108
|
+
);
|
|
51109
|
+
}
|
|
51110
|
+
});
|
|
51111
|
+
}
|
|
51112
|
+
function placeCutterAtPositions(cutter, positions, z2) {
|
|
51113
|
+
return union(...positions.map(([x2, y2]) => cutter.translate(x2, y2, z2)));
|
|
51114
|
+
}
|
|
51115
|
+
function boltedServiceCover(options) {
|
|
51116
|
+
const width = requirePositive$6(options.width, "width");
|
|
51117
|
+
const depth = requirePositive$6(options.depth, "depth");
|
|
51118
|
+
const coverThickness = requirePositive$6(options.coverThickness ?? 3, "coverThickness");
|
|
51119
|
+
const parentThickness = requirePositive$6(options.parentThickness ?? 8, "parentThickness");
|
|
51120
|
+
const ledgeWidth = requirePositive$6(options.ledgeWidth ?? 8, "ledgeWidth");
|
|
51121
|
+
const gasketThickness = Math.max(0, options.gasketThickness ?? 0.8);
|
|
51122
|
+
const gasketInset = Math.max(0, options.gasketInset ?? 2);
|
|
51123
|
+
const screwSize = options.screwSize ?? "M4";
|
|
51124
|
+
const segments = options.segments ?? 36;
|
|
51125
|
+
const sizeData = METRIC_HOLE_TABLE[screwSize];
|
|
51126
|
+
if (!sizeData) throw new Error(`boltedServiceCover: unsupported screwSize "${screwSize}"`);
|
|
51127
|
+
const screwLength = requirePositive$6(
|
|
51128
|
+
options.screwLength ?? parentThickness + gasketThickness + coverThickness + 4,
|
|
51129
|
+
"screwLength"
|
|
51130
|
+
);
|
|
51131
|
+
const coverFit = options.coverFit ?? "normal";
|
|
51132
|
+
const counterboreEnabled = options.counterbore ?? true;
|
|
51133
|
+
const [insetX, insetY] = resolveBoltInset(options.boltInset, Math.max(ledgeWidth * 0.65, sizeData.head * 0.75));
|
|
51134
|
+
if (insetX * 2 >= width || insetY * 2 >= depth) {
|
|
51135
|
+
throw new Error("boltedServiceCover: boltInset leaves no room for a four-corner bolt pattern");
|
|
51136
|
+
}
|
|
51137
|
+
const boltPositions = options.boltPositions ?? [
|
|
51138
|
+
[-width / 2 + insetX, -depth / 2 + insetY],
|
|
51139
|
+
[width / 2 - insetX, -depth / 2 + insetY],
|
|
51140
|
+
[-width / 2 + insetX, depth / 2 - insetY],
|
|
51141
|
+
[width / 2 - insetX, depth / 2 - insetY]
|
|
51142
|
+
];
|
|
51143
|
+
if (boltPositions.length === 0) throw new Error("boltedServiceCover: boltPositions must contain at least one point");
|
|
51144
|
+
const parentWidth = width + ledgeWidth * 2;
|
|
51145
|
+
const parentDepth = depth + ledgeWidth * 2;
|
|
51146
|
+
const openingWidth = Math.max(1, width - ledgeWidth * 2);
|
|
51147
|
+
const openingDepth = Math.max(1, depth - ledgeWidth * 2);
|
|
51148
|
+
validateBoltPositionsForServiceCover({
|
|
51149
|
+
positions: boltPositions,
|
|
51150
|
+
coverWidth: width,
|
|
51151
|
+
coverDepth: depth,
|
|
51152
|
+
openingWidth,
|
|
51153
|
+
openingDepth,
|
|
51154
|
+
holeRadius: sizeData[coverFit] / 2
|
|
51155
|
+
});
|
|
51156
|
+
const coverHole = fastenerHole({
|
|
51157
|
+
size: screwSize,
|
|
51158
|
+
fit: coverFit,
|
|
51159
|
+
depth: coverThickness + 0.6,
|
|
51160
|
+
center: true,
|
|
51161
|
+
segments,
|
|
51162
|
+
...counterboreEnabled ? { counterbore: { depth: Math.min(coverThickness * 0.6, Math.max(0.6, coverThickness - 0.4)) } } : {}
|
|
51163
|
+
});
|
|
51164
|
+
const parentTap = fastenerHole({ size: screwSize, fit: "tap", depth: parentThickness + 0.6, center: true, segments });
|
|
51165
|
+
const parentThreadEnvelope = fastenerHole({
|
|
51166
|
+
size: screwSize,
|
|
51167
|
+
fit: "close",
|
|
51168
|
+
depth: parentThickness + 0.6,
|
|
51169
|
+
center: true,
|
|
51170
|
+
segments
|
|
51171
|
+
});
|
|
51172
|
+
const openingCutter = box(openingWidth, openingDepth, parentThickness + 1).translate(0, 0, -0.5);
|
|
51173
|
+
const parentTappedPattern = placeCutterAtPositions(parentTap, boltPositions, parentThickness / 2);
|
|
51174
|
+
const parentThreadEnvelopePattern = placeCutterAtPositions(parentThreadEnvelope, boltPositions, parentThickness / 2);
|
|
51175
|
+
const parent = box(parentWidth, parentDepth, parentThickness).subtract(openingCutter).subtract(parentThreadEnvelopePattern).color("#4b5563");
|
|
51176
|
+
let coverBlank = box(width, depth, coverThickness);
|
|
51177
|
+
if (options.pullTabs ?? true) {
|
|
51178
|
+
const tabWidth = Math.min(width * 0.18, Math.max(sizeData.head * 1.6, 12));
|
|
51179
|
+
const tabDepth = Math.max(4, coverThickness * 1.4);
|
|
51180
|
+
const tabOverlap = Math.min(0.5, tabDepth * 0.25);
|
|
51181
|
+
const tabY = -depth / 2 - tabDepth / 2 + tabOverlap;
|
|
51182
|
+
const tabX = width * 0.23;
|
|
51183
|
+
coverBlank = union(
|
|
51184
|
+
coverBlank,
|
|
51185
|
+
box(tabWidth, tabDepth, coverThickness).translate(-tabX, tabY, 0),
|
|
51186
|
+
box(tabWidth, tabDepth, coverThickness).translate(tabX, tabY, 0)
|
|
51187
|
+
);
|
|
51188
|
+
}
|
|
51189
|
+
const coverClearancePattern = placeCutterAtPositions(coverHole, boltPositions, coverThickness / 2);
|
|
51190
|
+
const cover = coverBlank.subtract(coverClearancePattern).translate(0, 0, parentThickness + gasketThickness).color("#334155");
|
|
51191
|
+
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;
|
|
51192
|
+
const hardware = fastenerSet(screwSize, screwLength, { washerUnderHead: false, washerUnderNut: false, fit: coverFit, segments });
|
|
51193
|
+
const screwOriginZ = parentThickness + gasketThickness + coverThickness;
|
|
51194
|
+
const screws = boltPositions.map(([x2, y2]) => hardware.bolt.translate(x2, y2, screwOriginZ).color("#94a3b8"));
|
|
51195
|
+
const parts = [
|
|
51196
|
+
{ name: "service cover parent ledge with threaded hole envelopes", shape: parent },
|
|
51197
|
+
...gasket ? [{ name: "service cover gasket seated on ledge", shape: gasket }] : [],
|
|
51198
|
+
{ name: "bolted service cover plate with fused pull tabs", shape: cover },
|
|
51199
|
+
...screws.map((shape, index2) => ({ name: `installed ${screwSize} cover screw ${index2 + 1}`, shape }))
|
|
51200
|
+
];
|
|
51201
|
+
return {
|
|
51202
|
+
parts,
|
|
51203
|
+
parent,
|
|
51204
|
+
cover,
|
|
51205
|
+
gasket,
|
|
51206
|
+
screws,
|
|
51207
|
+
boltPositions,
|
|
51208
|
+
cutters: {
|
|
51209
|
+
coverClearance: coverClearancePattern,
|
|
51210
|
+
parentTapped: parentTappedPattern,
|
|
51211
|
+
parentThreadEnvelope: parentThreadEnvelopePattern
|
|
51212
|
+
},
|
|
51213
|
+
dims: {
|
|
51214
|
+
width,
|
|
51215
|
+
depth,
|
|
51216
|
+
coverThickness,
|
|
51217
|
+
parentThickness,
|
|
51218
|
+
ledgeWidth,
|
|
51219
|
+
gasketThickness,
|
|
51220
|
+
screwSize,
|
|
51221
|
+
screwLength,
|
|
51222
|
+
clearanceDia: sizeData[coverFit],
|
|
51223
|
+
tapDia: sizeData.tap,
|
|
51224
|
+
threadEnvelopeDia: sizeData.close
|
|
51225
|
+
}
|
|
51226
|
+
};
|
|
51227
|
+
}
|
|
51228
|
+
function datumEnclosureAssembly(options) {
|
|
51229
|
+
const width = requirePositive$6(options.width, "width");
|
|
51230
|
+
const depth = requirePositive$6(options.depth, "depth");
|
|
51231
|
+
const height = requirePositive$6(options.height, "height");
|
|
51232
|
+
const wallThickness = requirePositive$6(options.wallThickness ?? 2.4, "wallThickness");
|
|
51233
|
+
const baseThickness = requirePositive$6(options.baseThickness ?? wallThickness, "baseThickness");
|
|
51234
|
+
const coverThickness = requirePositive$6(options.coverThickness ?? 2.4, "coverThickness");
|
|
51235
|
+
const ledgeWidth = requirePositive$6(options.ledgeWidth ?? Math.max(3.6, wallThickness * 1.35), "ledgeWidth");
|
|
51236
|
+
const gasketThickness = requireNonNegative(options.gasketThickness ?? 0.8, "gasketThickness");
|
|
51237
|
+
const faceClearance = requirePositive$6(options.faceClearance ?? 0.04, "faceClearance");
|
|
51238
|
+
const screwSize = options.screwSize ?? "M3";
|
|
51239
|
+
const coverFit = options.coverFit ?? "normal";
|
|
51240
|
+
const segments = options.segments ?? 32;
|
|
51241
|
+
const sizeData = METRIC_HOLE_TABLE[screwSize];
|
|
51242
|
+
if (!sizeData) throw new Error(`datumEnclosureAssembly: unsupported screwSize "${screwSize}"`);
|
|
51243
|
+
const innerWidth = width - wallThickness * 2;
|
|
51244
|
+
const innerDepth = depth - wallThickness * 2;
|
|
51245
|
+
if (innerWidth <= ledgeWidth * 2 + 8 || innerDepth <= ledgeWidth * 2 + 8) {
|
|
51246
|
+
throw new Error("datumEnclosureAssembly: wallThickness and ledgeWidth leave too little internal opening");
|
|
51247
|
+
}
|
|
51248
|
+
if (height <= baseThickness + coverThickness + 4) {
|
|
51249
|
+
throw new Error("datumEnclosureAssembly: height must leave room for internal ribs and standoffs");
|
|
51250
|
+
}
|
|
51251
|
+
const standoffDiameter = requirePositive$6(
|
|
51252
|
+
options.standoffDiameter ?? Math.max(sizeData.head * 1.65, sizeData.close * 2.2),
|
|
51253
|
+
"standoffDiameter"
|
|
51254
|
+
);
|
|
51255
|
+
const minInset = wallThickness + Math.max(ledgeWidth, standoffDiameter / 2 + 1.2);
|
|
51256
|
+
const [insetX, insetY] = resolveBoltInset(options.screwInset, minInset);
|
|
51257
|
+
if (insetX * 2 >= width || insetY * 2 >= depth) {
|
|
51258
|
+
throw new Error("datumEnclosureAssembly: screwInset leaves no room for the standoff datum");
|
|
51259
|
+
}
|
|
51260
|
+
const screwPositions = options.screwPositions ?? [
|
|
51261
|
+
[-width / 2 + insetX, -depth / 2 + insetY],
|
|
51262
|
+
[width / 2 - insetX, -depth / 2 + insetY],
|
|
51263
|
+
[-width / 2 + insetX, depth / 2 - insetY],
|
|
51264
|
+
[width / 2 - insetX, depth / 2 - insetY]
|
|
51265
|
+
];
|
|
51266
|
+
if (screwPositions.length === 0) throw new Error("datumEnclosureAssembly: screwPositions must contain at least one point");
|
|
51267
|
+
for (const [index2, [x2, y2]] of screwPositions.entries()) {
|
|
51268
|
+
if (!Number.isFinite(x2) || !Number.isFinite(y2)) {
|
|
51269
|
+
throw new Error(`datumEnclosureAssembly: screwPositions[${index2}] must contain finite numbers`);
|
|
51270
|
+
}
|
|
51271
|
+
if (Math.abs(x2) + standoffDiameter / 2 > innerWidth / 2 || Math.abs(y2) + standoffDiameter / 2 > innerDepth / 2) {
|
|
51272
|
+
throw new Error(`datumEnclosureAssembly: screwPositions[${index2}] does not fit inside the enclosure walls`);
|
|
51273
|
+
}
|
|
51274
|
+
}
|
|
51275
|
+
const ribHeight = requirePositive$6(options.ribHeight ?? Math.min(height * 0.24, Math.max(2.4, baseThickness * 1.4)), "ribHeight");
|
|
51276
|
+
const ribThickness = requirePositive$6(options.ribThickness ?? Math.max(1.2, wallThickness * 0.75), "ribThickness");
|
|
51277
|
+
const portWidth = requirePositive$6(options.portWidth ?? Math.min(innerWidth * 0.28, Math.max(12, width * 0.16)), "portWidth");
|
|
51278
|
+
const portHeight = requirePositive$6(options.portHeight ?? Math.min(height * 0.42, Math.max(5, height * 0.28)), "portHeight");
|
|
51279
|
+
if (portWidth >= innerWidth - ledgeWidth * 2) {
|
|
51280
|
+
throw new Error("datumEnclosureAssembly: portWidth must fit between internal ledges and standoffs");
|
|
51281
|
+
}
|
|
51282
|
+
if (portHeight >= height - baseThickness - 1) {
|
|
51283
|
+
throw new Error("datumEnclosureAssembly: portHeight must leave material above and below the service port");
|
|
51284
|
+
}
|
|
51285
|
+
const screwLength = requirePositive$6(
|
|
51286
|
+
options.screwLength ?? coverThickness + gasketThickness + Math.max(6, height * 0.45),
|
|
51287
|
+
"screwLength"
|
|
51288
|
+
);
|
|
51289
|
+
const coverHole = fastenerHole({
|
|
51290
|
+
size: screwSize,
|
|
51291
|
+
fit: coverFit,
|
|
51292
|
+
depth: coverThickness + 0.6,
|
|
51293
|
+
center: true,
|
|
51294
|
+
segments,
|
|
51295
|
+
counterbore: { depth: Math.min(coverThickness * 0.6, Math.max(0.6, coverThickness - 0.35)) }
|
|
51296
|
+
});
|
|
51297
|
+
const standoffTap = fastenerHole({ size: screwSize, fit: "tap", depth: height + 0.8, center: true, segments });
|
|
51298
|
+
const standoffThreadEnvelope = fastenerHole({ size: screwSize, fit: "close", depth: height + 0.8, center: true, segments });
|
|
51299
|
+
const coverClearance = placeCutterAtPositions(coverHole, screwPositions, coverThickness / 2);
|
|
51300
|
+
const standoffTappedPattern = placeCutterAtPositions(standoffTap, screwPositions, height / 2);
|
|
51301
|
+
const standoffThreadEnvelopePattern = placeCutterAtPositions(standoffThreadEnvelope, screwPositions, height / 2);
|
|
51302
|
+
const fuseOverlap = Math.min(0.06, Math.max(0.02, wallThickness * 0.02));
|
|
51303
|
+
const ledgeThickness = Math.min(Math.max(1.1, coverThickness * 0.45), height * 0.2);
|
|
51304
|
+
const sideX = width / 2 - wallThickness / 2;
|
|
51305
|
+
const sideY = depth / 2 - wallThickness / 2;
|
|
51306
|
+
const ledgeZ = height - ledgeThickness;
|
|
51307
|
+
const baseSolids = [
|
|
51308
|
+
box(width, depth, baseThickness),
|
|
51309
|
+
box(wallThickness, depth, height).translate(sideX, 0, 0),
|
|
51310
|
+
box(wallThickness, depth, height).translate(-sideX, 0, 0),
|
|
51311
|
+
box(width, wallThickness, height).translate(0, sideY, 0),
|
|
51312
|
+
box(width, wallThickness, height).translate(0, -sideY, 0),
|
|
51313
|
+
box(ledgeWidth, innerDepth, ledgeThickness).translate(-width / 2 + wallThickness + ledgeWidth / 2, 0, ledgeZ),
|
|
51314
|
+
box(ledgeWidth, innerDepth, ledgeThickness).translate(width / 2 - wallThickness - ledgeWidth / 2, 0, ledgeZ),
|
|
51315
|
+
box(innerWidth, ledgeWidth, ledgeThickness).translate(0, -depth / 2 + wallThickness + ledgeWidth / 2, ledgeZ),
|
|
51316
|
+
box(innerWidth, ledgeWidth, ledgeThickness).translate(0, depth / 2 - wallThickness - ledgeWidth / 2, ledgeZ),
|
|
51317
|
+
box(Math.max(1, innerWidth - standoffDiameter * 1.8), ribThickness, ribHeight + fuseOverlap).translate(
|
|
51318
|
+
0,
|
|
51319
|
+
0,
|
|
51320
|
+
baseThickness - fuseOverlap
|
|
51321
|
+
),
|
|
51322
|
+
box(ribThickness, Math.max(1, innerDepth - standoffDiameter * 1.8), ribHeight + fuseOverlap).translate(
|
|
51323
|
+
0,
|
|
51324
|
+
0,
|
|
51325
|
+
baseThickness - fuseOverlap
|
|
51326
|
+
),
|
|
51327
|
+
...screwPositions.map(
|
|
51328
|
+
([x2, y2]) => cylinder(height - baseThickness + fuseOverlap, standoffDiameter / 2, void 0, segments).translate(
|
|
51329
|
+
x2,
|
|
51330
|
+
y2,
|
|
51331
|
+
baseThickness - fuseOverlap
|
|
51332
|
+
)
|
|
51333
|
+
)
|
|
51334
|
+
];
|
|
51335
|
+
const servicePort = box(portWidth, wallThickness + 1, portHeight).translate(
|
|
51336
|
+
0,
|
|
51337
|
+
-depth / 2 + wallThickness / 2,
|
|
51338
|
+
baseThickness + Math.max(0.8, (height - baseThickness - portHeight) * 0.35)
|
|
51339
|
+
);
|
|
51340
|
+
const base = union(...baseSolids).subtract(standoffThreadEnvelopePattern).subtract(servicePort).color("#475569");
|
|
51341
|
+
const gasketFrameCutter = box(Math.max(1, width - ledgeWidth * 2), Math.max(1, depth - ledgeWidth * 2), gasketThickness + 0.6).translate(
|
|
51342
|
+
0,
|
|
51343
|
+
0,
|
|
51344
|
+
-0.3
|
|
51345
|
+
);
|
|
51346
|
+
const gasket = gasketThickness > 0 ? box(width, depth, gasketThickness).subtract(gasketFrameCutter).subtract(placeCutterAtPositions(coverHole, screwPositions, gasketThickness / 2)).translate(0, 0, height + faceClearance).color("#111827") : null;
|
|
51347
|
+
const coverZ = height + faceClearance + (gasket ? gasketThickness + faceClearance : 0);
|
|
51348
|
+
const cover = box(width, depth, coverThickness).subtract(coverClearance).translate(0, 0, coverZ).color("#334155");
|
|
51349
|
+
const hardware = fastenerSet(screwSize, screwLength, { washerUnderHead: false, washerUnderNut: false, fit: coverFit, segments });
|
|
51350
|
+
const screwOriginZ = coverZ + coverThickness;
|
|
51351
|
+
const screws = screwPositions.map(([x2, y2]) => hardware.bolt.translate(x2, y2, screwOriginZ).color("#94a3b8"));
|
|
51352
|
+
const parts = [
|
|
51353
|
+
{ name: "datum enclosure base tray with walls ribs standoffs and service port", shape: base },
|
|
51354
|
+
...gasket ? [{ name: "datum enclosure gasket seated on continuous ledge", shape: gasket }] : [],
|
|
51355
|
+
{ name: "datum enclosure cover plate with matched screw pattern", shape: cover },
|
|
51356
|
+
...screws.map((shape, index2) => ({ name: `installed ${screwSize} enclosure screw ${index2 + 1}`, shape }))
|
|
51357
|
+
];
|
|
51358
|
+
return {
|
|
51359
|
+
parts,
|
|
51360
|
+
base,
|
|
51361
|
+
cover,
|
|
51362
|
+
gasket,
|
|
51363
|
+
screws,
|
|
51364
|
+
screwPositions,
|
|
51365
|
+
cutters: {
|
|
51366
|
+
coverClearance,
|
|
51367
|
+
standoffTapped: standoffTappedPattern,
|
|
51368
|
+
standoffThreadEnvelope: standoffThreadEnvelopePattern,
|
|
51369
|
+
servicePort
|
|
51370
|
+
},
|
|
51371
|
+
dims: {
|
|
51372
|
+
width,
|
|
51373
|
+
depth,
|
|
51374
|
+
height,
|
|
51375
|
+
innerWidth,
|
|
51376
|
+
innerDepth,
|
|
51377
|
+
wallThickness,
|
|
51378
|
+
baseThickness,
|
|
51379
|
+
coverThickness,
|
|
51380
|
+
ledgeWidth,
|
|
51381
|
+
gasketThickness,
|
|
51382
|
+
faceClearance,
|
|
51383
|
+
screwSize,
|
|
51384
|
+
screwLength,
|
|
51385
|
+
standoffDiameter,
|
|
51386
|
+
ribHeight,
|
|
51387
|
+
ribThickness,
|
|
51388
|
+
portWidth,
|
|
51389
|
+
portHeight,
|
|
51390
|
+
clearanceDia: sizeData[coverFit],
|
|
51391
|
+
tapDia: sizeData.tap,
|
|
51392
|
+
threadEnvelopeDia: sizeData.close
|
|
51393
|
+
}
|
|
51394
|
+
};
|
|
51395
|
+
}
|
|
51396
|
+
function snapLatchCoverAssembly(options) {
|
|
51397
|
+
const width = requirePositive$6(options.width, "width");
|
|
51398
|
+
const depth = requirePositive$6(options.depth, "depth");
|
|
51399
|
+
const coverThickness = requirePositive$6(options.coverThickness ?? 2.4, "coverThickness");
|
|
51400
|
+
const parentThickness = requirePositive$6(options.parentThickness ?? 6, "parentThickness");
|
|
51401
|
+
const ledgeWidth = requirePositive$6(options.ledgeWidth ?? 8, "ledgeWidth");
|
|
51402
|
+
const runningClearance = requirePositive$6(options.runningClearance ?? 0.25, "runningClearance");
|
|
51403
|
+
const faceClearance = requirePositive$6(options.faceClearance ?? 0.04, "faceClearance");
|
|
51404
|
+
const latchWidth = requirePositive$6(options.latchWidth ?? Math.min(width * 0.22, Math.max(12, width * 0.16)), "latchWidth");
|
|
51405
|
+
const latchThickness = requirePositive$6(options.latchThickness ?? 1.6, "latchThickness");
|
|
51406
|
+
const hookThrow = requirePositive$6(options.hookThrow ?? 3.2, "hookThrow");
|
|
51407
|
+
const hookThickness = requirePositive$6(options.hookThickness ?? 1.6, "hookThickness");
|
|
51408
|
+
const openingWidth = width - ledgeWidth * 2;
|
|
51409
|
+
const openingDepth = depth - ledgeWidth * 2;
|
|
51410
|
+
if (openingWidth <= Math.max(8, latchWidth * 0.8) || openingDepth <= 8) {
|
|
51411
|
+
throw new Error("snapLatchCoverAssembly: ledgeWidth leaves too little service opening under the cover");
|
|
51412
|
+
}
|
|
51413
|
+
if (latchWidth >= openingWidth) {
|
|
51414
|
+
throw new Error("snapLatchCoverAssembly: latchWidth must fit along the receiver opening");
|
|
51415
|
+
}
|
|
51416
|
+
if (latchThickness + runningClearance * 2 >= ledgeWidth) {
|
|
51417
|
+
throw new Error("snapLatchCoverAssembly: latchThickness and clearance must fit inside the receiver ledge");
|
|
51418
|
+
}
|
|
51419
|
+
if (hookThrow + latchThickness / 2 + runningClearance >= ledgeWidth * 1.5) {
|
|
51420
|
+
throw new Error("snapLatchCoverAssembly: hookThrow is too large for the available underside catch land");
|
|
51421
|
+
}
|
|
51422
|
+
const parentWidth = width + ledgeWidth * 2;
|
|
51423
|
+
const parentDepth = depth + ledgeWidth * 2;
|
|
51424
|
+
const fuseOverlap = Math.min(0.04, faceClearance * 0.7);
|
|
51425
|
+
const hookClearance = Math.min(0.08, runningClearance * 0.32);
|
|
51426
|
+
const coverMinZ = parentThickness + faceClearance;
|
|
51427
|
+
const stemMinZ = -hookClearance - hookThickness;
|
|
51428
|
+
const stemHeight = coverMinZ + fuseOverlap - stemMinZ;
|
|
51429
|
+
const slotY = openingDepth / 2 + ledgeWidth / 2;
|
|
51430
|
+
const latchWindow = (sign2) => box(latchWidth + runningClearance * 2, latchThickness + runningClearance * 2, parentThickness + 0.8).translate(
|
|
51431
|
+
0,
|
|
51432
|
+
sign2 * slotY,
|
|
51433
|
+
-0.4
|
|
51434
|
+
);
|
|
51435
|
+
const latchWindows = union(latchWindow(1), latchWindow(-1));
|
|
51436
|
+
const serviceOpening = box(openingWidth, openingDepth, parentThickness + 1).translate(0, 0, -0.5);
|
|
51437
|
+
const parent = box(parentWidth, parentDepth, parentThickness).subtract(serviceOpening).subtract(latchWindows).color("#475569");
|
|
51438
|
+
const coverPlate = box(width, depth, coverThickness).translate(0, 0, coverMinZ);
|
|
51439
|
+
const snapHook = (sign2) => {
|
|
51440
|
+
const y2 = sign2 * slotY;
|
|
51441
|
+
const stem = box(latchWidth, latchThickness, stemHeight).translate(0, y2, stemMinZ);
|
|
51442
|
+
const barb = box(latchWidth, latchThickness + hookThrow, hookThickness).translate(
|
|
51443
|
+
0,
|
|
51444
|
+
y2 + sign2 * (hookThrow / 2),
|
|
51445
|
+
stemMinZ
|
|
51446
|
+
);
|
|
51447
|
+
const rootRib = box(latchWidth, Math.max(latchThickness, hookThrow * 0.55), coverThickness * 0.65).translate(
|
|
51448
|
+
0,
|
|
51449
|
+
y2 - sign2 * (ledgeWidth * 0.18),
|
|
51450
|
+
coverMinZ
|
|
51451
|
+
);
|
|
51452
|
+
return union(stem, barb, rootRib);
|
|
51453
|
+
};
|
|
51454
|
+
const cover = union(coverPlate, snapHook(1), snapHook(-1)).color("#111827");
|
|
51455
|
+
const parts = [
|
|
51456
|
+
{ name: "snap cover receiver frame with latch windows and catch lands", shape: parent },
|
|
51457
|
+
{ name: "one-piece snap cover with fused hooks and underside barbs", shape: cover }
|
|
51458
|
+
];
|
|
51459
|
+
return {
|
|
51460
|
+
parts,
|
|
51461
|
+
parent,
|
|
51462
|
+
cover,
|
|
51463
|
+
cutters: {
|
|
51464
|
+
serviceOpening,
|
|
51465
|
+
latchWindows
|
|
51466
|
+
},
|
|
51467
|
+
dims: {
|
|
51468
|
+
width,
|
|
51469
|
+
depth,
|
|
51470
|
+
parentWidth,
|
|
51471
|
+
parentDepth,
|
|
51472
|
+
openingWidth,
|
|
51473
|
+
openingDepth,
|
|
51474
|
+
coverThickness,
|
|
51475
|
+
parentThickness,
|
|
51476
|
+
ledgeWidth,
|
|
51477
|
+
latchWidth,
|
|
51478
|
+
latchThickness,
|
|
51479
|
+
hookThrow,
|
|
51480
|
+
hookThickness,
|
|
51481
|
+
runningClearance,
|
|
51482
|
+
faceClearance
|
|
51483
|
+
}
|
|
51484
|
+
};
|
|
51485
|
+
}
|
|
51486
|
+
function pinnedLeverAssembly(options) {
|
|
51487
|
+
const armLength = requirePositive$6(options.armLength, "armLength");
|
|
51488
|
+
const armWidth = requirePositive$6(options.armWidth ?? 10, "armWidth");
|
|
51489
|
+
const leverThickness = requirePositive$6(options.leverThickness ?? 5, "leverThickness");
|
|
51490
|
+
const pinDiameter = requirePositive$6(options.pinDiameter ?? 5, "pinDiameter");
|
|
51491
|
+
const pinClearance = requireNonNegative(options.pinClearance ?? 0.25, "pinClearance");
|
|
51492
|
+
const boreDiameter = pinDiameter + pinClearance;
|
|
51493
|
+
const hubRadius = requirePositive$6(options.hubRadius ?? Math.max(armWidth * 0.85, pinDiameter * 1.8), "hubRadius");
|
|
51494
|
+
const supportThickness = requirePositive$6(options.supportThickness ?? Math.max(6, pinDiameter * 1.4), "supportThickness");
|
|
51495
|
+
const supportWidth = requirePositive$6(options.supportWidth ?? hubRadius * 2 + 18, "supportWidth");
|
|
51496
|
+
const supportDepth = requirePositive$6(options.supportDepth ?? Math.max(armWidth + 18, hubRadius * 2 + 10), "supportDepth");
|
|
51497
|
+
const washerSize = options.washerSize ?? metricWasherSizeForPin(pinDiameter);
|
|
51498
|
+
const washerDims = WASHER_TABLE[washerSize];
|
|
51499
|
+
if (!washerDims) throw new Error(`pinnedLeverAssembly: unsupported washerSize "${washerSize}"`);
|
|
51500
|
+
if (washerDims.id <= pinDiameter) {
|
|
51501
|
+
throw new Error(`pinnedLeverAssembly: ${washerSize} washer inner diameter is too small for a ${pinDiameter} mm pin`);
|
|
51502
|
+
}
|
|
51503
|
+
if (hubRadius <= boreDiameter / 2 + Math.max(1, pinDiameter * 0.25)) {
|
|
51504
|
+
throw new Error("pinnedLeverAssembly: hubRadius leaves too little material around the pivot bore");
|
|
51505
|
+
}
|
|
51506
|
+
if (supportWidth <= boreDiameter + 4 || supportDepth <= boreDiameter + 4) {
|
|
51507
|
+
throw new Error("pinnedLeverAssembly: support dimensions leave too little material around the pivot bore");
|
|
51508
|
+
}
|
|
51509
|
+
const segments = options.segments ?? 40;
|
|
51510
|
+
const gripLength = requirePositive$6(options.gripLength ?? Math.min(armLength * 0.32, Math.max(16, armWidth * 2.4)), "gripLength");
|
|
51511
|
+
const gripWidth = requirePositive$6(options.gripWidth ?? armWidth * 1.55, "gripWidth");
|
|
51512
|
+
if (gripLength >= armLength) throw new Error("pinnedLeverAssembly: gripLength must be shorter than armLength");
|
|
51513
|
+
const armOverlap = Math.min(hubRadius * 0.65, armLength * 0.25);
|
|
51514
|
+
const armStartX = hubRadius - armOverlap;
|
|
51515
|
+
const armCenterX = armStartX + armLength / 2;
|
|
51516
|
+
const gripCenterX = armStartX + armLength - gripLength / 2;
|
|
51517
|
+
const runningClearance = 0.03;
|
|
51518
|
+
const lowerWasherZ = supportThickness + runningClearance;
|
|
51519
|
+
const leverZ = lowerWasherZ + washerDims.t + runningClearance;
|
|
51520
|
+
const upperWasherZ = leverZ + leverThickness + runningClearance;
|
|
51521
|
+
const stackHeight = upperWasherZ + washerDims.t;
|
|
51522
|
+
const pinHeadThickness = Math.max(washerDims.t, pinDiameter * 0.35);
|
|
51523
|
+
const pinHeadRadius = Math.max(washerDims.od * 0.42, pinDiameter * 0.8);
|
|
51524
|
+
const supportBore = cylinder(supportThickness + 1, boreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
|
|
51525
|
+
let supportBlank = box(supportWidth, supportDepth, supportThickness);
|
|
51526
|
+
if (options.stopBlock ?? true) {
|
|
51527
|
+
const stopLength = Math.min(armLength * 0.22, Math.max(10, armWidth * 1.4));
|
|
51528
|
+
const stopWidth = Math.max(4, pinDiameter * 0.7);
|
|
51529
|
+
const stopHeight = supportThickness;
|
|
51530
|
+
const stopX = hubRadius + stopLength / 2;
|
|
51531
|
+
const stopY = armWidth / 2 + stopWidth / 2 + runningClearance;
|
|
51532
|
+
supportBlank = union(supportBlank, box(stopLength, stopWidth, stopHeight).translate(stopX, stopY, 0));
|
|
51533
|
+
}
|
|
51534
|
+
const support = supportBlank.subtract(supportBore).color("#475569");
|
|
51535
|
+
const hub = cylinder(leverThickness, hubRadius, void 0, segments);
|
|
51536
|
+
const arm = box(armLength, armWidth, leverThickness).translate(armCenterX, 0, 0);
|
|
51537
|
+
const grip = box(gripLength, gripWidth, leverThickness).translate(gripCenterX, 0, 0);
|
|
51538
|
+
const leverSolids = [hub, arm, grip];
|
|
51539
|
+
if (options.detentBoss ?? true) {
|
|
51540
|
+
const bossRadius = Math.min(armWidth * 0.42, hubRadius * 0.42);
|
|
51541
|
+
const bossX = hubRadius + Math.min(armLength * 0.22, armWidth * 2);
|
|
51542
|
+
const bossY = -armWidth / 2 - bossRadius * 0.45;
|
|
51543
|
+
leverSolids.push(cylinder(leverThickness, bossRadius, void 0, segments).translate(bossX, bossY, 0));
|
|
51544
|
+
}
|
|
51545
|
+
const leverBore = cylinder(leverThickness + 1, boreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
|
|
51546
|
+
const lever = union(...leverSolids).subtract(leverBore).translate(0, 0, leverZ).color("#7f1d1d");
|
|
51547
|
+
const lowerWasher = washer(washerSize, { segments }).translate(0, 0, lowerWasherZ).color("#94a3b8");
|
|
51548
|
+
const upperWasher = washer(washerSize, { segments }).translate(0, 0, upperWasherZ).color("#94a3b8");
|
|
51549
|
+
const shaft = cylinder(stackHeight, pinDiameter / 2, void 0, segments);
|
|
51550
|
+
const lowerRetainer = cylinder(pinHeadThickness, pinHeadRadius, void 0, segments).translate(0, 0, -pinHeadThickness - runningClearance);
|
|
51551
|
+
const upperHead = cylinder(pinHeadThickness, pinHeadRadius, void 0, segments).translate(0, 0, stackHeight + runningClearance);
|
|
51552
|
+
const pin = union(shaft, lowerRetainer, upperHead).color("#cbd5e1");
|
|
51553
|
+
const pivotBore = cylinder(stackHeight + 1, boreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
|
|
51554
|
+
const parts = [
|
|
51555
|
+
{ name: "pivot support block with bearing bore and low stop land", shape: support },
|
|
51556
|
+
{ name: "lower thrust washer under pinned lever", shape: lowerWasher },
|
|
51557
|
+
{ name: "fused pinned lever with hub arm grip and detent boss", shape: lever },
|
|
51558
|
+
{ name: "upper thrust washer over pinned lever", shape: upperWasher },
|
|
51559
|
+
{ name: "retained pivot pin through lever stack", shape: pin }
|
|
51560
|
+
];
|
|
51561
|
+
return {
|
|
51562
|
+
parts,
|
|
51563
|
+
support,
|
|
51564
|
+
lever,
|
|
51565
|
+
pin,
|
|
51566
|
+
washers: {
|
|
51567
|
+
lower: lowerWasher,
|
|
51568
|
+
upper: upperWasher
|
|
51569
|
+
},
|
|
51570
|
+
cutters: {
|
|
51571
|
+
pivotBore
|
|
51572
|
+
},
|
|
51573
|
+
dims: {
|
|
51574
|
+
armLength,
|
|
51575
|
+
armWidth,
|
|
51576
|
+
leverThickness,
|
|
51577
|
+
hubRadius,
|
|
51578
|
+
pinDiameter,
|
|
51579
|
+
boreDiameter,
|
|
51580
|
+
supportWidth,
|
|
51581
|
+
supportDepth,
|
|
51582
|
+
supportThickness,
|
|
51583
|
+
washerSize,
|
|
51584
|
+
washerThickness: washerDims.t,
|
|
51585
|
+
stackHeight
|
|
51586
|
+
}
|
|
51587
|
+
};
|
|
51588
|
+
}
|
|
51589
|
+
function retainedShaftAssembly(options) {
|
|
51590
|
+
const supportSpacing = requirePositive$6(options.supportSpacing, "supportSpacing");
|
|
51591
|
+
const shaftDiameter = requirePositive$6(options.shaftDiameter ?? 8, "shaftDiameter");
|
|
51592
|
+
const boreClearance = requireNonNegative(options.boreClearance ?? 0.35, "boreClearance");
|
|
51593
|
+
const boreDiameter = shaftDiameter + boreClearance;
|
|
51594
|
+
const supportThickness = requirePositive$6(options.supportThickness ?? Math.max(5, shaftDiameter * 0.75), "supportThickness");
|
|
51595
|
+
const washerSize = options.washerSize ?? metricWasherSizeForPin(shaftDiameter);
|
|
51596
|
+
const washerDims = WASHER_TABLE[washerSize];
|
|
51597
|
+
if (!washerDims) throw new Error(`retainedShaftAssembly: unsupported washerSize "${washerSize}"`);
|
|
51598
|
+
if (washerDims.id <= shaftDiameter) {
|
|
51599
|
+
throw new Error(`retainedShaftAssembly: ${washerSize} washer inner diameter is too small for a ${shaftDiameter} mm shaft`);
|
|
51600
|
+
}
|
|
51601
|
+
const knobDiameter = requirePositive$6(options.knobDiameter ?? shaftDiameter * 3, "knobDiameter");
|
|
51602
|
+
const knobThickness = requirePositive$6(options.knobThickness ?? Math.max(8, shaftDiameter), "knobThickness");
|
|
51603
|
+
const retainerThickness = requirePositive$6(
|
|
51604
|
+
options.retainerThickness ?? Math.max(washerDims.t, shaftDiameter * 0.35),
|
|
51605
|
+
"retainerThickness"
|
|
51606
|
+
);
|
|
51607
|
+
const runningClearance = requireNonNegative(options.runningClearance ?? 0.05, "runningClearance");
|
|
51608
|
+
const supportWidth = requirePositive$6(options.supportWidth ?? Math.max(28, knobDiameter * 1.25), "supportWidth");
|
|
51609
|
+
const supportHeight = requirePositive$6(options.supportHeight ?? Math.max(34, knobDiameter * 1.45), "supportHeight");
|
|
51610
|
+
const segments = options.segments ?? 40;
|
|
51611
|
+
if (supportSpacing <= supportThickness) {
|
|
51612
|
+
throw new Error("retainedShaftAssembly: supportSpacing must leave a gap between support cheeks");
|
|
51613
|
+
}
|
|
51614
|
+
if (supportWidth <= boreDiameter + 4 || supportHeight <= boreDiameter + 4) {
|
|
51615
|
+
throw new Error("retainedShaftAssembly: support dimensions leave too little material around the shaft bore");
|
|
51616
|
+
}
|
|
51617
|
+
const leftSupportX = -supportSpacing / 2;
|
|
51618
|
+
const rightSupportX = supportSpacing / 2;
|
|
51619
|
+
const leftOuterFaceX = leftSupportX - supportThickness / 2;
|
|
51620
|
+
const rightOuterFaceX = rightSupportX + supportThickness / 2;
|
|
51621
|
+
const leftWasherX = leftOuterFaceX - runningClearance - washerDims.t / 2;
|
|
51622
|
+
const rightWasherX = rightOuterFaceX + runningClearance + washerDims.t / 2;
|
|
51623
|
+
const leftKnobX = leftOuterFaceX - runningClearance * 2 - washerDims.t - knobThickness / 2;
|
|
51624
|
+
const rightKnobX = rightOuterFaceX + runningClearance * 2 + washerDims.t + knobThickness / 2;
|
|
51625
|
+
const leftStackOuterX = leftKnobX - knobThickness / 2;
|
|
51626
|
+
const rightStackOuterX = rightKnobX + knobThickness / 2;
|
|
51627
|
+
const minimumShaftLength = rightStackOuterX - leftStackOuterX + retainerThickness * 2 + runningClearance * 2;
|
|
51628
|
+
const shaftLength = requirePositive$6(options.shaftLength ?? minimumShaftLength, "shaftLength");
|
|
51629
|
+
if (shaftLength < minimumShaftLength) {
|
|
51630
|
+
throw new Error("retainedShaftAssembly: shaftLength is too short to retain both supports, washers, and knobs");
|
|
51631
|
+
}
|
|
51632
|
+
const supportBore = cylinderAlongX(supportThickness + 1, boreDiameter / 2, 0, segments);
|
|
51633
|
+
const makeSupport = (x2) => box(supportThickness, supportWidth, supportHeight).translate(x2, 0, -supportHeight / 2).subtract(supportBore.translate(x2, 0, 0)).color("#334155");
|
|
51634
|
+
const knobBore = cylinder(knobThickness + 1, boreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
|
|
51635
|
+
const makeKnob = (x2) => cylinder(knobThickness, knobDiameter / 2, void 0, 18).subtract(knobBore).pointAlong([1, 0, 0]).translate(x2 - knobThickness / 2, 0, 0).color("#111827");
|
|
51636
|
+
const retainerRadius = Math.max(shaftDiameter * 0.85, knobDiameter * 0.36);
|
|
51637
|
+
const shaftCore = cylinderAlongX(shaftLength, shaftDiameter / 2, 0, segments);
|
|
51638
|
+
const leftRetainer = cylinderAlongX(retainerThickness, retainerRadius, -shaftLength / 2 + retainerThickness / 2, segments);
|
|
51639
|
+
const rightRetainer = cylinderAlongX(retainerThickness, retainerRadius, shaftLength / 2 - retainerThickness / 2, segments);
|
|
51640
|
+
const shaft = union(shaftCore, leftRetainer, rightRetainer).color("#cbd5e1");
|
|
51641
|
+
const leftSupport = makeSupport(leftSupportX);
|
|
51642
|
+
const rightSupport = makeSupport(rightSupportX);
|
|
51643
|
+
const leftWasher = washerAlongX(washerSize, leftWasherX, segments).color("#94a3b8");
|
|
51644
|
+
const rightWasher = washerAlongX(washerSize, rightWasherX, segments).color("#94a3b8");
|
|
51645
|
+
const leftKnob = makeKnob(leftKnobX);
|
|
51646
|
+
const rightKnob = makeKnob(rightKnobX);
|
|
51647
|
+
const shaftBore = cylinderAlongX(supportThickness + knobThickness + 2, boreDiameter / 2, 0, segments);
|
|
51648
|
+
const parts = [
|
|
51649
|
+
{ name: "left bored support cheek for retained shaft", shape: leftSupport },
|
|
51650
|
+
{ name: "right bored support cheek for retained shaft", shape: rightSupport },
|
|
51651
|
+
{ name: "retained through shaft with end heads", shape: shaft },
|
|
51652
|
+
{ name: `left ${washerSize} thrust washer on shaft`, shape: leftWasher },
|
|
51653
|
+
{ name: `right ${washerSize} thrust washer on shaft`, shape: rightWasher },
|
|
51654
|
+
{ name: "left retained hand knob with shaft bore", shape: leftKnob },
|
|
51655
|
+
{ name: "right retained hand knob with shaft bore", shape: rightKnob }
|
|
51656
|
+
];
|
|
51657
|
+
return {
|
|
51658
|
+
parts,
|
|
51659
|
+
supports: {
|
|
51660
|
+
left: leftSupport,
|
|
51661
|
+
right: rightSupport
|
|
51662
|
+
},
|
|
51663
|
+
shaft,
|
|
51664
|
+
washers: {
|
|
51665
|
+
left: leftWasher,
|
|
51666
|
+
right: rightWasher
|
|
51667
|
+
},
|
|
51668
|
+
knobs: {
|
|
51669
|
+
left: leftKnob,
|
|
51670
|
+
right: rightKnob
|
|
51671
|
+
},
|
|
51672
|
+
cutters: {
|
|
51673
|
+
shaftBore
|
|
51674
|
+
},
|
|
51675
|
+
dims: {
|
|
51676
|
+
supportSpacing,
|
|
51677
|
+
supportThickness,
|
|
51678
|
+
supportWidth,
|
|
51679
|
+
supportHeight,
|
|
51680
|
+
shaftDiameter,
|
|
51681
|
+
shaftLength,
|
|
51682
|
+
boreDiameter,
|
|
51683
|
+
washerSize,
|
|
51684
|
+
washerThickness: washerDims.t,
|
|
51685
|
+
knobDiameter,
|
|
51686
|
+
knobThickness,
|
|
51687
|
+
retainerThickness,
|
|
51688
|
+
runningClearance
|
|
51689
|
+
}
|
|
51690
|
+
};
|
|
51691
|
+
}
|
|
51692
|
+
function capturedLinearSlide(options) {
|
|
51693
|
+
const length4 = requirePositive$6(options.length, "length");
|
|
51694
|
+
const railWidth = requirePositive$6(options.railWidth ?? 38, "railWidth");
|
|
51695
|
+
const baseThickness = requirePositive$6(options.baseThickness ?? 2.4, "baseThickness");
|
|
51696
|
+
const wallThickness = requirePositive$6(options.wallThickness ?? 2, "wallThickness");
|
|
51697
|
+
const wallHeight = requirePositive$6(options.wallHeight ?? 9, "wallHeight");
|
|
51698
|
+
const lipWidth = requirePositive$6(options.lipWidth ?? 4, "lipWidth");
|
|
51699
|
+
const lipThickness = requirePositive$6(options.lipThickness ?? 1.8, "lipThickness");
|
|
51700
|
+
const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
|
|
51701
|
+
const endStopLength = requirePositive$6(options.endStopLength ?? 6, "endStopLength");
|
|
51702
|
+
const carriageLength = requirePositive$6(options.carriageLength ?? length4 * 0.32, "carriageLength");
|
|
51703
|
+
const innerWidth = railWidth - wallThickness * 2;
|
|
51704
|
+
const throatWidth = innerWidth - lipWidth * 2;
|
|
51705
|
+
if (innerWidth <= 0) throw new Error("capturedLinearSlide: wallThickness leaves no inner rail width");
|
|
51706
|
+
if (throatWidth <= 0) throw new Error("capturedLinearSlide: lipWidth closes the rail throat");
|
|
51707
|
+
const carriageWidth = requirePositive$6(options.carriageWidth ?? innerWidth - runningClearance * 2, "carriageWidth");
|
|
51708
|
+
const carriageThickness = requirePositive$6(options.carriageThickness ?? 4, "carriageThickness");
|
|
51709
|
+
if (carriageWidth >= innerWidth - runningClearance) {
|
|
51710
|
+
throw new Error("capturedLinearSlide: carriageWidth leaves too little side clearance inside the rail");
|
|
51711
|
+
}
|
|
51712
|
+
if (carriageWidth <= throatWidth + runningClearance) {
|
|
51713
|
+
throw new Error("capturedLinearSlide: carriageWidth must be wider than the lip throat so the rail actually captures it");
|
|
51714
|
+
}
|
|
51715
|
+
if (carriageThickness + runningClearance * 2 >= wallHeight) {
|
|
51716
|
+
throw new Error("capturedLinearSlide: carriage is too tall to clear the return lips");
|
|
51717
|
+
}
|
|
51718
|
+
const maxTravel = length4 - endStopLength * 2 - carriageLength;
|
|
51719
|
+
if (maxTravel <= 0) {
|
|
51720
|
+
throw new Error("capturedLinearSlide: rail length, end stops, and carriage length leave no travel");
|
|
51721
|
+
}
|
|
51722
|
+
const travel = options.travel ?? maxTravel / 2;
|
|
51723
|
+
if (!Number.isFinite(travel) || travel < 0 || travel > maxTravel) {
|
|
51724
|
+
throw new Error(`capturedLinearSlide: travel must be between 0 and ${maxTravel}`);
|
|
51725
|
+
}
|
|
51726
|
+
const carriageCenterX = -maxTravel / 2 + travel;
|
|
51727
|
+
const fuseOverlap = Math.min(0.04, runningClearance * 0.1);
|
|
51728
|
+
const sideY = railWidth / 2 - wallThickness / 2;
|
|
51729
|
+
const lipY = railWidth / 2 - wallThickness - lipWidth / 2 + fuseOverlap / 2;
|
|
51730
|
+
const stopZ = baseThickness - fuseOverlap;
|
|
51731
|
+
const rail2 = union(
|
|
51732
|
+
box(length4, railWidth, baseThickness),
|
|
51733
|
+
box(length4, wallThickness, wallHeight + fuseOverlap).translate(0, sideY, baseThickness - fuseOverlap),
|
|
51734
|
+
box(length4, wallThickness, wallHeight + fuseOverlap).translate(0, -sideY, baseThickness - fuseOverlap),
|
|
51735
|
+
box(length4, lipWidth, lipThickness + fuseOverlap).translate(0, lipY, baseThickness + wallHeight - fuseOverlap),
|
|
51736
|
+
box(length4, lipWidth, lipThickness + fuseOverlap).translate(0, -lipY, baseThickness + wallHeight - fuseOverlap),
|
|
51737
|
+
box(endStopLength, throatWidth, carriageThickness + fuseOverlap).translate(-length4 / 2 + endStopLength / 2, 0, stopZ),
|
|
51738
|
+
box(endStopLength, throatWidth, carriageThickness + fuseOverlap).translate(length4 / 2 - endStopLength / 2, 0, stopZ)
|
|
51739
|
+
).color("#475569");
|
|
51740
|
+
const carriage = union(
|
|
51741
|
+
box(carriageLength, carriageWidth, carriageThickness),
|
|
51742
|
+
box(carriageLength * 0.78, throatWidth - runningClearance * 2, Math.max(1, carriageThickness * 0.38)).translate(
|
|
51743
|
+
0,
|
|
51744
|
+
0,
|
|
51745
|
+
carriageThickness
|
|
51746
|
+
)
|
|
51747
|
+
).translate(carriageCenterX, 0, baseThickness + runningClearance).color("#111827");
|
|
51748
|
+
const parts = [
|
|
51749
|
+
{ name: "captured linear rail with return lips and end stops", shape: rail2 },
|
|
51750
|
+
{ name: "sliding carriage captured under rail lips", shape: carriage }
|
|
51751
|
+
];
|
|
51752
|
+
return {
|
|
51753
|
+
parts,
|
|
51754
|
+
rail: rail2,
|
|
51755
|
+
carriage,
|
|
51756
|
+
dims: {
|
|
51757
|
+
length: length4,
|
|
51758
|
+
railWidth,
|
|
51759
|
+
innerWidth,
|
|
51760
|
+
throatWidth,
|
|
51761
|
+
baseThickness,
|
|
51762
|
+
wallThickness,
|
|
51763
|
+
wallHeight,
|
|
51764
|
+
lipWidth,
|
|
51765
|
+
lipThickness,
|
|
51766
|
+
carriageLength,
|
|
51767
|
+
carriageWidth,
|
|
51768
|
+
carriageThickness,
|
|
51769
|
+
endStopLength,
|
|
51770
|
+
runningClearance,
|
|
51771
|
+
maxTravel,
|
|
51772
|
+
travel,
|
|
51773
|
+
carriageCenterX
|
|
51774
|
+
}
|
|
51775
|
+
};
|
|
51776
|
+
}
|
|
51777
|
+
function capturedCartridgeGuideAssembly(options) {
|
|
51778
|
+
const length4 = requirePositive$6(options.length, "length");
|
|
51779
|
+
const guideWidth = requirePositive$6(options.guideWidth ?? 42, "guideWidth");
|
|
51780
|
+
const baseThickness = requirePositive$6(options.baseThickness ?? 3, "baseThickness");
|
|
51781
|
+
const wallThickness = requirePositive$6(options.wallThickness ?? 2.5, "wallThickness");
|
|
51782
|
+
const wallHeight = requirePositive$6(options.wallHeight ?? 12, "wallHeight");
|
|
51783
|
+
const lipWidth = requirePositive$6(options.lipWidth ?? 4, "lipWidth");
|
|
51784
|
+
const lipThickness = requirePositive$6(options.lipThickness ?? 2, "lipThickness");
|
|
51785
|
+
const rearStopLength = requirePositive$6(options.rearStopLength ?? 7, "rearStopLength");
|
|
51786
|
+
const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
|
|
51787
|
+
const cartridgeLength = requirePositive$6(options.cartridgeLength ?? length4 * 0.58, "cartridgeLength");
|
|
51788
|
+
const cartridgeHeight = requirePositive$6(options.cartridgeHeight ?? 10, "cartridgeHeight");
|
|
51789
|
+
const flangeThickness = requirePositive$6(options.flangeThickness ?? 3, "flangeThickness");
|
|
51790
|
+
const pullTabLength = requirePositive$6(options.pullTabLength ?? 10, "pullTabLength");
|
|
51791
|
+
const innerWidth = guideWidth - wallThickness * 2;
|
|
51792
|
+
const throatWidth = innerWidth - lipWidth * 2;
|
|
51793
|
+
if (innerWidth <= 0) throw new Error("capturedCartridgeGuideAssembly: wallThickness leaves no inner guide width");
|
|
51794
|
+
if (throatWidth <= 0) throw new Error("capturedCartridgeGuideAssembly: lipWidth closes the guide throat");
|
|
51795
|
+
if (wallHeight <= lipThickness + flangeThickness + runningClearance * 2) {
|
|
51796
|
+
throw new Error("capturedCartridgeGuideAssembly: wallHeight leaves too little vertical capture clearance");
|
|
51797
|
+
}
|
|
51798
|
+
const cartridgeWidth = requirePositive$6(options.cartridgeWidth ?? innerWidth - runningClearance * 2, "cartridgeWidth");
|
|
51799
|
+
const cartridgeBodyWidth = throatWidth - runningClearance * 2;
|
|
51800
|
+
if (cartridgeBodyWidth <= 0) {
|
|
51801
|
+
throw new Error("capturedCartridgeGuideAssembly: throatWidth and runningClearance leave no cartridge body width");
|
|
51802
|
+
}
|
|
51803
|
+
if (cartridgeWidth >= innerWidth - runningClearance) {
|
|
51804
|
+
throw new Error("capturedCartridgeGuideAssembly: cartridgeWidth leaves too little side clearance inside the guide");
|
|
51805
|
+
}
|
|
51806
|
+
if (cartridgeWidth <= throatWidth + runningClearance) {
|
|
51807
|
+
throw new Error("capturedCartridgeGuideAssembly: cartridge flange must be wider than the guide throat so the cartridge is captured");
|
|
51808
|
+
}
|
|
51809
|
+
const maxInsertion = length4 - rearStopLength - cartridgeLength;
|
|
51810
|
+
if (maxInsertion <= 0) {
|
|
51811
|
+
throw new Error("capturedCartridgeGuideAssembly: length, rearStopLength, and cartridgeLength leave no insertion travel");
|
|
51812
|
+
}
|
|
51813
|
+
const insertion = options.insertion ?? maxInsertion * 0.4;
|
|
51814
|
+
if (!Number.isFinite(insertion) || insertion < 0 || insertion > maxInsertion) {
|
|
51815
|
+
throw new Error(`capturedCartridgeGuideAssembly: insertion must be between 0 and ${maxInsertion}`);
|
|
51816
|
+
}
|
|
51817
|
+
const cartridgeCenterX = -length4 / 2 + cartridgeLength / 2 + insertion;
|
|
51818
|
+
const fuseOverlap = Math.min(0.04, runningClearance * 0.1);
|
|
51819
|
+
const sideY = guideWidth / 2 - wallThickness / 2;
|
|
51820
|
+
const lipY = guideWidth / 2 - wallThickness - lipWidth / 2 + fuseOverlap / 2;
|
|
51821
|
+
const guide = union(
|
|
51822
|
+
box(length4, guideWidth, baseThickness),
|
|
51823
|
+
box(length4, wallThickness, wallHeight + fuseOverlap).translate(0, sideY, baseThickness - fuseOverlap),
|
|
51824
|
+
box(length4, wallThickness, wallHeight + fuseOverlap).translate(0, -sideY, baseThickness - fuseOverlap),
|
|
51825
|
+
box(length4, lipWidth, lipThickness + fuseOverlap).translate(0, lipY, baseThickness + wallHeight - fuseOverlap),
|
|
51826
|
+
box(length4, lipWidth, lipThickness + fuseOverlap).translate(0, -lipY, baseThickness + wallHeight - fuseOverlap),
|
|
51827
|
+
box(rearStopLength, throatWidth, Math.max(flangeThickness + runningClearance, 4)).translate(
|
|
51828
|
+
length4 / 2 - rearStopLength / 2,
|
|
51829
|
+
0,
|
|
51830
|
+
baseThickness - fuseOverlap
|
|
51831
|
+
)
|
|
51832
|
+
).color("#475569");
|
|
51833
|
+
const flangeZ = baseThickness + runningClearance;
|
|
51834
|
+
const bodyHeight = Math.max(1, cartridgeHeight - flangeThickness);
|
|
51835
|
+
const bodyZ = flangeZ + flangeThickness;
|
|
51836
|
+
const tabOverlap = Math.min(0.6, pullTabLength * 0.15);
|
|
51837
|
+
const pullTabX = cartridgeCenterX - cartridgeLength / 2 - pullTabLength / 2 + tabOverlap;
|
|
51838
|
+
const pullTabWidth = Math.max(cartridgeBodyWidth * 0.55, 12);
|
|
51839
|
+
const cartridge = union(
|
|
51840
|
+
box(cartridgeLength, cartridgeWidth, flangeThickness).translate(cartridgeCenterX, 0, flangeZ),
|
|
51841
|
+
box(cartridgeLength * 0.88, cartridgeBodyWidth, bodyHeight).translate(cartridgeCenterX, 0, bodyZ),
|
|
51842
|
+
box(pullTabLength, pullTabWidth, Math.max(flangeThickness, 3)).translate(pullTabX, 0, flangeZ)
|
|
51843
|
+
).color("#111827");
|
|
51844
|
+
const parts = [
|
|
51845
|
+
{ name: "captured cartridge guide with return lips and rear stop", shape: guide },
|
|
51846
|
+
{ name: "removable cartridge with captured flange and pull tab", shape: cartridge }
|
|
51847
|
+
];
|
|
51848
|
+
return {
|
|
51849
|
+
parts,
|
|
51850
|
+
guide,
|
|
51851
|
+
cartridge,
|
|
51852
|
+
dims: {
|
|
51853
|
+
length: length4,
|
|
51854
|
+
guideWidth,
|
|
51855
|
+
innerWidth,
|
|
51856
|
+
throatWidth,
|
|
51857
|
+
baseThickness,
|
|
51858
|
+
wallThickness,
|
|
51859
|
+
wallHeight,
|
|
51860
|
+
lipWidth,
|
|
51861
|
+
lipThickness,
|
|
51862
|
+
rearStopLength,
|
|
51863
|
+
cartridgeLength,
|
|
51864
|
+
cartridgeWidth,
|
|
51865
|
+
cartridgeBodyWidth,
|
|
51866
|
+
cartridgeHeight,
|
|
51867
|
+
flangeThickness,
|
|
51868
|
+
pullTabLength,
|
|
51869
|
+
runningClearance,
|
|
51870
|
+
maxInsertion,
|
|
51871
|
+
insertion,
|
|
51872
|
+
cartridgeCenterX
|
|
51873
|
+
}
|
|
51874
|
+
};
|
|
51875
|
+
}
|
|
51876
|
+
function livingHingeCoverAssembly(options) {
|
|
51877
|
+
const width = requirePositive$6(options.width, "width");
|
|
51878
|
+
const coverDepth = requirePositive$6(options.coverDepth ?? 42, "coverDepth");
|
|
51879
|
+
const fixedLeafDepth = requirePositive$6(options.fixedLeafDepth ?? 18, "fixedLeafDepth");
|
|
51880
|
+
const leafThickness = requirePositive$6(options.leafThickness ?? 2, "leafThickness");
|
|
51881
|
+
const hingeWebWidth = requirePositive$6(options.hingeWebWidth ?? 3.2, "hingeWebWidth");
|
|
51882
|
+
const hingeWebThickness = requirePositive$6(options.hingeWebThickness ?? 0.45, "hingeWebThickness");
|
|
51883
|
+
const pullLipDepth = requirePositive$6(options.pullLipDepth ?? 5, "pullLipDepth");
|
|
51884
|
+
const snapBarbWidth = requirePositive$6(options.snapBarbWidth ?? width * 0.35, "snapBarbWidth");
|
|
51885
|
+
const snapBarbDepth = requirePositive$6(options.snapBarbDepth ?? 2.4, "snapBarbDepth");
|
|
51886
|
+
const snapBarbHeight = requirePositive$6(options.snapBarbHeight ?? 1.4, "snapBarbHeight");
|
|
51887
|
+
const catchLandDepth = requirePositive$6(options.catchLandDepth ?? 2.4, "catchLandDepth");
|
|
51888
|
+
if (hingeWebThickness >= leafThickness * 0.55) {
|
|
51889
|
+
throw new Error("livingHingeCoverAssembly: hingeWebThickness must be much thinner than the rigid leaves");
|
|
51890
|
+
}
|
|
51891
|
+
if (hingeWebWidth >= Math.min(coverDepth, fixedLeafDepth) * 0.45) {
|
|
51892
|
+
throw new Error("livingHingeCoverAssembly: hingeWebWidth is too wide for the selected leaves");
|
|
51893
|
+
}
|
|
51894
|
+
if (snapBarbWidth >= width - 2) {
|
|
51895
|
+
throw new Error("livingHingeCoverAssembly: snapBarbWidth must leave side material on the cover leaf");
|
|
51896
|
+
}
|
|
51897
|
+
const fuseOverlap = Math.min(0.04, hingeWebWidth * 0.02);
|
|
51898
|
+
const fixedCenterY = -hingeWebWidth / 2 - fixedLeafDepth / 2 + fuseOverlap / 2;
|
|
51899
|
+
const coverCenterY = hingeWebWidth / 2 + coverDepth / 2 - fuseOverlap / 2;
|
|
51900
|
+
const fixedLeaf = box(width, fixedLeafDepth + fuseOverlap, leafThickness).translate(0, fixedCenterY, 0);
|
|
51901
|
+
const movingLeaf = box(width, coverDepth + fuseOverlap, leafThickness).translate(0, coverCenterY, 0);
|
|
51902
|
+
const hingeWeb = box(width, hingeWebWidth + fuseOverlap * 2, hingeWebThickness).translate(0, 0, 0);
|
|
51903
|
+
const pullLip = box(width * 0.92, pullLipDepth, leafThickness).translate(0, coverCenterY + coverDepth / 2 + pullLipDepth / 2 - fuseOverlap, 0);
|
|
51904
|
+
const snapBarb = box(snapBarbWidth, snapBarbDepth, snapBarbHeight).translate(
|
|
51905
|
+
0,
|
|
51906
|
+
coverCenterY + coverDepth / 2 - snapBarbDepth / 2,
|
|
51907
|
+
leafThickness
|
|
51908
|
+
);
|
|
51909
|
+
const catchLand = box(width * 0.55, catchLandDepth, Math.max(0.8, leafThickness * 0.45)).translate(
|
|
51910
|
+
0,
|
|
51911
|
+
fixedCenterY - fixedLeafDepth / 2 + catchLandDepth / 2,
|
|
51912
|
+
leafThickness
|
|
51913
|
+
);
|
|
51914
|
+
const cover = union(fixedLeaf, movingLeaf, hingeWeb, pullLip, snapBarb, catchLand).color("#0f766e");
|
|
51915
|
+
const overallDepth = fixedLeafDepth + hingeWebWidth + coverDepth + pullLipDepth;
|
|
51916
|
+
const flexRatio = leafThickness / hingeWebThickness;
|
|
51917
|
+
return {
|
|
51918
|
+
parts: [{ name: "one-piece molded living hinge cover with snap barb", shape: cover }],
|
|
51919
|
+
cover,
|
|
51920
|
+
fixedLeaf,
|
|
51921
|
+
movingLeaf,
|
|
51922
|
+
hingeWeb,
|
|
51923
|
+
snapBarb,
|
|
51924
|
+
catchLand,
|
|
51925
|
+
dims: {
|
|
51926
|
+
width,
|
|
51927
|
+
coverDepth,
|
|
51928
|
+
fixedLeafDepth,
|
|
51929
|
+
leafThickness,
|
|
51930
|
+
hingeWebWidth,
|
|
51931
|
+
hingeWebThickness,
|
|
51932
|
+
pullLipDepth,
|
|
51933
|
+
snapBarbWidth,
|
|
51934
|
+
snapBarbDepth,
|
|
51935
|
+
snapBarbHeight,
|
|
51936
|
+
catchLandDepth,
|
|
51937
|
+
flexRatio,
|
|
51938
|
+
overallDepth
|
|
51939
|
+
}
|
|
51940
|
+
};
|
|
51941
|
+
}
|
|
51942
|
+
function knuckledHingeAssembly(options) {
|
|
51943
|
+
const length4 = requirePositive$6(options.length, "length");
|
|
51944
|
+
const leafLength = requirePositive$6(options.leafLength ?? 36, "leafLength");
|
|
51945
|
+
const leafThickness = requirePositive$6(options.leafThickness ?? 1.6, "leafThickness");
|
|
51946
|
+
const barrelOuterRadius = requirePositive$6(options.barrelOuterRadius ?? 3, "barrelOuterRadius");
|
|
51947
|
+
const pinDiameter = requirePositive$6(options.pinDiameter ?? 2, "pinDiameter");
|
|
51948
|
+
const pinClearance = requireNonNegative(options.pinClearance ?? 0.25, "pinClearance");
|
|
51949
|
+
const boreDiameter = pinDiameter + pinClearance;
|
|
51950
|
+
const knuckleGap = requireNonNegative(options.knuckleGap ?? 0.45, "knuckleGap");
|
|
51951
|
+
const openAngleDeg = Number.isFinite(options.openAngleDeg ?? 35) ? options.openAngleDeg ?? 35 : 35;
|
|
51952
|
+
const retainerThickness = requirePositive$6(
|
|
51953
|
+
options.retainerThickness ?? Math.max(leafThickness, pinDiameter * 0.7),
|
|
51954
|
+
"retainerThickness"
|
|
51955
|
+
);
|
|
51956
|
+
const segments = options.segments ?? 36;
|
|
51957
|
+
const knuckleCount = options.knuckleCount ?? 5;
|
|
51958
|
+
if (!Number.isInteger(knuckleCount) || knuckleCount < 3 || knuckleCount % 2 === 0) {
|
|
51959
|
+
throw new Error("knuckledHingeAssembly: knuckleCount must be an odd integer >= 3");
|
|
51960
|
+
}
|
|
51961
|
+
if (barrelOuterRadius <= boreDiameter / 2 + Math.max(0.35, pinDiameter * 0.18)) {
|
|
51962
|
+
throw new Error("knuckledHingeAssembly: barrelOuterRadius leaves too little wall around the pin bore");
|
|
51963
|
+
}
|
|
51964
|
+
const knuckleLength = (length4 - knuckleGap * (knuckleCount - 1)) / knuckleCount;
|
|
51965
|
+
if (knuckleLength <= pinDiameter * 1.4) {
|
|
51966
|
+
throw new Error("knuckledHingeAssembly: length, knuckleCount, and knuckleGap make knuckles too short");
|
|
51967
|
+
}
|
|
51968
|
+
const leafRootClearance = Math.max(0.12, Math.min(knuckleGap * 0.35, 0.35));
|
|
51969
|
+
const barrelLeafOverlap = Math.min(barrelOuterRadius * 0.18, leafThickness * 0.35);
|
|
51970
|
+
const bridgeDepth = leafRootClearance + barrelLeafOverlap + 0.2;
|
|
51971
|
+
const fixedLeafPlate = box(length4, leafLength, leafThickness).translate(
|
|
51972
|
+
0,
|
|
51973
|
+
barrelOuterRadius + leafRootClearance + leafLength / 2,
|
|
51974
|
+
-leafThickness / 2
|
|
51975
|
+
);
|
|
51976
|
+
const movingLeafPlate = box(length4, leafLength, leafThickness).translate(
|
|
51977
|
+
0,
|
|
51978
|
+
-barrelOuterRadius - leafRootClearance - leafLength / 2,
|
|
51979
|
+
-leafThickness / 2
|
|
51980
|
+
);
|
|
51981
|
+
const fixedKnuckles = [];
|
|
51982
|
+
const movingKnuckles = [];
|
|
51983
|
+
const fixedBridges = [];
|
|
51984
|
+
const movingBridges = [];
|
|
51985
|
+
for (let index2 = 0; index2 < knuckleCount; index2 += 1) {
|
|
51986
|
+
const xStart = -length4 / 2 + index2 * (knuckleLength + knuckleGap);
|
|
51987
|
+
const xCenter = xStart + knuckleLength / 2;
|
|
51988
|
+
const knuckle = tubeAlongX(knuckleLength, barrelOuterRadius, boreDiameter / 2, xCenter, segments);
|
|
51989
|
+
if (index2 % 2 === 0) {
|
|
51990
|
+
fixedKnuckles.push(knuckle);
|
|
51991
|
+
fixedBridges.push(
|
|
51992
|
+
box(knuckleLength, bridgeDepth, leafThickness).translate(
|
|
51993
|
+
xCenter,
|
|
51994
|
+
barrelOuterRadius - barrelLeafOverlap + bridgeDepth / 2,
|
|
51995
|
+
-leafThickness / 2
|
|
51996
|
+
)
|
|
51997
|
+
);
|
|
51998
|
+
} else {
|
|
51999
|
+
movingKnuckles.push(knuckle);
|
|
52000
|
+
movingBridges.push(
|
|
52001
|
+
box(knuckleLength, bridgeDepth, leafThickness).translate(
|
|
52002
|
+
xCenter,
|
|
52003
|
+
-barrelOuterRadius + barrelLeafOverlap - bridgeDepth / 2,
|
|
52004
|
+
-leafThickness / 2
|
|
52005
|
+
)
|
|
52006
|
+
);
|
|
52007
|
+
}
|
|
52008
|
+
}
|
|
52009
|
+
const fixedLeaf = union(fixedLeafPlate, ...fixedKnuckles, ...fixedBridges).color("#475569");
|
|
52010
|
+
const movingLeaf = union(movingLeafPlate, ...movingKnuckles, ...movingBridges).rotateX(openAngleDeg).color("#111827");
|
|
52011
|
+
const pinCore = cylinderAlongX(length4 + retainerThickness * 2, pinDiameter / 2, 0, segments);
|
|
52012
|
+
const retainerRadius = Math.max(barrelOuterRadius * 0.85, pinDiameter);
|
|
52013
|
+
const leftHead = cylinderAlongX(retainerThickness, retainerRadius, -length4 / 2 - retainerThickness / 2, segments);
|
|
52014
|
+
const rightHead = cylinderAlongX(retainerThickness, retainerRadius, length4 / 2 + retainerThickness / 2, segments);
|
|
52015
|
+
const pin = union(pinCore, leftHead, rightHead).color("#cbd5e1");
|
|
52016
|
+
const pinBore = cylinderAlongX(length4 + retainerThickness * 2, boreDiameter / 2, 0, segments);
|
|
52017
|
+
const parts = [
|
|
52018
|
+
{ name: "fixed hinge leaf with alternating knuckles", shape: fixedLeaf },
|
|
52019
|
+
{ name: "moving hinge leaf with alternating knuckles", shape: movingLeaf },
|
|
52020
|
+
{ name: "retained hinge pin through knuckle stack", shape: pin }
|
|
52021
|
+
];
|
|
52022
|
+
return {
|
|
52023
|
+
parts,
|
|
52024
|
+
fixedLeaf,
|
|
52025
|
+
movingLeaf,
|
|
52026
|
+
pin,
|
|
52027
|
+
cutters: {
|
|
52028
|
+
pinBore
|
|
52029
|
+
},
|
|
52030
|
+
dims: {
|
|
52031
|
+
length: length4,
|
|
52032
|
+
leafLength,
|
|
52033
|
+
leafThickness,
|
|
52034
|
+
barrelOuterRadius,
|
|
52035
|
+
pinDiameter,
|
|
52036
|
+
boreDiameter,
|
|
52037
|
+
knuckleGap,
|
|
52038
|
+
knuckleCount,
|
|
52039
|
+
knuckleLength,
|
|
52040
|
+
openAngleDeg,
|
|
52041
|
+
retainerThickness
|
|
52042
|
+
}
|
|
52043
|
+
};
|
|
52044
|
+
}
|
|
52045
|
+
function clevisPinJointAssembly(options = {}) {
|
|
52046
|
+
const pinDiameter = requirePositive$6(options.pinDiameter ?? 4, "pinDiameter");
|
|
52047
|
+
const pinClearance = requireNonNegative(options.pinClearance ?? 0.3, "pinClearance");
|
|
52048
|
+
const boreDiameter = pinDiameter + pinClearance;
|
|
52049
|
+
const linkThickness = requirePositive$6(options.linkThickness ?? Math.max(5, pinDiameter * 1.5), "linkThickness");
|
|
52050
|
+
const earThickness = requirePositive$6(options.earThickness ?? Math.max(3.5, pinDiameter), "earThickness");
|
|
52051
|
+
const runningClearance = requireNonNegative(options.runningClearance ?? 0.25, "runningClearance");
|
|
52052
|
+
const linkArmWidth = requirePositive$6(options.linkArmWidth ?? pinDiameter * 2.4, "linkArmWidth");
|
|
52053
|
+
const eyeOuterRadius = requirePositive$6(
|
|
52054
|
+
options.eyeOuterRadius ?? Math.max(pinDiameter * 1.8, linkArmWidth / 2 + 1.4),
|
|
52055
|
+
"eyeOuterRadius"
|
|
52056
|
+
);
|
|
52057
|
+
const earLength = requirePositive$6(options.earLength ?? Math.max(eyeOuterRadius * 2.55, pinDiameter * 4.2), "earLength");
|
|
52058
|
+
const earHeight = requirePositive$6(options.earHeight ?? Math.max(eyeOuterRadius * 2.25, pinDiameter * 4.4), "earHeight");
|
|
52059
|
+
const linkArmLength = requirePositive$6(options.linkArmLength ?? 34, "linkArmLength");
|
|
52060
|
+
const retainerThickness = requirePositive$6(
|
|
52061
|
+
options.retainerThickness ?? Math.max(1.2, pinDiameter * 0.35),
|
|
52062
|
+
"retainerThickness"
|
|
52063
|
+
);
|
|
52064
|
+
const segments = options.segments ?? 40;
|
|
52065
|
+
if (eyeOuterRadius <= boreDiameter / 2 + Math.max(0.8, pinDiameter * 0.25)) {
|
|
52066
|
+
throw new Error("clevisPinJointAssembly: eyeOuterRadius leaves too little material around the pin bore");
|
|
52067
|
+
}
|
|
52068
|
+
if (earHeight <= boreDiameter + Math.max(3, pinDiameter)) {
|
|
52069
|
+
throw new Error("clevisPinJointAssembly: earHeight leaves too little material around the pin bore");
|
|
52070
|
+
}
|
|
52071
|
+
if (earLength / 2 <= eyeOuterRadius + runningClearance) {
|
|
52072
|
+
throw new Error("clevisPinJointAssembly: earLength must extend behind the link eye for a rear clevis bridge");
|
|
52073
|
+
}
|
|
52074
|
+
const clevisGap = linkThickness + runningClearance * 2;
|
|
52075
|
+
const earCenterY = clevisGap / 2 + earThickness / 2;
|
|
52076
|
+
const totalStackY = clevisGap + earThickness * 2;
|
|
52077
|
+
const pinLength = totalStackY + retainerThickness * 2 + runningClearance * 2;
|
|
52078
|
+
const bridgeClearX = -eyeOuterRadius - runningClearance;
|
|
52079
|
+
const bridgeLength = Math.max(pinDiameter * 2.2, 4);
|
|
52080
|
+
const bridgeHeight = Math.min(earHeight * 0.48, Math.max(pinDiameter * 1.4, eyeOuterRadius * 0.75));
|
|
52081
|
+
const bridgeCenterX = bridgeClearX - bridgeLength / 2;
|
|
52082
|
+
const bridgeCenterZ = -earHeight / 2 + bridgeHeight / 2;
|
|
52083
|
+
const pinBore = cylinderAlongY(totalStackY + 0.8, boreDiameter / 2, 0, segments);
|
|
52084
|
+
const clevisBlank = union(
|
|
52085
|
+
box(earLength, earThickness, earHeight).translate(0, earCenterY, -earHeight / 2),
|
|
52086
|
+
box(earLength, earThickness, earHeight).translate(0, -earCenterY, -earHeight / 2),
|
|
52087
|
+
box(bridgeLength, totalStackY, bridgeHeight).translate(bridgeCenterX, 0, bridgeCenterZ)
|
|
52088
|
+
);
|
|
52089
|
+
const clevis = clevisBlank.subtract(pinBore).color("#475569");
|
|
52090
|
+
const eye = tubeAlongY(linkThickness, eyeOuterRadius, boreDiameter / 2, 0, segments);
|
|
52091
|
+
const armOverlap = Math.min(eyeOuterRadius * 0.65, linkArmLength * 0.25);
|
|
52092
|
+
const armCenterX = eyeOuterRadius - armOverlap + linkArmLength / 2;
|
|
52093
|
+
const linkArm = box(linkArmLength, linkThickness, linkArmWidth).translate(armCenterX, 0, -linkArmWidth / 2);
|
|
52094
|
+
const link = union(eye, linkArm).color("#111827");
|
|
52095
|
+
const pinCore = cylinderAlongY(pinLength, pinDiameter / 2, 0, segments);
|
|
52096
|
+
const headRadius = Math.max(pinDiameter * 0.9, boreDiameter / 2 + 0.8);
|
|
52097
|
+
const headY = totalStackY / 2 + runningClearance + retainerThickness / 2;
|
|
52098
|
+
const headA = cylinderAlongY(retainerThickness, headRadius, headY, segments);
|
|
52099
|
+
const headB = cylinderAlongY(retainerThickness, headRadius, -headY, segments);
|
|
52100
|
+
const pin = union(pinCore, headA, headB).color("#cbd5e1");
|
|
52101
|
+
const cutter = cylinderAlongY(pinLength + 1, boreDiameter / 2, 0, segments);
|
|
52102
|
+
const parts = [
|
|
52103
|
+
{ name: "bored clevis yoke with rear bridge", shape: clevis },
|
|
52104
|
+
{ name: "center link eye captured in clevis", shape: link },
|
|
52105
|
+
{ name: "retained clevis pin through link eye", shape: pin }
|
|
52106
|
+
];
|
|
52107
|
+
return {
|
|
52108
|
+
parts,
|
|
52109
|
+
clevis,
|
|
52110
|
+
link,
|
|
52111
|
+
pin,
|
|
52112
|
+
cutters: {
|
|
52113
|
+
pinBore: cutter
|
|
52114
|
+
},
|
|
52115
|
+
dims: {
|
|
52116
|
+
pinDiameter,
|
|
52117
|
+
boreDiameter,
|
|
52118
|
+
linkThickness,
|
|
52119
|
+
earThickness,
|
|
52120
|
+
runningClearance,
|
|
52121
|
+
earLength,
|
|
52122
|
+
earHeight,
|
|
52123
|
+
linkArmLength,
|
|
52124
|
+
linkArmWidth,
|
|
52125
|
+
eyeOuterRadius,
|
|
52126
|
+
retainerThickness,
|
|
52127
|
+
pinLength,
|
|
52128
|
+
clevisGap
|
|
52129
|
+
}
|
|
52130
|
+
};
|
|
52131
|
+
}
|
|
52132
|
+
function seatedBearingAssembly(options) {
|
|
52133
|
+
const bearingOuterDiameter = requirePositive$6(options.bearingOuterDiameter, "bearingOuterDiameter");
|
|
52134
|
+
const bearingInnerDiameter = requirePositive$6(options.bearingInnerDiameter, "bearingInnerDiameter");
|
|
52135
|
+
const bearingWidth = requirePositive$6(options.bearingWidth, "bearingWidth");
|
|
52136
|
+
const shaftDiameter = requirePositive$6(options.shaftDiameter ?? Math.max(1, bearingInnerDiameter - 0.4), "shaftDiameter");
|
|
52137
|
+
const pocketClearance = requireNonNegative(options.pocketClearance ?? 0.2, "pocketClearance");
|
|
52138
|
+
const shaftClearance = requireNonNegative(options.shaftClearance ?? 0.35, "shaftClearance");
|
|
52139
|
+
const runningClearance = requireNonNegative(options.runningClearance ?? 0.05, "runningClearance");
|
|
52140
|
+
const housingThickness = requirePositive$6(options.housingThickness ?? bearingWidth + 5, "housingThickness");
|
|
52141
|
+
const bossHeight = requirePositive$6(options.bossHeight ?? Math.max(2, bearingWidth * 0.45), "bossHeight");
|
|
52142
|
+
const bossOuterDiameter = requirePositive$6(
|
|
52143
|
+
options.bossOuterDiameter ?? bearingOuterDiameter + Math.max(8, bearingOuterDiameter * 0.36),
|
|
52144
|
+
"bossOuterDiameter"
|
|
52145
|
+
);
|
|
52146
|
+
const housingWidth = requirePositive$6(options.housingWidth ?? Math.max(bossOuterDiameter + 12, bearingOuterDiameter * 2.1), "housingWidth");
|
|
52147
|
+
const housingDepth = requirePositive$6(options.housingDepth ?? Math.max(bossOuterDiameter + 12, bearingOuterDiameter * 1.8), "housingDepth");
|
|
52148
|
+
const shaftOverhang = requirePositive$6(options.shaftOverhang ?? Math.max(8, bearingOuterDiameter * 0.45), "shaftOverhang");
|
|
52149
|
+
const shoulderDiameter = requirePositive$6(options.shoulderDiameter ?? Math.max(shaftDiameter * 1.65, bearingInnerDiameter + 2), "shoulderDiameter");
|
|
52150
|
+
const shoulderThickness = requirePositive$6(options.shoulderThickness ?? Math.max(1.5, shaftDiameter * 0.32), "shoulderThickness");
|
|
52151
|
+
const segments = options.segments ?? 48;
|
|
52152
|
+
if (bearingOuterDiameter <= bearingInnerDiameter + Math.max(1, bearingOuterDiameter * 0.08)) {
|
|
52153
|
+
throw new Error("seatedBearingAssembly: bearingOuterDiameter leaves too little bearing wall around the bore");
|
|
52154
|
+
}
|
|
52155
|
+
if (shaftDiameter + shaftClearance >= bearingInnerDiameter) {
|
|
52156
|
+
throw new Error("seatedBearingAssembly: shaftDiameter plus shaftClearance must fit inside the bearing bore");
|
|
52157
|
+
}
|
|
52158
|
+
if (shoulderDiameter >= bearingOuterDiameter - runningClearance * 2) {
|
|
52159
|
+
throw new Error("seatedBearingAssembly: shoulderDiameter must stay smaller than the bearing outer race");
|
|
52160
|
+
}
|
|
52161
|
+
const pocketDiameter = bearingOuterDiameter + pocketClearance;
|
|
52162
|
+
const shaftBoreDiameter = shaftDiameter + shaftClearance;
|
|
52163
|
+
const totalHousingHeight = housingThickness + bossHeight;
|
|
52164
|
+
const pocketDepth = bearingWidth + runningClearance * 2;
|
|
52165
|
+
if (pocketDepth >= totalHousingHeight - runningClearance) {
|
|
52166
|
+
throw new Error("seatedBearingAssembly: housingThickness and bossHeight must leave a shoulder below the bearing pocket");
|
|
52167
|
+
}
|
|
52168
|
+
if (bossOuterDiameter <= pocketDiameter + Math.max(2, bearingOuterDiameter * 0.12)) {
|
|
52169
|
+
throw new Error("seatedBearingAssembly: bossOuterDiameter leaves too little wall around the bearing pocket");
|
|
52170
|
+
}
|
|
52171
|
+
if (housingWidth <= pocketDiameter + 6 || housingDepth <= pocketDiameter + 6) {
|
|
52172
|
+
throw new Error("seatedBearingAssembly: housing dimensions leave too little material around the bearing pocket");
|
|
52173
|
+
}
|
|
52174
|
+
if (shoulderThickness * 2 + runningClearance * 2 >= shaftOverhang) {
|
|
52175
|
+
throw new Error("seatedBearingAssembly: shaftOverhang must leave room for retaining collars outside the housing");
|
|
52176
|
+
}
|
|
52177
|
+
const pocketBottomZ = totalHousingHeight - pocketDepth;
|
|
52178
|
+
const bearingZ = pocketBottomZ + runningClearance;
|
|
52179
|
+
const lowerShoulderZ = -runningClearance - shoulderThickness;
|
|
52180
|
+
const upperShoulderZ = totalHousingHeight + runningClearance;
|
|
52181
|
+
const shaftLength = totalHousingHeight + shaftOverhang * 2;
|
|
52182
|
+
const bossFuseOverlap = Math.min(0.08, Math.max(0.02, bossHeight * 0.03));
|
|
52183
|
+
const bearingPocket = cylinder(pocketDepth + 0.4, pocketDiameter / 2, void 0, segments).translate(0, 0, pocketBottomZ - 0.2);
|
|
52184
|
+
const shaftBore = cylinder(totalHousingHeight + 1, shaftBoreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
|
|
52185
|
+
const housingBase = box(housingWidth, housingDepth, housingThickness).subtract(bearingPocket).subtract(shaftBore);
|
|
52186
|
+
const housingBoss = cylinder(bossHeight + bossFuseOverlap, bossOuterDiameter / 2, void 0, segments).translate(
|
|
52187
|
+
0,
|
|
52188
|
+
0,
|
|
52189
|
+
housingThickness - bossFuseOverlap
|
|
52190
|
+
).subtract(bearingPocket);
|
|
52191
|
+
const housing = union(housingBase, housingBoss).color("#475569");
|
|
52192
|
+
const bearingRing = tubeAlongZ(bearingWidth, bearingOuterDiameter / 2, bearingInnerDiameter / 2, segments);
|
|
52193
|
+
const shieldInset = Math.min(bearingWidth * 0.18, 0.7);
|
|
52194
|
+
const shieldOuterRadius = bearingOuterDiameter / 2 - Math.max(0.45, (bearingOuterDiameter - bearingInnerDiameter) * 0.08);
|
|
52195
|
+
const shieldInnerRadius = bearingInnerDiameter / 2 + Math.max(0.2, (bearingOuterDiameter - bearingInnerDiameter) * 0.035);
|
|
52196
|
+
const bearingShield = shieldOuterRadius > shieldInnerRadius + 0.2 ? union(
|
|
52197
|
+
tubeAlongZ(Math.min(0.35, bearingWidth * 0.08), shieldOuterRadius, shieldInnerRadius, segments).translate(0, 0, shieldInset),
|
|
52198
|
+
tubeAlongZ(Math.min(0.35, bearingWidth * 0.08), shieldOuterRadius, shieldInnerRadius, segments).translate(
|
|
52199
|
+
0,
|
|
52200
|
+
0,
|
|
52201
|
+
bearingWidth - shieldInset - Math.min(0.35, bearingWidth * 0.08)
|
|
52202
|
+
)
|
|
52203
|
+
) : null;
|
|
52204
|
+
const bearing = (bearingShield ? union(bearingRing, bearingShield) : bearingRing).translate(0, 0, bearingZ).color("#111827");
|
|
52205
|
+
const shaftCore = cylinder(shaftLength, shaftDiameter / 2, void 0, segments).translate(0, 0, -shaftOverhang);
|
|
52206
|
+
const lowerShoulder = cylinder(shoulderThickness, shoulderDiameter / 2, void 0, segments).translate(0, 0, lowerShoulderZ);
|
|
52207
|
+
const upperShoulder = cylinder(shoulderThickness, shoulderDiameter / 2, void 0, segments).translate(0, 0, upperShoulderZ);
|
|
52208
|
+
const shaft = union(shaftCore, lowerShoulder, upperShoulder).color("#cbd5e1");
|
|
52209
|
+
const parts = [
|
|
52210
|
+
{ name: "bearing housing with counterbore pocket and shoulder", shape: housing },
|
|
52211
|
+
{ name: "purchased radial bearing seated in counterbore", shape: bearing },
|
|
52212
|
+
{ name: "shaft through bearing bore with retaining collars", shape: shaft }
|
|
52213
|
+
];
|
|
52214
|
+
return {
|
|
52215
|
+
parts,
|
|
52216
|
+
housing,
|
|
52217
|
+
bearing,
|
|
52218
|
+
shaft,
|
|
52219
|
+
cutters: {
|
|
52220
|
+
bearingPocket,
|
|
52221
|
+
shaftBore
|
|
52222
|
+
},
|
|
52223
|
+
dims: {
|
|
52224
|
+
bearingOuterDiameter,
|
|
52225
|
+
bearingInnerDiameter,
|
|
52226
|
+
bearingWidth,
|
|
52227
|
+
shaftDiameter,
|
|
52228
|
+
housingWidth,
|
|
52229
|
+
housingDepth,
|
|
52230
|
+
housingThickness,
|
|
52231
|
+
bossOuterDiameter,
|
|
52232
|
+
bossHeight,
|
|
52233
|
+
totalHousingHeight,
|
|
52234
|
+
pocketDiameter,
|
|
52235
|
+
pocketDepth,
|
|
52236
|
+
shaftBoreDiameter,
|
|
52237
|
+
runningClearance,
|
|
52238
|
+
shaftLength,
|
|
52239
|
+
shoulderDiameter,
|
|
52240
|
+
shoulderThickness
|
|
52241
|
+
}
|
|
52242
|
+
};
|
|
52243
|
+
}
|
|
52244
|
+
function cableGlandAnchorAssembly(options) {
|
|
52245
|
+
const cableDiameter = requirePositive$6(options.cableDiameter, "cableDiameter");
|
|
52246
|
+
const panelThickness = requirePositive$6(options.panelThickness ?? 3, "panelThickness");
|
|
52247
|
+
const panelWidth = requirePositive$6(options.panelWidth ?? Math.max(54, cableDiameter * 7), "panelWidth");
|
|
52248
|
+
const panelHeight = requirePositive$6(options.panelHeight ?? Math.max(38, cableDiameter * 5), "panelHeight");
|
|
52249
|
+
const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
|
|
52250
|
+
const panelHoleClearance = requirePositive$6(options.panelHoleClearance ?? 0.25, "panelHoleClearance");
|
|
52251
|
+
const cableBoreDiameter = cableDiameter + runningClearance * 2;
|
|
52252
|
+
const glandOuterDiameter = requirePositive$6(options.glandOuterDiameter ?? cableDiameter + Math.max(6, cableDiameter * 0.9), "glandOuterDiameter");
|
|
52253
|
+
const nutOuterDiameter = requirePositive$6(options.nutOuterDiameter ?? glandOuterDiameter + Math.max(6, cableDiameter * 0.8), "nutOuterDiameter");
|
|
52254
|
+
const nutThickness = requirePositive$6(options.nutThickness ?? Math.max(4, cableDiameter * 0.8), "nutThickness");
|
|
52255
|
+
const flangeDiameter = requirePositive$6(options.flangeDiameter ?? glandOuterDiameter + Math.max(5, cableDiameter * 0.7), "flangeDiameter");
|
|
52256
|
+
const flangeThickness = requirePositive$6(options.flangeThickness ?? Math.max(2, panelThickness * 0.45), "flangeThickness");
|
|
52257
|
+
const minGlandLength = panelThickness + nutThickness + flangeThickness + runningClearance * 4;
|
|
52258
|
+
const glandLength = requirePositive$6(options.glandLength ?? minGlandLength + Math.max(8, cableDiameter), "glandLength");
|
|
52259
|
+
const cableLength = requirePositive$6(options.cableLength ?? glandLength + Math.max(36, cableDiameter * 5), "cableLength");
|
|
52260
|
+
const segments = options.segments ?? 40;
|
|
52261
|
+
if (glandOuterDiameter <= cableBoreDiameter + Math.max(1.2, cableDiameter * 0.18)) {
|
|
52262
|
+
throw new Error("cableGlandAnchorAssembly: glandOuterDiameter leaves too little wall around the cable bore");
|
|
52263
|
+
}
|
|
52264
|
+
if (nutOuterDiameter <= glandOuterDiameter + Math.max(1.5, cableDiameter * 0.2)) {
|
|
52265
|
+
throw new Error("cableGlandAnchorAssembly: nutOuterDiameter must leave material around the gland body");
|
|
52266
|
+
}
|
|
52267
|
+
if (flangeDiameter <= glandOuterDiameter + Math.max(1.2, cableDiameter * 0.16)) {
|
|
52268
|
+
throw new Error("cableGlandAnchorAssembly: flangeDiameter must be larger than the gland body");
|
|
52269
|
+
}
|
|
52270
|
+
if (panelWidth <= flangeDiameter + 8 || panelHeight <= flangeDiameter + 8) {
|
|
52271
|
+
throw new Error("cableGlandAnchorAssembly: panel dimensions leave too little material around the gland hole");
|
|
52272
|
+
}
|
|
52273
|
+
if (glandLength <= minGlandLength) {
|
|
52274
|
+
throw new Error("cableGlandAnchorAssembly: glandLength must span the panel, flange, compression nut, and clearances");
|
|
52275
|
+
}
|
|
52276
|
+
if (cableLength <= glandLength + runningClearance * 2) {
|
|
52277
|
+
throw new Error("cableGlandAnchorAssembly: cableLength must extend beyond the gland body");
|
|
52278
|
+
}
|
|
52279
|
+
const panelHoleDiameter = glandOuterDiameter + panelHoleClearance * 2;
|
|
52280
|
+
const glandOuterRadius = glandOuterDiameter / 2;
|
|
52281
|
+
const cableBoreRadius = cableBoreDiameter / 2;
|
|
52282
|
+
const faceClearance = Math.min(0.05, runningClearance * 0.15);
|
|
52283
|
+
const flangePocketDepth = Math.min(Math.max(0.35, panelThickness * 0.18), panelThickness * 0.4, flangeThickness * 0.55);
|
|
52284
|
+
const panelHole = cylinderAlongX(panelThickness + 0.8, panelHoleDiameter / 2, 0, segments);
|
|
52285
|
+
const flangeSeatPocket = cylinderAlongX(
|
|
52286
|
+
flangePocketDepth + 0.2,
|
|
52287
|
+
flangeDiameter / 2 + panelHoleClearance,
|
|
52288
|
+
panelThickness / 2 - flangePocketDepth / 2,
|
|
52289
|
+
segments
|
|
52290
|
+
);
|
|
52291
|
+
const cableBore = cylinderAlongX(glandLength + 0.8, cableBoreRadius, 0, segments);
|
|
52292
|
+
const panel = box(panelThickness, panelWidth, panelHeight).translate(0, 0, -panelHeight / 2).subtract(panelHole).subtract(flangeSeatPocket).color("#475569");
|
|
52293
|
+
const glandBody = tubeAlongX(glandLength, glandOuterRadius, cableBoreRadius, 0, segments);
|
|
52294
|
+
const flangeCenterX = panelThickness / 2 - flangePocketDepth + faceClearance + flangeThickness / 2;
|
|
52295
|
+
const flange = tubeAlongX(flangeThickness, flangeDiameter / 2, cableBoreRadius, flangeCenterX, segments);
|
|
52296
|
+
const gland = union(glandBody, flange).color("#94a3b8");
|
|
52297
|
+
const nutInnerRadius = glandOuterRadius + Math.min(0.12, runningClearance * 0.4);
|
|
52298
|
+
const nutCenterX = -panelThickness / 2 - faceClearance - nutThickness / 2;
|
|
52299
|
+
const compressionNut = tubeAlongX(nutThickness, nutOuterDiameter / 2, nutInnerRadius, nutCenterX, segments).color("#cbd5e1");
|
|
52300
|
+
const cable = cylinderAlongX(cableLength, cableDiameter / 2, 0, segments).color("#111827");
|
|
52301
|
+
const parts = [
|
|
52302
|
+
{ name: "panel with gland clearance hole", shape: panel },
|
|
52303
|
+
{ name: "hollow cable gland body with panel flange", shape: gland },
|
|
52304
|
+
{ name: "compression nut around gland body", shape: compressionNut },
|
|
52305
|
+
{ name: "routed cable through gland bore", shape: cable }
|
|
52306
|
+
];
|
|
52307
|
+
return {
|
|
52308
|
+
parts,
|
|
52309
|
+
panel,
|
|
52310
|
+
gland,
|
|
52311
|
+
compressionNut,
|
|
52312
|
+
cable,
|
|
52313
|
+
cutters: {
|
|
52314
|
+
panelHole,
|
|
52315
|
+
flangeSeatPocket,
|
|
52316
|
+
cableBore
|
|
52317
|
+
},
|
|
52318
|
+
dims: {
|
|
52319
|
+
cableDiameter,
|
|
52320
|
+
cableBoreDiameter,
|
|
52321
|
+
panelThickness,
|
|
52322
|
+
panelWidth,
|
|
52323
|
+
panelHeight,
|
|
52324
|
+
glandOuterDiameter,
|
|
52325
|
+
glandLength,
|
|
52326
|
+
nutOuterDiameter,
|
|
52327
|
+
nutThickness,
|
|
52328
|
+
flangeDiameter,
|
|
52329
|
+
flangeThickness,
|
|
52330
|
+
runningClearance,
|
|
52331
|
+
faceClearance,
|
|
52332
|
+
flangePocketDepth,
|
|
52333
|
+
panelHoleDiameter,
|
|
52334
|
+
cableLength
|
|
52335
|
+
}
|
|
52336
|
+
};
|
|
52337
|
+
}
|
|
52338
|
+
function hoseBarbPortAssembly(options) {
|
|
52339
|
+
const hoseInnerDiameter = requirePositive$6(options.hoseInnerDiameter, "hoseInnerDiameter");
|
|
52340
|
+
const runningClearance = requirePositive$6(options.runningClearance ?? 0.18, "runningClearance");
|
|
52341
|
+
const faceClearance = requirePositive$6(options.faceClearance ?? 0.04, "faceClearance");
|
|
52342
|
+
const barbRootDiameter = requirePositive$6(
|
|
52343
|
+
options.barbRootDiameter ?? Math.max(1, hoseInnerDiameter - Math.max(0.25, hoseInnerDiameter * 0.06)),
|
|
52344
|
+
"barbRootDiameter"
|
|
52345
|
+
);
|
|
52346
|
+
const barbPeakDiameter = requirePositive$6(
|
|
52347
|
+
options.barbPeakDiameter ?? hoseInnerDiameter + Math.max(0.65, hoseInnerDiameter * 0.12),
|
|
52348
|
+
"barbPeakDiameter"
|
|
52349
|
+
);
|
|
52350
|
+
const installedHoseBoreDiameter = barbPeakDiameter + runningClearance * 2;
|
|
52351
|
+
const hoseOuterDiameter = requirePositive$6(
|
|
52352
|
+
options.hoseOuterDiameter ?? Math.max(installedHoseBoreDiameter + 2.4, hoseInnerDiameter + Math.max(3, hoseInnerDiameter * 0.55)),
|
|
52353
|
+
"hoseOuterDiameter"
|
|
52354
|
+
);
|
|
52355
|
+
const fluidBoreDiameter = requirePositive$6(options.fluidBoreDiameter ?? hoseInnerDiameter * 0.65, "fluidBoreDiameter");
|
|
52356
|
+
const blockThickness = requirePositive$6(options.blockThickness ?? Math.max(7, hoseInnerDiameter * 1.2), "blockThickness");
|
|
52357
|
+
const barbCount = options.barbCount ?? 3;
|
|
52358
|
+
const barbLength = requirePositive$6(options.barbLength ?? Math.max(2.6, hoseInnerDiameter * 0.55), "barbLength");
|
|
52359
|
+
const barbStackLength = barbCount * barbLength;
|
|
52360
|
+
const shoulderDiameter = requirePositive$6(
|
|
52361
|
+
options.shoulderDiameter ?? barbPeakDiameter + Math.max(4, hoseInnerDiameter * 0.65),
|
|
52362
|
+
"shoulderDiameter"
|
|
52363
|
+
);
|
|
52364
|
+
const shoulderThickness = requirePositive$6(options.shoulderThickness ?? Math.max(2, hoseInnerDiameter * 0.35), "shoulderThickness");
|
|
52365
|
+
const bossDiameter = requirePositive$6(options.bossDiameter ?? shoulderDiameter + Math.max(4, hoseInnerDiameter * 0.6), "bossDiameter");
|
|
52366
|
+
const bossHeight = requirePositive$6(options.bossHeight ?? Math.max(2.4, hoseInnerDiameter * 0.45), "bossHeight");
|
|
52367
|
+
const blockWidth = requirePositive$6(options.blockWidth ?? bossDiameter + Math.max(14, hoseInnerDiameter * 2.4), "blockWidth");
|
|
52368
|
+
const blockHeight = requirePositive$6(options.blockHeight ?? bossDiameter + Math.max(12, hoseInnerDiameter * 2.1), "blockHeight");
|
|
52369
|
+
const hoseLength = requirePositive$6(options.hoseLength ?? barbStackLength + Math.max(32, hoseInnerDiameter * 5), "hoseLength");
|
|
52370
|
+
const clampWidth = requirePositive$6(options.clampWidth ?? Math.max(4, hoseOuterDiameter * 0.45), "clampWidth");
|
|
52371
|
+
const clampThickness = requirePositive$6(options.clampThickness ?? 0.9, "clampThickness");
|
|
52372
|
+
const segments = options.segments ?? 40;
|
|
52373
|
+
if (!Number.isInteger(barbCount) || barbCount < 1 || barbCount > 8) {
|
|
52374
|
+
throw new Error("hoseBarbPortAssembly: barbCount must be an integer from 1 to 8");
|
|
52375
|
+
}
|
|
52376
|
+
if (barbPeakDiameter <= hoseInnerDiameter) {
|
|
52377
|
+
throw new Error("hoseBarbPortAssembly: barbPeakDiameter must exceed hoseInnerDiameter so the barb retains the hose");
|
|
52378
|
+
}
|
|
52379
|
+
if (barbRootDiameter >= barbPeakDiameter - Math.max(0.25, hoseInnerDiameter * 0.04)) {
|
|
52380
|
+
throw new Error("hoseBarbPortAssembly: barbRootDiameter must leave a visible barb rise");
|
|
52381
|
+
}
|
|
52382
|
+
if (fluidBoreDiameter >= barbRootDiameter - Math.max(0.8, hoseInnerDiameter * 0.12)) {
|
|
52383
|
+
throw new Error("hoseBarbPortAssembly: fluidBoreDiameter leaves too little wall in the barb fitting");
|
|
52384
|
+
}
|
|
52385
|
+
if (hoseOuterDiameter <= installedHoseBoreDiameter + Math.max(1.2, hoseInnerDiameter * 0.16)) {
|
|
52386
|
+
throw new Error("hoseBarbPortAssembly: hoseOuterDiameter leaves too little hose wall around the installed barb envelope");
|
|
52387
|
+
}
|
|
52388
|
+
if (shoulderDiameter <= barbPeakDiameter + Math.max(1.5, hoseInnerDiameter * 0.2)) {
|
|
52389
|
+
throw new Error("hoseBarbPortAssembly: shoulderDiameter must be larger than the barb peaks");
|
|
52390
|
+
}
|
|
52391
|
+
if (bossDiameter <= shoulderDiameter + Math.max(1.5, hoseInnerDiameter * 0.2)) {
|
|
52392
|
+
throw new Error("hoseBarbPortAssembly: bossDiameter must leave material around the shoulder seat");
|
|
52393
|
+
}
|
|
52394
|
+
if (blockWidth <= bossDiameter + 8 || blockHeight <= bossDiameter + 8) {
|
|
52395
|
+
throw new Error("hoseBarbPortAssembly: receiver block dimensions leave too little material around the port boss");
|
|
52396
|
+
}
|
|
52397
|
+
const portBoreDiameter = barbRootDiameter + runningClearance * 2;
|
|
52398
|
+
const portBore = cylinderAlongX(blockThickness + bossHeight + 0.8, portBoreDiameter / 2, bossHeight / 2, segments);
|
|
52399
|
+
const fuseOverlap = Math.min(0.04, faceClearance * 0.7);
|
|
52400
|
+
const bossCenterX = blockThickness / 2 + bossHeight / 2 - fuseOverlap;
|
|
52401
|
+
const receiver = union(
|
|
52402
|
+
box(blockThickness, blockWidth, blockHeight).translate(0, 0, -blockHeight / 2),
|
|
52403
|
+
cylinderAlongX(bossHeight + fuseOverlap, bossDiameter / 2, bossCenterX, segments)
|
|
52404
|
+
).subtract(portBore).color("#475569");
|
|
52405
|
+
const bossFaceX = blockThickness / 2 + bossHeight;
|
|
52406
|
+
const shoulderCenterX = bossFaceX + faceClearance + shoulderThickness / 2;
|
|
52407
|
+
const barbStartX = shoulderCenterX + shoulderThickness / 2;
|
|
52408
|
+
const fittingStartX = -blockThickness / 2 - runningClearance;
|
|
52409
|
+
const fittingEndX = barbStartX + barbStackLength;
|
|
52410
|
+
const fittingCore = tubeAlongX(fittingEndX - fittingStartX, barbRootDiameter / 2, fluidBoreDiameter / 2, (fittingStartX + fittingEndX) / 2, segments);
|
|
52411
|
+
const shoulder = tubeAlongX(shoulderThickness, shoulderDiameter / 2, fluidBoreDiameter / 2, shoulderCenterX, segments);
|
|
52412
|
+
const barbSolids = [];
|
|
52413
|
+
const ridgeLength = Math.max(0.8, Math.min(barbLength * 0.45, hoseInnerDiameter * 0.28));
|
|
52414
|
+
for (let index2 = 0; index2 < barbCount; index2 += 1) {
|
|
52415
|
+
const startX = barbStartX + index2 * barbLength;
|
|
52416
|
+
const ridgeCenterX = startX + barbLength - ridgeLength / 2;
|
|
52417
|
+
barbSolids.push(tubeAlongX(ridgeLength, barbPeakDiameter / 2, fluidBoreDiameter / 2, ridgeCenterX, segments));
|
|
52418
|
+
}
|
|
52419
|
+
const fitting = union(fittingCore, shoulder, ...barbSolids).color("#94a3b8");
|
|
52420
|
+
const hoseStartX = barbStartX + faceClearance;
|
|
52421
|
+
const hoseCenterX = hoseStartX + hoseLength / 2;
|
|
52422
|
+
const installedHoseBore = cylinderAlongX(hoseLength + 0.8, installedHoseBoreDiameter / 2, hoseCenterX, segments);
|
|
52423
|
+
const hose = tubeAlongX(hoseLength, hoseOuterDiameter / 2, installedHoseBoreDiameter / 2, hoseCenterX, segments).color("#111827");
|
|
52424
|
+
const clampCenterX = barbStartX + Math.min(barbStackLength * 0.55, Math.max(barbLength, clampWidth));
|
|
52425
|
+
const clamp2 = tubeAlongX(
|
|
52426
|
+
clampWidth,
|
|
52427
|
+
hoseOuterDiameter / 2 + clampThickness,
|
|
52428
|
+
hoseOuterDiameter / 2 + Math.min(0.08, runningClearance * 0.45),
|
|
52429
|
+
clampCenterX,
|
|
52430
|
+
segments
|
|
52431
|
+
).color("#cbd5e1");
|
|
52432
|
+
const parts = [
|
|
52433
|
+
{ name: "bored pump or filter body with raised hose-port boss", shape: receiver },
|
|
52434
|
+
{ name: "hollow hose barb fitting with shoulder and retention ridges", shape: fitting },
|
|
52435
|
+
{ name: "installed flexible hose over barb tail", shape: hose },
|
|
52436
|
+
{ name: "clamp band over hose and barb ridges", shape: clamp2 }
|
|
52437
|
+
];
|
|
52438
|
+
return {
|
|
52439
|
+
parts,
|
|
52440
|
+
receiver,
|
|
52441
|
+
fitting,
|
|
52442
|
+
hose,
|
|
52443
|
+
clamp: clamp2,
|
|
52444
|
+
cutters: {
|
|
52445
|
+
portBore,
|
|
52446
|
+
installedHoseBore
|
|
52447
|
+
},
|
|
52448
|
+
dims: {
|
|
52449
|
+
hoseInnerDiameter,
|
|
52450
|
+
hoseOuterDiameter,
|
|
52451
|
+
installedHoseBoreDiameter,
|
|
52452
|
+
blockThickness,
|
|
52453
|
+
blockWidth,
|
|
52454
|
+
blockHeight,
|
|
52455
|
+
bossDiameter,
|
|
52456
|
+
bossHeight,
|
|
52457
|
+
fluidBoreDiameter,
|
|
52458
|
+
barbRootDiameter,
|
|
52459
|
+
barbPeakDiameter,
|
|
52460
|
+
barbCount,
|
|
52461
|
+
barbLength,
|
|
52462
|
+
barbStackLength,
|
|
52463
|
+
shoulderDiameter,
|
|
52464
|
+
shoulderThickness,
|
|
52465
|
+
hoseLength,
|
|
52466
|
+
clampWidth,
|
|
52467
|
+
clampThickness,
|
|
52468
|
+
runningClearance,
|
|
52469
|
+
faceClearance
|
|
52470
|
+
}
|
|
52471
|
+
};
|
|
52472
|
+
}
|
|
52473
|
+
function routedTubeClipAssembly(options) {
|
|
52474
|
+
const tubeDiameter = requirePositive$6(options.tubeDiameter, "tubeDiameter");
|
|
52475
|
+
const tubeLength = requirePositive$6(options.tubeLength ?? 120, "tubeLength");
|
|
52476
|
+
const panelThickness = requirePositive$6(options.panelThickness ?? 3, "panelThickness");
|
|
52477
|
+
const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
|
|
52478
|
+
const screwSize = options.screwSize ?? "M3";
|
|
52479
|
+
const segments = options.segments ?? 32;
|
|
52480
|
+
const sizeData = METRIC_HOLE_TABLE[screwSize];
|
|
52481
|
+
if (!sizeData) throw new Error(`routedTubeClipAssembly: unsupported screwSize "${screwSize}"`);
|
|
52482
|
+
const clipCount = options.clipCount ?? 3;
|
|
52483
|
+
if (!Number.isInteger(clipCount) || clipCount < 1 || clipCount > 8) {
|
|
52484
|
+
throw new Error("routedTubeClipAssembly: clipCount must be an integer from 1 to 8");
|
|
52485
|
+
}
|
|
52486
|
+
const screwDiameter = parseFloat(screwSize.replace("M", ""));
|
|
52487
|
+
const screwHeadDiameter = sizeData.head;
|
|
52488
|
+
const tubeBoreDiameter = tubeDiameter + runningClearance * 2;
|
|
52489
|
+
const clipWallThickness = requirePositive$6(
|
|
52490
|
+
options.clipWallThickness ?? Math.max(screwHeadDiameter + 1.2, tubeDiameter * 0.45, 5),
|
|
52491
|
+
"clipWallThickness"
|
|
52492
|
+
);
|
|
52493
|
+
const clipWidth = requirePositive$6(options.clipWidth ?? Math.max(screwHeadDiameter + 3, tubeDiameter * 1.4, 10), "clipWidth");
|
|
52494
|
+
const clipDepth = tubeBoreDiameter + clipWallThickness * 2;
|
|
52495
|
+
const bottomWall = Math.max(1.2, clipWallThickness * 0.35);
|
|
52496
|
+
const topWall = Math.max(2, clipWallThickness * 0.45);
|
|
52497
|
+
const clipHeight = bottomWall + tubeBoreDiameter + topWall;
|
|
52498
|
+
const tubeCenterZ = panelThickness + bottomWall + tubeBoreDiameter / 2;
|
|
52499
|
+
const panelLength = requirePositive$6(options.panelLength ?? tubeLength + 24, "panelLength");
|
|
52500
|
+
const panelWidth = requirePositive$6(options.panelWidth ?? clipDepth + Math.max(14, screwHeadDiameter * 2), "panelWidth");
|
|
52501
|
+
if (tubeLength <= clipWidth + 8) {
|
|
52502
|
+
throw new Error("routedTubeClipAssembly: tubeLength must leave visible tube beyond the clip body");
|
|
52503
|
+
}
|
|
52504
|
+
const defaultSpacing = clipCount === 1 ? 0 : Math.max(clipWidth + 8, (tubeLength - clipWidth * 2) / (clipCount - 1));
|
|
52505
|
+
const clipSpacing = options.clipSpacing === void 0 ? defaultSpacing : requirePositive$6(options.clipSpacing, "clipSpacing");
|
|
52506
|
+
const clipCenters = Array.from({ length: clipCount }, (_2, index2) => (index2 - (clipCount - 1) / 2) * clipSpacing);
|
|
52507
|
+
const maxClipExtent = Math.max(...clipCenters.map((x2) => Math.abs(x2) + clipWidth / 2));
|
|
52508
|
+
if (maxClipExtent > tubeLength / 2 - 2) {
|
|
52509
|
+
throw new Error("routedTubeClipAssembly: clipSpacing places a clip beyond the routed tube length");
|
|
52510
|
+
}
|
|
52511
|
+
if (maxClipExtent > panelLength / 2 - 2) {
|
|
52512
|
+
throw new Error("routedTubeClipAssembly: panelLength is too short for the clip pattern");
|
|
52513
|
+
}
|
|
52514
|
+
const boreRadius = tubeBoreDiameter / 2;
|
|
52515
|
+
const screwY = boreRadius + clipWallThickness / 2;
|
|
52516
|
+
if (screwY + screwHeadDiameter / 2 > clipDepth / 2 - 0.2) {
|
|
52517
|
+
throw new Error("routedTubeClipAssembly: clipWallThickness leaves too little land for screw heads");
|
|
52518
|
+
}
|
|
52519
|
+
if (clipDepth > panelWidth - Math.max(4, screwHeadDiameter * 0.5)) {
|
|
52520
|
+
throw new Error("routedTubeClipAssembly: panelWidth leaves too little material beside the clips");
|
|
52521
|
+
}
|
|
52522
|
+
const screwPositions = clipCenters.flatMap((x2) => [
|
|
52523
|
+
[x2, -screwY],
|
|
52524
|
+
[x2, screwY]
|
|
52525
|
+
]);
|
|
52526
|
+
const screwClearanceDiameter = Math.max(sizeData.loose, screwDiameter + 0.8);
|
|
52527
|
+
const panelThreadEnvelopeDiameter = screwClearanceDiameter;
|
|
52528
|
+
const clipTopZ = panelThickness + clipHeight;
|
|
52529
|
+
const clipTubeBores = union(
|
|
52530
|
+
...clipCenters.map((x2) => cylinderAlongX(clipWidth + 0.8, boreRadius, x2, segments).translate(0, 0, tubeCenterZ))
|
|
52531
|
+
);
|
|
52532
|
+
const clipScrewClearances = union(
|
|
52533
|
+
...screwPositions.map(([x2, y2]) => cylinder(clipHeight + 0.8, screwClearanceDiameter / 2, void 0, segments).translate(x2, y2, panelThickness - 0.4))
|
|
52534
|
+
);
|
|
52535
|
+
const panelThreadEnvelopes = union(
|
|
52536
|
+
...screwPositions.map(([x2, y2]) => cylinder(panelThickness + 0.8, panelThreadEnvelopeDiameter / 2, void 0, segments).translate(x2, y2, -0.4))
|
|
52537
|
+
);
|
|
52538
|
+
const panel = box(panelLength, panelWidth, panelThickness).subtract(panelThreadEnvelopes).color("#475569");
|
|
52539
|
+
const tube2 = cylinderAlongX(tubeLength, tubeDiameter / 2, 0, segments).translate(0, 0, tubeCenterZ).color("#0f172a");
|
|
52540
|
+
const clips = clipCenters.map((x2) => {
|
|
52541
|
+
const body = box(clipWidth, clipDepth, clipHeight).translate(x2, 0, panelThickness);
|
|
52542
|
+
const tubeBore = cylinderAlongX(clipWidth + 0.8, boreRadius, x2, segments).translate(0, 0, tubeCenterZ);
|
|
52543
|
+
const screwHoles = union(
|
|
52544
|
+
cylinder(clipHeight + 0.8, screwClearanceDiameter / 2, void 0, segments).translate(x2, -screwY, panelThickness - 0.4),
|
|
52545
|
+
cylinder(clipHeight + 0.8, screwClearanceDiameter / 2, void 0, segments).translate(x2, screwY, panelThickness - 0.4)
|
|
52546
|
+
);
|
|
52547
|
+
return body.subtract(tubeBore).subtract(screwHoles).color("#94a3b8");
|
|
52548
|
+
});
|
|
52549
|
+
const screwLength = clipHeight + panelThickness * 0.65;
|
|
52550
|
+
const screwHeadHeight = Math.max(1.2, screwDiameter * 0.55);
|
|
52551
|
+
const screwBlank = union(
|
|
52552
|
+
cylinder(screwLength, screwDiameter / 2, void 0, segments).translate(0, 0, clipTopZ - screwLength),
|
|
52553
|
+
cylinder(screwHeadHeight, screwHeadDiameter / 2, void 0, segments).translate(0, 0, clipTopZ)
|
|
52554
|
+
).color("#cbd5e1");
|
|
52555
|
+
const screws = screwPositions.map(([x2, y2]) => screwBlank.translate(x2, y2, 0));
|
|
52556
|
+
const parts = [
|
|
52557
|
+
{ name: "panel with tube-clip screw receiving holes", shape: panel },
|
|
52558
|
+
{ name: "routed flexible tube through retained clip bores", shape: tube2 },
|
|
52559
|
+
...clips.map((shape, index2) => ({ name: `saddle tube clip ${index2 + 1} with through-bore`, shape })),
|
|
52560
|
+
...screws.map((shape, index2) => ({ name: `installed ${screwSize} tube clip screw ${index2 + 1}`, shape }))
|
|
52561
|
+
];
|
|
52562
|
+
return {
|
|
52563
|
+
parts,
|
|
52564
|
+
panel,
|
|
52565
|
+
tube: tube2,
|
|
52566
|
+
clips,
|
|
52567
|
+
screws,
|
|
52568
|
+
clipCenters,
|
|
52569
|
+
screwPositions,
|
|
52570
|
+
cutters: {
|
|
52571
|
+
clipTubeBores,
|
|
52572
|
+
clipScrewClearances,
|
|
52573
|
+
panelThreadEnvelopes
|
|
52574
|
+
},
|
|
52575
|
+
dims: {
|
|
52576
|
+
tubeDiameter,
|
|
52577
|
+
tubeLength,
|
|
52578
|
+
tubeBoreDiameter,
|
|
52579
|
+
panelLength,
|
|
52580
|
+
panelWidth,
|
|
52581
|
+
panelThickness,
|
|
52582
|
+
clipCount,
|
|
52583
|
+
clipWidth,
|
|
52584
|
+
clipDepth,
|
|
52585
|
+
clipHeight,
|
|
52586
|
+
clipWallThickness,
|
|
52587
|
+
tubeCenterZ,
|
|
52588
|
+
screwSize,
|
|
52589
|
+
screwDiameter,
|
|
52590
|
+
screwHeadDiameter,
|
|
52591
|
+
screwLength,
|
|
52592
|
+
screwClearanceDiameter,
|
|
52593
|
+
panelThreadEnvelopeDiameter,
|
|
52594
|
+
runningClearance
|
|
52595
|
+
}
|
|
52596
|
+
};
|
|
52597
|
+
}
|
|
52598
|
+
function pcbTerminalBlockAssembly(options = {}) {
|
|
52599
|
+
const terminalCount = options.terminalCount ?? 4;
|
|
52600
|
+
if (!Number.isInteger(terminalCount) || terminalCount < 1 || terminalCount > 24) {
|
|
52601
|
+
throw new Error("pcbTerminalBlockAssembly: terminalCount must be an integer from 1 to 24");
|
|
52602
|
+
}
|
|
52603
|
+
const terminalPitch = requirePositive$6(options.terminalPitch ?? 5.08, "terminalPitch");
|
|
52604
|
+
const terminalBlockWidth = terminalPitch * terminalCount + 3;
|
|
52605
|
+
const boardWidth = requirePositive$6(options.boardWidth ?? Math.max(50, terminalBlockWidth + 28), "boardWidth");
|
|
52606
|
+
const boardDepth = requirePositive$6(options.boardDepth ?? 38, "boardDepth");
|
|
52607
|
+
const boardThickness = requirePositive$6(options.boardThickness ?? 1.6, "boardThickness");
|
|
52608
|
+
const backplateThickness = requirePositive$6(options.backplateThickness ?? 3, "backplateThickness");
|
|
52609
|
+
const backplateMargin = requirePositive$6(options.backplateMargin ?? 5, "backplateMargin");
|
|
52610
|
+
const standoffHeight = requirePositive$6(options.standoffHeight ?? 6, "standoffHeight");
|
|
52611
|
+
const screwSize = options.screwSize ?? "M3";
|
|
52612
|
+
const segments = options.segments ?? 28;
|
|
52613
|
+
const sizeData = METRIC_HOLE_TABLE[screwSize];
|
|
52614
|
+
if (!sizeData) throw new Error(`pcbTerminalBlockAssembly: unsupported screwSize "${screwSize}"`);
|
|
52615
|
+
const screwDiameter = parseFloat(screwSize.replace("M", ""));
|
|
52616
|
+
const screwHeadDiameter = sizeData.head;
|
|
52617
|
+
const screwHeadHeight = Math.max(1.2, screwDiameter * 0.55);
|
|
52618
|
+
const standoffDiameter = requirePositive$6(
|
|
52619
|
+
options.standoffDiameter ?? Math.max(screwHeadDiameter * 1.45, sizeData.normal + 3),
|
|
52620
|
+
"standoffDiameter"
|
|
52621
|
+
);
|
|
52622
|
+
const [mountInsetX, mountInsetY] = resolveBoltInset(
|
|
52623
|
+
options.mountingInset,
|
|
52624
|
+
Math.max(standoffDiameter / 2 + 1.2, screwHeadDiameter * 0.75)
|
|
52625
|
+
);
|
|
52626
|
+
if (mountInsetX * 2 >= boardWidth || mountInsetY * 2 >= boardDepth) {
|
|
52627
|
+
throw new Error("pcbTerminalBlockAssembly: mountingInset leaves no room for the PCB mounting pattern");
|
|
52628
|
+
}
|
|
52629
|
+
const terminalBlockDepth = requirePositive$6(options.terminalBlockDepth ?? 10, "terminalBlockDepth");
|
|
52630
|
+
const terminalBlockHeight = requirePositive$6(options.terminalBlockHeight ?? 9, "terminalBlockHeight");
|
|
52631
|
+
const terminalEdgeInset = requirePositive$6(options.terminalEdgeInset ?? 5, "terminalEdgeInset");
|
|
52632
|
+
const pinDiameter = requirePositive$6(options.pinDiameter ?? 0.9, "pinDiameter");
|
|
52633
|
+
const pinClearance = requirePositive$6(options.pinClearance ?? 0.25, "pinClearance");
|
|
52634
|
+
const pinTailLength = requireNonNegative(options.pinTailLength ?? 0, "pinTailLength");
|
|
52635
|
+
const wirePortDiameter = requirePositive$6(options.wirePortDiameter ?? 2.6, "wirePortDiameter");
|
|
52636
|
+
const pinHoleDiameter = pinDiameter + pinClearance;
|
|
52637
|
+
const terminalCenterY = -boardDepth / 2 + terminalEdgeInset + terminalBlockDepth / 2;
|
|
52638
|
+
const pinY = terminalCenterY + terminalBlockDepth * 0.24;
|
|
52639
|
+
const firstPinX = -((terminalCount - 1) * terminalPitch) / 2;
|
|
52640
|
+
const pinPositions = Array.from({ length: terminalCount }, (_2, index2) => [firstPinX + index2 * terminalPitch, pinY]);
|
|
52641
|
+
const mountingPositions = [
|
|
52642
|
+
[-boardWidth / 2 + mountInsetX, -boardDepth / 2 + mountInsetY],
|
|
52643
|
+
[boardWidth / 2 - mountInsetX, -boardDepth / 2 + mountInsetY],
|
|
52644
|
+
[-boardWidth / 2 + mountInsetX, boardDepth / 2 - mountInsetY],
|
|
52645
|
+
[boardWidth / 2 - mountInsetX, boardDepth / 2 - mountInsetY]
|
|
52646
|
+
];
|
|
52647
|
+
if (terminalBlockWidth >= boardWidth - mountInsetX * 2) {
|
|
52648
|
+
throw new Error("pcbTerminalBlockAssembly: terminal block is too wide for the PCB mounting pattern");
|
|
52649
|
+
}
|
|
52650
|
+
if (terminalEdgeInset + terminalBlockDepth >= boardDepth - mountInsetY * 2) {
|
|
52651
|
+
throw new Error("pcbTerminalBlockAssembly: terminal block depth collides with the rear mounting datum");
|
|
52652
|
+
}
|
|
52653
|
+
if (pinHoleDiameter >= terminalPitch * 0.55) {
|
|
52654
|
+
throw new Error("pcbTerminalBlockAssembly: pinDiameter and pinClearance leave too little PCB web between terminal holes");
|
|
52655
|
+
}
|
|
52656
|
+
if (wirePortDiameter >= Math.min(terminalPitch * 0.72, terminalBlockHeight * 0.65)) {
|
|
52657
|
+
throw new Error("pcbTerminalBlockAssembly: wirePortDiameter is too large for the terminal pitch or body height");
|
|
52658
|
+
}
|
|
52659
|
+
for (const [index2, [x2, y2]] of [...mountingPositions, ...pinPositions].entries()) {
|
|
52660
|
+
if (!Number.isFinite(x2) || !Number.isFinite(y2)) {
|
|
52661
|
+
throw new Error(`pcbTerminalBlockAssembly: generated datum position ${index2} is not finite`);
|
|
52662
|
+
}
|
|
52663
|
+
}
|
|
52664
|
+
const backplateWidth = boardWidth + backplateMargin * 2;
|
|
52665
|
+
const backplateDepth = boardDepth + backplateMargin * 2;
|
|
52666
|
+
const boardBottomZ = backplateThickness + standoffHeight;
|
|
52667
|
+
const boardTopZ = boardBottomZ + boardThickness;
|
|
52668
|
+
const standoffOverlap = Math.min(0.08, standoffHeight * 0.03);
|
|
52669
|
+
const standoffThreadEnvelopeDiameter = Math.max(sizeData.loose, screwDiameter + 1);
|
|
52670
|
+
const standoffThreadEnvelope = cylinder(standoffHeight + 0.8, standoffThreadEnvelopeDiameter / 2, void 0, segments).translate(
|
|
52671
|
+
0,
|
|
52672
|
+
0,
|
|
52673
|
+
backplateThickness - 0.4
|
|
52674
|
+
);
|
|
52675
|
+
const standoffThreadEnvelopes = union(...mountingPositions.map(([x2, y2]) => standoffThreadEnvelope.translate(x2, y2, 0)));
|
|
52676
|
+
const standoff = cylinder(standoffHeight + standoffOverlap, standoffDiameter / 2, void 0, segments).translate(0, 0, backplateThickness - standoffOverlap).subtract(standoffThreadEnvelope);
|
|
52677
|
+
const standoffs = union(...mountingPositions.map(([x2, y2]) => standoff.translate(x2, y2, 0)));
|
|
52678
|
+
const backplate = union(box(backplateWidth, backplateDepth, backplateThickness), standoffs).color("#475569");
|
|
52679
|
+
const boardMountingHoleDiameter = sizeData.normal;
|
|
52680
|
+
const boardMountHole = cylinder(boardThickness + 0.8, boardMountingHoleDiameter / 2, void 0, segments).translate(
|
|
52681
|
+
0,
|
|
52682
|
+
0,
|
|
52683
|
+
boardBottomZ - 0.4
|
|
52684
|
+
);
|
|
52685
|
+
const pcbMountingHoles = union(...mountingPositions.map(([x2, y2]) => boardMountHole.translate(x2, y2, 0)));
|
|
52686
|
+
const pinHole = cylinder(boardThickness + 0.8, pinHoleDiameter / 2, void 0, segments).translate(0, 0, boardBottomZ - 0.4);
|
|
52687
|
+
const pcbPinHoles = union(...pinPositions.map(([x2, y2]) => pinHole.translate(x2, y2, 0)));
|
|
52688
|
+
const pcb = box(boardWidth, boardDepth, boardThickness).translate(0, 0, boardBottomZ).subtract(pcbMountingHoles).subtract(pcbPinHoles).color("#166534");
|
|
52689
|
+
const terminalBodyBlank = box(terminalBlockWidth, terminalBlockDepth, terminalBlockHeight).translate(0, terminalCenterY, boardTopZ);
|
|
52690
|
+
const wirePort = cylinderAlongY(terminalBlockDepth + 0.8, wirePortDiameter / 2, terminalCenterY, segments).translate(
|
|
52691
|
+
0,
|
|
52692
|
+
0,
|
|
52693
|
+
boardTopZ + terminalBlockHeight * 0.42
|
|
52694
|
+
);
|
|
52695
|
+
const wirePorts = union(...pinPositions.map(([x2]) => wirePort.translate(x2, 0, 0)));
|
|
52696
|
+
const clampScrewPockets = union(
|
|
52697
|
+
...pinPositions.map(
|
|
52698
|
+
([x2]) => cylinder(Math.max(0.6, terminalBlockHeight * 0.22), Math.min(terminalPitch * 0.22, wirePortDiameter * 0.42), void 0, segments).translate(
|
|
52699
|
+
x2,
|
|
52700
|
+
terminalCenterY + terminalBlockDepth * 0.12,
|
|
52701
|
+
boardTopZ + terminalBlockHeight * 0.76
|
|
52702
|
+
)
|
|
52703
|
+
)
|
|
52704
|
+
);
|
|
52705
|
+
const pinLength = boardThickness + pinTailLength + Math.min(0.6, terminalBlockHeight * 0.08);
|
|
52706
|
+
const pinStartZ = boardBottomZ - pinTailLength;
|
|
52707
|
+
const pins = union(...pinPositions.map(([x2, y2]) => cylinder(pinLength, pinDiameter / 2, void 0, segments).translate(x2, y2, pinStartZ)));
|
|
52708
|
+
const terminalBlock = union(terminalBodyBlank.subtract(wirePorts).subtract(clampScrewPockets), pins).color("#16a34a");
|
|
52709
|
+
const screwShaftLength = boardThickness + standoffHeight * 0.85;
|
|
52710
|
+
const mountingHardware = fastenerSet(screwSize, screwShaftLength, {
|
|
52711
|
+
washerUnderHead: false,
|
|
52712
|
+
washerUnderNut: false,
|
|
52713
|
+
fit: "normal",
|
|
52714
|
+
segments
|
|
52715
|
+
});
|
|
52716
|
+
const screws = mountingPositions.map(([x2, y2]) => mountingHardware.bolt.translate(x2, y2, boardTopZ).color("#cbd5e1"));
|
|
52717
|
+
const parts = [
|
|
52718
|
+
{ name: "electronics backplate with fused PCB standoffs", shape: backplate },
|
|
52719
|
+
{ name: "PCB with mounting holes and terminal pin clearances", shape: pcb },
|
|
52720
|
+
{ name: "seated purchased terminal block with through-board pins", shape: terminalBlock },
|
|
52721
|
+
...screws.map((shape, index2) => ({ name: `installed ${screwSize} PCB mounting screw ${index2 + 1}`, shape }))
|
|
52722
|
+
];
|
|
52723
|
+
return {
|
|
52724
|
+
parts,
|
|
52725
|
+
backplate,
|
|
52726
|
+
pcb,
|
|
52727
|
+
terminalBlock,
|
|
52728
|
+
screws,
|
|
52729
|
+
mountingPositions,
|
|
52730
|
+
pinPositions,
|
|
52731
|
+
cutters: {
|
|
52732
|
+
pcbMountingHoles,
|
|
52733
|
+
pcbPinHoles,
|
|
52734
|
+
standoffThreadEnvelopes
|
|
52735
|
+
},
|
|
52736
|
+
dims: {
|
|
52737
|
+
terminalCount,
|
|
52738
|
+
terminalPitch,
|
|
52739
|
+
boardWidth,
|
|
52740
|
+
boardDepth,
|
|
52741
|
+
boardThickness,
|
|
52742
|
+
backplateWidth,
|
|
52743
|
+
backplateDepth,
|
|
52744
|
+
backplateThickness,
|
|
52745
|
+
standoffHeight,
|
|
52746
|
+
standoffDiameter,
|
|
52747
|
+
screwSize,
|
|
52748
|
+
screwDiameter,
|
|
52749
|
+
screwHeadDiameter,
|
|
52750
|
+
screwHeadHeight,
|
|
52751
|
+
screwShaftLength,
|
|
52752
|
+
boardMountingHoleDiameter,
|
|
52753
|
+
standoffThreadEnvelopeDiameter,
|
|
52754
|
+
terminalBlockWidth,
|
|
52755
|
+
terminalBlockDepth,
|
|
52756
|
+
terminalBlockHeight,
|
|
52757
|
+
terminalEdgeInset,
|
|
52758
|
+
pinDiameter,
|
|
52759
|
+
pinClearance,
|
|
52760
|
+
pinHoleDiameter,
|
|
52761
|
+
pinTailLength,
|
|
52762
|
+
wirePortDiameter
|
|
52763
|
+
}
|
|
52764
|
+
};
|
|
52765
|
+
}
|
|
52766
|
+
function thumbScrewClampAssembly(options = {}) {
|
|
52767
|
+
const screwSize = options.screwSize ?? "M6";
|
|
52768
|
+
const segments = options.segments ?? 36;
|
|
52769
|
+
const sizeData = METRIC_HOLE_TABLE[screwSize];
|
|
52770
|
+
if (!sizeData) throw new Error(`thumbScrewClampAssembly: unsupported screwSize "${screwSize}"`);
|
|
52771
|
+
const screwDiameter = parseFloat(screwSize.replace("M", ""));
|
|
52772
|
+
const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
|
|
52773
|
+
const faceClearance = requireNonNegative(options.faceClearance ?? 0, "faceClearance");
|
|
52774
|
+
const threadEnvelopeDiameter = Math.max(sizeData.normal, screwDiameter + runningClearance * 2);
|
|
52775
|
+
const pressurePadDiameter = requirePositive$6(
|
|
52776
|
+
options.pressurePadDiameter ?? Math.max(screwDiameter * 3.2, 18),
|
|
52777
|
+
"pressurePadDiameter"
|
|
52778
|
+
);
|
|
52779
|
+
const pressurePadThickness = requirePositive$6(
|
|
52780
|
+
options.pressurePadThickness ?? Math.max(screwDiameter * 0.72, 4),
|
|
52781
|
+
"pressurePadThickness"
|
|
52782
|
+
);
|
|
52783
|
+
const knobDiameter = requirePositive$6(options.knobDiameter ?? Math.max(screwDiameter * 4.2, 24), "knobDiameter");
|
|
52784
|
+
const knobThickness = requirePositive$6(options.knobThickness ?? Math.max(screwDiameter * 0.9, 7), "knobThickness");
|
|
52785
|
+
const workpieceThickness = requirePositive$6(options.workpieceThickness ?? 18, "workpieceThickness");
|
|
52786
|
+
const workpieceDepth = requirePositive$6(options.workpieceDepth ?? Math.max(46, pressurePadDiameter * 1.5), "workpieceDepth");
|
|
52787
|
+
const workpieceHeight = requirePositive$6(options.workpieceHeight ?? Math.max(pressurePadDiameter * 1.35, 24), "workpieceHeight");
|
|
52788
|
+
const frameDepth = requirePositive$6(
|
|
52789
|
+
options.frameDepth ?? Math.max(workpieceDepth + 12, pressurePadDiameter + 16),
|
|
52790
|
+
"frameDepth"
|
|
52791
|
+
);
|
|
52792
|
+
const baseThickness = requirePositive$6(options.baseThickness ?? Math.max(screwDiameter, 6), "baseThickness");
|
|
52793
|
+
const jawThickness = requirePositive$6(options.jawThickness ?? Math.max(screwDiameter * 1.35, 9), "jawThickness");
|
|
52794
|
+
const supportThickness = requirePositive$6(
|
|
52795
|
+
options.supportThickness ?? Math.max(screwDiameter * 1.8, 12),
|
|
52796
|
+
"supportThickness"
|
|
52797
|
+
);
|
|
52798
|
+
const bossLength = requirePositive$6(options.bossLength ?? Math.max(screwDiameter * 1.1, 8), "bossLength");
|
|
52799
|
+
const bossDiameter = requirePositive$6(options.bossDiameter ?? Math.max(threadEnvelopeDiameter + 5, screwDiameter * 2.5), "bossDiameter");
|
|
52800
|
+
const exposedScrewLength = requirePositive$6(
|
|
52801
|
+
options.exposedScrewLength ?? Math.max(pressurePadDiameter * 0.45, screwDiameter * 2.2),
|
|
52802
|
+
"exposedScrewLength"
|
|
52803
|
+
);
|
|
52804
|
+
const screwCenterZ = baseThickness + Math.max(workpieceHeight * 0.52, pressurePadDiameter * 0.68);
|
|
52805
|
+
const frameHeight = requirePositive$6(
|
|
52806
|
+
options.frameHeight ?? screwCenterZ - baseThickness + pressurePadDiameter / 2 + Math.max(baseThickness, 7),
|
|
52807
|
+
"frameHeight"
|
|
52808
|
+
);
|
|
52809
|
+
if (workpieceDepth > frameDepth - 6) {
|
|
52810
|
+
throw new Error("thumbScrewClampAssembly: frameDepth must leave side material around the clamped workpiece");
|
|
52811
|
+
}
|
|
52812
|
+
if (pressurePadDiameter > frameDepth - 4) {
|
|
52813
|
+
throw new Error("thumbScrewClampAssembly: pressurePadDiameter is too large for the frame depth");
|
|
52814
|
+
}
|
|
52815
|
+
if (bossDiameter > frameDepth - 4) {
|
|
52816
|
+
throw new Error("thumbScrewClampAssembly: bossDiameter is too large for the frame depth");
|
|
52817
|
+
}
|
|
52818
|
+
if (screwCenterZ - pressurePadDiameter / 2 <= baseThickness + 0.5) {
|
|
52819
|
+
throw new Error("thumbScrewClampAssembly: pressure pad collides with the base bridge");
|
|
52820
|
+
}
|
|
52821
|
+
if (baseThickness + frameHeight - screwCenterZ <= pressurePadDiameter / 2 + 2) {
|
|
52822
|
+
throw new Error("thumbScrewClampAssembly: frameHeight leaves too little material above the screw axis");
|
|
52823
|
+
}
|
|
52824
|
+
if (threadEnvelopeDiameter + 4 > Math.min(frameDepth, frameHeight)) {
|
|
52825
|
+
throw new Error("thumbScrewClampAssembly: threaded boss bore leaves too little surrounding frame material");
|
|
52826
|
+
}
|
|
52827
|
+
const workpieceLeftFaceX = -workpieceThickness / 2;
|
|
52828
|
+
const workpieceRightFaceX = workpieceThickness / 2;
|
|
52829
|
+
const anvilOverlap = Math.min(0.35, pressurePadThickness * 0.18);
|
|
52830
|
+
const anvilPadCenterX = workpieceLeftFaceX - faceClearance - pressurePadThickness / 2;
|
|
52831
|
+
const pressurePadCenterX = workpieceRightFaceX + faceClearance + pressurePadThickness / 2;
|
|
52832
|
+
const fixedJawRightFaceX = anvilPadCenterX - pressurePadThickness / 2 + anvilOverlap;
|
|
52833
|
+
const fixedJawCenterX = fixedJawRightFaceX - jawThickness / 2;
|
|
52834
|
+
const pressurePadRightFaceX = pressurePadCenterX + pressurePadThickness / 2;
|
|
52835
|
+
const supportInnerFaceX = pressurePadRightFaceX + exposedScrewLength;
|
|
52836
|
+
const supportCenterX = supportInnerFaceX + supportThickness / 2;
|
|
52837
|
+
const supportOuterFaceX = supportInnerFaceX + supportThickness;
|
|
52838
|
+
const frameLeftFaceX = fixedJawCenterX - jawThickness / 2;
|
|
52839
|
+
const frameRightFaceX = supportOuterFaceX;
|
|
52840
|
+
const baseLength = frameRightFaceX - frameLeftFaceX;
|
|
52841
|
+
if (baseLength <= 0 || !Number.isFinite(baseLength)) {
|
|
52842
|
+
throw new Error("thumbScrewClampAssembly: generated clamp frame length is invalid");
|
|
52843
|
+
}
|
|
52844
|
+
const bossCenterX = supportInnerFaceX + (supportThickness + bossLength) / 2;
|
|
52845
|
+
const threadedBossBore = cylinderAlongX(supportThickness + bossLength + 1, threadEnvelopeDiameter / 2, bossCenterX, segments).translate(
|
|
52846
|
+
0,
|
|
52847
|
+
0,
|
|
52848
|
+
screwCenterZ
|
|
52849
|
+
);
|
|
52850
|
+
const frameOverlap = Math.min(0.12, baseThickness * 0.04);
|
|
52851
|
+
const base = box(baseLength, frameDepth, baseThickness).translate((frameLeftFaceX + frameRightFaceX) / 2, 0, 0);
|
|
52852
|
+
const fixedJaw = box(jawThickness, frameDepth, frameHeight + frameOverlap).translate(fixedJawCenterX, 0, baseThickness - frameOverlap);
|
|
52853
|
+
const support = box(supportThickness, frameDepth, frameHeight + frameOverlap).translate(supportCenterX, 0, baseThickness - frameOverlap);
|
|
52854
|
+
const boss2 = cylinderAlongX(supportThickness + bossLength, bossDiameter / 2, bossCenterX, segments).translate(0, 0, screwCenterZ);
|
|
52855
|
+
const anvilPad = cylinderAlongX(pressurePadThickness, pressurePadDiameter / 2, anvilPadCenterX, segments).translate(0, 0, screwCenterZ);
|
|
52856
|
+
const frame = union(base, fixedJaw, support, boss2, anvilPad).subtract(threadedBossBore).color("#475569");
|
|
52857
|
+
const workpieceBottomZ = screwCenterZ - workpieceHeight / 2;
|
|
52858
|
+
const workpiece = box(workpieceThickness, workpieceDepth, workpieceHeight).translate(0, 0, workpieceBottomZ).color("#a16207");
|
|
52859
|
+
const pressurePad = cylinderAlongX(pressurePadThickness, pressurePadDiameter / 2, pressurePadCenterX, segments).translate(0, 0, screwCenterZ);
|
|
52860
|
+
const knobCenterX = supportOuterFaceX + bossLength + runningClearance + knobThickness / 2;
|
|
52861
|
+
const knob = cylinderAlongX(knobThickness, knobDiameter / 2, knobCenterX, segments).translate(0, 0, screwCenterZ);
|
|
52862
|
+
const shaftLeftX = pressurePadRightFaceX - Math.min(pressurePadThickness * 0.45, screwDiameter * 0.45);
|
|
52863
|
+
const shaftRightX = knobCenterX + knobThickness / 2;
|
|
52864
|
+
const shaftLength = shaftRightX - shaftLeftX;
|
|
52865
|
+
if (shaftLength <= supportThickness + bossLength) {
|
|
52866
|
+
throw new Error("thumbScrewClampAssembly: generated screw length is too short for the threaded support");
|
|
52867
|
+
}
|
|
52868
|
+
const shaft = cylinderAlongX(shaftLength, screwDiameter / 2, (shaftLeftX + shaftRightX) / 2, segments).translate(0, 0, screwCenterZ);
|
|
52869
|
+
const clampScrew = union(shaft, pressurePad, knob).color("#cbd5e1");
|
|
52870
|
+
const workpieceEnvelope = box(workpieceThickness, workpieceDepth, workpieceHeight).translate(0, 0, workpieceBottomZ);
|
|
52871
|
+
return {
|
|
52872
|
+
parts: [
|
|
52873
|
+
{ name: "thumb-screw clamp frame with fixed anvil and threaded boss", shape: frame },
|
|
52874
|
+
{ name: "representative clamped workpiece between pads", shape: workpiece },
|
|
52875
|
+
{ name: "installed thumb screw with captive pressure pad and hand knob", shape: clampScrew }
|
|
52876
|
+
],
|
|
52877
|
+
frame,
|
|
52878
|
+
workpiece,
|
|
52879
|
+
clampScrew,
|
|
52880
|
+
cutters: {
|
|
52881
|
+
threadedBossBore,
|
|
52882
|
+
workpieceEnvelope
|
|
52883
|
+
},
|
|
52884
|
+
dims: {
|
|
52885
|
+
screwSize,
|
|
52886
|
+
screwDiameter,
|
|
52887
|
+
threadEnvelopeDiameter,
|
|
52888
|
+
workpieceThickness,
|
|
52889
|
+
workpieceDepth,
|
|
52890
|
+
workpieceHeight,
|
|
52891
|
+
frameDepth,
|
|
52892
|
+
frameHeight,
|
|
52893
|
+
baseThickness,
|
|
52894
|
+
jawThickness,
|
|
52895
|
+
supportThickness,
|
|
52896
|
+
bossLength,
|
|
52897
|
+
bossDiameter,
|
|
52898
|
+
exposedScrewLength,
|
|
52899
|
+
pressurePadDiameter,
|
|
52900
|
+
pressurePadThickness,
|
|
52901
|
+
knobDiameter,
|
|
52902
|
+
knobThickness,
|
|
52903
|
+
screwCenterZ,
|
|
52904
|
+
fixedAnvilFaceX: workpieceLeftFaceX - faceClearance,
|
|
52905
|
+
pressurePadFaceX: workpieceRightFaceX + faceClearance,
|
|
52906
|
+
supportInnerFaceX,
|
|
52907
|
+
runningClearance,
|
|
52908
|
+
faceClearance
|
|
52909
|
+
}
|
|
52910
|
+
};
|
|
52911
|
+
}
|
|
51034
52912
|
function fastenerSet(size, boltLength, options) {
|
|
51035
52913
|
const sizeData = METRIC_HOLE_TABLE[size];
|
|
51036
52914
|
if (!sizeData) throw new Error(`fastenerSet: unsupported size "${size}"`);
|
|
@@ -51091,6 +52969,22 @@ const partLibrary = {
|
|
|
51091
52969
|
nut,
|
|
51092
52970
|
washer,
|
|
51093
52971
|
fastenerSet,
|
|
52972
|
+
boltedServiceCover,
|
|
52973
|
+
datumEnclosureAssembly,
|
|
52974
|
+
snapLatchCoverAssembly,
|
|
52975
|
+
pinnedLeverAssembly,
|
|
52976
|
+
retainedShaftAssembly,
|
|
52977
|
+
capturedLinearSlide,
|
|
52978
|
+
capturedCartridgeGuideAssembly,
|
|
52979
|
+
livingHingeCoverAssembly,
|
|
52980
|
+
knuckledHingeAssembly,
|
|
52981
|
+
clevisPinJointAssembly,
|
|
52982
|
+
seatedBearingAssembly,
|
|
52983
|
+
cableGlandAnchorAssembly,
|
|
52984
|
+
hoseBarbPortAssembly,
|
|
52985
|
+
routedTubeClipAssembly,
|
|
52986
|
+
pcbTerminalBlockAssembly,
|
|
52987
|
+
thumbScrewClampAssembly,
|
|
51094
52988
|
pipeRoute,
|
|
51095
52989
|
elbow,
|
|
51096
52990
|
beltDrive,
|
|
@@ -84620,10 +86514,14 @@ function spec(name, checkFn) {
|
|
|
84620
86514
|
};
|
|
84621
86515
|
}
|
|
84622
86516
|
let _collected = [];
|
|
86517
|
+
let _collisionAllowances = [];
|
|
86518
|
+
let _physicalComponentExpectations = [];
|
|
84623
86519
|
let _counter = 0;
|
|
84624
86520
|
let _activeGroup = null;
|
|
84625
86521
|
function resetVerifications() {
|
|
84626
86522
|
_collected = [];
|
|
86523
|
+
_collisionAllowances = [];
|
|
86524
|
+
_physicalComponentExpectations = [];
|
|
84627
86525
|
_counter = 0;
|
|
84628
86526
|
}
|
|
84629
86527
|
function getCollectedVerifications() {
|
|
@@ -84657,15 +86555,35 @@ function push(result) {
|
|
|
84657
86555
|
function roundNum(n, digits = 4) {
|
|
84658
86556
|
return Number.isFinite(n) ? n.toFixed(digits).replace(/\.?0+$/, "") : String(n);
|
|
84659
86557
|
}
|
|
86558
|
+
function meshDerivedManifoldBackend(shape) {
|
|
86559
|
+
const mesh = getShapeRuntimeBackend(shape).getMesh();
|
|
86560
|
+
return reconstructBackendFromMesh({
|
|
86561
|
+
numProp: mesh.numProp,
|
|
86562
|
+
triVerts: mesh.triVerts,
|
|
86563
|
+
vertProperties: mesh.vertProperties,
|
|
86564
|
+
mergeFromVert: mesh.mergeFromVert ?? new Uint32Array(),
|
|
86565
|
+
mergeToVert: mesh.mergeToVert ?? new Uint32Array()
|
|
86566
|
+
});
|
|
86567
|
+
}
|
|
86568
|
+
function backendForMinGap(shape) {
|
|
86569
|
+
const backend = getShapeRuntimeBackend(shape);
|
|
86570
|
+
if (isManifoldCapableBackend(backend)) return { backend, method: "exact", dispose: false };
|
|
86571
|
+
return { backend: meshDerivedManifoldBackend(shape), method: "mesh-derived", dispose: true };
|
|
86572
|
+
}
|
|
84660
86573
|
function computeMinGap(a2, b, searchLength) {
|
|
84661
|
-
const backendA =
|
|
84662
|
-
const backendB =
|
|
84663
|
-
|
|
84664
|
-
|
|
86574
|
+
const backendA = backendForMinGap(a2);
|
|
86575
|
+
const backendB = backendForMinGap(b);
|
|
86576
|
+
try {
|
|
86577
|
+
const manifoldA = requireManifoldShapeBackend(backendA.backend, "verification.minGap");
|
|
86578
|
+
const manifoldB = requireManifoldShapeBackend(backendB.backend, "verification.minGap");
|
|
86579
|
+
return {
|
|
86580
|
+
gap: manifoldA.minGap(manifoldB, searchLength),
|
|
86581
|
+
method: backendA.method === "exact" && backendB.method === "exact" ? "exact" : "mesh-derived"
|
|
86582
|
+
};
|
|
86583
|
+
} finally {
|
|
86584
|
+
if (backendA.dispose) disposeShapeBackend(backendA.backend);
|
|
86585
|
+
if (backendB.dispose) disposeShapeBackend(backendB.backend);
|
|
84665
86586
|
}
|
|
84666
|
-
const manifoldA = backendA.requireManifold("verification.minGap");
|
|
84667
|
-
const manifoldB = requireManifoldShapeBackend(backendB, "verification.minGap");
|
|
84668
|
-
return manifoldA.minGap(manifoldB, searchLength);
|
|
84669
86587
|
}
|
|
84670
86588
|
function vec3Dot(a2, b) {
|
|
84671
86589
|
return a2[0] * b[0] + a2[1] * b[1] + a2[2] * b[2];
|
|
@@ -84800,9 +86718,144 @@ const verify = {
|
|
|
84800
86718
|
actual: `${roundNum(d2, 3)} mm`
|
|
84801
86719
|
});
|
|
84802
86720
|
} catch (e) {
|
|
84803
|
-
push({
|
|
86721
|
+
push({
|
|
86722
|
+
id: nextId(),
|
|
86723
|
+
label,
|
|
86724
|
+
kind: "interface",
|
|
86725
|
+
status: "fail",
|
|
86726
|
+
message: `Error: ${e instanceof Error ? e.message : String(e)}`,
|
|
86727
|
+
line: line2
|
|
86728
|
+
});
|
|
86729
|
+
}
|
|
86730
|
+
},
|
|
86731
|
+
/**
|
|
86732
|
+
* Check the distance between two named connectors on a shape or group.
|
|
86733
|
+
*
|
|
86734
|
+
* Use this when connectors + `matchTo()` define a static assembly interface.
|
|
86735
|
+
* It proves the mate at runtime, unlike a plain source-level connector
|
|
86736
|
+
* declaration. The common case is `expected = 0`, meaning the two connector
|
|
86737
|
+
* origins should coincide after placement.
|
|
86738
|
+
*
|
|
86739
|
+
* **Example**
|
|
86740
|
+
*
|
|
86741
|
+
* ```ts
|
|
86742
|
+
* verify.connectorDistance("leg is seated", bench, "Rail.leg_0", "Leg0.head", 0, 0.01);
|
|
86743
|
+
* ```
|
|
86744
|
+
*/
|
|
86745
|
+
connectorDistance(label, target, connectorA, connectorB, expected = 0, tolerance = 0.01) {
|
|
86746
|
+
const line2 = captureSourceLine();
|
|
86747
|
+
try {
|
|
86748
|
+
const actual = target.connectorDistance(connectorA, connectorB);
|
|
86749
|
+
const diff = Math.abs(actual - expected);
|
|
86750
|
+
const passed = diff <= Math.abs(tolerance);
|
|
86751
|
+
push({
|
|
86752
|
+
id: nextId(),
|
|
86753
|
+
label,
|
|
86754
|
+
kind: "interface",
|
|
86755
|
+
status: passed ? "pass" : "fail",
|
|
86756
|
+
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`,
|
|
86757
|
+
line: passed ? void 0 : line2,
|
|
86758
|
+
expected: `${roundNum(expected, 4)} ± ${roundNum(tolerance, 4)} mm`,
|
|
86759
|
+
actual: `${roundNum(actual, 4)} mm`
|
|
86760
|
+
});
|
|
86761
|
+
} catch (e) {
|
|
86762
|
+
push({
|
|
86763
|
+
id: nextId(),
|
|
86764
|
+
label,
|
|
86765
|
+
kind: "interface",
|
|
86766
|
+
status: "fail",
|
|
86767
|
+
message: `Error: ${e instanceof Error ? e.message : String(e)}`,
|
|
86768
|
+
line: line2
|
|
86769
|
+
});
|
|
84804
86770
|
}
|
|
84805
86771
|
},
|
|
86772
|
+
/**
|
|
86773
|
+
* Declare the expected physical connectivity component count for the returned visible model.
|
|
86774
|
+
*
|
|
86775
|
+
* **Details**
|
|
86776
|
+
*
|
|
86777
|
+
* Use this for generated mechanical models that should have a clear component graph:
|
|
86778
|
+
* one connected fixture, a purchased part plus a removable cartridge, a root assembly plus
|
|
86779
|
+
* named intentional ghosts, and so on. `forgecad inspect mechanical-integrity` resolves the returned
|
|
86780
|
+
* visible objects with the same physical-connectivity analysis used in the quality gate and
|
|
86781
|
+
* fails if the actual component count differs.
|
|
86782
|
+
*
|
|
86783
|
+
* This catches the common generated-CAD failure where a script returns a visually plausible
|
|
86784
|
+
* artifact but the handle, screw, washer, cover, or terminal block is actually a separate island.
|
|
86785
|
+
*
|
|
86786
|
+
* **Example**
|
|
86787
|
+
*
|
|
86788
|
+
* ```ts
|
|
86789
|
+
* verify.physicalComponentCount("vise is one connected installed assembly", 1);
|
|
86790
|
+
* ```
|
|
86791
|
+
*/
|
|
86792
|
+
physicalComponentCount(label, expected) {
|
|
86793
|
+
const line2 = captureSourceLine();
|
|
86794
|
+
const id = nextId();
|
|
86795
|
+
if (!Number.isInteger(expected) || expected < 0) {
|
|
86796
|
+
push({
|
|
86797
|
+
id,
|
|
86798
|
+
label,
|
|
86799
|
+
kind: "interface",
|
|
86800
|
+
status: "fail",
|
|
86801
|
+
message: "Expected physical component count must be a non-negative integer",
|
|
86802
|
+
line: line2
|
|
86803
|
+
});
|
|
86804
|
+
return;
|
|
86805
|
+
}
|
|
86806
|
+
_physicalComponentExpectations.push({ id, label, expected, line: line2 });
|
|
86807
|
+
push({
|
|
86808
|
+
id,
|
|
86809
|
+
label,
|
|
86810
|
+
kind: "interface",
|
|
86811
|
+
status: "pass",
|
|
86812
|
+
message: `Expected ${expected} physical component(s); checked by mechanical-integrity connectivity`
|
|
86813
|
+
});
|
|
86814
|
+
},
|
|
86815
|
+
/**
|
|
86816
|
+
* Declare that two visible objects intentionally overlap because the overlap is real manufacturing intent.
|
|
86817
|
+
*
|
|
86818
|
+
* **Details**
|
|
86819
|
+
*
|
|
86820
|
+
* Use this only for overlaps that a mechanical reviewer would accept as actual matter sharing volume:
|
|
86821
|
+
* welded/fused regions, overmolded inserts, potted electronics, cast-in hardware, or deliberately
|
|
86822
|
+
* bonded laminations. This is not a shortcut for screws without holes, shafts without bores, covers
|
|
86823
|
+
* without pockets, or parts placed with collision as a positioning hack.
|
|
86824
|
+
*
|
|
86825
|
+
* `forgecad inspect mechanical-integrity --collisions` only honors this declaration when both shapes are
|
|
86826
|
+
* returned as visible objects and the exact collision report finds that same object pair. Unused or
|
|
86827
|
+
* non-visible declarations fail the quality gate so annotations cannot hide unrelated collisions.
|
|
86828
|
+
*
|
|
86829
|
+
* **Example**
|
|
86830
|
+
*
|
|
86831
|
+
* ```ts
|
|
86832
|
+
* verify.intentionalOverlap("rubber grip is overmolded on handle", rubberGrip, handleCore, "overmolded insert");
|
|
86833
|
+
* ```
|
|
86834
|
+
*/
|
|
86835
|
+
intentionalOverlap(label, a2, b, reason) {
|
|
86836
|
+
const line2 = captureSourceLine();
|
|
86837
|
+
const id = nextId();
|
|
86838
|
+
const trimmedReason = String(reason ?? "").trim();
|
|
86839
|
+
if (trimmedReason.length === 0) {
|
|
86840
|
+
push({
|
|
86841
|
+
id,
|
|
86842
|
+
label,
|
|
86843
|
+
kind: "interface",
|
|
86844
|
+
status: "fail",
|
|
86845
|
+
message: "Intentional overlap requires a manufacturing reason",
|
|
86846
|
+
line: line2
|
|
86847
|
+
});
|
|
86848
|
+
return;
|
|
86849
|
+
}
|
|
86850
|
+
_collisionAllowances.push({ id, label, reason: trimmedReason, a: a2, b, line: line2 });
|
|
86851
|
+
push({
|
|
86852
|
+
id,
|
|
86853
|
+
label,
|
|
86854
|
+
kind: "interface",
|
|
86855
|
+
status: "pass",
|
|
86856
|
+
message: `Intentional overlap declared: ${trimmedReason}`
|
|
86857
|
+
});
|
|
86858
|
+
},
|
|
84806
86859
|
/**
|
|
84807
86860
|
* Check that two shapes do not collide (minGap > 0).
|
|
84808
86861
|
*
|
|
@@ -84811,19 +86864,28 @@ const verify = {
|
|
|
84811
86864
|
notColliding(label, a2, b, searchLength = 1) {
|
|
84812
86865
|
const line2 = captureSourceLine();
|
|
84813
86866
|
try {
|
|
84814
|
-
const gap = computeMinGap(a2, b, searchLength);
|
|
86867
|
+
const { gap, method } = computeMinGap(a2, b, searchLength);
|
|
86868
|
+
const methodLabel = method === "exact" ? "exact min gap" : "mesh-derived min gap";
|
|
84815
86869
|
const passed = gap > 0;
|
|
84816
86870
|
push({
|
|
84817
86871
|
id: nextId(),
|
|
84818
86872
|
label,
|
|
86873
|
+
kind: "interface",
|
|
84819
86874
|
status: passed ? "pass" : "fail",
|
|
84820
|
-
message: passed ? `No collision (
|
|
86875
|
+
message: passed ? `No collision (${methodLabel} ${roundNum(gap, 3)} mm)` : `Shapes are colliding (${methodLabel} ${roundNum(gap, 3)} mm ≤ 0)`,
|
|
84821
86876
|
line: passed ? void 0 : line2,
|
|
84822
86877
|
expected: "> 0 mm",
|
|
84823
86878
|
actual: `${roundNum(gap, 3)} mm`
|
|
84824
86879
|
});
|
|
84825
86880
|
} catch (e) {
|
|
84826
|
-
push({
|
|
86881
|
+
push({
|
|
86882
|
+
id: nextId(),
|
|
86883
|
+
label,
|
|
86884
|
+
kind: "interface",
|
|
86885
|
+
status: "fail",
|
|
86886
|
+
message: `Error: ${e instanceof Error ? e.message : String(e)}`,
|
|
86887
|
+
line: line2
|
|
86888
|
+
});
|
|
84827
86889
|
}
|
|
84828
86890
|
},
|
|
84829
86891
|
/**
|
|
@@ -84832,13 +86894,15 @@ const verify = {
|
|
|
84832
86894
|
minClearance(label, a2, b, minGap, searchLength = 10) {
|
|
84833
86895
|
const line2 = captureSourceLine();
|
|
84834
86896
|
try {
|
|
84835
|
-
const gap = computeMinGap(a2, b, searchLength);
|
|
86897
|
+
const { gap, method } = computeMinGap(a2, b, searchLength);
|
|
86898
|
+
const methodLabel = method === "exact" ? "exact gap" : "mesh-derived gap";
|
|
84836
86899
|
const passed = gap >= minGap;
|
|
84837
86900
|
push({
|
|
84838
86901
|
id: nextId(),
|
|
84839
86902
|
label,
|
|
86903
|
+
kind: "interface",
|
|
84840
86904
|
status: passed ? "pass" : "fail",
|
|
84841
|
-
message: passed ?
|
|
86905
|
+
message: passed ? `${methodLabel} ${roundNum(gap, 3)} mm ≥ ${roundNum(minGap, 3)} mm` : `${methodLabel} ${roundNum(gap, 3)} mm < required ${roundNum(minGap, 3)} mm`,
|
|
84842
86906
|
line: passed ? void 0 : line2,
|
|
84843
86907
|
expected: `≥ ${roundNum(minGap, 3)} mm`,
|
|
84844
86908
|
actual: `${roundNum(gap, 3)} mm`
|
|
@@ -84847,6 +86911,90 @@ const verify = {
|
|
|
84847
86911
|
push({ id: nextId(), label, status: "fail", message: `Error: ${e instanceof Error ? e.message : String(e)}`, line: line2 });
|
|
84848
86912
|
}
|
|
84849
86913
|
},
|
|
86914
|
+
/**
|
|
86915
|
+
* Check that the clearance gap between two shapes is inside an allowed range.
|
|
86916
|
+
*
|
|
86917
|
+
* **Details**
|
|
86918
|
+
*
|
|
86919
|
+
* Use this for seated and retained interfaces where a part must be close
|
|
86920
|
+
* enough to be mechanically accountable, but must not collide beyond the
|
|
86921
|
+
* allowed minimum. It catches both failure modes that make generated CAD look
|
|
86922
|
+
* fake: parts floating away from their receiver, and parts intersecting their
|
|
86923
|
+
* receiver because the pocket, bore, or running clearance was not modeled.
|
|
86924
|
+
*
|
|
86925
|
+
* For contact, use a narrow range such as `[-0.01, 0.05]` to tolerate tiny
|
|
86926
|
+
* numerical noise. For a running fit, use the intended clearance band.
|
|
86927
|
+
*
|
|
86928
|
+
* Manifold-backed shapes use exact min-gap distance. Other backends use a
|
|
86929
|
+
* mesh-derived min-gap check and say so in the verification message; keep
|
|
86930
|
+
* `forgecad inspect mechanical-integrity --collisions` in the acceptance gate for
|
|
86931
|
+
* positive-volume interference.
|
|
86932
|
+
*
|
|
86933
|
+
* **Example**
|
|
86934
|
+
*
|
|
86935
|
+
* ```ts
|
|
86936
|
+
* verify.clearanceBetween("cover is seated on gasket", cover, gasket, -0.01, 0.05);
|
|
86937
|
+
* verify.clearanceBetween("carriage runs inside rail", carriage, rail, 0.2, 0.5);
|
|
86938
|
+
* ```
|
|
86939
|
+
*/
|
|
86940
|
+
clearanceBetween(label, a2, b, minGap, maxGap, searchLength) {
|
|
86941
|
+
const line2 = captureSourceLine();
|
|
86942
|
+
try {
|
|
86943
|
+
if (!Number.isFinite(minGap) || !Number.isFinite(maxGap)) {
|
|
86944
|
+
push({
|
|
86945
|
+
id: nextId(),
|
|
86946
|
+
label,
|
|
86947
|
+
kind: "interface",
|
|
86948
|
+
status: "fail",
|
|
86949
|
+
message: "Clearance range must use finite numbers",
|
|
86950
|
+
line: line2
|
|
86951
|
+
});
|
|
86952
|
+
return;
|
|
86953
|
+
}
|
|
86954
|
+
if (maxGap < minGap) {
|
|
86955
|
+
push({
|
|
86956
|
+
id: nextId(),
|
|
86957
|
+
label,
|
|
86958
|
+
kind: "interface",
|
|
86959
|
+
status: "fail",
|
|
86960
|
+
message: `Clearance max ${roundNum(maxGap, 3)} mm is smaller than min ${roundNum(minGap, 3)} mm`,
|
|
86961
|
+
line: line2
|
|
86962
|
+
});
|
|
86963
|
+
return;
|
|
86964
|
+
}
|
|
86965
|
+
const search = searchLength ?? Math.max(10, Math.abs(maxGap) * 2 + 1);
|
|
86966
|
+
const { gap, method } = computeMinGap(a2, b, search);
|
|
86967
|
+
const methodLabel = method === "exact" ? "exact gap" : "mesh-derived gap";
|
|
86968
|
+
const passed = gap >= minGap && gap <= maxGap;
|
|
86969
|
+
let message;
|
|
86970
|
+
if (passed) {
|
|
86971
|
+
message = `${methodLabel} ${roundNum(gap, 3)} mm in [${roundNum(minGap, 3)}, ${roundNum(maxGap, 3)}] mm`;
|
|
86972
|
+
} else if (gap < minGap) {
|
|
86973
|
+
message = `${methodLabel} ${roundNum(gap, 3)} mm < allowed minimum ${roundNum(minGap, 3)} mm`;
|
|
86974
|
+
} else {
|
|
86975
|
+
message = `${methodLabel} ${roundNum(gap, 3)} mm > allowed maximum ${roundNum(maxGap, 3)} mm`;
|
|
86976
|
+
}
|
|
86977
|
+
push({
|
|
86978
|
+
id: nextId(),
|
|
86979
|
+
label,
|
|
86980
|
+
kind: "interface",
|
|
86981
|
+
status: passed ? "pass" : "fail",
|
|
86982
|
+
message,
|
|
86983
|
+
line: passed ? void 0 : line2,
|
|
86984
|
+
expected: `[${roundNum(minGap, 3)}, ${roundNum(maxGap, 3)}] mm`,
|
|
86985
|
+
actual: `${roundNum(gap, 3)} mm`
|
|
86986
|
+
});
|
|
86987
|
+
} catch (e) {
|
|
86988
|
+
push({
|
|
86989
|
+
id: nextId(),
|
|
86990
|
+
label,
|
|
86991
|
+
kind: "interface",
|
|
86992
|
+
status: "fail",
|
|
86993
|
+
message: `Error: ${e instanceof Error ? e.message : String(e)}`,
|
|
86994
|
+
line: line2
|
|
86995
|
+
});
|
|
86996
|
+
}
|
|
86997
|
+
},
|
|
84850
86998
|
/**
|
|
84851
86999
|
* Check that two face normals are parallel (within toleranceDeg degrees).
|
|
84852
87000
|
*/
|