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
@@ -799,18 +799,18 @@ function cloneSdfNode(node) {
799
799
  }
800
800
  }
801
801
  const SHEET_METAL_EDGES = ["top", "right", "bottom", "left"];
802
- const EPS$c = 1e-9;
802
+ const EPS$d = 1e-9;
803
803
  function isFinitePositive$3(value) {
804
804
  return Number.isFinite(value) && value > 0;
805
805
  }
806
806
  function isFiniteNonNegative(value) {
807
807
  return Number.isFinite(value) && value >= 0;
808
808
  }
809
- function cloneVec3$3(vec2) {
809
+ function cloneVec3$5(vec2) {
810
810
  return [vec2[0], vec2[1], vec2[2]];
811
811
  }
812
812
  function cloneFaceAxis(vec2) {
813
- return vec2 ? cloneVec3$3(vec2) : void 0;
813
+ return vec2 ? cloneVec3$5(vec2) : void 0;
814
814
  }
815
815
  function cloneSheetMetalModel(model) {
816
816
  if (!model) return null;
@@ -840,7 +840,7 @@ function edgeDisplayName(edge) {
840
840
  return `sheetMetal().flange("${edge}", ...)`;
841
841
  }
842
842
  function normalizeAngle(angleDeg) {
843
- return Math.abs(angleDeg) <= EPS$c ? 0 : angleDeg;
843
+ return Math.abs(angleDeg) <= EPS$d ? 0 : angleDeg;
844
844
  }
845
845
  function validateSheetMetalModel(model) {
846
846
  if (!isFinitePositive$3(model.panel.width) || !isFinitePositive$3(model.panel.height)) {
@@ -852,7 +852,7 @@ function validateSheetMetalModel(model) {
852
852
  if (!isFiniteNonNegative(model.bendRadius)) {
853
853
  return "sheetMetal() requires a finite non-negative bendRadius.";
854
854
  }
855
- if (model.bendRadius <= EPS$c) {
855
+ if (model.bendRadius <= EPS$d) {
856
856
  return "sheetMetal() v1 requires a positive bendRadius so the bend region stays explicit instead of collapsing into a sharp fold.";
857
857
  }
858
858
  if (model.bendAllowance.kind !== "k-factor") {
@@ -914,7 +914,7 @@ function deriveSheetMetalModel(model) {
914
914
  const trimEnd = flanges.has(adjacent.end) ? model.cornerRelief.size : 0;
915
915
  const fullLength = edge === "top" || edge === "bottom" ? model.panel.width : model.panel.height;
916
916
  const span = fullLength - trimStart - trimEnd;
917
- if (!(span > EPS$c)) {
917
+ if (!(span > EPS$d)) {
918
918
  throw new Error(
919
919
  `${edgeDisplayName(edge)} loses all usable span after applying the defended rectangular corner relief size ${model.cornerRelief.size}.`
920
920
  );
@@ -972,7 +972,7 @@ function transformPlacement(origin, u2, v, normal) {
972
972
  };
973
973
  }
974
974
  function translatePlan(plan, x2, y2, z2) {
975
- if (Math.abs(x2) <= EPS$c && Math.abs(y2) <= EPS$c && Math.abs(z2) <= EPS$c) return cloneShapeCompilePlan(plan);
975
+ if (Math.abs(x2) <= EPS$d && Math.abs(y2) <= EPS$d && Math.abs(z2) <= EPS$d) return cloneShapeCompilePlan(plan);
976
976
  return appendShapeCompileTransform(cloneShapeCompilePlan(plan), {
977
977
  kind: "translate",
978
978
  x: x2,
@@ -1120,8 +1120,8 @@ function lowerSheetMetalBasePlan(model, output) {
1120
1120
  function descriptor(name, center, normal, planar, uAxis, vAxis, semantic = "face", memberNames = [name], coplanar = planar) {
1121
1121
  return {
1122
1122
  name,
1123
- center: cloneVec3$3(center),
1124
- normal: cloneVec3$3(normal),
1123
+ center: cloneVec3$5(center),
1124
+ normal: cloneVec3$5(normal),
1125
1125
  planar,
1126
1126
  uAxis: cloneFaceAxis(uAxis),
1127
1127
  vAxis: cloneFaceAxis(vAxis),
@@ -1374,10 +1374,10 @@ function describeSheetMetalPlanarRegionFrames(model, output) {
1374
1374
  const size = sheetMetalPlanarRegionSize(derived, face.name);
1375
1375
  return {
1376
1376
  name: face.name,
1377
- center: cloneVec3$3(face.center),
1378
- normal: cloneVec3$3(face.normal),
1379
- uAxis: cloneVec3$3(face.uAxis),
1380
- vAxis: cloneVec3$3(face.vAxis),
1377
+ center: cloneVec3$5(face.center),
1378
+ normal: cloneVec3$5(face.normal),
1379
+ uAxis: cloneVec3$5(face.uAxis),
1380
+ vAxis: cloneVec3$5(face.vAxis),
1381
1381
  width: size.width,
1382
1382
  height: size.height,
1383
1383
  thickness: derived.thickness
@@ -1416,7 +1416,7 @@ function cloneShapeWorkplanePlacement(placement) {
1416
1416
  placement: cloneSketchPlacementModel(placement.placement)
1417
1417
  };
1418
1418
  }
1419
- const EPS$b = 1e-10;
1419
+ const EPS$c = 1e-10;
1420
1420
  function subVec3(a2, b) {
1421
1421
  return [a2[0] - b[0], a2[1] - b[1], a2[2] - b[2]];
1422
1422
  }
@@ -1442,7 +1442,7 @@ function projectRadial(v, axis) {
1442
1442
  function signedAngleAroundAxis(from, to, axis) {
1443
1443
  const fromLen = lengthVec3$1(from);
1444
1444
  const toLen = lengthVec3$1(to);
1445
- if (fromLen < EPS$b || toLen < EPS$b) return 0;
1445
+ if (fromLen < EPS$c || toLen < EPS$c) return 0;
1446
1446
  const fn = scaleVec3(from, 1 / fromLen);
1447
1447
  const tn = scaleVec3(to, 1 / toLen);
1448
1448
  const sin2 = dotVec3$4(axis, crossVec3$2(fn, tn));
@@ -1463,19 +1463,19 @@ function solveRotateAroundAngle(axis, pivot, movingPoint, targetPoint, options =
1463
1463
  const targetDecomp = projectRadial(target, unitAxis);
1464
1464
  const movingRadialLen = lengthVec3$1(movingDecomp.radial);
1465
1465
  const targetRadialLen = lengthVec3$1(targetDecomp.radial);
1466
- if (movingRadialLen < EPS$b) {
1467
- if (mode === "line" && targetRadialLen >= EPS$b) {
1466
+ if (movingRadialLen < EPS$c) {
1467
+ if (mode === "line" && targetRadialLen >= EPS$c) {
1468
1468
  throw new Error("rotateAroundTo(...): moving point lies on the rotation axis, so line alignment is impossible");
1469
1469
  }
1470
1470
  return 0;
1471
1471
  }
1472
1472
  if (mode === "plane") {
1473
- if (targetRadialLen < EPS$b) {
1473
+ if (targetRadialLen < EPS$c) {
1474
1474
  throw new Error("rotateAroundTo(...): target point lies on the rotation axis, so the target plane is undefined");
1475
1475
  }
1476
1476
  return signedAngleAroundAxis(movingDecomp.radial, targetDecomp.radial, unitAxis);
1477
1477
  }
1478
- if (targetRadialLen < EPS$b) {
1478
+ if (targetRadialLen < EPS$c) {
1479
1479
  throw new Error("rotateAroundTo(...): target line lies on the rotation axis, but the moving point does not");
1480
1480
  }
1481
1481
  const axialTol = 1e-8 * Math.max(1, Math.abs(movingDecomp.axial), Math.abs(targetDecomp.axial));
@@ -1515,10 +1515,10 @@ function multiplyMat4(a2, b) {
1515
1515
  }
1516
1516
  function normalizeVec3$5(v) {
1517
1517
  const len2 = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
1518
- if (len2 < EPS$b) throw new Error("Axis must be non-zero");
1518
+ if (len2 < EPS$c) throw new Error("Axis must be non-zero");
1519
1519
  return [v[0] / len2, v[1] / len2, v[2] / len2];
1520
1520
  }
1521
- function transformPoint$1(m2, p2, w2) {
1521
+ function transformPoint$2(m2, p2, w2) {
1522
1522
  const x2 = p2[0], y2 = p2[1], z2 = p2[2];
1523
1523
  return [
1524
1524
  m2[0] * x2 + m2[4] * y2 + m2[8] * z2 + m2[12] * w2,
@@ -1545,7 +1545,7 @@ function invertMat4(m2) {
1545
1545
  const b10 = a21 * a33 - a23 * a31;
1546
1546
  const b11 = a22 * a33 - a23 * a32;
1547
1547
  const det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
1548
- if (Math.abs(det) < EPS$b) throw new Error("Transform matrix is not invertible");
1548
+ if (Math.abs(det) < EPS$c) throw new Error("Transform matrix is not invertible");
1549
1549
  const invDet = 1 / det;
1550
1550
  out[0] = (a11 * b11 - a12 * b10 + a13 * b09) * invDet;
1551
1551
  out[1] = (a02 * b10 - a01 * b11 - a03 * b09) * invDet;
@@ -1638,11 +1638,11 @@ class Transform {
1638
1638
  }
1639
1639
  /** Transform a point using homogeneous coordinates. */
1640
1640
  point(p2) {
1641
- return transformPoint$1(this.m, p2, 1);
1641
+ return transformPoint$2(this.m, p2, 1);
1642
1642
  }
1643
1643
  /** Transform a direction vector without translation. */
1644
1644
  vector(v) {
1645
- return transformPoint$1(this.m, v, 0);
1645
+ return transformPoint$2(this.m, v, 0);
1646
1646
  }
1647
1647
  /** Return the transform as a raw 4x4 matrix array. */
1648
1648
  toArray() {
@@ -3302,14 +3302,14 @@ function sweepPathToPolylineAdaptive(path2, baseSamples = 48) {
3302
3302
  pts.push(evalPathAt(path2, 1));
3303
3303
  return pts;
3304
3304
  }
3305
- const EPS$a = 1e-8;
3305
+ const EPS$b = 1e-8;
3306
3306
  const SUPPORTED_VERTICAL_EDGE_NAMES = ["vert-bl", "vert-br", "vert-tr", "vert-tl"];
3307
3307
  function midpoint$4(start, end) {
3308
3308
  return [(start[0] + end[0]) * 0.5, (start[1] + end[1]) * 0.5, (start[2] + end[2]) * 0.5];
3309
3309
  }
3310
3310
  function normalize$8(v) {
3311
3311
  const len2 = Math.hypot(v[0], v[1], v[2]);
3312
- if (len2 <= EPS$a) throw new Error("Edge feature selection requires a non-zero direction vector");
3312
+ if (len2 <= EPS$b) throw new Error("Edge feature selection requires a non-zero direction vector");
3313
3313
  return [v[0] / len2, v[1] / len2, v[2] / len2];
3314
3314
  }
3315
3315
  function subtract(a2, b) {
@@ -3391,7 +3391,7 @@ function rigidTransformForEdgeStep(step) {
3391
3391
  case "mirror": {
3392
3392
  const [nx0, ny0, nz0] = [step.normalX, step.normalY, step.normalZ];
3393
3393
  const len2 = Math.hypot(nx0, ny0, nz0);
3394
- if (len2 <= EPS$a) return Transform.identity();
3394
+ if (len2 <= EPS$b) return Transform.identity();
3395
3395
  const nx = nx0 / len2;
3396
3396
  const ny = ny0 / len2;
3397
3397
  const nz = nz0 / len2;
@@ -3702,7 +3702,7 @@ function isRectangleProfile(points) {
3702
3702
  return [next[0] - point2[0], next[1] - point2[1]];
3703
3703
  });
3704
3704
  const lengths2 = vectors.map(([x2, y2]) => Math.hypot(x2, y2));
3705
- if (lengths2.some((length4) => length4 <= EPS$a)) return false;
3705
+ if (lengths2.some((length4) => length4 <= EPS$b)) return false;
3706
3706
  const dot01 = vectors[0][0] * vectors[1][0] + vectors[0][1] * vectors[1][1];
3707
3707
  const dot12 = vectors[1][0] * vectors[2][0] + vectors[1][1] * vectors[2][1];
3708
3708
  const dot23 = vectors[2][0] * vectors[3][0] + vectors[2][1] * vectors[3][1];
@@ -5769,13 +5769,13 @@ function parseMeshFile(data, format) {
5769
5769
  return parse3mf(data);
5770
5770
  }
5771
5771
  }
5772
- const EPS$9 = 1e-8;
5772
+ const EPS$a = 1e-8;
5773
5773
  function length$3(v) {
5774
5774
  return Math.hypot(v[0], v[1], v[2]);
5775
5775
  }
5776
5776
  function normalize$7(v) {
5777
5777
  const len2 = length$3(v);
5778
- if (len2 < EPS$9) throw new Error("Plane normal must be non-zero");
5778
+ if (len2 < EPS$a) throw new Error("Plane normal must be non-zero");
5779
5779
  return [v[0] / len2, v[1] / len2, v[2] / len2];
5780
5780
  }
5781
5781
  function resolvePlaneOriginNormal(plane) {
@@ -5797,12 +5797,12 @@ function resolvePlaneOriginNormal(plane) {
5797
5797
  function rotationToPlaneSpace(normal) {
5798
5798
  const n = normalize$7(normal);
5799
5799
  const dot2 = n[2];
5800
- if (dot2 > 1 - EPS$9) {
5800
+ if (dot2 > 1 - EPS$a) {
5801
5801
  return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
5802
5802
  }
5803
5803
  let axis;
5804
5804
  let angle;
5805
- if (dot2 < -1 + EPS$9) {
5805
+ if (dot2 < -1 + EPS$a) {
5806
5806
  axis = [1, 0, 0];
5807
5807
  angle = Math.PI;
5808
5808
  } else {
@@ -8364,7 +8364,7 @@ function scale$6(v, s) {
8364
8364
  function sub$7(a2, b) {
8365
8365
  return [a2[0] - b[0], a2[1] - b[1], a2[2] - b[2]];
8366
8366
  }
8367
- function cross$7(a2, b) {
8367
+ function cross$8(a2, b) {
8368
8368
  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]];
8369
8369
  }
8370
8370
  function makeEdge(name, start, end, faceName, curve) {
@@ -8400,7 +8400,7 @@ function buildSurfaceSheetTopology(boundaries, options = {}) {
8400
8400
  const center = options.center ?? average$1(corners);
8401
8401
  const uAxis = normalizeAxis$1(sub$7(midpoint$3(u1Start, u1End), midpoint$3(u0Start, u0End)));
8402
8402
  const vAxis = normalizeAxis$1(sub$7(midpoint$3(v1Start, v1End), midpoint$3(v0Start, v0End)));
8403
- const normal = normalizeAxis$1(options.normal ?? cross$7(uAxis, vAxis));
8403
+ const normal = normalizeAxis$1(options.normal ?? cross$8(uAxis, vAxis));
8404
8404
  const faces = /* @__PURE__ */ new Map();
8405
8405
  faces.set(faceName, {
8406
8406
  name: faceName,
@@ -8432,7 +8432,7 @@ function attachSurfaceSheetTopology(shape, boundaries, options = {}) {
8432
8432
  });
8433
8433
  return shape;
8434
8434
  }
8435
- function cloneVec3$2(value) {
8435
+ function cloneVec3$4(value) {
8436
8436
  return [value[0], value[1], value[2]];
8437
8437
  }
8438
8438
  function cloneNurbsFaceTrimLoop(loop) {
@@ -8454,38 +8454,38 @@ function cloneFaceSurface(surface) {
8454
8454
  if (!surface) return void 0;
8455
8455
  switch (surface.kind) {
8456
8456
  case "plane":
8457
- return { kind: "plane", normal: cloneVec3$2(surface.normal) };
8457
+ return { kind: "plane", normal: cloneVec3$4(surface.normal) };
8458
8458
  case "cylinder":
8459
8459
  return {
8460
8460
  kind: "cylinder",
8461
- origin: cloneVec3$2(surface.origin),
8462
- axis: cloneVec3$2(surface.axis),
8461
+ origin: cloneVec3$4(surface.origin),
8462
+ axis: cloneVec3$4(surface.axis),
8463
8463
  radius: surface.radius,
8464
8464
  height: surface.height
8465
8465
  };
8466
8466
  case "cone":
8467
8467
  return {
8468
8468
  kind: "cone",
8469
- origin: cloneVec3$2(surface.origin),
8470
- axis: cloneVec3$2(surface.axis),
8469
+ origin: cloneVec3$4(surface.origin),
8470
+ axis: cloneVec3$4(surface.axis),
8471
8471
  radiusBottom: surface.radiusBottom,
8472
8472
  radiusTop: surface.radiusTop,
8473
8473
  height: surface.height
8474
8474
  };
8475
8475
  case "sphere":
8476
- return { kind: "sphere", center: cloneVec3$2(surface.center), radius: surface.radius };
8476
+ return { kind: "sphere", center: cloneVec3$4(surface.center), radius: surface.radius };
8477
8477
  case "torus":
8478
8478
  return {
8479
8479
  kind: "torus",
8480
- center: cloneVec3$2(surface.center),
8481
- axis: cloneVec3$2(surface.axis),
8480
+ center: cloneVec3$4(surface.center),
8481
+ axis: cloneVec3$4(surface.axis),
8482
8482
  majorRadius: surface.majorRadius,
8483
8483
  minorRadius: surface.minorRadius
8484
8484
  };
8485
8485
  case "ruled":
8486
8486
  return {
8487
8487
  kind: "ruled",
8488
- rails: surface.rails.map((rail2) => rail2.map(cloneVec3$2))
8488
+ rails: surface.rails.map((rail2) => rail2.map(cloneVec3$4))
8489
8489
  };
8490
8490
  case "nurbs":
8491
8491
  return {
@@ -8498,9 +8498,9 @@ function cloneEdgeCurve(curve) {
8498
8498
  if (!curve) return void 0;
8499
8499
  switch (curve.kind) {
8500
8500
  case "line":
8501
- return { ...curve, start: cloneVec3$2(curve.start), end: cloneVec3$2(curve.end) };
8501
+ return { ...curve, start: cloneVec3$4(curve.start), end: cloneVec3$4(curve.end) };
8502
8502
  case "circle":
8503
- return { ...curve, center: cloneVec3$2(curve.center), axis: cloneVec3$2(curve.axis) };
8503
+ return { ...curve, center: cloneVec3$4(curve.center), axis: cloneVec3$4(curve.axis) };
8504
8504
  case "surfaceIso":
8505
8505
  return { ...curve, parameterRange: [curve.parameterRange[0], curve.parameterRange[1]] };
8506
8506
  case "nurbsUv":
@@ -8518,7 +8518,7 @@ function cloneEdgeCurve(curve) {
8518
8518
  }
8519
8519
  }
8520
8520
  function makeLineEdgeCurve(start, end, faceName) {
8521
- return faceName ? { kind: "line", start: cloneVec3$2(start), end: cloneVec3$2(end), faceName } : { kind: "line", start: cloneVec3$2(start), end: cloneVec3$2(end) };
8521
+ return faceName ? { kind: "line", start: cloneVec3$4(start), end: cloneVec3$4(end), faceName } : { kind: "line", start: cloneVec3$4(start), end: cloneVec3$4(end) };
8522
8522
  }
8523
8523
  function edgeCurveFaceName(curve) {
8524
8524
  switch (curve.kind) {
@@ -10302,6 +10302,7 @@ function buildSweepLevelSetInput(profilePolygons, pathInput, options) {
10302
10302
  edgeLength: options.edgeLength
10303
10303
  };
10304
10304
  }
10305
+ const EPS$9 = 1e-9;
10305
10306
  function resamplePolygon(poly, targetCount) {
10306
10307
  if (poly.length < 2) return poly;
10307
10308
  if (targetCount <= 0) return [];
@@ -10339,6 +10340,78 @@ function resamplePolygon(poly, targetCount) {
10339
10340
  }
10340
10341
  return out;
10341
10342
  }
10343
+ function resamplePolygonByAngle(poly, targetCount, center = polygonCentroid$2(poly)) {
10344
+ if (poly.length < 3 || targetCount <= 0) return null;
10345
+ if (!isConvexPolygon(poly)) return null;
10346
+ const out = [];
10347
+ for (let index2 = 0; index2 < targetCount; index2 += 1) {
10348
+ const angle = index2 / targetCount * Math.PI * 2;
10349
+ const point2 = rayPolygonIntersection(center, [Math.cos(angle), Math.sin(angle)], poly);
10350
+ if (!point2) return null;
10351
+ out.push(point2);
10352
+ }
10353
+ return out;
10354
+ }
10355
+ function rayPolygonIntersection(origin, direction2, poly) {
10356
+ let bestT = Infinity;
10357
+ let best = null;
10358
+ for (let index2 = 0; index2 < poly.length; index2 += 1) {
10359
+ const a2 = poly[index2];
10360
+ const b = poly[(index2 + 1) % poly.length];
10361
+ const edge = [b[0] - a2[0], b[1] - a2[1]];
10362
+ const denom = cross$7(direction2, edge);
10363
+ if (Math.abs(denom) < EPS$9) continue;
10364
+ const delta = [a2[0] - origin[0], a2[1] - origin[1]];
10365
+ const rayT = cross$7(delta, edge) / denom;
10366
+ const edgeT = cross$7(delta, direction2) / denom;
10367
+ if (rayT >= -EPS$9 && edgeT >= -EPS$9 && edgeT <= 1 + EPS$9 && rayT < bestT) {
10368
+ bestT = rayT;
10369
+ best = [origin[0] + direction2[0] * rayT, origin[1] + direction2[1] * rayT];
10370
+ }
10371
+ }
10372
+ return best;
10373
+ }
10374
+ function polygonCentroid$2(poly) {
10375
+ let area2 = 0;
10376
+ let cx = 0;
10377
+ let cy = 0;
10378
+ for (let index2 = 0; index2 < poly.length; index2 += 1) {
10379
+ const a2 = poly[index2];
10380
+ const b = poly[(index2 + 1) % poly.length];
10381
+ const crossValue = cross$7(a2, b);
10382
+ area2 += crossValue;
10383
+ cx += (a2[0] + b[0]) * crossValue;
10384
+ cy += (a2[1] + b[1]) * crossValue;
10385
+ }
10386
+ if (Math.abs(area2) < EPS$9) return averagePoint(poly);
10387
+ return [cx / (3 * area2), cy / (3 * area2)];
10388
+ }
10389
+ function averagePoint(poly) {
10390
+ let x2 = 0;
10391
+ let y2 = 0;
10392
+ for (const point2 of poly) {
10393
+ x2 += point2[0];
10394
+ y2 += point2[1];
10395
+ }
10396
+ return [x2 / poly.length, y2 / poly.length];
10397
+ }
10398
+ function isConvexPolygon(poly) {
10399
+ let sign2 = 0;
10400
+ for (let index2 = 0; index2 < poly.length; index2 += 1) {
10401
+ const a2 = poly[index2];
10402
+ const b = poly[(index2 + 1) % poly.length];
10403
+ const c2 = poly[(index2 + 2) % poly.length];
10404
+ const turn = cross$7([b[0] - a2[0], b[1] - a2[1]], [c2[0] - b[0], c2[1] - b[1]]);
10405
+ if (Math.abs(turn) < EPS$9) continue;
10406
+ const currentSign = Math.sign(turn);
10407
+ if (sign2 !== 0 && currentSign !== sign2) return false;
10408
+ sign2 = currentSign;
10409
+ }
10410
+ return sign2 !== 0;
10411
+ }
10412
+ function cross$7(a2, b) {
10413
+ return a2[0] * b[1] - a2[1] * b[0];
10414
+ }
10342
10415
  function loftStitched(profiles2, heights, wasm) {
10343
10416
  if (profiles2.length < 2) return null;
10344
10417
  const classified = profiles2.map((loops) => classifyLoops(loops));
@@ -10467,8 +10540,10 @@ function stitchSingleLoopLoft(loops, heights, wasm) {
10467
10540
  maxPoints = Math.max(maxPoints, loop.length);
10468
10541
  }
10469
10542
  const N = Math.max(maxPoints, 24);
10543
+ const angularSamples = normalizedLoops.map((loop) => resamplePolygonByAngle(loop, N));
10544
+ const useAngularSamples = angularSamples.every((samples) => samples != null);
10470
10545
  const resampled = normalizedLoops.map((loop, i) => {
10471
- const pts2d = resamplePolygon(loop, N);
10546
+ const pts2d = useAngularSamples ? angularSamples[i] : resamplePolygon(loop, N);
10472
10547
  const z2 = heights[i];
10473
10548
  return pts2d.map(([x2, y2]) => [x2, y2, z2]);
10474
10549
  });
@@ -10530,7 +10605,7 @@ async function initManifoldWasm() {
10530
10605
  if (_wasm$1) return _wasm$1;
10531
10606
  performance.mark("manifold:start");
10532
10607
  const Module = (await __vitePreload(async () => {
10533
- const { default: __vite_default__ } = await import("./manifold-CRoBhJKH.js");
10608
+ const { default: __vite_default__ } = await import("./manifold-DjYsd7A_.js");
10534
10609
  return { default: __vite_default__ };
10535
10610
  }, true ? [] : void 0)).default;
10536
10611
  performance.mark("manifold:imported");
@@ -30608,17 +30683,17 @@ function emptyFaceTable() {
30608
30683
  blockedQueries: []
30609
30684
  };
30610
30685
  }
30611
- function cloneVec3$1(vec2) {
30686
+ function cloneVec3$3(vec2) {
30612
30687
  return [vec2[0], vec2[1], vec2[2]];
30613
30688
  }
30614
30689
  function cloneFaceRefValue(face) {
30615
30690
  return {
30616
30691
  ...face,
30617
- normal: cloneVec3$1(face.normal),
30618
- center: cloneVec3$1(face.center),
30692
+ normal: cloneVec3$3(face.normal),
30693
+ center: cloneVec3$3(face.center),
30619
30694
  query: cloneFaceQueryRef(face.query),
30620
- uAxis: face.uAxis ? cloneVec3$1(face.uAxis) : void 0,
30621
- vAxis: face.vAxis ? cloneVec3$1(face.vAxis) : void 0,
30695
+ uAxis: face.uAxis ? cloneVec3$3(face.uAxis) : void 0,
30696
+ vAxis: face.vAxis ? cloneVec3$3(face.vAxis) : void 0,
30622
30697
  surface: cloneFaceSurface(face.surface),
30623
30698
  descendant: face.descendant ? cloneFaceDescendantMetadata(face.descendant) : void 0
30624
30699
  };
@@ -31473,11 +31548,11 @@ function resolveShapeFaceTableInternal(plan, owner) {
31473
31548
  for (const descriptor2 of describeSheetMetalFaces(plan.model, plan.output)) {
31474
31549
  registerFace(table2, {
31475
31550
  name: descriptor2.name,
31476
- normal: cloneVec3$1(descriptor2.normal),
31477
- center: cloneVec3$1(descriptor2.center),
31551
+ normal: cloneVec3$3(descriptor2.normal),
31552
+ center: cloneVec3$3(descriptor2.center),
31478
31553
  planar: descriptor2.planar,
31479
- uAxis: descriptor2.uAxis ? cloneVec3$1(descriptor2.uAxis) : void 0,
31480
- vAxis: descriptor2.vAxis ? cloneVec3$1(descriptor2.vAxis) : void 0,
31554
+ uAxis: descriptor2.uAxis ? cloneVec3$3(descriptor2.uAxis) : void 0,
31555
+ vAxis: descriptor2.vAxis ? cloneVec3$3(descriptor2.vAxis) : void 0,
31481
31556
  query: createTrackedFaceQuery(descriptor2.name, owner),
31482
31557
  descendant: createFaceDescendantMetadata(descriptor2.semantic, descriptor2.memberNames, descriptor2.coplanar)
31483
31558
  });
@@ -31504,7 +31579,7 @@ function resolveShapeFaceTableInternal(plan, owner) {
31504
31579
  baseFace.center[2] - baseFace.normal[2] * plan.thickness
31505
31580
  ],
31506
31581
  normal: [-baseFace.normal[0], -baseFace.normal[1], -baseFace.normal[2]],
31507
- uAxis: baseFace.uAxis ? cloneVec3$1(baseFace.uAxis) : void 0,
31582
+ uAxis: baseFace.uAxis ? cloneVec3$3(baseFace.uAxis) : void 0,
31508
31583
  vAxis: baseFace.vAxis ? [-baseFace.vAxis[0], -baseFace.vAxis[1], -baseFace.vAxis[2]] : void 0,
31509
31584
  query: createdQuery
31510
31585
  });
@@ -31570,15 +31645,15 @@ function resolveShapeFaceTableInternal(plan, owner) {
31570
31645
  if (counterboreFloorQuery && plan.hole.counterbore) {
31571
31646
  registerFace(table2, {
31572
31647
  name: "counterbore-floor",
31573
- normal: cloneVec3$1(workplane.normal),
31648
+ normal: cloneVec3$3(workplane.normal),
31574
31649
  center: [
31575
31650
  origin[0] + inward[0] * plan.hole.counterbore.depth,
31576
31651
  origin[1] + inward[1] * plan.hole.counterbore.depth,
31577
31652
  origin[2] + inward[2] * plan.hole.counterbore.depth
31578
31653
  ],
31579
31654
  planar: true,
31580
- uAxis: cloneVec3$1(workplane.u),
31581
- vAxis: cloneVec3$1(workplane.v),
31655
+ uAxis: cloneVec3$3(workplane.u),
31656
+ vAxis: cloneVec3$3(workplane.v),
31582
31657
  query: counterboreFloorQuery
31583
31658
  });
31584
31659
  }
@@ -31602,11 +31677,11 @@ function resolveShapeFaceTableInternal(plan, owner) {
31602
31677
  if (floorQuery) {
31603
31678
  registerFace(table2, {
31604
31679
  name: "floor",
31605
- normal: cloneVec3$1(workplane.normal),
31680
+ normal: cloneVec3$3(workplane.normal),
31606
31681
  center: [origin[0] + inward[0] * forward.depth, origin[1] + inward[1] * forward.depth, origin[2] + inward[2] * forward.depth],
31607
31682
  planar: true,
31608
- uAxis: cloneVec3$1(workplane.u),
31609
- vAxis: cloneVec3$1(workplane.v),
31683
+ uAxis: cloneVec3$3(workplane.u),
31684
+ vAxis: cloneVec3$3(workplane.v),
31610
31685
  query: floorQuery
31611
31686
  });
31612
31687
  }
@@ -31617,7 +31692,7 @@ function resolveShapeFaceTableInternal(plan, owner) {
31617
31692
  normal: [-workplane.normal[0], -workplane.normal[1], -workplane.normal[2]],
31618
31693
  center: [origin[0] - inward[0] * reverse.depth, origin[1] - inward[1] * reverse.depth, origin[2] - inward[2] * reverse.depth],
31619
31694
  planar: true,
31620
- uAxis: cloneVec3$1(workplane.u),
31695
+ uAxis: cloneVec3$3(workplane.u),
31621
31696
  vAxis: [-workplane.v[0], -workplane.v[1], -workplane.v[2]],
31622
31697
  query: capQuery
31623
31698
  });
@@ -31796,11 +31871,11 @@ function resolveShapeFaceTableInternal(plan, owner) {
31796
31871
  })();
31797
31872
  registerFace(table2, {
31798
31873
  name: "floor",
31799
- normal: cloneVec3$1(placement.workplane.normal),
31874
+ normal: cloneVec3$3(placement.workplane.normal),
31800
31875
  center: floorCenter,
31801
31876
  planar: true,
31802
- uAxis: cloneVec3$1(placement.workplane.u),
31803
- vAxis: cloneVec3$1(placement.workplane.v),
31877
+ uAxis: cloneVec3$3(placement.workplane.u),
31878
+ vAxis: cloneVec3$3(placement.workplane.v),
31804
31879
  query: floorQuery
31805
31880
  });
31806
31881
  }
@@ -31815,7 +31890,7 @@ function resolveShapeFaceTableInternal(plan, owner) {
31815
31890
  origin[2] - depthDir[2] * reverse.depth
31816
31891
  ],
31817
31892
  planar: true,
31818
- uAxis: cloneVec3$1(placement.workplane.u),
31893
+ uAxis: cloneVec3$3(placement.workplane.u),
31819
31894
  vAxis: [-placement.workplane.v[0], -placement.workplane.v[1], -placement.workplane.v[2]],
31820
31895
  query: capQuery
31821
31896
  });
@@ -32001,7 +32076,7 @@ function preservedShapeFaceQueries(basePlan) {
32001
32076
  return listShapeFaceQueries(basePlan);
32002
32077
  }
32003
32078
  const PLACEMENT_REFERENCE_KINDS = ["points", "edges", "surfaces", "objects"];
32004
- function cloneVec3(value, label) {
32079
+ function cloneVec3$2(value, label) {
32005
32080
  if (!Array.isArray(value) || value.length < 3) {
32006
32081
  throw new Error(`${label} must be a [x, y, z] tuple`);
32007
32082
  }
@@ -32027,23 +32102,23 @@ function isBoundsObject(value) {
32027
32102
  function toObjectBounds(value, label) {
32028
32103
  if (isBoundsObject(value)) {
32029
32104
  return {
32030
- min: cloneVec3(value.min, `${label}.min`),
32031
- max: cloneVec3(value.max, `${label}.max`)
32105
+ min: cloneVec3$2(value.min, `${label}.min`),
32106
+ max: cloneVec3$2(value.max, `${label}.max`)
32032
32107
  };
32033
32108
  }
32034
32109
  if (typeof value === "object" && value != null) {
32035
32110
  if ("boundingBox" in value && typeof value.boundingBox === "function") {
32036
32111
  const bounds = value.boundingBox();
32037
32112
  return {
32038
- min: cloneVec3(bounds.min, `${label}.min`),
32039
- max: cloneVec3(bounds.max, `${label}.max`)
32113
+ min: cloneVec3$2(bounds.min, `${label}.min`),
32114
+ max: cloneVec3$2(bounds.max, `${label}.max`)
32040
32115
  };
32041
32116
  }
32042
32117
  if ("_bbox" in value && typeof value._bbox === "function") {
32043
32118
  const bounds = value._bbox();
32044
32119
  return {
32045
- min: cloneVec3(bounds.min, `${label}.min`),
32046
- max: cloneVec3(bounds.max, `${label}.max`)
32120
+ min: cloneVec3$2(bounds.min, `${label}.min`),
32121
+ max: cloneVec3$2(bounds.max, `${label}.max`)
32047
32122
  };
32048
32123
  }
32049
32124
  }
@@ -32087,24 +32162,24 @@ function createPlacementReferences() {
32087
32162
  function clonePlacementReferences(refs) {
32088
32163
  const out = createPlacementReferences();
32089
32164
  for (const [name, point2] of Object.entries(refs.points)) {
32090
- out.points[name] = cloneVec3(point2, `points.${name}`);
32165
+ out.points[name] = cloneVec3$2(point2, `points.${name}`);
32091
32166
  }
32092
32167
  for (const [name, edge] of Object.entries(refs.edges)) {
32093
32168
  out.edges[name] = {
32094
- start: cloneVec3(edge.start, `edges.${name}.start`),
32095
- end: cloneVec3(edge.end, `edges.${name}.end`)
32169
+ start: cloneVec3$2(edge.start, `edges.${name}.start`),
32170
+ end: cloneVec3$2(edge.end, `edges.${name}.end`)
32096
32171
  };
32097
32172
  }
32098
32173
  for (const [name, surface] of Object.entries(refs.surfaces)) {
32099
32174
  out.surfaces[name] = {
32100
- center: cloneVec3(surface.center, `surfaces.${name}.center`),
32101
- normal: cloneVec3(surface.normal, `surfaces.${name}.normal`)
32175
+ center: cloneVec3$2(surface.center, `surfaces.${name}.center`),
32176
+ normal: cloneVec3$2(surface.normal, `surfaces.${name}.normal`)
32102
32177
  };
32103
32178
  }
32104
32179
  for (const [name, objectRef] of Object.entries(refs.objects)) {
32105
32180
  out.objects[name] = {
32106
- min: cloneVec3(objectRef.min, `objects.${name}.min`),
32107
- max: cloneVec3(objectRef.max, `objects.${name}.max`)
32181
+ min: cloneVec3$2(objectRef.min, `objects.${name}.min`),
32182
+ max: cloneVec3$2(objectRef.max, `objects.${name}.max`)
32108
32183
  };
32109
32184
  }
32110
32185
  return out;
@@ -32112,18 +32187,18 @@ function clonePlacementReferences(refs) {
32112
32187
  function normalizePlacementReferenceInput(input = {}) {
32113
32188
  const out = createPlacementReferences();
32114
32189
  for (const [name, point2] of Object.entries(input.points ?? {})) {
32115
- out.points[name] = cloneVec3(point2, `points.${name}`);
32190
+ out.points[name] = cloneVec3$2(point2, `points.${name}`);
32116
32191
  }
32117
32192
  for (const [name, edge] of Object.entries(input.edges ?? {})) {
32118
32193
  out.edges[name] = {
32119
- start: cloneVec3(edge.start, `edges.${name}.start`),
32120
- end: cloneVec3(edge.end, `edges.${name}.end`)
32194
+ start: cloneVec3$2(edge.start, `edges.${name}.start`),
32195
+ end: cloneVec3$2(edge.end, `edges.${name}.end`)
32121
32196
  };
32122
32197
  }
32123
32198
  for (const [name, surface] of Object.entries(input.surfaces ?? {})) {
32124
32199
  out.surfaces[name] = {
32125
- center: cloneVec3(surface.center, `surfaces.${name}.center`),
32126
- normal: normalizeVector$1(cloneVec3(surface.normal, `surfaces.${name}.normal`))
32200
+ center: cloneVec3$2(surface.center, `surfaces.${name}.center`),
32201
+ normal: normalizeVector$1(cloneVec3$2(surface.normal, `surfaces.${name}.normal`))
32127
32202
  };
32128
32203
  }
32129
32204
  for (const [name, objectRef] of Object.entries(input.objects ?? {})) {
@@ -32134,23 +32209,23 @@ function normalizePlacementReferenceInput(input = {}) {
32134
32209
  function mergePlacementReferences(...refsList) {
32135
32210
  const out = createPlacementReferences();
32136
32211
  for (const refs of refsList) {
32137
- for (const [name, point2] of Object.entries(refs.points)) out.points[name] = cloneVec3(point2, `points.${name}`);
32212
+ for (const [name, point2] of Object.entries(refs.points)) out.points[name] = cloneVec3$2(point2, `points.${name}`);
32138
32213
  for (const [name, edge] of Object.entries(refs.edges)) {
32139
32214
  out.edges[name] = {
32140
- start: cloneVec3(edge.start, `edges.${name}.start`),
32141
- end: cloneVec3(edge.end, `edges.${name}.end`)
32215
+ start: cloneVec3$2(edge.start, `edges.${name}.start`),
32216
+ end: cloneVec3$2(edge.end, `edges.${name}.end`)
32142
32217
  };
32143
32218
  }
32144
32219
  for (const [name, surface] of Object.entries(refs.surfaces)) {
32145
32220
  out.surfaces[name] = {
32146
- center: cloneVec3(surface.center, `surfaces.${name}.center`),
32147
- normal: cloneVec3(surface.normal, `surfaces.${name}.normal`)
32221
+ center: cloneVec3$2(surface.center, `surfaces.${name}.center`),
32222
+ normal: cloneVec3$2(surface.normal, `surfaces.${name}.normal`)
32148
32223
  };
32149
32224
  }
32150
32225
  for (const [name, objectRef] of Object.entries(refs.objects)) {
32151
32226
  out.objects[name] = {
32152
- min: cloneVec3(objectRef.min, `objects.${name}.min`),
32153
- max: cloneVec3(objectRef.max, `objects.${name}.max`)
32227
+ min: cloneVec3$2(objectRef.min, `objects.${name}.min`),
32228
+ max: cloneVec3$2(objectRef.max, `objects.${name}.max`)
32154
32229
  };
32155
32230
  }
32156
32231
  }
@@ -32194,7 +32269,7 @@ function resolvePointFromKind(refs, kind, name, selector, originalRef) {
32194
32269
  const point2 = refs.points[name];
32195
32270
  if (!point2) return null;
32196
32271
  if (selector != null) placementRefSelectorError(originalRef, "does not support selectors");
32197
- return cloneVec3(point2, `points.${name}`);
32272
+ return cloneVec3$2(point2, `points.${name}`);
32198
32273
  }
32199
32274
  case "edges": {
32200
32275
  const edge = refs.edges[name];
@@ -32202,8 +32277,8 @@ function resolvePointFromKind(refs, kind, name, selector, originalRef) {
32202
32277
  if (selector == null || selector === "midpoint" || selector === "center") {
32203
32278
  return midpoint$2(edge.start, edge.end);
32204
32279
  }
32205
- if (selector === "start") return cloneVec3(edge.start, `edges.${name}.start`);
32206
- if (selector === "end") return cloneVec3(edge.end, `edges.${name}.end`);
32280
+ if (selector === "start") return cloneVec3$2(edge.start, `edges.${name}.start`);
32281
+ if (selector === "end") return cloneVec3$2(edge.end, `edges.${name}.end`);
32207
32282
  placementRefSelectorError(originalRef, "supports only .start, .end, or .midpoint");
32208
32283
  }
32209
32284
  case "surfaces": {
@@ -32212,7 +32287,7 @@ function resolvePointFromKind(refs, kind, name, selector, originalRef) {
32212
32287
  if (selector != null && selector !== "center") {
32213
32288
  placementRefSelectorError(originalRef, "supports only .center");
32214
32289
  }
32215
- return cloneVec3(surface.center, `surfaces.${name}.center`);
32290
+ return cloneVec3$2(surface.center, `surfaces.${name}.center`);
32216
32291
  }
32217
32292
  case "objects": {
32218
32293
  const objectRef = refs.objects[name];
@@ -41412,7 +41487,7 @@ function rodrigues(rv) {
41412
41487
  function rotateVec3(R, v) {
41413
41488
  return [R[0] * v[0] + R[1] * v[1] + R[2] * v[2], R[3] * v[0] + R[4] * v[1] + R[5] * v[2], R[6] * v[0] + R[7] * v[1] + R[8] * v[2]];
41414
41489
  }
41415
- function transformPoint(rv, translation, point2) {
41490
+ function transformPoint$1(rv, translation, point2) {
41416
41491
  const R = rodrigues(rv);
41417
41492
  const rotated = rotateVec3(R, point2);
41418
41493
  return [rotated[0] + translation[0], rotated[1] + translation[1], rotated[2] + translation[2]];
@@ -41642,7 +41717,7 @@ function createContext(bodies) {
41642
41717
  toWorld(bodyId, point2) {
41643
41718
  const body = bodies.get(bodyId);
41644
41719
  if (!body) throw new Error(`Unknown body: ${bodyId}`);
41645
- return transformPoint(body.rotation, body.position, point2);
41720
+ return transformPoint$1(body.rotation, body.position, point2);
41646
41721
  },
41647
41722
  toWorldDir(bodyId, dir) {
41648
41723
  const body = bodies.get(bodyId);
@@ -41656,7 +41731,7 @@ function createContext(bodies) {
41656
41731
  if (!face) throw new Error(`Unknown face "${faceName}" on body "${bodyId}"`);
41657
41732
  return {
41658
41733
  normal: normalize3(transformDir(body.rotation, face.normal)),
41659
- center: transformPoint(body.rotation, body.position, face.center)
41734
+ center: transformPoint$1(body.rotation, body.position, face.center)
41660
41735
  };
41661
41736
  },
41662
41737
  worldAxis(bodyId, axisName) {
@@ -41665,7 +41740,7 @@ function createContext(bodies) {
41665
41740
  const axis = body.axes.get(axisName);
41666
41741
  if (!axis) throw new Error(`Unknown axis "${axisName}" on body "${bodyId}"`);
41667
41742
  return {
41668
- origin: transformPoint(body.rotation, body.position, axis.origin),
41743
+ origin: transformPoint$1(body.rotation, body.position, axis.origin),
41669
41744
  direction: normalize3(transformDir(body.rotation, axis.direction))
41670
41745
  };
41671
41746
  },
@@ -41674,7 +41749,7 @@ function createContext(bodies) {
41674
41749
  if (!body) throw new Error(`Unknown body: ${bodyId}`);
41675
41750
  const pt = body.points.get(pointName);
41676
41751
  if (!pt) throw new Error(`Unknown point "${pointName}" on body "${bodyId}"`);
41677
- return transformPoint(body.rotation, body.position, pt.position);
41752
+ return transformPoint$1(body.rotation, body.position, pt.position);
41678
41753
  }
41679
41754
  };
41680
41755
  }
@@ -47218,10 +47293,8 @@ class PathBuilder {
47218
47293
  if (radius <= 0) throw new Error("fillet: radius must be positive");
47219
47294
  const n = this.segs.length;
47220
47295
  if (n < 2) throw new Error("fillet: need at least 2 segments before a fillet");
47221
- const prev = this.segs[n - 2];
47222
47296
  const curr = this.segs[n - 1];
47223
- curr.kind === "line" || curr.kind === "move" ? prev.kind === "line" || prev.kind === "move" ? 0 : 0 : 0;
47224
- const { trimA, trimB, arcSeg } = this.computeFilletGeom(radius);
47297
+ const { trimA, arcSeg } = this.computeFilletGeom(radius);
47225
47298
  if (!arcSeg) throw new Error("fillet: cannot fillet these segments (parallel or degenerate)");
47226
47299
  this.trimLastSegEnd(n - 2, trimA[0], trimA[1]);
47227
47300
  const trimmedSeg = { ...curr };
@@ -47293,7 +47366,6 @@ class PathBuilder {
47293
47366
  }
47294
47367
  getSegDirAt(seg, which) {
47295
47368
  if (seg.kind === "line" || seg.kind === "move") {
47296
- this.segs.length;
47297
47369
  const idx = this.segs.indexOf(seg);
47298
47370
  if (seg.kind === "line") {
47299
47371
  let sx, sy;
@@ -47535,6 +47607,41 @@ class PathBuilder {
47535
47607
  }
47536
47608
  return pts;
47537
47609
  }
47610
+ /**
47611
+ * Return the open path as a sampled 2D polyline.
47612
+ *
47613
+ * This is for construction geometry such as guide rails, measured centerlines,
47614
+ * and curve-driven helpers where the authored path should stay open instead of
47615
+ * becoming a filled sketch or stroked profile.
47616
+ *
47617
+ * **Example**
47618
+ *
47619
+ * ```ts
47620
+ * const rail = path()
47621
+ * .moveTo(24, 0)
47622
+ * .bezierTo(32, 44, 28, 92, 18, 120)
47623
+ * .toPolyline();
47624
+ * ```
47625
+ *
47626
+ * @returns A sampled open polyline.
47627
+ * @category Path Builder
47628
+ */
47629
+ toPolyline() {
47630
+ const moveCount = this.segs.filter((seg) => seg.kind === "move").length;
47631
+ if (moveCount > 1) {
47632
+ throw new Error("path().toPolyline() supports one continuous open path. Use separate path() builders for separate rails.");
47633
+ }
47634
+ const pts = [];
47635
+ for (const point2 of this.tessellate()) {
47636
+ if (!point2.every(Number.isFinite)) throw new Error("path().toPolyline() produced a non-finite point");
47637
+ const previous = pts[pts.length - 1];
47638
+ if (!previous || Math.hypot(point2[0] - previous[0], point2[1] - previous[1]) > 1e-9) {
47639
+ pts.push(point2);
47640
+ }
47641
+ }
47642
+ if (pts.length < 2) throw new Error("path().toPolyline() needs at least 2 points");
47643
+ return pts;
47644
+ }
47538
47645
  // ── Output ────────────────────────────────────────────────────────────────
47539
47646
  /**
47540
47647
  * Close the path and return a filled `Sketch`.
@@ -49392,7 +49499,7 @@ function spurGear(options) {
49392
49499
  });
49393
49500
  return attachGearMeta(shapeWithConnectors, meta2);
49394
49501
  }
49395
- function requirePositive$7(scope, name, value) {
49502
+ function requirePositive$8(scope, name, value) {
49396
49503
  if (!isFinitePositive(value)) throw new Error(`${scope}: "${name}" must be > 0`);
49397
49504
  }
49398
49505
  function requireOptionalBore(scope, boreDiameter, maxDiameter) {
@@ -49414,8 +49521,8 @@ function cutBore$1(shape, boreDiameter) {
49414
49521
  return shape.subtract(cutter);
49415
49522
  }
49416
49523
  function gearBodyDisk(options) {
49417
- requirePositive$7("gearBodyDisk", "outerRadius", options.outerRadius);
49418
- requirePositive$7("gearBodyDisk", "faceWidth", options.faceWidth);
49524
+ requirePositive$8("gearBodyDisk", "outerRadius", options.outerRadius);
49525
+ requirePositive$8("gearBodyDisk", "faceWidth", options.faceWidth);
49419
49526
  const bore = requireOptionalBore("gearBodyDisk", options.boreDiameter, options.outerRadius * 2);
49420
49527
  const segments = resolveSegments(options.segments);
49421
49528
  const outer = circle2d(options.outerRadius, segments);
@@ -49423,14 +49530,14 @@ function gearBodyDisk(options) {
49423
49530
  return sketchExtrude(profile, options.faceWidth);
49424
49531
  }
49425
49532
  function gearBodyDiskWithHub(options) {
49426
- requirePositive$7("gearBodyDiskWithHub", "hubDiameter", options.hubDiameter);
49533
+ requirePositive$8("gearBodyDiskWithHub", "hubDiameter", options.hubDiameter);
49427
49534
  if (options.hubDiameter >= options.outerRadius * 2) {
49428
49535
  throw new Error('gearBodyDiskWithHub: "hubDiameter" must be smaller than the outer diameter');
49429
49536
  }
49430
49537
  const bore = requireOptionalBore("gearBodyDiskWithHub", options.boreDiameter, options.hubDiameter);
49431
49538
  const base = gearBodyDisk({ ...options, boreDiameter: 0 });
49432
49539
  const hubFaceWidth = options.hubFaceWidth ?? options.faceWidth * 1.5;
49433
- requirePositive$7("gearBodyDiskWithHub", "hubFaceWidth", hubFaceWidth);
49540
+ requirePositive$8("gearBodyDiskWithHub", "hubFaceWidth", hubFaceWidth);
49434
49541
  const hub = cylinder(hubFaceWidth, options.hubDiameter * 0.5, void 0, options.segments).translate(
49435
49542
  0,
49436
49543
  0,
@@ -49439,11 +49546,11 @@ function gearBodyDiskWithHub(options) {
49439
49546
  return cutBore$1(base.add(hub), bore);
49440
49547
  }
49441
49548
  function gearBodySpoked(options) {
49442
- requirePositive$7("gearBodySpoked", "outerRadius", options.outerRadius);
49443
- requirePositive$7("gearBodySpoked", "faceWidth", options.faceWidth);
49444
- requirePositive$7("gearBodySpoked", "rimWidth", options.rimWidth);
49445
- requirePositive$7("gearBodySpoked", "hubDiameter", options.hubDiameter);
49446
- requirePositive$7("gearBodySpoked", "spokeWidth", options.spokeWidth);
49549
+ requirePositive$8("gearBodySpoked", "outerRadius", options.outerRadius);
49550
+ requirePositive$8("gearBodySpoked", "faceWidth", options.faceWidth);
49551
+ requirePositive$8("gearBodySpoked", "rimWidth", options.rimWidth);
49552
+ requirePositive$8("gearBodySpoked", "hubDiameter", options.hubDiameter);
49553
+ requirePositive$8("gearBodySpoked", "spokeWidth", options.spokeWidth);
49447
49554
  if (!Number.isInteger(options.spokeCount) || options.spokeCount < 2) {
49448
49555
  throw new Error('gearBodySpoked: "spokeCount" must be an integer >= 2');
49449
49556
  }
@@ -49466,12 +49573,12 @@ function gearBodySpoked(options) {
49466
49573
  }
49467
49574
  function gearBodyFromProfile(profile, options) {
49468
49575
  if (!(profile instanceof Sketch)) throw new Error('gearBodyFromProfile: "profile" must be a Sketch');
49469
- requirePositive$7("gearBodyFromProfile", "faceWidth", options.faceWidth);
49576
+ requirePositive$8("gearBodyFromProfile", "faceWidth", options.faceWidth);
49470
49577
  const bore = options.boreDiameter ?? 0;
49471
49578
  if (!Number.isFinite(bore) || bore < 0) throw new Error('gearBodyFromProfile: "boreDiameter" must be >= 0');
49472
49579
  return cutBore$1(sketchExtrude(profile, options.faceWidth), bore);
49473
49580
  }
49474
- function requirePositive$6(scope, name, value) {
49581
+ function requirePositive$7(scope, name, value) {
49475
49582
  if (!isFinitePositive(value)) throw new Error(`${scope}: "${name}" must be > 0`);
49476
49583
  }
49477
49584
  function requireFiniteAngle(scope, name, value) {
@@ -49533,7 +49640,7 @@ function buildSpurTeethRegion(options, name, faceWidth) {
49533
49640
  }
49534
49641
  function buildSolidArcRegion(options, name, faceWidth) {
49535
49642
  const scope = "driveWheel.addSolidArcBetween";
49536
- requirePositive$6(scope, "outerRadius", options.outerRadius);
49643
+ requirePositive$7(scope, "outerRadius", options.outerRadius);
49537
49644
  const innerRadius = options.innerRadius ?? 0;
49538
49645
  if (!Number.isFinite(innerRadius) || innerRadius < 0) throw new Error(`${scope}: "innerRadius" must be >= 0`);
49539
49646
  if (innerRadius >= options.outerRadius) throw new Error(`${scope}: "innerRadius" must be smaller than "outerRadius"`);
@@ -49599,7 +49706,7 @@ class DriveWheelBuilder {
49599
49706
  __publicField(this, "boreDiameter");
49600
49707
  __publicField(this, "regions", []);
49601
49708
  if (options.body !== void 0 && !(options.body instanceof Shape$1)) throw new Error('driveWheel: "body" must be a Shape');
49602
- if (options.faceWidth !== void 0) requirePositive$6("driveWheel", "faceWidth", options.faceWidth);
49709
+ if (options.faceWidth !== void 0) requirePositive$7("driveWheel", "faceWidth", options.faceWidth);
49603
49710
  const boreDiameter = options.boreDiameter ?? 0;
49604
49711
  if (!Number.isFinite(boreDiameter) || boreDiameter < 0) throw new Error('driveWheel: "boreDiameter" must be >= 0');
49605
49712
  this.body = options.body;
@@ -49634,7 +49741,7 @@ class DriveWheelBuilder {
49634
49741
  if (options.innerRadius !== void 0 && (!Number.isFinite(options.innerRadius) || options.innerRadius < 0)) {
49635
49742
  throw new Error(`${scope}: "innerRadius" must be >= 0`);
49636
49743
  }
49637
- if (options.outerRadius !== void 0) requirePositive$6(scope, "outerRadius", options.outerRadius);
49744
+ if (options.outerRadius !== void 0) requirePositive$7(scope, "outerRadius", options.outerRadius);
49638
49745
  this.regions.push({
49639
49746
  shape: shape.clone(),
49640
49747
  meta: {
@@ -49700,7 +49807,7 @@ class DriveWheelBuilder {
49700
49807
  resolveFaceWidth(scope, localFaceWidth) {
49701
49808
  const faceWidth = localFaceWidth ?? this.faceWidth;
49702
49809
  if (faceWidth === void 0) throw new Error(`${scope}: "faceWidth" is required unless driveWheel({ faceWidth }) was set`);
49703
- requirePositive$6(scope, "faceWidth", faceWidth);
49810
+ requirePositive$7(scope, "faceWidth", faceWidth);
49704
49811
  if (this.faceWidth !== void 0 && localFaceWidth !== void 0 && Math.abs(this.faceWidth - localFaceWidth) > EPSILON$1) {
49705
49812
  throw new Error(`${scope}: region faceWidth must match driveWheel faceWidth`);
49706
49813
  }
@@ -50853,6 +50960,1867 @@ function washer(size, options) {
50853
50960
  const bore = cylinder(dims.t + 1, dims.id / 2, void 0, segs);
50854
50961
  return outer.subtract(bore);
50855
50962
  }
50963
+ function requirePositive$6(value, name) {
50964
+ if (!Number.isFinite(value) || value <= 0) throw new Error(`${name} must be a positive finite number`);
50965
+ return value;
50966
+ }
50967
+ function requireNonNegative(value, name) {
50968
+ if (!Number.isFinite(value) || value < 0) throw new Error(`${name} must be a non-negative finite number`);
50969
+ return value;
50970
+ }
50971
+ function metricWasherSizeForPin(pinDiameter) {
50972
+ if (pinDiameter <= 2) return "M2";
50973
+ if (pinDiameter <= 2.5) return "M2.5";
50974
+ if (pinDiameter <= 3) return "M3";
50975
+ if (pinDiameter <= 4) return "M4";
50976
+ if (pinDiameter <= 5) return "M5";
50977
+ if (pinDiameter <= 6) return "M6";
50978
+ if (pinDiameter <= 8) return "M8";
50979
+ return "M10";
50980
+ }
50981
+ function cylinderAlongX(length4, radius, xCenter, segments) {
50982
+ return cylinder(length4, radius, void 0, segments).pointAlong([1, 0, 0]).translate(xCenter - length4 / 2, 0, 0);
50983
+ }
50984
+ function tubeAlongX(length4, outerRadius, innerRadius, xCenter, segments) {
50985
+ return cylinderAlongX(length4, outerRadius, xCenter, segments).subtract(cylinderAlongX(length4 + 0.4, innerRadius, xCenter, segments));
50986
+ }
50987
+ function cylinderAlongY(length4, radius, yCenter, segments) {
50988
+ return cylinder(length4, radius, void 0, segments).pointAlong([0, 1, 0]).translate(0, yCenter - length4 / 2, 0);
50989
+ }
50990
+ function tubeAlongY(length4, outerRadius, innerRadius, yCenter, segments) {
50991
+ return cylinderAlongY(length4, outerRadius, yCenter, segments).subtract(cylinderAlongY(length4 + 0.4, innerRadius, yCenter, segments));
50992
+ }
50993
+ function tubeAlongZ(height, outerRadius, innerRadius, segments) {
50994
+ return cylinder(height, outerRadius, void 0, segments).subtract(
50995
+ cylinder(height + 0.4, innerRadius, void 0, segments).translate(0, 0, -0.2)
50996
+ );
50997
+ }
50998
+ function washerAlongX(size, xCenter, segments) {
50999
+ const dims = WASHER_TABLE[size];
51000
+ return washer(size, { segments }).pointAlong([1, 0, 0]).translate(xCenter - dims.t / 2, 0, 0);
51001
+ }
51002
+ function resolveBoltInset(raw, fallback) {
51003
+ if (raw === void 0) return [fallback, fallback];
51004
+ if (typeof raw === "number") return [requirePositive$6(raw, "boltInset"), requirePositive$6(raw, "boltInset")];
51005
+ if (raw.length !== 2) throw new Error("boltInset tuple must be [x, y]");
51006
+ return [requirePositive$6(raw[0], "boltInset[0]"), requirePositive$6(raw[1], "boltInset[1]")];
51007
+ }
51008
+ function validateBoltPositionsForServiceCover(args) {
51009
+ args.positions.forEach(([x2, y2], index2) => {
51010
+ if (!Number.isFinite(x2) || !Number.isFinite(y2)) {
51011
+ throw new Error(`boltedServiceCover: boltPositions[${index2}] must contain finite numbers`);
51012
+ }
51013
+ if (Math.abs(x2) + args.holeRadius >= args.coverWidth / 2 || Math.abs(y2) + args.holeRadius >= args.coverDepth / 2) {
51014
+ throw new Error(`boltedServiceCover: boltPositions[${index2}] is too close to the cover edge`);
51015
+ }
51016
+ const overlapsOpening = Math.abs(x2) - args.holeRadius <= args.openingWidth / 2 && Math.abs(y2) - args.holeRadius <= args.openingDepth / 2;
51017
+ if (overlapsOpening) {
51018
+ throw new Error(
51019
+ `boltedServiceCover: boltPositions[${index2}] lands over the service opening; decrease boltInset, increase ledgeWidth, or provide a smaller opening`
51020
+ );
51021
+ }
51022
+ });
51023
+ }
51024
+ function placeCutterAtPositions(cutter, positions, z2) {
51025
+ return union(...positions.map(([x2, y2]) => cutter.translate(x2, y2, z2)));
51026
+ }
51027
+ function boltedServiceCover(options) {
51028
+ const width = requirePositive$6(options.width, "width");
51029
+ const depth = requirePositive$6(options.depth, "depth");
51030
+ const coverThickness = requirePositive$6(options.coverThickness ?? 3, "coverThickness");
51031
+ const parentThickness = requirePositive$6(options.parentThickness ?? 8, "parentThickness");
51032
+ const ledgeWidth = requirePositive$6(options.ledgeWidth ?? 8, "ledgeWidth");
51033
+ const gasketThickness = Math.max(0, options.gasketThickness ?? 0.8);
51034
+ const gasketInset = Math.max(0, options.gasketInset ?? 2);
51035
+ const screwSize = options.screwSize ?? "M4";
51036
+ const segments = options.segments ?? 36;
51037
+ const sizeData = METRIC_HOLE_TABLE[screwSize];
51038
+ if (!sizeData) throw new Error(`boltedServiceCover: unsupported screwSize "${screwSize}"`);
51039
+ const screwLength = requirePositive$6(
51040
+ options.screwLength ?? parentThickness + gasketThickness + coverThickness + 4,
51041
+ "screwLength"
51042
+ );
51043
+ const coverFit = options.coverFit ?? "normal";
51044
+ const counterboreEnabled = options.counterbore ?? true;
51045
+ const [insetX, insetY] = resolveBoltInset(options.boltInset, Math.max(ledgeWidth * 0.65, sizeData.head * 0.75));
51046
+ if (insetX * 2 >= width || insetY * 2 >= depth) {
51047
+ throw new Error("boltedServiceCover: boltInset leaves no room for a four-corner bolt pattern");
51048
+ }
51049
+ const boltPositions = options.boltPositions ?? [
51050
+ [-width / 2 + insetX, -depth / 2 + insetY],
51051
+ [width / 2 - insetX, -depth / 2 + insetY],
51052
+ [-width / 2 + insetX, depth / 2 - insetY],
51053
+ [width / 2 - insetX, depth / 2 - insetY]
51054
+ ];
51055
+ if (boltPositions.length === 0) throw new Error("boltedServiceCover: boltPositions must contain at least one point");
51056
+ const parentWidth = width + ledgeWidth * 2;
51057
+ const parentDepth = depth + ledgeWidth * 2;
51058
+ const openingWidth = Math.max(1, width - ledgeWidth * 2);
51059
+ const openingDepth = Math.max(1, depth - ledgeWidth * 2);
51060
+ validateBoltPositionsForServiceCover({
51061
+ positions: boltPositions,
51062
+ coverWidth: width,
51063
+ coverDepth: depth,
51064
+ openingWidth,
51065
+ openingDepth,
51066
+ holeRadius: sizeData[coverFit] / 2
51067
+ });
51068
+ const coverHole = fastenerHole({
51069
+ size: screwSize,
51070
+ fit: coverFit,
51071
+ depth: coverThickness + 0.6,
51072
+ center: true,
51073
+ segments,
51074
+ ...counterboreEnabled ? { counterbore: { depth: Math.min(coverThickness * 0.6, Math.max(0.6, coverThickness - 0.4)) } } : {}
51075
+ });
51076
+ const parentTap = fastenerHole({ size: screwSize, fit: "tap", depth: parentThickness + 0.6, center: true, segments });
51077
+ const parentThreadEnvelope = fastenerHole({
51078
+ size: screwSize,
51079
+ fit: "close",
51080
+ depth: parentThickness + 0.6,
51081
+ center: true,
51082
+ segments
51083
+ });
51084
+ const openingCutter = box(openingWidth, openingDepth, parentThickness + 1).translate(0, 0, -0.5);
51085
+ const parentTappedPattern = placeCutterAtPositions(parentTap, boltPositions, parentThickness / 2);
51086
+ const parentThreadEnvelopePattern = placeCutterAtPositions(parentThreadEnvelope, boltPositions, parentThickness / 2);
51087
+ const parent = box(parentWidth, parentDepth, parentThickness).subtract(openingCutter).subtract(parentThreadEnvelopePattern).color("#4b5563");
51088
+ let coverBlank = box(width, depth, coverThickness);
51089
+ if (options.pullTabs ?? true) {
51090
+ const tabWidth = Math.min(width * 0.18, Math.max(sizeData.head * 1.6, 12));
51091
+ const tabDepth = Math.max(4, coverThickness * 1.4);
51092
+ const tabOverlap = Math.min(0.5, tabDepth * 0.25);
51093
+ const tabY = -depth / 2 - tabDepth / 2 + tabOverlap;
51094
+ const tabX = width * 0.23;
51095
+ coverBlank = union(
51096
+ coverBlank,
51097
+ box(tabWidth, tabDepth, coverThickness).translate(-tabX, tabY, 0),
51098
+ box(tabWidth, tabDepth, coverThickness).translate(tabX, tabY, 0)
51099
+ );
51100
+ }
51101
+ const coverClearancePattern = placeCutterAtPositions(coverHole, boltPositions, coverThickness / 2);
51102
+ const cover2 = coverBlank.subtract(coverClearancePattern).translate(0, 0, parentThickness + gasketThickness).color("#334155");
51103
+ 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;
51104
+ const hardware = fastenerSet(screwSize, screwLength, { washerUnderHead: false, washerUnderNut: false, fit: coverFit, segments });
51105
+ const screwOriginZ = parentThickness + gasketThickness + coverThickness;
51106
+ const screws = boltPositions.map(([x2, y2]) => hardware.bolt.translate(x2, y2, screwOriginZ).color("#94a3b8"));
51107
+ const parts = [
51108
+ { name: "service cover parent ledge with threaded hole envelopes", shape: parent },
51109
+ ...gasket ? [{ name: "service cover gasket seated on ledge", shape: gasket }] : [],
51110
+ { name: "bolted service cover plate with fused pull tabs", shape: cover2 },
51111
+ ...screws.map((shape, index2) => ({ name: `installed ${screwSize} cover screw ${index2 + 1}`, shape }))
51112
+ ];
51113
+ return {
51114
+ parts,
51115
+ parent,
51116
+ cover: cover2,
51117
+ gasket,
51118
+ screws,
51119
+ boltPositions,
51120
+ cutters: {
51121
+ coverClearance: coverClearancePattern,
51122
+ parentTapped: parentTappedPattern,
51123
+ parentThreadEnvelope: parentThreadEnvelopePattern
51124
+ },
51125
+ dims: {
51126
+ width,
51127
+ depth,
51128
+ coverThickness,
51129
+ parentThickness,
51130
+ ledgeWidth,
51131
+ gasketThickness,
51132
+ screwSize,
51133
+ screwLength,
51134
+ clearanceDia: sizeData[coverFit],
51135
+ tapDia: sizeData.tap,
51136
+ threadEnvelopeDia: sizeData.close
51137
+ }
51138
+ };
51139
+ }
51140
+ function datumEnclosureAssembly(options) {
51141
+ const width = requirePositive$6(options.width, "width");
51142
+ const depth = requirePositive$6(options.depth, "depth");
51143
+ const height = requirePositive$6(options.height, "height");
51144
+ const wallThickness = requirePositive$6(options.wallThickness ?? 2.4, "wallThickness");
51145
+ const baseThickness = requirePositive$6(options.baseThickness ?? wallThickness, "baseThickness");
51146
+ const coverThickness = requirePositive$6(options.coverThickness ?? 2.4, "coverThickness");
51147
+ const ledgeWidth = requirePositive$6(options.ledgeWidth ?? Math.max(3.6, wallThickness * 1.35), "ledgeWidth");
51148
+ const gasketThickness = requireNonNegative(options.gasketThickness ?? 0.8, "gasketThickness");
51149
+ const faceClearance = requirePositive$6(options.faceClearance ?? 0.04, "faceClearance");
51150
+ const screwSize = options.screwSize ?? "M3";
51151
+ const coverFit = options.coverFit ?? "normal";
51152
+ const segments = options.segments ?? 32;
51153
+ const sizeData = METRIC_HOLE_TABLE[screwSize];
51154
+ if (!sizeData) throw new Error(`datumEnclosureAssembly: unsupported screwSize "${screwSize}"`);
51155
+ const innerWidth = width - wallThickness * 2;
51156
+ const innerDepth = depth - wallThickness * 2;
51157
+ if (innerWidth <= ledgeWidth * 2 + 8 || innerDepth <= ledgeWidth * 2 + 8) {
51158
+ throw new Error("datumEnclosureAssembly: wallThickness and ledgeWidth leave too little internal opening");
51159
+ }
51160
+ if (height <= baseThickness + coverThickness + 4) {
51161
+ throw new Error("datumEnclosureAssembly: height must leave room for internal ribs and standoffs");
51162
+ }
51163
+ const standoffDiameter = requirePositive$6(
51164
+ options.standoffDiameter ?? Math.max(sizeData.head * 1.65, sizeData.close * 2.2),
51165
+ "standoffDiameter"
51166
+ );
51167
+ const minInset = wallThickness + Math.max(ledgeWidth, standoffDiameter / 2 + 1.2);
51168
+ const [insetX, insetY] = resolveBoltInset(options.screwInset, minInset);
51169
+ if (insetX * 2 >= width || insetY * 2 >= depth) {
51170
+ throw new Error("datumEnclosureAssembly: screwInset leaves no room for the standoff datum");
51171
+ }
51172
+ const screwPositions = options.screwPositions ?? [
51173
+ [-width / 2 + insetX, -depth / 2 + insetY],
51174
+ [width / 2 - insetX, -depth / 2 + insetY],
51175
+ [-width / 2 + insetX, depth / 2 - insetY],
51176
+ [width / 2 - insetX, depth / 2 - insetY]
51177
+ ];
51178
+ if (screwPositions.length === 0) throw new Error("datumEnclosureAssembly: screwPositions must contain at least one point");
51179
+ for (const [index2, [x2, y2]] of screwPositions.entries()) {
51180
+ if (!Number.isFinite(x2) || !Number.isFinite(y2)) {
51181
+ throw new Error(`datumEnclosureAssembly: screwPositions[${index2}] must contain finite numbers`);
51182
+ }
51183
+ if (Math.abs(x2) + standoffDiameter / 2 > innerWidth / 2 || Math.abs(y2) + standoffDiameter / 2 > innerDepth / 2) {
51184
+ throw new Error(`datumEnclosureAssembly: screwPositions[${index2}] does not fit inside the enclosure walls`);
51185
+ }
51186
+ }
51187
+ const ribHeight = requirePositive$6(options.ribHeight ?? Math.min(height * 0.24, Math.max(2.4, baseThickness * 1.4)), "ribHeight");
51188
+ const ribThickness = requirePositive$6(options.ribThickness ?? Math.max(1.2, wallThickness * 0.75), "ribThickness");
51189
+ const portWidth = requirePositive$6(options.portWidth ?? Math.min(innerWidth * 0.28, Math.max(12, width * 0.16)), "portWidth");
51190
+ const portHeight = requirePositive$6(options.portHeight ?? Math.min(height * 0.42, Math.max(5, height * 0.28)), "portHeight");
51191
+ if (portWidth >= innerWidth - ledgeWidth * 2) {
51192
+ throw new Error("datumEnclosureAssembly: portWidth must fit between internal ledges and standoffs");
51193
+ }
51194
+ if (portHeight >= height - baseThickness - 1) {
51195
+ throw new Error("datumEnclosureAssembly: portHeight must leave material above and below the service port");
51196
+ }
51197
+ const screwLength = requirePositive$6(
51198
+ options.screwLength ?? coverThickness + gasketThickness + Math.max(6, height * 0.45),
51199
+ "screwLength"
51200
+ );
51201
+ const coverHole = fastenerHole({
51202
+ size: screwSize,
51203
+ fit: coverFit,
51204
+ depth: coverThickness + 0.6,
51205
+ center: true,
51206
+ segments,
51207
+ counterbore: { depth: Math.min(coverThickness * 0.6, Math.max(0.6, coverThickness - 0.35)) }
51208
+ });
51209
+ const standoffTap = fastenerHole({ size: screwSize, fit: "tap", depth: height + 0.8, center: true, segments });
51210
+ const standoffThreadEnvelope = fastenerHole({ size: screwSize, fit: "close", depth: height + 0.8, center: true, segments });
51211
+ const coverClearance = placeCutterAtPositions(coverHole, screwPositions, coverThickness / 2);
51212
+ const standoffTappedPattern = placeCutterAtPositions(standoffTap, screwPositions, height / 2);
51213
+ const standoffThreadEnvelopePattern = placeCutterAtPositions(standoffThreadEnvelope, screwPositions, height / 2);
51214
+ const fuseOverlap = Math.min(0.06, Math.max(0.02, wallThickness * 0.02));
51215
+ const ledgeThickness = Math.min(Math.max(1.1, coverThickness * 0.45), height * 0.2);
51216
+ const sideX = width / 2 - wallThickness / 2;
51217
+ const sideY = depth / 2 - wallThickness / 2;
51218
+ const ledgeZ = height - ledgeThickness;
51219
+ const baseSolids = [
51220
+ box(width, depth, baseThickness),
51221
+ box(wallThickness, depth, height).translate(sideX, 0, 0),
51222
+ box(wallThickness, depth, height).translate(-sideX, 0, 0),
51223
+ box(width, wallThickness, height).translate(0, sideY, 0),
51224
+ box(width, wallThickness, height).translate(0, -sideY, 0),
51225
+ box(ledgeWidth, innerDepth, ledgeThickness).translate(-width / 2 + wallThickness + ledgeWidth / 2, 0, ledgeZ),
51226
+ box(ledgeWidth, innerDepth, ledgeThickness).translate(width / 2 - wallThickness - ledgeWidth / 2, 0, ledgeZ),
51227
+ box(innerWidth, ledgeWidth, ledgeThickness).translate(0, -depth / 2 + wallThickness + ledgeWidth / 2, ledgeZ),
51228
+ box(innerWidth, ledgeWidth, ledgeThickness).translate(0, depth / 2 - wallThickness - ledgeWidth / 2, ledgeZ),
51229
+ box(Math.max(1, innerWidth - standoffDiameter * 1.8), ribThickness, ribHeight + fuseOverlap).translate(
51230
+ 0,
51231
+ 0,
51232
+ baseThickness - fuseOverlap
51233
+ ),
51234
+ box(ribThickness, Math.max(1, innerDepth - standoffDiameter * 1.8), ribHeight + fuseOverlap).translate(
51235
+ 0,
51236
+ 0,
51237
+ baseThickness - fuseOverlap
51238
+ ),
51239
+ ...screwPositions.map(
51240
+ ([x2, y2]) => cylinder(height - baseThickness + fuseOverlap, standoffDiameter / 2, void 0, segments).translate(
51241
+ x2,
51242
+ y2,
51243
+ baseThickness - fuseOverlap
51244
+ )
51245
+ )
51246
+ ];
51247
+ const servicePort = box(portWidth, wallThickness + 1, portHeight).translate(
51248
+ 0,
51249
+ -depth / 2 + wallThickness / 2,
51250
+ baseThickness + Math.max(0.8, (height - baseThickness - portHeight) * 0.35)
51251
+ );
51252
+ const base = union(...baseSolids).subtract(standoffThreadEnvelopePattern).subtract(servicePort).color("#475569");
51253
+ const gasketFrameCutter = box(Math.max(1, width - ledgeWidth * 2), Math.max(1, depth - ledgeWidth * 2), gasketThickness + 0.6).translate(
51254
+ 0,
51255
+ 0,
51256
+ -0.3
51257
+ );
51258
+ const gasket = gasketThickness > 0 ? box(width, depth, gasketThickness).subtract(gasketFrameCutter).subtract(placeCutterAtPositions(coverHole, screwPositions, gasketThickness / 2)).translate(0, 0, height + faceClearance).color("#111827") : null;
51259
+ const coverZ = height + faceClearance + (gasket ? gasketThickness + faceClearance : 0);
51260
+ const cover2 = box(width, depth, coverThickness).subtract(coverClearance).translate(0, 0, coverZ).color("#334155");
51261
+ const hardware = fastenerSet(screwSize, screwLength, { washerUnderHead: false, washerUnderNut: false, fit: coverFit, segments });
51262
+ const screwOriginZ = coverZ + coverThickness;
51263
+ const screws = screwPositions.map(([x2, y2]) => hardware.bolt.translate(x2, y2, screwOriginZ).color("#94a3b8"));
51264
+ const parts = [
51265
+ { name: "datum enclosure base tray with walls ribs standoffs and service port", shape: base },
51266
+ ...gasket ? [{ name: "datum enclosure gasket seated on continuous ledge", shape: gasket }] : [],
51267
+ { name: "datum enclosure cover plate with matched screw pattern", shape: cover2 },
51268
+ ...screws.map((shape, index2) => ({ name: `installed ${screwSize} enclosure screw ${index2 + 1}`, shape }))
51269
+ ];
51270
+ return {
51271
+ parts,
51272
+ base,
51273
+ cover: cover2,
51274
+ gasket,
51275
+ screws,
51276
+ screwPositions,
51277
+ cutters: {
51278
+ coverClearance,
51279
+ standoffTapped: standoffTappedPattern,
51280
+ standoffThreadEnvelope: standoffThreadEnvelopePattern,
51281
+ servicePort
51282
+ },
51283
+ dims: {
51284
+ width,
51285
+ depth,
51286
+ height,
51287
+ innerWidth,
51288
+ innerDepth,
51289
+ wallThickness,
51290
+ baseThickness,
51291
+ coverThickness,
51292
+ ledgeWidth,
51293
+ gasketThickness,
51294
+ faceClearance,
51295
+ screwSize,
51296
+ screwLength,
51297
+ standoffDiameter,
51298
+ ribHeight,
51299
+ ribThickness,
51300
+ portWidth,
51301
+ portHeight,
51302
+ clearanceDia: sizeData[coverFit],
51303
+ tapDia: sizeData.tap,
51304
+ threadEnvelopeDia: sizeData.close
51305
+ }
51306
+ };
51307
+ }
51308
+ function snapLatchCoverAssembly(options) {
51309
+ const width = requirePositive$6(options.width, "width");
51310
+ const depth = requirePositive$6(options.depth, "depth");
51311
+ const coverThickness = requirePositive$6(options.coverThickness ?? 2.4, "coverThickness");
51312
+ const parentThickness = requirePositive$6(options.parentThickness ?? 6, "parentThickness");
51313
+ const ledgeWidth = requirePositive$6(options.ledgeWidth ?? 8, "ledgeWidth");
51314
+ const runningClearance = requirePositive$6(options.runningClearance ?? 0.25, "runningClearance");
51315
+ const faceClearance = requirePositive$6(options.faceClearance ?? 0.04, "faceClearance");
51316
+ const latchWidth = requirePositive$6(options.latchWidth ?? Math.min(width * 0.22, Math.max(12, width * 0.16)), "latchWidth");
51317
+ const latchThickness = requirePositive$6(options.latchThickness ?? 1.6, "latchThickness");
51318
+ const hookThrow = requirePositive$6(options.hookThrow ?? 3.2, "hookThrow");
51319
+ const hookThickness = requirePositive$6(options.hookThickness ?? 1.6, "hookThickness");
51320
+ const openingWidth = width - ledgeWidth * 2;
51321
+ const openingDepth = depth - ledgeWidth * 2;
51322
+ if (openingWidth <= Math.max(8, latchWidth * 0.8) || openingDepth <= 8) {
51323
+ throw new Error("snapLatchCoverAssembly: ledgeWidth leaves too little service opening under the cover");
51324
+ }
51325
+ if (latchWidth >= openingWidth) {
51326
+ throw new Error("snapLatchCoverAssembly: latchWidth must fit along the receiver opening");
51327
+ }
51328
+ if (latchThickness + runningClearance * 2 >= ledgeWidth) {
51329
+ throw new Error("snapLatchCoverAssembly: latchThickness and clearance must fit inside the receiver ledge");
51330
+ }
51331
+ if (hookThrow + latchThickness / 2 + runningClearance >= ledgeWidth * 1.5) {
51332
+ throw new Error("snapLatchCoverAssembly: hookThrow is too large for the available underside catch land");
51333
+ }
51334
+ const parentWidth = width + ledgeWidth * 2;
51335
+ const parentDepth = depth + ledgeWidth * 2;
51336
+ const fuseOverlap = Math.min(0.04, faceClearance * 0.7);
51337
+ const hookClearance = Math.min(0.08, runningClearance * 0.32);
51338
+ const coverMinZ = parentThickness + faceClearance;
51339
+ const stemMinZ = -hookClearance - hookThickness;
51340
+ const stemHeight = coverMinZ + fuseOverlap - stemMinZ;
51341
+ const slotY = openingDepth / 2 + ledgeWidth / 2;
51342
+ const latchWindow = (sign2) => box(latchWidth + runningClearance * 2, latchThickness + runningClearance * 2, parentThickness + 0.8).translate(
51343
+ 0,
51344
+ sign2 * slotY,
51345
+ -0.4
51346
+ );
51347
+ const latchWindows = union(latchWindow(1), latchWindow(-1));
51348
+ const serviceOpening = box(openingWidth, openingDepth, parentThickness + 1).translate(0, 0, -0.5);
51349
+ const parent = box(parentWidth, parentDepth, parentThickness).subtract(serviceOpening).subtract(latchWindows).color("#475569");
51350
+ const coverPlate = box(width, depth, coverThickness).translate(0, 0, coverMinZ);
51351
+ const snapHook = (sign2) => {
51352
+ const y2 = sign2 * slotY;
51353
+ const stem = box(latchWidth, latchThickness, stemHeight).translate(0, y2, stemMinZ);
51354
+ const barb = box(latchWidth, latchThickness + hookThrow, hookThickness).translate(
51355
+ 0,
51356
+ y2 + sign2 * (hookThrow / 2),
51357
+ stemMinZ
51358
+ );
51359
+ const rootRib = box(latchWidth, Math.max(latchThickness, hookThrow * 0.55), coverThickness * 0.65).translate(
51360
+ 0,
51361
+ y2 - sign2 * (ledgeWidth * 0.18),
51362
+ coverMinZ
51363
+ );
51364
+ return union(stem, barb, rootRib);
51365
+ };
51366
+ const cover2 = union(coverPlate, snapHook(1), snapHook(-1)).color("#111827");
51367
+ const parts = [
51368
+ { name: "snap cover receiver frame with latch windows and catch lands", shape: parent },
51369
+ { name: "one-piece snap cover with fused hooks and underside barbs", shape: cover2 }
51370
+ ];
51371
+ return {
51372
+ parts,
51373
+ parent,
51374
+ cover: cover2,
51375
+ cutters: {
51376
+ serviceOpening,
51377
+ latchWindows
51378
+ },
51379
+ dims: {
51380
+ width,
51381
+ depth,
51382
+ parentWidth,
51383
+ parentDepth,
51384
+ openingWidth,
51385
+ openingDepth,
51386
+ coverThickness,
51387
+ parentThickness,
51388
+ ledgeWidth,
51389
+ latchWidth,
51390
+ latchThickness,
51391
+ hookThrow,
51392
+ hookThickness,
51393
+ runningClearance,
51394
+ faceClearance
51395
+ }
51396
+ };
51397
+ }
51398
+ function pinnedLeverAssembly(options) {
51399
+ const armLength = requirePositive$6(options.armLength, "armLength");
51400
+ const armWidth = requirePositive$6(options.armWidth ?? 10, "armWidth");
51401
+ const leverThickness = requirePositive$6(options.leverThickness ?? 5, "leverThickness");
51402
+ const pinDiameter = requirePositive$6(options.pinDiameter ?? 5, "pinDiameter");
51403
+ const pinClearance = requireNonNegative(options.pinClearance ?? 0.25, "pinClearance");
51404
+ const boreDiameter = pinDiameter + pinClearance;
51405
+ const hubRadius = requirePositive$6(options.hubRadius ?? Math.max(armWidth * 0.85, pinDiameter * 1.8), "hubRadius");
51406
+ const supportThickness = requirePositive$6(options.supportThickness ?? Math.max(6, pinDiameter * 1.4), "supportThickness");
51407
+ const supportWidth = requirePositive$6(options.supportWidth ?? hubRadius * 2 + 18, "supportWidth");
51408
+ const supportDepth = requirePositive$6(options.supportDepth ?? Math.max(armWidth + 18, hubRadius * 2 + 10), "supportDepth");
51409
+ const washerSize = options.washerSize ?? metricWasherSizeForPin(pinDiameter);
51410
+ const washerDims = WASHER_TABLE[washerSize];
51411
+ if (!washerDims) throw new Error(`pinnedLeverAssembly: unsupported washerSize "${washerSize}"`);
51412
+ if (washerDims.id <= pinDiameter) {
51413
+ throw new Error(`pinnedLeverAssembly: ${washerSize} washer inner diameter is too small for a ${pinDiameter} mm pin`);
51414
+ }
51415
+ if (hubRadius <= boreDiameter / 2 + Math.max(1, pinDiameter * 0.25)) {
51416
+ throw new Error("pinnedLeverAssembly: hubRadius leaves too little material around the pivot bore");
51417
+ }
51418
+ if (supportWidth <= boreDiameter + 4 || supportDepth <= boreDiameter + 4) {
51419
+ throw new Error("pinnedLeverAssembly: support dimensions leave too little material around the pivot bore");
51420
+ }
51421
+ const segments = options.segments ?? 40;
51422
+ const gripLength = requirePositive$6(options.gripLength ?? Math.min(armLength * 0.32, Math.max(16, armWidth * 2.4)), "gripLength");
51423
+ const gripWidth = requirePositive$6(options.gripWidth ?? armWidth * 1.55, "gripWidth");
51424
+ if (gripLength >= armLength) throw new Error("pinnedLeverAssembly: gripLength must be shorter than armLength");
51425
+ const armOverlap = Math.min(hubRadius * 0.65, armLength * 0.25);
51426
+ const armStartX = hubRadius - armOverlap;
51427
+ const armCenterX = armStartX + armLength / 2;
51428
+ const gripCenterX = armStartX + armLength - gripLength / 2;
51429
+ const runningClearance = 0.03;
51430
+ const lowerWasherZ = supportThickness + runningClearance;
51431
+ const leverZ = lowerWasherZ + washerDims.t + runningClearance;
51432
+ const upperWasherZ = leverZ + leverThickness + runningClearance;
51433
+ const stackHeight = upperWasherZ + washerDims.t;
51434
+ const pinHeadThickness = Math.max(washerDims.t, pinDiameter * 0.35);
51435
+ const pinHeadRadius = Math.max(washerDims.od * 0.42, pinDiameter * 0.8);
51436
+ const supportBore = cylinder(supportThickness + 1, boreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
51437
+ let supportBlank = box(supportWidth, supportDepth, supportThickness);
51438
+ if (options.stopBlock ?? true) {
51439
+ const stopLength = Math.min(armLength * 0.22, Math.max(10, armWidth * 1.4));
51440
+ const stopWidth = Math.max(4, pinDiameter * 0.7);
51441
+ const stopHeight = supportThickness;
51442
+ const stopX = hubRadius + stopLength / 2;
51443
+ const stopY = armWidth / 2 + stopWidth / 2 + runningClearance;
51444
+ supportBlank = union(supportBlank, box(stopLength, stopWidth, stopHeight).translate(stopX, stopY, 0));
51445
+ }
51446
+ const support = supportBlank.subtract(supportBore).color("#475569");
51447
+ const hub = cylinder(leverThickness, hubRadius, void 0, segments);
51448
+ const arm = box(armLength, armWidth, leverThickness).translate(armCenterX, 0, 0);
51449
+ const grip = box(gripLength, gripWidth, leverThickness).translate(gripCenterX, 0, 0);
51450
+ const leverSolids = [hub, arm, grip];
51451
+ if (options.detentBoss ?? true) {
51452
+ const bossRadius = Math.min(armWidth * 0.42, hubRadius * 0.42);
51453
+ const bossX = hubRadius + Math.min(armLength * 0.22, armWidth * 2);
51454
+ const bossY = -armWidth / 2 - bossRadius * 0.45;
51455
+ leverSolids.push(cylinder(leverThickness, bossRadius, void 0, segments).translate(bossX, bossY, 0));
51456
+ }
51457
+ const leverBore = cylinder(leverThickness + 1, boreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
51458
+ const lever = union(...leverSolids).subtract(leverBore).translate(0, 0, leverZ).color("#7f1d1d");
51459
+ const lowerWasher = washer(washerSize, { segments }).translate(0, 0, lowerWasherZ).color("#94a3b8");
51460
+ const upperWasher = washer(washerSize, { segments }).translate(0, 0, upperWasherZ).color("#94a3b8");
51461
+ const shaft = cylinder(stackHeight, pinDiameter / 2, void 0, segments);
51462
+ const lowerRetainer = cylinder(pinHeadThickness, pinHeadRadius, void 0, segments).translate(0, 0, -pinHeadThickness - runningClearance);
51463
+ const upperHead = cylinder(pinHeadThickness, pinHeadRadius, void 0, segments).translate(0, 0, stackHeight + runningClearance);
51464
+ const pin = union(shaft, lowerRetainer, upperHead).color("#cbd5e1");
51465
+ const pivotBore = cylinder(stackHeight + 1, boreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
51466
+ const parts = [
51467
+ { name: "pivot support block with bearing bore and low stop land", shape: support },
51468
+ { name: "lower thrust washer under pinned lever", shape: lowerWasher },
51469
+ { name: "fused pinned lever with hub arm grip and detent boss", shape: lever },
51470
+ { name: "upper thrust washer over pinned lever", shape: upperWasher },
51471
+ { name: "retained pivot pin through lever stack", shape: pin }
51472
+ ];
51473
+ return {
51474
+ parts,
51475
+ support,
51476
+ lever,
51477
+ pin,
51478
+ washers: {
51479
+ lower: lowerWasher,
51480
+ upper: upperWasher
51481
+ },
51482
+ cutters: {
51483
+ pivotBore
51484
+ },
51485
+ dims: {
51486
+ armLength,
51487
+ armWidth,
51488
+ leverThickness,
51489
+ hubRadius,
51490
+ pinDiameter,
51491
+ boreDiameter,
51492
+ supportWidth,
51493
+ supportDepth,
51494
+ supportThickness,
51495
+ washerSize,
51496
+ washerThickness: washerDims.t,
51497
+ stackHeight
51498
+ }
51499
+ };
51500
+ }
51501
+ function retainedShaftAssembly(options) {
51502
+ const supportSpacing = requirePositive$6(options.supportSpacing, "supportSpacing");
51503
+ const shaftDiameter = requirePositive$6(options.shaftDiameter ?? 8, "shaftDiameter");
51504
+ const boreClearance = requireNonNegative(options.boreClearance ?? 0.35, "boreClearance");
51505
+ const boreDiameter = shaftDiameter + boreClearance;
51506
+ const supportThickness = requirePositive$6(options.supportThickness ?? Math.max(5, shaftDiameter * 0.75), "supportThickness");
51507
+ const washerSize = options.washerSize ?? metricWasherSizeForPin(shaftDiameter);
51508
+ const washerDims = WASHER_TABLE[washerSize];
51509
+ if (!washerDims) throw new Error(`retainedShaftAssembly: unsupported washerSize "${washerSize}"`);
51510
+ if (washerDims.id <= shaftDiameter) {
51511
+ throw new Error(`retainedShaftAssembly: ${washerSize} washer inner diameter is too small for a ${shaftDiameter} mm shaft`);
51512
+ }
51513
+ const knobDiameter = requirePositive$6(options.knobDiameter ?? shaftDiameter * 3, "knobDiameter");
51514
+ const knobThickness = requirePositive$6(options.knobThickness ?? Math.max(8, shaftDiameter), "knobThickness");
51515
+ const retainerThickness = requirePositive$6(
51516
+ options.retainerThickness ?? Math.max(washerDims.t, shaftDiameter * 0.35),
51517
+ "retainerThickness"
51518
+ );
51519
+ const runningClearance = requireNonNegative(options.runningClearance ?? 0.05, "runningClearance");
51520
+ const supportWidth = requirePositive$6(options.supportWidth ?? Math.max(28, knobDiameter * 1.25), "supportWidth");
51521
+ const supportHeight = requirePositive$6(options.supportHeight ?? Math.max(34, knobDiameter * 1.45), "supportHeight");
51522
+ const segments = options.segments ?? 40;
51523
+ if (supportSpacing <= supportThickness) {
51524
+ throw new Error("retainedShaftAssembly: supportSpacing must leave a gap between support cheeks");
51525
+ }
51526
+ if (supportWidth <= boreDiameter + 4 || supportHeight <= boreDiameter + 4) {
51527
+ throw new Error("retainedShaftAssembly: support dimensions leave too little material around the shaft bore");
51528
+ }
51529
+ const leftSupportX = -supportSpacing / 2;
51530
+ const rightSupportX = supportSpacing / 2;
51531
+ const leftOuterFaceX = leftSupportX - supportThickness / 2;
51532
+ const rightOuterFaceX = rightSupportX + supportThickness / 2;
51533
+ const leftWasherX = leftOuterFaceX - runningClearance - washerDims.t / 2;
51534
+ const rightWasherX = rightOuterFaceX + runningClearance + washerDims.t / 2;
51535
+ const leftKnobX = leftOuterFaceX - runningClearance * 2 - washerDims.t - knobThickness / 2;
51536
+ const rightKnobX = rightOuterFaceX + runningClearance * 2 + washerDims.t + knobThickness / 2;
51537
+ const leftStackOuterX = leftKnobX - knobThickness / 2;
51538
+ const rightStackOuterX = rightKnobX + knobThickness / 2;
51539
+ const minimumShaftLength = rightStackOuterX - leftStackOuterX + retainerThickness * 2 + runningClearance * 2;
51540
+ const shaftLength = requirePositive$6(options.shaftLength ?? minimumShaftLength, "shaftLength");
51541
+ if (shaftLength < minimumShaftLength) {
51542
+ throw new Error("retainedShaftAssembly: shaftLength is too short to retain both supports, washers, and knobs");
51543
+ }
51544
+ const supportBore = cylinderAlongX(supportThickness + 1, boreDiameter / 2, 0, segments);
51545
+ const makeSupport = (x2) => box(supportThickness, supportWidth, supportHeight).translate(x2, 0, -supportHeight / 2).subtract(supportBore.translate(x2, 0, 0)).color("#334155");
51546
+ const knobBore = cylinder(knobThickness + 1, boreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
51547
+ const makeKnob = (x2) => cylinder(knobThickness, knobDiameter / 2, void 0, 18).subtract(knobBore).pointAlong([1, 0, 0]).translate(x2 - knobThickness / 2, 0, 0).color("#111827");
51548
+ const retainerRadius = Math.max(shaftDiameter * 0.85, knobDiameter * 0.36);
51549
+ const shaftCore = cylinderAlongX(shaftLength, shaftDiameter / 2, 0, segments);
51550
+ const leftRetainer = cylinderAlongX(retainerThickness, retainerRadius, -shaftLength / 2 + retainerThickness / 2, segments);
51551
+ const rightRetainer = cylinderAlongX(retainerThickness, retainerRadius, shaftLength / 2 - retainerThickness / 2, segments);
51552
+ const shaft = union(shaftCore, leftRetainer, rightRetainer).color("#cbd5e1");
51553
+ const leftSupport = makeSupport(leftSupportX);
51554
+ const rightSupport = makeSupport(rightSupportX);
51555
+ const leftWasher = washerAlongX(washerSize, leftWasherX, segments).color("#94a3b8");
51556
+ const rightWasher = washerAlongX(washerSize, rightWasherX, segments).color("#94a3b8");
51557
+ const leftKnob = makeKnob(leftKnobX);
51558
+ const rightKnob = makeKnob(rightKnobX);
51559
+ const shaftBore = cylinderAlongX(supportThickness + knobThickness + 2, boreDiameter / 2, 0, segments);
51560
+ const parts = [
51561
+ { name: "left bored support cheek for retained shaft", shape: leftSupport },
51562
+ { name: "right bored support cheek for retained shaft", shape: rightSupport },
51563
+ { name: "retained through shaft with end heads", shape: shaft },
51564
+ { name: `left ${washerSize} thrust washer on shaft`, shape: leftWasher },
51565
+ { name: `right ${washerSize} thrust washer on shaft`, shape: rightWasher },
51566
+ { name: "left retained hand knob with shaft bore", shape: leftKnob },
51567
+ { name: "right retained hand knob with shaft bore", shape: rightKnob }
51568
+ ];
51569
+ return {
51570
+ parts,
51571
+ supports: {
51572
+ left: leftSupport,
51573
+ right: rightSupport
51574
+ },
51575
+ shaft,
51576
+ washers: {
51577
+ left: leftWasher,
51578
+ right: rightWasher
51579
+ },
51580
+ knobs: {
51581
+ left: leftKnob,
51582
+ right: rightKnob
51583
+ },
51584
+ cutters: {
51585
+ shaftBore
51586
+ },
51587
+ dims: {
51588
+ supportSpacing,
51589
+ supportThickness,
51590
+ supportWidth,
51591
+ supportHeight,
51592
+ shaftDiameter,
51593
+ shaftLength,
51594
+ boreDiameter,
51595
+ washerSize,
51596
+ washerThickness: washerDims.t,
51597
+ knobDiameter,
51598
+ knobThickness,
51599
+ retainerThickness,
51600
+ runningClearance
51601
+ }
51602
+ };
51603
+ }
51604
+ function capturedLinearSlide(options) {
51605
+ const length4 = requirePositive$6(options.length, "length");
51606
+ const railWidth = requirePositive$6(options.railWidth ?? 38, "railWidth");
51607
+ const baseThickness = requirePositive$6(options.baseThickness ?? 2.4, "baseThickness");
51608
+ const wallThickness = requirePositive$6(options.wallThickness ?? 2, "wallThickness");
51609
+ const wallHeight = requirePositive$6(options.wallHeight ?? 9, "wallHeight");
51610
+ const lipWidth = requirePositive$6(options.lipWidth ?? 4, "lipWidth");
51611
+ const lipThickness = requirePositive$6(options.lipThickness ?? 1.8, "lipThickness");
51612
+ const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
51613
+ const endStopLength = requirePositive$6(options.endStopLength ?? 6, "endStopLength");
51614
+ const carriageLength = requirePositive$6(options.carriageLength ?? length4 * 0.32, "carriageLength");
51615
+ const innerWidth = railWidth - wallThickness * 2;
51616
+ const throatWidth = innerWidth - lipWidth * 2;
51617
+ if (innerWidth <= 0) throw new Error("capturedLinearSlide: wallThickness leaves no inner rail width");
51618
+ if (throatWidth <= 0) throw new Error("capturedLinearSlide: lipWidth closes the rail throat");
51619
+ const carriageWidth = requirePositive$6(options.carriageWidth ?? innerWidth - runningClearance * 2, "carriageWidth");
51620
+ const carriageThickness = requirePositive$6(options.carriageThickness ?? 4, "carriageThickness");
51621
+ if (carriageWidth >= innerWidth - runningClearance) {
51622
+ throw new Error("capturedLinearSlide: carriageWidth leaves too little side clearance inside the rail");
51623
+ }
51624
+ if (carriageWidth <= throatWidth + runningClearance) {
51625
+ throw new Error("capturedLinearSlide: carriageWidth must be wider than the lip throat so the rail actually captures it");
51626
+ }
51627
+ if (carriageThickness + runningClearance * 2 >= wallHeight) {
51628
+ throw new Error("capturedLinearSlide: carriage is too tall to clear the return lips");
51629
+ }
51630
+ const maxTravel = length4 - endStopLength * 2 - carriageLength;
51631
+ if (maxTravel <= 0) {
51632
+ throw new Error("capturedLinearSlide: rail length, end stops, and carriage length leave no travel");
51633
+ }
51634
+ const travel = options.travel ?? maxTravel / 2;
51635
+ if (!Number.isFinite(travel) || travel < 0 || travel > maxTravel) {
51636
+ throw new Error(`capturedLinearSlide: travel must be between 0 and ${maxTravel}`);
51637
+ }
51638
+ const carriageCenterX = -maxTravel / 2 + travel;
51639
+ const fuseOverlap = Math.min(0.04, runningClearance * 0.1);
51640
+ const sideY = railWidth / 2 - wallThickness / 2;
51641
+ const lipY = railWidth / 2 - wallThickness - lipWidth / 2 + fuseOverlap / 2;
51642
+ const stopZ = baseThickness - fuseOverlap;
51643
+ const rail2 = union(
51644
+ box(length4, railWidth, baseThickness),
51645
+ box(length4, wallThickness, wallHeight + fuseOverlap).translate(0, sideY, baseThickness - fuseOverlap),
51646
+ box(length4, wallThickness, wallHeight + fuseOverlap).translate(0, -sideY, baseThickness - fuseOverlap),
51647
+ box(length4, lipWidth, lipThickness + fuseOverlap).translate(0, lipY, baseThickness + wallHeight - fuseOverlap),
51648
+ box(length4, lipWidth, lipThickness + fuseOverlap).translate(0, -lipY, baseThickness + wallHeight - fuseOverlap),
51649
+ box(endStopLength, throatWidth, carriageThickness + fuseOverlap).translate(-length4 / 2 + endStopLength / 2, 0, stopZ),
51650
+ box(endStopLength, throatWidth, carriageThickness + fuseOverlap).translate(length4 / 2 - endStopLength / 2, 0, stopZ)
51651
+ ).color("#475569");
51652
+ const carriage = union(
51653
+ box(carriageLength, carriageWidth, carriageThickness),
51654
+ box(carriageLength * 0.78, throatWidth - runningClearance * 2, Math.max(1, carriageThickness * 0.38)).translate(
51655
+ 0,
51656
+ 0,
51657
+ carriageThickness
51658
+ )
51659
+ ).translate(carriageCenterX, 0, baseThickness + runningClearance).color("#111827");
51660
+ const parts = [
51661
+ { name: "captured linear rail with return lips and end stops", shape: rail2 },
51662
+ { name: "sliding carriage captured under rail lips", shape: carriage }
51663
+ ];
51664
+ return {
51665
+ parts,
51666
+ rail: rail2,
51667
+ carriage,
51668
+ dims: {
51669
+ length: length4,
51670
+ railWidth,
51671
+ innerWidth,
51672
+ throatWidth,
51673
+ baseThickness,
51674
+ wallThickness,
51675
+ wallHeight,
51676
+ lipWidth,
51677
+ lipThickness,
51678
+ carriageLength,
51679
+ carriageWidth,
51680
+ carriageThickness,
51681
+ endStopLength,
51682
+ runningClearance,
51683
+ maxTravel,
51684
+ travel,
51685
+ carriageCenterX
51686
+ }
51687
+ };
51688
+ }
51689
+ function capturedCartridgeGuideAssembly(options) {
51690
+ const length4 = requirePositive$6(options.length, "length");
51691
+ const guideWidth = requirePositive$6(options.guideWidth ?? 42, "guideWidth");
51692
+ const baseThickness = requirePositive$6(options.baseThickness ?? 3, "baseThickness");
51693
+ const wallThickness = requirePositive$6(options.wallThickness ?? 2.5, "wallThickness");
51694
+ const wallHeight = requirePositive$6(options.wallHeight ?? 12, "wallHeight");
51695
+ const lipWidth = requirePositive$6(options.lipWidth ?? 4, "lipWidth");
51696
+ const lipThickness = requirePositive$6(options.lipThickness ?? 2, "lipThickness");
51697
+ const rearStopLength = requirePositive$6(options.rearStopLength ?? 7, "rearStopLength");
51698
+ const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
51699
+ const cartridgeLength = requirePositive$6(options.cartridgeLength ?? length4 * 0.58, "cartridgeLength");
51700
+ const cartridgeHeight = requirePositive$6(options.cartridgeHeight ?? 10, "cartridgeHeight");
51701
+ const flangeThickness = requirePositive$6(options.flangeThickness ?? 3, "flangeThickness");
51702
+ const pullTabLength = requirePositive$6(options.pullTabLength ?? 10, "pullTabLength");
51703
+ const innerWidth = guideWidth - wallThickness * 2;
51704
+ const throatWidth = innerWidth - lipWidth * 2;
51705
+ if (innerWidth <= 0) throw new Error("capturedCartridgeGuideAssembly: wallThickness leaves no inner guide width");
51706
+ if (throatWidth <= 0) throw new Error("capturedCartridgeGuideAssembly: lipWidth closes the guide throat");
51707
+ if (wallHeight <= lipThickness + flangeThickness + runningClearance * 2) {
51708
+ throw new Error("capturedCartridgeGuideAssembly: wallHeight leaves too little vertical capture clearance");
51709
+ }
51710
+ const cartridgeWidth = requirePositive$6(options.cartridgeWidth ?? innerWidth - runningClearance * 2, "cartridgeWidth");
51711
+ const cartridgeBodyWidth = throatWidth - runningClearance * 2;
51712
+ if (cartridgeBodyWidth <= 0) {
51713
+ throw new Error("capturedCartridgeGuideAssembly: throatWidth and runningClearance leave no cartridge body width");
51714
+ }
51715
+ if (cartridgeWidth >= innerWidth - runningClearance) {
51716
+ throw new Error("capturedCartridgeGuideAssembly: cartridgeWidth leaves too little side clearance inside the guide");
51717
+ }
51718
+ if (cartridgeWidth <= throatWidth + runningClearance) {
51719
+ throw new Error("capturedCartridgeGuideAssembly: cartridge flange must be wider than the guide throat so the cartridge is captured");
51720
+ }
51721
+ const maxInsertion = length4 - rearStopLength - cartridgeLength;
51722
+ if (maxInsertion <= 0) {
51723
+ throw new Error("capturedCartridgeGuideAssembly: length, rearStopLength, and cartridgeLength leave no insertion travel");
51724
+ }
51725
+ const insertion = options.insertion ?? maxInsertion * 0.4;
51726
+ if (!Number.isFinite(insertion) || insertion < 0 || insertion > maxInsertion) {
51727
+ throw new Error(`capturedCartridgeGuideAssembly: insertion must be between 0 and ${maxInsertion}`);
51728
+ }
51729
+ const cartridgeCenterX = -length4 / 2 + cartridgeLength / 2 + insertion;
51730
+ const fuseOverlap = Math.min(0.04, runningClearance * 0.1);
51731
+ const sideY = guideWidth / 2 - wallThickness / 2;
51732
+ const lipY = guideWidth / 2 - wallThickness - lipWidth / 2 + fuseOverlap / 2;
51733
+ const guide = union(
51734
+ box(length4, guideWidth, baseThickness),
51735
+ box(length4, wallThickness, wallHeight + fuseOverlap).translate(0, sideY, baseThickness - fuseOverlap),
51736
+ box(length4, wallThickness, wallHeight + fuseOverlap).translate(0, -sideY, baseThickness - fuseOverlap),
51737
+ box(length4, lipWidth, lipThickness + fuseOverlap).translate(0, lipY, baseThickness + wallHeight - fuseOverlap),
51738
+ box(length4, lipWidth, lipThickness + fuseOverlap).translate(0, -lipY, baseThickness + wallHeight - fuseOverlap),
51739
+ box(rearStopLength, throatWidth, Math.max(flangeThickness + runningClearance, 4)).translate(
51740
+ length4 / 2 - rearStopLength / 2,
51741
+ 0,
51742
+ baseThickness - fuseOverlap
51743
+ )
51744
+ ).color("#475569");
51745
+ const flangeZ = baseThickness + runningClearance;
51746
+ const bodyHeight = Math.max(1, cartridgeHeight - flangeThickness);
51747
+ const bodyZ = flangeZ + flangeThickness;
51748
+ const tabOverlap = Math.min(0.6, pullTabLength * 0.15);
51749
+ const pullTabX = cartridgeCenterX - cartridgeLength / 2 - pullTabLength / 2 + tabOverlap;
51750
+ const pullTabWidth = Math.max(cartridgeBodyWidth * 0.55, 12);
51751
+ const cartridge = union(
51752
+ box(cartridgeLength, cartridgeWidth, flangeThickness).translate(cartridgeCenterX, 0, flangeZ),
51753
+ box(cartridgeLength * 0.88, cartridgeBodyWidth, bodyHeight).translate(cartridgeCenterX, 0, bodyZ),
51754
+ box(pullTabLength, pullTabWidth, Math.max(flangeThickness, 3)).translate(pullTabX, 0, flangeZ)
51755
+ ).color("#111827");
51756
+ const parts = [
51757
+ { name: "captured cartridge guide with return lips and rear stop", shape: guide },
51758
+ { name: "removable cartridge with captured flange and pull tab", shape: cartridge }
51759
+ ];
51760
+ return {
51761
+ parts,
51762
+ guide,
51763
+ cartridge,
51764
+ dims: {
51765
+ length: length4,
51766
+ guideWidth,
51767
+ innerWidth,
51768
+ throatWidth,
51769
+ baseThickness,
51770
+ wallThickness,
51771
+ wallHeight,
51772
+ lipWidth,
51773
+ lipThickness,
51774
+ rearStopLength,
51775
+ cartridgeLength,
51776
+ cartridgeWidth,
51777
+ cartridgeBodyWidth,
51778
+ cartridgeHeight,
51779
+ flangeThickness,
51780
+ pullTabLength,
51781
+ runningClearance,
51782
+ maxInsertion,
51783
+ insertion,
51784
+ cartridgeCenterX
51785
+ }
51786
+ };
51787
+ }
51788
+ function livingHingeCoverAssembly(options) {
51789
+ const width = requirePositive$6(options.width, "width");
51790
+ const coverDepth = requirePositive$6(options.coverDepth ?? 42, "coverDepth");
51791
+ const fixedLeafDepth = requirePositive$6(options.fixedLeafDepth ?? 18, "fixedLeafDepth");
51792
+ const leafThickness = requirePositive$6(options.leafThickness ?? 2, "leafThickness");
51793
+ const hingeWebWidth = requirePositive$6(options.hingeWebWidth ?? 3.2, "hingeWebWidth");
51794
+ const hingeWebThickness = requirePositive$6(options.hingeWebThickness ?? 0.45, "hingeWebThickness");
51795
+ const pullLipDepth = requirePositive$6(options.pullLipDepth ?? 5, "pullLipDepth");
51796
+ const snapBarbWidth = requirePositive$6(options.snapBarbWidth ?? width * 0.35, "snapBarbWidth");
51797
+ const snapBarbDepth = requirePositive$6(options.snapBarbDepth ?? 2.4, "snapBarbDepth");
51798
+ const snapBarbHeight = requirePositive$6(options.snapBarbHeight ?? 1.4, "snapBarbHeight");
51799
+ const catchLandDepth = requirePositive$6(options.catchLandDepth ?? 2.4, "catchLandDepth");
51800
+ if (hingeWebThickness >= leafThickness * 0.55) {
51801
+ throw new Error("livingHingeCoverAssembly: hingeWebThickness must be much thinner than the rigid leaves");
51802
+ }
51803
+ if (hingeWebWidth >= Math.min(coverDepth, fixedLeafDepth) * 0.45) {
51804
+ throw new Error("livingHingeCoverAssembly: hingeWebWidth is too wide for the selected leaves");
51805
+ }
51806
+ if (snapBarbWidth >= width - 2) {
51807
+ throw new Error("livingHingeCoverAssembly: snapBarbWidth must leave side material on the cover leaf");
51808
+ }
51809
+ const fuseOverlap = Math.min(0.04, hingeWebWidth * 0.02);
51810
+ const fixedCenterY = -hingeWebWidth / 2 - fixedLeafDepth / 2 + fuseOverlap / 2;
51811
+ const coverCenterY = hingeWebWidth / 2 + coverDepth / 2 - fuseOverlap / 2;
51812
+ const fixedLeaf = box(width, fixedLeafDepth + fuseOverlap, leafThickness).translate(0, fixedCenterY, 0);
51813
+ const movingLeaf = box(width, coverDepth + fuseOverlap, leafThickness).translate(0, coverCenterY, 0);
51814
+ const hingeWeb = box(width, hingeWebWidth + fuseOverlap * 2, hingeWebThickness).translate(0, 0, 0);
51815
+ const pullLip = box(width * 0.92, pullLipDepth, leafThickness).translate(0, coverCenterY + coverDepth / 2 + pullLipDepth / 2 - fuseOverlap, 0);
51816
+ const snapBarb = box(snapBarbWidth, snapBarbDepth, snapBarbHeight).translate(
51817
+ 0,
51818
+ coverCenterY + coverDepth / 2 - snapBarbDepth / 2,
51819
+ leafThickness
51820
+ );
51821
+ const catchLand = box(width * 0.55, catchLandDepth, Math.max(0.8, leafThickness * 0.45)).translate(
51822
+ 0,
51823
+ fixedCenterY - fixedLeafDepth / 2 + catchLandDepth / 2,
51824
+ leafThickness
51825
+ );
51826
+ const cover2 = union(fixedLeaf, movingLeaf, hingeWeb, pullLip, snapBarb, catchLand).color("#0f766e");
51827
+ const overallDepth = fixedLeafDepth + hingeWebWidth + coverDepth + pullLipDepth;
51828
+ const flexRatio = leafThickness / hingeWebThickness;
51829
+ return {
51830
+ parts: [{ name: "one-piece molded living hinge cover with snap barb", shape: cover2 }],
51831
+ cover: cover2,
51832
+ fixedLeaf,
51833
+ movingLeaf,
51834
+ hingeWeb,
51835
+ snapBarb,
51836
+ catchLand,
51837
+ dims: {
51838
+ width,
51839
+ coverDepth,
51840
+ fixedLeafDepth,
51841
+ leafThickness,
51842
+ hingeWebWidth,
51843
+ hingeWebThickness,
51844
+ pullLipDepth,
51845
+ snapBarbWidth,
51846
+ snapBarbDepth,
51847
+ snapBarbHeight,
51848
+ catchLandDepth,
51849
+ flexRatio,
51850
+ overallDepth
51851
+ }
51852
+ };
51853
+ }
51854
+ function knuckledHingeAssembly(options) {
51855
+ const length4 = requirePositive$6(options.length, "length");
51856
+ const leafLength = requirePositive$6(options.leafLength ?? 36, "leafLength");
51857
+ const leafThickness = requirePositive$6(options.leafThickness ?? 1.6, "leafThickness");
51858
+ const barrelOuterRadius = requirePositive$6(options.barrelOuterRadius ?? 3, "barrelOuterRadius");
51859
+ const pinDiameter = requirePositive$6(options.pinDiameter ?? 2, "pinDiameter");
51860
+ const pinClearance = requireNonNegative(options.pinClearance ?? 0.25, "pinClearance");
51861
+ const boreDiameter = pinDiameter + pinClearance;
51862
+ const knuckleGap = requireNonNegative(options.knuckleGap ?? 0.45, "knuckleGap");
51863
+ const openAngleDeg = Number.isFinite(options.openAngleDeg ?? 35) ? options.openAngleDeg ?? 35 : 35;
51864
+ const retainerThickness = requirePositive$6(
51865
+ options.retainerThickness ?? Math.max(leafThickness, pinDiameter * 0.7),
51866
+ "retainerThickness"
51867
+ );
51868
+ const segments = options.segments ?? 36;
51869
+ const knuckleCount = options.knuckleCount ?? 5;
51870
+ if (!Number.isInteger(knuckleCount) || knuckleCount < 3 || knuckleCount % 2 === 0) {
51871
+ throw new Error("knuckledHingeAssembly: knuckleCount must be an odd integer >= 3");
51872
+ }
51873
+ if (barrelOuterRadius <= boreDiameter / 2 + Math.max(0.35, pinDiameter * 0.18)) {
51874
+ throw new Error("knuckledHingeAssembly: barrelOuterRadius leaves too little wall around the pin bore");
51875
+ }
51876
+ const knuckleLength = (length4 - knuckleGap * (knuckleCount - 1)) / knuckleCount;
51877
+ if (knuckleLength <= pinDiameter * 1.4) {
51878
+ throw new Error("knuckledHingeAssembly: length, knuckleCount, and knuckleGap make knuckles too short");
51879
+ }
51880
+ const leafRootClearance = Math.max(0.12, Math.min(knuckleGap * 0.35, 0.35));
51881
+ const barrelLeafOverlap = Math.min(barrelOuterRadius * 0.18, leafThickness * 0.35);
51882
+ const bridgeDepth = leafRootClearance + barrelLeafOverlap + 0.2;
51883
+ const fixedLeafPlate = box(length4, leafLength, leafThickness).translate(
51884
+ 0,
51885
+ barrelOuterRadius + leafRootClearance + leafLength / 2,
51886
+ -leafThickness / 2
51887
+ );
51888
+ const movingLeafPlate = box(length4, leafLength, leafThickness).translate(
51889
+ 0,
51890
+ -barrelOuterRadius - leafRootClearance - leafLength / 2,
51891
+ -leafThickness / 2
51892
+ );
51893
+ const fixedKnuckles = [];
51894
+ const movingKnuckles = [];
51895
+ const fixedBridges = [];
51896
+ const movingBridges = [];
51897
+ for (let index2 = 0; index2 < knuckleCount; index2 += 1) {
51898
+ const xStart = -length4 / 2 + index2 * (knuckleLength + knuckleGap);
51899
+ const xCenter = xStart + knuckleLength / 2;
51900
+ const knuckle = tubeAlongX(knuckleLength, barrelOuterRadius, boreDiameter / 2, xCenter, segments);
51901
+ if (index2 % 2 === 0) {
51902
+ fixedKnuckles.push(knuckle);
51903
+ fixedBridges.push(
51904
+ box(knuckleLength, bridgeDepth, leafThickness).translate(
51905
+ xCenter,
51906
+ barrelOuterRadius - barrelLeafOverlap + bridgeDepth / 2,
51907
+ -leafThickness / 2
51908
+ )
51909
+ );
51910
+ } else {
51911
+ movingKnuckles.push(knuckle);
51912
+ movingBridges.push(
51913
+ box(knuckleLength, bridgeDepth, leafThickness).translate(
51914
+ xCenter,
51915
+ -barrelOuterRadius + barrelLeafOverlap - bridgeDepth / 2,
51916
+ -leafThickness / 2
51917
+ )
51918
+ );
51919
+ }
51920
+ }
51921
+ const fixedLeaf = union(fixedLeafPlate, ...fixedKnuckles, ...fixedBridges).color("#475569");
51922
+ const movingLeaf = union(movingLeafPlate, ...movingKnuckles, ...movingBridges).rotateX(openAngleDeg).color("#111827");
51923
+ const pinCore = cylinderAlongX(length4 + retainerThickness * 2, pinDiameter / 2, 0, segments);
51924
+ const retainerRadius = Math.max(barrelOuterRadius * 0.85, pinDiameter);
51925
+ const leftHead = cylinderAlongX(retainerThickness, retainerRadius, -length4 / 2 - retainerThickness / 2, segments);
51926
+ const rightHead = cylinderAlongX(retainerThickness, retainerRadius, length4 / 2 + retainerThickness / 2, segments);
51927
+ const pin = union(pinCore, leftHead, rightHead).color("#cbd5e1");
51928
+ const pinBore = cylinderAlongX(length4 + retainerThickness * 2, boreDiameter / 2, 0, segments);
51929
+ const parts = [
51930
+ { name: "fixed hinge leaf with alternating knuckles", shape: fixedLeaf },
51931
+ { name: "moving hinge leaf with alternating knuckles", shape: movingLeaf },
51932
+ { name: "retained hinge pin through knuckle stack", shape: pin }
51933
+ ];
51934
+ return {
51935
+ parts,
51936
+ fixedLeaf,
51937
+ movingLeaf,
51938
+ pin,
51939
+ cutters: {
51940
+ pinBore
51941
+ },
51942
+ dims: {
51943
+ length: length4,
51944
+ leafLength,
51945
+ leafThickness,
51946
+ barrelOuterRadius,
51947
+ pinDiameter,
51948
+ boreDiameter,
51949
+ knuckleGap,
51950
+ knuckleCount,
51951
+ knuckleLength,
51952
+ openAngleDeg,
51953
+ retainerThickness
51954
+ }
51955
+ };
51956
+ }
51957
+ function clevisPinJointAssembly(options = {}) {
51958
+ const pinDiameter = requirePositive$6(options.pinDiameter ?? 4, "pinDiameter");
51959
+ const pinClearance = requireNonNegative(options.pinClearance ?? 0.3, "pinClearance");
51960
+ const boreDiameter = pinDiameter + pinClearance;
51961
+ const linkThickness = requirePositive$6(options.linkThickness ?? Math.max(5, pinDiameter * 1.5), "linkThickness");
51962
+ const earThickness = requirePositive$6(options.earThickness ?? Math.max(3.5, pinDiameter), "earThickness");
51963
+ const runningClearance = requireNonNegative(options.runningClearance ?? 0.25, "runningClearance");
51964
+ const linkArmWidth = requirePositive$6(options.linkArmWidth ?? pinDiameter * 2.4, "linkArmWidth");
51965
+ const eyeOuterRadius = requirePositive$6(
51966
+ options.eyeOuterRadius ?? Math.max(pinDiameter * 1.8, linkArmWidth / 2 + 1.4),
51967
+ "eyeOuterRadius"
51968
+ );
51969
+ const earLength = requirePositive$6(options.earLength ?? Math.max(eyeOuterRadius * 2.55, pinDiameter * 4.2), "earLength");
51970
+ const earHeight = requirePositive$6(options.earHeight ?? Math.max(eyeOuterRadius * 2.25, pinDiameter * 4.4), "earHeight");
51971
+ const linkArmLength = requirePositive$6(options.linkArmLength ?? 34, "linkArmLength");
51972
+ const retainerThickness = requirePositive$6(
51973
+ options.retainerThickness ?? Math.max(1.2, pinDiameter * 0.35),
51974
+ "retainerThickness"
51975
+ );
51976
+ const segments = options.segments ?? 40;
51977
+ if (eyeOuterRadius <= boreDiameter / 2 + Math.max(0.8, pinDiameter * 0.25)) {
51978
+ throw new Error("clevisPinJointAssembly: eyeOuterRadius leaves too little material around the pin bore");
51979
+ }
51980
+ if (earHeight <= boreDiameter + Math.max(3, pinDiameter)) {
51981
+ throw new Error("clevisPinJointAssembly: earHeight leaves too little material around the pin bore");
51982
+ }
51983
+ if (earLength / 2 <= eyeOuterRadius + runningClearance) {
51984
+ throw new Error("clevisPinJointAssembly: earLength must extend behind the link eye for a rear clevis bridge");
51985
+ }
51986
+ const clevisGap = linkThickness + runningClearance * 2;
51987
+ const earCenterY = clevisGap / 2 + earThickness / 2;
51988
+ const totalStackY = clevisGap + earThickness * 2;
51989
+ const pinLength = totalStackY + retainerThickness * 2 + runningClearance * 2;
51990
+ const bridgeClearX = -eyeOuterRadius - runningClearance;
51991
+ const bridgeLength = Math.max(pinDiameter * 2.2, 4);
51992
+ const bridgeHeight = Math.min(earHeight * 0.48, Math.max(pinDiameter * 1.4, eyeOuterRadius * 0.75));
51993
+ const bridgeCenterX = bridgeClearX - bridgeLength / 2;
51994
+ const bridgeCenterZ = -earHeight / 2 + bridgeHeight / 2;
51995
+ const pinBore = cylinderAlongY(totalStackY + 0.8, boreDiameter / 2, 0, segments);
51996
+ const clevisBlank = union(
51997
+ box(earLength, earThickness, earHeight).translate(0, earCenterY, -earHeight / 2),
51998
+ box(earLength, earThickness, earHeight).translate(0, -earCenterY, -earHeight / 2),
51999
+ box(bridgeLength, totalStackY, bridgeHeight).translate(bridgeCenterX, 0, bridgeCenterZ)
52000
+ );
52001
+ const clevis = clevisBlank.subtract(pinBore).color("#475569");
52002
+ const eye = tubeAlongY(linkThickness, eyeOuterRadius, boreDiameter / 2, 0, segments);
52003
+ const armOverlap = Math.min(eyeOuterRadius * 0.65, linkArmLength * 0.25);
52004
+ const armCenterX = eyeOuterRadius - armOverlap + linkArmLength / 2;
52005
+ const linkArm = box(linkArmLength, linkThickness, linkArmWidth).translate(armCenterX, 0, -linkArmWidth / 2);
52006
+ const link = union(eye, linkArm).color("#111827");
52007
+ const pinCore = cylinderAlongY(pinLength, pinDiameter / 2, 0, segments);
52008
+ const headRadius = Math.max(pinDiameter * 0.9, boreDiameter / 2 + 0.8);
52009
+ const headY = totalStackY / 2 + runningClearance + retainerThickness / 2;
52010
+ const headA = cylinderAlongY(retainerThickness, headRadius, headY, segments);
52011
+ const headB = cylinderAlongY(retainerThickness, headRadius, -headY, segments);
52012
+ const pin = union(pinCore, headA, headB).color("#cbd5e1");
52013
+ const cutter = cylinderAlongY(pinLength + 1, boreDiameter / 2, 0, segments);
52014
+ const parts = [
52015
+ { name: "bored clevis yoke with rear bridge", shape: clevis },
52016
+ { name: "center link eye captured in clevis", shape: link },
52017
+ { name: "retained clevis pin through link eye", shape: pin }
52018
+ ];
52019
+ return {
52020
+ parts,
52021
+ clevis,
52022
+ link,
52023
+ pin,
52024
+ cutters: {
52025
+ pinBore: cutter
52026
+ },
52027
+ dims: {
52028
+ pinDiameter,
52029
+ boreDiameter,
52030
+ linkThickness,
52031
+ earThickness,
52032
+ runningClearance,
52033
+ earLength,
52034
+ earHeight,
52035
+ linkArmLength,
52036
+ linkArmWidth,
52037
+ eyeOuterRadius,
52038
+ retainerThickness,
52039
+ pinLength,
52040
+ clevisGap
52041
+ }
52042
+ };
52043
+ }
52044
+ function seatedBearingAssembly(options) {
52045
+ const bearingOuterDiameter = requirePositive$6(options.bearingOuterDiameter, "bearingOuterDiameter");
52046
+ const bearingInnerDiameter = requirePositive$6(options.bearingInnerDiameter, "bearingInnerDiameter");
52047
+ const bearingWidth = requirePositive$6(options.bearingWidth, "bearingWidth");
52048
+ const shaftDiameter = requirePositive$6(options.shaftDiameter ?? Math.max(1, bearingInnerDiameter - 0.4), "shaftDiameter");
52049
+ const pocketClearance = requireNonNegative(options.pocketClearance ?? 0.2, "pocketClearance");
52050
+ const shaftClearance = requireNonNegative(options.shaftClearance ?? 0.35, "shaftClearance");
52051
+ const runningClearance = requireNonNegative(options.runningClearance ?? 0.05, "runningClearance");
52052
+ const housingThickness = requirePositive$6(options.housingThickness ?? bearingWidth + 5, "housingThickness");
52053
+ const bossHeight = requirePositive$6(options.bossHeight ?? Math.max(2, bearingWidth * 0.45), "bossHeight");
52054
+ const bossOuterDiameter = requirePositive$6(
52055
+ options.bossOuterDiameter ?? bearingOuterDiameter + Math.max(8, bearingOuterDiameter * 0.36),
52056
+ "bossOuterDiameter"
52057
+ );
52058
+ const housingWidth = requirePositive$6(options.housingWidth ?? Math.max(bossOuterDiameter + 12, bearingOuterDiameter * 2.1), "housingWidth");
52059
+ const housingDepth = requirePositive$6(options.housingDepth ?? Math.max(bossOuterDiameter + 12, bearingOuterDiameter * 1.8), "housingDepth");
52060
+ const shaftOverhang = requirePositive$6(options.shaftOverhang ?? Math.max(8, bearingOuterDiameter * 0.45), "shaftOverhang");
52061
+ const shoulderDiameter = requirePositive$6(options.shoulderDiameter ?? Math.max(shaftDiameter * 1.65, bearingInnerDiameter + 2), "shoulderDiameter");
52062
+ const shoulderThickness = requirePositive$6(options.shoulderThickness ?? Math.max(1.5, shaftDiameter * 0.32), "shoulderThickness");
52063
+ const segments = options.segments ?? 48;
52064
+ if (bearingOuterDiameter <= bearingInnerDiameter + Math.max(1, bearingOuterDiameter * 0.08)) {
52065
+ throw new Error("seatedBearingAssembly: bearingOuterDiameter leaves too little bearing wall around the bore");
52066
+ }
52067
+ if (shaftDiameter + shaftClearance >= bearingInnerDiameter) {
52068
+ throw new Error("seatedBearingAssembly: shaftDiameter plus shaftClearance must fit inside the bearing bore");
52069
+ }
52070
+ if (shoulderDiameter >= bearingOuterDiameter - runningClearance * 2) {
52071
+ throw new Error("seatedBearingAssembly: shoulderDiameter must stay smaller than the bearing outer race");
52072
+ }
52073
+ const pocketDiameter = bearingOuterDiameter + pocketClearance;
52074
+ const shaftBoreDiameter = shaftDiameter + shaftClearance;
52075
+ const totalHousingHeight = housingThickness + bossHeight;
52076
+ const pocketDepth = bearingWidth + runningClearance * 2;
52077
+ if (pocketDepth >= totalHousingHeight - runningClearance) {
52078
+ throw new Error("seatedBearingAssembly: housingThickness and bossHeight must leave a shoulder below the bearing pocket");
52079
+ }
52080
+ if (bossOuterDiameter <= pocketDiameter + Math.max(2, bearingOuterDiameter * 0.12)) {
52081
+ throw new Error("seatedBearingAssembly: bossOuterDiameter leaves too little wall around the bearing pocket");
52082
+ }
52083
+ if (housingWidth <= pocketDiameter + 6 || housingDepth <= pocketDiameter + 6) {
52084
+ throw new Error("seatedBearingAssembly: housing dimensions leave too little material around the bearing pocket");
52085
+ }
52086
+ if (shoulderThickness * 2 + runningClearance * 2 >= shaftOverhang) {
52087
+ throw new Error("seatedBearingAssembly: shaftOverhang must leave room for retaining collars outside the housing");
52088
+ }
52089
+ const pocketBottomZ = totalHousingHeight - pocketDepth;
52090
+ const bearingZ = pocketBottomZ + runningClearance;
52091
+ const lowerShoulderZ = -runningClearance - shoulderThickness;
52092
+ const upperShoulderZ = totalHousingHeight + runningClearance;
52093
+ const shaftLength = totalHousingHeight + shaftOverhang * 2;
52094
+ const bossFuseOverlap = Math.min(0.08, Math.max(0.02, bossHeight * 0.03));
52095
+ const bearingPocket = cylinder(pocketDepth + 0.4, pocketDiameter / 2, void 0, segments).translate(0, 0, pocketBottomZ - 0.2);
52096
+ const shaftBore = cylinder(totalHousingHeight + 1, shaftBoreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
52097
+ const housingBase = box(housingWidth, housingDepth, housingThickness).subtract(bearingPocket).subtract(shaftBore);
52098
+ const housingBoss = cylinder(bossHeight + bossFuseOverlap, bossOuterDiameter / 2, void 0, segments).translate(
52099
+ 0,
52100
+ 0,
52101
+ housingThickness - bossFuseOverlap
52102
+ ).subtract(bearingPocket);
52103
+ const housing = union(housingBase, housingBoss).color("#475569");
52104
+ const bearingRing = tubeAlongZ(bearingWidth, bearingOuterDiameter / 2, bearingInnerDiameter / 2, segments);
52105
+ const shieldInset = Math.min(bearingWidth * 0.18, 0.7);
52106
+ const shieldOuterRadius = bearingOuterDiameter / 2 - Math.max(0.45, (bearingOuterDiameter - bearingInnerDiameter) * 0.08);
52107
+ const shieldInnerRadius = bearingInnerDiameter / 2 + Math.max(0.2, (bearingOuterDiameter - bearingInnerDiameter) * 0.035);
52108
+ const bearingShield = shieldOuterRadius > shieldInnerRadius + 0.2 ? union(
52109
+ tubeAlongZ(Math.min(0.35, bearingWidth * 0.08), shieldOuterRadius, shieldInnerRadius, segments).translate(0, 0, shieldInset),
52110
+ tubeAlongZ(Math.min(0.35, bearingWidth * 0.08), shieldOuterRadius, shieldInnerRadius, segments).translate(
52111
+ 0,
52112
+ 0,
52113
+ bearingWidth - shieldInset - Math.min(0.35, bearingWidth * 0.08)
52114
+ )
52115
+ ) : null;
52116
+ const bearing = (bearingShield ? union(bearingRing, bearingShield) : bearingRing).translate(0, 0, bearingZ).color("#111827");
52117
+ const shaftCore = cylinder(shaftLength, shaftDiameter / 2, void 0, segments).translate(0, 0, -shaftOverhang);
52118
+ const lowerShoulder = cylinder(shoulderThickness, shoulderDiameter / 2, void 0, segments).translate(0, 0, lowerShoulderZ);
52119
+ const upperShoulder = cylinder(shoulderThickness, shoulderDiameter / 2, void 0, segments).translate(0, 0, upperShoulderZ);
52120
+ const shaft = union(shaftCore, lowerShoulder, upperShoulder).color("#cbd5e1");
52121
+ const parts = [
52122
+ { name: "bearing housing with counterbore pocket and shoulder", shape: housing },
52123
+ { name: "purchased radial bearing seated in counterbore", shape: bearing },
52124
+ { name: "shaft through bearing bore with retaining collars", shape: shaft }
52125
+ ];
52126
+ return {
52127
+ parts,
52128
+ housing,
52129
+ bearing,
52130
+ shaft,
52131
+ cutters: {
52132
+ bearingPocket,
52133
+ shaftBore
52134
+ },
52135
+ dims: {
52136
+ bearingOuterDiameter,
52137
+ bearingInnerDiameter,
52138
+ bearingWidth,
52139
+ shaftDiameter,
52140
+ housingWidth,
52141
+ housingDepth,
52142
+ housingThickness,
52143
+ bossOuterDiameter,
52144
+ bossHeight,
52145
+ totalHousingHeight,
52146
+ pocketDiameter,
52147
+ pocketDepth,
52148
+ shaftBoreDiameter,
52149
+ runningClearance,
52150
+ shaftLength,
52151
+ shoulderDiameter,
52152
+ shoulderThickness
52153
+ }
52154
+ };
52155
+ }
52156
+ function cableGlandAnchorAssembly(options) {
52157
+ const cableDiameter = requirePositive$6(options.cableDiameter, "cableDiameter");
52158
+ const panelThickness = requirePositive$6(options.panelThickness ?? 3, "panelThickness");
52159
+ const panelWidth = requirePositive$6(options.panelWidth ?? Math.max(54, cableDiameter * 7), "panelWidth");
52160
+ const panelHeight = requirePositive$6(options.panelHeight ?? Math.max(38, cableDiameter * 5), "panelHeight");
52161
+ const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
52162
+ const panelHoleClearance = requirePositive$6(options.panelHoleClearance ?? 0.25, "panelHoleClearance");
52163
+ const cableBoreDiameter = cableDiameter + runningClearance * 2;
52164
+ const glandOuterDiameter = requirePositive$6(options.glandOuterDiameter ?? cableDiameter + Math.max(6, cableDiameter * 0.9), "glandOuterDiameter");
52165
+ const nutOuterDiameter = requirePositive$6(options.nutOuterDiameter ?? glandOuterDiameter + Math.max(6, cableDiameter * 0.8), "nutOuterDiameter");
52166
+ const nutThickness = requirePositive$6(options.nutThickness ?? Math.max(4, cableDiameter * 0.8), "nutThickness");
52167
+ const flangeDiameter = requirePositive$6(options.flangeDiameter ?? glandOuterDiameter + Math.max(5, cableDiameter * 0.7), "flangeDiameter");
52168
+ const flangeThickness = requirePositive$6(options.flangeThickness ?? Math.max(2, panelThickness * 0.45), "flangeThickness");
52169
+ const minGlandLength = panelThickness + nutThickness + flangeThickness + runningClearance * 4;
52170
+ const glandLength = requirePositive$6(options.glandLength ?? minGlandLength + Math.max(8, cableDiameter), "glandLength");
52171
+ const cableLength = requirePositive$6(options.cableLength ?? glandLength + Math.max(36, cableDiameter * 5), "cableLength");
52172
+ const segments = options.segments ?? 40;
52173
+ if (glandOuterDiameter <= cableBoreDiameter + Math.max(1.2, cableDiameter * 0.18)) {
52174
+ throw new Error("cableGlandAnchorAssembly: glandOuterDiameter leaves too little wall around the cable bore");
52175
+ }
52176
+ if (nutOuterDiameter <= glandOuterDiameter + Math.max(1.5, cableDiameter * 0.2)) {
52177
+ throw new Error("cableGlandAnchorAssembly: nutOuterDiameter must leave material around the gland body");
52178
+ }
52179
+ if (flangeDiameter <= glandOuterDiameter + Math.max(1.2, cableDiameter * 0.16)) {
52180
+ throw new Error("cableGlandAnchorAssembly: flangeDiameter must be larger than the gland body");
52181
+ }
52182
+ if (panelWidth <= flangeDiameter + 8 || panelHeight <= flangeDiameter + 8) {
52183
+ throw new Error("cableGlandAnchorAssembly: panel dimensions leave too little material around the gland hole");
52184
+ }
52185
+ if (glandLength <= minGlandLength) {
52186
+ throw new Error("cableGlandAnchorAssembly: glandLength must span the panel, flange, compression nut, and clearances");
52187
+ }
52188
+ if (cableLength <= glandLength + runningClearance * 2) {
52189
+ throw new Error("cableGlandAnchorAssembly: cableLength must extend beyond the gland body");
52190
+ }
52191
+ const panelHoleDiameter = glandOuterDiameter + panelHoleClearance * 2;
52192
+ const glandOuterRadius = glandOuterDiameter / 2;
52193
+ const cableBoreRadius = cableBoreDiameter / 2;
52194
+ const faceClearance = Math.min(0.05, runningClearance * 0.15);
52195
+ const flangePocketDepth = Math.min(Math.max(0.35, panelThickness * 0.18), panelThickness * 0.4, flangeThickness * 0.55);
52196
+ const panelHole = cylinderAlongX(panelThickness + 0.8, panelHoleDiameter / 2, 0, segments);
52197
+ const flangeSeatPocket = cylinderAlongX(
52198
+ flangePocketDepth + 0.2,
52199
+ flangeDiameter / 2 + panelHoleClearance,
52200
+ panelThickness / 2 - flangePocketDepth / 2,
52201
+ segments
52202
+ );
52203
+ const cableBore = cylinderAlongX(glandLength + 0.8, cableBoreRadius, 0, segments);
52204
+ const panel = box(panelThickness, panelWidth, panelHeight).translate(0, 0, -panelHeight / 2).subtract(panelHole).subtract(flangeSeatPocket).color("#475569");
52205
+ const glandBody = tubeAlongX(glandLength, glandOuterRadius, cableBoreRadius, 0, segments);
52206
+ const flangeCenterX = panelThickness / 2 - flangePocketDepth + faceClearance + flangeThickness / 2;
52207
+ const flange = tubeAlongX(flangeThickness, flangeDiameter / 2, cableBoreRadius, flangeCenterX, segments);
52208
+ const gland = union(glandBody, flange).color("#94a3b8");
52209
+ const nutInnerRadius = glandOuterRadius + Math.min(0.12, runningClearance * 0.4);
52210
+ const nutCenterX = -panelThickness / 2 - faceClearance - nutThickness / 2;
52211
+ const compressionNut = tubeAlongX(nutThickness, nutOuterDiameter / 2, nutInnerRadius, nutCenterX, segments).color("#cbd5e1");
52212
+ const cable = cylinderAlongX(cableLength, cableDiameter / 2, 0, segments).color("#111827");
52213
+ const parts = [
52214
+ { name: "panel with gland clearance hole", shape: panel },
52215
+ { name: "hollow cable gland body with panel flange", shape: gland },
52216
+ { name: "compression nut around gland body", shape: compressionNut },
52217
+ { name: "routed cable through gland bore", shape: cable }
52218
+ ];
52219
+ return {
52220
+ parts,
52221
+ panel,
52222
+ gland,
52223
+ compressionNut,
52224
+ cable,
52225
+ cutters: {
52226
+ panelHole,
52227
+ flangeSeatPocket,
52228
+ cableBore
52229
+ },
52230
+ dims: {
52231
+ cableDiameter,
52232
+ cableBoreDiameter,
52233
+ panelThickness,
52234
+ panelWidth,
52235
+ panelHeight,
52236
+ glandOuterDiameter,
52237
+ glandLength,
52238
+ nutOuterDiameter,
52239
+ nutThickness,
52240
+ flangeDiameter,
52241
+ flangeThickness,
52242
+ runningClearance,
52243
+ faceClearance,
52244
+ flangePocketDepth,
52245
+ panelHoleDiameter,
52246
+ cableLength
52247
+ }
52248
+ };
52249
+ }
52250
+ function hoseBarbPortAssembly(options) {
52251
+ const hoseInnerDiameter = requirePositive$6(options.hoseInnerDiameter, "hoseInnerDiameter");
52252
+ const runningClearance = requirePositive$6(options.runningClearance ?? 0.18, "runningClearance");
52253
+ const faceClearance = requirePositive$6(options.faceClearance ?? 0.04, "faceClearance");
52254
+ const barbRootDiameter = requirePositive$6(
52255
+ options.barbRootDiameter ?? Math.max(1, hoseInnerDiameter - Math.max(0.25, hoseInnerDiameter * 0.06)),
52256
+ "barbRootDiameter"
52257
+ );
52258
+ const barbPeakDiameter = requirePositive$6(
52259
+ options.barbPeakDiameter ?? hoseInnerDiameter + Math.max(0.65, hoseInnerDiameter * 0.12),
52260
+ "barbPeakDiameter"
52261
+ );
52262
+ const installedHoseBoreDiameter = barbPeakDiameter + runningClearance * 2;
52263
+ const hoseOuterDiameter = requirePositive$6(
52264
+ options.hoseOuterDiameter ?? Math.max(installedHoseBoreDiameter + 2.4, hoseInnerDiameter + Math.max(3, hoseInnerDiameter * 0.55)),
52265
+ "hoseOuterDiameter"
52266
+ );
52267
+ const fluidBoreDiameter = requirePositive$6(options.fluidBoreDiameter ?? hoseInnerDiameter * 0.65, "fluidBoreDiameter");
52268
+ const blockThickness = requirePositive$6(options.blockThickness ?? Math.max(7, hoseInnerDiameter * 1.2), "blockThickness");
52269
+ const barbCount = options.barbCount ?? 3;
52270
+ const barbLength = requirePositive$6(options.barbLength ?? Math.max(2.6, hoseInnerDiameter * 0.55), "barbLength");
52271
+ const barbStackLength = barbCount * barbLength;
52272
+ const shoulderDiameter = requirePositive$6(
52273
+ options.shoulderDiameter ?? barbPeakDiameter + Math.max(4, hoseInnerDiameter * 0.65),
52274
+ "shoulderDiameter"
52275
+ );
52276
+ const shoulderThickness = requirePositive$6(options.shoulderThickness ?? Math.max(2, hoseInnerDiameter * 0.35), "shoulderThickness");
52277
+ const bossDiameter = requirePositive$6(options.bossDiameter ?? shoulderDiameter + Math.max(4, hoseInnerDiameter * 0.6), "bossDiameter");
52278
+ const bossHeight = requirePositive$6(options.bossHeight ?? Math.max(2.4, hoseInnerDiameter * 0.45), "bossHeight");
52279
+ const blockWidth = requirePositive$6(options.blockWidth ?? bossDiameter + Math.max(14, hoseInnerDiameter * 2.4), "blockWidth");
52280
+ const blockHeight = requirePositive$6(options.blockHeight ?? bossDiameter + Math.max(12, hoseInnerDiameter * 2.1), "blockHeight");
52281
+ const hoseLength = requirePositive$6(options.hoseLength ?? barbStackLength + Math.max(32, hoseInnerDiameter * 5), "hoseLength");
52282
+ const clampWidth = requirePositive$6(options.clampWidth ?? Math.max(4, hoseOuterDiameter * 0.45), "clampWidth");
52283
+ const clampThickness = requirePositive$6(options.clampThickness ?? 0.9, "clampThickness");
52284
+ const segments = options.segments ?? 40;
52285
+ if (!Number.isInteger(barbCount) || barbCount < 1 || barbCount > 8) {
52286
+ throw new Error("hoseBarbPortAssembly: barbCount must be an integer from 1 to 8");
52287
+ }
52288
+ if (barbPeakDiameter <= hoseInnerDiameter) {
52289
+ throw new Error("hoseBarbPortAssembly: barbPeakDiameter must exceed hoseInnerDiameter so the barb retains the hose");
52290
+ }
52291
+ if (barbRootDiameter >= barbPeakDiameter - Math.max(0.25, hoseInnerDiameter * 0.04)) {
52292
+ throw new Error("hoseBarbPortAssembly: barbRootDiameter must leave a visible barb rise");
52293
+ }
52294
+ if (fluidBoreDiameter >= barbRootDiameter - Math.max(0.8, hoseInnerDiameter * 0.12)) {
52295
+ throw new Error("hoseBarbPortAssembly: fluidBoreDiameter leaves too little wall in the barb fitting");
52296
+ }
52297
+ if (hoseOuterDiameter <= installedHoseBoreDiameter + Math.max(1.2, hoseInnerDiameter * 0.16)) {
52298
+ throw new Error("hoseBarbPortAssembly: hoseOuterDiameter leaves too little hose wall around the installed barb envelope");
52299
+ }
52300
+ if (shoulderDiameter <= barbPeakDiameter + Math.max(1.5, hoseInnerDiameter * 0.2)) {
52301
+ throw new Error("hoseBarbPortAssembly: shoulderDiameter must be larger than the barb peaks");
52302
+ }
52303
+ if (bossDiameter <= shoulderDiameter + Math.max(1.5, hoseInnerDiameter * 0.2)) {
52304
+ throw new Error("hoseBarbPortAssembly: bossDiameter must leave material around the shoulder seat");
52305
+ }
52306
+ if (blockWidth <= bossDiameter + 8 || blockHeight <= bossDiameter + 8) {
52307
+ throw new Error("hoseBarbPortAssembly: receiver block dimensions leave too little material around the port boss");
52308
+ }
52309
+ const portBoreDiameter = barbRootDiameter + runningClearance * 2;
52310
+ const portBore = cylinderAlongX(blockThickness + bossHeight + 0.8, portBoreDiameter / 2, bossHeight / 2, segments);
52311
+ const fuseOverlap = Math.min(0.04, faceClearance * 0.7);
52312
+ const bossCenterX = blockThickness / 2 + bossHeight / 2 - fuseOverlap;
52313
+ const receiver = union(
52314
+ box(blockThickness, blockWidth, blockHeight).translate(0, 0, -blockHeight / 2),
52315
+ cylinderAlongX(bossHeight + fuseOverlap, bossDiameter / 2, bossCenterX, segments)
52316
+ ).subtract(portBore).color("#475569");
52317
+ const bossFaceX = blockThickness / 2 + bossHeight;
52318
+ const shoulderCenterX = bossFaceX + faceClearance + shoulderThickness / 2;
52319
+ const barbStartX = shoulderCenterX + shoulderThickness / 2;
52320
+ const fittingStartX = -blockThickness / 2 - runningClearance;
52321
+ const fittingEndX = barbStartX + barbStackLength;
52322
+ const fittingCore = tubeAlongX(fittingEndX - fittingStartX, barbRootDiameter / 2, fluidBoreDiameter / 2, (fittingStartX + fittingEndX) / 2, segments);
52323
+ const shoulder = tubeAlongX(shoulderThickness, shoulderDiameter / 2, fluidBoreDiameter / 2, shoulderCenterX, segments);
52324
+ const barbSolids = [];
52325
+ const ridgeLength = Math.max(0.8, Math.min(barbLength * 0.45, hoseInnerDiameter * 0.28));
52326
+ for (let index2 = 0; index2 < barbCount; index2 += 1) {
52327
+ const startX = barbStartX + index2 * barbLength;
52328
+ const ridgeCenterX = startX + barbLength - ridgeLength / 2;
52329
+ barbSolids.push(tubeAlongX(ridgeLength, barbPeakDiameter / 2, fluidBoreDiameter / 2, ridgeCenterX, segments));
52330
+ }
52331
+ const fitting = union(fittingCore, shoulder, ...barbSolids).color("#94a3b8");
52332
+ const hoseStartX = barbStartX + faceClearance;
52333
+ const hoseCenterX = hoseStartX + hoseLength / 2;
52334
+ const installedHoseBore = cylinderAlongX(hoseLength + 0.8, installedHoseBoreDiameter / 2, hoseCenterX, segments);
52335
+ const hose = tubeAlongX(hoseLength, hoseOuterDiameter / 2, installedHoseBoreDiameter / 2, hoseCenterX, segments).color("#111827");
52336
+ const clampCenterX = barbStartX + Math.min(barbStackLength * 0.55, Math.max(barbLength, clampWidth));
52337
+ const clamp2 = tubeAlongX(
52338
+ clampWidth,
52339
+ hoseOuterDiameter / 2 + clampThickness,
52340
+ hoseOuterDiameter / 2 + Math.min(0.08, runningClearance * 0.45),
52341
+ clampCenterX,
52342
+ segments
52343
+ ).color("#cbd5e1");
52344
+ const parts = [
52345
+ { name: "bored pump or filter body with raised hose-port boss", shape: receiver },
52346
+ { name: "hollow hose barb fitting with shoulder and retention ridges", shape: fitting },
52347
+ { name: "installed flexible hose over barb tail", shape: hose },
52348
+ { name: "clamp band over hose and barb ridges", shape: clamp2 }
52349
+ ];
52350
+ return {
52351
+ parts,
52352
+ receiver,
52353
+ fitting,
52354
+ hose,
52355
+ clamp: clamp2,
52356
+ cutters: {
52357
+ portBore,
52358
+ installedHoseBore
52359
+ },
52360
+ dims: {
52361
+ hoseInnerDiameter,
52362
+ hoseOuterDiameter,
52363
+ installedHoseBoreDiameter,
52364
+ blockThickness,
52365
+ blockWidth,
52366
+ blockHeight,
52367
+ bossDiameter,
52368
+ bossHeight,
52369
+ fluidBoreDiameter,
52370
+ barbRootDiameter,
52371
+ barbPeakDiameter,
52372
+ barbCount,
52373
+ barbLength,
52374
+ barbStackLength,
52375
+ shoulderDiameter,
52376
+ shoulderThickness,
52377
+ hoseLength,
52378
+ clampWidth,
52379
+ clampThickness,
52380
+ runningClearance,
52381
+ faceClearance
52382
+ }
52383
+ };
52384
+ }
52385
+ function routedTubeClipAssembly(options) {
52386
+ const tubeDiameter = requirePositive$6(options.tubeDiameter, "tubeDiameter");
52387
+ const tubeLength = requirePositive$6(options.tubeLength ?? 120, "tubeLength");
52388
+ const panelThickness = requirePositive$6(options.panelThickness ?? 3, "panelThickness");
52389
+ const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
52390
+ const screwSize = options.screwSize ?? "M3";
52391
+ const segments = options.segments ?? 32;
52392
+ const sizeData = METRIC_HOLE_TABLE[screwSize];
52393
+ if (!sizeData) throw new Error(`routedTubeClipAssembly: unsupported screwSize "${screwSize}"`);
52394
+ const clipCount = options.clipCount ?? 3;
52395
+ if (!Number.isInteger(clipCount) || clipCount < 1 || clipCount > 8) {
52396
+ throw new Error("routedTubeClipAssembly: clipCount must be an integer from 1 to 8");
52397
+ }
52398
+ const screwDiameter = parseFloat(screwSize.replace("M", ""));
52399
+ const screwHeadDiameter = sizeData.head;
52400
+ const tubeBoreDiameter = tubeDiameter + runningClearance * 2;
52401
+ const clipWallThickness = requirePositive$6(
52402
+ options.clipWallThickness ?? Math.max(screwHeadDiameter + 1.2, tubeDiameter * 0.45, 5),
52403
+ "clipWallThickness"
52404
+ );
52405
+ const clipWidth = requirePositive$6(options.clipWidth ?? Math.max(screwHeadDiameter + 3, tubeDiameter * 1.4, 10), "clipWidth");
52406
+ const clipDepth = tubeBoreDiameter + clipWallThickness * 2;
52407
+ const bottomWall = Math.max(1.2, clipWallThickness * 0.35);
52408
+ const topWall = Math.max(2, clipWallThickness * 0.45);
52409
+ const clipHeight = bottomWall + tubeBoreDiameter + topWall;
52410
+ const tubeCenterZ = panelThickness + bottomWall + tubeBoreDiameter / 2;
52411
+ const panelLength = requirePositive$6(options.panelLength ?? tubeLength + 24, "panelLength");
52412
+ const panelWidth = requirePositive$6(options.panelWidth ?? clipDepth + Math.max(14, screwHeadDiameter * 2), "panelWidth");
52413
+ if (tubeLength <= clipWidth + 8) {
52414
+ throw new Error("routedTubeClipAssembly: tubeLength must leave visible tube beyond the clip body");
52415
+ }
52416
+ const defaultSpacing = clipCount === 1 ? 0 : Math.max(clipWidth + 8, (tubeLength - clipWidth * 2) / (clipCount - 1));
52417
+ const clipSpacing = options.clipSpacing === void 0 ? defaultSpacing : requirePositive$6(options.clipSpacing, "clipSpacing");
52418
+ const clipCenters = Array.from({ length: clipCount }, (_2, index2) => (index2 - (clipCount - 1) / 2) * clipSpacing);
52419
+ const maxClipExtent = Math.max(...clipCenters.map((x2) => Math.abs(x2) + clipWidth / 2));
52420
+ if (maxClipExtent > tubeLength / 2 - 2) {
52421
+ throw new Error("routedTubeClipAssembly: clipSpacing places a clip beyond the routed tube length");
52422
+ }
52423
+ if (maxClipExtent > panelLength / 2 - 2) {
52424
+ throw new Error("routedTubeClipAssembly: panelLength is too short for the clip pattern");
52425
+ }
52426
+ const boreRadius = tubeBoreDiameter / 2;
52427
+ const screwY = boreRadius + clipWallThickness / 2;
52428
+ if (screwY + screwHeadDiameter / 2 > clipDepth / 2 - 0.2) {
52429
+ throw new Error("routedTubeClipAssembly: clipWallThickness leaves too little land for screw heads");
52430
+ }
52431
+ if (clipDepth > panelWidth - Math.max(4, screwHeadDiameter * 0.5)) {
52432
+ throw new Error("routedTubeClipAssembly: panelWidth leaves too little material beside the clips");
52433
+ }
52434
+ const screwPositions = clipCenters.flatMap((x2) => [
52435
+ [x2, -screwY],
52436
+ [x2, screwY]
52437
+ ]);
52438
+ const screwClearanceDiameter = Math.max(sizeData.loose, screwDiameter + 0.8);
52439
+ const panelThreadEnvelopeDiameter = screwClearanceDiameter;
52440
+ const clipTopZ = panelThickness + clipHeight;
52441
+ const clipTubeBores = union(
52442
+ ...clipCenters.map((x2) => cylinderAlongX(clipWidth + 0.8, boreRadius, x2, segments).translate(0, 0, tubeCenterZ))
52443
+ );
52444
+ const clipScrewClearances = union(
52445
+ ...screwPositions.map(([x2, y2]) => cylinder(clipHeight + 0.8, screwClearanceDiameter / 2, void 0, segments).translate(x2, y2, panelThickness - 0.4))
52446
+ );
52447
+ const panelThreadEnvelopes = union(
52448
+ ...screwPositions.map(([x2, y2]) => cylinder(panelThickness + 0.8, panelThreadEnvelopeDiameter / 2, void 0, segments).translate(x2, y2, -0.4))
52449
+ );
52450
+ const panel = box(panelLength, panelWidth, panelThickness).subtract(panelThreadEnvelopes).color("#475569");
52451
+ const tube2 = cylinderAlongX(tubeLength, tubeDiameter / 2, 0, segments).translate(0, 0, tubeCenterZ).color("#0f172a");
52452
+ const clips = clipCenters.map((x2) => {
52453
+ const body = box(clipWidth, clipDepth, clipHeight).translate(x2, 0, panelThickness);
52454
+ const tubeBore = cylinderAlongX(clipWidth + 0.8, boreRadius, x2, segments).translate(0, 0, tubeCenterZ);
52455
+ const screwHoles = union(
52456
+ cylinder(clipHeight + 0.8, screwClearanceDiameter / 2, void 0, segments).translate(x2, -screwY, panelThickness - 0.4),
52457
+ cylinder(clipHeight + 0.8, screwClearanceDiameter / 2, void 0, segments).translate(x2, screwY, panelThickness - 0.4)
52458
+ );
52459
+ return body.subtract(tubeBore).subtract(screwHoles).color("#94a3b8");
52460
+ });
52461
+ const screwLength = clipHeight + panelThickness * 0.65;
52462
+ const screwHeadHeight = Math.max(1.2, screwDiameter * 0.55);
52463
+ const screwBlank = union(
52464
+ cylinder(screwLength, screwDiameter / 2, void 0, segments).translate(0, 0, clipTopZ - screwLength),
52465
+ cylinder(screwHeadHeight, screwHeadDiameter / 2, void 0, segments).translate(0, 0, clipTopZ)
52466
+ ).color("#cbd5e1");
52467
+ const screws = screwPositions.map(([x2, y2]) => screwBlank.translate(x2, y2, 0));
52468
+ const parts = [
52469
+ { name: "panel with tube-clip screw receiving holes", shape: panel },
52470
+ { name: "routed flexible tube through retained clip bores", shape: tube2 },
52471
+ ...clips.map((shape, index2) => ({ name: `saddle tube clip ${index2 + 1} with through-bore`, shape })),
52472
+ ...screws.map((shape, index2) => ({ name: `installed ${screwSize} tube clip screw ${index2 + 1}`, shape }))
52473
+ ];
52474
+ return {
52475
+ parts,
52476
+ panel,
52477
+ tube: tube2,
52478
+ clips,
52479
+ screws,
52480
+ clipCenters,
52481
+ screwPositions,
52482
+ cutters: {
52483
+ clipTubeBores,
52484
+ clipScrewClearances,
52485
+ panelThreadEnvelopes
52486
+ },
52487
+ dims: {
52488
+ tubeDiameter,
52489
+ tubeLength,
52490
+ tubeBoreDiameter,
52491
+ panelLength,
52492
+ panelWidth,
52493
+ panelThickness,
52494
+ clipCount,
52495
+ clipWidth,
52496
+ clipDepth,
52497
+ clipHeight,
52498
+ clipWallThickness,
52499
+ tubeCenterZ,
52500
+ screwSize,
52501
+ screwDiameter,
52502
+ screwHeadDiameter,
52503
+ screwLength,
52504
+ screwClearanceDiameter,
52505
+ panelThreadEnvelopeDiameter,
52506
+ runningClearance
52507
+ }
52508
+ };
52509
+ }
52510
+ function pcbTerminalBlockAssembly(options = {}) {
52511
+ const terminalCount = options.terminalCount ?? 4;
52512
+ if (!Number.isInteger(terminalCount) || terminalCount < 1 || terminalCount > 24) {
52513
+ throw new Error("pcbTerminalBlockAssembly: terminalCount must be an integer from 1 to 24");
52514
+ }
52515
+ const terminalPitch = requirePositive$6(options.terminalPitch ?? 5.08, "terminalPitch");
52516
+ const terminalBlockWidth = terminalPitch * terminalCount + 3;
52517
+ const boardWidth = requirePositive$6(options.boardWidth ?? Math.max(50, terminalBlockWidth + 28), "boardWidth");
52518
+ const boardDepth = requirePositive$6(options.boardDepth ?? 38, "boardDepth");
52519
+ const boardThickness = requirePositive$6(options.boardThickness ?? 1.6, "boardThickness");
52520
+ const backplateThickness = requirePositive$6(options.backplateThickness ?? 3, "backplateThickness");
52521
+ const backplateMargin = requirePositive$6(options.backplateMargin ?? 5, "backplateMargin");
52522
+ const standoffHeight = requirePositive$6(options.standoffHeight ?? 6, "standoffHeight");
52523
+ const screwSize = options.screwSize ?? "M3";
52524
+ const segments = options.segments ?? 28;
52525
+ const sizeData = METRIC_HOLE_TABLE[screwSize];
52526
+ if (!sizeData) throw new Error(`pcbTerminalBlockAssembly: unsupported screwSize "${screwSize}"`);
52527
+ const screwDiameter = parseFloat(screwSize.replace("M", ""));
52528
+ const screwHeadDiameter = sizeData.head;
52529
+ const screwHeadHeight = Math.max(1.2, screwDiameter * 0.55);
52530
+ const standoffDiameter = requirePositive$6(
52531
+ options.standoffDiameter ?? Math.max(screwHeadDiameter * 1.45, sizeData.normal + 3),
52532
+ "standoffDiameter"
52533
+ );
52534
+ const [mountInsetX, mountInsetY] = resolveBoltInset(
52535
+ options.mountingInset,
52536
+ Math.max(standoffDiameter / 2 + 1.2, screwHeadDiameter * 0.75)
52537
+ );
52538
+ if (mountInsetX * 2 >= boardWidth || mountInsetY * 2 >= boardDepth) {
52539
+ throw new Error("pcbTerminalBlockAssembly: mountingInset leaves no room for the PCB mounting pattern");
52540
+ }
52541
+ const terminalBlockDepth = requirePositive$6(options.terminalBlockDepth ?? 10, "terminalBlockDepth");
52542
+ const terminalBlockHeight = requirePositive$6(options.terminalBlockHeight ?? 9, "terminalBlockHeight");
52543
+ const terminalEdgeInset = requirePositive$6(options.terminalEdgeInset ?? 5, "terminalEdgeInset");
52544
+ const pinDiameter = requirePositive$6(options.pinDiameter ?? 0.9, "pinDiameter");
52545
+ const pinClearance = requirePositive$6(options.pinClearance ?? 0.25, "pinClearance");
52546
+ const pinTailLength = requireNonNegative(options.pinTailLength ?? 0, "pinTailLength");
52547
+ const wirePortDiameter = requirePositive$6(options.wirePortDiameter ?? 2.6, "wirePortDiameter");
52548
+ const pinHoleDiameter = pinDiameter + pinClearance;
52549
+ const terminalCenterY = -boardDepth / 2 + terminalEdgeInset + terminalBlockDepth / 2;
52550
+ const pinY = terminalCenterY + terminalBlockDepth * 0.24;
52551
+ const firstPinX = -((terminalCount - 1) * terminalPitch) / 2;
52552
+ const pinPositions = Array.from({ length: terminalCount }, (_2, index2) => [firstPinX + index2 * terminalPitch, pinY]);
52553
+ const mountingPositions = [
52554
+ [-boardWidth / 2 + mountInsetX, -boardDepth / 2 + mountInsetY],
52555
+ [boardWidth / 2 - mountInsetX, -boardDepth / 2 + mountInsetY],
52556
+ [-boardWidth / 2 + mountInsetX, boardDepth / 2 - mountInsetY],
52557
+ [boardWidth / 2 - mountInsetX, boardDepth / 2 - mountInsetY]
52558
+ ];
52559
+ if (terminalBlockWidth >= boardWidth - mountInsetX * 2) {
52560
+ throw new Error("pcbTerminalBlockAssembly: terminal block is too wide for the PCB mounting pattern");
52561
+ }
52562
+ if (terminalEdgeInset + terminalBlockDepth >= boardDepth - mountInsetY * 2) {
52563
+ throw new Error("pcbTerminalBlockAssembly: terminal block depth collides with the rear mounting datum");
52564
+ }
52565
+ if (pinHoleDiameter >= terminalPitch * 0.55) {
52566
+ throw new Error("pcbTerminalBlockAssembly: pinDiameter and pinClearance leave too little PCB web between terminal holes");
52567
+ }
52568
+ if (wirePortDiameter >= Math.min(terminalPitch * 0.72, terminalBlockHeight * 0.65)) {
52569
+ throw new Error("pcbTerminalBlockAssembly: wirePortDiameter is too large for the terminal pitch or body height");
52570
+ }
52571
+ for (const [index2, [x2, y2]] of [...mountingPositions, ...pinPositions].entries()) {
52572
+ if (!Number.isFinite(x2) || !Number.isFinite(y2)) {
52573
+ throw new Error(`pcbTerminalBlockAssembly: generated datum position ${index2} is not finite`);
52574
+ }
52575
+ }
52576
+ const backplateWidth = boardWidth + backplateMargin * 2;
52577
+ const backplateDepth = boardDepth + backplateMargin * 2;
52578
+ const boardBottomZ = backplateThickness + standoffHeight;
52579
+ const boardTopZ = boardBottomZ + boardThickness;
52580
+ const standoffOverlap = Math.min(0.08, standoffHeight * 0.03);
52581
+ const standoffThreadEnvelopeDiameter = Math.max(sizeData.loose, screwDiameter + 1);
52582
+ const standoffThreadEnvelope = cylinder(standoffHeight + 0.8, standoffThreadEnvelopeDiameter / 2, void 0, segments).translate(
52583
+ 0,
52584
+ 0,
52585
+ backplateThickness - 0.4
52586
+ );
52587
+ const standoffThreadEnvelopes = union(...mountingPositions.map(([x2, y2]) => standoffThreadEnvelope.translate(x2, y2, 0)));
52588
+ const standoff = cylinder(standoffHeight + standoffOverlap, standoffDiameter / 2, void 0, segments).translate(0, 0, backplateThickness - standoffOverlap).subtract(standoffThreadEnvelope);
52589
+ const standoffs = union(...mountingPositions.map(([x2, y2]) => standoff.translate(x2, y2, 0)));
52590
+ const backplate = union(box(backplateWidth, backplateDepth, backplateThickness), standoffs).color("#475569");
52591
+ const boardMountingHoleDiameter = sizeData.normal;
52592
+ const boardMountHole = cylinder(boardThickness + 0.8, boardMountingHoleDiameter / 2, void 0, segments).translate(
52593
+ 0,
52594
+ 0,
52595
+ boardBottomZ - 0.4
52596
+ );
52597
+ const pcbMountingHoles = union(...mountingPositions.map(([x2, y2]) => boardMountHole.translate(x2, y2, 0)));
52598
+ const pinHole = cylinder(boardThickness + 0.8, pinHoleDiameter / 2, void 0, segments).translate(0, 0, boardBottomZ - 0.4);
52599
+ const pcbPinHoles = union(...pinPositions.map(([x2, y2]) => pinHole.translate(x2, y2, 0)));
52600
+ const pcb = box(boardWidth, boardDepth, boardThickness).translate(0, 0, boardBottomZ).subtract(pcbMountingHoles).subtract(pcbPinHoles).color("#166534");
52601
+ const terminalBodyBlank = box(terminalBlockWidth, terminalBlockDepth, terminalBlockHeight).translate(0, terminalCenterY, boardTopZ);
52602
+ const wirePort = cylinderAlongY(terminalBlockDepth + 0.8, wirePortDiameter / 2, terminalCenterY, segments).translate(
52603
+ 0,
52604
+ 0,
52605
+ boardTopZ + terminalBlockHeight * 0.42
52606
+ );
52607
+ const wirePorts = union(...pinPositions.map(([x2]) => wirePort.translate(x2, 0, 0)));
52608
+ const clampScrewPockets = union(
52609
+ ...pinPositions.map(
52610
+ ([x2]) => cylinder(Math.max(0.6, terminalBlockHeight * 0.22), Math.min(terminalPitch * 0.22, wirePortDiameter * 0.42), void 0, segments).translate(
52611
+ x2,
52612
+ terminalCenterY + terminalBlockDepth * 0.12,
52613
+ boardTopZ + terminalBlockHeight * 0.76
52614
+ )
52615
+ )
52616
+ );
52617
+ const pinLength = boardThickness + pinTailLength + Math.min(0.6, terminalBlockHeight * 0.08);
52618
+ const pinStartZ = boardBottomZ - pinTailLength;
52619
+ const pins = union(...pinPositions.map(([x2, y2]) => cylinder(pinLength, pinDiameter / 2, void 0, segments).translate(x2, y2, pinStartZ)));
52620
+ const terminalBlock = union(terminalBodyBlank.subtract(wirePorts).subtract(clampScrewPockets), pins).color("#16a34a");
52621
+ const screwShaftLength = boardThickness + standoffHeight * 0.85;
52622
+ const mountingHardware = fastenerSet(screwSize, screwShaftLength, {
52623
+ washerUnderHead: false,
52624
+ washerUnderNut: false,
52625
+ fit: "normal",
52626
+ segments
52627
+ });
52628
+ const screws = mountingPositions.map(([x2, y2]) => mountingHardware.bolt.translate(x2, y2, boardTopZ).color("#cbd5e1"));
52629
+ const parts = [
52630
+ { name: "electronics backplate with fused PCB standoffs", shape: backplate },
52631
+ { name: "PCB with mounting holes and terminal pin clearances", shape: pcb },
52632
+ { name: "seated purchased terminal block with through-board pins", shape: terminalBlock },
52633
+ ...screws.map((shape, index2) => ({ name: `installed ${screwSize} PCB mounting screw ${index2 + 1}`, shape }))
52634
+ ];
52635
+ return {
52636
+ parts,
52637
+ backplate,
52638
+ pcb,
52639
+ terminalBlock,
52640
+ screws,
52641
+ mountingPositions,
52642
+ pinPositions,
52643
+ cutters: {
52644
+ pcbMountingHoles,
52645
+ pcbPinHoles,
52646
+ standoffThreadEnvelopes
52647
+ },
52648
+ dims: {
52649
+ terminalCount,
52650
+ terminalPitch,
52651
+ boardWidth,
52652
+ boardDepth,
52653
+ boardThickness,
52654
+ backplateWidth,
52655
+ backplateDepth,
52656
+ backplateThickness,
52657
+ standoffHeight,
52658
+ standoffDiameter,
52659
+ screwSize,
52660
+ screwDiameter,
52661
+ screwHeadDiameter,
52662
+ screwHeadHeight,
52663
+ screwShaftLength,
52664
+ boardMountingHoleDiameter,
52665
+ standoffThreadEnvelopeDiameter,
52666
+ terminalBlockWidth,
52667
+ terminalBlockDepth,
52668
+ terminalBlockHeight,
52669
+ terminalEdgeInset,
52670
+ pinDiameter,
52671
+ pinClearance,
52672
+ pinHoleDiameter,
52673
+ pinTailLength,
52674
+ wirePortDiameter
52675
+ }
52676
+ };
52677
+ }
52678
+ function thumbScrewClampAssembly(options = {}) {
52679
+ const screwSize = options.screwSize ?? "M6";
52680
+ const segments = options.segments ?? 36;
52681
+ const sizeData = METRIC_HOLE_TABLE[screwSize];
52682
+ if (!sizeData) throw new Error(`thumbScrewClampAssembly: unsupported screwSize "${screwSize}"`);
52683
+ const screwDiameter = parseFloat(screwSize.replace("M", ""));
52684
+ const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
52685
+ const faceClearance = requireNonNegative(options.faceClearance ?? 0, "faceClearance");
52686
+ const threadEnvelopeDiameter = Math.max(sizeData.normal, screwDiameter + runningClearance * 2);
52687
+ const pressurePadDiameter = requirePositive$6(
52688
+ options.pressurePadDiameter ?? Math.max(screwDiameter * 3.2, 18),
52689
+ "pressurePadDiameter"
52690
+ );
52691
+ const pressurePadThickness = requirePositive$6(
52692
+ options.pressurePadThickness ?? Math.max(screwDiameter * 0.72, 4),
52693
+ "pressurePadThickness"
52694
+ );
52695
+ const knobDiameter = requirePositive$6(options.knobDiameter ?? Math.max(screwDiameter * 4.2, 24), "knobDiameter");
52696
+ const knobThickness = requirePositive$6(options.knobThickness ?? Math.max(screwDiameter * 0.9, 7), "knobThickness");
52697
+ const workpieceThickness = requirePositive$6(options.workpieceThickness ?? 18, "workpieceThickness");
52698
+ const workpieceDepth = requirePositive$6(options.workpieceDepth ?? Math.max(46, pressurePadDiameter * 1.5), "workpieceDepth");
52699
+ const workpieceHeight = requirePositive$6(options.workpieceHeight ?? Math.max(pressurePadDiameter * 1.35, 24), "workpieceHeight");
52700
+ const frameDepth = requirePositive$6(
52701
+ options.frameDepth ?? Math.max(workpieceDepth + 12, pressurePadDiameter + 16),
52702
+ "frameDepth"
52703
+ );
52704
+ const baseThickness = requirePositive$6(options.baseThickness ?? Math.max(screwDiameter, 6), "baseThickness");
52705
+ const jawThickness = requirePositive$6(options.jawThickness ?? Math.max(screwDiameter * 1.35, 9), "jawThickness");
52706
+ const supportThickness = requirePositive$6(
52707
+ options.supportThickness ?? Math.max(screwDiameter * 1.8, 12),
52708
+ "supportThickness"
52709
+ );
52710
+ const bossLength = requirePositive$6(options.bossLength ?? Math.max(screwDiameter * 1.1, 8), "bossLength");
52711
+ const bossDiameter = requirePositive$6(options.bossDiameter ?? Math.max(threadEnvelopeDiameter + 5, screwDiameter * 2.5), "bossDiameter");
52712
+ const exposedScrewLength = requirePositive$6(
52713
+ options.exposedScrewLength ?? Math.max(pressurePadDiameter * 0.45, screwDiameter * 2.2),
52714
+ "exposedScrewLength"
52715
+ );
52716
+ const screwCenterZ = baseThickness + Math.max(workpieceHeight * 0.52, pressurePadDiameter * 0.68);
52717
+ const frameHeight = requirePositive$6(
52718
+ options.frameHeight ?? screwCenterZ - baseThickness + pressurePadDiameter / 2 + Math.max(baseThickness, 7),
52719
+ "frameHeight"
52720
+ );
52721
+ if (workpieceDepth > frameDepth - 6) {
52722
+ throw new Error("thumbScrewClampAssembly: frameDepth must leave side material around the clamped workpiece");
52723
+ }
52724
+ if (pressurePadDiameter > frameDepth - 4) {
52725
+ throw new Error("thumbScrewClampAssembly: pressurePadDiameter is too large for the frame depth");
52726
+ }
52727
+ if (bossDiameter > frameDepth - 4) {
52728
+ throw new Error("thumbScrewClampAssembly: bossDiameter is too large for the frame depth");
52729
+ }
52730
+ if (screwCenterZ - pressurePadDiameter / 2 <= baseThickness + 0.5) {
52731
+ throw new Error("thumbScrewClampAssembly: pressure pad collides with the base bridge");
52732
+ }
52733
+ if (baseThickness + frameHeight - screwCenterZ <= pressurePadDiameter / 2 + 2) {
52734
+ throw new Error("thumbScrewClampAssembly: frameHeight leaves too little material above the screw axis");
52735
+ }
52736
+ if (threadEnvelopeDiameter + 4 > Math.min(frameDepth, frameHeight)) {
52737
+ throw new Error("thumbScrewClampAssembly: threaded boss bore leaves too little surrounding frame material");
52738
+ }
52739
+ const workpieceLeftFaceX = -workpieceThickness / 2;
52740
+ const workpieceRightFaceX = workpieceThickness / 2;
52741
+ const anvilOverlap = Math.min(0.35, pressurePadThickness * 0.18);
52742
+ const anvilPadCenterX = workpieceLeftFaceX - faceClearance - pressurePadThickness / 2;
52743
+ const pressurePadCenterX = workpieceRightFaceX + faceClearance + pressurePadThickness / 2;
52744
+ const fixedJawRightFaceX = anvilPadCenterX - pressurePadThickness / 2 + anvilOverlap;
52745
+ const fixedJawCenterX = fixedJawRightFaceX - jawThickness / 2;
52746
+ const pressurePadRightFaceX = pressurePadCenterX + pressurePadThickness / 2;
52747
+ const supportInnerFaceX = pressurePadRightFaceX + exposedScrewLength;
52748
+ const supportCenterX = supportInnerFaceX + supportThickness / 2;
52749
+ const supportOuterFaceX = supportInnerFaceX + supportThickness;
52750
+ const frameLeftFaceX = fixedJawCenterX - jawThickness / 2;
52751
+ const frameRightFaceX = supportOuterFaceX;
52752
+ const baseLength = frameRightFaceX - frameLeftFaceX;
52753
+ if (baseLength <= 0 || !Number.isFinite(baseLength)) {
52754
+ throw new Error("thumbScrewClampAssembly: generated clamp frame length is invalid");
52755
+ }
52756
+ const bossCenterX = supportInnerFaceX + (supportThickness + bossLength) / 2;
52757
+ const threadedBossBore = cylinderAlongX(supportThickness + bossLength + 1, threadEnvelopeDiameter / 2, bossCenterX, segments).translate(
52758
+ 0,
52759
+ 0,
52760
+ screwCenterZ
52761
+ );
52762
+ const frameOverlap = Math.min(0.12, baseThickness * 0.04);
52763
+ const base = box(baseLength, frameDepth, baseThickness).translate((frameLeftFaceX + frameRightFaceX) / 2, 0, 0);
52764
+ const fixedJaw = box(jawThickness, frameDepth, frameHeight + frameOverlap).translate(fixedJawCenterX, 0, baseThickness - frameOverlap);
52765
+ const support = box(supportThickness, frameDepth, frameHeight + frameOverlap).translate(supportCenterX, 0, baseThickness - frameOverlap);
52766
+ const boss2 = cylinderAlongX(supportThickness + bossLength, bossDiameter / 2, bossCenterX, segments).translate(0, 0, screwCenterZ);
52767
+ const anvilPad = cylinderAlongX(pressurePadThickness, pressurePadDiameter / 2, anvilPadCenterX, segments).translate(0, 0, screwCenterZ);
52768
+ const frame = union(base, fixedJaw, support, boss2, anvilPad).subtract(threadedBossBore).color("#475569");
52769
+ const workpieceBottomZ = screwCenterZ - workpieceHeight / 2;
52770
+ const workpiece = box(workpieceThickness, workpieceDepth, workpieceHeight).translate(0, 0, workpieceBottomZ).color("#a16207");
52771
+ const pressurePad = cylinderAlongX(pressurePadThickness, pressurePadDiameter / 2, pressurePadCenterX, segments).translate(0, 0, screwCenterZ);
52772
+ const knobCenterX = supportOuterFaceX + bossLength + runningClearance + knobThickness / 2;
52773
+ const knob = cylinderAlongX(knobThickness, knobDiameter / 2, knobCenterX, segments).translate(0, 0, screwCenterZ);
52774
+ const shaftLeftX = pressurePadRightFaceX - Math.min(pressurePadThickness * 0.45, screwDiameter * 0.45);
52775
+ const shaftRightX = knobCenterX + knobThickness / 2;
52776
+ const shaftLength = shaftRightX - shaftLeftX;
52777
+ if (shaftLength <= supportThickness + bossLength) {
52778
+ throw new Error("thumbScrewClampAssembly: generated screw length is too short for the threaded support");
52779
+ }
52780
+ const shaft = cylinderAlongX(shaftLength, screwDiameter / 2, (shaftLeftX + shaftRightX) / 2, segments).translate(0, 0, screwCenterZ);
52781
+ const clampScrew = union(shaft, pressurePad, knob).color("#cbd5e1");
52782
+ const workpieceEnvelope = box(workpieceThickness, workpieceDepth, workpieceHeight).translate(0, 0, workpieceBottomZ);
52783
+ return {
52784
+ parts: [
52785
+ { name: "thumb-screw clamp frame with fixed anvil and threaded boss", shape: frame },
52786
+ { name: "representative clamped workpiece between pads", shape: workpiece },
52787
+ { name: "installed thumb screw with captive pressure pad and hand knob", shape: clampScrew }
52788
+ ],
52789
+ frame,
52790
+ workpiece,
52791
+ clampScrew,
52792
+ cutters: {
52793
+ threadedBossBore,
52794
+ workpieceEnvelope
52795
+ },
52796
+ dims: {
52797
+ screwSize,
52798
+ screwDiameter,
52799
+ threadEnvelopeDiameter,
52800
+ workpieceThickness,
52801
+ workpieceDepth,
52802
+ workpieceHeight,
52803
+ frameDepth,
52804
+ frameHeight,
52805
+ baseThickness,
52806
+ jawThickness,
52807
+ supportThickness,
52808
+ bossLength,
52809
+ bossDiameter,
52810
+ exposedScrewLength,
52811
+ pressurePadDiameter,
52812
+ pressurePadThickness,
52813
+ knobDiameter,
52814
+ knobThickness,
52815
+ screwCenterZ,
52816
+ fixedAnvilFaceX: workpieceLeftFaceX - faceClearance,
52817
+ pressurePadFaceX: workpieceRightFaceX + faceClearance,
52818
+ supportInnerFaceX,
52819
+ runningClearance,
52820
+ faceClearance
52821
+ }
52822
+ };
52823
+ }
50856
52824
  function fastenerSet(size, boltLength, options) {
50857
52825
  const sizeData = METRIC_HOLE_TABLE[size];
50858
52826
  if (!sizeData) throw new Error(`fastenerSet: unsupported size "${size}"`);
@@ -50913,6 +52881,22 @@ const partLibrary = {
50913
52881
  nut,
50914
52882
  washer,
50915
52883
  fastenerSet,
52884
+ boltedServiceCover,
52885
+ datumEnclosureAssembly,
52886
+ snapLatchCoverAssembly,
52887
+ pinnedLeverAssembly,
52888
+ retainedShaftAssembly,
52889
+ capturedLinearSlide,
52890
+ capturedCartridgeGuideAssembly,
52891
+ livingHingeCoverAssembly,
52892
+ knuckledHingeAssembly,
52893
+ clevisPinJointAssembly,
52894
+ seatedBearingAssembly,
52895
+ cableGlandAnchorAssembly,
52896
+ hoseBarbPortAssembly,
52897
+ routedTubeClipAssembly,
52898
+ pcbTerminalBlockAssembly,
52899
+ thumbScrewClampAssembly,
50916
52900
  pipeRoute,
50917
52901
  elbow,
50918
52902
  beltDrive,
@@ -51253,11 +53237,11 @@ function inverseLerp(x2, y2, value) {
51253
53237
  return 0;
51254
53238
  }
51255
53239
  }
51256
- function lerp$3(x2, y2, t) {
53240
+ function lerp$5(x2, y2, t) {
51257
53241
  return (1 - t) * x2 + t * y2;
51258
53242
  }
51259
53243
  function damp(x2, y2, lambda, dt) {
51260
- return lerp$3(x2, y2, 1 - Math.exp(-lambda * dt));
53244
+ return lerp$5(x2, y2, 1 - Math.exp(-lambda * dt));
51261
53245
  }
51262
53246
  function pingpong(x2, length4 = 1) {
51263
53247
  return length4 - Math.abs(euclideanModulo(x2, length4 * 2) - length4);
@@ -51450,7 +53434,7 @@ const MathUtils = {
51450
53434
  * @param {number} t - The interpolation factor in the closed interval `[0, 1]`.
51451
53435
  * @return {number} The interpolated value.
51452
53436
  */
51453
- lerp: lerp$3,
53437
+ lerp: lerp$5,
51454
53438
  /**
51455
53439
  * Smoothly interpolate a number from `x` to `y` in a spring-like manner using a delta
51456
53440
  * time to maintain frame rate independent movement. For details, see
@@ -60404,9 +62388,9 @@ class Color {
60404
62388
  lerpHSL(color, alpha) {
60405
62389
  this.getHSL(_hslA);
60406
62390
  color.getHSL(_hslB);
60407
- const h = lerp$3(_hslA.h, _hslB.h, alpha);
60408
- const s = lerp$3(_hslA.s, _hslB.s, alpha);
60409
- const l = lerp$3(_hslA.l, _hslB.l, alpha);
62391
+ const h = lerp$5(_hslA.h, _hslB.h, alpha);
62392
+ const s = lerp$5(_hslA.s, _hslB.s, alpha);
62393
+ const l = lerp$5(_hslA.l, _hslB.l, alpha);
60410
62394
  this.setHSL(h, s, l);
60411
62395
  return this;
60412
62396
  }
@@ -92404,7 +94388,7 @@ function requireFinite$7(value, label) {
92404
94388
  }
92405
94389
  return value;
92406
94390
  }
92407
- function requireVec3$2(value, label) {
94391
+ function requireVec3$3(value, label) {
92408
94392
  if (!Array.isArray(value) || value.length !== 3) {
92409
94393
  throw new Error(`${label} must be [x, y, z]`);
92410
94394
  }
@@ -92448,7 +94432,7 @@ function normalizeOptions(options) {
92448
94432
  out.size = requireFinite$7(options.size, "Viewport.label options.size");
92449
94433
  if (out.size <= 0) throw new Error("Viewport.label options.size must be positive");
92450
94434
  }
92451
- if (options.offset !== void 0) out.offset = requireVec3$2(options.offset, "Viewport.label options.offset");
94435
+ if (options.offset !== void 0) out.offset = requireVec3$3(options.offset, "Viewport.label options.offset");
92452
94436
  if (options.anchor !== void 0) {
92453
94437
  if (!VALID_ANCHORS.has(options.anchor)) {
92454
94438
  throw new Error(`Viewport.label options.anchor must be one of: ${Array.from(VALID_ANCHORS).join(", ")}`);
@@ -92465,7 +94449,7 @@ function collectRenderLabel(text, at, options) {
92465
94449
  if (typeof text !== "string" || text.trim().length === 0) {
92466
94450
  throw new Error("Viewport.label text must be a non-empty string");
92467
94451
  }
92468
- const normalizedAt = requireVec3$2(at, "Viewport.label at");
94452
+ const normalizedAt = requireVec3$3(at, "Viewport.label at");
92469
94453
  const normalizedOptions = normalizeOptions(options);
92470
94454
  _collected$4.push({
92471
94455
  id: `render-label-${_nextId++}`,
@@ -92884,7 +94868,7 @@ function requireFinite$6(value, label) {
92884
94868
  }
92885
94869
  return value;
92886
94870
  }
92887
- function requireVec3$1(value, label) {
94871
+ function requireVec3$2(value, label) {
92888
94872
  if (!Array.isArray(value) || value.length !== 3) {
92889
94873
  throw new Error(`${label} must be [x, y, z]`);
92890
94874
  }
@@ -92912,9 +94896,9 @@ const VALID_ENVIRONMENT_PRESETS = /* @__PURE__ */ new Set([
92912
94896
  ]);
92913
94897
  function validateCamera(cam, label) {
92914
94898
  const out = {};
92915
- if (cam.position !== void 0) out.position = requireVec3$1(cam.position, `${label}.position`);
92916
- if (cam.target !== void 0) out.target = requireVec3$1(cam.target, `${label}.target`);
92917
- if (cam.up !== void 0) out.up = requireVec3$1(cam.up, `${label}.up`);
94899
+ if (cam.position !== void 0) out.position = requireVec3$2(cam.position, `${label}.position`);
94900
+ if (cam.target !== void 0) out.target = requireVec3$2(cam.target, `${label}.target`);
94901
+ if (cam.up !== void 0) out.up = requireVec3$2(cam.up, `${label}.up`);
92918
94902
  if (cam.fov !== void 0) {
92919
94903
  out.fov = requireFinite$6(cam.fov, `${label}.fov`);
92920
94904
  if (out.fov <= 0 || out.fov >= 180) throw new Error(`${label}.fov must be between 0 and 180`);
@@ -93049,8 +95033,8 @@ function validateLight(light, label) {
93049
95033
  const out = { type: light.type };
93050
95034
  if (light.color !== void 0) out.color = requireColor(light.color, `${label}.color`);
93051
95035
  if (light.intensity !== void 0) out.intensity = requireFinite$6(light.intensity, `${label}.intensity`);
93052
- if (light.position !== void 0) out.position = requireVec3$1(light.position, `${label}.position`);
93053
- if (light.target !== void 0) out.target = requireVec3$1(light.target, `${label}.target`);
95036
+ if (light.position !== void 0) out.position = requireVec3$2(light.position, `${label}.position`);
95037
+ if (light.target !== void 0) out.target = requireVec3$2(light.target, `${label}.target`);
93054
95038
  if (light.groundColor !== void 0) out.groundColor = requireColor(light.groundColor, `${label}.groundColor`);
93055
95039
  if (light.skyColor !== void 0) out.skyColor = requireColor(light.skyColor, `${label}.skyColor`);
93056
95040
  if (light.angle !== void 0) out.angle = requireFinite$6(light.angle, `${label}.angle`);
@@ -94582,7 +96566,7 @@ function scale$1(v, s) {
94582
96566
  function dot$2(a2, b) {
94583
96567
  return a2[0] * b[0] + a2[1] * b[1] + a2[2] * b[2];
94584
96568
  }
94585
- function lerp$2(a2, b, t) {
96569
+ function lerp$4(a2, b, t) {
94586
96570
  return a2 + (b - a2) * t;
94587
96571
  }
94588
96572
  function frameMatrix$1(x2, y2, z2, p2) {
@@ -94593,7 +96577,7 @@ function axisVector(axis, sign2 = 1) {
94593
96577
  if (axis === "Y") return [0, sign2, 0];
94594
96578
  return [0, 0, sign2];
94595
96579
  }
94596
- function axisPosition(axis, point2) {
96580
+ function axisPosition$1(axis, point2) {
94597
96581
  return point2[AXIS_INDEX[axis]];
94598
96582
  }
94599
96583
  function crossPointForStation(axis, point2) {
@@ -94601,7 +96585,7 @@ function crossPointForStation(axis, point2) {
94601
96585
  if (axis === "Y") return [point2[0], -point2[2]];
94602
96586
  return [point2[1], point2[2]];
94603
96587
  }
94604
- function orientLoftToAxis(shape, axis) {
96588
+ function orientLoftToAxis$1(shape, axis) {
94605
96589
  if (axis === "Z") return shape;
94606
96590
  if (axis === "Y") return shape.rotateX(-90);
94607
96591
  return shape.transform([0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1]);
@@ -94658,9 +96642,9 @@ function interpolateQuery(a2, b, t) {
94658
96642
  }
94659
96643
  return {
94660
96644
  side: sideA,
94661
- u: lerp$2(a2.u ?? 0.5, b.u ?? 0.5, t),
94662
- v: lerp$2(a2.v ?? 0.5, b.v ?? 0.5, t),
94663
- offset: lerp$2(a2.offset ?? 0, b.offset ?? 0, t)
96645
+ u: lerp$4(a2.u ?? 0.5, b.u ?? 0.5, t),
96646
+ v: lerp$4(a2.v ?? 0.5, b.v ?? 0.5, t),
96647
+ offset: lerp$4(a2.offset ?? 0, b.offset ?? 0, t)
94664
96648
  };
94665
96649
  }
94666
96650
  function resolvePathQueries(points) {
@@ -94727,8 +96711,8 @@ class ProductSkin {
94727
96711
  this.stations = stations;
94728
96712
  this.rails = rails;
94729
96713
  for (const [name2, query] of Object.entries(refs)) this.refQueries.set(name2, cloneQuery(query));
94730
- this.axisMin = Math.min(...stations.map((station) => axisPosition(axis, station.center)));
94731
- this.axisMax = Math.max(...stations.map((station) => axisPosition(axis, station.center)));
96714
+ this.axisMin = Math.min(...stations.map((station) => axisPosition$1(axis, station.center)));
96715
+ this.axisMax = Math.max(...stations.map((station) => axisPosition$1(axis, station.center)));
94732
96716
  this.diagnosticsValue = {
94733
96717
  ...diagnostics,
94734
96718
  stationNames: stations.map((station) => station.name),
@@ -94785,24 +96769,24 @@ class ProductSkin {
94785
96769
  }
94786
96770
  /** Interpolate center, width, and depth at a normalized v or absolute axis value. */
94787
96771
  stationAt(vOrAxis) {
94788
- const axisValue = vOrAxis >= 0 && vOrAxis <= 1 ? lerp$2(this.axisMin, this.axisMax, vOrAxis) : clamp$5(vOrAxis, this.axisMin, this.axisMax);
96772
+ const axisValue = vOrAxis >= 0 && vOrAxis <= 1 ? lerp$4(this.axisMin, this.axisMax, vOrAxis) : clamp$5(vOrAxis, this.axisMin, this.axisMax);
94789
96773
  const sorted = this.stations;
94790
96774
  for (let index2 = 0; index2 < sorted.length - 1; index2 += 1) {
94791
96775
  const a2 = sorted[index2];
94792
96776
  const b = sorted[index2 + 1];
94793
- const aAxis = axisPosition(this.axis, a2.center);
94794
- const bAxis = axisPosition(this.axis, b.center);
96777
+ const aAxis = axisPosition$1(this.axis, a2.center);
96778
+ const bAxis = axisPosition$1(this.axis, b.center);
94795
96779
  if (axisValue < aAxis - EPS$5 || axisValue > bAxis + EPS$5) continue;
94796
96780
  const span = Math.max(EPS$5, bAxis - aAxis);
94797
96781
  const t = clamp$5((axisValue - aAxis) / span, 0, 1);
94798
96782
  return {
94799
96783
  axisValue,
94800
- 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)],
94801
- width: lerp$2(a2.profile.width, b.profile.width, t),
94802
- depth: lerp$2(a2.profile.depth, b.profile.depth, t),
96784
+ 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)],
96785
+ width: lerp$4(a2.profile.width, b.profile.width, t),
96786
+ depth: lerp$4(a2.profile.depth, b.profile.depth, t),
94803
96787
  dWidth: (b.profile.width - a2.profile.width) / span,
94804
96788
  dDepth: (b.profile.depth - a2.profile.depth) / span,
94805
- exponent: lerp$2(profileExponent(a2), profileExponent(b), t),
96789
+ exponent: lerp$4(profileExponent(a2), profileExponent(b), t),
94806
96790
  kind: a2.profile.kind === b.profile.kind ? a2.profile.kind : "custom"
94807
96791
  };
94808
96792
  }
@@ -94924,7 +96908,7 @@ class ProductSkinBuilder {
94924
96908
  }
94925
96909
  /** Set named cross-section stations for the product skin. */
94926
96910
  stations(stations) {
94927
- this.stationsValue = stations.map(toStationSpec).sort((a2, b) => axisPosition(this.axisValue, a2.center) - axisPosition(this.axisValue, b.center));
96911
+ this.stationsValue = stations.map(toStationSpec).sort((a2, b) => axisPosition$1(this.axisValue, a2.center) - axisPosition$1(this.axisValue, b.center));
94928
96912
  return this;
94929
96913
  }
94930
96914
  /** Attach named guide rails for product-skin construction and downstream surface references. */
@@ -94974,9 +96958,9 @@ class ProductSkinBuilder {
94974
96958
  const [x2, y2] = crossPointForStation(this.axisValue, station.center);
94975
96959
  return station.profile.sketch.translate(x2, y2);
94976
96960
  });
94977
- const heights = this.stationsValue.map((station) => axisPosition(this.axisValue, station.center));
96961
+ const heights = this.stationsValue.map((station) => axisPosition$1(this.axisValue, station.center));
94978
96962
  let shape = loft(localProfiles, heights, { edgeLength: this.edgeLengthValue });
94979
- shape = orientLoftToAxis(shape, this.axisValue);
96963
+ shape = orientLoftToAxis$1(shape, this.axisValue);
94980
96964
  if (this.colorValue) shape = shape.color(this.colorValue);
94981
96965
  shape = applyMaterial(shape, this.materialValue).as(this.name);
94982
96966
  const warnings = [];
@@ -95635,7 +97619,7 @@ function requirePositive$3(value, label) {
95635
97619
  function clamp$4(value, min2, max2) {
95636
97620
  return Math.max(min2, Math.min(max2, value));
95637
97621
  }
95638
- function lerp$1(a2, b, t) {
97622
+ function lerp$3(a2, b, t) {
95639
97623
  return a2 + (b - a2) * t;
95640
97624
  }
95641
97625
  function add(a2, b) {
@@ -95685,19 +97669,19 @@ function transformLocal(point2, tangentAcross, normal, tangentAlong, x2, y2, z2
95685
97669
  function interpolateCylinder(a2, b, t, mode) {
95686
97670
  let delta = b.angle - a2.angle;
95687
97671
  if (mode === "shortest" && Math.abs(delta) > 180) delta -= Math.sign(delta) * 360;
95688
- 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) };
97672
+ 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) };
95689
97673
  }
95690
97674
  function interpolatePlane(a2, b, t) {
95691
- 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) };
97675
+ 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) };
95692
97676
  }
95693
97677
  function interpolateProductSkin(a2, b, t) {
95694
97678
  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.");
95695
97679
  return {
95696
97680
  kind: "productSkin",
95697
97681
  side: a2.side ?? b.side,
95698
- u: lerp$1(a2.u ?? 0.5, b.u ?? 0.5, t),
95699
- v: lerp$1(a2.v ?? 0.5, b.v ?? 0.5, t),
95700
- offset: lerp$1(a2.offset ?? 0, b.offset ?? 0, t)
97682
+ u: lerp$3(a2.u ?? 0.5, b.u ?? 0.5, t),
97683
+ v: lerp$3(a2.v ?? 0.5, b.v ?? 0.5, t),
97684
+ offset: lerp$3(a2.offset ?? 0, b.offset ?? 0, t)
95701
97685
  };
95702
97686
  }
95703
97687
  class SurfacePath {
@@ -96020,11 +98004,11 @@ function coordinateOnSide(coordinate, side, label) {
96020
98004
  return { ...coordinate, kind: "productSkin", side };
96021
98005
  }
96022
98006
  class ProductSkinCarrier {
96023
- constructor(skin, name = skin.name, sideValue, offsetValue = 0) {
98007
+ constructor(skin, name = skin.name, sideValue2, offsetValue = 0) {
96024
98008
  __publicField(this, "kind", "productSkin");
96025
98009
  this.skin = skin;
96026
98010
  this.name = name;
96027
- this.sideValue = sideValue;
98011
+ this.sideValue = sideValue2;
96028
98012
  this.offsetValue = offsetValue;
96029
98013
  }
96030
98014
  surface(side) {
@@ -96795,7 +98779,7 @@ function counterboresForPlate(spec2, width, height, thickness, diagnostics) {
96795
98779
  function minWidthAcrossAlongRange(widthAtT, length4, minAlong, maxAlong) {
96796
98780
  let minWidth = Number.POSITIVE_INFINITY;
96797
98781
  for (let index2 = 0; index2 <= 8; index2 += 1) {
96798
- const along = lerp$1(minAlong, maxAlong, index2 / 8);
98782
+ const along = lerp$3(minAlong, maxAlong, index2 / 8);
96799
98783
  const t = Math.max(0, Math.min(1, (along + length4 / 2) / Math.max(length4, 1e-8)));
96800
98784
  minWidth = Math.min(minWidth, widthAtT(t));
96801
98785
  }
@@ -97095,7 +99079,7 @@ function pathParameterAtDistance(samples, distance2) {
97095
99079
  const segmentLength = Math.hypot(b.point[0] - a2.point[0], b.point[1] - a2.point[1], b.point[2] - a2.point[2]);
97096
99080
  if (traveled + segmentLength >= distance2) {
97097
99081
  const localT = segmentLength <= 1e-8 ? 0 : (distance2 - traveled) / segmentLength;
97098
- return lerp$1(a2.t, b.t, localT);
99082
+ return lerp$3(a2.t, b.t, localT);
97099
99083
  }
97100
99084
  traveled += segmentLength;
97101
99085
  }
@@ -97148,7 +99132,7 @@ function compileBandFootprintMesh(path2, input) {
97148
99132
  const width = input.widthAt(t);
97149
99133
  const along = distance2 - length4 / 2;
97150
99134
  for (let acrossIndex = 0; acrossIndex <= acrossSegments; acrossIndex += 1) {
97151
- const across = lerp$1(-width / 2, width / 2, acrossIndex / acrossSegments);
99135
+ const across = lerp$3(-width / 2, width / 2, acrossIndex / acrossSegments);
97152
99136
  mesh.vertices.push(pointAtProfile([across, along], false));
97153
99137
  }
97154
99138
  }
@@ -97158,7 +99142,7 @@ function compileBandFootprintMesh(path2, input) {
97158
99142
  const width = input.widthAt(t);
97159
99143
  const along = distance2 - length4 / 2;
97160
99144
  for (let acrossIndex = 0; acrossIndex <= acrossSegments; acrossIndex += 1) {
97161
- const across = lerp$1(-width / 2, width / 2, acrossIndex / acrossSegments);
99145
+ const across = lerp$3(-width / 2, width / 2, acrossIndex / acrossSegments);
97162
99146
  mesh.vertices.push(pointAtProfile([across, along], true));
97163
99147
  }
97164
99148
  }
@@ -97170,7 +99154,7 @@ function compileBandFootprintMesh(path2, input) {
97170
99154
  const width = input.widthAt(t);
97171
99155
  const along = distance2 - length4 / 2;
97172
99156
  for (let acrossIndex = 0; acrossIndex < acrossSegments; acrossIndex += 1) {
97173
- const across = lerp$1(-width / 2, width / 2, (acrossIndex + 0.5) / acrossSegments);
99157
+ const across = lerp$3(-width / 2, width / 2, (acrossIndex + 0.5) / acrossSegments);
97174
99158
  filled[alongIndex][acrossIndex] = !holes.some((hole2) => pointInProfileLoop([across, along], hole2));
97175
99159
  }
97176
99160
  }
@@ -99348,7 +101332,7 @@ const Constraint = {
99348
101332
  return builder.constrain({ type: "length", line: resolveLineId(builder, line2), value });
99349
101333
  }
99350
101334
  };
99351
- function requireVec3(v, label) {
101335
+ function requireVec3$1(v, label) {
99352
101336
  if (!Array.isArray(v) || v.length !== 3 || !Number.isFinite(v[0]) || !Number.isFinite(v[1]) || !Number.isFinite(v[2])) {
99353
101337
  throw new Error(`${label} must be a [number, number, number] with finite values, got ${JSON.stringify(v)}`);
99354
101338
  }
@@ -99361,24 +101345,24 @@ function requireFiniteNumber(n, label) {
99361
101345
  return n;
99362
101346
  }
99363
101347
  function distance$1(a2, b) {
99364
- requireVec3(a2, "a");
99365
- requireVec3(b, "b");
101348
+ requireVec3$1(a2, "a");
101349
+ requireVec3$1(b, "b");
99366
101350
  return Math.hypot(b[0] - a2[0], b[1] - a2[1], b[2] - a2[2]);
99367
101351
  }
99368
101352
  function midpoint$1(a2, b) {
99369
- requireVec3(a2, "a");
99370
- requireVec3(b, "b");
101353
+ requireVec3$1(a2, "a");
101354
+ requireVec3$1(b, "b");
99371
101355
  return [(a2[0] + b[0]) / 2, (a2[1] + b[1]) / 2, (a2[2] + b[2]) / 2];
99372
101356
  }
99373
- function lerp(a2, b, t) {
99374
- requireVec3(a2, "a");
99375
- requireVec3(b, "b");
101357
+ function lerp$2(a2, b, t) {
101358
+ requireVec3$1(a2, "a");
101359
+ requireVec3$1(b, "b");
99376
101360
  requireFiniteNumber(t, "t");
99377
101361
  return [a2[0] + (b[0] - a2[0]) * t, a2[1] + (b[1] - a2[1]) * t, a2[2] + (b[2] - a2[2]) * t];
99378
101362
  }
99379
101363
  function direction(a2, b) {
99380
- requireVec3(a2, "a");
99381
- requireVec3(b, "b");
101364
+ requireVec3$1(a2, "a");
101365
+ requireVec3$1(b, "b");
99382
101366
  const dx = b[0] - a2[0];
99383
101367
  const dy = b[1] - a2[1];
99384
101368
  const dz = b[2] - a2[2];
@@ -99389,8 +101373,8 @@ function direction(a2, b) {
99389
101373
  return [dx / len2, dy / len2, dz / len2];
99390
101374
  }
99391
101375
  function offset(point2, dir, amount) {
99392
- requireVec3(point2, "point");
99393
- requireVec3(dir, "dir");
101376
+ requireVec3$1(point2, "point");
101377
+ requireVec3$1(dir, "dir");
99394
101378
  requireFiniteNumber(amount, "amount");
99395
101379
  return [point2[0] + dir[0] * amount, point2[1] + dir[1] * amount, point2[2] + dir[2] * amount];
99396
101380
  }
@@ -99400,7 +101384,7 @@ const Points2 = {
99400
101384
  /** Center point between two 3D points. */
99401
101385
  midpoint: midpoint$1,
99402
101386
  /** Linearly interpolate between two 3D points. t=0 returns a, t=1 returns b. */
99403
- lerp,
101387
+ lerp: lerp$2,
99404
101388
  /** Unit direction vector from a to b. Throws if a and b are the same point. */
99405
101389
  direction,
99406
101390
  /** Move a point along a direction vector by a given amount. */
@@ -104532,9 +106516,84 @@ class ConstraintSketch extends Sketch {
104532
106516
  * Select the single arrangement region that contains the given seed point.
104533
106517
  * Throws if no region contains the seed.
104534
106518
  */
104535
- detectArrangementRegion(seed) {
106519
+ detectArrangementRegion(_seed2) {
104536
106520
  throw new Error("Not implemented");
104537
106521
  }
106522
+ /**
106523
+ * Return the solved constrained path as a sampled 2D polyline.
106524
+ *
106525
+ * Use this when a construction rail was authored with `constrainedSketch()`
106526
+ * and should feed another operation such as `Loft.pathOnXz(...)`.
106527
+ * The sketch must contain exactly one profile path.
106528
+ *
106529
+ * @param samples - Samples per curved segment. Default 32.
106530
+ * @returns The solved path as an open polyline.
106531
+ */
106532
+ toPolyline(samples = 32) {
106533
+ if (!Number.isFinite(samples) || samples < 2) throw new Error("ConstraintSketch.toPolyline() samples must be at least 2");
106534
+ const profileLoops = this.definition.loops.filter((loop) => loop.type === "profile");
106535
+ if (profileLoops.length !== 1) {
106536
+ throw new Error("ConstraintSketch.toPolyline() requires exactly one profile path");
106537
+ }
106538
+ const sampleCount = Math.max(2, Math.round(samples));
106539
+ const pointMap = new Map(this.definition.points.map((point2) => [point2.id, point2]));
106540
+ const lineMap = new Map(this.definition.lines.map((line2) => [line2.id, line2]));
106541
+ const arcMap = new Map(this.definition.arcs.map((arc) => [arc.id, arc]));
106542
+ const bezierMap = new Map(this.definition.beziers.map((bezier) => [bezier.id, bezier]));
106543
+ const points = [];
106544
+ const appendStart = (point2, label) => {
106545
+ const previous = points[points.length - 1];
106546
+ if (!previous) {
106547
+ points.push(point2);
106548
+ return;
106549
+ }
106550
+ if (Math.hypot(point2[0] - previous[0], point2[1] - previous[1]) > 1e-6) {
106551
+ throw new Error(`ConstraintSketch.toPolyline() profile path is not continuous at ${label}`);
106552
+ }
106553
+ };
106554
+ const appendPoint = (point2) => {
106555
+ const previous = points[points.length - 1];
106556
+ if (!previous || Math.hypot(point2[0] - previous[0], point2[1] - previous[1]) > 1e-9) points.push(point2);
106557
+ };
106558
+ const requirePoint = (id, label) => {
106559
+ const point2 = pointMap.get(id);
106560
+ if (!point2) throw new Error(`ConstraintSketch.toPolyline() missing ${label}`);
106561
+ return [point2.x, point2.y];
106562
+ };
106563
+ for (const segment of profileLoops[0].segments) {
106564
+ if (segment.kind === "line") {
106565
+ const line2 = lineMap.get(segment.line);
106566
+ if (!line2) throw new Error(`ConstraintSketch.toPolyline() missing line "${segment.line}"`);
106567
+ appendStart(requirePoint(line2.a, `line "${segment.line}" start point`), `line "${segment.line}"`);
106568
+ appendPoint(requirePoint(line2.b, `line "${segment.line}" end point`));
106569
+ } else if (segment.kind === "arc") {
106570
+ const arc = arcMap.get(segment.arc);
106571
+ if (!arc) throw new Error(`ConstraintSketch.toPolyline() missing arc "${segment.arc}"`);
106572
+ const center = requirePoint(arc.center, `arc "${segment.arc}" center point`);
106573
+ const start = requirePoint(arc.start, `arc "${segment.arc}" start point`);
106574
+ const end = requirePoint(arc.end, `arc "${segment.arc}" end point`);
106575
+ appendStart(start, `arc "${segment.arc}"`);
106576
+ const startAngle = Math.atan2(start[1] - center[1], start[0] - center[0]);
106577
+ const endAngle = Math.atan2(end[1] - center[1], end[0] - center[0]);
106578
+ for (const point2 of tessellateArc(center[0], center[1], arc.radius, startAngle, endAngle, arc.clockwise, sampleCount)) {
106579
+ appendPoint(point2);
106580
+ }
106581
+ } else {
106582
+ const bezier = bezierMap.get(segment.bezier);
106583
+ if (!bezier) throw new Error(`ConstraintSketch.toPolyline() missing bezier "${segment.bezier}"`);
106584
+ const p0 = requirePoint(bezier.p0, `bezier "${segment.bezier}" start point`);
106585
+ const p1 = requirePoint(bezier.p1, `bezier "${segment.bezier}" first control point`);
106586
+ const p2 = requirePoint(bezier.p2, `bezier "${segment.bezier}" second control point`);
106587
+ const p3 = requirePoint(bezier.p3, `bezier "${segment.bezier}" end point`);
106588
+ appendStart(p0, `bezier "${segment.bezier}"`);
106589
+ for (const point2 of tessellateBezier(p0[0], p0[1], p1[0], p1[1], p2[0], p2[1], p3[0], p3[1], sampleCount)) {
106590
+ appendPoint(point2);
106591
+ }
106592
+ }
106593
+ }
106594
+ if (points.length < 2) throw new Error("ConstraintSketch.toPolyline() needs at least 2 points");
106595
+ return points;
106596
+ }
104538
106597
  /**
104539
106598
  * Re-solve the sketch after changing the value of one existing constraint.
104540
106599
  *
@@ -119832,6 +121891,295 @@ function polygonVertices(sides, radius, options) {
119832
121891
  centerY: options == null ? void 0 : options.centerY
119833
121892
  });
119834
121893
  }
121894
+ const LOFT_GUIDE_EPS = 1e-8;
121895
+ function orientLoftToAxis(shape, axis) {
121896
+ if (axis === "Z") return shape;
121897
+ if (axis === "Y") return shape.rotateX(-90);
121898
+ return shape.transform([0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1]);
121899
+ }
121900
+ function buildRailEvaluators(rails, axis, start, end, railSamples) {
121901
+ const seen2 = /* @__PURE__ */ new Set();
121902
+ return rails.map((rail2) => {
121903
+ if (seen2.has(rail2.side)) throw new Error(`Loft.withGuideRails() received more than one ${rail2.side} rail`);
121904
+ seen2.add(rail2.side);
121905
+ const sampled = sampleRailPath(rail2.path, railSamples);
121906
+ if (sampled.length < 2) throw new Error("Loft guide rails require at least two points");
121907
+ const points = sampled.map((point2) => ({ position: axisPosition(axis, point2), cross: crossPointForAxis(axis, point2) }));
121908
+ const ordered = points[points.length - 1].position >= points[0].position ? points : [...points].reverse();
121909
+ validateRailCoverage(ordered, start, end);
121910
+ return { side: rail2.side, points: ordered };
121911
+ });
121912
+ }
121913
+ function railCrossAt(rail2, position) {
121914
+ const points = rail2.points;
121915
+ if (position <= points[0].position + LOFT_GUIDE_EPS) return points[0].cross;
121916
+ const last = points[points.length - 1];
121917
+ if (position >= last.position - LOFT_GUIDE_EPS) return last.cross;
121918
+ for (let index2 = 0; index2 < points.length - 1; index2 += 1) {
121919
+ const a2 = points[index2];
121920
+ const b = points[index2 + 1];
121921
+ if (position >= a2.position - LOFT_GUIDE_EPS && position <= b.position + LOFT_GUIDE_EPS) {
121922
+ const t = (position - a2.position) / (b.position - a2.position);
121923
+ return [lerp$1(a2.cross[0], b.cross[0], t), lerp$1(a2.cross[1], b.cross[1], t)];
121924
+ }
121925
+ }
121926
+ throw new Error("Loft guide rail does not cover requested station position");
121927
+ }
121928
+ function validateRailCoverage(points, start, end) {
121929
+ for (let index2 = 1; index2 < points.length; index2 += 1) {
121930
+ if (points[index2].position - points[index2 - 1].position < LOFT_GUIDE_EPS) {
121931
+ throw new Error("Loft guide rails must be monotone along the loft axis");
121932
+ }
121933
+ }
121934
+ if (points[0].position - start > LOFT_GUIDE_EPS || end - points[points.length - 1].position > LOFT_GUIDE_EPS) {
121935
+ throw new Error("Loft guide rails must cover the full station range");
121936
+ }
121937
+ }
121938
+ function sampleRailPath(path2, samples) {
121939
+ if (Array.isArray(path2)) return path2.map((point2, index2) => requireVec3(point2, `Loft guide rail point ${index2}`));
121940
+ if (path2 instanceof Curve3D || path2 instanceof HermiteCurve3D || path2 instanceof QuinticHermiteCurve3D || path2 instanceof NurbsCurve3D) {
121941
+ return path2.sample(Math.max(2, Math.round(samples))).map((point2, index2) => requireVec3(point2, `Loft guide rail sample ${index2}`));
121942
+ }
121943
+ throw new Error("Loft guide rail path must be a Vec3[] or ForgeCAD 3D curve");
121944
+ }
121945
+ function requireVec3(point2, label) {
121946
+ if (!Array.isArray(point2) || point2.length !== 3 || !point2.every(Number.isFinite)) {
121947
+ throw new Error(`${label} must be a finite [x, y, z] point`);
121948
+ }
121949
+ return [point2[0], point2[1], point2[2]];
121950
+ }
121951
+ function axisPosition(axis, point2) {
121952
+ if (axis === "X") return point2[0];
121953
+ if (axis === "Y") return point2[1];
121954
+ return point2[2];
121955
+ }
121956
+ function crossPointForAxis(axis, point2) {
121957
+ if (axis === "X") return [point2[1], point2[2]];
121958
+ if (axis === "Y") return [point2[0], -point2[2]];
121959
+ return [point2[0], point2[1]];
121960
+ }
121961
+ function lerp$1(a2, b, t) {
121962
+ return a2 + (b - a2) * t;
121963
+ }
121964
+ function loftWithGuideRails(stations, rails, options = {}) {
121965
+ if (stations.length < 2) throw new Error("Loft.withGuideRails() requires at least two stations");
121966
+ if (rails.length === 0) throw new Error("Loft.withGuideRails() requires at least one guide rail");
121967
+ const sortedStations = sortedValidStations(stations);
121968
+ const axis = options.axis ?? "Z";
121969
+ const start = sortedStations[0].position;
121970
+ const end = sortedStations[sortedStations.length - 1].position;
121971
+ const railEvaluators = buildRailEvaluators(rails, axis, start, end, options.railSamples ?? 64);
121972
+ const positions = generatedPositions(sortedStations, options.samples);
121973
+ const profiles2 = positions.map((position) => {
121974
+ const source = profileForPosition(sortedStations, position);
121975
+ const bounds = boundsForPosition(sortedStations, position);
121976
+ return fitProfileToBounds(source, applyRailsToBounds(bounds, railEvaluators, position));
121977
+ });
121978
+ const shape = loft(profiles2, positions, {
121979
+ edgeLength: options.edgeLength,
121980
+ boundsPadding: options.boundsPadding
121981
+ });
121982
+ return orientLoftToAxis(shape, axis);
121983
+ }
121984
+ function sortedValidStations(stations) {
121985
+ const sorted = [...stations].sort((a2, b) => a2.position - b.position);
121986
+ for (let index2 = 0; index2 < sorted.length; index2 += 1) {
121987
+ if (!Number.isFinite(sorted[index2].position)) throw new Error("Loft.withGuideRails station position must be finite");
121988
+ if (!(sorted[index2].profile instanceof Sketch)) throw new Error("Loft.withGuideRails() stations must use Sketch profiles");
121989
+ if (index2 > 0 && sorted[index2].position - sorted[index2 - 1].position < LOFT_GUIDE_EPS) {
121990
+ throw new Error("Loft.withGuideRails() requires unique, strictly increasing station positions");
121991
+ }
121992
+ }
121993
+ return sorted;
121994
+ }
121995
+ function generatedPositions(stations, samples) {
121996
+ const count = Math.max(2, Math.round(samples ?? Math.max(9, (stations.length - 1) * 8 + 1)));
121997
+ const start = stations[0].position;
121998
+ const end = stations[stations.length - 1].position;
121999
+ const values = /* @__PURE__ */ new Set();
122000
+ const positions = [];
122001
+ const addPosition = (position) => {
122002
+ const key = position.toFixed(9);
122003
+ if (!values.has(key)) {
122004
+ values.add(key);
122005
+ positions.push(position);
122006
+ }
122007
+ };
122008
+ for (let index2 = 0; index2 < count; index2 += 1) addPosition(start + (end - start) * index2 / (count - 1));
122009
+ for (const station of stations) addPosition(station.position);
122010
+ return positions.sort((a2, b) => a2 - b);
122011
+ }
122012
+ function profileForPosition(stations, position) {
122013
+ for (let index2 = 0; index2 < stations.length - 1; index2 += 1) {
122014
+ if (position <= stations[index2 + 1].position + LOFT_GUIDE_EPS) return stations[index2].profile;
122015
+ }
122016
+ return stations[stations.length - 1].profile;
122017
+ }
122018
+ function boundsForPosition(stations, position) {
122019
+ if (position <= stations[0].position + LOFT_GUIDE_EPS) return sketchBounds(stations[0].profile);
122020
+ const last = stations[stations.length - 1];
122021
+ if (position >= last.position - LOFT_GUIDE_EPS) return sketchBounds(last.profile);
122022
+ for (let index2 = 0; index2 < stations.length - 1; index2 += 1) {
122023
+ const a2 = stations[index2];
122024
+ const b = stations[index2 + 1];
122025
+ if (position >= a2.position - LOFT_GUIDE_EPS && position <= b.position + LOFT_GUIDE_EPS) {
122026
+ return lerpBounds(sketchBounds(a2.profile), sketchBounds(b.profile), (position - a2.position) / (b.position - a2.position));
122027
+ }
122028
+ }
122029
+ return sketchBounds(last.profile);
122030
+ }
122031
+ function applyRailsToBounds(bounds, rails, position) {
122032
+ const centerRail = rails.find((rail2) => rail2.side === "center");
122033
+ const center = centerRail ? railCrossAt(centerRail, position) : void 0;
122034
+ const next = { ...bounds };
122035
+ applyAxisRail(next, "X", sideValue(rails, "left", position, 0), sideValue(rails, "right", position, 0), center == null ? void 0 : center[0]);
122036
+ applyAxisRail(next, "Y", sideValue(rails, "back", position, 1), sideValue(rails, "front", position, 1), center == null ? void 0 : center[1]);
122037
+ if (next.maxX - next.minX < LOFT_GUIDE_EPS || next.maxY - next.minY < LOFT_GUIDE_EPS) {
122038
+ throw new Error("Loft.withGuideRails() guide rails produced a non-positive section size");
122039
+ }
122040
+ return next;
122041
+ }
122042
+ function sideValue(rails, side, position, crossIndex) {
122043
+ const rail2 = rails.find((entry) => entry.side === side);
122044
+ return rail2 ? railCrossAt(rail2, position)[crossIndex] : void 0;
122045
+ }
122046
+ function applyAxisRail(bounds, axis, minRail, maxRail, center) {
122047
+ const minKey = axis === "X" ? "minX" : "minY";
122048
+ const maxKey = axis === "X" ? "maxX" : "maxY";
122049
+ const width = bounds[maxKey] - bounds[minKey];
122050
+ if (minRail != null && maxRail != null) {
122051
+ if (maxRail - minRail < LOFT_GUIDE_EPS) throw new Error("Loft.withGuideRails() opposite guide rails crossed");
122052
+ if (center != null && Math.abs((minRail + maxRail) / 2 - center) > 1e-5) {
122053
+ throw new Error("Loft.withGuideRails() center rail conflicts with opposite side rails");
122054
+ }
122055
+ bounds[minKey] = minRail;
122056
+ bounds[maxKey] = maxRail;
122057
+ } else if (maxRail != null) {
122058
+ bounds[maxKey] = maxRail;
122059
+ bounds[minKey] = center != null ? 2 * center - maxRail : maxRail - width;
122060
+ } else if (minRail != null) {
122061
+ bounds[minKey] = minRail;
122062
+ bounds[maxKey] = center != null ? 2 * center - minRail : minRail + width;
122063
+ } else if (center != null) {
122064
+ bounds[minKey] = center - width / 2;
122065
+ bounds[maxKey] = center + width / 2;
122066
+ }
122067
+ }
122068
+ function fitProfileToBounds(profile, target) {
122069
+ const source = sketchBounds(profile);
122070
+ const sourceWidth = source.maxX - source.minX;
122071
+ const sourceDepth = source.maxY - source.minY;
122072
+ if (sourceWidth < LOFT_GUIDE_EPS || sourceDepth < LOFT_GUIDE_EPS) {
122073
+ throw new Error("Loft.withGuideRails() station profiles must have positive bounds");
122074
+ }
122075
+ const sourceCenter = [(source.minX + source.maxX) / 2, (source.minY + source.maxY) / 2];
122076
+ const targetCenter = [(target.minX + target.maxX) / 2, (target.minY + target.maxY) / 2];
122077
+ return profile.scaleAround(sourceCenter, [(target.maxX - target.minX) / sourceWidth, (target.maxY - target.minY) / sourceDepth]).translate(targetCenter[0] - sourceCenter[0], targetCenter[1] - sourceCenter[1]);
122078
+ }
122079
+ function sketchBounds(profile) {
122080
+ const bounds = profile.bounds();
122081
+ return { minX: bounds.min[0], maxX: bounds.max[0], minY: bounds.min[1], maxY: bounds.max[1] };
122082
+ }
122083
+ function lerpBounds(a2, b, t) {
122084
+ return {
122085
+ minX: lerp(a2.minX, b.minX, t),
122086
+ maxX: lerp(a2.maxX, b.maxX, t),
122087
+ minY: lerp(a2.minY, b.minY, t),
122088
+ maxY: lerp(a2.maxY, b.maxY, t)
122089
+ };
122090
+ }
122091
+ function lerp(a2, b, t) {
122092
+ return a2 + (b - a2) * t;
122093
+ }
122094
+ function mapLoftPath2D(path2, label, mapper) {
122095
+ const points = sampleLoftPath2D(path2, label);
122096
+ return points.map((point2, index2) => {
122097
+ if (!Array.isArray(point2) || point2.length !== 2 || !point2.every(Number.isFinite)) {
122098
+ throw new Error(`${label} point ${index2} must be a finite [x, y] point`);
122099
+ }
122100
+ return mapper([point2[0], point2[1]]);
122101
+ });
122102
+ }
122103
+ function sampleLoftPath2D(path2, label) {
122104
+ if (Array.isArray(path2)) {
122105
+ if (path2.length < 2) throw new Error(`${label} requires at least two [x, y] points`);
122106
+ return path2;
122107
+ }
122108
+ if (!path2 || typeof path2 !== "object" || typeof path2.toPolyline !== "function") {
122109
+ throw new Error(`${label} requires a 2D path, solved constrained path, or [x, y] point array`);
122110
+ }
122111
+ const points = path2.toPolyline();
122112
+ if (!Array.isArray(points) || points.length < 2) throw new Error(`${label} path must produce at least two [x, y] points`);
122113
+ return points;
122114
+ }
122115
+ const Loft = {
122116
+ /** Create a loft station from a 2D profile and an axis position. */
122117
+ station(profile, position) {
122118
+ if (!Number.isFinite(position)) throw new Error("Loft.station position must be finite");
122119
+ return { profile, position };
122120
+ },
122121
+ /** Create a guide rail that constrains the section-local negative-X side. */
122122
+ leftRail(path2) {
122123
+ return { side: "left", path: path2 };
122124
+ },
122125
+ /** Create a guide rail that constrains the section-local positive-X side. */
122126
+ rightRail(path2) {
122127
+ return { side: "right", path: path2 };
122128
+ },
122129
+ /** Create a guide rail that constrains the section-local positive-Y side. */
122130
+ frontRail(path2) {
122131
+ return { side: "front", path: path2 };
122132
+ },
122133
+ /** Create a guide rail that constrains the section-local negative-Y side. */
122134
+ backRail(path2) {
122135
+ return { side: "back", path: path2 };
122136
+ },
122137
+ /** Create a guide rail that moves section centers along the loft. */
122138
+ centerRail(path2) {
122139
+ return { side: "center", path: path2 };
122140
+ },
122141
+ /**
122142
+ * Place a 2D guide path onto the XZ plane.
122143
+ *
122144
+ * The path's first coordinate becomes X and its second coordinate becomes Z.
122145
+ * Use this for left/right silhouette rails authored with `path()` or `constrainedSketch()`.
122146
+ */
122147
+ pathOnXz(path2, y2 = 0) {
122148
+ if (!Number.isFinite(y2)) throw new Error("Loft.pathOnXz y must be finite");
122149
+ return mapLoftPath2D(path2, "Loft.pathOnXz", ([x2, z2]) => [x2, y2, z2]);
122150
+ },
122151
+ /**
122152
+ * Place a 2D guide path onto the YZ plane.
122153
+ *
122154
+ * The path's first coordinate becomes Y and its second coordinate becomes Z.
122155
+ * Use this for front/back crown rails authored with `path()` or `constrainedSketch()`.
122156
+ */
122157
+ pathOnYz(path2, x2 = 0) {
122158
+ if (!Number.isFinite(x2)) throw new Error("Loft.pathOnYz x must be finite");
122159
+ return mapLoftPath2D(path2, "Loft.pathOnYz", ([y2, z2]) => [x2, y2, z2]);
122160
+ },
122161
+ /**
122162
+ * Place a 2D guide path onto the XY plane.
122163
+ *
122164
+ * The path's first coordinate becomes X and its second coordinate becomes Y.
122165
+ * Use this when lofting along X or Y and a rail lives in a horizontal sketch plane.
122166
+ */
122167
+ pathOnXy(path2, z2 = 0) {
122168
+ if (!Number.isFinite(z2)) throw new Error("Loft.pathOnXy z must be finite");
122169
+ return mapLoftPath2D(path2, "Loft.pathOnXy", ([x2, y2]) => [x2, y2, z2]);
122170
+ },
122171
+ /**
122172
+ * Loft through profile stations while forcing generated sections to follow guide rails.
122173
+ *
122174
+ * Stations define the cross-section family. Guide rails define the side or center
122175
+ * paths the loft must pass through. With opposite side rails, the section is scaled
122176
+ * to touch both rails. With one side rail, the section keeps its interpolated size
122177
+ * unless a center rail is also present.
122178
+ */
122179
+ withGuideRails(stations, rails, options = {}) {
122180
+ return loftWithGuideRails(stations, rails, options);
122181
+ }
122182
+ };
119835
122183
  let collectedHighlights = [];
119836
122184
  function resetHighlights() {
119837
122185
  collectedHighlights = [];
@@ -124953,10 +127301,14 @@ function spec(name, checkFn) {
124953
127301
  };
124954
127302
  }
124955
127303
  let _collected = [];
127304
+ let _collisionAllowances = [];
127305
+ let _physicalComponentExpectations = [];
124956
127306
  let _counter = 0;
124957
127307
  let _activeGroup = null;
124958
127308
  function resetVerifications() {
124959
127309
  _collected = [];
127310
+ _collisionAllowances = [];
127311
+ _physicalComponentExpectations = [];
124960
127312
  _counter = 0;
124961
127313
  }
124962
127314
  function getCollectedVerifications() {
@@ -124990,15 +127342,35 @@ function push(result) {
124990
127342
  function roundNum(n, digits = 4) {
124991
127343
  return Number.isFinite(n) ? n.toFixed(digits).replace(/\.?0+$/, "") : String(n);
124992
127344
  }
127345
+ function meshDerivedManifoldBackend(shape) {
127346
+ const mesh = getShapeRuntimeBackend(shape).getMesh();
127347
+ return reconstructBackendFromMesh({
127348
+ numProp: mesh.numProp,
127349
+ triVerts: mesh.triVerts,
127350
+ vertProperties: mesh.vertProperties,
127351
+ mergeFromVert: mesh.mergeFromVert ?? new Uint32Array(),
127352
+ mergeToVert: mesh.mergeToVert ?? new Uint32Array()
127353
+ });
127354
+ }
127355
+ function backendForMinGap(shape) {
127356
+ const backend = getShapeRuntimeBackend(shape);
127357
+ if (isManifoldCapableBackend(backend)) return { backend, method: "exact", dispose: false };
127358
+ return { backend: meshDerivedManifoldBackend(shape), method: "mesh-derived", dispose: true };
127359
+ }
124993
127360
  function computeMinGap(a2, b, searchLength) {
124994
- const backendA = getShapeRuntimeBackend(a2);
124995
- const backendB = getShapeRuntimeBackend(b);
124996
- if (!isManifoldCapableBackend(backendA)) {
124997
- throw new Error("notColliding/minClearance require Manifold-backed shapes");
127361
+ const backendA = backendForMinGap(a2);
127362
+ const backendB = backendForMinGap(b);
127363
+ try {
127364
+ const manifoldA = requireManifoldShapeBackend(backendA.backend, "verification.minGap");
127365
+ const manifoldB = requireManifoldShapeBackend(backendB.backend, "verification.minGap");
127366
+ return {
127367
+ gap: manifoldA.minGap(manifoldB, searchLength),
127368
+ method: backendA.method === "exact" && backendB.method === "exact" ? "exact" : "mesh-derived"
127369
+ };
127370
+ } finally {
127371
+ if (backendA.dispose) disposeShapeBackend(backendA.backend);
127372
+ if (backendB.dispose) disposeShapeBackend(backendB.backend);
124998
127373
  }
124999
- const manifoldA = backendA.requireManifold("verification.minGap");
125000
- const manifoldB = requireManifoldShapeBackend(backendB, "verification.minGap");
125001
- return manifoldA.minGap(manifoldB, searchLength);
125002
127374
  }
125003
127375
  function vec3Dot(a2, b) {
125004
127376
  return a2[0] * b[0] + a2[1] * b[1] + a2[2] * b[2];
@@ -125133,8 +127505,143 @@ const verify = {
125133
127505
  actual: `${roundNum(d2, 3)} mm`
125134
127506
  });
125135
127507
  } catch (e) {
125136
- push({ id: nextId(), label, status: "fail", message: `Error: ${e instanceof Error ? e.message : String(e)}`, line: line2 });
127508
+ push({
127509
+ id: nextId(),
127510
+ label,
127511
+ kind: "interface",
127512
+ status: "fail",
127513
+ message: `Error: ${e instanceof Error ? e.message : String(e)}`,
127514
+ line: line2
127515
+ });
127516
+ }
127517
+ },
127518
+ /**
127519
+ * Check the distance between two named connectors on a shape or group.
127520
+ *
127521
+ * Use this when connectors + `matchTo()` define a static assembly interface.
127522
+ * It proves the mate at runtime, unlike a plain source-level connector
127523
+ * declaration. The common case is `expected = 0`, meaning the two connector
127524
+ * origins should coincide after placement.
127525
+ *
127526
+ * **Example**
127527
+ *
127528
+ * ```ts
127529
+ * verify.connectorDistance("leg is seated", bench, "Rail.leg_0", "Leg0.head", 0, 0.01);
127530
+ * ```
127531
+ */
127532
+ connectorDistance(label, target, connectorA, connectorB, expected = 0, tolerance = 0.01) {
127533
+ const line2 = captureSourceLine();
127534
+ try {
127535
+ const actual = target.connectorDistance(connectorA, connectorB);
127536
+ const diff = Math.abs(actual - expected);
127537
+ const passed = diff <= Math.abs(tolerance);
127538
+ push({
127539
+ id: nextId(),
127540
+ label,
127541
+ kind: "interface",
127542
+ status: passed ? "pass" : "fail",
127543
+ 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`,
127544
+ line: passed ? void 0 : line2,
127545
+ expected: `${roundNum(expected, 4)} ± ${roundNum(tolerance, 4)} mm`,
127546
+ actual: `${roundNum(actual, 4)} mm`
127547
+ });
127548
+ } catch (e) {
127549
+ push({
127550
+ id: nextId(),
127551
+ label,
127552
+ kind: "interface",
127553
+ status: "fail",
127554
+ message: `Error: ${e instanceof Error ? e.message : String(e)}`,
127555
+ line: line2
127556
+ });
127557
+ }
127558
+ },
127559
+ /**
127560
+ * Declare the expected physical connectivity component count for the returned visible model.
127561
+ *
127562
+ * **Details**
127563
+ *
127564
+ * Use this for generated mechanical models that should have a clear component graph:
127565
+ * one connected fixture, a purchased part plus a removable cartridge, a root assembly plus
127566
+ * named intentional ghosts, and so on. `forgecad inspect mechanical-integrity` resolves the returned
127567
+ * visible objects with the same physical-connectivity analysis used in the quality gate and
127568
+ * fails if the actual component count differs.
127569
+ *
127570
+ * This catches the common generated-CAD failure where a script returns a visually plausible
127571
+ * artifact but the handle, screw, washer, cover, or terminal block is actually a separate island.
127572
+ *
127573
+ * **Example**
127574
+ *
127575
+ * ```ts
127576
+ * verify.physicalComponentCount("vise is one connected installed assembly", 1);
127577
+ * ```
127578
+ */
127579
+ physicalComponentCount(label, expected) {
127580
+ const line2 = captureSourceLine();
127581
+ const id = nextId();
127582
+ if (!Number.isInteger(expected) || expected < 0) {
127583
+ push({
127584
+ id,
127585
+ label,
127586
+ kind: "interface",
127587
+ status: "fail",
127588
+ message: "Expected physical component count must be a non-negative integer",
127589
+ line: line2
127590
+ });
127591
+ return;
127592
+ }
127593
+ _physicalComponentExpectations.push({ id, label, expected, line: line2 });
127594
+ push({
127595
+ id,
127596
+ label,
127597
+ kind: "interface",
127598
+ status: "pass",
127599
+ message: `Expected ${expected} physical component(s); checked by mechanical-integrity connectivity`
127600
+ });
127601
+ },
127602
+ /**
127603
+ * Declare that two visible objects intentionally overlap because the overlap is real manufacturing intent.
127604
+ *
127605
+ * **Details**
127606
+ *
127607
+ * Use this only for overlaps that a mechanical reviewer would accept as actual matter sharing volume:
127608
+ * welded/fused regions, overmolded inserts, potted electronics, cast-in hardware, or deliberately
127609
+ * bonded laminations. This is not a shortcut for screws without holes, shafts without bores, covers
127610
+ * without pockets, or parts placed with collision as a positioning hack.
127611
+ *
127612
+ * `forgecad inspect mechanical-integrity --collisions` only honors this declaration when both shapes are
127613
+ * returned as visible objects and the exact collision report finds that same object pair. Unused or
127614
+ * non-visible declarations fail the quality gate so annotations cannot hide unrelated collisions.
127615
+ *
127616
+ * **Example**
127617
+ *
127618
+ * ```ts
127619
+ * verify.intentionalOverlap("rubber grip is overmolded on handle", rubberGrip, handleCore, "overmolded insert");
127620
+ * ```
127621
+ */
127622
+ intentionalOverlap(label, a2, b, reason) {
127623
+ const line2 = captureSourceLine();
127624
+ const id = nextId();
127625
+ const trimmedReason = String(reason ?? "").trim();
127626
+ if (trimmedReason.length === 0) {
127627
+ push({
127628
+ id,
127629
+ label,
127630
+ kind: "interface",
127631
+ status: "fail",
127632
+ message: "Intentional overlap requires a manufacturing reason",
127633
+ line: line2
127634
+ });
127635
+ return;
125137
127636
  }
127637
+ _collisionAllowances.push({ id, label, reason: trimmedReason, a: a2, b, line: line2 });
127638
+ push({
127639
+ id,
127640
+ label,
127641
+ kind: "interface",
127642
+ status: "pass",
127643
+ message: `Intentional overlap declared: ${trimmedReason}`
127644
+ });
125138
127645
  },
125139
127646
  /**
125140
127647
  * Check that two shapes do not collide (minGap > 0).
@@ -125144,19 +127651,28 @@ const verify = {
125144
127651
  notColliding(label, a2, b, searchLength = 1) {
125145
127652
  const line2 = captureSourceLine();
125146
127653
  try {
125147
- const gap = computeMinGap(a2, b, searchLength);
127654
+ const { gap, method } = computeMinGap(a2, b, searchLength);
127655
+ const methodLabel = method === "exact" ? "exact min gap" : "mesh-derived min gap";
125148
127656
  const passed = gap > 0;
125149
127657
  push({
125150
127658
  id: nextId(),
125151
127659
  label,
127660
+ kind: "interface",
125152
127661
  status: passed ? "pass" : "fail",
125153
- message: passed ? `No collision (min gap ${roundNum(gap, 3)} mm)` : `Shapes are colliding (min gap ${roundNum(gap, 3)} mm ≤ 0)`,
127662
+ message: passed ? `No collision (${methodLabel} ${roundNum(gap, 3)} mm)` : `Shapes are colliding (${methodLabel} ${roundNum(gap, 3)} mm ≤ 0)`,
125154
127663
  line: passed ? void 0 : line2,
125155
127664
  expected: "> 0 mm",
125156
127665
  actual: `${roundNum(gap, 3)} mm`
125157
127666
  });
125158
127667
  } catch (e) {
125159
- push({ id: nextId(), label, status: "fail", message: `Error: ${e instanceof Error ? e.message : String(e)}`, line: line2 });
127668
+ push({
127669
+ id: nextId(),
127670
+ label,
127671
+ kind: "interface",
127672
+ status: "fail",
127673
+ message: `Error: ${e instanceof Error ? e.message : String(e)}`,
127674
+ line: line2
127675
+ });
125160
127676
  }
125161
127677
  },
125162
127678
  /**
@@ -125165,13 +127681,15 @@ const verify = {
125165
127681
  minClearance(label, a2, b, minGap, searchLength = 10) {
125166
127682
  const line2 = captureSourceLine();
125167
127683
  try {
125168
- const gap = computeMinGap(a2, b, searchLength);
127684
+ const { gap, method } = computeMinGap(a2, b, searchLength);
127685
+ const methodLabel = method === "exact" ? "exact gap" : "mesh-derived gap";
125169
127686
  const passed = gap >= minGap;
125170
127687
  push({
125171
127688
  id: nextId(),
125172
127689
  label,
127690
+ kind: "interface",
125173
127691
  status: passed ? "pass" : "fail",
125174
- message: passed ? `Gap ${roundNum(gap, 3)} mm ≥ ${roundNum(minGap, 3)} mm` : `Gap ${roundNum(gap, 3)} mm < required ${roundNum(minGap, 3)} mm`,
127692
+ message: passed ? `${methodLabel} ${roundNum(gap, 3)} mm ≥ ${roundNum(minGap, 3)} mm` : `${methodLabel} ${roundNum(gap, 3)} mm < required ${roundNum(minGap, 3)} mm`,
125175
127693
  line: passed ? void 0 : line2,
125176
127694
  expected: `≥ ${roundNum(minGap, 3)} mm`,
125177
127695
  actual: `${roundNum(gap, 3)} mm`
@@ -125180,6 +127698,90 @@ const verify = {
125180
127698
  push({ id: nextId(), label, status: "fail", message: `Error: ${e instanceof Error ? e.message : String(e)}`, line: line2 });
125181
127699
  }
125182
127700
  },
127701
+ /**
127702
+ * Check that the clearance gap between two shapes is inside an allowed range.
127703
+ *
127704
+ * **Details**
127705
+ *
127706
+ * Use this for seated and retained interfaces where a part must be close
127707
+ * enough to be mechanically accountable, but must not collide beyond the
127708
+ * allowed minimum. It catches both failure modes that make generated CAD look
127709
+ * fake: parts floating away from their receiver, and parts intersecting their
127710
+ * receiver because the pocket, bore, or running clearance was not modeled.
127711
+ *
127712
+ * For contact, use a narrow range such as `[-0.01, 0.05]` to tolerate tiny
127713
+ * numerical noise. For a running fit, use the intended clearance band.
127714
+ *
127715
+ * Manifold-backed shapes use exact min-gap distance. Other backends use a
127716
+ * mesh-derived min-gap check and say so in the verification message; keep
127717
+ * `forgecad inspect mechanical-integrity --collisions` in the acceptance gate for
127718
+ * positive-volume interference.
127719
+ *
127720
+ * **Example**
127721
+ *
127722
+ * ```ts
127723
+ * verify.clearanceBetween("cover is seated on gasket", cover, gasket, -0.01, 0.05);
127724
+ * verify.clearanceBetween("carriage runs inside rail", carriage, rail, 0.2, 0.5);
127725
+ * ```
127726
+ */
127727
+ clearanceBetween(label, a2, b, minGap, maxGap, searchLength) {
127728
+ const line2 = captureSourceLine();
127729
+ try {
127730
+ if (!Number.isFinite(minGap) || !Number.isFinite(maxGap)) {
127731
+ push({
127732
+ id: nextId(),
127733
+ label,
127734
+ kind: "interface",
127735
+ status: "fail",
127736
+ message: "Clearance range must use finite numbers",
127737
+ line: line2
127738
+ });
127739
+ return;
127740
+ }
127741
+ if (maxGap < minGap) {
127742
+ push({
127743
+ id: nextId(),
127744
+ label,
127745
+ kind: "interface",
127746
+ status: "fail",
127747
+ message: `Clearance max ${roundNum(maxGap, 3)} mm is smaller than min ${roundNum(minGap, 3)} mm`,
127748
+ line: line2
127749
+ });
127750
+ return;
127751
+ }
127752
+ const search = searchLength ?? Math.max(10, Math.abs(maxGap) * 2 + 1);
127753
+ const { gap, method } = computeMinGap(a2, b, search);
127754
+ const methodLabel = method === "exact" ? "exact gap" : "mesh-derived gap";
127755
+ const passed = gap >= minGap && gap <= maxGap;
127756
+ let message;
127757
+ if (passed) {
127758
+ message = `${methodLabel} ${roundNum(gap, 3)} mm in [${roundNum(minGap, 3)}, ${roundNum(maxGap, 3)}] mm`;
127759
+ } else if (gap < minGap) {
127760
+ message = `${methodLabel} ${roundNum(gap, 3)} mm < allowed minimum ${roundNum(minGap, 3)} mm`;
127761
+ } else {
127762
+ message = `${methodLabel} ${roundNum(gap, 3)} mm > allowed maximum ${roundNum(maxGap, 3)} mm`;
127763
+ }
127764
+ push({
127765
+ id: nextId(),
127766
+ label,
127767
+ kind: "interface",
127768
+ status: passed ? "pass" : "fail",
127769
+ message,
127770
+ line: passed ? void 0 : line2,
127771
+ expected: `[${roundNum(minGap, 3)}, ${roundNum(maxGap, 3)}] mm`,
127772
+ actual: `${roundNum(gap, 3)} mm`
127773
+ });
127774
+ } catch (e) {
127775
+ push({
127776
+ id: nextId(),
127777
+ label,
127778
+ kind: "interface",
127779
+ status: "fail",
127780
+ message: `Error: ${e instanceof Error ? e.message : String(e)}`,
127781
+ line: line2
127782
+ });
127783
+ }
127784
+ },
125183
127785
  /**
125184
127786
  * Check that two face normals are parallel (within toleranceDeg degrees).
125185
127787
  */
@@ -338491,6 +341093,7 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
338491
341093
  nurbsSurface,
338492
341094
  spline2d,
338493
341095
  spline3d,
341096
+ Loft,
338494
341097
  loft,
338495
341098
  loftAlongSpine,
338496
341099
  sweep,
@@ -338946,6 +341549,356 @@ function runScript(code, fileName = "main.forge.js", allFiles = {}, options = {}
338946
341549
  }
338947
341550
  }
338948
341551
  }
341552
+ const DEFAULT_LEAF_SIZE = 8;
341553
+ function cloneVec3$1(value) {
341554
+ return [value[0], value[1], value[2]];
341555
+ }
341556
+ function emptyBounds() {
341557
+ return {
341558
+ min: [Infinity, Infinity, Infinity],
341559
+ max: [-Infinity, -Infinity, -Infinity]
341560
+ };
341561
+ }
341562
+ function expandBounds(bounds, entry) {
341563
+ for (let axis = 0; axis < 3; axis += 1) {
341564
+ bounds.min[axis] = Math.min(bounds.min[axis], entry.min[axis]);
341565
+ bounds.max[axis] = Math.max(bounds.max[axis], entry.max[axis]);
341566
+ }
341567
+ }
341568
+ function boundsFor(entries, indexes) {
341569
+ const bounds = emptyBounds();
341570
+ for (const index2 of indexes) expandBounds(bounds, entries[index2]);
341571
+ return bounds;
341572
+ }
341573
+ function longestAxis(bounds) {
341574
+ const sizes = [bounds.max[0] - bounds.min[0], bounds.max[1] - bounds.min[1], bounds.max[2] - bounds.min[2]];
341575
+ if (sizes[1] > sizes[0] && sizes[1] >= sizes[2]) return 1;
341576
+ if (sizes[2] > sizes[0] && sizes[2] > sizes[1]) return 2;
341577
+ return 0;
341578
+ }
341579
+ function centerOnAxis(entry, axis) {
341580
+ return (entry.min[axis] + entry.max[axis]) * 0.5;
341581
+ }
341582
+ function nodeVolume(node) {
341583
+ return Math.max(0, node.max[0] - node.min[0]) * Math.max(0, node.max[1] - node.min[1]) * Math.max(0, node.max[2] - node.min[2]);
341584
+ }
341585
+ function aabbOverlaps(a2, b, padding = 0) {
341586
+ if (padding <= 0) {
341587
+ return [0, 1, 2].every((axis) => a2.min[axis] < b.max[axis] && a2.max[axis] > b.min[axis]);
341588
+ }
341589
+ return [0, 1, 2].every((axis) => a2.min[axis] <= b.max[axis] + padding && a2.max[axis] + padding >= b.min[axis]);
341590
+ }
341591
+ function aabbGaps(a2, b) {
341592
+ return [
341593
+ Math.max(0, b.min[0] - a2.max[0], a2.min[0] - b.max[0]),
341594
+ Math.max(0, b.min[1] - a2.max[1], a2.min[1] - b.max[1]),
341595
+ Math.max(0, b.min[2] - a2.max[2], a2.min[2] - b.max[2])
341596
+ ];
341597
+ }
341598
+ function aabbInteriorOverlaps(a2, b) {
341599
+ return [0, 1, 2].every((axis) => Math.min(a2.max[axis], b.max[axis]) - Math.max(a2.min[axis], b.min[axis]) > 0);
341600
+ }
341601
+ function aabbOverlapVolume(a2, b) {
341602
+ let volume = 1;
341603
+ for (const axis of [0, 1, 2]) {
341604
+ const overlap = Math.min(a2.max[axis], b.max[axis]) - Math.max(a2.min[axis], b.min[axis]);
341605
+ if (overlap <= 0) return 0;
341606
+ volume *= overlap;
341607
+ }
341608
+ return volume;
341609
+ }
341610
+ class AabbSpatialIndex {
341611
+ constructor(entries, leafSize = DEFAULT_LEAF_SIZE) {
341612
+ __publicField(this, "root");
341613
+ __publicField(this, "nodeCount");
341614
+ this.entries = entries;
341615
+ this.leafSize = leafSize;
341616
+ let nodeCount = 0;
341617
+ const build = (indexes) => {
341618
+ nodeCount += 1;
341619
+ const bounds = boundsFor(entries, indexes);
341620
+ if (indexes.length <= this.leafSize) {
341621
+ return { min: cloneVec3$1(bounds.min), max: cloneVec3$1(bounds.max), left: null, right: null, indexes };
341622
+ }
341623
+ const axis = longestAxis(bounds);
341624
+ indexes.sort((a2, b) => centerOnAxis(entries[a2], axis) - centerOnAxis(entries[b], axis) || a2 - b);
341625
+ const mid = Math.max(1, Math.floor(indexes.length / 2));
341626
+ return {
341627
+ min: cloneVec3$1(bounds.min),
341628
+ max: cloneVec3$1(bounds.max),
341629
+ left: build(indexes.slice(0, mid)),
341630
+ right: build(indexes.slice(mid)),
341631
+ indexes: null
341632
+ };
341633
+ };
341634
+ this.root = entries.length > 0 ? build(entries.map((_2, index2) => index2)) : null;
341635
+ this.nodeCount = nodeCount;
341636
+ }
341637
+ overlapPairs(options = {}) {
341638
+ const padding = Math.max(0, options.padding ?? 0);
341639
+ const pairs = [];
341640
+ let nodePairChecks = 0;
341641
+ let leafPairChecks = 0;
341642
+ const addLeafPair = (sourceIndex, targetIndex) => {
341643
+ var _a3;
341644
+ const source = this.entries[sourceIndex];
341645
+ const target = this.entries[targetIndex];
341646
+ leafPairChecks += 1;
341647
+ if ((_a3 = options.skipPair) == null ? void 0 : _a3.call(options, source, target, sourceIndex, targetIndex)) return;
341648
+ if (!aabbOverlaps(source, target, padding)) return;
341649
+ pairs.push({ sourceIndex, targetIndex });
341650
+ };
341651
+ const between = (a2, b) => {
341652
+ nodePairChecks += 1;
341653
+ if (!aabbOverlaps(a2, b, padding)) return;
341654
+ if (a2.indexes && b.indexes) {
341655
+ for (const sourceIndex of a2.indexes) {
341656
+ for (const targetIndex of b.indexes) addLeafPair(Math.min(sourceIndex, targetIndex), Math.max(sourceIndex, targetIndex));
341657
+ }
341658
+ return;
341659
+ }
341660
+ if (!b.indexes && (a2.indexes || nodeVolume(a2) <= nodeVolume(b))) {
341661
+ between(a2, b.left);
341662
+ between(a2, b.right);
341663
+ return;
341664
+ }
341665
+ between(a2.left, b);
341666
+ between(a2.right, b);
341667
+ };
341668
+ const within = (node) => {
341669
+ if (node.indexes) {
341670
+ for (let i = 0; i < node.indexes.length; i += 1) {
341671
+ for (let j = i + 1; j < node.indexes.length; j += 1) addLeafPair(node.indexes[i], node.indexes[j]);
341672
+ }
341673
+ return;
341674
+ }
341675
+ within(node.left);
341676
+ within(node.right);
341677
+ between(node.left, node.right);
341678
+ };
341679
+ if (this.root) within(this.root);
341680
+ return {
341681
+ pairs,
341682
+ stats: {
341683
+ objectCount: this.entries.length,
341684
+ nodeCount: this.nodeCount,
341685
+ nodePairChecks,
341686
+ leafPairChecks
341687
+ }
341688
+ };
341689
+ }
341690
+ }
341691
+ const DEFAULT_COLLISION_INSPECTION_OPTIONS = {
341692
+ minOverlapVolume: 0.1
341693
+ };
341694
+ function cloneVec3(value) {
341695
+ return [value[0], value[1], value[2]];
341696
+ }
341697
+ function isIdentityTransform(matrix) {
341698
+ if (!matrix) return true;
341699
+ const identity = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
341700
+ return identity.every((value, index2) => Math.abs(matrix[index2] - value) <= 1e-12);
341701
+ }
341702
+ function transformPoint(matrix, point2) {
341703
+ const [x2, y2, z2] = point2;
341704
+ return [
341705
+ matrix[0] * x2 + matrix[4] * y2 + matrix[8] * z2 + matrix[12],
341706
+ matrix[1] * x2 + matrix[5] * y2 + matrix[9] * z2 + matrix[13],
341707
+ matrix[2] * x2 + matrix[6] * y2 + matrix[10] * z2 + matrix[14]
341708
+ ];
341709
+ }
341710
+ function transformBBox(min2, max2, matrix) {
341711
+ const corners = [
341712
+ [min2[0], min2[1], min2[2]],
341713
+ [min2[0], min2[1], max2[2]],
341714
+ [min2[0], max2[1], min2[2]],
341715
+ [min2[0], max2[1], max2[2]],
341716
+ [max2[0], min2[1], min2[2]],
341717
+ [max2[0], min2[1], max2[2]],
341718
+ [max2[0], max2[1], min2[2]],
341719
+ [max2[0], max2[1], max2[2]]
341720
+ ];
341721
+ const outMin = [Infinity, Infinity, Infinity];
341722
+ const outMax = [-Infinity, -Infinity, -Infinity];
341723
+ for (const corner of corners) {
341724
+ const transformed = transformPoint(matrix, corner);
341725
+ for (let axis = 0; axis < 3; axis += 1) {
341726
+ outMin[axis] = Math.min(outMin[axis], transformed[axis]);
341727
+ outMax[axis] = Math.max(outMax[axis], transformed[axis]);
341728
+ }
341729
+ }
341730
+ return { min: outMin, max: outMax };
341731
+ }
341732
+ function prepareEntry(entry) {
341733
+ if (isIdentityTransform(entry.transform)) {
341734
+ return {
341735
+ ...entry,
341736
+ min: cloneVec3(entry.min),
341737
+ max: cloneVec3(entry.max)
341738
+ };
341739
+ }
341740
+ const bbox = transformBBox(entry.min, entry.max, entry.transform);
341741
+ return {
341742
+ ...entry,
341743
+ shape: entry.shape.transform(entry.transform),
341744
+ min: bbox.min,
341745
+ max: bbox.max
341746
+ };
341747
+ }
341748
+ function collisionId(a2, b) {
341749
+ return `${a2.id}__${b.id}`;
341750
+ }
341751
+ function shouldSkipPair(a2, b, options) {
341752
+ if (options.skipIntraGroup && a2.groupName && a2.groupName === b.groupName) return true;
341753
+ if (options.skipMockPairs && a2.mock && b.mock) return true;
341754
+ return false;
341755
+ }
341756
+ function collectCandidatePairs(entries, options) {
341757
+ const index2 = new AabbSpatialIndex(entries, 1);
341758
+ const result = index2.overlapPairs({
341759
+ padding: options.bboxPadding,
341760
+ skipPair: (a2, b) => shouldSkipPair(a2, b, options)
341761
+ });
341762
+ const pairs = result.pairs.map((pair) => ({
341763
+ ...pair,
341764
+ bboxOverlapVolume: aabbOverlapVolume(entries[pair.sourceIndex], entries[pair.targetIndex])
341765
+ }));
341766
+ pairs.sort((a2, b) => b.bboxOverlapVolume - a2.bboxOverlapVolume || a2.sourceIndex - b.sourceIndex || a2.targetIndex - b.targetIndex);
341767
+ return { pairs, bboxPairChecks: result.stats.leafPairChecks };
341768
+ }
341769
+ function serializeCollisionFinding(finding) {
341770
+ return {
341771
+ index: finding.index,
341772
+ id: finding.id,
341773
+ sourceIndex: finding.sourceIndex,
341774
+ targetIndex: finding.targetIndex,
341775
+ sourceId: finding.sourceId,
341776
+ targetId: finding.targetId,
341777
+ sourceName: finding.sourceName,
341778
+ targetName: finding.targetName,
341779
+ overlapVolume: finding.overlapVolume
341780
+ };
341781
+ }
341782
+ function analyzeCollisionIntersections(entries, rawOptions = {}) {
341783
+ const maxCandidatePairs = rawOptions.maxCandidatePairs === void 0 ? null : Math.max(0, Math.trunc(rawOptions.maxCandidatePairs));
341784
+ const maxElapsedMs = rawOptions.maxElapsedMs === void 0 ? null : Math.max(0, rawOptions.maxElapsedMs);
341785
+ const options = {
341786
+ minOverlapVolume: rawOptions.minOverlapVolume ?? DEFAULT_COLLISION_INSPECTION_OPTIONS.minOverlapVolume,
341787
+ maxCandidatePairs,
341788
+ maxElapsedMs,
341789
+ bboxPadding: Math.max(0, rawOptions.bboxPadding ?? 0),
341790
+ skipIntraGroup: rawOptions.skipIntraGroup === true,
341791
+ skipMockPairs: rawOptions.skipMockPairs === true,
341792
+ includeBBoxCandidates: rawOptions.includeBBoxCandidates !== false
341793
+ };
341794
+ const warnings = [];
341795
+ const collisions = [];
341796
+ const preparedEntries = entries.map((entry) => prepareEntry(entry));
341797
+ const { pairs: candidatePairs, bboxPairChecks } = collectCandidatePairs(preparedEntries, options);
341798
+ const limitedCandidatePairs = options.maxCandidatePairs === null ? candidatePairs : candidatePairs.slice(0, options.maxCandidatePairs);
341799
+ const pairLimitSkippedPairCount = candidatePairs.length - limitedCandidatePairs.length;
341800
+ let testedPairCount = 0;
341801
+ let timeBudgetStopped = false;
341802
+ const exactStarted = performance.now();
341803
+ for (const pair of limitedCandidatePairs) {
341804
+ if (options.maxElapsedMs !== null && (options.maxElapsedMs <= 0 || performance.now() - exactStarted >= options.maxElapsedMs)) {
341805
+ timeBudgetStopped = true;
341806
+ break;
341807
+ }
341808
+ testedPairCount += 1;
341809
+ const a2 = preparedEntries[pair.sourceIndex];
341810
+ const b = preparedEntries[pair.targetIndex];
341811
+ try {
341812
+ const hit = a2.shape.intersect(b.shape);
341813
+ if (hit.isEmpty()) continue;
341814
+ const overlapVolume = hit.volume();
341815
+ if (!Number.isFinite(overlapVolume) || overlapVolume <= options.minOverlapVolume) continue;
341816
+ collisions.push({
341817
+ index: collisions.length + 1,
341818
+ id: collisionId(a2, b),
341819
+ sourceIndex: pair.sourceIndex,
341820
+ targetIndex: pair.targetIndex,
341821
+ sourceId: a2.id,
341822
+ targetId: b.id,
341823
+ sourceName: a2.name,
341824
+ targetName: b.name,
341825
+ overlapVolume,
341826
+ shape: hit
341827
+ });
341828
+ } catch (err2) {
341829
+ const message = err2 instanceof Error ? err2.message : String(err2);
341830
+ warnings.push(`Could not boolean-test ${a2.name} against ${b.name}: ${message}`);
341831
+ }
341832
+ }
341833
+ const exactCheckMs = performance.now() - exactStarted;
341834
+ const timeBudgetSkippedPairCount = timeBudgetStopped ? limitedCandidatePairs.length - testedPairCount : 0;
341835
+ const skippedPairCount = pairLimitSkippedPairCount + timeBudgetSkippedPairCount;
341836
+ const bboxCandidates = options.includeBBoxCandidates ? candidatePairs.map((pair, index2) => {
341837
+ const source = preparedEntries[pair.sourceIndex];
341838
+ const target = preparedEntries[pair.targetIndex];
341839
+ const exactChecked = index2 < testedPairCount;
341840
+ let skippedBy;
341841
+ if (!exactChecked) {
341842
+ skippedBy = index2 >= limitedCandidatePairs.length ? "pair-limit" : "time-budget";
341843
+ }
341844
+ return {
341845
+ index: index2 + 1,
341846
+ sourceIndex: pair.sourceIndex,
341847
+ targetIndex: pair.targetIndex,
341848
+ sourceId: source.id,
341849
+ targetId: target.id,
341850
+ sourceName: source.name,
341851
+ targetName: target.name,
341852
+ bboxOverlapVolume: pair.bboxOverlapVolume,
341853
+ exactChecked,
341854
+ ...skippedBy ? { skippedBy } : {}
341855
+ };
341856
+ }) : [];
341857
+ if (pairLimitSkippedPairCount > 0) {
341858
+ warnings.push(
341859
+ `Collision check candidate set was limited to ${limitedCandidatePairs.length}/${candidatePairs.length} bbox-overlapping object pair(s); ${pairLimitSkippedPairCount} lower-priority pair(s) were skipped by maxCandidatePairs=${options.maxCandidatePairs}. Increase the collision pair limit for exhaustive checking.`
341860
+ );
341861
+ }
341862
+ if (timeBudgetSkippedPairCount > 0) {
341863
+ warnings.push(
341864
+ `Collision check stopped after ${exactCheckMs.toFixed(0)} ms; tested ${testedPairCount}/${limitedCandidatePairs.length} selected bbox-overlapping object pair(s), skipping ${timeBudgetSkippedPairCount} by maxElapsedMs=${options.maxElapsedMs}. Increase the collision time budget for a deeper check.`
341865
+ );
341866
+ }
341867
+ const objects = preparedEntries.map((entry, index2) => ({
341868
+ index: index2,
341869
+ id: entry.id,
341870
+ name: entry.name,
341871
+ groupName: entry.groupName,
341872
+ treePath: entry.treePath,
341873
+ mock: entry.mock === true,
341874
+ bbox: {
341875
+ min: cloneVec3(entry.min),
341876
+ max: cloneVec3(entry.max)
341877
+ }
341878
+ }));
341879
+ return {
341880
+ method: "boolean-intersection",
341881
+ options,
341882
+ broadphase: {
341883
+ method: "aabb-bvh",
341884
+ allPairCount: preparedEntries.length * (preparedEntries.length - 1) / 2,
341885
+ bboxPairChecks,
341886
+ booleanPairChecks: testedPairCount
341887
+ },
341888
+ objectCount: objects.length,
341889
+ candidatePairCount: candidatePairs.length,
341890
+ testedPairCount,
341891
+ skippedPairCount,
341892
+ pairLimitSkippedPairCount,
341893
+ timeBudgetSkippedPairCount,
341894
+ exactCheckMs,
341895
+ collisionCount: collisions.length,
341896
+ bboxCandidates,
341897
+ objects,
341898
+ collisions,
341899
+ warnings
341900
+ };
341901
+ }
338949
341902
  const VECTOR_KEYS = /* @__PURE__ */ new Set(["pos", "position", "target", "lookat", "aim", "up"]);
338950
341903
  const roundNumber = (value, digits) => {
338951
341904
  const scale2 = 10 ** digits;
@@ -339212,64 +342165,72 @@ export {
339212
342165
  DEFAULT_ACTIVE_BACKEND as ay,
339213
342166
  isConstraintSketch as az,
339214
342167
  PCFShadowMap as b,
342168
+ initKernelManifoldOnly as b$,
339215
342169
  PointLight as b0,
339216
342170
  DirectionalLight as b1,
339217
- buildShapeFromCompilePlan as b2,
342171
+ analyzeCollisionIntersections as b2,
339218
342172
  shapeToGeometry as b3,
339219
- sketchToSvg as b4,
339220
- sketchToDxf as b5,
339221
- runScript as b6,
339222
- MeshPhysicalMaterial as b7,
339223
- LineSegments as b8,
339224
- getRenderStylePreset as b9,
339225
- resolveJointAnimation as bA,
339226
- resolveJointViewValues as bB,
339227
- getShapePorts as bC,
339228
- getShapeUsedPorts as bD,
339229
- DEFAULT_VIEW_CONFIG as bE,
339230
- getKernelFaceNameForTriangle as bF,
339231
- initKernel as bG,
339232
- initSolverWasm as bH,
339233
- BoxGeometry as bI,
339234
- localAabbPlaneRelation as bJ,
339235
- ShapeUtils as bK,
339236
- Group as bL,
339237
- intersectWithPlane as bM,
339238
- parseCameraCliSpec as bN,
339239
- PMREMGenerator as bO,
339240
- worldAuthorPlaneToLocal as bP,
339241
- generateCuttingLayoutPdf as bQ,
339242
- getCameraForwardVector as bR,
339243
- RENDER_STYLE_OPTIONS as bS,
339244
- initKernelManifoldOnly as bT,
339245
- __viteBrowserExternal$1 as bU,
339246
- AdditiveBlending as ba,
339247
- CatmullRomCurve3 as bb,
339248
- TubeGeometry as bc,
339249
- MeshStandardMaterial as bd,
339250
- compileSdfNode3 as be,
339251
- buildSdfRaymarchFragmentShader as bf,
339252
- SDF_RAYMARCH_PROXY_VERTEX_SHADER as bg,
339253
- Shape2 as bh,
339254
- ShapeGeometry as bi,
339255
- ShaderLib as bj,
339256
- CylinderGeometry as bk,
339257
- parseViewportCameraState as bl,
339258
- createResolvedExplodeConfig as bm,
339259
- explodeBoundsCenter as bn,
339260
- explodeMergeBounds as bo,
339261
- resolveExplodeDirective as bp,
339262
- computeExplodeMotion as bq,
339263
- getSketchWorldMatrix as br,
339264
- explodeAdd as bs,
339265
- hasExplodeOverride as bt,
339266
- resolveExplodeLocalFanDirection as bu,
339267
- explodeMul as bv,
339268
- explodeLeafFanStage as bw,
339269
- normalizeCutPlane as bx,
339270
- toClippingPlane as by,
339271
- findJointAnimationClip as bz,
342173
+ buildShapeFromCompilePlan as b4,
342174
+ sketchToSvg as b5,
342175
+ sketchToDxf as b6,
342176
+ runScript as b7,
342177
+ MeshPhysicalMaterial as b8,
342178
+ LineSegments as b9,
342179
+ findJointAnimationClip as bA,
342180
+ resolveJointAnimation as bB,
342181
+ resolveJointViewValues as bC,
342182
+ getShapePorts as bD,
342183
+ getShapeUsedPorts as bE,
342184
+ DEFAULT_VIEW_CONFIG as bF,
342185
+ getKernelFaceNameForTriangle as bG,
342186
+ initKernel as bH,
342187
+ initSolverWasm as bI,
342188
+ BoxGeometry as bJ,
342189
+ localAabbPlaneRelation as bK,
342190
+ ShapeUtils as bL,
342191
+ aabbInteriorOverlaps as bM,
342192
+ aabbOverlapVolume as bN,
342193
+ AabbSpatialIndex as bO,
342194
+ aabbGaps as bP,
342195
+ Group as bQ,
342196
+ intersectWithPlane as bR,
342197
+ parseCameraCliSpec as bS,
342198
+ PMREMGenerator as bT,
342199
+ PointsMaterial as bU,
342200
+ Points$1 as bV,
342201
+ serializeCollisionFinding as bW,
342202
+ worldAuthorPlaneToLocal as bX,
342203
+ generateCuttingLayoutPdf as bY,
342204
+ getCameraForwardVector as bZ,
342205
+ RENDER_STYLE_OPTIONS as b_,
342206
+ getRenderStylePreset as ba,
342207
+ AdditiveBlending as bb,
342208
+ CatmullRomCurve3 as bc,
342209
+ TubeGeometry as bd,
342210
+ MeshStandardMaterial as be,
342211
+ compileSdfNode3 as bf,
342212
+ buildSdfRaymarchFragmentShader as bg,
342213
+ SDF_RAYMARCH_PROXY_VERTEX_SHADER as bh,
342214
+ Shape2 as bi,
342215
+ ShapeGeometry as bj,
342216
+ ShaderLib as bk,
342217
+ CylinderGeometry as bl,
342218
+ parseViewportCameraState as bm,
342219
+ createResolvedExplodeConfig as bn,
342220
+ explodeBoundsCenter as bo,
342221
+ explodeMergeBounds as bp,
342222
+ resolveExplodeDirective as bq,
342223
+ computeExplodeMotion as br,
342224
+ getSketchWorldMatrix as bs,
342225
+ explodeAdd as bt,
342226
+ hasExplodeOverride as bu,
342227
+ resolveExplodeLocalFanDirection as bv,
342228
+ explodeMul as bw,
342229
+ explodeLeafFanStage as bx,
342230
+ normalizeCutPlane as by,
342231
+ toClippingPlane as bz,
339272
342232
  SRGBColorSpace as c,
342233
+ __viteBrowserExternal$1 as c0,
339273
342234
  Layers as d,
339274
342235
  Color as e,
339275
342236
  RGBAFormat as f,