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
|
@@ -10030,7 +10030,7 @@ let _wasm$1 = null;
|
|
|
10030
10030
|
async function initManifoldWasm() {
|
|
10031
10031
|
if (_wasm$1) return _wasm$1;
|
|
10032
10032
|
performance.mark("manifold:start");
|
|
10033
|
-
const Module = (await import("./manifold-
|
|
10033
|
+
const Module = (await import("./manifold-Bk26ViCr.js")).default;
|
|
10034
10034
|
performance.mark("manifold:imported");
|
|
10035
10035
|
const wasm = await Module();
|
|
10036
10036
|
wasm.setup();
|
|
@@ -48809,7 +48809,7 @@ function spurGear(options) {
|
|
|
48809
48809
|
});
|
|
48810
48810
|
return attachGearMeta(shapeWithConnectors, meta2);
|
|
48811
48811
|
}
|
|
48812
|
-
function requirePositive$
|
|
48812
|
+
function requirePositive$8(scope, name, value) {
|
|
48813
48813
|
if (!isFinitePositive(value)) throw new Error(`${scope}: "${name}" must be > 0`);
|
|
48814
48814
|
}
|
|
48815
48815
|
function requireOptionalBore(scope, boreDiameter, maxDiameter) {
|
|
@@ -48831,8 +48831,8 @@ function cutBore$1(shape, boreDiameter) {
|
|
|
48831
48831
|
return shape.subtract(cutter);
|
|
48832
48832
|
}
|
|
48833
48833
|
function gearBodyDisk(options) {
|
|
48834
|
-
requirePositive$
|
|
48835
|
-
requirePositive$
|
|
48834
|
+
requirePositive$8("gearBodyDisk", "outerRadius", options.outerRadius);
|
|
48835
|
+
requirePositive$8("gearBodyDisk", "faceWidth", options.faceWidth);
|
|
48836
48836
|
const bore = requireOptionalBore("gearBodyDisk", options.boreDiameter, options.outerRadius * 2);
|
|
48837
48837
|
const segments = resolveSegments(options.segments);
|
|
48838
48838
|
const outer = circle2d(options.outerRadius, segments);
|
|
@@ -48840,14 +48840,14 @@ function gearBodyDisk(options) {
|
|
|
48840
48840
|
return sketchExtrude(profile, options.faceWidth);
|
|
48841
48841
|
}
|
|
48842
48842
|
function gearBodyDiskWithHub(options) {
|
|
48843
|
-
requirePositive$
|
|
48843
|
+
requirePositive$8("gearBodyDiskWithHub", "hubDiameter", options.hubDiameter);
|
|
48844
48844
|
if (options.hubDiameter >= options.outerRadius * 2) {
|
|
48845
48845
|
throw new Error('gearBodyDiskWithHub: "hubDiameter" must be smaller than the outer diameter');
|
|
48846
48846
|
}
|
|
48847
48847
|
const bore = requireOptionalBore("gearBodyDiskWithHub", options.boreDiameter, options.hubDiameter);
|
|
48848
48848
|
const base = gearBodyDisk({ ...options, boreDiameter: 0 });
|
|
48849
48849
|
const hubFaceWidth = options.hubFaceWidth ?? options.faceWidth * 1.5;
|
|
48850
|
-
requirePositive$
|
|
48850
|
+
requirePositive$8("gearBodyDiskWithHub", "hubFaceWidth", hubFaceWidth);
|
|
48851
48851
|
const hub = cylinder(hubFaceWidth, options.hubDiameter * 0.5, void 0, options.segments).translate(
|
|
48852
48852
|
0,
|
|
48853
48853
|
0,
|
|
@@ -48856,11 +48856,11 @@ function gearBodyDiskWithHub(options) {
|
|
|
48856
48856
|
return cutBore$1(base.add(hub), bore);
|
|
48857
48857
|
}
|
|
48858
48858
|
function gearBodySpoked(options) {
|
|
48859
|
-
requirePositive$
|
|
48860
|
-
requirePositive$
|
|
48861
|
-
requirePositive$
|
|
48862
|
-
requirePositive$
|
|
48863
|
-
requirePositive$
|
|
48859
|
+
requirePositive$8("gearBodySpoked", "outerRadius", options.outerRadius);
|
|
48860
|
+
requirePositive$8("gearBodySpoked", "faceWidth", options.faceWidth);
|
|
48861
|
+
requirePositive$8("gearBodySpoked", "rimWidth", options.rimWidth);
|
|
48862
|
+
requirePositive$8("gearBodySpoked", "hubDiameter", options.hubDiameter);
|
|
48863
|
+
requirePositive$8("gearBodySpoked", "spokeWidth", options.spokeWidth);
|
|
48864
48864
|
if (!Number.isInteger(options.spokeCount) || options.spokeCount < 2) {
|
|
48865
48865
|
throw new Error('gearBodySpoked: "spokeCount" must be an integer >= 2');
|
|
48866
48866
|
}
|
|
@@ -48883,12 +48883,12 @@ function gearBodySpoked(options) {
|
|
|
48883
48883
|
}
|
|
48884
48884
|
function gearBodyFromProfile(profile, options) {
|
|
48885
48885
|
if (!(profile instanceof Sketch)) throw new Error('gearBodyFromProfile: "profile" must be a Sketch');
|
|
48886
|
-
requirePositive$
|
|
48886
|
+
requirePositive$8("gearBodyFromProfile", "faceWidth", options.faceWidth);
|
|
48887
48887
|
const bore = options.boreDiameter ?? 0;
|
|
48888
48888
|
if (!Number.isFinite(bore) || bore < 0) throw new Error('gearBodyFromProfile: "boreDiameter" must be >= 0');
|
|
48889
48889
|
return cutBore$1(sketchExtrude(profile, options.faceWidth), bore);
|
|
48890
48890
|
}
|
|
48891
|
-
function requirePositive$
|
|
48891
|
+
function requirePositive$7(scope, name, value) {
|
|
48892
48892
|
if (!isFinitePositive(value)) throw new Error(`${scope}: "${name}" must be > 0`);
|
|
48893
48893
|
}
|
|
48894
48894
|
function requireFiniteAngle(scope, name, value) {
|
|
@@ -48950,7 +48950,7 @@ function buildSpurTeethRegion(options, name, faceWidth) {
|
|
|
48950
48950
|
}
|
|
48951
48951
|
function buildSolidArcRegion(options, name, faceWidth) {
|
|
48952
48952
|
const scope = "driveWheel.addSolidArcBetween";
|
|
48953
|
-
requirePositive$
|
|
48953
|
+
requirePositive$7(scope, "outerRadius", options.outerRadius);
|
|
48954
48954
|
const innerRadius = options.innerRadius ?? 0;
|
|
48955
48955
|
if (!Number.isFinite(innerRadius) || innerRadius < 0) throw new Error(`${scope}: "innerRadius" must be >= 0`);
|
|
48956
48956
|
if (innerRadius >= options.outerRadius) throw new Error(`${scope}: "innerRadius" must be smaller than "outerRadius"`);
|
|
@@ -49016,7 +49016,7 @@ class DriveWheelBuilder {
|
|
|
49016
49016
|
__publicField(this, "boreDiameter");
|
|
49017
49017
|
__publicField(this, "regions", []);
|
|
49018
49018
|
if (options.body !== void 0 && !(options.body instanceof Shape)) throw new Error('driveWheel: "body" must be a Shape');
|
|
49019
|
-
if (options.faceWidth !== void 0) requirePositive$
|
|
49019
|
+
if (options.faceWidth !== void 0) requirePositive$7("driveWheel", "faceWidth", options.faceWidth);
|
|
49020
49020
|
const boreDiameter = options.boreDiameter ?? 0;
|
|
49021
49021
|
if (!Number.isFinite(boreDiameter) || boreDiameter < 0) throw new Error('driveWheel: "boreDiameter" must be >= 0');
|
|
49022
49022
|
this.body = options.body;
|
|
@@ -49051,7 +49051,7 @@ class DriveWheelBuilder {
|
|
|
49051
49051
|
if (options.innerRadius !== void 0 && (!Number.isFinite(options.innerRadius) || options.innerRadius < 0)) {
|
|
49052
49052
|
throw new Error(`${scope}: "innerRadius" must be >= 0`);
|
|
49053
49053
|
}
|
|
49054
|
-
if (options.outerRadius !== void 0) requirePositive$
|
|
49054
|
+
if (options.outerRadius !== void 0) requirePositive$7(scope, "outerRadius", options.outerRadius);
|
|
49055
49055
|
this.regions.push({
|
|
49056
49056
|
shape: shape.clone(),
|
|
49057
49057
|
meta: {
|
|
@@ -49117,7 +49117,7 @@ class DriveWheelBuilder {
|
|
|
49117
49117
|
resolveFaceWidth(scope, localFaceWidth) {
|
|
49118
49118
|
const faceWidth = localFaceWidth ?? this.faceWidth;
|
|
49119
49119
|
if (faceWidth === void 0) throw new Error(`${scope}: "faceWidth" is required unless driveWheel({ faceWidth }) was set`);
|
|
49120
|
-
requirePositive$
|
|
49120
|
+
requirePositive$7(scope, "faceWidth", faceWidth);
|
|
49121
49121
|
if (this.faceWidth !== void 0 && localFaceWidth !== void 0 && Math.abs(this.faceWidth - localFaceWidth) > EPSILON$1) {
|
|
49122
49122
|
throw new Error(`${scope}: region faceWidth must match driveWheel faceWidth`);
|
|
49123
49123
|
}
|
|
@@ -50270,6 +50270,1867 @@ function washer(size, options) {
|
|
|
50270
50270
|
const bore = cylinder(dims.t + 1, dims.id / 2, void 0, segs);
|
|
50271
50271
|
return outer.subtract(bore);
|
|
50272
50272
|
}
|
|
50273
|
+
function requirePositive$6(value, name) {
|
|
50274
|
+
if (!Number.isFinite(value) || value <= 0) throw new Error(`${name} must be a positive finite number`);
|
|
50275
|
+
return value;
|
|
50276
|
+
}
|
|
50277
|
+
function requireNonNegative(value, name) {
|
|
50278
|
+
if (!Number.isFinite(value) || value < 0) throw new Error(`${name} must be a non-negative finite number`);
|
|
50279
|
+
return value;
|
|
50280
|
+
}
|
|
50281
|
+
function metricWasherSizeForPin(pinDiameter) {
|
|
50282
|
+
if (pinDiameter <= 2) return "M2";
|
|
50283
|
+
if (pinDiameter <= 2.5) return "M2.5";
|
|
50284
|
+
if (pinDiameter <= 3) return "M3";
|
|
50285
|
+
if (pinDiameter <= 4) return "M4";
|
|
50286
|
+
if (pinDiameter <= 5) return "M5";
|
|
50287
|
+
if (pinDiameter <= 6) return "M6";
|
|
50288
|
+
if (pinDiameter <= 8) return "M8";
|
|
50289
|
+
return "M10";
|
|
50290
|
+
}
|
|
50291
|
+
function cylinderAlongX(length4, radius, xCenter, segments) {
|
|
50292
|
+
return cylinder(length4, radius, void 0, segments).pointAlong([1, 0, 0]).translate(xCenter - length4 / 2, 0, 0);
|
|
50293
|
+
}
|
|
50294
|
+
function tubeAlongX(length4, outerRadius, innerRadius, xCenter, segments) {
|
|
50295
|
+
return cylinderAlongX(length4, outerRadius, xCenter, segments).subtract(cylinderAlongX(length4 + 0.4, innerRadius, xCenter, segments));
|
|
50296
|
+
}
|
|
50297
|
+
function cylinderAlongY(length4, radius, yCenter, segments) {
|
|
50298
|
+
return cylinder(length4, radius, void 0, segments).pointAlong([0, 1, 0]).translate(0, yCenter - length4 / 2, 0);
|
|
50299
|
+
}
|
|
50300
|
+
function tubeAlongY(length4, outerRadius, innerRadius, yCenter, segments) {
|
|
50301
|
+
return cylinderAlongY(length4, outerRadius, yCenter, segments).subtract(cylinderAlongY(length4 + 0.4, innerRadius, yCenter, segments));
|
|
50302
|
+
}
|
|
50303
|
+
function tubeAlongZ(height, outerRadius, innerRadius, segments) {
|
|
50304
|
+
return cylinder(height, outerRadius, void 0, segments).subtract(
|
|
50305
|
+
cylinder(height + 0.4, innerRadius, void 0, segments).translate(0, 0, -0.2)
|
|
50306
|
+
);
|
|
50307
|
+
}
|
|
50308
|
+
function washerAlongX(size, xCenter, segments) {
|
|
50309
|
+
const dims = WASHER_TABLE[size];
|
|
50310
|
+
return washer(size, { segments }).pointAlong([1, 0, 0]).translate(xCenter - dims.t / 2, 0, 0);
|
|
50311
|
+
}
|
|
50312
|
+
function resolveBoltInset(raw, fallback) {
|
|
50313
|
+
if (raw === void 0) return [fallback, fallback];
|
|
50314
|
+
if (typeof raw === "number") return [requirePositive$6(raw, "boltInset"), requirePositive$6(raw, "boltInset")];
|
|
50315
|
+
if (raw.length !== 2) throw new Error("boltInset tuple must be [x, y]");
|
|
50316
|
+
return [requirePositive$6(raw[0], "boltInset[0]"), requirePositive$6(raw[1], "boltInset[1]")];
|
|
50317
|
+
}
|
|
50318
|
+
function validateBoltPositionsForServiceCover(args) {
|
|
50319
|
+
args.positions.forEach(([x2, y2], index2) => {
|
|
50320
|
+
if (!Number.isFinite(x2) || !Number.isFinite(y2)) {
|
|
50321
|
+
throw new Error(`boltedServiceCover: boltPositions[${index2}] must contain finite numbers`);
|
|
50322
|
+
}
|
|
50323
|
+
if (Math.abs(x2) + args.holeRadius >= args.coverWidth / 2 || Math.abs(y2) + args.holeRadius >= args.coverDepth / 2) {
|
|
50324
|
+
throw new Error(`boltedServiceCover: boltPositions[${index2}] is too close to the cover edge`);
|
|
50325
|
+
}
|
|
50326
|
+
const overlapsOpening = Math.abs(x2) - args.holeRadius <= args.openingWidth / 2 && Math.abs(y2) - args.holeRadius <= args.openingDepth / 2;
|
|
50327
|
+
if (overlapsOpening) {
|
|
50328
|
+
throw new Error(
|
|
50329
|
+
`boltedServiceCover: boltPositions[${index2}] lands over the service opening; decrease boltInset, increase ledgeWidth, or provide a smaller opening`
|
|
50330
|
+
);
|
|
50331
|
+
}
|
|
50332
|
+
});
|
|
50333
|
+
}
|
|
50334
|
+
function placeCutterAtPositions(cutter, positions, z2) {
|
|
50335
|
+
return union(...positions.map(([x2, y2]) => cutter.translate(x2, y2, z2)));
|
|
50336
|
+
}
|
|
50337
|
+
function boltedServiceCover(options) {
|
|
50338
|
+
const width = requirePositive$6(options.width, "width");
|
|
50339
|
+
const depth = requirePositive$6(options.depth, "depth");
|
|
50340
|
+
const coverThickness = requirePositive$6(options.coverThickness ?? 3, "coverThickness");
|
|
50341
|
+
const parentThickness = requirePositive$6(options.parentThickness ?? 8, "parentThickness");
|
|
50342
|
+
const ledgeWidth = requirePositive$6(options.ledgeWidth ?? 8, "ledgeWidth");
|
|
50343
|
+
const gasketThickness = Math.max(0, options.gasketThickness ?? 0.8);
|
|
50344
|
+
const gasketInset = Math.max(0, options.gasketInset ?? 2);
|
|
50345
|
+
const screwSize = options.screwSize ?? "M4";
|
|
50346
|
+
const segments = options.segments ?? 36;
|
|
50347
|
+
const sizeData = METRIC_HOLE_TABLE[screwSize];
|
|
50348
|
+
if (!sizeData) throw new Error(`boltedServiceCover: unsupported screwSize "${screwSize}"`);
|
|
50349
|
+
const screwLength = requirePositive$6(
|
|
50350
|
+
options.screwLength ?? parentThickness + gasketThickness + coverThickness + 4,
|
|
50351
|
+
"screwLength"
|
|
50352
|
+
);
|
|
50353
|
+
const coverFit = options.coverFit ?? "normal";
|
|
50354
|
+
const counterboreEnabled = options.counterbore ?? true;
|
|
50355
|
+
const [insetX, insetY] = resolveBoltInset(options.boltInset, Math.max(ledgeWidth * 0.65, sizeData.head * 0.75));
|
|
50356
|
+
if (insetX * 2 >= width || insetY * 2 >= depth) {
|
|
50357
|
+
throw new Error("boltedServiceCover: boltInset leaves no room for a four-corner bolt pattern");
|
|
50358
|
+
}
|
|
50359
|
+
const boltPositions = options.boltPositions ?? [
|
|
50360
|
+
[-width / 2 + insetX, -depth / 2 + insetY],
|
|
50361
|
+
[width / 2 - insetX, -depth / 2 + insetY],
|
|
50362
|
+
[-width / 2 + insetX, depth / 2 - insetY],
|
|
50363
|
+
[width / 2 - insetX, depth / 2 - insetY]
|
|
50364
|
+
];
|
|
50365
|
+
if (boltPositions.length === 0) throw new Error("boltedServiceCover: boltPositions must contain at least one point");
|
|
50366
|
+
const parentWidth = width + ledgeWidth * 2;
|
|
50367
|
+
const parentDepth = depth + ledgeWidth * 2;
|
|
50368
|
+
const openingWidth = Math.max(1, width - ledgeWidth * 2);
|
|
50369
|
+
const openingDepth = Math.max(1, depth - ledgeWidth * 2);
|
|
50370
|
+
validateBoltPositionsForServiceCover({
|
|
50371
|
+
positions: boltPositions,
|
|
50372
|
+
coverWidth: width,
|
|
50373
|
+
coverDepth: depth,
|
|
50374
|
+
openingWidth,
|
|
50375
|
+
openingDepth,
|
|
50376
|
+
holeRadius: sizeData[coverFit] / 2
|
|
50377
|
+
});
|
|
50378
|
+
const coverHole = fastenerHole({
|
|
50379
|
+
size: screwSize,
|
|
50380
|
+
fit: coverFit,
|
|
50381
|
+
depth: coverThickness + 0.6,
|
|
50382
|
+
center: true,
|
|
50383
|
+
segments,
|
|
50384
|
+
...counterboreEnabled ? { counterbore: { depth: Math.min(coverThickness * 0.6, Math.max(0.6, coverThickness - 0.4)) } } : {}
|
|
50385
|
+
});
|
|
50386
|
+
const parentTap = fastenerHole({ size: screwSize, fit: "tap", depth: parentThickness + 0.6, center: true, segments });
|
|
50387
|
+
const parentThreadEnvelope = fastenerHole({
|
|
50388
|
+
size: screwSize,
|
|
50389
|
+
fit: "close",
|
|
50390
|
+
depth: parentThickness + 0.6,
|
|
50391
|
+
center: true,
|
|
50392
|
+
segments
|
|
50393
|
+
});
|
|
50394
|
+
const openingCutter = box(openingWidth, openingDepth, parentThickness + 1).translate(0, 0, -0.5);
|
|
50395
|
+
const parentTappedPattern = placeCutterAtPositions(parentTap, boltPositions, parentThickness / 2);
|
|
50396
|
+
const parentThreadEnvelopePattern = placeCutterAtPositions(parentThreadEnvelope, boltPositions, parentThickness / 2);
|
|
50397
|
+
const parent = box(parentWidth, parentDepth, parentThickness).subtract(openingCutter).subtract(parentThreadEnvelopePattern).color("#4b5563");
|
|
50398
|
+
let coverBlank = box(width, depth, coverThickness);
|
|
50399
|
+
if (options.pullTabs ?? true) {
|
|
50400
|
+
const tabWidth = Math.min(width * 0.18, Math.max(sizeData.head * 1.6, 12));
|
|
50401
|
+
const tabDepth = Math.max(4, coverThickness * 1.4);
|
|
50402
|
+
const tabOverlap = Math.min(0.5, tabDepth * 0.25);
|
|
50403
|
+
const tabY = -depth / 2 - tabDepth / 2 + tabOverlap;
|
|
50404
|
+
const tabX = width * 0.23;
|
|
50405
|
+
coverBlank = union(
|
|
50406
|
+
coverBlank,
|
|
50407
|
+
box(tabWidth, tabDepth, coverThickness).translate(-tabX, tabY, 0),
|
|
50408
|
+
box(tabWidth, tabDepth, coverThickness).translate(tabX, tabY, 0)
|
|
50409
|
+
);
|
|
50410
|
+
}
|
|
50411
|
+
const coverClearancePattern = placeCutterAtPositions(coverHole, boltPositions, coverThickness / 2);
|
|
50412
|
+
const cover = coverBlank.subtract(coverClearancePattern).translate(0, 0, parentThickness + gasketThickness).color("#334155");
|
|
50413
|
+
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;
|
|
50414
|
+
const hardware = fastenerSet(screwSize, screwLength, { washerUnderHead: false, washerUnderNut: false, fit: coverFit, segments });
|
|
50415
|
+
const screwOriginZ = parentThickness + gasketThickness + coverThickness;
|
|
50416
|
+
const screws = boltPositions.map(([x2, y2]) => hardware.bolt.translate(x2, y2, screwOriginZ).color("#94a3b8"));
|
|
50417
|
+
const parts = [
|
|
50418
|
+
{ name: "service cover parent ledge with threaded hole envelopes", shape: parent },
|
|
50419
|
+
...gasket ? [{ name: "service cover gasket seated on ledge", shape: gasket }] : [],
|
|
50420
|
+
{ name: "bolted service cover plate with fused pull tabs", shape: cover },
|
|
50421
|
+
...screws.map((shape, index2) => ({ name: `installed ${screwSize} cover screw ${index2 + 1}`, shape }))
|
|
50422
|
+
];
|
|
50423
|
+
return {
|
|
50424
|
+
parts,
|
|
50425
|
+
parent,
|
|
50426
|
+
cover,
|
|
50427
|
+
gasket,
|
|
50428
|
+
screws,
|
|
50429
|
+
boltPositions,
|
|
50430
|
+
cutters: {
|
|
50431
|
+
coverClearance: coverClearancePattern,
|
|
50432
|
+
parentTapped: parentTappedPattern,
|
|
50433
|
+
parentThreadEnvelope: parentThreadEnvelopePattern
|
|
50434
|
+
},
|
|
50435
|
+
dims: {
|
|
50436
|
+
width,
|
|
50437
|
+
depth,
|
|
50438
|
+
coverThickness,
|
|
50439
|
+
parentThickness,
|
|
50440
|
+
ledgeWidth,
|
|
50441
|
+
gasketThickness,
|
|
50442
|
+
screwSize,
|
|
50443
|
+
screwLength,
|
|
50444
|
+
clearanceDia: sizeData[coverFit],
|
|
50445
|
+
tapDia: sizeData.tap,
|
|
50446
|
+
threadEnvelopeDia: sizeData.close
|
|
50447
|
+
}
|
|
50448
|
+
};
|
|
50449
|
+
}
|
|
50450
|
+
function datumEnclosureAssembly(options) {
|
|
50451
|
+
const width = requirePositive$6(options.width, "width");
|
|
50452
|
+
const depth = requirePositive$6(options.depth, "depth");
|
|
50453
|
+
const height = requirePositive$6(options.height, "height");
|
|
50454
|
+
const wallThickness = requirePositive$6(options.wallThickness ?? 2.4, "wallThickness");
|
|
50455
|
+
const baseThickness = requirePositive$6(options.baseThickness ?? wallThickness, "baseThickness");
|
|
50456
|
+
const coverThickness = requirePositive$6(options.coverThickness ?? 2.4, "coverThickness");
|
|
50457
|
+
const ledgeWidth = requirePositive$6(options.ledgeWidth ?? Math.max(3.6, wallThickness * 1.35), "ledgeWidth");
|
|
50458
|
+
const gasketThickness = requireNonNegative(options.gasketThickness ?? 0.8, "gasketThickness");
|
|
50459
|
+
const faceClearance = requirePositive$6(options.faceClearance ?? 0.04, "faceClearance");
|
|
50460
|
+
const screwSize = options.screwSize ?? "M3";
|
|
50461
|
+
const coverFit = options.coverFit ?? "normal";
|
|
50462
|
+
const segments = options.segments ?? 32;
|
|
50463
|
+
const sizeData = METRIC_HOLE_TABLE[screwSize];
|
|
50464
|
+
if (!sizeData) throw new Error(`datumEnclosureAssembly: unsupported screwSize "${screwSize}"`);
|
|
50465
|
+
const innerWidth = width - wallThickness * 2;
|
|
50466
|
+
const innerDepth = depth - wallThickness * 2;
|
|
50467
|
+
if (innerWidth <= ledgeWidth * 2 + 8 || innerDepth <= ledgeWidth * 2 + 8) {
|
|
50468
|
+
throw new Error("datumEnclosureAssembly: wallThickness and ledgeWidth leave too little internal opening");
|
|
50469
|
+
}
|
|
50470
|
+
if (height <= baseThickness + coverThickness + 4) {
|
|
50471
|
+
throw new Error("datumEnclosureAssembly: height must leave room for internal ribs and standoffs");
|
|
50472
|
+
}
|
|
50473
|
+
const standoffDiameter = requirePositive$6(
|
|
50474
|
+
options.standoffDiameter ?? Math.max(sizeData.head * 1.65, sizeData.close * 2.2),
|
|
50475
|
+
"standoffDiameter"
|
|
50476
|
+
);
|
|
50477
|
+
const minInset = wallThickness + Math.max(ledgeWidth, standoffDiameter / 2 + 1.2);
|
|
50478
|
+
const [insetX, insetY] = resolveBoltInset(options.screwInset, minInset);
|
|
50479
|
+
if (insetX * 2 >= width || insetY * 2 >= depth) {
|
|
50480
|
+
throw new Error("datumEnclosureAssembly: screwInset leaves no room for the standoff datum");
|
|
50481
|
+
}
|
|
50482
|
+
const screwPositions = options.screwPositions ?? [
|
|
50483
|
+
[-width / 2 + insetX, -depth / 2 + insetY],
|
|
50484
|
+
[width / 2 - insetX, -depth / 2 + insetY],
|
|
50485
|
+
[-width / 2 + insetX, depth / 2 - insetY],
|
|
50486
|
+
[width / 2 - insetX, depth / 2 - insetY]
|
|
50487
|
+
];
|
|
50488
|
+
if (screwPositions.length === 0) throw new Error("datumEnclosureAssembly: screwPositions must contain at least one point");
|
|
50489
|
+
for (const [index2, [x2, y2]] of screwPositions.entries()) {
|
|
50490
|
+
if (!Number.isFinite(x2) || !Number.isFinite(y2)) {
|
|
50491
|
+
throw new Error(`datumEnclosureAssembly: screwPositions[${index2}] must contain finite numbers`);
|
|
50492
|
+
}
|
|
50493
|
+
if (Math.abs(x2) + standoffDiameter / 2 > innerWidth / 2 || Math.abs(y2) + standoffDiameter / 2 > innerDepth / 2) {
|
|
50494
|
+
throw new Error(`datumEnclosureAssembly: screwPositions[${index2}] does not fit inside the enclosure walls`);
|
|
50495
|
+
}
|
|
50496
|
+
}
|
|
50497
|
+
const ribHeight = requirePositive$6(options.ribHeight ?? Math.min(height * 0.24, Math.max(2.4, baseThickness * 1.4)), "ribHeight");
|
|
50498
|
+
const ribThickness = requirePositive$6(options.ribThickness ?? Math.max(1.2, wallThickness * 0.75), "ribThickness");
|
|
50499
|
+
const portWidth = requirePositive$6(options.portWidth ?? Math.min(innerWidth * 0.28, Math.max(12, width * 0.16)), "portWidth");
|
|
50500
|
+
const portHeight = requirePositive$6(options.portHeight ?? Math.min(height * 0.42, Math.max(5, height * 0.28)), "portHeight");
|
|
50501
|
+
if (portWidth >= innerWidth - ledgeWidth * 2) {
|
|
50502
|
+
throw new Error("datumEnclosureAssembly: portWidth must fit between internal ledges and standoffs");
|
|
50503
|
+
}
|
|
50504
|
+
if (portHeight >= height - baseThickness - 1) {
|
|
50505
|
+
throw new Error("datumEnclosureAssembly: portHeight must leave material above and below the service port");
|
|
50506
|
+
}
|
|
50507
|
+
const screwLength = requirePositive$6(
|
|
50508
|
+
options.screwLength ?? coverThickness + gasketThickness + Math.max(6, height * 0.45),
|
|
50509
|
+
"screwLength"
|
|
50510
|
+
);
|
|
50511
|
+
const coverHole = fastenerHole({
|
|
50512
|
+
size: screwSize,
|
|
50513
|
+
fit: coverFit,
|
|
50514
|
+
depth: coverThickness + 0.6,
|
|
50515
|
+
center: true,
|
|
50516
|
+
segments,
|
|
50517
|
+
counterbore: { depth: Math.min(coverThickness * 0.6, Math.max(0.6, coverThickness - 0.35)) }
|
|
50518
|
+
});
|
|
50519
|
+
const standoffTap = fastenerHole({ size: screwSize, fit: "tap", depth: height + 0.8, center: true, segments });
|
|
50520
|
+
const standoffThreadEnvelope = fastenerHole({ size: screwSize, fit: "close", depth: height + 0.8, center: true, segments });
|
|
50521
|
+
const coverClearance = placeCutterAtPositions(coverHole, screwPositions, coverThickness / 2);
|
|
50522
|
+
const standoffTappedPattern = placeCutterAtPositions(standoffTap, screwPositions, height / 2);
|
|
50523
|
+
const standoffThreadEnvelopePattern = placeCutterAtPositions(standoffThreadEnvelope, screwPositions, height / 2);
|
|
50524
|
+
const fuseOverlap = Math.min(0.06, Math.max(0.02, wallThickness * 0.02));
|
|
50525
|
+
const ledgeThickness = Math.min(Math.max(1.1, coverThickness * 0.45), height * 0.2);
|
|
50526
|
+
const sideX = width / 2 - wallThickness / 2;
|
|
50527
|
+
const sideY = depth / 2 - wallThickness / 2;
|
|
50528
|
+
const ledgeZ = height - ledgeThickness;
|
|
50529
|
+
const baseSolids = [
|
|
50530
|
+
box(width, depth, baseThickness),
|
|
50531
|
+
box(wallThickness, depth, height).translate(sideX, 0, 0),
|
|
50532
|
+
box(wallThickness, depth, height).translate(-sideX, 0, 0),
|
|
50533
|
+
box(width, wallThickness, height).translate(0, sideY, 0),
|
|
50534
|
+
box(width, wallThickness, height).translate(0, -sideY, 0),
|
|
50535
|
+
box(ledgeWidth, innerDepth, ledgeThickness).translate(-width / 2 + wallThickness + ledgeWidth / 2, 0, ledgeZ),
|
|
50536
|
+
box(ledgeWidth, innerDepth, ledgeThickness).translate(width / 2 - wallThickness - ledgeWidth / 2, 0, ledgeZ),
|
|
50537
|
+
box(innerWidth, ledgeWidth, ledgeThickness).translate(0, -depth / 2 + wallThickness + ledgeWidth / 2, ledgeZ),
|
|
50538
|
+
box(innerWidth, ledgeWidth, ledgeThickness).translate(0, depth / 2 - wallThickness - ledgeWidth / 2, ledgeZ),
|
|
50539
|
+
box(Math.max(1, innerWidth - standoffDiameter * 1.8), ribThickness, ribHeight + fuseOverlap).translate(
|
|
50540
|
+
0,
|
|
50541
|
+
0,
|
|
50542
|
+
baseThickness - fuseOverlap
|
|
50543
|
+
),
|
|
50544
|
+
box(ribThickness, Math.max(1, innerDepth - standoffDiameter * 1.8), ribHeight + fuseOverlap).translate(
|
|
50545
|
+
0,
|
|
50546
|
+
0,
|
|
50547
|
+
baseThickness - fuseOverlap
|
|
50548
|
+
),
|
|
50549
|
+
...screwPositions.map(
|
|
50550
|
+
([x2, y2]) => cylinder(height - baseThickness + fuseOverlap, standoffDiameter / 2, void 0, segments).translate(
|
|
50551
|
+
x2,
|
|
50552
|
+
y2,
|
|
50553
|
+
baseThickness - fuseOverlap
|
|
50554
|
+
)
|
|
50555
|
+
)
|
|
50556
|
+
];
|
|
50557
|
+
const servicePort = box(portWidth, wallThickness + 1, portHeight).translate(
|
|
50558
|
+
0,
|
|
50559
|
+
-depth / 2 + wallThickness / 2,
|
|
50560
|
+
baseThickness + Math.max(0.8, (height - baseThickness - portHeight) * 0.35)
|
|
50561
|
+
);
|
|
50562
|
+
const base = union(...baseSolids).subtract(standoffThreadEnvelopePattern).subtract(servicePort).color("#475569");
|
|
50563
|
+
const gasketFrameCutter = box(Math.max(1, width - ledgeWidth * 2), Math.max(1, depth - ledgeWidth * 2), gasketThickness + 0.6).translate(
|
|
50564
|
+
0,
|
|
50565
|
+
0,
|
|
50566
|
+
-0.3
|
|
50567
|
+
);
|
|
50568
|
+
const gasket = gasketThickness > 0 ? box(width, depth, gasketThickness).subtract(gasketFrameCutter).subtract(placeCutterAtPositions(coverHole, screwPositions, gasketThickness / 2)).translate(0, 0, height + faceClearance).color("#111827") : null;
|
|
50569
|
+
const coverZ = height + faceClearance + (gasket ? gasketThickness + faceClearance : 0);
|
|
50570
|
+
const cover = box(width, depth, coverThickness).subtract(coverClearance).translate(0, 0, coverZ).color("#334155");
|
|
50571
|
+
const hardware = fastenerSet(screwSize, screwLength, { washerUnderHead: false, washerUnderNut: false, fit: coverFit, segments });
|
|
50572
|
+
const screwOriginZ = coverZ + coverThickness;
|
|
50573
|
+
const screws = screwPositions.map(([x2, y2]) => hardware.bolt.translate(x2, y2, screwOriginZ).color("#94a3b8"));
|
|
50574
|
+
const parts = [
|
|
50575
|
+
{ name: "datum enclosure base tray with walls ribs standoffs and service port", shape: base },
|
|
50576
|
+
...gasket ? [{ name: "datum enclosure gasket seated on continuous ledge", shape: gasket }] : [],
|
|
50577
|
+
{ name: "datum enclosure cover plate with matched screw pattern", shape: cover },
|
|
50578
|
+
...screws.map((shape, index2) => ({ name: `installed ${screwSize} enclosure screw ${index2 + 1}`, shape }))
|
|
50579
|
+
];
|
|
50580
|
+
return {
|
|
50581
|
+
parts,
|
|
50582
|
+
base,
|
|
50583
|
+
cover,
|
|
50584
|
+
gasket,
|
|
50585
|
+
screws,
|
|
50586
|
+
screwPositions,
|
|
50587
|
+
cutters: {
|
|
50588
|
+
coverClearance,
|
|
50589
|
+
standoffTapped: standoffTappedPattern,
|
|
50590
|
+
standoffThreadEnvelope: standoffThreadEnvelopePattern,
|
|
50591
|
+
servicePort
|
|
50592
|
+
},
|
|
50593
|
+
dims: {
|
|
50594
|
+
width,
|
|
50595
|
+
depth,
|
|
50596
|
+
height,
|
|
50597
|
+
innerWidth,
|
|
50598
|
+
innerDepth,
|
|
50599
|
+
wallThickness,
|
|
50600
|
+
baseThickness,
|
|
50601
|
+
coverThickness,
|
|
50602
|
+
ledgeWidth,
|
|
50603
|
+
gasketThickness,
|
|
50604
|
+
faceClearance,
|
|
50605
|
+
screwSize,
|
|
50606
|
+
screwLength,
|
|
50607
|
+
standoffDiameter,
|
|
50608
|
+
ribHeight,
|
|
50609
|
+
ribThickness,
|
|
50610
|
+
portWidth,
|
|
50611
|
+
portHeight,
|
|
50612
|
+
clearanceDia: sizeData[coverFit],
|
|
50613
|
+
tapDia: sizeData.tap,
|
|
50614
|
+
threadEnvelopeDia: sizeData.close
|
|
50615
|
+
}
|
|
50616
|
+
};
|
|
50617
|
+
}
|
|
50618
|
+
function snapLatchCoverAssembly(options) {
|
|
50619
|
+
const width = requirePositive$6(options.width, "width");
|
|
50620
|
+
const depth = requirePositive$6(options.depth, "depth");
|
|
50621
|
+
const coverThickness = requirePositive$6(options.coverThickness ?? 2.4, "coverThickness");
|
|
50622
|
+
const parentThickness = requirePositive$6(options.parentThickness ?? 6, "parentThickness");
|
|
50623
|
+
const ledgeWidth = requirePositive$6(options.ledgeWidth ?? 8, "ledgeWidth");
|
|
50624
|
+
const runningClearance = requirePositive$6(options.runningClearance ?? 0.25, "runningClearance");
|
|
50625
|
+
const faceClearance = requirePositive$6(options.faceClearance ?? 0.04, "faceClearance");
|
|
50626
|
+
const latchWidth = requirePositive$6(options.latchWidth ?? Math.min(width * 0.22, Math.max(12, width * 0.16)), "latchWidth");
|
|
50627
|
+
const latchThickness = requirePositive$6(options.latchThickness ?? 1.6, "latchThickness");
|
|
50628
|
+
const hookThrow = requirePositive$6(options.hookThrow ?? 3.2, "hookThrow");
|
|
50629
|
+
const hookThickness = requirePositive$6(options.hookThickness ?? 1.6, "hookThickness");
|
|
50630
|
+
const openingWidth = width - ledgeWidth * 2;
|
|
50631
|
+
const openingDepth = depth - ledgeWidth * 2;
|
|
50632
|
+
if (openingWidth <= Math.max(8, latchWidth * 0.8) || openingDepth <= 8) {
|
|
50633
|
+
throw new Error("snapLatchCoverAssembly: ledgeWidth leaves too little service opening under the cover");
|
|
50634
|
+
}
|
|
50635
|
+
if (latchWidth >= openingWidth) {
|
|
50636
|
+
throw new Error("snapLatchCoverAssembly: latchWidth must fit along the receiver opening");
|
|
50637
|
+
}
|
|
50638
|
+
if (latchThickness + runningClearance * 2 >= ledgeWidth) {
|
|
50639
|
+
throw new Error("snapLatchCoverAssembly: latchThickness and clearance must fit inside the receiver ledge");
|
|
50640
|
+
}
|
|
50641
|
+
if (hookThrow + latchThickness / 2 + runningClearance >= ledgeWidth * 1.5) {
|
|
50642
|
+
throw new Error("snapLatchCoverAssembly: hookThrow is too large for the available underside catch land");
|
|
50643
|
+
}
|
|
50644
|
+
const parentWidth = width + ledgeWidth * 2;
|
|
50645
|
+
const parentDepth = depth + ledgeWidth * 2;
|
|
50646
|
+
const fuseOverlap = Math.min(0.04, faceClearance * 0.7);
|
|
50647
|
+
const hookClearance = Math.min(0.08, runningClearance * 0.32);
|
|
50648
|
+
const coverMinZ = parentThickness + faceClearance;
|
|
50649
|
+
const stemMinZ = -hookClearance - hookThickness;
|
|
50650
|
+
const stemHeight = coverMinZ + fuseOverlap - stemMinZ;
|
|
50651
|
+
const slotY = openingDepth / 2 + ledgeWidth / 2;
|
|
50652
|
+
const latchWindow = (sign2) => box(latchWidth + runningClearance * 2, latchThickness + runningClearance * 2, parentThickness + 0.8).translate(
|
|
50653
|
+
0,
|
|
50654
|
+
sign2 * slotY,
|
|
50655
|
+
-0.4
|
|
50656
|
+
);
|
|
50657
|
+
const latchWindows = union(latchWindow(1), latchWindow(-1));
|
|
50658
|
+
const serviceOpening = box(openingWidth, openingDepth, parentThickness + 1).translate(0, 0, -0.5);
|
|
50659
|
+
const parent = box(parentWidth, parentDepth, parentThickness).subtract(serviceOpening).subtract(latchWindows).color("#475569");
|
|
50660
|
+
const coverPlate = box(width, depth, coverThickness).translate(0, 0, coverMinZ);
|
|
50661
|
+
const snapHook = (sign2) => {
|
|
50662
|
+
const y2 = sign2 * slotY;
|
|
50663
|
+
const stem = box(latchWidth, latchThickness, stemHeight).translate(0, y2, stemMinZ);
|
|
50664
|
+
const barb = box(latchWidth, latchThickness + hookThrow, hookThickness).translate(
|
|
50665
|
+
0,
|
|
50666
|
+
y2 + sign2 * (hookThrow / 2),
|
|
50667
|
+
stemMinZ
|
|
50668
|
+
);
|
|
50669
|
+
const rootRib = box(latchWidth, Math.max(latchThickness, hookThrow * 0.55), coverThickness * 0.65).translate(
|
|
50670
|
+
0,
|
|
50671
|
+
y2 - sign2 * (ledgeWidth * 0.18),
|
|
50672
|
+
coverMinZ
|
|
50673
|
+
);
|
|
50674
|
+
return union(stem, barb, rootRib);
|
|
50675
|
+
};
|
|
50676
|
+
const cover = union(coverPlate, snapHook(1), snapHook(-1)).color("#111827");
|
|
50677
|
+
const parts = [
|
|
50678
|
+
{ name: "snap cover receiver frame with latch windows and catch lands", shape: parent },
|
|
50679
|
+
{ name: "one-piece snap cover with fused hooks and underside barbs", shape: cover }
|
|
50680
|
+
];
|
|
50681
|
+
return {
|
|
50682
|
+
parts,
|
|
50683
|
+
parent,
|
|
50684
|
+
cover,
|
|
50685
|
+
cutters: {
|
|
50686
|
+
serviceOpening,
|
|
50687
|
+
latchWindows
|
|
50688
|
+
},
|
|
50689
|
+
dims: {
|
|
50690
|
+
width,
|
|
50691
|
+
depth,
|
|
50692
|
+
parentWidth,
|
|
50693
|
+
parentDepth,
|
|
50694
|
+
openingWidth,
|
|
50695
|
+
openingDepth,
|
|
50696
|
+
coverThickness,
|
|
50697
|
+
parentThickness,
|
|
50698
|
+
ledgeWidth,
|
|
50699
|
+
latchWidth,
|
|
50700
|
+
latchThickness,
|
|
50701
|
+
hookThrow,
|
|
50702
|
+
hookThickness,
|
|
50703
|
+
runningClearance,
|
|
50704
|
+
faceClearance
|
|
50705
|
+
}
|
|
50706
|
+
};
|
|
50707
|
+
}
|
|
50708
|
+
function pinnedLeverAssembly(options) {
|
|
50709
|
+
const armLength = requirePositive$6(options.armLength, "armLength");
|
|
50710
|
+
const armWidth = requirePositive$6(options.armWidth ?? 10, "armWidth");
|
|
50711
|
+
const leverThickness = requirePositive$6(options.leverThickness ?? 5, "leverThickness");
|
|
50712
|
+
const pinDiameter = requirePositive$6(options.pinDiameter ?? 5, "pinDiameter");
|
|
50713
|
+
const pinClearance = requireNonNegative(options.pinClearance ?? 0.25, "pinClearance");
|
|
50714
|
+
const boreDiameter = pinDiameter + pinClearance;
|
|
50715
|
+
const hubRadius = requirePositive$6(options.hubRadius ?? Math.max(armWidth * 0.85, pinDiameter * 1.8), "hubRadius");
|
|
50716
|
+
const supportThickness = requirePositive$6(options.supportThickness ?? Math.max(6, pinDiameter * 1.4), "supportThickness");
|
|
50717
|
+
const supportWidth = requirePositive$6(options.supportWidth ?? hubRadius * 2 + 18, "supportWidth");
|
|
50718
|
+
const supportDepth = requirePositive$6(options.supportDepth ?? Math.max(armWidth + 18, hubRadius * 2 + 10), "supportDepth");
|
|
50719
|
+
const washerSize = options.washerSize ?? metricWasherSizeForPin(pinDiameter);
|
|
50720
|
+
const washerDims = WASHER_TABLE[washerSize];
|
|
50721
|
+
if (!washerDims) throw new Error(`pinnedLeverAssembly: unsupported washerSize "${washerSize}"`);
|
|
50722
|
+
if (washerDims.id <= pinDiameter) {
|
|
50723
|
+
throw new Error(`pinnedLeverAssembly: ${washerSize} washer inner diameter is too small for a ${pinDiameter} mm pin`);
|
|
50724
|
+
}
|
|
50725
|
+
if (hubRadius <= boreDiameter / 2 + Math.max(1, pinDiameter * 0.25)) {
|
|
50726
|
+
throw new Error("pinnedLeverAssembly: hubRadius leaves too little material around the pivot bore");
|
|
50727
|
+
}
|
|
50728
|
+
if (supportWidth <= boreDiameter + 4 || supportDepth <= boreDiameter + 4) {
|
|
50729
|
+
throw new Error("pinnedLeverAssembly: support dimensions leave too little material around the pivot bore");
|
|
50730
|
+
}
|
|
50731
|
+
const segments = options.segments ?? 40;
|
|
50732
|
+
const gripLength = requirePositive$6(options.gripLength ?? Math.min(armLength * 0.32, Math.max(16, armWidth * 2.4)), "gripLength");
|
|
50733
|
+
const gripWidth = requirePositive$6(options.gripWidth ?? armWidth * 1.55, "gripWidth");
|
|
50734
|
+
if (gripLength >= armLength) throw new Error("pinnedLeverAssembly: gripLength must be shorter than armLength");
|
|
50735
|
+
const armOverlap = Math.min(hubRadius * 0.65, armLength * 0.25);
|
|
50736
|
+
const armStartX = hubRadius - armOverlap;
|
|
50737
|
+
const armCenterX = armStartX + armLength / 2;
|
|
50738
|
+
const gripCenterX = armStartX + armLength - gripLength / 2;
|
|
50739
|
+
const runningClearance = 0.03;
|
|
50740
|
+
const lowerWasherZ = supportThickness + runningClearance;
|
|
50741
|
+
const leverZ = lowerWasherZ + washerDims.t + runningClearance;
|
|
50742
|
+
const upperWasherZ = leverZ + leverThickness + runningClearance;
|
|
50743
|
+
const stackHeight = upperWasherZ + washerDims.t;
|
|
50744
|
+
const pinHeadThickness = Math.max(washerDims.t, pinDiameter * 0.35);
|
|
50745
|
+
const pinHeadRadius = Math.max(washerDims.od * 0.42, pinDiameter * 0.8);
|
|
50746
|
+
const supportBore = cylinder(supportThickness + 1, boreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
|
|
50747
|
+
let supportBlank = box(supportWidth, supportDepth, supportThickness);
|
|
50748
|
+
if (options.stopBlock ?? true) {
|
|
50749
|
+
const stopLength = Math.min(armLength * 0.22, Math.max(10, armWidth * 1.4));
|
|
50750
|
+
const stopWidth = Math.max(4, pinDiameter * 0.7);
|
|
50751
|
+
const stopHeight = supportThickness;
|
|
50752
|
+
const stopX = hubRadius + stopLength / 2;
|
|
50753
|
+
const stopY = armWidth / 2 + stopWidth / 2 + runningClearance;
|
|
50754
|
+
supportBlank = union(supportBlank, box(stopLength, stopWidth, stopHeight).translate(stopX, stopY, 0));
|
|
50755
|
+
}
|
|
50756
|
+
const support = supportBlank.subtract(supportBore).color("#475569");
|
|
50757
|
+
const hub = cylinder(leverThickness, hubRadius, void 0, segments);
|
|
50758
|
+
const arm = box(armLength, armWidth, leverThickness).translate(armCenterX, 0, 0);
|
|
50759
|
+
const grip = box(gripLength, gripWidth, leverThickness).translate(gripCenterX, 0, 0);
|
|
50760
|
+
const leverSolids = [hub, arm, grip];
|
|
50761
|
+
if (options.detentBoss ?? true) {
|
|
50762
|
+
const bossRadius = Math.min(armWidth * 0.42, hubRadius * 0.42);
|
|
50763
|
+
const bossX = hubRadius + Math.min(armLength * 0.22, armWidth * 2);
|
|
50764
|
+
const bossY = -armWidth / 2 - bossRadius * 0.45;
|
|
50765
|
+
leverSolids.push(cylinder(leverThickness, bossRadius, void 0, segments).translate(bossX, bossY, 0));
|
|
50766
|
+
}
|
|
50767
|
+
const leverBore = cylinder(leverThickness + 1, boreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
|
|
50768
|
+
const lever = union(...leverSolids).subtract(leverBore).translate(0, 0, leverZ).color("#7f1d1d");
|
|
50769
|
+
const lowerWasher = washer(washerSize, { segments }).translate(0, 0, lowerWasherZ).color("#94a3b8");
|
|
50770
|
+
const upperWasher = washer(washerSize, { segments }).translate(0, 0, upperWasherZ).color("#94a3b8");
|
|
50771
|
+
const shaft = cylinder(stackHeight, pinDiameter / 2, void 0, segments);
|
|
50772
|
+
const lowerRetainer = cylinder(pinHeadThickness, pinHeadRadius, void 0, segments).translate(0, 0, -pinHeadThickness - runningClearance);
|
|
50773
|
+
const upperHead = cylinder(pinHeadThickness, pinHeadRadius, void 0, segments).translate(0, 0, stackHeight + runningClearance);
|
|
50774
|
+
const pin = union(shaft, lowerRetainer, upperHead).color("#cbd5e1");
|
|
50775
|
+
const pivotBore = cylinder(stackHeight + 1, boreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
|
|
50776
|
+
const parts = [
|
|
50777
|
+
{ name: "pivot support block with bearing bore and low stop land", shape: support },
|
|
50778
|
+
{ name: "lower thrust washer under pinned lever", shape: lowerWasher },
|
|
50779
|
+
{ name: "fused pinned lever with hub arm grip and detent boss", shape: lever },
|
|
50780
|
+
{ name: "upper thrust washer over pinned lever", shape: upperWasher },
|
|
50781
|
+
{ name: "retained pivot pin through lever stack", shape: pin }
|
|
50782
|
+
];
|
|
50783
|
+
return {
|
|
50784
|
+
parts,
|
|
50785
|
+
support,
|
|
50786
|
+
lever,
|
|
50787
|
+
pin,
|
|
50788
|
+
washers: {
|
|
50789
|
+
lower: lowerWasher,
|
|
50790
|
+
upper: upperWasher
|
|
50791
|
+
},
|
|
50792
|
+
cutters: {
|
|
50793
|
+
pivotBore
|
|
50794
|
+
},
|
|
50795
|
+
dims: {
|
|
50796
|
+
armLength,
|
|
50797
|
+
armWidth,
|
|
50798
|
+
leverThickness,
|
|
50799
|
+
hubRadius,
|
|
50800
|
+
pinDiameter,
|
|
50801
|
+
boreDiameter,
|
|
50802
|
+
supportWidth,
|
|
50803
|
+
supportDepth,
|
|
50804
|
+
supportThickness,
|
|
50805
|
+
washerSize,
|
|
50806
|
+
washerThickness: washerDims.t,
|
|
50807
|
+
stackHeight
|
|
50808
|
+
}
|
|
50809
|
+
};
|
|
50810
|
+
}
|
|
50811
|
+
function retainedShaftAssembly(options) {
|
|
50812
|
+
const supportSpacing = requirePositive$6(options.supportSpacing, "supportSpacing");
|
|
50813
|
+
const shaftDiameter = requirePositive$6(options.shaftDiameter ?? 8, "shaftDiameter");
|
|
50814
|
+
const boreClearance = requireNonNegative(options.boreClearance ?? 0.35, "boreClearance");
|
|
50815
|
+
const boreDiameter = shaftDiameter + boreClearance;
|
|
50816
|
+
const supportThickness = requirePositive$6(options.supportThickness ?? Math.max(5, shaftDiameter * 0.75), "supportThickness");
|
|
50817
|
+
const washerSize = options.washerSize ?? metricWasherSizeForPin(shaftDiameter);
|
|
50818
|
+
const washerDims = WASHER_TABLE[washerSize];
|
|
50819
|
+
if (!washerDims) throw new Error(`retainedShaftAssembly: unsupported washerSize "${washerSize}"`);
|
|
50820
|
+
if (washerDims.id <= shaftDiameter) {
|
|
50821
|
+
throw new Error(`retainedShaftAssembly: ${washerSize} washer inner diameter is too small for a ${shaftDiameter} mm shaft`);
|
|
50822
|
+
}
|
|
50823
|
+
const knobDiameter = requirePositive$6(options.knobDiameter ?? shaftDiameter * 3, "knobDiameter");
|
|
50824
|
+
const knobThickness = requirePositive$6(options.knobThickness ?? Math.max(8, shaftDiameter), "knobThickness");
|
|
50825
|
+
const retainerThickness = requirePositive$6(
|
|
50826
|
+
options.retainerThickness ?? Math.max(washerDims.t, shaftDiameter * 0.35),
|
|
50827
|
+
"retainerThickness"
|
|
50828
|
+
);
|
|
50829
|
+
const runningClearance = requireNonNegative(options.runningClearance ?? 0.05, "runningClearance");
|
|
50830
|
+
const supportWidth = requirePositive$6(options.supportWidth ?? Math.max(28, knobDiameter * 1.25), "supportWidth");
|
|
50831
|
+
const supportHeight = requirePositive$6(options.supportHeight ?? Math.max(34, knobDiameter * 1.45), "supportHeight");
|
|
50832
|
+
const segments = options.segments ?? 40;
|
|
50833
|
+
if (supportSpacing <= supportThickness) {
|
|
50834
|
+
throw new Error("retainedShaftAssembly: supportSpacing must leave a gap between support cheeks");
|
|
50835
|
+
}
|
|
50836
|
+
if (supportWidth <= boreDiameter + 4 || supportHeight <= boreDiameter + 4) {
|
|
50837
|
+
throw new Error("retainedShaftAssembly: support dimensions leave too little material around the shaft bore");
|
|
50838
|
+
}
|
|
50839
|
+
const leftSupportX = -supportSpacing / 2;
|
|
50840
|
+
const rightSupportX = supportSpacing / 2;
|
|
50841
|
+
const leftOuterFaceX = leftSupportX - supportThickness / 2;
|
|
50842
|
+
const rightOuterFaceX = rightSupportX + supportThickness / 2;
|
|
50843
|
+
const leftWasherX = leftOuterFaceX - runningClearance - washerDims.t / 2;
|
|
50844
|
+
const rightWasherX = rightOuterFaceX + runningClearance + washerDims.t / 2;
|
|
50845
|
+
const leftKnobX = leftOuterFaceX - runningClearance * 2 - washerDims.t - knobThickness / 2;
|
|
50846
|
+
const rightKnobX = rightOuterFaceX + runningClearance * 2 + washerDims.t + knobThickness / 2;
|
|
50847
|
+
const leftStackOuterX = leftKnobX - knobThickness / 2;
|
|
50848
|
+
const rightStackOuterX = rightKnobX + knobThickness / 2;
|
|
50849
|
+
const minimumShaftLength = rightStackOuterX - leftStackOuterX + retainerThickness * 2 + runningClearance * 2;
|
|
50850
|
+
const shaftLength = requirePositive$6(options.shaftLength ?? minimumShaftLength, "shaftLength");
|
|
50851
|
+
if (shaftLength < minimumShaftLength) {
|
|
50852
|
+
throw new Error("retainedShaftAssembly: shaftLength is too short to retain both supports, washers, and knobs");
|
|
50853
|
+
}
|
|
50854
|
+
const supportBore = cylinderAlongX(supportThickness + 1, boreDiameter / 2, 0, segments);
|
|
50855
|
+
const makeSupport = (x2) => box(supportThickness, supportWidth, supportHeight).translate(x2, 0, -supportHeight / 2).subtract(supportBore.translate(x2, 0, 0)).color("#334155");
|
|
50856
|
+
const knobBore = cylinder(knobThickness + 1, boreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
|
|
50857
|
+
const makeKnob = (x2) => cylinder(knobThickness, knobDiameter / 2, void 0, 18).subtract(knobBore).pointAlong([1, 0, 0]).translate(x2 - knobThickness / 2, 0, 0).color("#111827");
|
|
50858
|
+
const retainerRadius = Math.max(shaftDiameter * 0.85, knobDiameter * 0.36);
|
|
50859
|
+
const shaftCore = cylinderAlongX(shaftLength, shaftDiameter / 2, 0, segments);
|
|
50860
|
+
const leftRetainer = cylinderAlongX(retainerThickness, retainerRadius, -shaftLength / 2 + retainerThickness / 2, segments);
|
|
50861
|
+
const rightRetainer = cylinderAlongX(retainerThickness, retainerRadius, shaftLength / 2 - retainerThickness / 2, segments);
|
|
50862
|
+
const shaft = union(shaftCore, leftRetainer, rightRetainer).color("#cbd5e1");
|
|
50863
|
+
const leftSupport = makeSupport(leftSupportX);
|
|
50864
|
+
const rightSupport = makeSupport(rightSupportX);
|
|
50865
|
+
const leftWasher = washerAlongX(washerSize, leftWasherX, segments).color("#94a3b8");
|
|
50866
|
+
const rightWasher = washerAlongX(washerSize, rightWasherX, segments).color("#94a3b8");
|
|
50867
|
+
const leftKnob = makeKnob(leftKnobX);
|
|
50868
|
+
const rightKnob = makeKnob(rightKnobX);
|
|
50869
|
+
const shaftBore = cylinderAlongX(supportThickness + knobThickness + 2, boreDiameter / 2, 0, segments);
|
|
50870
|
+
const parts = [
|
|
50871
|
+
{ name: "left bored support cheek for retained shaft", shape: leftSupport },
|
|
50872
|
+
{ name: "right bored support cheek for retained shaft", shape: rightSupport },
|
|
50873
|
+
{ name: "retained through shaft with end heads", shape: shaft },
|
|
50874
|
+
{ name: `left ${washerSize} thrust washer on shaft`, shape: leftWasher },
|
|
50875
|
+
{ name: `right ${washerSize} thrust washer on shaft`, shape: rightWasher },
|
|
50876
|
+
{ name: "left retained hand knob with shaft bore", shape: leftKnob },
|
|
50877
|
+
{ name: "right retained hand knob with shaft bore", shape: rightKnob }
|
|
50878
|
+
];
|
|
50879
|
+
return {
|
|
50880
|
+
parts,
|
|
50881
|
+
supports: {
|
|
50882
|
+
left: leftSupport,
|
|
50883
|
+
right: rightSupport
|
|
50884
|
+
},
|
|
50885
|
+
shaft,
|
|
50886
|
+
washers: {
|
|
50887
|
+
left: leftWasher,
|
|
50888
|
+
right: rightWasher
|
|
50889
|
+
},
|
|
50890
|
+
knobs: {
|
|
50891
|
+
left: leftKnob,
|
|
50892
|
+
right: rightKnob
|
|
50893
|
+
},
|
|
50894
|
+
cutters: {
|
|
50895
|
+
shaftBore
|
|
50896
|
+
},
|
|
50897
|
+
dims: {
|
|
50898
|
+
supportSpacing,
|
|
50899
|
+
supportThickness,
|
|
50900
|
+
supportWidth,
|
|
50901
|
+
supportHeight,
|
|
50902
|
+
shaftDiameter,
|
|
50903
|
+
shaftLength,
|
|
50904
|
+
boreDiameter,
|
|
50905
|
+
washerSize,
|
|
50906
|
+
washerThickness: washerDims.t,
|
|
50907
|
+
knobDiameter,
|
|
50908
|
+
knobThickness,
|
|
50909
|
+
retainerThickness,
|
|
50910
|
+
runningClearance
|
|
50911
|
+
}
|
|
50912
|
+
};
|
|
50913
|
+
}
|
|
50914
|
+
function capturedLinearSlide(options) {
|
|
50915
|
+
const length4 = requirePositive$6(options.length, "length");
|
|
50916
|
+
const railWidth = requirePositive$6(options.railWidth ?? 38, "railWidth");
|
|
50917
|
+
const baseThickness = requirePositive$6(options.baseThickness ?? 2.4, "baseThickness");
|
|
50918
|
+
const wallThickness = requirePositive$6(options.wallThickness ?? 2, "wallThickness");
|
|
50919
|
+
const wallHeight = requirePositive$6(options.wallHeight ?? 9, "wallHeight");
|
|
50920
|
+
const lipWidth = requirePositive$6(options.lipWidth ?? 4, "lipWidth");
|
|
50921
|
+
const lipThickness = requirePositive$6(options.lipThickness ?? 1.8, "lipThickness");
|
|
50922
|
+
const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
|
|
50923
|
+
const endStopLength = requirePositive$6(options.endStopLength ?? 6, "endStopLength");
|
|
50924
|
+
const carriageLength = requirePositive$6(options.carriageLength ?? length4 * 0.32, "carriageLength");
|
|
50925
|
+
const innerWidth = railWidth - wallThickness * 2;
|
|
50926
|
+
const throatWidth = innerWidth - lipWidth * 2;
|
|
50927
|
+
if (innerWidth <= 0) throw new Error("capturedLinearSlide: wallThickness leaves no inner rail width");
|
|
50928
|
+
if (throatWidth <= 0) throw new Error("capturedLinearSlide: lipWidth closes the rail throat");
|
|
50929
|
+
const carriageWidth = requirePositive$6(options.carriageWidth ?? innerWidth - runningClearance * 2, "carriageWidth");
|
|
50930
|
+
const carriageThickness = requirePositive$6(options.carriageThickness ?? 4, "carriageThickness");
|
|
50931
|
+
if (carriageWidth >= innerWidth - runningClearance) {
|
|
50932
|
+
throw new Error("capturedLinearSlide: carriageWidth leaves too little side clearance inside the rail");
|
|
50933
|
+
}
|
|
50934
|
+
if (carriageWidth <= throatWidth + runningClearance) {
|
|
50935
|
+
throw new Error("capturedLinearSlide: carriageWidth must be wider than the lip throat so the rail actually captures it");
|
|
50936
|
+
}
|
|
50937
|
+
if (carriageThickness + runningClearance * 2 >= wallHeight) {
|
|
50938
|
+
throw new Error("capturedLinearSlide: carriage is too tall to clear the return lips");
|
|
50939
|
+
}
|
|
50940
|
+
const maxTravel = length4 - endStopLength * 2 - carriageLength;
|
|
50941
|
+
if (maxTravel <= 0) {
|
|
50942
|
+
throw new Error("capturedLinearSlide: rail length, end stops, and carriage length leave no travel");
|
|
50943
|
+
}
|
|
50944
|
+
const travel = options.travel ?? maxTravel / 2;
|
|
50945
|
+
if (!Number.isFinite(travel) || travel < 0 || travel > maxTravel) {
|
|
50946
|
+
throw new Error(`capturedLinearSlide: travel must be between 0 and ${maxTravel}`);
|
|
50947
|
+
}
|
|
50948
|
+
const carriageCenterX = -maxTravel / 2 + travel;
|
|
50949
|
+
const fuseOverlap = Math.min(0.04, runningClearance * 0.1);
|
|
50950
|
+
const sideY = railWidth / 2 - wallThickness / 2;
|
|
50951
|
+
const lipY = railWidth / 2 - wallThickness - lipWidth / 2 + fuseOverlap / 2;
|
|
50952
|
+
const stopZ = baseThickness - fuseOverlap;
|
|
50953
|
+
const rail2 = union(
|
|
50954
|
+
box(length4, railWidth, baseThickness),
|
|
50955
|
+
box(length4, wallThickness, wallHeight + fuseOverlap).translate(0, sideY, baseThickness - fuseOverlap),
|
|
50956
|
+
box(length4, wallThickness, wallHeight + fuseOverlap).translate(0, -sideY, baseThickness - fuseOverlap),
|
|
50957
|
+
box(length4, lipWidth, lipThickness + fuseOverlap).translate(0, lipY, baseThickness + wallHeight - fuseOverlap),
|
|
50958
|
+
box(length4, lipWidth, lipThickness + fuseOverlap).translate(0, -lipY, baseThickness + wallHeight - fuseOverlap),
|
|
50959
|
+
box(endStopLength, throatWidth, carriageThickness + fuseOverlap).translate(-length4 / 2 + endStopLength / 2, 0, stopZ),
|
|
50960
|
+
box(endStopLength, throatWidth, carriageThickness + fuseOverlap).translate(length4 / 2 - endStopLength / 2, 0, stopZ)
|
|
50961
|
+
).color("#475569");
|
|
50962
|
+
const carriage = union(
|
|
50963
|
+
box(carriageLength, carriageWidth, carriageThickness),
|
|
50964
|
+
box(carriageLength * 0.78, throatWidth - runningClearance * 2, Math.max(1, carriageThickness * 0.38)).translate(
|
|
50965
|
+
0,
|
|
50966
|
+
0,
|
|
50967
|
+
carriageThickness
|
|
50968
|
+
)
|
|
50969
|
+
).translate(carriageCenterX, 0, baseThickness + runningClearance).color("#111827");
|
|
50970
|
+
const parts = [
|
|
50971
|
+
{ name: "captured linear rail with return lips and end stops", shape: rail2 },
|
|
50972
|
+
{ name: "sliding carriage captured under rail lips", shape: carriage }
|
|
50973
|
+
];
|
|
50974
|
+
return {
|
|
50975
|
+
parts,
|
|
50976
|
+
rail: rail2,
|
|
50977
|
+
carriage,
|
|
50978
|
+
dims: {
|
|
50979
|
+
length: length4,
|
|
50980
|
+
railWidth,
|
|
50981
|
+
innerWidth,
|
|
50982
|
+
throatWidth,
|
|
50983
|
+
baseThickness,
|
|
50984
|
+
wallThickness,
|
|
50985
|
+
wallHeight,
|
|
50986
|
+
lipWidth,
|
|
50987
|
+
lipThickness,
|
|
50988
|
+
carriageLength,
|
|
50989
|
+
carriageWidth,
|
|
50990
|
+
carriageThickness,
|
|
50991
|
+
endStopLength,
|
|
50992
|
+
runningClearance,
|
|
50993
|
+
maxTravel,
|
|
50994
|
+
travel,
|
|
50995
|
+
carriageCenterX
|
|
50996
|
+
}
|
|
50997
|
+
};
|
|
50998
|
+
}
|
|
50999
|
+
function capturedCartridgeGuideAssembly(options) {
|
|
51000
|
+
const length4 = requirePositive$6(options.length, "length");
|
|
51001
|
+
const guideWidth = requirePositive$6(options.guideWidth ?? 42, "guideWidth");
|
|
51002
|
+
const baseThickness = requirePositive$6(options.baseThickness ?? 3, "baseThickness");
|
|
51003
|
+
const wallThickness = requirePositive$6(options.wallThickness ?? 2.5, "wallThickness");
|
|
51004
|
+
const wallHeight = requirePositive$6(options.wallHeight ?? 12, "wallHeight");
|
|
51005
|
+
const lipWidth = requirePositive$6(options.lipWidth ?? 4, "lipWidth");
|
|
51006
|
+
const lipThickness = requirePositive$6(options.lipThickness ?? 2, "lipThickness");
|
|
51007
|
+
const rearStopLength = requirePositive$6(options.rearStopLength ?? 7, "rearStopLength");
|
|
51008
|
+
const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
|
|
51009
|
+
const cartridgeLength = requirePositive$6(options.cartridgeLength ?? length4 * 0.58, "cartridgeLength");
|
|
51010
|
+
const cartridgeHeight = requirePositive$6(options.cartridgeHeight ?? 10, "cartridgeHeight");
|
|
51011
|
+
const flangeThickness = requirePositive$6(options.flangeThickness ?? 3, "flangeThickness");
|
|
51012
|
+
const pullTabLength = requirePositive$6(options.pullTabLength ?? 10, "pullTabLength");
|
|
51013
|
+
const innerWidth = guideWidth - wallThickness * 2;
|
|
51014
|
+
const throatWidth = innerWidth - lipWidth * 2;
|
|
51015
|
+
if (innerWidth <= 0) throw new Error("capturedCartridgeGuideAssembly: wallThickness leaves no inner guide width");
|
|
51016
|
+
if (throatWidth <= 0) throw new Error("capturedCartridgeGuideAssembly: lipWidth closes the guide throat");
|
|
51017
|
+
if (wallHeight <= lipThickness + flangeThickness + runningClearance * 2) {
|
|
51018
|
+
throw new Error("capturedCartridgeGuideAssembly: wallHeight leaves too little vertical capture clearance");
|
|
51019
|
+
}
|
|
51020
|
+
const cartridgeWidth = requirePositive$6(options.cartridgeWidth ?? innerWidth - runningClearance * 2, "cartridgeWidth");
|
|
51021
|
+
const cartridgeBodyWidth = throatWidth - runningClearance * 2;
|
|
51022
|
+
if (cartridgeBodyWidth <= 0) {
|
|
51023
|
+
throw new Error("capturedCartridgeGuideAssembly: throatWidth and runningClearance leave no cartridge body width");
|
|
51024
|
+
}
|
|
51025
|
+
if (cartridgeWidth >= innerWidth - runningClearance) {
|
|
51026
|
+
throw new Error("capturedCartridgeGuideAssembly: cartridgeWidth leaves too little side clearance inside the guide");
|
|
51027
|
+
}
|
|
51028
|
+
if (cartridgeWidth <= throatWidth + runningClearance) {
|
|
51029
|
+
throw new Error("capturedCartridgeGuideAssembly: cartridge flange must be wider than the guide throat so the cartridge is captured");
|
|
51030
|
+
}
|
|
51031
|
+
const maxInsertion = length4 - rearStopLength - cartridgeLength;
|
|
51032
|
+
if (maxInsertion <= 0) {
|
|
51033
|
+
throw new Error("capturedCartridgeGuideAssembly: length, rearStopLength, and cartridgeLength leave no insertion travel");
|
|
51034
|
+
}
|
|
51035
|
+
const insertion = options.insertion ?? maxInsertion * 0.4;
|
|
51036
|
+
if (!Number.isFinite(insertion) || insertion < 0 || insertion > maxInsertion) {
|
|
51037
|
+
throw new Error(`capturedCartridgeGuideAssembly: insertion must be between 0 and ${maxInsertion}`);
|
|
51038
|
+
}
|
|
51039
|
+
const cartridgeCenterX = -length4 / 2 + cartridgeLength / 2 + insertion;
|
|
51040
|
+
const fuseOverlap = Math.min(0.04, runningClearance * 0.1);
|
|
51041
|
+
const sideY = guideWidth / 2 - wallThickness / 2;
|
|
51042
|
+
const lipY = guideWidth / 2 - wallThickness - lipWidth / 2 + fuseOverlap / 2;
|
|
51043
|
+
const guide = union(
|
|
51044
|
+
box(length4, guideWidth, baseThickness),
|
|
51045
|
+
box(length4, wallThickness, wallHeight + fuseOverlap).translate(0, sideY, baseThickness - fuseOverlap),
|
|
51046
|
+
box(length4, wallThickness, wallHeight + fuseOverlap).translate(0, -sideY, baseThickness - fuseOverlap),
|
|
51047
|
+
box(length4, lipWidth, lipThickness + fuseOverlap).translate(0, lipY, baseThickness + wallHeight - fuseOverlap),
|
|
51048
|
+
box(length4, lipWidth, lipThickness + fuseOverlap).translate(0, -lipY, baseThickness + wallHeight - fuseOverlap),
|
|
51049
|
+
box(rearStopLength, throatWidth, Math.max(flangeThickness + runningClearance, 4)).translate(
|
|
51050
|
+
length4 / 2 - rearStopLength / 2,
|
|
51051
|
+
0,
|
|
51052
|
+
baseThickness - fuseOverlap
|
|
51053
|
+
)
|
|
51054
|
+
).color("#475569");
|
|
51055
|
+
const flangeZ = baseThickness + runningClearance;
|
|
51056
|
+
const bodyHeight = Math.max(1, cartridgeHeight - flangeThickness);
|
|
51057
|
+
const bodyZ = flangeZ + flangeThickness;
|
|
51058
|
+
const tabOverlap = Math.min(0.6, pullTabLength * 0.15);
|
|
51059
|
+
const pullTabX = cartridgeCenterX - cartridgeLength / 2 - pullTabLength / 2 + tabOverlap;
|
|
51060
|
+
const pullTabWidth = Math.max(cartridgeBodyWidth * 0.55, 12);
|
|
51061
|
+
const cartridge = union(
|
|
51062
|
+
box(cartridgeLength, cartridgeWidth, flangeThickness).translate(cartridgeCenterX, 0, flangeZ),
|
|
51063
|
+
box(cartridgeLength * 0.88, cartridgeBodyWidth, bodyHeight).translate(cartridgeCenterX, 0, bodyZ),
|
|
51064
|
+
box(pullTabLength, pullTabWidth, Math.max(flangeThickness, 3)).translate(pullTabX, 0, flangeZ)
|
|
51065
|
+
).color("#111827");
|
|
51066
|
+
const parts = [
|
|
51067
|
+
{ name: "captured cartridge guide with return lips and rear stop", shape: guide },
|
|
51068
|
+
{ name: "removable cartridge with captured flange and pull tab", shape: cartridge }
|
|
51069
|
+
];
|
|
51070
|
+
return {
|
|
51071
|
+
parts,
|
|
51072
|
+
guide,
|
|
51073
|
+
cartridge,
|
|
51074
|
+
dims: {
|
|
51075
|
+
length: length4,
|
|
51076
|
+
guideWidth,
|
|
51077
|
+
innerWidth,
|
|
51078
|
+
throatWidth,
|
|
51079
|
+
baseThickness,
|
|
51080
|
+
wallThickness,
|
|
51081
|
+
wallHeight,
|
|
51082
|
+
lipWidth,
|
|
51083
|
+
lipThickness,
|
|
51084
|
+
rearStopLength,
|
|
51085
|
+
cartridgeLength,
|
|
51086
|
+
cartridgeWidth,
|
|
51087
|
+
cartridgeBodyWidth,
|
|
51088
|
+
cartridgeHeight,
|
|
51089
|
+
flangeThickness,
|
|
51090
|
+
pullTabLength,
|
|
51091
|
+
runningClearance,
|
|
51092
|
+
maxInsertion,
|
|
51093
|
+
insertion,
|
|
51094
|
+
cartridgeCenterX
|
|
51095
|
+
}
|
|
51096
|
+
};
|
|
51097
|
+
}
|
|
51098
|
+
function livingHingeCoverAssembly(options) {
|
|
51099
|
+
const width = requirePositive$6(options.width, "width");
|
|
51100
|
+
const coverDepth = requirePositive$6(options.coverDepth ?? 42, "coverDepth");
|
|
51101
|
+
const fixedLeafDepth = requirePositive$6(options.fixedLeafDepth ?? 18, "fixedLeafDepth");
|
|
51102
|
+
const leafThickness = requirePositive$6(options.leafThickness ?? 2, "leafThickness");
|
|
51103
|
+
const hingeWebWidth = requirePositive$6(options.hingeWebWidth ?? 3.2, "hingeWebWidth");
|
|
51104
|
+
const hingeWebThickness = requirePositive$6(options.hingeWebThickness ?? 0.45, "hingeWebThickness");
|
|
51105
|
+
const pullLipDepth = requirePositive$6(options.pullLipDepth ?? 5, "pullLipDepth");
|
|
51106
|
+
const snapBarbWidth = requirePositive$6(options.snapBarbWidth ?? width * 0.35, "snapBarbWidth");
|
|
51107
|
+
const snapBarbDepth = requirePositive$6(options.snapBarbDepth ?? 2.4, "snapBarbDepth");
|
|
51108
|
+
const snapBarbHeight = requirePositive$6(options.snapBarbHeight ?? 1.4, "snapBarbHeight");
|
|
51109
|
+
const catchLandDepth = requirePositive$6(options.catchLandDepth ?? 2.4, "catchLandDepth");
|
|
51110
|
+
if (hingeWebThickness >= leafThickness * 0.55) {
|
|
51111
|
+
throw new Error("livingHingeCoverAssembly: hingeWebThickness must be much thinner than the rigid leaves");
|
|
51112
|
+
}
|
|
51113
|
+
if (hingeWebWidth >= Math.min(coverDepth, fixedLeafDepth) * 0.45) {
|
|
51114
|
+
throw new Error("livingHingeCoverAssembly: hingeWebWidth is too wide for the selected leaves");
|
|
51115
|
+
}
|
|
51116
|
+
if (snapBarbWidth >= width - 2) {
|
|
51117
|
+
throw new Error("livingHingeCoverAssembly: snapBarbWidth must leave side material on the cover leaf");
|
|
51118
|
+
}
|
|
51119
|
+
const fuseOverlap = Math.min(0.04, hingeWebWidth * 0.02);
|
|
51120
|
+
const fixedCenterY = -hingeWebWidth / 2 - fixedLeafDepth / 2 + fuseOverlap / 2;
|
|
51121
|
+
const coverCenterY = hingeWebWidth / 2 + coverDepth / 2 - fuseOverlap / 2;
|
|
51122
|
+
const fixedLeaf = box(width, fixedLeafDepth + fuseOverlap, leafThickness).translate(0, fixedCenterY, 0);
|
|
51123
|
+
const movingLeaf = box(width, coverDepth + fuseOverlap, leafThickness).translate(0, coverCenterY, 0);
|
|
51124
|
+
const hingeWeb = box(width, hingeWebWidth + fuseOverlap * 2, hingeWebThickness).translate(0, 0, 0);
|
|
51125
|
+
const pullLip = box(width * 0.92, pullLipDepth, leafThickness).translate(0, coverCenterY + coverDepth / 2 + pullLipDepth / 2 - fuseOverlap, 0);
|
|
51126
|
+
const snapBarb = box(snapBarbWidth, snapBarbDepth, snapBarbHeight).translate(
|
|
51127
|
+
0,
|
|
51128
|
+
coverCenterY + coverDepth / 2 - snapBarbDepth / 2,
|
|
51129
|
+
leafThickness
|
|
51130
|
+
);
|
|
51131
|
+
const catchLand = box(width * 0.55, catchLandDepth, Math.max(0.8, leafThickness * 0.45)).translate(
|
|
51132
|
+
0,
|
|
51133
|
+
fixedCenterY - fixedLeafDepth / 2 + catchLandDepth / 2,
|
|
51134
|
+
leafThickness
|
|
51135
|
+
);
|
|
51136
|
+
const cover = union(fixedLeaf, movingLeaf, hingeWeb, pullLip, snapBarb, catchLand).color("#0f766e");
|
|
51137
|
+
const overallDepth = fixedLeafDepth + hingeWebWidth + coverDepth + pullLipDepth;
|
|
51138
|
+
const flexRatio = leafThickness / hingeWebThickness;
|
|
51139
|
+
return {
|
|
51140
|
+
parts: [{ name: "one-piece molded living hinge cover with snap barb", shape: cover }],
|
|
51141
|
+
cover,
|
|
51142
|
+
fixedLeaf,
|
|
51143
|
+
movingLeaf,
|
|
51144
|
+
hingeWeb,
|
|
51145
|
+
snapBarb,
|
|
51146
|
+
catchLand,
|
|
51147
|
+
dims: {
|
|
51148
|
+
width,
|
|
51149
|
+
coverDepth,
|
|
51150
|
+
fixedLeafDepth,
|
|
51151
|
+
leafThickness,
|
|
51152
|
+
hingeWebWidth,
|
|
51153
|
+
hingeWebThickness,
|
|
51154
|
+
pullLipDepth,
|
|
51155
|
+
snapBarbWidth,
|
|
51156
|
+
snapBarbDepth,
|
|
51157
|
+
snapBarbHeight,
|
|
51158
|
+
catchLandDepth,
|
|
51159
|
+
flexRatio,
|
|
51160
|
+
overallDepth
|
|
51161
|
+
}
|
|
51162
|
+
};
|
|
51163
|
+
}
|
|
51164
|
+
function knuckledHingeAssembly(options) {
|
|
51165
|
+
const length4 = requirePositive$6(options.length, "length");
|
|
51166
|
+
const leafLength = requirePositive$6(options.leafLength ?? 36, "leafLength");
|
|
51167
|
+
const leafThickness = requirePositive$6(options.leafThickness ?? 1.6, "leafThickness");
|
|
51168
|
+
const barrelOuterRadius = requirePositive$6(options.barrelOuterRadius ?? 3, "barrelOuterRadius");
|
|
51169
|
+
const pinDiameter = requirePositive$6(options.pinDiameter ?? 2, "pinDiameter");
|
|
51170
|
+
const pinClearance = requireNonNegative(options.pinClearance ?? 0.25, "pinClearance");
|
|
51171
|
+
const boreDiameter = pinDiameter + pinClearance;
|
|
51172
|
+
const knuckleGap = requireNonNegative(options.knuckleGap ?? 0.45, "knuckleGap");
|
|
51173
|
+
const openAngleDeg = Number.isFinite(options.openAngleDeg ?? 35) ? options.openAngleDeg ?? 35 : 35;
|
|
51174
|
+
const retainerThickness = requirePositive$6(
|
|
51175
|
+
options.retainerThickness ?? Math.max(leafThickness, pinDiameter * 0.7),
|
|
51176
|
+
"retainerThickness"
|
|
51177
|
+
);
|
|
51178
|
+
const segments = options.segments ?? 36;
|
|
51179
|
+
const knuckleCount = options.knuckleCount ?? 5;
|
|
51180
|
+
if (!Number.isInteger(knuckleCount) || knuckleCount < 3 || knuckleCount % 2 === 0) {
|
|
51181
|
+
throw new Error("knuckledHingeAssembly: knuckleCount must be an odd integer >= 3");
|
|
51182
|
+
}
|
|
51183
|
+
if (barrelOuterRadius <= boreDiameter / 2 + Math.max(0.35, pinDiameter * 0.18)) {
|
|
51184
|
+
throw new Error("knuckledHingeAssembly: barrelOuterRadius leaves too little wall around the pin bore");
|
|
51185
|
+
}
|
|
51186
|
+
const knuckleLength = (length4 - knuckleGap * (knuckleCount - 1)) / knuckleCount;
|
|
51187
|
+
if (knuckleLength <= pinDiameter * 1.4) {
|
|
51188
|
+
throw new Error("knuckledHingeAssembly: length, knuckleCount, and knuckleGap make knuckles too short");
|
|
51189
|
+
}
|
|
51190
|
+
const leafRootClearance = Math.max(0.12, Math.min(knuckleGap * 0.35, 0.35));
|
|
51191
|
+
const barrelLeafOverlap = Math.min(barrelOuterRadius * 0.18, leafThickness * 0.35);
|
|
51192
|
+
const bridgeDepth = leafRootClearance + barrelLeafOverlap + 0.2;
|
|
51193
|
+
const fixedLeafPlate = box(length4, leafLength, leafThickness).translate(
|
|
51194
|
+
0,
|
|
51195
|
+
barrelOuterRadius + leafRootClearance + leafLength / 2,
|
|
51196
|
+
-leafThickness / 2
|
|
51197
|
+
);
|
|
51198
|
+
const movingLeafPlate = box(length4, leafLength, leafThickness).translate(
|
|
51199
|
+
0,
|
|
51200
|
+
-barrelOuterRadius - leafRootClearance - leafLength / 2,
|
|
51201
|
+
-leafThickness / 2
|
|
51202
|
+
);
|
|
51203
|
+
const fixedKnuckles = [];
|
|
51204
|
+
const movingKnuckles = [];
|
|
51205
|
+
const fixedBridges = [];
|
|
51206
|
+
const movingBridges = [];
|
|
51207
|
+
for (let index2 = 0; index2 < knuckleCount; index2 += 1) {
|
|
51208
|
+
const xStart = -length4 / 2 + index2 * (knuckleLength + knuckleGap);
|
|
51209
|
+
const xCenter = xStart + knuckleLength / 2;
|
|
51210
|
+
const knuckle = tubeAlongX(knuckleLength, barrelOuterRadius, boreDiameter / 2, xCenter, segments);
|
|
51211
|
+
if (index2 % 2 === 0) {
|
|
51212
|
+
fixedKnuckles.push(knuckle);
|
|
51213
|
+
fixedBridges.push(
|
|
51214
|
+
box(knuckleLength, bridgeDepth, leafThickness).translate(
|
|
51215
|
+
xCenter,
|
|
51216
|
+
barrelOuterRadius - barrelLeafOverlap + bridgeDepth / 2,
|
|
51217
|
+
-leafThickness / 2
|
|
51218
|
+
)
|
|
51219
|
+
);
|
|
51220
|
+
} else {
|
|
51221
|
+
movingKnuckles.push(knuckle);
|
|
51222
|
+
movingBridges.push(
|
|
51223
|
+
box(knuckleLength, bridgeDepth, leafThickness).translate(
|
|
51224
|
+
xCenter,
|
|
51225
|
+
-barrelOuterRadius + barrelLeafOverlap - bridgeDepth / 2,
|
|
51226
|
+
-leafThickness / 2
|
|
51227
|
+
)
|
|
51228
|
+
);
|
|
51229
|
+
}
|
|
51230
|
+
}
|
|
51231
|
+
const fixedLeaf = union(fixedLeafPlate, ...fixedKnuckles, ...fixedBridges).color("#475569");
|
|
51232
|
+
const movingLeaf = union(movingLeafPlate, ...movingKnuckles, ...movingBridges).rotateX(openAngleDeg).color("#111827");
|
|
51233
|
+
const pinCore = cylinderAlongX(length4 + retainerThickness * 2, pinDiameter / 2, 0, segments);
|
|
51234
|
+
const retainerRadius = Math.max(barrelOuterRadius * 0.85, pinDiameter);
|
|
51235
|
+
const leftHead = cylinderAlongX(retainerThickness, retainerRadius, -length4 / 2 - retainerThickness / 2, segments);
|
|
51236
|
+
const rightHead = cylinderAlongX(retainerThickness, retainerRadius, length4 / 2 + retainerThickness / 2, segments);
|
|
51237
|
+
const pin = union(pinCore, leftHead, rightHead).color("#cbd5e1");
|
|
51238
|
+
const pinBore = cylinderAlongX(length4 + retainerThickness * 2, boreDiameter / 2, 0, segments);
|
|
51239
|
+
const parts = [
|
|
51240
|
+
{ name: "fixed hinge leaf with alternating knuckles", shape: fixedLeaf },
|
|
51241
|
+
{ name: "moving hinge leaf with alternating knuckles", shape: movingLeaf },
|
|
51242
|
+
{ name: "retained hinge pin through knuckle stack", shape: pin }
|
|
51243
|
+
];
|
|
51244
|
+
return {
|
|
51245
|
+
parts,
|
|
51246
|
+
fixedLeaf,
|
|
51247
|
+
movingLeaf,
|
|
51248
|
+
pin,
|
|
51249
|
+
cutters: {
|
|
51250
|
+
pinBore
|
|
51251
|
+
},
|
|
51252
|
+
dims: {
|
|
51253
|
+
length: length4,
|
|
51254
|
+
leafLength,
|
|
51255
|
+
leafThickness,
|
|
51256
|
+
barrelOuterRadius,
|
|
51257
|
+
pinDiameter,
|
|
51258
|
+
boreDiameter,
|
|
51259
|
+
knuckleGap,
|
|
51260
|
+
knuckleCount,
|
|
51261
|
+
knuckleLength,
|
|
51262
|
+
openAngleDeg,
|
|
51263
|
+
retainerThickness
|
|
51264
|
+
}
|
|
51265
|
+
};
|
|
51266
|
+
}
|
|
51267
|
+
function clevisPinJointAssembly(options = {}) {
|
|
51268
|
+
const pinDiameter = requirePositive$6(options.pinDiameter ?? 4, "pinDiameter");
|
|
51269
|
+
const pinClearance = requireNonNegative(options.pinClearance ?? 0.3, "pinClearance");
|
|
51270
|
+
const boreDiameter = pinDiameter + pinClearance;
|
|
51271
|
+
const linkThickness = requirePositive$6(options.linkThickness ?? Math.max(5, pinDiameter * 1.5), "linkThickness");
|
|
51272
|
+
const earThickness = requirePositive$6(options.earThickness ?? Math.max(3.5, pinDiameter), "earThickness");
|
|
51273
|
+
const runningClearance = requireNonNegative(options.runningClearance ?? 0.25, "runningClearance");
|
|
51274
|
+
const linkArmWidth = requirePositive$6(options.linkArmWidth ?? pinDiameter * 2.4, "linkArmWidth");
|
|
51275
|
+
const eyeOuterRadius = requirePositive$6(
|
|
51276
|
+
options.eyeOuterRadius ?? Math.max(pinDiameter * 1.8, linkArmWidth / 2 + 1.4),
|
|
51277
|
+
"eyeOuterRadius"
|
|
51278
|
+
);
|
|
51279
|
+
const earLength = requirePositive$6(options.earLength ?? Math.max(eyeOuterRadius * 2.55, pinDiameter * 4.2), "earLength");
|
|
51280
|
+
const earHeight = requirePositive$6(options.earHeight ?? Math.max(eyeOuterRadius * 2.25, pinDiameter * 4.4), "earHeight");
|
|
51281
|
+
const linkArmLength = requirePositive$6(options.linkArmLength ?? 34, "linkArmLength");
|
|
51282
|
+
const retainerThickness = requirePositive$6(
|
|
51283
|
+
options.retainerThickness ?? Math.max(1.2, pinDiameter * 0.35),
|
|
51284
|
+
"retainerThickness"
|
|
51285
|
+
);
|
|
51286
|
+
const segments = options.segments ?? 40;
|
|
51287
|
+
if (eyeOuterRadius <= boreDiameter / 2 + Math.max(0.8, pinDiameter * 0.25)) {
|
|
51288
|
+
throw new Error("clevisPinJointAssembly: eyeOuterRadius leaves too little material around the pin bore");
|
|
51289
|
+
}
|
|
51290
|
+
if (earHeight <= boreDiameter + Math.max(3, pinDiameter)) {
|
|
51291
|
+
throw new Error("clevisPinJointAssembly: earHeight leaves too little material around the pin bore");
|
|
51292
|
+
}
|
|
51293
|
+
if (earLength / 2 <= eyeOuterRadius + runningClearance) {
|
|
51294
|
+
throw new Error("clevisPinJointAssembly: earLength must extend behind the link eye for a rear clevis bridge");
|
|
51295
|
+
}
|
|
51296
|
+
const clevisGap = linkThickness + runningClearance * 2;
|
|
51297
|
+
const earCenterY = clevisGap / 2 + earThickness / 2;
|
|
51298
|
+
const totalStackY = clevisGap + earThickness * 2;
|
|
51299
|
+
const pinLength = totalStackY + retainerThickness * 2 + runningClearance * 2;
|
|
51300
|
+
const bridgeClearX = -eyeOuterRadius - runningClearance;
|
|
51301
|
+
const bridgeLength = Math.max(pinDiameter * 2.2, 4);
|
|
51302
|
+
const bridgeHeight = Math.min(earHeight * 0.48, Math.max(pinDiameter * 1.4, eyeOuterRadius * 0.75));
|
|
51303
|
+
const bridgeCenterX = bridgeClearX - bridgeLength / 2;
|
|
51304
|
+
const bridgeCenterZ = -earHeight / 2 + bridgeHeight / 2;
|
|
51305
|
+
const pinBore = cylinderAlongY(totalStackY + 0.8, boreDiameter / 2, 0, segments);
|
|
51306
|
+
const clevisBlank = union(
|
|
51307
|
+
box(earLength, earThickness, earHeight).translate(0, earCenterY, -earHeight / 2),
|
|
51308
|
+
box(earLength, earThickness, earHeight).translate(0, -earCenterY, -earHeight / 2),
|
|
51309
|
+
box(bridgeLength, totalStackY, bridgeHeight).translate(bridgeCenterX, 0, bridgeCenterZ)
|
|
51310
|
+
);
|
|
51311
|
+
const clevis = clevisBlank.subtract(pinBore).color("#475569");
|
|
51312
|
+
const eye = tubeAlongY(linkThickness, eyeOuterRadius, boreDiameter / 2, 0, segments);
|
|
51313
|
+
const armOverlap = Math.min(eyeOuterRadius * 0.65, linkArmLength * 0.25);
|
|
51314
|
+
const armCenterX = eyeOuterRadius - armOverlap + linkArmLength / 2;
|
|
51315
|
+
const linkArm = box(linkArmLength, linkThickness, linkArmWidth).translate(armCenterX, 0, -linkArmWidth / 2);
|
|
51316
|
+
const link = union(eye, linkArm).color("#111827");
|
|
51317
|
+
const pinCore = cylinderAlongY(pinLength, pinDiameter / 2, 0, segments);
|
|
51318
|
+
const headRadius = Math.max(pinDiameter * 0.9, boreDiameter / 2 + 0.8);
|
|
51319
|
+
const headY = totalStackY / 2 + runningClearance + retainerThickness / 2;
|
|
51320
|
+
const headA = cylinderAlongY(retainerThickness, headRadius, headY, segments);
|
|
51321
|
+
const headB = cylinderAlongY(retainerThickness, headRadius, -headY, segments);
|
|
51322
|
+
const pin = union(pinCore, headA, headB).color("#cbd5e1");
|
|
51323
|
+
const cutter = cylinderAlongY(pinLength + 1, boreDiameter / 2, 0, segments);
|
|
51324
|
+
const parts = [
|
|
51325
|
+
{ name: "bored clevis yoke with rear bridge", shape: clevis },
|
|
51326
|
+
{ name: "center link eye captured in clevis", shape: link },
|
|
51327
|
+
{ name: "retained clevis pin through link eye", shape: pin }
|
|
51328
|
+
];
|
|
51329
|
+
return {
|
|
51330
|
+
parts,
|
|
51331
|
+
clevis,
|
|
51332
|
+
link,
|
|
51333
|
+
pin,
|
|
51334
|
+
cutters: {
|
|
51335
|
+
pinBore: cutter
|
|
51336
|
+
},
|
|
51337
|
+
dims: {
|
|
51338
|
+
pinDiameter,
|
|
51339
|
+
boreDiameter,
|
|
51340
|
+
linkThickness,
|
|
51341
|
+
earThickness,
|
|
51342
|
+
runningClearance,
|
|
51343
|
+
earLength,
|
|
51344
|
+
earHeight,
|
|
51345
|
+
linkArmLength,
|
|
51346
|
+
linkArmWidth,
|
|
51347
|
+
eyeOuterRadius,
|
|
51348
|
+
retainerThickness,
|
|
51349
|
+
pinLength,
|
|
51350
|
+
clevisGap
|
|
51351
|
+
}
|
|
51352
|
+
};
|
|
51353
|
+
}
|
|
51354
|
+
function seatedBearingAssembly(options) {
|
|
51355
|
+
const bearingOuterDiameter = requirePositive$6(options.bearingOuterDiameter, "bearingOuterDiameter");
|
|
51356
|
+
const bearingInnerDiameter = requirePositive$6(options.bearingInnerDiameter, "bearingInnerDiameter");
|
|
51357
|
+
const bearingWidth = requirePositive$6(options.bearingWidth, "bearingWidth");
|
|
51358
|
+
const shaftDiameter = requirePositive$6(options.shaftDiameter ?? Math.max(1, bearingInnerDiameter - 0.4), "shaftDiameter");
|
|
51359
|
+
const pocketClearance = requireNonNegative(options.pocketClearance ?? 0.2, "pocketClearance");
|
|
51360
|
+
const shaftClearance = requireNonNegative(options.shaftClearance ?? 0.35, "shaftClearance");
|
|
51361
|
+
const runningClearance = requireNonNegative(options.runningClearance ?? 0.05, "runningClearance");
|
|
51362
|
+
const housingThickness = requirePositive$6(options.housingThickness ?? bearingWidth + 5, "housingThickness");
|
|
51363
|
+
const bossHeight = requirePositive$6(options.bossHeight ?? Math.max(2, bearingWidth * 0.45), "bossHeight");
|
|
51364
|
+
const bossOuterDiameter = requirePositive$6(
|
|
51365
|
+
options.bossOuterDiameter ?? bearingOuterDiameter + Math.max(8, bearingOuterDiameter * 0.36),
|
|
51366
|
+
"bossOuterDiameter"
|
|
51367
|
+
);
|
|
51368
|
+
const housingWidth = requirePositive$6(options.housingWidth ?? Math.max(bossOuterDiameter + 12, bearingOuterDiameter * 2.1), "housingWidth");
|
|
51369
|
+
const housingDepth = requirePositive$6(options.housingDepth ?? Math.max(bossOuterDiameter + 12, bearingOuterDiameter * 1.8), "housingDepth");
|
|
51370
|
+
const shaftOverhang = requirePositive$6(options.shaftOverhang ?? Math.max(8, bearingOuterDiameter * 0.45), "shaftOverhang");
|
|
51371
|
+
const shoulderDiameter = requirePositive$6(options.shoulderDiameter ?? Math.max(shaftDiameter * 1.65, bearingInnerDiameter + 2), "shoulderDiameter");
|
|
51372
|
+
const shoulderThickness = requirePositive$6(options.shoulderThickness ?? Math.max(1.5, shaftDiameter * 0.32), "shoulderThickness");
|
|
51373
|
+
const segments = options.segments ?? 48;
|
|
51374
|
+
if (bearingOuterDiameter <= bearingInnerDiameter + Math.max(1, bearingOuterDiameter * 0.08)) {
|
|
51375
|
+
throw new Error("seatedBearingAssembly: bearingOuterDiameter leaves too little bearing wall around the bore");
|
|
51376
|
+
}
|
|
51377
|
+
if (shaftDiameter + shaftClearance >= bearingInnerDiameter) {
|
|
51378
|
+
throw new Error("seatedBearingAssembly: shaftDiameter plus shaftClearance must fit inside the bearing bore");
|
|
51379
|
+
}
|
|
51380
|
+
if (shoulderDiameter >= bearingOuterDiameter - runningClearance * 2) {
|
|
51381
|
+
throw new Error("seatedBearingAssembly: shoulderDiameter must stay smaller than the bearing outer race");
|
|
51382
|
+
}
|
|
51383
|
+
const pocketDiameter = bearingOuterDiameter + pocketClearance;
|
|
51384
|
+
const shaftBoreDiameter = shaftDiameter + shaftClearance;
|
|
51385
|
+
const totalHousingHeight = housingThickness + bossHeight;
|
|
51386
|
+
const pocketDepth = bearingWidth + runningClearance * 2;
|
|
51387
|
+
if (pocketDepth >= totalHousingHeight - runningClearance) {
|
|
51388
|
+
throw new Error("seatedBearingAssembly: housingThickness and bossHeight must leave a shoulder below the bearing pocket");
|
|
51389
|
+
}
|
|
51390
|
+
if (bossOuterDiameter <= pocketDiameter + Math.max(2, bearingOuterDiameter * 0.12)) {
|
|
51391
|
+
throw new Error("seatedBearingAssembly: bossOuterDiameter leaves too little wall around the bearing pocket");
|
|
51392
|
+
}
|
|
51393
|
+
if (housingWidth <= pocketDiameter + 6 || housingDepth <= pocketDiameter + 6) {
|
|
51394
|
+
throw new Error("seatedBearingAssembly: housing dimensions leave too little material around the bearing pocket");
|
|
51395
|
+
}
|
|
51396
|
+
if (shoulderThickness * 2 + runningClearance * 2 >= shaftOverhang) {
|
|
51397
|
+
throw new Error("seatedBearingAssembly: shaftOverhang must leave room for retaining collars outside the housing");
|
|
51398
|
+
}
|
|
51399
|
+
const pocketBottomZ = totalHousingHeight - pocketDepth;
|
|
51400
|
+
const bearingZ = pocketBottomZ + runningClearance;
|
|
51401
|
+
const lowerShoulderZ = -runningClearance - shoulderThickness;
|
|
51402
|
+
const upperShoulderZ = totalHousingHeight + runningClearance;
|
|
51403
|
+
const shaftLength = totalHousingHeight + shaftOverhang * 2;
|
|
51404
|
+
const bossFuseOverlap = Math.min(0.08, Math.max(0.02, bossHeight * 0.03));
|
|
51405
|
+
const bearingPocket = cylinder(pocketDepth + 0.4, pocketDiameter / 2, void 0, segments).translate(0, 0, pocketBottomZ - 0.2);
|
|
51406
|
+
const shaftBore = cylinder(totalHousingHeight + 1, shaftBoreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
|
|
51407
|
+
const housingBase = box(housingWidth, housingDepth, housingThickness).subtract(bearingPocket).subtract(shaftBore);
|
|
51408
|
+
const housingBoss = cylinder(bossHeight + bossFuseOverlap, bossOuterDiameter / 2, void 0, segments).translate(
|
|
51409
|
+
0,
|
|
51410
|
+
0,
|
|
51411
|
+
housingThickness - bossFuseOverlap
|
|
51412
|
+
).subtract(bearingPocket);
|
|
51413
|
+
const housing = union(housingBase, housingBoss).color("#475569");
|
|
51414
|
+
const bearingRing = tubeAlongZ(bearingWidth, bearingOuterDiameter / 2, bearingInnerDiameter / 2, segments);
|
|
51415
|
+
const shieldInset = Math.min(bearingWidth * 0.18, 0.7);
|
|
51416
|
+
const shieldOuterRadius = bearingOuterDiameter / 2 - Math.max(0.45, (bearingOuterDiameter - bearingInnerDiameter) * 0.08);
|
|
51417
|
+
const shieldInnerRadius = bearingInnerDiameter / 2 + Math.max(0.2, (bearingOuterDiameter - bearingInnerDiameter) * 0.035);
|
|
51418
|
+
const bearingShield = shieldOuterRadius > shieldInnerRadius + 0.2 ? union(
|
|
51419
|
+
tubeAlongZ(Math.min(0.35, bearingWidth * 0.08), shieldOuterRadius, shieldInnerRadius, segments).translate(0, 0, shieldInset),
|
|
51420
|
+
tubeAlongZ(Math.min(0.35, bearingWidth * 0.08), shieldOuterRadius, shieldInnerRadius, segments).translate(
|
|
51421
|
+
0,
|
|
51422
|
+
0,
|
|
51423
|
+
bearingWidth - shieldInset - Math.min(0.35, bearingWidth * 0.08)
|
|
51424
|
+
)
|
|
51425
|
+
) : null;
|
|
51426
|
+
const bearing = (bearingShield ? union(bearingRing, bearingShield) : bearingRing).translate(0, 0, bearingZ).color("#111827");
|
|
51427
|
+
const shaftCore = cylinder(shaftLength, shaftDiameter / 2, void 0, segments).translate(0, 0, -shaftOverhang);
|
|
51428
|
+
const lowerShoulder = cylinder(shoulderThickness, shoulderDiameter / 2, void 0, segments).translate(0, 0, lowerShoulderZ);
|
|
51429
|
+
const upperShoulder = cylinder(shoulderThickness, shoulderDiameter / 2, void 0, segments).translate(0, 0, upperShoulderZ);
|
|
51430
|
+
const shaft = union(shaftCore, lowerShoulder, upperShoulder).color("#cbd5e1");
|
|
51431
|
+
const parts = [
|
|
51432
|
+
{ name: "bearing housing with counterbore pocket and shoulder", shape: housing },
|
|
51433
|
+
{ name: "purchased radial bearing seated in counterbore", shape: bearing },
|
|
51434
|
+
{ name: "shaft through bearing bore with retaining collars", shape: shaft }
|
|
51435
|
+
];
|
|
51436
|
+
return {
|
|
51437
|
+
parts,
|
|
51438
|
+
housing,
|
|
51439
|
+
bearing,
|
|
51440
|
+
shaft,
|
|
51441
|
+
cutters: {
|
|
51442
|
+
bearingPocket,
|
|
51443
|
+
shaftBore
|
|
51444
|
+
},
|
|
51445
|
+
dims: {
|
|
51446
|
+
bearingOuterDiameter,
|
|
51447
|
+
bearingInnerDiameter,
|
|
51448
|
+
bearingWidth,
|
|
51449
|
+
shaftDiameter,
|
|
51450
|
+
housingWidth,
|
|
51451
|
+
housingDepth,
|
|
51452
|
+
housingThickness,
|
|
51453
|
+
bossOuterDiameter,
|
|
51454
|
+
bossHeight,
|
|
51455
|
+
totalHousingHeight,
|
|
51456
|
+
pocketDiameter,
|
|
51457
|
+
pocketDepth,
|
|
51458
|
+
shaftBoreDiameter,
|
|
51459
|
+
runningClearance,
|
|
51460
|
+
shaftLength,
|
|
51461
|
+
shoulderDiameter,
|
|
51462
|
+
shoulderThickness
|
|
51463
|
+
}
|
|
51464
|
+
};
|
|
51465
|
+
}
|
|
51466
|
+
function cableGlandAnchorAssembly(options) {
|
|
51467
|
+
const cableDiameter = requirePositive$6(options.cableDiameter, "cableDiameter");
|
|
51468
|
+
const panelThickness = requirePositive$6(options.panelThickness ?? 3, "panelThickness");
|
|
51469
|
+
const panelWidth = requirePositive$6(options.panelWidth ?? Math.max(54, cableDiameter * 7), "panelWidth");
|
|
51470
|
+
const panelHeight = requirePositive$6(options.panelHeight ?? Math.max(38, cableDiameter * 5), "panelHeight");
|
|
51471
|
+
const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
|
|
51472
|
+
const panelHoleClearance = requirePositive$6(options.panelHoleClearance ?? 0.25, "panelHoleClearance");
|
|
51473
|
+
const cableBoreDiameter = cableDiameter + runningClearance * 2;
|
|
51474
|
+
const glandOuterDiameter = requirePositive$6(options.glandOuterDiameter ?? cableDiameter + Math.max(6, cableDiameter * 0.9), "glandOuterDiameter");
|
|
51475
|
+
const nutOuterDiameter = requirePositive$6(options.nutOuterDiameter ?? glandOuterDiameter + Math.max(6, cableDiameter * 0.8), "nutOuterDiameter");
|
|
51476
|
+
const nutThickness = requirePositive$6(options.nutThickness ?? Math.max(4, cableDiameter * 0.8), "nutThickness");
|
|
51477
|
+
const flangeDiameter = requirePositive$6(options.flangeDiameter ?? glandOuterDiameter + Math.max(5, cableDiameter * 0.7), "flangeDiameter");
|
|
51478
|
+
const flangeThickness = requirePositive$6(options.flangeThickness ?? Math.max(2, panelThickness * 0.45), "flangeThickness");
|
|
51479
|
+
const minGlandLength = panelThickness + nutThickness + flangeThickness + runningClearance * 4;
|
|
51480
|
+
const glandLength = requirePositive$6(options.glandLength ?? minGlandLength + Math.max(8, cableDiameter), "glandLength");
|
|
51481
|
+
const cableLength = requirePositive$6(options.cableLength ?? glandLength + Math.max(36, cableDiameter * 5), "cableLength");
|
|
51482
|
+
const segments = options.segments ?? 40;
|
|
51483
|
+
if (glandOuterDiameter <= cableBoreDiameter + Math.max(1.2, cableDiameter * 0.18)) {
|
|
51484
|
+
throw new Error("cableGlandAnchorAssembly: glandOuterDiameter leaves too little wall around the cable bore");
|
|
51485
|
+
}
|
|
51486
|
+
if (nutOuterDiameter <= glandOuterDiameter + Math.max(1.5, cableDiameter * 0.2)) {
|
|
51487
|
+
throw new Error("cableGlandAnchorAssembly: nutOuterDiameter must leave material around the gland body");
|
|
51488
|
+
}
|
|
51489
|
+
if (flangeDiameter <= glandOuterDiameter + Math.max(1.2, cableDiameter * 0.16)) {
|
|
51490
|
+
throw new Error("cableGlandAnchorAssembly: flangeDiameter must be larger than the gland body");
|
|
51491
|
+
}
|
|
51492
|
+
if (panelWidth <= flangeDiameter + 8 || panelHeight <= flangeDiameter + 8) {
|
|
51493
|
+
throw new Error("cableGlandAnchorAssembly: panel dimensions leave too little material around the gland hole");
|
|
51494
|
+
}
|
|
51495
|
+
if (glandLength <= minGlandLength) {
|
|
51496
|
+
throw new Error("cableGlandAnchorAssembly: glandLength must span the panel, flange, compression nut, and clearances");
|
|
51497
|
+
}
|
|
51498
|
+
if (cableLength <= glandLength + runningClearance * 2) {
|
|
51499
|
+
throw new Error("cableGlandAnchorAssembly: cableLength must extend beyond the gland body");
|
|
51500
|
+
}
|
|
51501
|
+
const panelHoleDiameter = glandOuterDiameter + panelHoleClearance * 2;
|
|
51502
|
+
const glandOuterRadius = glandOuterDiameter / 2;
|
|
51503
|
+
const cableBoreRadius = cableBoreDiameter / 2;
|
|
51504
|
+
const faceClearance = Math.min(0.05, runningClearance * 0.15);
|
|
51505
|
+
const flangePocketDepth = Math.min(Math.max(0.35, panelThickness * 0.18), panelThickness * 0.4, flangeThickness * 0.55);
|
|
51506
|
+
const panelHole = cylinderAlongX(panelThickness + 0.8, panelHoleDiameter / 2, 0, segments);
|
|
51507
|
+
const flangeSeatPocket = cylinderAlongX(
|
|
51508
|
+
flangePocketDepth + 0.2,
|
|
51509
|
+
flangeDiameter / 2 + panelHoleClearance,
|
|
51510
|
+
panelThickness / 2 - flangePocketDepth / 2,
|
|
51511
|
+
segments
|
|
51512
|
+
);
|
|
51513
|
+
const cableBore = cylinderAlongX(glandLength + 0.8, cableBoreRadius, 0, segments);
|
|
51514
|
+
const panel = box(panelThickness, panelWidth, panelHeight).translate(0, 0, -panelHeight / 2).subtract(panelHole).subtract(flangeSeatPocket).color("#475569");
|
|
51515
|
+
const glandBody = tubeAlongX(glandLength, glandOuterRadius, cableBoreRadius, 0, segments);
|
|
51516
|
+
const flangeCenterX = panelThickness / 2 - flangePocketDepth + faceClearance + flangeThickness / 2;
|
|
51517
|
+
const flange = tubeAlongX(flangeThickness, flangeDiameter / 2, cableBoreRadius, flangeCenterX, segments);
|
|
51518
|
+
const gland = union(glandBody, flange).color("#94a3b8");
|
|
51519
|
+
const nutInnerRadius = glandOuterRadius + Math.min(0.12, runningClearance * 0.4);
|
|
51520
|
+
const nutCenterX = -panelThickness / 2 - faceClearance - nutThickness / 2;
|
|
51521
|
+
const compressionNut = tubeAlongX(nutThickness, nutOuterDiameter / 2, nutInnerRadius, nutCenterX, segments).color("#cbd5e1");
|
|
51522
|
+
const cable = cylinderAlongX(cableLength, cableDiameter / 2, 0, segments).color("#111827");
|
|
51523
|
+
const parts = [
|
|
51524
|
+
{ name: "panel with gland clearance hole", shape: panel },
|
|
51525
|
+
{ name: "hollow cable gland body with panel flange", shape: gland },
|
|
51526
|
+
{ name: "compression nut around gland body", shape: compressionNut },
|
|
51527
|
+
{ name: "routed cable through gland bore", shape: cable }
|
|
51528
|
+
];
|
|
51529
|
+
return {
|
|
51530
|
+
parts,
|
|
51531
|
+
panel,
|
|
51532
|
+
gland,
|
|
51533
|
+
compressionNut,
|
|
51534
|
+
cable,
|
|
51535
|
+
cutters: {
|
|
51536
|
+
panelHole,
|
|
51537
|
+
flangeSeatPocket,
|
|
51538
|
+
cableBore
|
|
51539
|
+
},
|
|
51540
|
+
dims: {
|
|
51541
|
+
cableDiameter,
|
|
51542
|
+
cableBoreDiameter,
|
|
51543
|
+
panelThickness,
|
|
51544
|
+
panelWidth,
|
|
51545
|
+
panelHeight,
|
|
51546
|
+
glandOuterDiameter,
|
|
51547
|
+
glandLength,
|
|
51548
|
+
nutOuterDiameter,
|
|
51549
|
+
nutThickness,
|
|
51550
|
+
flangeDiameter,
|
|
51551
|
+
flangeThickness,
|
|
51552
|
+
runningClearance,
|
|
51553
|
+
faceClearance,
|
|
51554
|
+
flangePocketDepth,
|
|
51555
|
+
panelHoleDiameter,
|
|
51556
|
+
cableLength
|
|
51557
|
+
}
|
|
51558
|
+
};
|
|
51559
|
+
}
|
|
51560
|
+
function hoseBarbPortAssembly(options) {
|
|
51561
|
+
const hoseInnerDiameter = requirePositive$6(options.hoseInnerDiameter, "hoseInnerDiameter");
|
|
51562
|
+
const runningClearance = requirePositive$6(options.runningClearance ?? 0.18, "runningClearance");
|
|
51563
|
+
const faceClearance = requirePositive$6(options.faceClearance ?? 0.04, "faceClearance");
|
|
51564
|
+
const barbRootDiameter = requirePositive$6(
|
|
51565
|
+
options.barbRootDiameter ?? Math.max(1, hoseInnerDiameter - Math.max(0.25, hoseInnerDiameter * 0.06)),
|
|
51566
|
+
"barbRootDiameter"
|
|
51567
|
+
);
|
|
51568
|
+
const barbPeakDiameter = requirePositive$6(
|
|
51569
|
+
options.barbPeakDiameter ?? hoseInnerDiameter + Math.max(0.65, hoseInnerDiameter * 0.12),
|
|
51570
|
+
"barbPeakDiameter"
|
|
51571
|
+
);
|
|
51572
|
+
const installedHoseBoreDiameter = barbPeakDiameter + runningClearance * 2;
|
|
51573
|
+
const hoseOuterDiameter = requirePositive$6(
|
|
51574
|
+
options.hoseOuterDiameter ?? Math.max(installedHoseBoreDiameter + 2.4, hoseInnerDiameter + Math.max(3, hoseInnerDiameter * 0.55)),
|
|
51575
|
+
"hoseOuterDiameter"
|
|
51576
|
+
);
|
|
51577
|
+
const fluidBoreDiameter = requirePositive$6(options.fluidBoreDiameter ?? hoseInnerDiameter * 0.65, "fluidBoreDiameter");
|
|
51578
|
+
const blockThickness = requirePositive$6(options.blockThickness ?? Math.max(7, hoseInnerDiameter * 1.2), "blockThickness");
|
|
51579
|
+
const barbCount = options.barbCount ?? 3;
|
|
51580
|
+
const barbLength = requirePositive$6(options.barbLength ?? Math.max(2.6, hoseInnerDiameter * 0.55), "barbLength");
|
|
51581
|
+
const barbStackLength = barbCount * barbLength;
|
|
51582
|
+
const shoulderDiameter = requirePositive$6(
|
|
51583
|
+
options.shoulderDiameter ?? barbPeakDiameter + Math.max(4, hoseInnerDiameter * 0.65),
|
|
51584
|
+
"shoulderDiameter"
|
|
51585
|
+
);
|
|
51586
|
+
const shoulderThickness = requirePositive$6(options.shoulderThickness ?? Math.max(2, hoseInnerDiameter * 0.35), "shoulderThickness");
|
|
51587
|
+
const bossDiameter = requirePositive$6(options.bossDiameter ?? shoulderDiameter + Math.max(4, hoseInnerDiameter * 0.6), "bossDiameter");
|
|
51588
|
+
const bossHeight = requirePositive$6(options.bossHeight ?? Math.max(2.4, hoseInnerDiameter * 0.45), "bossHeight");
|
|
51589
|
+
const blockWidth = requirePositive$6(options.blockWidth ?? bossDiameter + Math.max(14, hoseInnerDiameter * 2.4), "blockWidth");
|
|
51590
|
+
const blockHeight = requirePositive$6(options.blockHeight ?? bossDiameter + Math.max(12, hoseInnerDiameter * 2.1), "blockHeight");
|
|
51591
|
+
const hoseLength = requirePositive$6(options.hoseLength ?? barbStackLength + Math.max(32, hoseInnerDiameter * 5), "hoseLength");
|
|
51592
|
+
const clampWidth = requirePositive$6(options.clampWidth ?? Math.max(4, hoseOuterDiameter * 0.45), "clampWidth");
|
|
51593
|
+
const clampThickness = requirePositive$6(options.clampThickness ?? 0.9, "clampThickness");
|
|
51594
|
+
const segments = options.segments ?? 40;
|
|
51595
|
+
if (!Number.isInteger(barbCount) || barbCount < 1 || barbCount > 8) {
|
|
51596
|
+
throw new Error("hoseBarbPortAssembly: barbCount must be an integer from 1 to 8");
|
|
51597
|
+
}
|
|
51598
|
+
if (barbPeakDiameter <= hoseInnerDiameter) {
|
|
51599
|
+
throw new Error("hoseBarbPortAssembly: barbPeakDiameter must exceed hoseInnerDiameter so the barb retains the hose");
|
|
51600
|
+
}
|
|
51601
|
+
if (barbRootDiameter >= barbPeakDiameter - Math.max(0.25, hoseInnerDiameter * 0.04)) {
|
|
51602
|
+
throw new Error("hoseBarbPortAssembly: barbRootDiameter must leave a visible barb rise");
|
|
51603
|
+
}
|
|
51604
|
+
if (fluidBoreDiameter >= barbRootDiameter - Math.max(0.8, hoseInnerDiameter * 0.12)) {
|
|
51605
|
+
throw new Error("hoseBarbPortAssembly: fluidBoreDiameter leaves too little wall in the barb fitting");
|
|
51606
|
+
}
|
|
51607
|
+
if (hoseOuterDiameter <= installedHoseBoreDiameter + Math.max(1.2, hoseInnerDiameter * 0.16)) {
|
|
51608
|
+
throw new Error("hoseBarbPortAssembly: hoseOuterDiameter leaves too little hose wall around the installed barb envelope");
|
|
51609
|
+
}
|
|
51610
|
+
if (shoulderDiameter <= barbPeakDiameter + Math.max(1.5, hoseInnerDiameter * 0.2)) {
|
|
51611
|
+
throw new Error("hoseBarbPortAssembly: shoulderDiameter must be larger than the barb peaks");
|
|
51612
|
+
}
|
|
51613
|
+
if (bossDiameter <= shoulderDiameter + Math.max(1.5, hoseInnerDiameter * 0.2)) {
|
|
51614
|
+
throw new Error("hoseBarbPortAssembly: bossDiameter must leave material around the shoulder seat");
|
|
51615
|
+
}
|
|
51616
|
+
if (blockWidth <= bossDiameter + 8 || blockHeight <= bossDiameter + 8) {
|
|
51617
|
+
throw new Error("hoseBarbPortAssembly: receiver block dimensions leave too little material around the port boss");
|
|
51618
|
+
}
|
|
51619
|
+
const portBoreDiameter = barbRootDiameter + runningClearance * 2;
|
|
51620
|
+
const portBore = cylinderAlongX(blockThickness + bossHeight + 0.8, portBoreDiameter / 2, bossHeight / 2, segments);
|
|
51621
|
+
const fuseOverlap = Math.min(0.04, faceClearance * 0.7);
|
|
51622
|
+
const bossCenterX = blockThickness / 2 + bossHeight / 2 - fuseOverlap;
|
|
51623
|
+
const receiver = union(
|
|
51624
|
+
box(blockThickness, blockWidth, blockHeight).translate(0, 0, -blockHeight / 2),
|
|
51625
|
+
cylinderAlongX(bossHeight + fuseOverlap, bossDiameter / 2, bossCenterX, segments)
|
|
51626
|
+
).subtract(portBore).color("#475569");
|
|
51627
|
+
const bossFaceX = blockThickness / 2 + bossHeight;
|
|
51628
|
+
const shoulderCenterX = bossFaceX + faceClearance + shoulderThickness / 2;
|
|
51629
|
+
const barbStartX = shoulderCenterX + shoulderThickness / 2;
|
|
51630
|
+
const fittingStartX = -blockThickness / 2 - runningClearance;
|
|
51631
|
+
const fittingEndX = barbStartX + barbStackLength;
|
|
51632
|
+
const fittingCore = tubeAlongX(fittingEndX - fittingStartX, barbRootDiameter / 2, fluidBoreDiameter / 2, (fittingStartX + fittingEndX) / 2, segments);
|
|
51633
|
+
const shoulder = tubeAlongX(shoulderThickness, shoulderDiameter / 2, fluidBoreDiameter / 2, shoulderCenterX, segments);
|
|
51634
|
+
const barbSolids = [];
|
|
51635
|
+
const ridgeLength = Math.max(0.8, Math.min(barbLength * 0.45, hoseInnerDiameter * 0.28));
|
|
51636
|
+
for (let index2 = 0; index2 < barbCount; index2 += 1) {
|
|
51637
|
+
const startX = barbStartX + index2 * barbLength;
|
|
51638
|
+
const ridgeCenterX = startX + barbLength - ridgeLength / 2;
|
|
51639
|
+
barbSolids.push(tubeAlongX(ridgeLength, barbPeakDiameter / 2, fluidBoreDiameter / 2, ridgeCenterX, segments));
|
|
51640
|
+
}
|
|
51641
|
+
const fitting = union(fittingCore, shoulder, ...barbSolids).color("#94a3b8");
|
|
51642
|
+
const hoseStartX = barbStartX + faceClearance;
|
|
51643
|
+
const hoseCenterX = hoseStartX + hoseLength / 2;
|
|
51644
|
+
const installedHoseBore = cylinderAlongX(hoseLength + 0.8, installedHoseBoreDiameter / 2, hoseCenterX, segments);
|
|
51645
|
+
const hose = tubeAlongX(hoseLength, hoseOuterDiameter / 2, installedHoseBoreDiameter / 2, hoseCenterX, segments).color("#111827");
|
|
51646
|
+
const clampCenterX = barbStartX + Math.min(barbStackLength * 0.55, Math.max(barbLength, clampWidth));
|
|
51647
|
+
const clamp2 = tubeAlongX(
|
|
51648
|
+
clampWidth,
|
|
51649
|
+
hoseOuterDiameter / 2 + clampThickness,
|
|
51650
|
+
hoseOuterDiameter / 2 + Math.min(0.08, runningClearance * 0.45),
|
|
51651
|
+
clampCenterX,
|
|
51652
|
+
segments
|
|
51653
|
+
).color("#cbd5e1");
|
|
51654
|
+
const parts = [
|
|
51655
|
+
{ name: "bored pump or filter body with raised hose-port boss", shape: receiver },
|
|
51656
|
+
{ name: "hollow hose barb fitting with shoulder and retention ridges", shape: fitting },
|
|
51657
|
+
{ name: "installed flexible hose over barb tail", shape: hose },
|
|
51658
|
+
{ name: "clamp band over hose and barb ridges", shape: clamp2 }
|
|
51659
|
+
];
|
|
51660
|
+
return {
|
|
51661
|
+
parts,
|
|
51662
|
+
receiver,
|
|
51663
|
+
fitting,
|
|
51664
|
+
hose,
|
|
51665
|
+
clamp: clamp2,
|
|
51666
|
+
cutters: {
|
|
51667
|
+
portBore,
|
|
51668
|
+
installedHoseBore
|
|
51669
|
+
},
|
|
51670
|
+
dims: {
|
|
51671
|
+
hoseInnerDiameter,
|
|
51672
|
+
hoseOuterDiameter,
|
|
51673
|
+
installedHoseBoreDiameter,
|
|
51674
|
+
blockThickness,
|
|
51675
|
+
blockWidth,
|
|
51676
|
+
blockHeight,
|
|
51677
|
+
bossDiameter,
|
|
51678
|
+
bossHeight,
|
|
51679
|
+
fluidBoreDiameter,
|
|
51680
|
+
barbRootDiameter,
|
|
51681
|
+
barbPeakDiameter,
|
|
51682
|
+
barbCount,
|
|
51683
|
+
barbLength,
|
|
51684
|
+
barbStackLength,
|
|
51685
|
+
shoulderDiameter,
|
|
51686
|
+
shoulderThickness,
|
|
51687
|
+
hoseLength,
|
|
51688
|
+
clampWidth,
|
|
51689
|
+
clampThickness,
|
|
51690
|
+
runningClearance,
|
|
51691
|
+
faceClearance
|
|
51692
|
+
}
|
|
51693
|
+
};
|
|
51694
|
+
}
|
|
51695
|
+
function routedTubeClipAssembly(options) {
|
|
51696
|
+
const tubeDiameter = requirePositive$6(options.tubeDiameter, "tubeDiameter");
|
|
51697
|
+
const tubeLength = requirePositive$6(options.tubeLength ?? 120, "tubeLength");
|
|
51698
|
+
const panelThickness = requirePositive$6(options.panelThickness ?? 3, "panelThickness");
|
|
51699
|
+
const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
|
|
51700
|
+
const screwSize = options.screwSize ?? "M3";
|
|
51701
|
+
const segments = options.segments ?? 32;
|
|
51702
|
+
const sizeData = METRIC_HOLE_TABLE[screwSize];
|
|
51703
|
+
if (!sizeData) throw new Error(`routedTubeClipAssembly: unsupported screwSize "${screwSize}"`);
|
|
51704
|
+
const clipCount = options.clipCount ?? 3;
|
|
51705
|
+
if (!Number.isInteger(clipCount) || clipCount < 1 || clipCount > 8) {
|
|
51706
|
+
throw new Error("routedTubeClipAssembly: clipCount must be an integer from 1 to 8");
|
|
51707
|
+
}
|
|
51708
|
+
const screwDiameter = parseFloat(screwSize.replace("M", ""));
|
|
51709
|
+
const screwHeadDiameter = sizeData.head;
|
|
51710
|
+
const tubeBoreDiameter = tubeDiameter + runningClearance * 2;
|
|
51711
|
+
const clipWallThickness = requirePositive$6(
|
|
51712
|
+
options.clipWallThickness ?? Math.max(screwHeadDiameter + 1.2, tubeDiameter * 0.45, 5),
|
|
51713
|
+
"clipWallThickness"
|
|
51714
|
+
);
|
|
51715
|
+
const clipWidth = requirePositive$6(options.clipWidth ?? Math.max(screwHeadDiameter + 3, tubeDiameter * 1.4, 10), "clipWidth");
|
|
51716
|
+
const clipDepth = tubeBoreDiameter + clipWallThickness * 2;
|
|
51717
|
+
const bottomWall = Math.max(1.2, clipWallThickness * 0.35);
|
|
51718
|
+
const topWall = Math.max(2, clipWallThickness * 0.45);
|
|
51719
|
+
const clipHeight = bottomWall + tubeBoreDiameter + topWall;
|
|
51720
|
+
const tubeCenterZ = panelThickness + bottomWall + tubeBoreDiameter / 2;
|
|
51721
|
+
const panelLength = requirePositive$6(options.panelLength ?? tubeLength + 24, "panelLength");
|
|
51722
|
+
const panelWidth = requirePositive$6(options.panelWidth ?? clipDepth + Math.max(14, screwHeadDiameter * 2), "panelWidth");
|
|
51723
|
+
if (tubeLength <= clipWidth + 8) {
|
|
51724
|
+
throw new Error("routedTubeClipAssembly: tubeLength must leave visible tube beyond the clip body");
|
|
51725
|
+
}
|
|
51726
|
+
const defaultSpacing = clipCount === 1 ? 0 : Math.max(clipWidth + 8, (tubeLength - clipWidth * 2) / (clipCount - 1));
|
|
51727
|
+
const clipSpacing = options.clipSpacing === void 0 ? defaultSpacing : requirePositive$6(options.clipSpacing, "clipSpacing");
|
|
51728
|
+
const clipCenters = Array.from({ length: clipCount }, (_2, index2) => (index2 - (clipCount - 1) / 2) * clipSpacing);
|
|
51729
|
+
const maxClipExtent = Math.max(...clipCenters.map((x2) => Math.abs(x2) + clipWidth / 2));
|
|
51730
|
+
if (maxClipExtent > tubeLength / 2 - 2) {
|
|
51731
|
+
throw new Error("routedTubeClipAssembly: clipSpacing places a clip beyond the routed tube length");
|
|
51732
|
+
}
|
|
51733
|
+
if (maxClipExtent > panelLength / 2 - 2) {
|
|
51734
|
+
throw new Error("routedTubeClipAssembly: panelLength is too short for the clip pattern");
|
|
51735
|
+
}
|
|
51736
|
+
const boreRadius = tubeBoreDiameter / 2;
|
|
51737
|
+
const screwY = boreRadius + clipWallThickness / 2;
|
|
51738
|
+
if (screwY + screwHeadDiameter / 2 > clipDepth / 2 - 0.2) {
|
|
51739
|
+
throw new Error("routedTubeClipAssembly: clipWallThickness leaves too little land for screw heads");
|
|
51740
|
+
}
|
|
51741
|
+
if (clipDepth > panelWidth - Math.max(4, screwHeadDiameter * 0.5)) {
|
|
51742
|
+
throw new Error("routedTubeClipAssembly: panelWidth leaves too little material beside the clips");
|
|
51743
|
+
}
|
|
51744
|
+
const screwPositions = clipCenters.flatMap((x2) => [
|
|
51745
|
+
[x2, -screwY],
|
|
51746
|
+
[x2, screwY]
|
|
51747
|
+
]);
|
|
51748
|
+
const screwClearanceDiameter = Math.max(sizeData.loose, screwDiameter + 0.8);
|
|
51749
|
+
const panelThreadEnvelopeDiameter = screwClearanceDiameter;
|
|
51750
|
+
const clipTopZ = panelThickness + clipHeight;
|
|
51751
|
+
const clipTubeBores = union(
|
|
51752
|
+
...clipCenters.map((x2) => cylinderAlongX(clipWidth + 0.8, boreRadius, x2, segments).translate(0, 0, tubeCenterZ))
|
|
51753
|
+
);
|
|
51754
|
+
const clipScrewClearances = union(
|
|
51755
|
+
...screwPositions.map(([x2, y2]) => cylinder(clipHeight + 0.8, screwClearanceDiameter / 2, void 0, segments).translate(x2, y2, panelThickness - 0.4))
|
|
51756
|
+
);
|
|
51757
|
+
const panelThreadEnvelopes = union(
|
|
51758
|
+
...screwPositions.map(([x2, y2]) => cylinder(panelThickness + 0.8, panelThreadEnvelopeDiameter / 2, void 0, segments).translate(x2, y2, -0.4))
|
|
51759
|
+
);
|
|
51760
|
+
const panel = box(panelLength, panelWidth, panelThickness).subtract(panelThreadEnvelopes).color("#475569");
|
|
51761
|
+
const tube2 = cylinderAlongX(tubeLength, tubeDiameter / 2, 0, segments).translate(0, 0, tubeCenterZ).color("#0f172a");
|
|
51762
|
+
const clips = clipCenters.map((x2) => {
|
|
51763
|
+
const body = box(clipWidth, clipDepth, clipHeight).translate(x2, 0, panelThickness);
|
|
51764
|
+
const tubeBore = cylinderAlongX(clipWidth + 0.8, boreRadius, x2, segments).translate(0, 0, tubeCenterZ);
|
|
51765
|
+
const screwHoles = union(
|
|
51766
|
+
cylinder(clipHeight + 0.8, screwClearanceDiameter / 2, void 0, segments).translate(x2, -screwY, panelThickness - 0.4),
|
|
51767
|
+
cylinder(clipHeight + 0.8, screwClearanceDiameter / 2, void 0, segments).translate(x2, screwY, panelThickness - 0.4)
|
|
51768
|
+
);
|
|
51769
|
+
return body.subtract(tubeBore).subtract(screwHoles).color("#94a3b8");
|
|
51770
|
+
});
|
|
51771
|
+
const screwLength = clipHeight + panelThickness * 0.65;
|
|
51772
|
+
const screwHeadHeight = Math.max(1.2, screwDiameter * 0.55);
|
|
51773
|
+
const screwBlank = union(
|
|
51774
|
+
cylinder(screwLength, screwDiameter / 2, void 0, segments).translate(0, 0, clipTopZ - screwLength),
|
|
51775
|
+
cylinder(screwHeadHeight, screwHeadDiameter / 2, void 0, segments).translate(0, 0, clipTopZ)
|
|
51776
|
+
).color("#cbd5e1");
|
|
51777
|
+
const screws = screwPositions.map(([x2, y2]) => screwBlank.translate(x2, y2, 0));
|
|
51778
|
+
const parts = [
|
|
51779
|
+
{ name: "panel with tube-clip screw receiving holes", shape: panel },
|
|
51780
|
+
{ name: "routed flexible tube through retained clip bores", shape: tube2 },
|
|
51781
|
+
...clips.map((shape, index2) => ({ name: `saddle tube clip ${index2 + 1} with through-bore`, shape })),
|
|
51782
|
+
...screws.map((shape, index2) => ({ name: `installed ${screwSize} tube clip screw ${index2 + 1}`, shape }))
|
|
51783
|
+
];
|
|
51784
|
+
return {
|
|
51785
|
+
parts,
|
|
51786
|
+
panel,
|
|
51787
|
+
tube: tube2,
|
|
51788
|
+
clips,
|
|
51789
|
+
screws,
|
|
51790
|
+
clipCenters,
|
|
51791
|
+
screwPositions,
|
|
51792
|
+
cutters: {
|
|
51793
|
+
clipTubeBores,
|
|
51794
|
+
clipScrewClearances,
|
|
51795
|
+
panelThreadEnvelopes
|
|
51796
|
+
},
|
|
51797
|
+
dims: {
|
|
51798
|
+
tubeDiameter,
|
|
51799
|
+
tubeLength,
|
|
51800
|
+
tubeBoreDiameter,
|
|
51801
|
+
panelLength,
|
|
51802
|
+
panelWidth,
|
|
51803
|
+
panelThickness,
|
|
51804
|
+
clipCount,
|
|
51805
|
+
clipWidth,
|
|
51806
|
+
clipDepth,
|
|
51807
|
+
clipHeight,
|
|
51808
|
+
clipWallThickness,
|
|
51809
|
+
tubeCenterZ,
|
|
51810
|
+
screwSize,
|
|
51811
|
+
screwDiameter,
|
|
51812
|
+
screwHeadDiameter,
|
|
51813
|
+
screwLength,
|
|
51814
|
+
screwClearanceDiameter,
|
|
51815
|
+
panelThreadEnvelopeDiameter,
|
|
51816
|
+
runningClearance
|
|
51817
|
+
}
|
|
51818
|
+
};
|
|
51819
|
+
}
|
|
51820
|
+
function pcbTerminalBlockAssembly(options = {}) {
|
|
51821
|
+
const terminalCount = options.terminalCount ?? 4;
|
|
51822
|
+
if (!Number.isInteger(terminalCount) || terminalCount < 1 || terminalCount > 24) {
|
|
51823
|
+
throw new Error("pcbTerminalBlockAssembly: terminalCount must be an integer from 1 to 24");
|
|
51824
|
+
}
|
|
51825
|
+
const terminalPitch = requirePositive$6(options.terminalPitch ?? 5.08, "terminalPitch");
|
|
51826
|
+
const terminalBlockWidth = terminalPitch * terminalCount + 3;
|
|
51827
|
+
const boardWidth = requirePositive$6(options.boardWidth ?? Math.max(50, terminalBlockWidth + 28), "boardWidth");
|
|
51828
|
+
const boardDepth = requirePositive$6(options.boardDepth ?? 38, "boardDepth");
|
|
51829
|
+
const boardThickness = requirePositive$6(options.boardThickness ?? 1.6, "boardThickness");
|
|
51830
|
+
const backplateThickness = requirePositive$6(options.backplateThickness ?? 3, "backplateThickness");
|
|
51831
|
+
const backplateMargin = requirePositive$6(options.backplateMargin ?? 5, "backplateMargin");
|
|
51832
|
+
const standoffHeight = requirePositive$6(options.standoffHeight ?? 6, "standoffHeight");
|
|
51833
|
+
const screwSize = options.screwSize ?? "M3";
|
|
51834
|
+
const segments = options.segments ?? 28;
|
|
51835
|
+
const sizeData = METRIC_HOLE_TABLE[screwSize];
|
|
51836
|
+
if (!sizeData) throw new Error(`pcbTerminalBlockAssembly: unsupported screwSize "${screwSize}"`);
|
|
51837
|
+
const screwDiameter = parseFloat(screwSize.replace("M", ""));
|
|
51838
|
+
const screwHeadDiameter = sizeData.head;
|
|
51839
|
+
const screwHeadHeight = Math.max(1.2, screwDiameter * 0.55);
|
|
51840
|
+
const standoffDiameter = requirePositive$6(
|
|
51841
|
+
options.standoffDiameter ?? Math.max(screwHeadDiameter * 1.45, sizeData.normal + 3),
|
|
51842
|
+
"standoffDiameter"
|
|
51843
|
+
);
|
|
51844
|
+
const [mountInsetX, mountInsetY] = resolveBoltInset(
|
|
51845
|
+
options.mountingInset,
|
|
51846
|
+
Math.max(standoffDiameter / 2 + 1.2, screwHeadDiameter * 0.75)
|
|
51847
|
+
);
|
|
51848
|
+
if (mountInsetX * 2 >= boardWidth || mountInsetY * 2 >= boardDepth) {
|
|
51849
|
+
throw new Error("pcbTerminalBlockAssembly: mountingInset leaves no room for the PCB mounting pattern");
|
|
51850
|
+
}
|
|
51851
|
+
const terminalBlockDepth = requirePositive$6(options.terminalBlockDepth ?? 10, "terminalBlockDepth");
|
|
51852
|
+
const terminalBlockHeight = requirePositive$6(options.terminalBlockHeight ?? 9, "terminalBlockHeight");
|
|
51853
|
+
const terminalEdgeInset = requirePositive$6(options.terminalEdgeInset ?? 5, "terminalEdgeInset");
|
|
51854
|
+
const pinDiameter = requirePositive$6(options.pinDiameter ?? 0.9, "pinDiameter");
|
|
51855
|
+
const pinClearance = requirePositive$6(options.pinClearance ?? 0.25, "pinClearance");
|
|
51856
|
+
const pinTailLength = requireNonNegative(options.pinTailLength ?? 0, "pinTailLength");
|
|
51857
|
+
const wirePortDiameter = requirePositive$6(options.wirePortDiameter ?? 2.6, "wirePortDiameter");
|
|
51858
|
+
const pinHoleDiameter = pinDiameter + pinClearance;
|
|
51859
|
+
const terminalCenterY = -boardDepth / 2 + terminalEdgeInset + terminalBlockDepth / 2;
|
|
51860
|
+
const pinY = terminalCenterY + terminalBlockDepth * 0.24;
|
|
51861
|
+
const firstPinX = -((terminalCount - 1) * terminalPitch) / 2;
|
|
51862
|
+
const pinPositions = Array.from({ length: terminalCount }, (_2, index2) => [firstPinX + index2 * terminalPitch, pinY]);
|
|
51863
|
+
const mountingPositions = [
|
|
51864
|
+
[-boardWidth / 2 + mountInsetX, -boardDepth / 2 + mountInsetY],
|
|
51865
|
+
[boardWidth / 2 - mountInsetX, -boardDepth / 2 + mountInsetY],
|
|
51866
|
+
[-boardWidth / 2 + mountInsetX, boardDepth / 2 - mountInsetY],
|
|
51867
|
+
[boardWidth / 2 - mountInsetX, boardDepth / 2 - mountInsetY]
|
|
51868
|
+
];
|
|
51869
|
+
if (terminalBlockWidth >= boardWidth - mountInsetX * 2) {
|
|
51870
|
+
throw new Error("pcbTerminalBlockAssembly: terminal block is too wide for the PCB mounting pattern");
|
|
51871
|
+
}
|
|
51872
|
+
if (terminalEdgeInset + terminalBlockDepth >= boardDepth - mountInsetY * 2) {
|
|
51873
|
+
throw new Error("pcbTerminalBlockAssembly: terminal block depth collides with the rear mounting datum");
|
|
51874
|
+
}
|
|
51875
|
+
if (pinHoleDiameter >= terminalPitch * 0.55) {
|
|
51876
|
+
throw new Error("pcbTerminalBlockAssembly: pinDiameter and pinClearance leave too little PCB web between terminal holes");
|
|
51877
|
+
}
|
|
51878
|
+
if (wirePortDiameter >= Math.min(terminalPitch * 0.72, terminalBlockHeight * 0.65)) {
|
|
51879
|
+
throw new Error("pcbTerminalBlockAssembly: wirePortDiameter is too large for the terminal pitch or body height");
|
|
51880
|
+
}
|
|
51881
|
+
for (const [index2, [x2, y2]] of [...mountingPositions, ...pinPositions].entries()) {
|
|
51882
|
+
if (!Number.isFinite(x2) || !Number.isFinite(y2)) {
|
|
51883
|
+
throw new Error(`pcbTerminalBlockAssembly: generated datum position ${index2} is not finite`);
|
|
51884
|
+
}
|
|
51885
|
+
}
|
|
51886
|
+
const backplateWidth = boardWidth + backplateMargin * 2;
|
|
51887
|
+
const backplateDepth = boardDepth + backplateMargin * 2;
|
|
51888
|
+
const boardBottomZ = backplateThickness + standoffHeight;
|
|
51889
|
+
const boardTopZ = boardBottomZ + boardThickness;
|
|
51890
|
+
const standoffOverlap = Math.min(0.08, standoffHeight * 0.03);
|
|
51891
|
+
const standoffThreadEnvelopeDiameter = Math.max(sizeData.loose, screwDiameter + 1);
|
|
51892
|
+
const standoffThreadEnvelope = cylinder(standoffHeight + 0.8, standoffThreadEnvelopeDiameter / 2, void 0, segments).translate(
|
|
51893
|
+
0,
|
|
51894
|
+
0,
|
|
51895
|
+
backplateThickness - 0.4
|
|
51896
|
+
);
|
|
51897
|
+
const standoffThreadEnvelopes = union(...mountingPositions.map(([x2, y2]) => standoffThreadEnvelope.translate(x2, y2, 0)));
|
|
51898
|
+
const standoff = cylinder(standoffHeight + standoffOverlap, standoffDiameter / 2, void 0, segments).translate(0, 0, backplateThickness - standoffOverlap).subtract(standoffThreadEnvelope);
|
|
51899
|
+
const standoffs = union(...mountingPositions.map(([x2, y2]) => standoff.translate(x2, y2, 0)));
|
|
51900
|
+
const backplate = union(box(backplateWidth, backplateDepth, backplateThickness), standoffs).color("#475569");
|
|
51901
|
+
const boardMountingHoleDiameter = sizeData.normal;
|
|
51902
|
+
const boardMountHole = cylinder(boardThickness + 0.8, boardMountingHoleDiameter / 2, void 0, segments).translate(
|
|
51903
|
+
0,
|
|
51904
|
+
0,
|
|
51905
|
+
boardBottomZ - 0.4
|
|
51906
|
+
);
|
|
51907
|
+
const pcbMountingHoles = union(...mountingPositions.map(([x2, y2]) => boardMountHole.translate(x2, y2, 0)));
|
|
51908
|
+
const pinHole = cylinder(boardThickness + 0.8, pinHoleDiameter / 2, void 0, segments).translate(0, 0, boardBottomZ - 0.4);
|
|
51909
|
+
const pcbPinHoles = union(...pinPositions.map(([x2, y2]) => pinHole.translate(x2, y2, 0)));
|
|
51910
|
+
const pcb = box(boardWidth, boardDepth, boardThickness).translate(0, 0, boardBottomZ).subtract(pcbMountingHoles).subtract(pcbPinHoles).color("#166534");
|
|
51911
|
+
const terminalBodyBlank = box(terminalBlockWidth, terminalBlockDepth, terminalBlockHeight).translate(0, terminalCenterY, boardTopZ);
|
|
51912
|
+
const wirePort = cylinderAlongY(terminalBlockDepth + 0.8, wirePortDiameter / 2, terminalCenterY, segments).translate(
|
|
51913
|
+
0,
|
|
51914
|
+
0,
|
|
51915
|
+
boardTopZ + terminalBlockHeight * 0.42
|
|
51916
|
+
);
|
|
51917
|
+
const wirePorts = union(...pinPositions.map(([x2]) => wirePort.translate(x2, 0, 0)));
|
|
51918
|
+
const clampScrewPockets = union(
|
|
51919
|
+
...pinPositions.map(
|
|
51920
|
+
([x2]) => cylinder(Math.max(0.6, terminalBlockHeight * 0.22), Math.min(terminalPitch * 0.22, wirePortDiameter * 0.42), void 0, segments).translate(
|
|
51921
|
+
x2,
|
|
51922
|
+
terminalCenterY + terminalBlockDepth * 0.12,
|
|
51923
|
+
boardTopZ + terminalBlockHeight * 0.76
|
|
51924
|
+
)
|
|
51925
|
+
)
|
|
51926
|
+
);
|
|
51927
|
+
const pinLength = boardThickness + pinTailLength + Math.min(0.6, terminalBlockHeight * 0.08);
|
|
51928
|
+
const pinStartZ = boardBottomZ - pinTailLength;
|
|
51929
|
+
const pins = union(...pinPositions.map(([x2, y2]) => cylinder(pinLength, pinDiameter / 2, void 0, segments).translate(x2, y2, pinStartZ)));
|
|
51930
|
+
const terminalBlock = union(terminalBodyBlank.subtract(wirePorts).subtract(clampScrewPockets), pins).color("#16a34a");
|
|
51931
|
+
const screwShaftLength = boardThickness + standoffHeight * 0.85;
|
|
51932
|
+
const mountingHardware = fastenerSet(screwSize, screwShaftLength, {
|
|
51933
|
+
washerUnderHead: false,
|
|
51934
|
+
washerUnderNut: false,
|
|
51935
|
+
fit: "normal",
|
|
51936
|
+
segments
|
|
51937
|
+
});
|
|
51938
|
+
const screws = mountingPositions.map(([x2, y2]) => mountingHardware.bolt.translate(x2, y2, boardTopZ).color("#cbd5e1"));
|
|
51939
|
+
const parts = [
|
|
51940
|
+
{ name: "electronics backplate with fused PCB standoffs", shape: backplate },
|
|
51941
|
+
{ name: "PCB with mounting holes and terminal pin clearances", shape: pcb },
|
|
51942
|
+
{ name: "seated purchased terminal block with through-board pins", shape: terminalBlock },
|
|
51943
|
+
...screws.map((shape, index2) => ({ name: `installed ${screwSize} PCB mounting screw ${index2 + 1}`, shape }))
|
|
51944
|
+
];
|
|
51945
|
+
return {
|
|
51946
|
+
parts,
|
|
51947
|
+
backplate,
|
|
51948
|
+
pcb,
|
|
51949
|
+
terminalBlock,
|
|
51950
|
+
screws,
|
|
51951
|
+
mountingPositions,
|
|
51952
|
+
pinPositions,
|
|
51953
|
+
cutters: {
|
|
51954
|
+
pcbMountingHoles,
|
|
51955
|
+
pcbPinHoles,
|
|
51956
|
+
standoffThreadEnvelopes
|
|
51957
|
+
},
|
|
51958
|
+
dims: {
|
|
51959
|
+
terminalCount,
|
|
51960
|
+
terminalPitch,
|
|
51961
|
+
boardWidth,
|
|
51962
|
+
boardDepth,
|
|
51963
|
+
boardThickness,
|
|
51964
|
+
backplateWidth,
|
|
51965
|
+
backplateDepth,
|
|
51966
|
+
backplateThickness,
|
|
51967
|
+
standoffHeight,
|
|
51968
|
+
standoffDiameter,
|
|
51969
|
+
screwSize,
|
|
51970
|
+
screwDiameter,
|
|
51971
|
+
screwHeadDiameter,
|
|
51972
|
+
screwHeadHeight,
|
|
51973
|
+
screwShaftLength,
|
|
51974
|
+
boardMountingHoleDiameter,
|
|
51975
|
+
standoffThreadEnvelopeDiameter,
|
|
51976
|
+
terminalBlockWidth,
|
|
51977
|
+
terminalBlockDepth,
|
|
51978
|
+
terminalBlockHeight,
|
|
51979
|
+
terminalEdgeInset,
|
|
51980
|
+
pinDiameter,
|
|
51981
|
+
pinClearance,
|
|
51982
|
+
pinHoleDiameter,
|
|
51983
|
+
pinTailLength,
|
|
51984
|
+
wirePortDiameter
|
|
51985
|
+
}
|
|
51986
|
+
};
|
|
51987
|
+
}
|
|
51988
|
+
function thumbScrewClampAssembly(options = {}) {
|
|
51989
|
+
const screwSize = options.screwSize ?? "M6";
|
|
51990
|
+
const segments = options.segments ?? 36;
|
|
51991
|
+
const sizeData = METRIC_HOLE_TABLE[screwSize];
|
|
51992
|
+
if (!sizeData) throw new Error(`thumbScrewClampAssembly: unsupported screwSize "${screwSize}"`);
|
|
51993
|
+
const screwDiameter = parseFloat(screwSize.replace("M", ""));
|
|
51994
|
+
const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
|
|
51995
|
+
const faceClearance = requireNonNegative(options.faceClearance ?? 0, "faceClearance");
|
|
51996
|
+
const threadEnvelopeDiameter = Math.max(sizeData.normal, screwDiameter + runningClearance * 2);
|
|
51997
|
+
const pressurePadDiameter = requirePositive$6(
|
|
51998
|
+
options.pressurePadDiameter ?? Math.max(screwDiameter * 3.2, 18),
|
|
51999
|
+
"pressurePadDiameter"
|
|
52000
|
+
);
|
|
52001
|
+
const pressurePadThickness = requirePositive$6(
|
|
52002
|
+
options.pressurePadThickness ?? Math.max(screwDiameter * 0.72, 4),
|
|
52003
|
+
"pressurePadThickness"
|
|
52004
|
+
);
|
|
52005
|
+
const knobDiameter = requirePositive$6(options.knobDiameter ?? Math.max(screwDiameter * 4.2, 24), "knobDiameter");
|
|
52006
|
+
const knobThickness = requirePositive$6(options.knobThickness ?? Math.max(screwDiameter * 0.9, 7), "knobThickness");
|
|
52007
|
+
const workpieceThickness = requirePositive$6(options.workpieceThickness ?? 18, "workpieceThickness");
|
|
52008
|
+
const workpieceDepth = requirePositive$6(options.workpieceDepth ?? Math.max(46, pressurePadDiameter * 1.5), "workpieceDepth");
|
|
52009
|
+
const workpieceHeight = requirePositive$6(options.workpieceHeight ?? Math.max(pressurePadDiameter * 1.35, 24), "workpieceHeight");
|
|
52010
|
+
const frameDepth = requirePositive$6(
|
|
52011
|
+
options.frameDepth ?? Math.max(workpieceDepth + 12, pressurePadDiameter + 16),
|
|
52012
|
+
"frameDepth"
|
|
52013
|
+
);
|
|
52014
|
+
const baseThickness = requirePositive$6(options.baseThickness ?? Math.max(screwDiameter, 6), "baseThickness");
|
|
52015
|
+
const jawThickness = requirePositive$6(options.jawThickness ?? Math.max(screwDiameter * 1.35, 9), "jawThickness");
|
|
52016
|
+
const supportThickness = requirePositive$6(
|
|
52017
|
+
options.supportThickness ?? Math.max(screwDiameter * 1.8, 12),
|
|
52018
|
+
"supportThickness"
|
|
52019
|
+
);
|
|
52020
|
+
const bossLength = requirePositive$6(options.bossLength ?? Math.max(screwDiameter * 1.1, 8), "bossLength");
|
|
52021
|
+
const bossDiameter = requirePositive$6(options.bossDiameter ?? Math.max(threadEnvelopeDiameter + 5, screwDiameter * 2.5), "bossDiameter");
|
|
52022
|
+
const exposedScrewLength = requirePositive$6(
|
|
52023
|
+
options.exposedScrewLength ?? Math.max(pressurePadDiameter * 0.45, screwDiameter * 2.2),
|
|
52024
|
+
"exposedScrewLength"
|
|
52025
|
+
);
|
|
52026
|
+
const screwCenterZ = baseThickness + Math.max(workpieceHeight * 0.52, pressurePadDiameter * 0.68);
|
|
52027
|
+
const frameHeight = requirePositive$6(
|
|
52028
|
+
options.frameHeight ?? screwCenterZ - baseThickness + pressurePadDiameter / 2 + Math.max(baseThickness, 7),
|
|
52029
|
+
"frameHeight"
|
|
52030
|
+
);
|
|
52031
|
+
if (workpieceDepth > frameDepth - 6) {
|
|
52032
|
+
throw new Error("thumbScrewClampAssembly: frameDepth must leave side material around the clamped workpiece");
|
|
52033
|
+
}
|
|
52034
|
+
if (pressurePadDiameter > frameDepth - 4) {
|
|
52035
|
+
throw new Error("thumbScrewClampAssembly: pressurePadDiameter is too large for the frame depth");
|
|
52036
|
+
}
|
|
52037
|
+
if (bossDiameter > frameDepth - 4) {
|
|
52038
|
+
throw new Error("thumbScrewClampAssembly: bossDiameter is too large for the frame depth");
|
|
52039
|
+
}
|
|
52040
|
+
if (screwCenterZ - pressurePadDiameter / 2 <= baseThickness + 0.5) {
|
|
52041
|
+
throw new Error("thumbScrewClampAssembly: pressure pad collides with the base bridge");
|
|
52042
|
+
}
|
|
52043
|
+
if (baseThickness + frameHeight - screwCenterZ <= pressurePadDiameter / 2 + 2) {
|
|
52044
|
+
throw new Error("thumbScrewClampAssembly: frameHeight leaves too little material above the screw axis");
|
|
52045
|
+
}
|
|
52046
|
+
if (threadEnvelopeDiameter + 4 > Math.min(frameDepth, frameHeight)) {
|
|
52047
|
+
throw new Error("thumbScrewClampAssembly: threaded boss bore leaves too little surrounding frame material");
|
|
52048
|
+
}
|
|
52049
|
+
const workpieceLeftFaceX = -workpieceThickness / 2;
|
|
52050
|
+
const workpieceRightFaceX = workpieceThickness / 2;
|
|
52051
|
+
const anvilOverlap = Math.min(0.35, pressurePadThickness * 0.18);
|
|
52052
|
+
const anvilPadCenterX = workpieceLeftFaceX - faceClearance - pressurePadThickness / 2;
|
|
52053
|
+
const pressurePadCenterX = workpieceRightFaceX + faceClearance + pressurePadThickness / 2;
|
|
52054
|
+
const fixedJawRightFaceX = anvilPadCenterX - pressurePadThickness / 2 + anvilOverlap;
|
|
52055
|
+
const fixedJawCenterX = fixedJawRightFaceX - jawThickness / 2;
|
|
52056
|
+
const pressurePadRightFaceX = pressurePadCenterX + pressurePadThickness / 2;
|
|
52057
|
+
const supportInnerFaceX = pressurePadRightFaceX + exposedScrewLength;
|
|
52058
|
+
const supportCenterX = supportInnerFaceX + supportThickness / 2;
|
|
52059
|
+
const supportOuterFaceX = supportInnerFaceX + supportThickness;
|
|
52060
|
+
const frameLeftFaceX = fixedJawCenterX - jawThickness / 2;
|
|
52061
|
+
const frameRightFaceX = supportOuterFaceX;
|
|
52062
|
+
const baseLength = frameRightFaceX - frameLeftFaceX;
|
|
52063
|
+
if (baseLength <= 0 || !Number.isFinite(baseLength)) {
|
|
52064
|
+
throw new Error("thumbScrewClampAssembly: generated clamp frame length is invalid");
|
|
52065
|
+
}
|
|
52066
|
+
const bossCenterX = supportInnerFaceX + (supportThickness + bossLength) / 2;
|
|
52067
|
+
const threadedBossBore = cylinderAlongX(supportThickness + bossLength + 1, threadEnvelopeDiameter / 2, bossCenterX, segments).translate(
|
|
52068
|
+
0,
|
|
52069
|
+
0,
|
|
52070
|
+
screwCenterZ
|
|
52071
|
+
);
|
|
52072
|
+
const frameOverlap = Math.min(0.12, baseThickness * 0.04);
|
|
52073
|
+
const base = box(baseLength, frameDepth, baseThickness).translate((frameLeftFaceX + frameRightFaceX) / 2, 0, 0);
|
|
52074
|
+
const fixedJaw = box(jawThickness, frameDepth, frameHeight + frameOverlap).translate(fixedJawCenterX, 0, baseThickness - frameOverlap);
|
|
52075
|
+
const support = box(supportThickness, frameDepth, frameHeight + frameOverlap).translate(supportCenterX, 0, baseThickness - frameOverlap);
|
|
52076
|
+
const boss2 = cylinderAlongX(supportThickness + bossLength, bossDiameter / 2, bossCenterX, segments).translate(0, 0, screwCenterZ);
|
|
52077
|
+
const anvilPad = cylinderAlongX(pressurePadThickness, pressurePadDiameter / 2, anvilPadCenterX, segments).translate(0, 0, screwCenterZ);
|
|
52078
|
+
const frame = union(base, fixedJaw, support, boss2, anvilPad).subtract(threadedBossBore).color("#475569");
|
|
52079
|
+
const workpieceBottomZ = screwCenterZ - workpieceHeight / 2;
|
|
52080
|
+
const workpiece = box(workpieceThickness, workpieceDepth, workpieceHeight).translate(0, 0, workpieceBottomZ).color("#a16207");
|
|
52081
|
+
const pressurePad = cylinderAlongX(pressurePadThickness, pressurePadDiameter / 2, pressurePadCenterX, segments).translate(0, 0, screwCenterZ);
|
|
52082
|
+
const knobCenterX = supportOuterFaceX + bossLength + runningClearance + knobThickness / 2;
|
|
52083
|
+
const knob = cylinderAlongX(knobThickness, knobDiameter / 2, knobCenterX, segments).translate(0, 0, screwCenterZ);
|
|
52084
|
+
const shaftLeftX = pressurePadRightFaceX - Math.min(pressurePadThickness * 0.45, screwDiameter * 0.45);
|
|
52085
|
+
const shaftRightX = knobCenterX + knobThickness / 2;
|
|
52086
|
+
const shaftLength = shaftRightX - shaftLeftX;
|
|
52087
|
+
if (shaftLength <= supportThickness + bossLength) {
|
|
52088
|
+
throw new Error("thumbScrewClampAssembly: generated screw length is too short for the threaded support");
|
|
52089
|
+
}
|
|
52090
|
+
const shaft = cylinderAlongX(shaftLength, screwDiameter / 2, (shaftLeftX + shaftRightX) / 2, segments).translate(0, 0, screwCenterZ);
|
|
52091
|
+
const clampScrew = union(shaft, pressurePad, knob).color("#cbd5e1");
|
|
52092
|
+
const workpieceEnvelope = box(workpieceThickness, workpieceDepth, workpieceHeight).translate(0, 0, workpieceBottomZ);
|
|
52093
|
+
return {
|
|
52094
|
+
parts: [
|
|
52095
|
+
{ name: "thumb-screw clamp frame with fixed anvil and threaded boss", shape: frame },
|
|
52096
|
+
{ name: "representative clamped workpiece between pads", shape: workpiece },
|
|
52097
|
+
{ name: "installed thumb screw with captive pressure pad and hand knob", shape: clampScrew }
|
|
52098
|
+
],
|
|
52099
|
+
frame,
|
|
52100
|
+
workpiece,
|
|
52101
|
+
clampScrew,
|
|
52102
|
+
cutters: {
|
|
52103
|
+
threadedBossBore,
|
|
52104
|
+
workpieceEnvelope
|
|
52105
|
+
},
|
|
52106
|
+
dims: {
|
|
52107
|
+
screwSize,
|
|
52108
|
+
screwDiameter,
|
|
52109
|
+
threadEnvelopeDiameter,
|
|
52110
|
+
workpieceThickness,
|
|
52111
|
+
workpieceDepth,
|
|
52112
|
+
workpieceHeight,
|
|
52113
|
+
frameDepth,
|
|
52114
|
+
frameHeight,
|
|
52115
|
+
baseThickness,
|
|
52116
|
+
jawThickness,
|
|
52117
|
+
supportThickness,
|
|
52118
|
+
bossLength,
|
|
52119
|
+
bossDiameter,
|
|
52120
|
+
exposedScrewLength,
|
|
52121
|
+
pressurePadDiameter,
|
|
52122
|
+
pressurePadThickness,
|
|
52123
|
+
knobDiameter,
|
|
52124
|
+
knobThickness,
|
|
52125
|
+
screwCenterZ,
|
|
52126
|
+
fixedAnvilFaceX: workpieceLeftFaceX - faceClearance,
|
|
52127
|
+
pressurePadFaceX: workpieceRightFaceX + faceClearance,
|
|
52128
|
+
supportInnerFaceX,
|
|
52129
|
+
runningClearance,
|
|
52130
|
+
faceClearance
|
|
52131
|
+
}
|
|
52132
|
+
};
|
|
52133
|
+
}
|
|
50273
52134
|
function fastenerSet(size, boltLength, options) {
|
|
50274
52135
|
const sizeData = METRIC_HOLE_TABLE[size];
|
|
50275
52136
|
if (!sizeData) throw new Error(`fastenerSet: unsupported size "${size}"`);
|
|
@@ -50330,6 +52191,22 @@ const partLibrary = {
|
|
|
50330
52191
|
nut,
|
|
50331
52192
|
washer,
|
|
50332
52193
|
fastenerSet,
|
|
52194
|
+
boltedServiceCover,
|
|
52195
|
+
datumEnclosureAssembly,
|
|
52196
|
+
snapLatchCoverAssembly,
|
|
52197
|
+
pinnedLeverAssembly,
|
|
52198
|
+
retainedShaftAssembly,
|
|
52199
|
+
capturedLinearSlide,
|
|
52200
|
+
capturedCartridgeGuideAssembly,
|
|
52201
|
+
livingHingeCoverAssembly,
|
|
52202
|
+
knuckledHingeAssembly,
|
|
52203
|
+
clevisPinJointAssembly,
|
|
52204
|
+
seatedBearingAssembly,
|
|
52205
|
+
cableGlandAnchorAssembly,
|
|
52206
|
+
hoseBarbPortAssembly,
|
|
52207
|
+
routedTubeClipAssembly,
|
|
52208
|
+
pcbTerminalBlockAssembly,
|
|
52209
|
+
thumbScrewClampAssembly,
|
|
50333
52210
|
pipeRoute,
|
|
50334
52211
|
elbow,
|
|
50335
52212
|
beltDrive,
|
|
@@ -92819,10 +94696,14 @@ function spec(name, checkFn) {
|
|
|
92819
94696
|
};
|
|
92820
94697
|
}
|
|
92821
94698
|
let _collected = [];
|
|
94699
|
+
let _collisionAllowances = [];
|
|
94700
|
+
let _physicalComponentExpectations = [];
|
|
92822
94701
|
let _counter = 0;
|
|
92823
94702
|
let _activeGroup = null;
|
|
92824
94703
|
function resetVerifications() {
|
|
92825
94704
|
_collected = [];
|
|
94705
|
+
_collisionAllowances = [];
|
|
94706
|
+
_physicalComponentExpectations = [];
|
|
92826
94707
|
_counter = 0;
|
|
92827
94708
|
}
|
|
92828
94709
|
function getCollectedVerifications() {
|
|
@@ -92856,15 +94737,35 @@ function push(result) {
|
|
|
92856
94737
|
function roundNum(n, digits = 4) {
|
|
92857
94738
|
return Number.isFinite(n) ? n.toFixed(digits).replace(/\.?0+$/, "") : String(n);
|
|
92858
94739
|
}
|
|
94740
|
+
function meshDerivedManifoldBackend(shape) {
|
|
94741
|
+
const mesh = getShapeRuntimeBackend(shape).getMesh();
|
|
94742
|
+
return reconstructBackendFromMesh({
|
|
94743
|
+
numProp: mesh.numProp,
|
|
94744
|
+
triVerts: mesh.triVerts,
|
|
94745
|
+
vertProperties: mesh.vertProperties,
|
|
94746
|
+
mergeFromVert: mesh.mergeFromVert ?? new Uint32Array(),
|
|
94747
|
+
mergeToVert: mesh.mergeToVert ?? new Uint32Array()
|
|
94748
|
+
});
|
|
94749
|
+
}
|
|
94750
|
+
function backendForMinGap(shape) {
|
|
94751
|
+
const backend = getShapeRuntimeBackend(shape);
|
|
94752
|
+
if (isManifoldCapableBackend(backend)) return { backend, method: "exact", dispose: false };
|
|
94753
|
+
return { backend: meshDerivedManifoldBackend(shape), method: "mesh-derived", dispose: true };
|
|
94754
|
+
}
|
|
92859
94755
|
function computeMinGap(a2, b, searchLength) {
|
|
92860
|
-
const backendA =
|
|
92861
|
-
const backendB =
|
|
92862
|
-
|
|
92863
|
-
|
|
94756
|
+
const backendA = backendForMinGap(a2);
|
|
94757
|
+
const backendB = backendForMinGap(b);
|
|
94758
|
+
try {
|
|
94759
|
+
const manifoldA = requireManifoldShapeBackend(backendA.backend, "verification.minGap");
|
|
94760
|
+
const manifoldB = requireManifoldShapeBackend(backendB.backend, "verification.minGap");
|
|
94761
|
+
return {
|
|
94762
|
+
gap: manifoldA.minGap(manifoldB, searchLength),
|
|
94763
|
+
method: backendA.method === "exact" && backendB.method === "exact" ? "exact" : "mesh-derived"
|
|
94764
|
+
};
|
|
94765
|
+
} finally {
|
|
94766
|
+
if (backendA.dispose) disposeShapeBackend(backendA.backend);
|
|
94767
|
+
if (backendB.dispose) disposeShapeBackend(backendB.backend);
|
|
92864
94768
|
}
|
|
92865
|
-
const manifoldA = backendA.requireManifold("verification.minGap");
|
|
92866
|
-
const manifoldB = requireManifoldShapeBackend(backendB, "verification.minGap");
|
|
92867
|
-
return manifoldA.minGap(manifoldB, searchLength);
|
|
92868
94769
|
}
|
|
92869
94770
|
function vec3Dot(a2, b) {
|
|
92870
94771
|
return a2[0] * b[0] + a2[1] * b[1] + a2[2] * b[2];
|
|
@@ -92999,8 +94900,143 @@ const verify = {
|
|
|
92999
94900
|
actual: `${roundNum(d2, 3)} mm`
|
|
93000
94901
|
});
|
|
93001
94902
|
} catch (e) {
|
|
93002
|
-
push({
|
|
94903
|
+
push({
|
|
94904
|
+
id: nextId(),
|
|
94905
|
+
label,
|
|
94906
|
+
kind: "interface",
|
|
94907
|
+
status: "fail",
|
|
94908
|
+
message: `Error: ${e instanceof Error ? e.message : String(e)}`,
|
|
94909
|
+
line: line2
|
|
94910
|
+
});
|
|
94911
|
+
}
|
|
94912
|
+
},
|
|
94913
|
+
/**
|
|
94914
|
+
* Check the distance between two named connectors on a shape or group.
|
|
94915
|
+
*
|
|
94916
|
+
* Use this when connectors + `matchTo()` define a static assembly interface.
|
|
94917
|
+
* It proves the mate at runtime, unlike a plain source-level connector
|
|
94918
|
+
* declaration. The common case is `expected = 0`, meaning the two connector
|
|
94919
|
+
* origins should coincide after placement.
|
|
94920
|
+
*
|
|
94921
|
+
* **Example**
|
|
94922
|
+
*
|
|
94923
|
+
* ```ts
|
|
94924
|
+
* verify.connectorDistance("leg is seated", bench, "Rail.leg_0", "Leg0.head", 0, 0.01);
|
|
94925
|
+
* ```
|
|
94926
|
+
*/
|
|
94927
|
+
connectorDistance(label, target, connectorA, connectorB, expected = 0, tolerance = 0.01) {
|
|
94928
|
+
const line2 = captureSourceLine();
|
|
94929
|
+
try {
|
|
94930
|
+
const actual = target.connectorDistance(connectorA, connectorB);
|
|
94931
|
+
const diff = Math.abs(actual - expected);
|
|
94932
|
+
const passed = diff <= Math.abs(tolerance);
|
|
94933
|
+
push({
|
|
94934
|
+
id: nextId(),
|
|
94935
|
+
label,
|
|
94936
|
+
kind: "interface",
|
|
94937
|
+
status: passed ? "pass" : "fail",
|
|
94938
|
+
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`,
|
|
94939
|
+
line: passed ? void 0 : line2,
|
|
94940
|
+
expected: `${roundNum(expected, 4)} ± ${roundNum(tolerance, 4)} mm`,
|
|
94941
|
+
actual: `${roundNum(actual, 4)} mm`
|
|
94942
|
+
});
|
|
94943
|
+
} catch (e) {
|
|
94944
|
+
push({
|
|
94945
|
+
id: nextId(),
|
|
94946
|
+
label,
|
|
94947
|
+
kind: "interface",
|
|
94948
|
+
status: "fail",
|
|
94949
|
+
message: `Error: ${e instanceof Error ? e.message : String(e)}`,
|
|
94950
|
+
line: line2
|
|
94951
|
+
});
|
|
94952
|
+
}
|
|
94953
|
+
},
|
|
94954
|
+
/**
|
|
94955
|
+
* Declare the expected physical connectivity component count for the returned visible model.
|
|
94956
|
+
*
|
|
94957
|
+
* **Details**
|
|
94958
|
+
*
|
|
94959
|
+
* Use this for generated mechanical models that should have a clear component graph:
|
|
94960
|
+
* one connected fixture, a purchased part plus a removable cartridge, a root assembly plus
|
|
94961
|
+
* named intentional ghosts, and so on. `forgecad inspect mechanical-integrity` resolves the returned
|
|
94962
|
+
* visible objects with the same physical-connectivity analysis used in the quality gate and
|
|
94963
|
+
* fails if the actual component count differs.
|
|
94964
|
+
*
|
|
94965
|
+
* This catches the common generated-CAD failure where a script returns a visually plausible
|
|
94966
|
+
* artifact but the handle, screw, washer, cover, or terminal block is actually a separate island.
|
|
94967
|
+
*
|
|
94968
|
+
* **Example**
|
|
94969
|
+
*
|
|
94970
|
+
* ```ts
|
|
94971
|
+
* verify.physicalComponentCount("vise is one connected installed assembly", 1);
|
|
94972
|
+
* ```
|
|
94973
|
+
*/
|
|
94974
|
+
physicalComponentCount(label, expected) {
|
|
94975
|
+
const line2 = captureSourceLine();
|
|
94976
|
+
const id = nextId();
|
|
94977
|
+
if (!Number.isInteger(expected) || expected < 0) {
|
|
94978
|
+
push({
|
|
94979
|
+
id,
|
|
94980
|
+
label,
|
|
94981
|
+
kind: "interface",
|
|
94982
|
+
status: "fail",
|
|
94983
|
+
message: "Expected physical component count must be a non-negative integer",
|
|
94984
|
+
line: line2
|
|
94985
|
+
});
|
|
94986
|
+
return;
|
|
94987
|
+
}
|
|
94988
|
+
_physicalComponentExpectations.push({ id, label, expected, line: line2 });
|
|
94989
|
+
push({
|
|
94990
|
+
id,
|
|
94991
|
+
label,
|
|
94992
|
+
kind: "interface",
|
|
94993
|
+
status: "pass",
|
|
94994
|
+
message: `Expected ${expected} physical component(s); checked by mechanical-integrity connectivity`
|
|
94995
|
+
});
|
|
94996
|
+
},
|
|
94997
|
+
/**
|
|
94998
|
+
* Declare that two visible objects intentionally overlap because the overlap is real manufacturing intent.
|
|
94999
|
+
*
|
|
95000
|
+
* **Details**
|
|
95001
|
+
*
|
|
95002
|
+
* Use this only for overlaps that a mechanical reviewer would accept as actual matter sharing volume:
|
|
95003
|
+
* welded/fused regions, overmolded inserts, potted electronics, cast-in hardware, or deliberately
|
|
95004
|
+
* bonded laminations. This is not a shortcut for screws without holes, shafts without bores, covers
|
|
95005
|
+
* without pockets, or parts placed with collision as a positioning hack.
|
|
95006
|
+
*
|
|
95007
|
+
* `forgecad inspect mechanical-integrity --collisions` only honors this declaration when both shapes are
|
|
95008
|
+
* returned as visible objects and the exact collision report finds that same object pair. Unused or
|
|
95009
|
+
* non-visible declarations fail the quality gate so annotations cannot hide unrelated collisions.
|
|
95010
|
+
*
|
|
95011
|
+
* **Example**
|
|
95012
|
+
*
|
|
95013
|
+
* ```ts
|
|
95014
|
+
* verify.intentionalOverlap("rubber grip is overmolded on handle", rubberGrip, handleCore, "overmolded insert");
|
|
95015
|
+
* ```
|
|
95016
|
+
*/
|
|
95017
|
+
intentionalOverlap(label, a2, b, reason) {
|
|
95018
|
+
const line2 = captureSourceLine();
|
|
95019
|
+
const id = nextId();
|
|
95020
|
+
const trimmedReason = String(reason ?? "").trim();
|
|
95021
|
+
if (trimmedReason.length === 0) {
|
|
95022
|
+
push({
|
|
95023
|
+
id,
|
|
95024
|
+
label,
|
|
95025
|
+
kind: "interface",
|
|
95026
|
+
status: "fail",
|
|
95027
|
+
message: "Intentional overlap requires a manufacturing reason",
|
|
95028
|
+
line: line2
|
|
95029
|
+
});
|
|
95030
|
+
return;
|
|
93003
95031
|
}
|
|
95032
|
+
_collisionAllowances.push({ id, label, reason: trimmedReason, a: a2, b, line: line2 });
|
|
95033
|
+
push({
|
|
95034
|
+
id,
|
|
95035
|
+
label,
|
|
95036
|
+
kind: "interface",
|
|
95037
|
+
status: "pass",
|
|
95038
|
+
message: `Intentional overlap declared: ${trimmedReason}`
|
|
95039
|
+
});
|
|
93004
95040
|
},
|
|
93005
95041
|
/**
|
|
93006
95042
|
* Check that two shapes do not collide (minGap > 0).
|
|
@@ -93010,19 +95046,28 @@ const verify = {
|
|
|
93010
95046
|
notColliding(label, a2, b, searchLength = 1) {
|
|
93011
95047
|
const line2 = captureSourceLine();
|
|
93012
95048
|
try {
|
|
93013
|
-
const gap = computeMinGap(a2, b, searchLength);
|
|
95049
|
+
const { gap, method } = computeMinGap(a2, b, searchLength);
|
|
95050
|
+
const methodLabel = method === "exact" ? "exact min gap" : "mesh-derived min gap";
|
|
93014
95051
|
const passed = gap > 0;
|
|
93015
95052
|
push({
|
|
93016
95053
|
id: nextId(),
|
|
93017
95054
|
label,
|
|
95055
|
+
kind: "interface",
|
|
93018
95056
|
status: passed ? "pass" : "fail",
|
|
93019
|
-
message: passed ? `No collision (
|
|
95057
|
+
message: passed ? `No collision (${methodLabel} ${roundNum(gap, 3)} mm)` : `Shapes are colliding (${methodLabel} ${roundNum(gap, 3)} mm ≤ 0)`,
|
|
93020
95058
|
line: passed ? void 0 : line2,
|
|
93021
95059
|
expected: "> 0 mm",
|
|
93022
95060
|
actual: `${roundNum(gap, 3)} mm`
|
|
93023
95061
|
});
|
|
93024
95062
|
} catch (e) {
|
|
93025
|
-
push({
|
|
95063
|
+
push({
|
|
95064
|
+
id: nextId(),
|
|
95065
|
+
label,
|
|
95066
|
+
kind: "interface",
|
|
95067
|
+
status: "fail",
|
|
95068
|
+
message: `Error: ${e instanceof Error ? e.message : String(e)}`,
|
|
95069
|
+
line: line2
|
|
95070
|
+
});
|
|
93026
95071
|
}
|
|
93027
95072
|
},
|
|
93028
95073
|
/**
|
|
@@ -93031,13 +95076,15 @@ const verify = {
|
|
|
93031
95076
|
minClearance(label, a2, b, minGap, searchLength = 10) {
|
|
93032
95077
|
const line2 = captureSourceLine();
|
|
93033
95078
|
try {
|
|
93034
|
-
const gap = computeMinGap(a2, b, searchLength);
|
|
95079
|
+
const { gap, method } = computeMinGap(a2, b, searchLength);
|
|
95080
|
+
const methodLabel = method === "exact" ? "exact gap" : "mesh-derived gap";
|
|
93035
95081
|
const passed = gap >= minGap;
|
|
93036
95082
|
push({
|
|
93037
95083
|
id: nextId(),
|
|
93038
95084
|
label,
|
|
95085
|
+
kind: "interface",
|
|
93039
95086
|
status: passed ? "pass" : "fail",
|
|
93040
|
-
message: passed ?
|
|
95087
|
+
message: passed ? `${methodLabel} ${roundNum(gap, 3)} mm ≥ ${roundNum(minGap, 3)} mm` : `${methodLabel} ${roundNum(gap, 3)} mm < required ${roundNum(minGap, 3)} mm`,
|
|
93041
95088
|
line: passed ? void 0 : line2,
|
|
93042
95089
|
expected: `≥ ${roundNum(minGap, 3)} mm`,
|
|
93043
95090
|
actual: `${roundNum(gap, 3)} mm`
|
|
@@ -93046,6 +95093,90 @@ const verify = {
|
|
|
93046
95093
|
push({ id: nextId(), label, status: "fail", message: `Error: ${e instanceof Error ? e.message : String(e)}`, line: line2 });
|
|
93047
95094
|
}
|
|
93048
95095
|
},
|
|
95096
|
+
/**
|
|
95097
|
+
* Check that the clearance gap between two shapes is inside an allowed range.
|
|
95098
|
+
*
|
|
95099
|
+
* **Details**
|
|
95100
|
+
*
|
|
95101
|
+
* Use this for seated and retained interfaces where a part must be close
|
|
95102
|
+
* enough to be mechanically accountable, but must not collide beyond the
|
|
95103
|
+
* allowed minimum. It catches both failure modes that make generated CAD look
|
|
95104
|
+
* fake: parts floating away from their receiver, and parts intersecting their
|
|
95105
|
+
* receiver because the pocket, bore, or running clearance was not modeled.
|
|
95106
|
+
*
|
|
95107
|
+
* For contact, use a narrow range such as `[-0.01, 0.05]` to tolerate tiny
|
|
95108
|
+
* numerical noise. For a running fit, use the intended clearance band.
|
|
95109
|
+
*
|
|
95110
|
+
* Manifold-backed shapes use exact min-gap distance. Other backends use a
|
|
95111
|
+
* mesh-derived min-gap check and say so in the verification message; keep
|
|
95112
|
+
* `forgecad inspect mechanical-integrity --collisions` in the acceptance gate for
|
|
95113
|
+
* positive-volume interference.
|
|
95114
|
+
*
|
|
95115
|
+
* **Example**
|
|
95116
|
+
*
|
|
95117
|
+
* ```ts
|
|
95118
|
+
* verify.clearanceBetween("cover is seated on gasket", cover, gasket, -0.01, 0.05);
|
|
95119
|
+
* verify.clearanceBetween("carriage runs inside rail", carriage, rail, 0.2, 0.5);
|
|
95120
|
+
* ```
|
|
95121
|
+
*/
|
|
95122
|
+
clearanceBetween(label, a2, b, minGap, maxGap, searchLength) {
|
|
95123
|
+
const line2 = captureSourceLine();
|
|
95124
|
+
try {
|
|
95125
|
+
if (!Number.isFinite(minGap) || !Number.isFinite(maxGap)) {
|
|
95126
|
+
push({
|
|
95127
|
+
id: nextId(),
|
|
95128
|
+
label,
|
|
95129
|
+
kind: "interface",
|
|
95130
|
+
status: "fail",
|
|
95131
|
+
message: "Clearance range must use finite numbers",
|
|
95132
|
+
line: line2
|
|
95133
|
+
});
|
|
95134
|
+
return;
|
|
95135
|
+
}
|
|
95136
|
+
if (maxGap < minGap) {
|
|
95137
|
+
push({
|
|
95138
|
+
id: nextId(),
|
|
95139
|
+
label,
|
|
95140
|
+
kind: "interface",
|
|
95141
|
+
status: "fail",
|
|
95142
|
+
message: `Clearance max ${roundNum(maxGap, 3)} mm is smaller than min ${roundNum(minGap, 3)} mm`,
|
|
95143
|
+
line: line2
|
|
95144
|
+
});
|
|
95145
|
+
return;
|
|
95146
|
+
}
|
|
95147
|
+
const search = searchLength ?? Math.max(10, Math.abs(maxGap) * 2 + 1);
|
|
95148
|
+
const { gap, method } = computeMinGap(a2, b, search);
|
|
95149
|
+
const methodLabel = method === "exact" ? "exact gap" : "mesh-derived gap";
|
|
95150
|
+
const passed = gap >= minGap && gap <= maxGap;
|
|
95151
|
+
let message;
|
|
95152
|
+
if (passed) {
|
|
95153
|
+
message = `${methodLabel} ${roundNum(gap, 3)} mm in [${roundNum(minGap, 3)}, ${roundNum(maxGap, 3)}] mm`;
|
|
95154
|
+
} else if (gap < minGap) {
|
|
95155
|
+
message = `${methodLabel} ${roundNum(gap, 3)} mm < allowed minimum ${roundNum(minGap, 3)} mm`;
|
|
95156
|
+
} else {
|
|
95157
|
+
message = `${methodLabel} ${roundNum(gap, 3)} mm > allowed maximum ${roundNum(maxGap, 3)} mm`;
|
|
95158
|
+
}
|
|
95159
|
+
push({
|
|
95160
|
+
id: nextId(),
|
|
95161
|
+
label,
|
|
95162
|
+
kind: "interface",
|
|
95163
|
+
status: passed ? "pass" : "fail",
|
|
95164
|
+
message,
|
|
95165
|
+
line: passed ? void 0 : line2,
|
|
95166
|
+
expected: `[${roundNum(minGap, 3)}, ${roundNum(maxGap, 3)}] mm`,
|
|
95167
|
+
actual: `${roundNum(gap, 3)} mm`
|
|
95168
|
+
});
|
|
95169
|
+
} catch (e) {
|
|
95170
|
+
push({
|
|
95171
|
+
id: nextId(),
|
|
95172
|
+
label,
|
|
95173
|
+
kind: "interface",
|
|
95174
|
+
status: "fail",
|
|
95175
|
+
message: `Error: ${e instanceof Error ? e.message : String(e)}`,
|
|
95176
|
+
line: line2
|
|
95177
|
+
});
|
|
95178
|
+
}
|
|
95179
|
+
},
|
|
93049
95180
|
/**
|
|
93050
95181
|
* Check that two face normals are parallel (within toleranceDeg degrees).
|
|
93051
95182
|
*/
|