forgecad 0.9.5 → 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 (86) hide show
  1. package/dist/assets/{AdminPage-uTtcSXtn.js → AdminPage-DX0mpSZT.js} +1 -1
  2. package/dist/assets/{BlogPage-DYJMjWx3.js → BlogPage-CI_P0_Pf.js} +1 -1
  3. package/dist/assets/{DocsPage-C58f0K5v.js → DocsPage-DLhIIZyJ.js} +3 -3
  4. package/dist/assets/EditorApp-BujZvuwX.js +12874 -0
  5. package/dist/assets/{EditorApp-DS0AIUrZ.css → EditorApp-DfFT2Dn8.css} +1 -0
  6. package/dist/assets/{EmbedViewer-CMXWA2LX.js → EmbedViewer-0S0qXKog.js} +2 -2
  7. package/dist/assets/{LandingPageProofDriven-CAu2OZFn.js → LandingPageProofDriven-O_yMtAri.js} +1 -1
  8. package/dist/assets/{PricingPage-BIgW7m3X.js → PricingPage-DGkX3Ahr.js} +1 -1
  9. package/dist/assets/{SettingsPage-N1l1tMXO.js → SettingsPage-DBsqTB_y.js} +82 -22
  10. package/dist/assets/{app-CFy7g5WP.js → app-BE2nD6Yz.js} +1246 -191
  11. package/dist/assets/cli/{render-BrVVdj_T.js → render-iP9qh475.js} +841 -586
  12. package/dist/assets/{evalWorker-c_SB9gg3.js → evalWorker-Ds5U4xtN.js} +2732 -112
  13. package/dist/assets/inspectWorker-Dll4eVyD.js +12620 -0
  14. package/dist/assets/{manifold-Dp6pvFr6.js → manifold-Bk26ViCr.js} +1 -1
  15. package/dist/assets/{manifold-CRoBhJKH.js → manifold-DjYsd7A_.js} +2 -2
  16. package/dist/assets/{manifold-Cjk7WhRs.js → manifold-sJ-axdXM.js} +1 -1
  17. package/dist/assets/{renderSceneState-3DfsSASX.js → renderSceneState-Bngp5MrQ.js} +1 -1
  18. package/dist/assets/{reportWorker-BLkuIoS8.js → reportWorker-CU8RZ4O0.js} +2715 -112
  19. package/dist/assets/{sectionPlaneMath-CykEnkvQ.js → sectionPlaneMath-BdTjyVfs.js} +3213 -252
  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 +9 -0
  25. package/dist/docs-raw/coding.md +1 -1
  26. package/dist/docs-raw/deployment.md +38 -23
  27. package/dist/docs-raw/generated/concepts.md +141 -7
  28. package/dist/docs-raw/generated/core.md +206 -1
  29. package/dist/docs-raw/generated/curves.md +97 -5
  30. package/dist/docs-raw/generated/lib.md +17 -1
  31. package/dist/docs-raw/generated/sketch.md +9 -1
  32. package/dist/docs-raw/generated/viewport.md +1 -1
  33. package/dist/docs-raw/guides/inspection-bundles.md +45 -16
  34. package/dist/docs-raw/platform/auth.md +2 -0
  35. package/dist/docs-raw/platform/google-oauth-setup.md +4 -0
  36. package/dist/docs-raw/runbook.md +3 -3
  37. package/dist/docs-raw/skills/forgecad-make-a-model.md +87 -8
  38. package/dist/docs-raw/skills/forgecad-prepare-prompt.md +14 -6
  39. package/dist/docs-raw/skills/forgecad-render-inspect.md +1 -1
  40. package/dist/docs-raw/skills/index.md +2 -2
  41. package/dist/index.html +1 -1
  42. package/dist/sitemap.xml +6 -6
  43. package/dist-cli/forgecad.js +8725 -4747
  44. package/dist-cli/forgecad.js.map +1 -1
  45. package/dist-skill/CONTEXT.md +375 -25
  46. package/dist-skill/docs/CLI.md +82 -53
  47. package/dist-skill/docs/generated/core.md +206 -1
  48. package/dist-skill/docs/generated/curves.md +97 -5
  49. package/dist-skill/docs/generated/lib.md +17 -1
  50. package/dist-skill/docs/generated/sketch.md +9 -1
  51. package/dist-skill/docs/generated/viewport.md +1 -1
  52. package/dist-skill/docs/guides/inspection-bundles.md +45 -16
  53. package/dist-skill/docs-dev/CLI.md +82 -53
  54. package/dist-skill/docs-dev/coding.md +1 -1
  55. package/dist-skill/docs-dev/generated/core.md +206 -1
  56. package/dist-skill/docs-dev/generated/curves.md +97 -5
  57. package/dist-skill/docs-dev/generated/lib.md +17 -1
  58. package/dist-skill/docs-dev/generated/sketch.md +9 -1
  59. package/dist-skill/docs-dev/generated/viewport.md +1 -1
  60. package/dist-skill/docs-dev/guides/inspection-bundles.md +45 -16
  61. package/dist-skill/library/forgecad-make-a-model/SKILL.md +87 -8
  62. package/dist-skill/library/forgecad-prepare-prompt/SKILL.md +14 -6
  63. package/dist-skill/library/forgecad-prepare-prompt/references/default-profiles.md +5 -3
  64. package/dist-skill/library/forgecad-prepare-prompt/references/master-prompt.md +7 -5
  65. package/dist-skill/library/forgecad-render-inspect/SKILL.md +1 -1
  66. package/examples/api/bolted-service-cover.forge.js +17 -0
  67. package/examples/api/cable-gland-anchor.forge.js +14 -0
  68. package/examples/api/captured-cartridge-guide.forge.js +14 -0
  69. package/examples/api/captured-linear-slide.forge.js +13 -0
  70. package/examples/api/clevis-pin-joint.forge.js +13 -0
  71. package/examples/api/datum-enclosure.forge.js +16 -0
  72. package/examples/api/guided-loft-olive-oil-bottle.forge.js +135 -0
  73. package/examples/api/hose-barb-port.forge.js +14 -0
  74. package/examples/api/intentional-overlap-overmold.forge.js +16 -0
  75. package/examples/api/knuckled-hinge-assembly.forge.js +15 -0
  76. package/examples/api/living-hinge-cover.forge.js +14 -0
  77. package/examples/api/pcb-terminal-block.forge.js +22 -0
  78. package/examples/api/pinned-lever-pivot-stack.forge.js +14 -0
  79. package/examples/api/retained-shaft-knob-stack.forge.js +15 -0
  80. package/examples/api/routed-tube-clip.forge.js +15 -0
  81. package/examples/api/seated-bearing-stack.forge.js +30 -0
  82. package/examples/api/snap-latch-cover.forge.js +14 -0
  83. package/examples/api/static-assembly-connectors.forge.js +14 -16
  84. package/examples/api/thumb-screw-clamp.forge.js +15 -0
  85. package/package.json +20 -2
  86. package/dist/assets/EditorApp-DNH1TEz1.js +0 -12729
@@ -721,7 +721,7 @@ function cloneSdfNode(node) {
721
721
  }
722
722
  }
723
723
  const SHEET_METAL_EDGES = ["top", "right", "bottom", "left"];
724
- const EPS$c = 1e-9;
724
+ const EPS$d = 1e-9;
725
725
  function isFinitePositive$3(value) {
726
726
  return Number.isFinite(value) && value > 0;
727
727
  }
@@ -762,7 +762,7 @@ function edgeDisplayName(edge) {
762
762
  return `sheetMetal().flange("${edge}", ...)`;
763
763
  }
764
764
  function normalizeAngle(angleDeg) {
765
- return Math.abs(angleDeg) <= EPS$c ? 0 : angleDeg;
765
+ return Math.abs(angleDeg) <= EPS$d ? 0 : angleDeg;
766
766
  }
767
767
  function validateSheetMetalModel(model) {
768
768
  if (!isFinitePositive$3(model.panel.width) || !isFinitePositive$3(model.panel.height)) {
@@ -774,7 +774,7 @@ function validateSheetMetalModel(model) {
774
774
  if (!isFiniteNonNegative(model.bendRadius)) {
775
775
  return "sheetMetal() requires a finite non-negative bendRadius.";
776
776
  }
777
- if (model.bendRadius <= EPS$c) {
777
+ if (model.bendRadius <= EPS$d) {
778
778
  return "sheetMetal() v1 requires a positive bendRadius so the bend region stays explicit instead of collapsing into a sharp fold.";
779
779
  }
780
780
  if (model.bendAllowance.kind !== "k-factor") {
@@ -836,7 +836,7 @@ function deriveSheetMetalModel(model) {
836
836
  const trimEnd = flanges.has(adjacent.end) ? model.cornerRelief.size : 0;
837
837
  const fullLength = edge === "top" || edge === "bottom" ? model.panel.width : model.panel.height;
838
838
  const span = fullLength - trimStart - trimEnd;
839
- if (!(span > EPS$c)) {
839
+ if (!(span > EPS$d)) {
840
840
  throw new Error(
841
841
  `${edgeDisplayName(edge)} loses all usable span after applying the defended rectangular corner relief size ${model.cornerRelief.size}.`
842
842
  );
@@ -894,7 +894,7 @@ function transformPlacement(origin, u2, v, normal) {
894
894
  };
895
895
  }
896
896
  function translatePlan(plan, x2, y2, z2) {
897
- if (Math.abs(x2) <= EPS$c && Math.abs(y2) <= EPS$c && Math.abs(z2) <= EPS$c) return cloneShapeCompilePlan(plan);
897
+ if (Math.abs(x2) <= EPS$d && Math.abs(y2) <= EPS$d && Math.abs(z2) <= EPS$d) return cloneShapeCompilePlan(plan);
898
898
  return appendShapeCompileTransform(cloneShapeCompilePlan(plan), {
899
899
  kind: "translate",
900
900
  x: x2,
@@ -1338,7 +1338,7 @@ function cloneShapeWorkplanePlacement(placement) {
1338
1338
  placement: cloneSketchPlacementModel(placement.placement)
1339
1339
  };
1340
1340
  }
1341
- const EPS$b = 1e-10;
1341
+ const EPS$c = 1e-10;
1342
1342
  function subVec3(a2, b) {
1343
1343
  return [a2[0] - b[0], a2[1] - b[1], a2[2] - b[2]];
1344
1344
  }
@@ -1364,7 +1364,7 @@ function projectRadial(v, axis) {
1364
1364
  function signedAngleAroundAxis(from, to, axis) {
1365
1365
  const fromLen = lengthVec3$1(from);
1366
1366
  const toLen = lengthVec3$1(to);
1367
- if (fromLen < EPS$b || toLen < EPS$b) return 0;
1367
+ if (fromLen < EPS$c || toLen < EPS$c) return 0;
1368
1368
  const fn = scaleVec3(from, 1 / fromLen);
1369
1369
  const tn = scaleVec3(to, 1 / toLen);
1370
1370
  const sin2 = dotVec3$4(axis, crossVec3$2(fn, tn));
@@ -1385,19 +1385,19 @@ function solveRotateAroundAngle(axis, pivot, movingPoint, targetPoint, options =
1385
1385
  const targetDecomp = projectRadial(target, unitAxis);
1386
1386
  const movingRadialLen = lengthVec3$1(movingDecomp.radial);
1387
1387
  const targetRadialLen = lengthVec3$1(targetDecomp.radial);
1388
- if (movingRadialLen < EPS$b) {
1389
- if (mode === "line" && targetRadialLen >= EPS$b) {
1388
+ if (movingRadialLen < EPS$c) {
1389
+ if (mode === "line" && targetRadialLen >= EPS$c) {
1390
1390
  throw new Error("rotateAroundTo(...): moving point lies on the rotation axis, so line alignment is impossible");
1391
1391
  }
1392
1392
  return 0;
1393
1393
  }
1394
1394
  if (mode === "plane") {
1395
- if (targetRadialLen < EPS$b) {
1395
+ if (targetRadialLen < EPS$c) {
1396
1396
  throw new Error("rotateAroundTo(...): target point lies on the rotation axis, so the target plane is undefined");
1397
1397
  }
1398
1398
  return signedAngleAroundAxis(movingDecomp.radial, targetDecomp.radial, unitAxis);
1399
1399
  }
1400
- if (targetRadialLen < EPS$b) {
1400
+ if (targetRadialLen < EPS$c) {
1401
1401
  throw new Error("rotateAroundTo(...): target line lies on the rotation axis, but the moving point does not");
1402
1402
  }
1403
1403
  const axialTol = 1e-8 * Math.max(1, Math.abs(movingDecomp.axial), Math.abs(targetDecomp.axial));
@@ -1437,7 +1437,7 @@ function multiplyMat4(a2, b) {
1437
1437
  }
1438
1438
  function normalizeVec3$5(v) {
1439
1439
  const len2 = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
1440
- if (len2 < EPS$b) throw new Error("Axis must be non-zero");
1440
+ if (len2 < EPS$c) throw new Error("Axis must be non-zero");
1441
1441
  return [v[0] / len2, v[1] / len2, v[2] / len2];
1442
1442
  }
1443
1443
  function transformPoint$1(m2, p2, w2) {
@@ -1467,7 +1467,7 @@ function invertMat4(m2) {
1467
1467
  const b10 = a21 * a33 - a23 * a31;
1468
1468
  const b11 = a22 * a33 - a23 * a32;
1469
1469
  const det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
1470
- if (Math.abs(det) < EPS$b) throw new Error("Transform matrix is not invertible");
1470
+ if (Math.abs(det) < EPS$c) throw new Error("Transform matrix is not invertible");
1471
1471
  const invDet = 1 / det;
1472
1472
  out[0] = (a11 * b11 - a12 * b10 + a13 * b09) * invDet;
1473
1473
  out[1] = (a02 * b10 - a01 * b11 - a03 * b09) * invDet;
@@ -3224,14 +3224,14 @@ function sweepPathToPolylineAdaptive(path2, baseSamples = 48) {
3224
3224
  pts.push(evalPathAt(path2, 1));
3225
3225
  return pts;
3226
3226
  }
3227
- const EPS$a = 1e-8;
3227
+ const EPS$b = 1e-8;
3228
3228
  const SUPPORTED_VERTICAL_EDGE_NAMES = ["vert-bl", "vert-br", "vert-tr", "vert-tl"];
3229
3229
  function midpoint$4(start, end) {
3230
3230
  return [(start[0] + end[0]) * 0.5, (start[1] + end[1]) * 0.5, (start[2] + end[2]) * 0.5];
3231
3231
  }
3232
3232
  function normalize$7(v) {
3233
3233
  const len2 = Math.hypot(v[0], v[1], v[2]);
3234
- if (len2 <= EPS$a) throw new Error("Edge feature selection requires a non-zero direction vector");
3234
+ if (len2 <= EPS$b) throw new Error("Edge feature selection requires a non-zero direction vector");
3235
3235
  return [v[0] / len2, v[1] / len2, v[2] / len2];
3236
3236
  }
3237
3237
  function subtract(a2, b) {
@@ -3313,7 +3313,7 @@ function rigidTransformForEdgeStep(step) {
3313
3313
  case "mirror": {
3314
3314
  const [nx0, ny0, nz0] = [step.normalX, step.normalY, step.normalZ];
3315
3315
  const len2 = Math.hypot(nx0, ny0, nz0);
3316
- if (len2 <= EPS$a) return Transform.identity();
3316
+ if (len2 <= EPS$b) return Transform.identity();
3317
3317
  const nx = nx0 / len2;
3318
3318
  const ny = ny0 / len2;
3319
3319
  const nz = nz0 / len2;
@@ -3624,7 +3624,7 @@ function isRectangleProfile(points) {
3624
3624
  return [next[0] - point2[0], next[1] - point2[1]];
3625
3625
  });
3626
3626
  const lengths2 = vectors.map(([x2, y2]) => Math.hypot(x2, y2));
3627
- if (lengths2.some((length4) => length4 <= EPS$a)) return false;
3627
+ if (lengths2.some((length4) => length4 <= EPS$b)) return false;
3628
3628
  const dot01 = vectors[0][0] * vectors[1][0] + vectors[0][1] * vectors[1][1];
3629
3629
  const dot12 = vectors[1][0] * vectors[2][0] + vectors[1][1] * vectors[2][1];
3630
3630
  const dot23 = vectors[2][0] * vectors[3][0] + vectors[2][1] * vectors[3][1];
@@ -5691,13 +5691,13 @@ function parseMeshFile(data, format) {
5691
5691
  return parse3mf(data);
5692
5692
  }
5693
5693
  }
5694
- const EPS$9 = 1e-8;
5694
+ const EPS$a = 1e-8;
5695
5695
  function length$3(v) {
5696
5696
  return Math.hypot(v[0], v[1], v[2]);
5697
5697
  }
5698
5698
  function normalize$6(v) {
5699
5699
  const len2 = length$3(v);
5700
- if (len2 < EPS$9) throw new Error("Plane normal must be non-zero");
5700
+ if (len2 < EPS$a) throw new Error("Plane normal must be non-zero");
5701
5701
  return [v[0] / len2, v[1] / len2, v[2] / len2];
5702
5702
  }
5703
5703
  function resolvePlaneOriginNormal(plane) {
@@ -5719,12 +5719,12 @@ function resolvePlaneOriginNormal(plane) {
5719
5719
  function rotationToPlaneSpace(normal) {
5720
5720
  const n = normalize$6(normal);
5721
5721
  const dot2 = n[2];
5722
- if (dot2 > 1 - EPS$9) {
5722
+ if (dot2 > 1 - EPS$a) {
5723
5723
  return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
5724
5724
  }
5725
5725
  let axis;
5726
5726
  let angle;
5727
- if (dot2 < -1 + EPS$9) {
5727
+ if (dot2 < -1 + EPS$a) {
5728
5728
  axis = [1, 0, 0];
5729
5729
  angle = Math.PI;
5730
5730
  } else {
@@ -8286,7 +8286,7 @@ function scale$6(v, s) {
8286
8286
  function sub$7(a2, b) {
8287
8287
  return [a2[0] - b[0], a2[1] - b[1], a2[2] - b[2]];
8288
8288
  }
8289
- function cross$7(a2, b) {
8289
+ function cross$8(a2, b) {
8290
8290
  return [a2[1] * b[2] - a2[2] * b[1], a2[2] * b[0] - a2[0] * b[2], a2[0] * b[1] - a2[1] * b[0]];
8291
8291
  }
8292
8292
  function makeEdge(name, start, end, faceName, curve) {
@@ -8322,7 +8322,7 @@ function buildSurfaceSheetTopology(boundaries, options = {}) {
8322
8322
  const center = options.center ?? average$1(corners);
8323
8323
  const uAxis = normalizeAxis$1(sub$7(midpoint$3(u1Start, u1End), midpoint$3(u0Start, u0End)));
8324
8324
  const vAxis = normalizeAxis$1(sub$7(midpoint$3(v1Start, v1End), midpoint$3(v0Start, v0End)));
8325
- const normal = normalizeAxis$1(options.normal ?? cross$7(uAxis, vAxis));
8325
+ const normal = normalizeAxis$1(options.normal ?? cross$8(uAxis, vAxis));
8326
8326
  const faces = /* @__PURE__ */ new Map();
8327
8327
  faces.set(faceName, {
8328
8328
  name: faceName,
@@ -10224,6 +10224,7 @@ function buildSweepLevelSetInput(profilePolygons, pathInput, options) {
10224
10224
  edgeLength: options.edgeLength
10225
10225
  };
10226
10226
  }
10227
+ const EPS$9 = 1e-9;
10227
10228
  function resamplePolygon(poly, targetCount) {
10228
10229
  if (poly.length < 2) return poly;
10229
10230
  if (targetCount <= 0) return [];
@@ -10261,6 +10262,78 @@ function resamplePolygon(poly, targetCount) {
10261
10262
  }
10262
10263
  return out;
10263
10264
  }
10265
+ function resamplePolygonByAngle(poly, targetCount, center = polygonCentroid$2(poly)) {
10266
+ if (poly.length < 3 || targetCount <= 0) return null;
10267
+ if (!isConvexPolygon(poly)) return null;
10268
+ const out = [];
10269
+ for (let index2 = 0; index2 < targetCount; index2 += 1) {
10270
+ const angle = index2 / targetCount * Math.PI * 2;
10271
+ const point2 = rayPolygonIntersection(center, [Math.cos(angle), Math.sin(angle)], poly);
10272
+ if (!point2) return null;
10273
+ out.push(point2);
10274
+ }
10275
+ return out;
10276
+ }
10277
+ function rayPolygonIntersection(origin, direction2, poly) {
10278
+ let bestT = Infinity;
10279
+ let best = null;
10280
+ for (let index2 = 0; index2 < poly.length; index2 += 1) {
10281
+ const a2 = poly[index2];
10282
+ const b = poly[(index2 + 1) % poly.length];
10283
+ const edge = [b[0] - a2[0], b[1] - a2[1]];
10284
+ const denom = cross$7(direction2, edge);
10285
+ if (Math.abs(denom) < EPS$9) continue;
10286
+ const delta = [a2[0] - origin[0], a2[1] - origin[1]];
10287
+ const rayT = cross$7(delta, edge) / denom;
10288
+ const edgeT = cross$7(delta, direction2) / denom;
10289
+ if (rayT >= -EPS$9 && edgeT >= -EPS$9 && edgeT <= 1 + EPS$9 && rayT < bestT) {
10290
+ bestT = rayT;
10291
+ best = [origin[0] + direction2[0] * rayT, origin[1] + direction2[1] * rayT];
10292
+ }
10293
+ }
10294
+ return best;
10295
+ }
10296
+ function polygonCentroid$2(poly) {
10297
+ let area2 = 0;
10298
+ let cx = 0;
10299
+ let cy = 0;
10300
+ for (let index2 = 0; index2 < poly.length; index2 += 1) {
10301
+ const a2 = poly[index2];
10302
+ const b = poly[(index2 + 1) % poly.length];
10303
+ const crossValue = cross$7(a2, b);
10304
+ area2 += crossValue;
10305
+ cx += (a2[0] + b[0]) * crossValue;
10306
+ cy += (a2[1] + b[1]) * crossValue;
10307
+ }
10308
+ if (Math.abs(area2) < EPS$9) return averagePoint(poly);
10309
+ return [cx / (3 * area2), cy / (3 * area2)];
10310
+ }
10311
+ function averagePoint(poly) {
10312
+ let x2 = 0;
10313
+ let y2 = 0;
10314
+ for (const point2 of poly) {
10315
+ x2 += point2[0];
10316
+ y2 += point2[1];
10317
+ }
10318
+ return [x2 / poly.length, y2 / poly.length];
10319
+ }
10320
+ function isConvexPolygon(poly) {
10321
+ let sign2 = 0;
10322
+ for (let index2 = 0; index2 < poly.length; index2 += 1) {
10323
+ const a2 = poly[index2];
10324
+ const b = poly[(index2 + 1) % poly.length];
10325
+ const c2 = poly[(index2 + 2) % poly.length];
10326
+ const turn = cross$7([b[0] - a2[0], b[1] - a2[1]], [c2[0] - b[0], c2[1] - b[1]]);
10327
+ if (Math.abs(turn) < EPS$9) continue;
10328
+ const currentSign = Math.sign(turn);
10329
+ if (sign2 !== 0 && currentSign !== sign2) return false;
10330
+ sign2 = currentSign;
10331
+ }
10332
+ return sign2 !== 0;
10333
+ }
10334
+ function cross$7(a2, b) {
10335
+ return a2[0] * b[1] - a2[1] * b[0];
10336
+ }
10264
10337
  function loftStitched(profiles2, heights, wasm) {
10265
10338
  if (profiles2.length < 2) return null;
10266
10339
  const classified = profiles2.map((loops) => classifyLoops(loops));
@@ -10389,8 +10462,10 @@ function stitchSingleLoopLoft(loops, heights, wasm) {
10389
10462
  maxPoints = Math.max(maxPoints, loop.length);
10390
10463
  }
10391
10464
  const N = Math.max(maxPoints, 24);
10465
+ const angularSamples = normalizedLoops.map((loop) => resamplePolygonByAngle(loop, N));
10466
+ const useAngularSamples = angularSamples.every((samples) => samples != null);
10392
10467
  const resampled = normalizedLoops.map((loop, i) => {
10393
- const pts2d = resamplePolygon(loop, N);
10468
+ const pts2d = useAngularSamples ? angularSamples[i] : resamplePolygon(loop, N);
10394
10469
  const z2 = heights[i];
10395
10470
  return pts2d.map(([x2, y2]) => [x2, y2, z2]);
10396
10471
  });
@@ -10451,7 +10526,7 @@ let _wasm$1 = null;
10451
10526
  async function initManifoldWasm() {
10452
10527
  if (_wasm$1) return _wasm$1;
10453
10528
  performance.mark("manifold:start");
10454
- const Module = (await import("./manifold-Cjk7WhRs.js")).default;
10529
+ const Module = (await import("./manifold-sJ-axdXM.js")).default;
10455
10530
  performance.mark("manifold:imported");
10456
10531
  const wasm = await Module();
10457
10532
  wasm.setup();
@@ -10583,6 +10658,23 @@ let ManifoldShapeBackend = _ManifoldShapeBackend;
10583
10658
  function wrapManifoldShapeBackend(manifold) {
10584
10659
  return new ManifoldShapeBackend(manifold);
10585
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
+ }
10586
10678
  function requireManifoldShapeBackend(backend, apiName = "requireManifoldShapeBackend()") {
10587
10679
  if (isManifoldCapableBackend(backend)) {
10588
10680
  return backend.requireManifold(apiName);
@@ -47289,10 +47381,8 @@ class PathBuilder {
47289
47381
  if (radius <= 0) throw new Error("fillet: radius must be positive");
47290
47382
  const n = this.segs.length;
47291
47383
  if (n < 2) throw new Error("fillet: need at least 2 segments before a fillet");
47292
- const prev = this.segs[n - 2];
47293
47384
  const curr = this.segs[n - 1];
47294
- curr.kind === "line" || curr.kind === "move" ? prev.kind === "line" || prev.kind === "move" ? 0 : 0 : 0;
47295
- const { trimA, trimB, arcSeg } = this.computeFilletGeom(radius);
47385
+ const { trimA, arcSeg } = this.computeFilletGeom(radius);
47296
47386
  if (!arcSeg) throw new Error("fillet: cannot fillet these segments (parallel or degenerate)");
47297
47387
  this.trimLastSegEnd(n - 2, trimA[0], trimA[1]);
47298
47388
  const trimmedSeg = { ...curr };
@@ -47364,7 +47454,6 @@ class PathBuilder {
47364
47454
  }
47365
47455
  getSegDirAt(seg, which) {
47366
47456
  if (seg.kind === "line" || seg.kind === "move") {
47367
- this.segs.length;
47368
47457
  const idx = this.segs.indexOf(seg);
47369
47458
  if (seg.kind === "line") {
47370
47459
  let sx, sy;
@@ -47606,6 +47695,41 @@ class PathBuilder {
47606
47695
  }
47607
47696
  return pts;
47608
47697
  }
47698
+ /**
47699
+ * Return the open path as a sampled 2D polyline.
47700
+ *
47701
+ * This is for construction geometry such as guide rails, measured centerlines,
47702
+ * and curve-driven helpers where the authored path should stay open instead of
47703
+ * becoming a filled sketch or stroked profile.
47704
+ *
47705
+ * **Example**
47706
+ *
47707
+ * ```ts
47708
+ * const rail = path()
47709
+ * .moveTo(24, 0)
47710
+ * .bezierTo(32, 44, 28, 92, 18, 120)
47711
+ * .toPolyline();
47712
+ * ```
47713
+ *
47714
+ * @returns A sampled open polyline.
47715
+ * @category Path Builder
47716
+ */
47717
+ toPolyline() {
47718
+ const moveCount = this.segs.filter((seg) => seg.kind === "move").length;
47719
+ if (moveCount > 1) {
47720
+ throw new Error("path().toPolyline() supports one continuous open path. Use separate path() builders for separate rails.");
47721
+ }
47722
+ const pts = [];
47723
+ for (const point2 of this.tessellate()) {
47724
+ if (!point2.every(Number.isFinite)) throw new Error("path().toPolyline() produced a non-finite point");
47725
+ const previous = pts[pts.length - 1];
47726
+ if (!previous || Math.hypot(point2[0] - previous[0], point2[1] - previous[1]) > 1e-9) {
47727
+ pts.push(point2);
47728
+ }
47729
+ }
47730
+ if (pts.length < 2) throw new Error("path().toPolyline() needs at least 2 points");
47731
+ return pts;
47732
+ }
47609
47733
  // ── Output ────────────────────────────────────────────────────────────────
47610
47734
  /**
47611
47735
  * Close the path and return a filled `Sketch`.
@@ -49463,7 +49587,7 @@ function spurGear(options) {
49463
49587
  });
49464
49588
  return attachGearMeta(shapeWithConnectors, meta2);
49465
49589
  }
49466
- function requirePositive$7(scope, name, value) {
49590
+ function requirePositive$8(scope, name, value) {
49467
49591
  if (!isFinitePositive(value)) throw new Error(`${scope}: "${name}" must be > 0`);
49468
49592
  }
49469
49593
  function requireOptionalBore(scope, boreDiameter, maxDiameter) {
@@ -49485,8 +49609,8 @@ function cutBore$1(shape, boreDiameter) {
49485
49609
  return shape.subtract(cutter);
49486
49610
  }
49487
49611
  function gearBodyDisk(options) {
49488
- requirePositive$7("gearBodyDisk", "outerRadius", options.outerRadius);
49489
- requirePositive$7("gearBodyDisk", "faceWidth", options.faceWidth);
49612
+ requirePositive$8("gearBodyDisk", "outerRadius", options.outerRadius);
49613
+ requirePositive$8("gearBodyDisk", "faceWidth", options.faceWidth);
49490
49614
  const bore = requireOptionalBore("gearBodyDisk", options.boreDiameter, options.outerRadius * 2);
49491
49615
  const segments = resolveSegments(options.segments);
49492
49616
  const outer = circle2d(options.outerRadius, segments);
@@ -49494,14 +49618,14 @@ function gearBodyDisk(options) {
49494
49618
  return sketchExtrude(profile, options.faceWidth);
49495
49619
  }
49496
49620
  function gearBodyDiskWithHub(options) {
49497
- requirePositive$7("gearBodyDiskWithHub", "hubDiameter", options.hubDiameter);
49621
+ requirePositive$8("gearBodyDiskWithHub", "hubDiameter", options.hubDiameter);
49498
49622
  if (options.hubDiameter >= options.outerRadius * 2) {
49499
49623
  throw new Error('gearBodyDiskWithHub: "hubDiameter" must be smaller than the outer diameter');
49500
49624
  }
49501
49625
  const bore = requireOptionalBore("gearBodyDiskWithHub", options.boreDiameter, options.hubDiameter);
49502
49626
  const base = gearBodyDisk({ ...options, boreDiameter: 0 });
49503
49627
  const hubFaceWidth = options.hubFaceWidth ?? options.faceWidth * 1.5;
49504
- requirePositive$7("gearBodyDiskWithHub", "hubFaceWidth", hubFaceWidth);
49628
+ requirePositive$8("gearBodyDiskWithHub", "hubFaceWidth", hubFaceWidth);
49505
49629
  const hub = cylinder(hubFaceWidth, options.hubDiameter * 0.5, void 0, options.segments).translate(
49506
49630
  0,
49507
49631
  0,
@@ -49510,11 +49634,11 @@ function gearBodyDiskWithHub(options) {
49510
49634
  return cutBore$1(base.add(hub), bore);
49511
49635
  }
49512
49636
  function gearBodySpoked(options) {
49513
- requirePositive$7("gearBodySpoked", "outerRadius", options.outerRadius);
49514
- requirePositive$7("gearBodySpoked", "faceWidth", options.faceWidth);
49515
- requirePositive$7("gearBodySpoked", "rimWidth", options.rimWidth);
49516
- requirePositive$7("gearBodySpoked", "hubDiameter", options.hubDiameter);
49517
- 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);
49518
49642
  if (!Number.isInteger(options.spokeCount) || options.spokeCount < 2) {
49519
49643
  throw new Error('gearBodySpoked: "spokeCount" must be an integer >= 2');
49520
49644
  }
@@ -49537,12 +49661,12 @@ function gearBodySpoked(options) {
49537
49661
  }
49538
49662
  function gearBodyFromProfile(profile, options) {
49539
49663
  if (!(profile instanceof Sketch)) throw new Error('gearBodyFromProfile: "profile" must be a Sketch');
49540
- requirePositive$7("gearBodyFromProfile", "faceWidth", options.faceWidth);
49664
+ requirePositive$8("gearBodyFromProfile", "faceWidth", options.faceWidth);
49541
49665
  const bore = options.boreDiameter ?? 0;
49542
49666
  if (!Number.isFinite(bore) || bore < 0) throw new Error('gearBodyFromProfile: "boreDiameter" must be >= 0');
49543
49667
  return cutBore$1(sketchExtrude(profile, options.faceWidth), bore);
49544
49668
  }
49545
- function requirePositive$6(scope, name, value) {
49669
+ function requirePositive$7(scope, name, value) {
49546
49670
  if (!isFinitePositive(value)) throw new Error(`${scope}: "${name}" must be > 0`);
49547
49671
  }
49548
49672
  function requireFiniteAngle(scope, name, value) {
@@ -49604,7 +49728,7 @@ function buildSpurTeethRegion(options, name, faceWidth) {
49604
49728
  }
49605
49729
  function buildSolidArcRegion(options, name, faceWidth) {
49606
49730
  const scope = "driveWheel.addSolidArcBetween";
49607
- requirePositive$6(scope, "outerRadius", options.outerRadius);
49731
+ requirePositive$7(scope, "outerRadius", options.outerRadius);
49608
49732
  const innerRadius = options.innerRadius ?? 0;
49609
49733
  if (!Number.isFinite(innerRadius) || innerRadius < 0) throw new Error(`${scope}: "innerRadius" must be >= 0`);
49610
49734
  if (innerRadius >= options.outerRadius) throw new Error(`${scope}: "innerRadius" must be smaller than "outerRadius"`);
@@ -49670,7 +49794,7 @@ class DriveWheelBuilder {
49670
49794
  __publicField(this, "boreDiameter");
49671
49795
  __publicField(this, "regions", []);
49672
49796
  if (options.body !== void 0 && !(options.body instanceof Shape)) throw new Error('driveWheel: "body" must be a Shape');
49673
- if (options.faceWidth !== void 0) requirePositive$6("driveWheel", "faceWidth", options.faceWidth);
49797
+ if (options.faceWidth !== void 0) requirePositive$7("driveWheel", "faceWidth", options.faceWidth);
49674
49798
  const boreDiameter = options.boreDiameter ?? 0;
49675
49799
  if (!Number.isFinite(boreDiameter) || boreDiameter < 0) throw new Error('driveWheel: "boreDiameter" must be >= 0');
49676
49800
  this.body = options.body;
@@ -49705,7 +49829,7 @@ class DriveWheelBuilder {
49705
49829
  if (options.innerRadius !== void 0 && (!Number.isFinite(options.innerRadius) || options.innerRadius < 0)) {
49706
49830
  throw new Error(`${scope}: "innerRadius" must be >= 0`);
49707
49831
  }
49708
- if (options.outerRadius !== void 0) requirePositive$6(scope, "outerRadius", options.outerRadius);
49832
+ if (options.outerRadius !== void 0) requirePositive$7(scope, "outerRadius", options.outerRadius);
49709
49833
  this.regions.push({
49710
49834
  shape: shape.clone(),
49711
49835
  meta: {
@@ -49771,7 +49895,7 @@ class DriveWheelBuilder {
49771
49895
  resolveFaceWidth(scope, localFaceWidth) {
49772
49896
  const faceWidth = localFaceWidth ?? this.faceWidth;
49773
49897
  if (faceWidth === void 0) throw new Error(`${scope}: "faceWidth" is required unless driveWheel({ faceWidth }) was set`);
49774
- requirePositive$6(scope, "faceWidth", faceWidth);
49898
+ requirePositive$7(scope, "faceWidth", faceWidth);
49775
49899
  if (this.faceWidth !== void 0 && localFaceWidth !== void 0 && Math.abs(this.faceWidth - localFaceWidth) > EPSILON$1) {
49776
49900
  throw new Error(`${scope}: region faceWidth must match driveWheel faceWidth`);
49777
49901
  }
@@ -50924,6 +51048,1867 @@ function washer(size, options) {
50924
51048
  const bore = cylinder(dims.t + 1, dims.id / 2, void 0, segs);
50925
51049
  return outer.subtract(bore);
50926
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
+ }
50927
52912
  function fastenerSet(size, boltLength, options) {
50928
52913
  const sizeData = METRIC_HOLE_TABLE[size];
50929
52914
  if (!sizeData) throw new Error(`fastenerSet: unsupported size "${size}"`);
@@ -50984,6 +52969,22 @@ const partLibrary = {
50984
52969
  nut,
50985
52970
  washer,
50986
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,
50987
52988
  pipeRoute,
50988
52989
  elbow,
50989
52990
  beltDrive,
@@ -52378,7 +54379,7 @@ function requireFinite$7(value, label) {
52378
54379
  }
52379
54380
  return value;
52380
54381
  }
52381
- function requireVec3$2(value, label) {
54382
+ function requireVec3$3(value, label) {
52382
54383
  if (!Array.isArray(value) || value.length !== 3) {
52383
54384
  throw new Error(`${label} must be [x, y, z]`);
52384
54385
  }
@@ -52422,7 +54423,7 @@ function normalizeOptions(options) {
52422
54423
  out.size = requireFinite$7(options.size, "Viewport.label options.size");
52423
54424
  if (out.size <= 0) throw new Error("Viewport.label options.size must be positive");
52424
54425
  }
52425
- if (options.offset !== void 0) out.offset = requireVec3$2(options.offset, "Viewport.label options.offset");
54426
+ if (options.offset !== void 0) out.offset = requireVec3$3(options.offset, "Viewport.label options.offset");
52426
54427
  if (options.anchor !== void 0) {
52427
54428
  if (!VALID_ANCHORS.has(options.anchor)) {
52428
54429
  throw new Error(`Viewport.label options.anchor must be one of: ${Array.from(VALID_ANCHORS).join(", ")}`);
@@ -52439,7 +54440,7 @@ function collectRenderLabel(text, at, options) {
52439
54440
  if (typeof text !== "string" || text.trim().length === 0) {
52440
54441
  throw new Error("Viewport.label text must be a non-empty string");
52441
54442
  }
52442
- const normalizedAt = requireVec3$2(at, "Viewport.label at");
54443
+ const normalizedAt = requireVec3$3(at, "Viewport.label at");
52443
54444
  const normalizedOptions = normalizeOptions(options);
52444
54445
  _collected$4.push({
52445
54446
  id: `render-label-${_nextId++}`,
@@ -52634,7 +54635,7 @@ function requireFinite$6(value, label) {
52634
54635
  }
52635
54636
  return value;
52636
54637
  }
52637
- function requireVec3$1(value, label) {
54638
+ function requireVec3$2(value, label) {
52638
54639
  if (!Array.isArray(value) || value.length !== 3) {
52639
54640
  throw new Error(`${label} must be [x, y, z]`);
52640
54641
  }
@@ -52662,9 +54663,9 @@ const VALID_ENVIRONMENT_PRESETS = /* @__PURE__ */ new Set([
52662
54663
  ]);
52663
54664
  function validateCamera(cam, label) {
52664
54665
  const out = {};
52665
- if (cam.position !== void 0) out.position = requireVec3$1(cam.position, `${label}.position`);
52666
- if (cam.target !== void 0) out.target = requireVec3$1(cam.target, `${label}.target`);
52667
- if (cam.up !== void 0) out.up = requireVec3$1(cam.up, `${label}.up`);
54666
+ if (cam.position !== void 0) out.position = requireVec3$2(cam.position, `${label}.position`);
54667
+ if (cam.target !== void 0) out.target = requireVec3$2(cam.target, `${label}.target`);
54668
+ if (cam.up !== void 0) out.up = requireVec3$2(cam.up, `${label}.up`);
52668
54669
  if (cam.fov !== void 0) {
52669
54670
  out.fov = requireFinite$6(cam.fov, `${label}.fov`);
52670
54671
  if (out.fov <= 0 || out.fov >= 180) throw new Error(`${label}.fov must be between 0 and 180`);
@@ -52799,8 +54800,8 @@ function validateLight(light, label) {
52799
54800
  const out = { type: light.type };
52800
54801
  if (light.color !== void 0) out.color = requireColor(light.color, `${label}.color`);
52801
54802
  if (light.intensity !== void 0) out.intensity = requireFinite$6(light.intensity, `${label}.intensity`);
52802
- if (light.position !== void 0) out.position = requireVec3$1(light.position, `${label}.position`);
52803
- if (light.target !== void 0) out.target = requireVec3$1(light.target, `${label}.target`);
54803
+ if (light.position !== void 0) out.position = requireVec3$2(light.position, `${label}.position`);
54804
+ if (light.target !== void 0) out.target = requireVec3$2(light.target, `${label}.target`);
52804
54805
  if (light.groundColor !== void 0) out.groundColor = requireColor(light.groundColor, `${label}.groundColor`);
52805
54806
  if (light.skyColor !== void 0) out.skyColor = requireColor(light.skyColor, `${label}.skyColor`);
52806
54807
  if (light.angle !== void 0) out.angle = requireFinite$6(light.angle, `${label}.angle`);
@@ -54332,7 +56333,7 @@ function scale$1(v, s) {
54332
56333
  function dot$2(a2, b) {
54333
56334
  return a2[0] * b[0] + a2[1] * b[1] + a2[2] * b[2];
54334
56335
  }
54335
- function lerp$2(a2, b, t) {
56336
+ function lerp$4(a2, b, t) {
54336
56337
  return a2 + (b - a2) * t;
54337
56338
  }
54338
56339
  function frameMatrix$1(x2, y2, z2, p2) {
@@ -54343,7 +56344,7 @@ function axisVector(axis, sign2 = 1) {
54343
56344
  if (axis === "Y") return [0, sign2, 0];
54344
56345
  return [0, 0, sign2];
54345
56346
  }
54346
- function axisPosition(axis, point2) {
56347
+ function axisPosition$1(axis, point2) {
54347
56348
  return point2[AXIS_INDEX[axis]];
54348
56349
  }
54349
56350
  function crossPointForStation(axis, point2) {
@@ -54351,7 +56352,7 @@ function crossPointForStation(axis, point2) {
54351
56352
  if (axis === "Y") return [point2[0], -point2[2]];
54352
56353
  return [point2[1], point2[2]];
54353
56354
  }
54354
- function orientLoftToAxis(shape, axis) {
56355
+ function orientLoftToAxis$1(shape, axis) {
54355
56356
  if (axis === "Z") return shape;
54356
56357
  if (axis === "Y") return shape.rotateX(-90);
54357
56358
  return shape.transform([0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1]);
@@ -54408,9 +56409,9 @@ function interpolateQuery(a2, b, t) {
54408
56409
  }
54409
56410
  return {
54410
56411
  side: sideA,
54411
- u: lerp$2(a2.u ?? 0.5, b.u ?? 0.5, t),
54412
- v: lerp$2(a2.v ?? 0.5, b.v ?? 0.5, t),
54413
- offset: lerp$2(a2.offset ?? 0, b.offset ?? 0, t)
56412
+ u: lerp$4(a2.u ?? 0.5, b.u ?? 0.5, t),
56413
+ v: lerp$4(a2.v ?? 0.5, b.v ?? 0.5, t),
56414
+ offset: lerp$4(a2.offset ?? 0, b.offset ?? 0, t)
54414
56415
  };
54415
56416
  }
54416
56417
  function resolvePathQueries(points) {
@@ -54477,8 +56478,8 @@ class ProductSkin {
54477
56478
  this.stations = stations;
54478
56479
  this.rails = rails;
54479
56480
  for (const [name2, query] of Object.entries(refs)) this.refQueries.set(name2, cloneQuery(query));
54480
- this.axisMin = Math.min(...stations.map((station) => axisPosition(axis, station.center)));
54481
- this.axisMax = Math.max(...stations.map((station) => axisPosition(axis, station.center)));
56481
+ this.axisMin = Math.min(...stations.map((station) => axisPosition$1(axis, station.center)));
56482
+ this.axisMax = Math.max(...stations.map((station) => axisPosition$1(axis, station.center)));
54482
56483
  this.diagnosticsValue = {
54483
56484
  ...diagnostics,
54484
56485
  stationNames: stations.map((station) => station.name),
@@ -54535,24 +56536,24 @@ class ProductSkin {
54535
56536
  }
54536
56537
  /** Interpolate center, width, and depth at a normalized v or absolute axis value. */
54537
56538
  stationAt(vOrAxis) {
54538
- const axisValue = vOrAxis >= 0 && vOrAxis <= 1 ? lerp$2(this.axisMin, this.axisMax, vOrAxis) : clamp$5(vOrAxis, this.axisMin, this.axisMax);
56539
+ const axisValue = vOrAxis >= 0 && vOrAxis <= 1 ? lerp$4(this.axisMin, this.axisMax, vOrAxis) : clamp$5(vOrAxis, this.axisMin, this.axisMax);
54539
56540
  const sorted = this.stations;
54540
56541
  for (let index2 = 0; index2 < sorted.length - 1; index2 += 1) {
54541
56542
  const a2 = sorted[index2];
54542
56543
  const b = sorted[index2 + 1];
54543
- const aAxis = axisPosition(this.axis, a2.center);
54544
- const bAxis = axisPosition(this.axis, b.center);
56544
+ const aAxis = axisPosition$1(this.axis, a2.center);
56545
+ const bAxis = axisPosition$1(this.axis, b.center);
54545
56546
  if (axisValue < aAxis - EPS$5 || axisValue > bAxis + EPS$5) continue;
54546
56547
  const span = Math.max(EPS$5, bAxis - aAxis);
54547
56548
  const t = clamp$5((axisValue - aAxis) / span, 0, 1);
54548
56549
  return {
54549
56550
  axisValue,
54550
- center: [lerp$2(a2.center[0], b.center[0], t), lerp$2(a2.center[1], b.center[1], t), lerp$2(a2.center[2], b.center[2], t)],
54551
- width: lerp$2(a2.profile.width, b.profile.width, t),
54552
- depth: lerp$2(a2.profile.depth, b.profile.depth, t),
56551
+ center: [lerp$4(a2.center[0], b.center[0], t), lerp$4(a2.center[1], b.center[1], t), lerp$4(a2.center[2], b.center[2], t)],
56552
+ width: lerp$4(a2.profile.width, b.profile.width, t),
56553
+ depth: lerp$4(a2.profile.depth, b.profile.depth, t),
54553
56554
  dWidth: (b.profile.width - a2.profile.width) / span,
54554
56555
  dDepth: (b.profile.depth - a2.profile.depth) / span,
54555
- exponent: lerp$2(profileExponent(a2), profileExponent(b), t),
56556
+ exponent: lerp$4(profileExponent(a2), profileExponent(b), t),
54556
56557
  kind: a2.profile.kind === b.profile.kind ? a2.profile.kind : "custom"
54557
56558
  };
54558
56559
  }
@@ -54674,7 +56675,7 @@ class ProductSkinBuilder {
54674
56675
  }
54675
56676
  /** Set named cross-section stations for the product skin. */
54676
56677
  stations(stations) {
54677
- this.stationsValue = stations.map(toStationSpec).sort((a2, b) => axisPosition(this.axisValue, a2.center) - axisPosition(this.axisValue, b.center));
56678
+ this.stationsValue = stations.map(toStationSpec).sort((a2, b) => axisPosition$1(this.axisValue, a2.center) - axisPosition$1(this.axisValue, b.center));
54678
56679
  return this;
54679
56680
  }
54680
56681
  /** Attach named guide rails for product-skin construction and downstream surface references. */
@@ -54724,9 +56725,9 @@ class ProductSkinBuilder {
54724
56725
  const [x2, y2] = crossPointForStation(this.axisValue, station.center);
54725
56726
  return station.profile.sketch.translate(x2, y2);
54726
56727
  });
54727
- const heights = this.stationsValue.map((station) => axisPosition(this.axisValue, station.center));
56728
+ const heights = this.stationsValue.map((station) => axisPosition$1(this.axisValue, station.center));
54728
56729
  let shape = loft(localProfiles, heights, { edgeLength: this.edgeLengthValue });
54729
- shape = orientLoftToAxis(shape, this.axisValue);
56730
+ shape = orientLoftToAxis$1(shape, this.axisValue);
54730
56731
  if (this.colorValue) shape = shape.color(this.colorValue);
54731
56732
  shape = applyMaterial(shape, this.materialValue).as(this.name);
54732
56733
  const warnings = [];
@@ -55385,7 +57386,7 @@ function requirePositive$3(value, label) {
55385
57386
  function clamp$4(value, min2, max2) {
55386
57387
  return Math.max(min2, Math.min(max2, value));
55387
57388
  }
55388
- function lerp$1(a2, b, t) {
57389
+ function lerp$3(a2, b, t) {
55389
57390
  return a2 + (b - a2) * t;
55390
57391
  }
55391
57392
  function add(a2, b) {
@@ -55435,19 +57436,19 @@ function transformLocal(point2, tangentAcross, normal, tangentAlong, x2, y2, z2
55435
57436
  function interpolateCylinder(a2, b, t, mode) {
55436
57437
  let delta = b.angle - a2.angle;
55437
57438
  if (mode === "shortest" && Math.abs(delta) > 180) delta -= Math.sign(delta) * 360;
55438
- return { kind: "cylinder", angle: a2.angle + delta * t, z: lerp$1(a2.z, b.z, t), offset: lerp$1(a2.offset ?? 0, b.offset ?? 0, t) };
57439
+ return { kind: "cylinder", angle: a2.angle + delta * t, z: lerp$3(a2.z, b.z, t), offset: lerp$3(a2.offset ?? 0, b.offset ?? 0, t) };
55439
57440
  }
55440
57441
  function interpolatePlane(a2, b, t) {
55441
- return { kind: "plane", x: lerp$1(a2.x, b.x, t), y: lerp$1(a2.y, b.y, t), offset: lerp$1(a2.offset ?? 0, b.offset ?? 0, t) };
57442
+ return { kind: "plane", x: lerp$3(a2.x, b.x, t), y: lerp$3(a2.y, b.y, t), offset: lerp$3(a2.offset ?? 0, b.offset ?? 0, t) };
55442
57443
  }
55443
57444
  function interpolateProductSkin(a2, b, t) {
55444
57445
  if ((a2.side ?? b.side) !== (b.side ?? a2.side)) throw new Error("SurfacePath on ProductSkin currently supports one side per path; split side transitions into separate members.");
55445
57446
  return {
55446
57447
  kind: "productSkin",
55447
57448
  side: a2.side ?? b.side,
55448
- u: lerp$1(a2.u ?? 0.5, b.u ?? 0.5, t),
55449
- v: lerp$1(a2.v ?? 0.5, b.v ?? 0.5, t),
55450
- offset: lerp$1(a2.offset ?? 0, b.offset ?? 0, t)
57449
+ u: lerp$3(a2.u ?? 0.5, b.u ?? 0.5, t),
57450
+ v: lerp$3(a2.v ?? 0.5, b.v ?? 0.5, t),
57451
+ offset: lerp$3(a2.offset ?? 0, b.offset ?? 0, t)
55451
57452
  };
55452
57453
  }
55453
57454
  class SurfacePath {
@@ -55770,11 +57771,11 @@ function coordinateOnSide(coordinate, side, label) {
55770
57771
  return { ...coordinate, kind: "productSkin", side };
55771
57772
  }
55772
57773
  class ProductSkinCarrier {
55773
- constructor(skin, name = skin.name, sideValue, offsetValue = 0) {
57774
+ constructor(skin, name = skin.name, sideValue2, offsetValue = 0) {
55774
57775
  __publicField(this, "kind", "productSkin");
55775
57776
  this.skin = skin;
55776
57777
  this.name = name;
55777
- this.sideValue = sideValue;
57778
+ this.sideValue = sideValue2;
55778
57779
  this.offsetValue = offsetValue;
55779
57780
  }
55780
57781
  surface(side) {
@@ -56545,7 +58546,7 @@ function counterboresForPlate(spec2, width, height, thickness, diagnostics) {
56545
58546
  function minWidthAcrossAlongRange(widthAtT, length4, minAlong, maxAlong) {
56546
58547
  let minWidth = Number.POSITIVE_INFINITY;
56547
58548
  for (let index2 = 0; index2 <= 8; index2 += 1) {
56548
- const along = lerp$1(minAlong, maxAlong, index2 / 8);
58549
+ const along = lerp$3(minAlong, maxAlong, index2 / 8);
56549
58550
  const t = Math.max(0, Math.min(1, (along + length4 / 2) / Math.max(length4, 1e-8)));
56550
58551
  minWidth = Math.min(minWidth, widthAtT(t));
56551
58552
  }
@@ -56845,7 +58846,7 @@ function pathParameterAtDistance(samples, distance2) {
56845
58846
  const segmentLength = Math.hypot(b.point[0] - a2.point[0], b.point[1] - a2.point[1], b.point[2] - a2.point[2]);
56846
58847
  if (traveled + segmentLength >= distance2) {
56847
58848
  const localT = segmentLength <= 1e-8 ? 0 : (distance2 - traveled) / segmentLength;
56848
- return lerp$1(a2.t, b.t, localT);
58849
+ return lerp$3(a2.t, b.t, localT);
56849
58850
  }
56850
58851
  traveled += segmentLength;
56851
58852
  }
@@ -56898,7 +58899,7 @@ function compileBandFootprintMesh(path2, input) {
56898
58899
  const width = input.widthAt(t);
56899
58900
  const along = distance2 - length4 / 2;
56900
58901
  for (let acrossIndex = 0; acrossIndex <= acrossSegments; acrossIndex += 1) {
56901
- const across = lerp$1(-width / 2, width / 2, acrossIndex / acrossSegments);
58902
+ const across = lerp$3(-width / 2, width / 2, acrossIndex / acrossSegments);
56902
58903
  mesh.vertices.push(pointAtProfile([across, along], false));
56903
58904
  }
56904
58905
  }
@@ -56908,7 +58909,7 @@ function compileBandFootprintMesh(path2, input) {
56908
58909
  const width = input.widthAt(t);
56909
58910
  const along = distance2 - length4 / 2;
56910
58911
  for (let acrossIndex = 0; acrossIndex <= acrossSegments; acrossIndex += 1) {
56911
- const across = lerp$1(-width / 2, width / 2, acrossIndex / acrossSegments);
58912
+ const across = lerp$3(-width / 2, width / 2, acrossIndex / acrossSegments);
56912
58913
  mesh.vertices.push(pointAtProfile([across, along], true));
56913
58914
  }
56914
58915
  }
@@ -56920,7 +58921,7 @@ function compileBandFootprintMesh(path2, input) {
56920
58921
  const width = input.widthAt(t);
56921
58922
  const along = distance2 - length4 / 2;
56922
58923
  for (let acrossIndex = 0; acrossIndex < acrossSegments; acrossIndex += 1) {
56923
- const across = lerp$1(-width / 2, width / 2, (acrossIndex + 0.5) / acrossSegments);
58924
+ const across = lerp$3(-width / 2, width / 2, (acrossIndex + 0.5) / acrossSegments);
56924
58925
  filled[alongIndex][acrossIndex] = !holes.some((hole2) => pointInProfileLoop([across, along], hole2));
56925
58926
  }
56926
58927
  }
@@ -59002,7 +61003,7 @@ const Constraint = {
59002
61003
  return builder.constrain({ type: "length", line: resolveLineId(builder, line2), value });
59003
61004
  }
59004
61005
  };
59005
- function requireVec3(v, label) {
61006
+ function requireVec3$1(v, label) {
59006
61007
  if (!Array.isArray(v) || v.length !== 3 || !Number.isFinite(v[0]) || !Number.isFinite(v[1]) || !Number.isFinite(v[2])) {
59007
61008
  throw new Error(`${label} must be a [number, number, number] with finite values, got ${JSON.stringify(v)}`);
59008
61009
  }
@@ -59015,24 +61016,24 @@ function requireFiniteNumber(n, label) {
59015
61016
  return n;
59016
61017
  }
59017
61018
  function distance$1(a2, b) {
59018
- requireVec3(a2, "a");
59019
- requireVec3(b, "b");
61019
+ requireVec3$1(a2, "a");
61020
+ requireVec3$1(b, "b");
59020
61021
  return Math.hypot(b[0] - a2[0], b[1] - a2[1], b[2] - a2[2]);
59021
61022
  }
59022
61023
  function midpoint$1(a2, b) {
59023
- requireVec3(a2, "a");
59024
- requireVec3(b, "b");
61024
+ requireVec3$1(a2, "a");
61025
+ requireVec3$1(b, "b");
59025
61026
  return [(a2[0] + b[0]) / 2, (a2[1] + b[1]) / 2, (a2[2] + b[2]) / 2];
59026
61027
  }
59027
- function lerp(a2, b, t) {
59028
- requireVec3(a2, "a");
59029
- requireVec3(b, "b");
61028
+ function lerp$2(a2, b, t) {
61029
+ requireVec3$1(a2, "a");
61030
+ requireVec3$1(b, "b");
59030
61031
  requireFiniteNumber(t, "t");
59031
61032
  return [a2[0] + (b[0] - a2[0]) * t, a2[1] + (b[1] - a2[1]) * t, a2[2] + (b[2] - a2[2]) * t];
59032
61033
  }
59033
61034
  function direction(a2, b) {
59034
- requireVec3(a2, "a");
59035
- requireVec3(b, "b");
61035
+ requireVec3$1(a2, "a");
61036
+ requireVec3$1(b, "b");
59036
61037
  const dx = b[0] - a2[0];
59037
61038
  const dy = b[1] - a2[1];
59038
61039
  const dz = b[2] - a2[2];
@@ -59043,8 +61044,8 @@ function direction(a2, b) {
59043
61044
  return [dx / len2, dy / len2, dz / len2];
59044
61045
  }
59045
61046
  function offset(point2, dir, amount) {
59046
- requireVec3(point2, "point");
59047
- requireVec3(dir, "dir");
61047
+ requireVec3$1(point2, "point");
61048
+ requireVec3$1(dir, "dir");
59048
61049
  requireFiniteNumber(amount, "amount");
59049
61050
  return [point2[0] + dir[0] * amount, point2[1] + dir[1] * amount, point2[2] + dir[2] * amount];
59050
61051
  }
@@ -59054,7 +61055,7 @@ const Points = {
59054
61055
  /** Center point between two 3D points. */
59055
61056
  midpoint: midpoint$1,
59056
61057
  /** Linearly interpolate between two 3D points. t=0 returns a, t=1 returns b. */
59057
- lerp,
61058
+ lerp: lerp$2,
59058
61059
  /** Unit direction vector from a to b. Throws if a and b are the same point. */
59059
61060
  direction,
59060
61061
  /** Move a point along a direction vector by a given amount. */
@@ -64186,9 +66187,84 @@ class ConstraintSketch extends Sketch {
64186
66187
  * Select the single arrangement region that contains the given seed point.
64187
66188
  * Throws if no region contains the seed.
64188
66189
  */
64189
- detectArrangementRegion(seed) {
66190
+ detectArrangementRegion(_seed) {
64190
66191
  throw new Error("Not implemented");
64191
66192
  }
66193
+ /**
66194
+ * Return the solved constrained path as a sampled 2D polyline.
66195
+ *
66196
+ * Use this when a construction rail was authored with `constrainedSketch()`
66197
+ * and should feed another operation such as `Loft.pathOnXz(...)`.
66198
+ * The sketch must contain exactly one profile path.
66199
+ *
66200
+ * @param samples - Samples per curved segment. Default 32.
66201
+ * @returns The solved path as an open polyline.
66202
+ */
66203
+ toPolyline(samples = 32) {
66204
+ if (!Number.isFinite(samples) || samples < 2) throw new Error("ConstraintSketch.toPolyline() samples must be at least 2");
66205
+ const profileLoops = this.definition.loops.filter((loop) => loop.type === "profile");
66206
+ if (profileLoops.length !== 1) {
66207
+ throw new Error("ConstraintSketch.toPolyline() requires exactly one profile path");
66208
+ }
66209
+ const sampleCount = Math.max(2, Math.round(samples));
66210
+ const pointMap = new Map(this.definition.points.map((point2) => [point2.id, point2]));
66211
+ const lineMap = new Map(this.definition.lines.map((line2) => [line2.id, line2]));
66212
+ const arcMap = new Map(this.definition.arcs.map((arc) => [arc.id, arc]));
66213
+ const bezierMap = new Map(this.definition.beziers.map((bezier) => [bezier.id, bezier]));
66214
+ const points = [];
66215
+ const appendStart = (point2, label) => {
66216
+ const previous = points[points.length - 1];
66217
+ if (!previous) {
66218
+ points.push(point2);
66219
+ return;
66220
+ }
66221
+ if (Math.hypot(point2[0] - previous[0], point2[1] - previous[1]) > 1e-6) {
66222
+ throw new Error(`ConstraintSketch.toPolyline() profile path is not continuous at ${label}`);
66223
+ }
66224
+ };
66225
+ const appendPoint = (point2) => {
66226
+ const previous = points[points.length - 1];
66227
+ if (!previous || Math.hypot(point2[0] - previous[0], point2[1] - previous[1]) > 1e-9) points.push(point2);
66228
+ };
66229
+ const requirePoint = (id, label) => {
66230
+ const point2 = pointMap.get(id);
66231
+ if (!point2) throw new Error(`ConstraintSketch.toPolyline() missing ${label}`);
66232
+ return [point2.x, point2.y];
66233
+ };
66234
+ for (const segment of profileLoops[0].segments) {
66235
+ if (segment.kind === "line") {
66236
+ const line2 = lineMap.get(segment.line);
66237
+ if (!line2) throw new Error(`ConstraintSketch.toPolyline() missing line "${segment.line}"`);
66238
+ appendStart(requirePoint(line2.a, `line "${segment.line}" start point`), `line "${segment.line}"`);
66239
+ appendPoint(requirePoint(line2.b, `line "${segment.line}" end point`));
66240
+ } else if (segment.kind === "arc") {
66241
+ const arc = arcMap.get(segment.arc);
66242
+ if (!arc) throw new Error(`ConstraintSketch.toPolyline() missing arc "${segment.arc}"`);
66243
+ const center = requirePoint(arc.center, `arc "${segment.arc}" center point`);
66244
+ const start = requirePoint(arc.start, `arc "${segment.arc}" start point`);
66245
+ const end = requirePoint(arc.end, `arc "${segment.arc}" end point`);
66246
+ appendStart(start, `arc "${segment.arc}"`);
66247
+ const startAngle = Math.atan2(start[1] - center[1], start[0] - center[0]);
66248
+ const endAngle = Math.atan2(end[1] - center[1], end[0] - center[0]);
66249
+ for (const point2 of tessellateArc(center[0], center[1], arc.radius, startAngle, endAngle, arc.clockwise, sampleCount)) {
66250
+ appendPoint(point2);
66251
+ }
66252
+ } else {
66253
+ const bezier = bezierMap.get(segment.bezier);
66254
+ if (!bezier) throw new Error(`ConstraintSketch.toPolyline() missing bezier "${segment.bezier}"`);
66255
+ const p0 = requirePoint(bezier.p0, `bezier "${segment.bezier}" start point`);
66256
+ const p1 = requirePoint(bezier.p1, `bezier "${segment.bezier}" first control point`);
66257
+ const p2 = requirePoint(bezier.p2, `bezier "${segment.bezier}" second control point`);
66258
+ const p3 = requirePoint(bezier.p3, `bezier "${segment.bezier}" end point`);
66259
+ appendStart(p0, `bezier "${segment.bezier}"`);
66260
+ for (const point2 of tessellateBezier(p0[0], p0[1], p1[0], p1[1], p2[0], p2[1], p3[0], p3[1], sampleCount)) {
66261
+ appendPoint(point2);
66262
+ }
66263
+ }
66264
+ }
66265
+ if (points.length < 2) throw new Error("ConstraintSketch.toPolyline() needs at least 2 points");
66266
+ return points;
66267
+ }
64192
66268
  /**
64193
66269
  * Re-solve the sketch after changing the value of one existing constraint.
64194
66270
  *
@@ -79473,6 +81549,295 @@ function polygonVertices(sides, radius, options) {
79473
81549
  centerY: options == null ? void 0 : options.centerY
79474
81550
  });
79475
81551
  }
81552
+ const LOFT_GUIDE_EPS = 1e-8;
81553
+ function orientLoftToAxis(shape, axis) {
81554
+ if (axis === "Z") return shape;
81555
+ if (axis === "Y") return shape.rotateX(-90);
81556
+ return shape.transform([0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1]);
81557
+ }
81558
+ function buildRailEvaluators(rails, axis, start, end, railSamples) {
81559
+ const seen = /* @__PURE__ */ new Set();
81560
+ return rails.map((rail2) => {
81561
+ if (seen.has(rail2.side)) throw new Error(`Loft.withGuideRails() received more than one ${rail2.side} rail`);
81562
+ seen.add(rail2.side);
81563
+ const sampled = sampleRailPath(rail2.path, railSamples);
81564
+ if (sampled.length < 2) throw new Error("Loft guide rails require at least two points");
81565
+ const points = sampled.map((point2) => ({ position: axisPosition(axis, point2), cross: crossPointForAxis(axis, point2) }));
81566
+ const ordered = points[points.length - 1].position >= points[0].position ? points : [...points].reverse();
81567
+ validateRailCoverage(ordered, start, end);
81568
+ return { side: rail2.side, points: ordered };
81569
+ });
81570
+ }
81571
+ function railCrossAt(rail2, position) {
81572
+ const points = rail2.points;
81573
+ if (position <= points[0].position + LOFT_GUIDE_EPS) return points[0].cross;
81574
+ const last = points[points.length - 1];
81575
+ if (position >= last.position - LOFT_GUIDE_EPS) return last.cross;
81576
+ for (let index2 = 0; index2 < points.length - 1; index2 += 1) {
81577
+ const a2 = points[index2];
81578
+ const b = points[index2 + 1];
81579
+ if (position >= a2.position - LOFT_GUIDE_EPS && position <= b.position + LOFT_GUIDE_EPS) {
81580
+ const t = (position - a2.position) / (b.position - a2.position);
81581
+ return [lerp$1(a2.cross[0], b.cross[0], t), lerp$1(a2.cross[1], b.cross[1], t)];
81582
+ }
81583
+ }
81584
+ throw new Error("Loft guide rail does not cover requested station position");
81585
+ }
81586
+ function validateRailCoverage(points, start, end) {
81587
+ for (let index2 = 1; index2 < points.length; index2 += 1) {
81588
+ if (points[index2].position - points[index2 - 1].position < LOFT_GUIDE_EPS) {
81589
+ throw new Error("Loft guide rails must be monotone along the loft axis");
81590
+ }
81591
+ }
81592
+ if (points[0].position - start > LOFT_GUIDE_EPS || end - points[points.length - 1].position > LOFT_GUIDE_EPS) {
81593
+ throw new Error("Loft guide rails must cover the full station range");
81594
+ }
81595
+ }
81596
+ function sampleRailPath(path2, samples) {
81597
+ if (Array.isArray(path2)) return path2.map((point2, index2) => requireVec3(point2, `Loft guide rail point ${index2}`));
81598
+ if (path2 instanceof Curve3D || path2 instanceof HermiteCurve3D || path2 instanceof QuinticHermiteCurve3D || path2 instanceof NurbsCurve3D) {
81599
+ return path2.sample(Math.max(2, Math.round(samples))).map((point2, index2) => requireVec3(point2, `Loft guide rail sample ${index2}`));
81600
+ }
81601
+ throw new Error("Loft guide rail path must be a Vec3[] or ForgeCAD 3D curve");
81602
+ }
81603
+ function requireVec3(point2, label) {
81604
+ if (!Array.isArray(point2) || point2.length !== 3 || !point2.every(Number.isFinite)) {
81605
+ throw new Error(`${label} must be a finite [x, y, z] point`);
81606
+ }
81607
+ return [point2[0], point2[1], point2[2]];
81608
+ }
81609
+ function axisPosition(axis, point2) {
81610
+ if (axis === "X") return point2[0];
81611
+ if (axis === "Y") return point2[1];
81612
+ return point2[2];
81613
+ }
81614
+ function crossPointForAxis(axis, point2) {
81615
+ if (axis === "X") return [point2[1], point2[2]];
81616
+ if (axis === "Y") return [point2[0], -point2[2]];
81617
+ return [point2[0], point2[1]];
81618
+ }
81619
+ function lerp$1(a2, b, t) {
81620
+ return a2 + (b - a2) * t;
81621
+ }
81622
+ function loftWithGuideRails(stations, rails, options = {}) {
81623
+ if (stations.length < 2) throw new Error("Loft.withGuideRails() requires at least two stations");
81624
+ if (rails.length === 0) throw new Error("Loft.withGuideRails() requires at least one guide rail");
81625
+ const sortedStations = sortedValidStations(stations);
81626
+ const axis = options.axis ?? "Z";
81627
+ const start = sortedStations[0].position;
81628
+ const end = sortedStations[sortedStations.length - 1].position;
81629
+ const railEvaluators = buildRailEvaluators(rails, axis, start, end, options.railSamples ?? 64);
81630
+ const positions = generatedPositions(sortedStations, options.samples);
81631
+ const profiles2 = positions.map((position) => {
81632
+ const source = profileForPosition(sortedStations, position);
81633
+ const bounds = boundsForPosition(sortedStations, position);
81634
+ return fitProfileToBounds(source, applyRailsToBounds(bounds, railEvaluators, position));
81635
+ });
81636
+ const shape = loft(profiles2, positions, {
81637
+ edgeLength: options.edgeLength,
81638
+ boundsPadding: options.boundsPadding
81639
+ });
81640
+ return orientLoftToAxis(shape, axis);
81641
+ }
81642
+ function sortedValidStations(stations) {
81643
+ const sorted = [...stations].sort((a2, b) => a2.position - b.position);
81644
+ for (let index2 = 0; index2 < sorted.length; index2 += 1) {
81645
+ if (!Number.isFinite(sorted[index2].position)) throw new Error("Loft.withGuideRails station position must be finite");
81646
+ if (!(sorted[index2].profile instanceof Sketch)) throw new Error("Loft.withGuideRails() stations must use Sketch profiles");
81647
+ if (index2 > 0 && sorted[index2].position - sorted[index2 - 1].position < LOFT_GUIDE_EPS) {
81648
+ throw new Error("Loft.withGuideRails() requires unique, strictly increasing station positions");
81649
+ }
81650
+ }
81651
+ return sorted;
81652
+ }
81653
+ function generatedPositions(stations, samples) {
81654
+ const count = Math.max(2, Math.round(samples ?? Math.max(9, (stations.length - 1) * 8 + 1)));
81655
+ const start = stations[0].position;
81656
+ const end = stations[stations.length - 1].position;
81657
+ const values = /* @__PURE__ */ new Set();
81658
+ const positions = [];
81659
+ const addPosition = (position) => {
81660
+ const key = position.toFixed(9);
81661
+ if (!values.has(key)) {
81662
+ values.add(key);
81663
+ positions.push(position);
81664
+ }
81665
+ };
81666
+ for (let index2 = 0; index2 < count; index2 += 1) addPosition(start + (end - start) * index2 / (count - 1));
81667
+ for (const station of stations) addPosition(station.position);
81668
+ return positions.sort((a2, b) => a2 - b);
81669
+ }
81670
+ function profileForPosition(stations, position) {
81671
+ for (let index2 = 0; index2 < stations.length - 1; index2 += 1) {
81672
+ if (position <= stations[index2 + 1].position + LOFT_GUIDE_EPS) return stations[index2].profile;
81673
+ }
81674
+ return stations[stations.length - 1].profile;
81675
+ }
81676
+ function boundsForPosition(stations, position) {
81677
+ if (position <= stations[0].position + LOFT_GUIDE_EPS) return sketchBounds(stations[0].profile);
81678
+ const last = stations[stations.length - 1];
81679
+ if (position >= last.position - LOFT_GUIDE_EPS) return sketchBounds(last.profile);
81680
+ for (let index2 = 0; index2 < stations.length - 1; index2 += 1) {
81681
+ const a2 = stations[index2];
81682
+ const b = stations[index2 + 1];
81683
+ if (position >= a2.position - LOFT_GUIDE_EPS && position <= b.position + LOFT_GUIDE_EPS) {
81684
+ return lerpBounds(sketchBounds(a2.profile), sketchBounds(b.profile), (position - a2.position) / (b.position - a2.position));
81685
+ }
81686
+ }
81687
+ return sketchBounds(last.profile);
81688
+ }
81689
+ function applyRailsToBounds(bounds, rails, position) {
81690
+ const centerRail = rails.find((rail2) => rail2.side === "center");
81691
+ const center = centerRail ? railCrossAt(centerRail, position) : void 0;
81692
+ const next = { ...bounds };
81693
+ applyAxisRail(next, "X", sideValue(rails, "left", position, 0), sideValue(rails, "right", position, 0), center == null ? void 0 : center[0]);
81694
+ applyAxisRail(next, "Y", sideValue(rails, "back", position, 1), sideValue(rails, "front", position, 1), center == null ? void 0 : center[1]);
81695
+ if (next.maxX - next.minX < LOFT_GUIDE_EPS || next.maxY - next.minY < LOFT_GUIDE_EPS) {
81696
+ throw new Error("Loft.withGuideRails() guide rails produced a non-positive section size");
81697
+ }
81698
+ return next;
81699
+ }
81700
+ function sideValue(rails, side, position, crossIndex) {
81701
+ const rail2 = rails.find((entry) => entry.side === side);
81702
+ return rail2 ? railCrossAt(rail2, position)[crossIndex] : void 0;
81703
+ }
81704
+ function applyAxisRail(bounds, axis, minRail, maxRail, center) {
81705
+ const minKey = axis === "X" ? "minX" : "minY";
81706
+ const maxKey = axis === "X" ? "maxX" : "maxY";
81707
+ const width = bounds[maxKey] - bounds[minKey];
81708
+ if (minRail != null && maxRail != null) {
81709
+ if (maxRail - minRail < LOFT_GUIDE_EPS) throw new Error("Loft.withGuideRails() opposite guide rails crossed");
81710
+ if (center != null && Math.abs((minRail + maxRail) / 2 - center) > 1e-5) {
81711
+ throw new Error("Loft.withGuideRails() center rail conflicts with opposite side rails");
81712
+ }
81713
+ bounds[minKey] = minRail;
81714
+ bounds[maxKey] = maxRail;
81715
+ } else if (maxRail != null) {
81716
+ bounds[maxKey] = maxRail;
81717
+ bounds[minKey] = center != null ? 2 * center - maxRail : maxRail - width;
81718
+ } else if (minRail != null) {
81719
+ bounds[minKey] = minRail;
81720
+ bounds[maxKey] = center != null ? 2 * center - minRail : minRail + width;
81721
+ } else if (center != null) {
81722
+ bounds[minKey] = center - width / 2;
81723
+ bounds[maxKey] = center + width / 2;
81724
+ }
81725
+ }
81726
+ function fitProfileToBounds(profile, target) {
81727
+ const source = sketchBounds(profile);
81728
+ const sourceWidth = source.maxX - source.minX;
81729
+ const sourceDepth = source.maxY - source.minY;
81730
+ if (sourceWidth < LOFT_GUIDE_EPS || sourceDepth < LOFT_GUIDE_EPS) {
81731
+ throw new Error("Loft.withGuideRails() station profiles must have positive bounds");
81732
+ }
81733
+ const sourceCenter = [(source.minX + source.maxX) / 2, (source.minY + source.maxY) / 2];
81734
+ const targetCenter = [(target.minX + target.maxX) / 2, (target.minY + target.maxY) / 2];
81735
+ return profile.scaleAround(sourceCenter, [(target.maxX - target.minX) / sourceWidth, (target.maxY - target.minY) / sourceDepth]).translate(targetCenter[0] - sourceCenter[0], targetCenter[1] - sourceCenter[1]);
81736
+ }
81737
+ function sketchBounds(profile) {
81738
+ const bounds = profile.bounds();
81739
+ return { minX: bounds.min[0], maxX: bounds.max[0], minY: bounds.min[1], maxY: bounds.max[1] };
81740
+ }
81741
+ function lerpBounds(a2, b, t) {
81742
+ return {
81743
+ minX: lerp(a2.minX, b.minX, t),
81744
+ maxX: lerp(a2.maxX, b.maxX, t),
81745
+ minY: lerp(a2.minY, b.minY, t),
81746
+ maxY: lerp(a2.maxY, b.maxY, t)
81747
+ };
81748
+ }
81749
+ function lerp(a2, b, t) {
81750
+ return a2 + (b - a2) * t;
81751
+ }
81752
+ function mapLoftPath2D(path2, label, mapper) {
81753
+ const points = sampleLoftPath2D(path2, label);
81754
+ return points.map((point2, index2) => {
81755
+ if (!Array.isArray(point2) || point2.length !== 2 || !point2.every(Number.isFinite)) {
81756
+ throw new Error(`${label} point ${index2} must be a finite [x, y] point`);
81757
+ }
81758
+ return mapper([point2[0], point2[1]]);
81759
+ });
81760
+ }
81761
+ function sampleLoftPath2D(path2, label) {
81762
+ if (Array.isArray(path2)) {
81763
+ if (path2.length < 2) throw new Error(`${label} requires at least two [x, y] points`);
81764
+ return path2;
81765
+ }
81766
+ if (!path2 || typeof path2 !== "object" || typeof path2.toPolyline !== "function") {
81767
+ throw new Error(`${label} requires a 2D path, solved constrained path, or [x, y] point array`);
81768
+ }
81769
+ const points = path2.toPolyline();
81770
+ if (!Array.isArray(points) || points.length < 2) throw new Error(`${label} path must produce at least two [x, y] points`);
81771
+ return points;
81772
+ }
81773
+ const Loft = {
81774
+ /** Create a loft station from a 2D profile and an axis position. */
81775
+ station(profile, position) {
81776
+ if (!Number.isFinite(position)) throw new Error("Loft.station position must be finite");
81777
+ return { profile, position };
81778
+ },
81779
+ /** Create a guide rail that constrains the section-local negative-X side. */
81780
+ leftRail(path2) {
81781
+ return { side: "left", path: path2 };
81782
+ },
81783
+ /** Create a guide rail that constrains the section-local positive-X side. */
81784
+ rightRail(path2) {
81785
+ return { side: "right", path: path2 };
81786
+ },
81787
+ /** Create a guide rail that constrains the section-local positive-Y side. */
81788
+ frontRail(path2) {
81789
+ return { side: "front", path: path2 };
81790
+ },
81791
+ /** Create a guide rail that constrains the section-local negative-Y side. */
81792
+ backRail(path2) {
81793
+ return { side: "back", path: path2 };
81794
+ },
81795
+ /** Create a guide rail that moves section centers along the loft. */
81796
+ centerRail(path2) {
81797
+ return { side: "center", path: path2 };
81798
+ },
81799
+ /**
81800
+ * Place a 2D guide path onto the XZ plane.
81801
+ *
81802
+ * The path's first coordinate becomes X and its second coordinate becomes Z.
81803
+ * Use this for left/right silhouette rails authored with `path()` or `constrainedSketch()`.
81804
+ */
81805
+ pathOnXz(path2, y2 = 0) {
81806
+ if (!Number.isFinite(y2)) throw new Error("Loft.pathOnXz y must be finite");
81807
+ return mapLoftPath2D(path2, "Loft.pathOnXz", ([x2, z2]) => [x2, y2, z2]);
81808
+ },
81809
+ /**
81810
+ * Place a 2D guide path onto the YZ plane.
81811
+ *
81812
+ * The path's first coordinate becomes Y and its second coordinate becomes Z.
81813
+ * Use this for front/back crown rails authored with `path()` or `constrainedSketch()`.
81814
+ */
81815
+ pathOnYz(path2, x2 = 0) {
81816
+ if (!Number.isFinite(x2)) throw new Error("Loft.pathOnYz x must be finite");
81817
+ return mapLoftPath2D(path2, "Loft.pathOnYz", ([y2, z2]) => [x2, y2, z2]);
81818
+ },
81819
+ /**
81820
+ * Place a 2D guide path onto the XY plane.
81821
+ *
81822
+ * The path's first coordinate becomes X and its second coordinate becomes Y.
81823
+ * Use this when lofting along X or Y and a rail lives in a horizontal sketch plane.
81824
+ */
81825
+ pathOnXy(path2, z2 = 0) {
81826
+ if (!Number.isFinite(z2)) throw new Error("Loft.pathOnXy z must be finite");
81827
+ return mapLoftPath2D(path2, "Loft.pathOnXy", ([x2, y2]) => [x2, y2, z2]);
81828
+ },
81829
+ /**
81830
+ * Loft through profile stations while forcing generated sections to follow guide rails.
81831
+ *
81832
+ * Stations define the cross-section family. Guide rails define the side or center
81833
+ * paths the loft must pass through. With opposite side rails, the section is scaled
81834
+ * to touch both rails. With one side rail, the section keeps its interpolated size
81835
+ * unless a center rail is also present.
81836
+ */
81837
+ withGuideRails(stations, rails, options = {}) {
81838
+ return loftWithGuideRails(stations, rails, options);
81839
+ }
81840
+ };
79476
81841
  let collectedHighlights = [];
79477
81842
  function resetHighlights() {
79478
81843
  collectedHighlights = [];
@@ -84149,10 +86514,14 @@ function spec(name, checkFn) {
84149
86514
  };
84150
86515
  }
84151
86516
  let _collected = [];
86517
+ let _collisionAllowances = [];
86518
+ let _physicalComponentExpectations = [];
84152
86519
  let _counter = 0;
84153
86520
  let _activeGroup = null;
84154
86521
  function resetVerifications() {
84155
86522
  _collected = [];
86523
+ _collisionAllowances = [];
86524
+ _physicalComponentExpectations = [];
84156
86525
  _counter = 0;
84157
86526
  }
84158
86527
  function getCollectedVerifications() {
@@ -84186,15 +86555,35 @@ function push(result) {
84186
86555
  function roundNum(n, digits = 4) {
84187
86556
  return Number.isFinite(n) ? n.toFixed(digits).replace(/\.?0+$/, "") : String(n);
84188
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
+ }
84189
86573
  function computeMinGap(a2, b, searchLength) {
84190
- const backendA = getShapeRuntimeBackend(a2);
84191
- const backendB = getShapeRuntimeBackend(b);
84192
- if (!isManifoldCapableBackend(backendA)) {
84193
- 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);
84194
86586
  }
84195
- const manifoldA = backendA.requireManifold("verification.minGap");
84196
- const manifoldB = requireManifoldShapeBackend(backendB, "verification.minGap");
84197
- return manifoldA.minGap(manifoldB, searchLength);
84198
86587
  }
84199
86588
  function vec3Dot(a2, b) {
84200
86589
  return a2[0] * b[0] + a2[1] * b[1] + a2[2] * b[2];
@@ -84329,8 +86718,143 @@ const verify = {
84329
86718
  actual: `${roundNum(d2, 3)} mm`
84330
86719
  });
84331
86720
  } catch (e) {
84332
- 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
+ });
86770
+ }
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;
84333
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
+ });
84334
86858
  },
84335
86859
  /**
84336
86860
  * Check that two shapes do not collide (minGap > 0).
@@ -84340,19 +86864,28 @@ const verify = {
84340
86864
  notColliding(label, a2, b, searchLength = 1) {
84341
86865
  const line2 = captureSourceLine();
84342
86866
  try {
84343
- 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";
84344
86869
  const passed = gap > 0;
84345
86870
  push({
84346
86871
  id: nextId(),
84347
86872
  label,
86873
+ kind: "interface",
84348
86874
  status: passed ? "pass" : "fail",
84349
- 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)`,
84350
86876
  line: passed ? void 0 : line2,
84351
86877
  expected: "> 0 mm",
84352
86878
  actual: `${roundNum(gap, 3)} mm`
84353
86879
  });
84354
86880
  } catch (e) {
84355
- 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
+ });
84356
86889
  }
84357
86890
  },
84358
86891
  /**
@@ -84361,13 +86894,15 @@ const verify = {
84361
86894
  minClearance(label, a2, b, minGap, searchLength = 10) {
84362
86895
  const line2 = captureSourceLine();
84363
86896
  try {
84364
- const gap = computeMinGap(a2, b, searchLength);
86897
+ const { gap, method } = computeMinGap(a2, b, searchLength);
86898
+ const methodLabel = method === "exact" ? "exact gap" : "mesh-derived gap";
84365
86899
  const passed = gap >= minGap;
84366
86900
  push({
84367
86901
  id: nextId(),
84368
86902
  label,
86903
+ kind: "interface",
84369
86904
  status: passed ? "pass" : "fail",
84370
- 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`,
84371
86906
  line: passed ? void 0 : line2,
84372
86907
  expected: `≥ ${roundNum(minGap, 3)} mm`,
84373
86908
  actual: `${roundNum(gap, 3)} mm`
@@ -84376,6 +86911,90 @@ const verify = {
84376
86911
  push({ id: nextId(), label, status: "fail", message: `Error: ${e instanceof Error ? e.message : String(e)}`, line: line2 });
84377
86912
  }
84378
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
+ },
84379
86998
  /**
84380
86999
  * Check that two face normals are parallel (within toleranceDeg degrees).
84381
87000
  */
@@ -296941,6 +299560,7 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
296941
299560
  nurbsSurface,
296942
299561
  spline2d,
296943
299562
  spline3d,
299563
+ Loft,
296944
299564
  loft,
296945
299565
  loftAlongSpine,
296946
299566
  sweep,