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.
Files changed (76) hide show
  1. package/dist/assets/{AdminPage-Da6hhpJx.js → AdminPage-DX0mpSZT.js} +1 -1
  2. package/dist/assets/{BlogPage-Bl_sKeWb.js → BlogPage-CI_P0_Pf.js} +1 -1
  3. package/dist/assets/{DocsPage-Blz3Tp4j.js → DocsPage-DLhIIZyJ.js} +3 -3
  4. package/dist/assets/{EditorApp-CuiPbtn5.js → EditorApp-BujZvuwX.js} +140 -20
  5. package/dist/assets/{EditorApp-DS0AIUrZ.css → EditorApp-DfFT2Dn8.css} +1 -0
  6. package/dist/assets/{EmbedViewer-BFG6-Ufm.js → EmbedViewer-0S0qXKog.js} +2 -2
  7. package/dist/assets/{LandingPageProofDriven-DB9fQd5P.js → LandingPageProofDriven-O_yMtAri.js} +1 -1
  8. package/dist/assets/{PricingPage-BMxYT_F0.js → PricingPage-DGkX3Ahr.js} +1 -1
  9. package/dist/assets/{SettingsPage-VVQNrCAg.js → SettingsPage-DBsqTB_y.js} +82 -22
  10. package/dist/assets/{app-Dl9ymBWC.js → app-BE2nD6Yz.js} +1056 -258
  11. package/dist/assets/cli/{render-CFtwKCCY.js → render-iP9qh475.js} +1533 -207
  12. package/dist/assets/{evalWorker-CRvbzTXm.js → evalWorker-Ds5U4xtN.js} +2178 -30
  13. package/dist/assets/inspectWorker-Dll4eVyD.js +12620 -0
  14. package/dist/assets/{manifold-DpBXFS2K.js → manifold-Bk26ViCr.js} +1 -1
  15. package/dist/assets/{manifold-DzZ4VRPs.js → manifold-DjYsd7A_.js} +2 -2
  16. package/dist/assets/{manifold-B9QSr-qP.js → manifold-sJ-axdXM.js} +1 -1
  17. package/dist/assets/{renderSceneState-BuAXF2jh.js → renderSceneState-Bngp5MrQ.js} +1 -1
  18. package/dist/assets/{reportWorker-BNWEnRg1.js → reportWorker-CU8RZ4O0.js} +2161 -30
  19. package/dist/assets/{distance-BEC2RjJi.js → sectionPlaneMath-BdTjyVfs.js} +2539 -1187
  20. package/dist/cli/render.html +1 -1
  21. package/dist/docs/index.html +1 -1
  22. package/dist/docs-raw/AI/usage.md +7 -2
  23. package/dist/docs-raw/CLI.md +82 -53
  24. package/dist/docs-raw/beta-operations.md +5 -0
  25. package/dist/docs-raw/coding.md +1 -1
  26. package/dist/docs-raw/generated/concepts.md +59 -2
  27. package/dist/docs-raw/generated/core.md +206 -1
  28. package/dist/docs-raw/generated/lib.md +17 -1
  29. package/dist/docs-raw/generated/viewport.md +1 -1
  30. package/dist/docs-raw/guides/inspection-bundles.md +36 -13
  31. package/dist/docs-raw/platform/auth.md +2 -0
  32. package/dist/docs-raw/platform/google-oauth-setup.md +4 -0
  33. package/dist/docs-raw/skills/forgecad-make-a-model.md +87 -8
  34. package/dist/docs-raw/skills/forgecad-prepare-prompt.md +14 -6
  35. package/dist/docs-raw/skills/forgecad-render-inspect.md +1 -1
  36. package/dist/docs-raw/skills/index.md +2 -2
  37. package/dist/index.html +1 -1
  38. package/dist/sitemap.xml +6 -6
  39. package/dist-cli/forgecad.js +7975 -4528
  40. package/dist-cli/forgecad.js.map +1 -1
  41. package/dist-skill/CONTEXT.md +260 -16
  42. package/dist-skill/docs/CLI.md +82 -53
  43. package/dist-skill/docs/generated/core.md +206 -1
  44. package/dist-skill/docs/generated/lib.md +17 -1
  45. package/dist-skill/docs/generated/viewport.md +1 -1
  46. package/dist-skill/docs/guides/inspection-bundles.md +36 -13
  47. package/dist-skill/docs-dev/CLI.md +82 -53
  48. package/dist-skill/docs-dev/coding.md +1 -1
  49. package/dist-skill/docs-dev/generated/core.md +206 -1
  50. package/dist-skill/docs-dev/generated/lib.md +17 -1
  51. package/dist-skill/docs-dev/generated/viewport.md +1 -1
  52. package/dist-skill/docs-dev/guides/inspection-bundles.md +36 -13
  53. package/dist-skill/library/forgecad-make-a-model/SKILL.md +87 -8
  54. package/dist-skill/library/forgecad-prepare-prompt/SKILL.md +14 -6
  55. package/dist-skill/library/forgecad-prepare-prompt/references/default-profiles.md +5 -3
  56. package/dist-skill/library/forgecad-prepare-prompt/references/master-prompt.md +7 -5
  57. package/dist-skill/library/forgecad-render-inspect/SKILL.md +1 -1
  58. package/examples/api/bolted-service-cover.forge.js +17 -0
  59. package/examples/api/cable-gland-anchor.forge.js +14 -0
  60. package/examples/api/captured-cartridge-guide.forge.js +14 -0
  61. package/examples/api/captured-linear-slide.forge.js +13 -0
  62. package/examples/api/clevis-pin-joint.forge.js +13 -0
  63. package/examples/api/datum-enclosure.forge.js +16 -0
  64. package/examples/api/hose-barb-port.forge.js +14 -0
  65. package/examples/api/intentional-overlap-overmold.forge.js +16 -0
  66. package/examples/api/knuckled-hinge-assembly.forge.js +15 -0
  67. package/examples/api/living-hinge-cover.forge.js +14 -0
  68. package/examples/api/pcb-terminal-block.forge.js +22 -0
  69. package/examples/api/pinned-lever-pivot-stack.forge.js +14 -0
  70. package/examples/api/retained-shaft-knob-stack.forge.js +15 -0
  71. package/examples/api/routed-tube-clip.forge.js +15 -0
  72. package/examples/api/seated-bearing-stack.forge.js +30 -0
  73. package/examples/api/snap-latch-cover.forge.js +14 -0
  74. package/examples/api/static-assembly-connectors.forge.js +14 -16
  75. package/examples/api/thumb-screw-clamp.forge.js +15 -0
  76. 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-DpBXFS2K.js")).default;
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$7(scope, name, value) {
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$7("gearBodyDisk", "outerRadius", options.outerRadius);
48835
- requirePositive$7("gearBodyDisk", "faceWidth", options.faceWidth);
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$7("gearBodyDiskWithHub", "hubDiameter", options.hubDiameter);
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$7("gearBodyDiskWithHub", "hubFaceWidth", hubFaceWidth);
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$7("gearBodySpoked", "outerRadius", options.outerRadius);
48860
- requirePositive$7("gearBodySpoked", "faceWidth", options.faceWidth);
48861
- requirePositive$7("gearBodySpoked", "rimWidth", options.rimWidth);
48862
- requirePositive$7("gearBodySpoked", "hubDiameter", options.hubDiameter);
48863
- requirePositive$7("gearBodySpoked", "spokeWidth", options.spokeWidth);
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$7("gearBodyFromProfile", "faceWidth", options.faceWidth);
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$6(scope, name, value) {
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$6(scope, "outerRadius", options.outerRadius);
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$6("driveWheel", "faceWidth", options.faceWidth);
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$6(scope, "outerRadius", options.outerRadius);
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$6(scope, "faceWidth", faceWidth);
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 = getShapeRuntimeBackend(a2);
92861
- const backendB = getShapeRuntimeBackend(b);
92862
- if (!isManifoldCapableBackend(backendA)) {
92863
- throw new Error("notColliding/minClearance require Manifold-backed shapes");
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({ id: nextId(), label, status: "fail", message: `Error: ${e instanceof Error ? e.message : String(e)}`, line: line2 });
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 (min gap ${roundNum(gap, 3)} mm)` : `Shapes are colliding (min gap ${roundNum(gap, 3)} mm ≤ 0)`,
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({ id: nextId(), label, status: "fail", message: `Error: ${e instanceof Error ? e.message : String(e)}`, line: line2 });
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 ? `Gap ${roundNum(gap, 3)} mm ≥ ${roundNum(minGap, 3)} mm` : `Gap ${roundNum(gap, 3)} mm < required ${roundNum(minGap, 3)} mm`,
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
  */