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