forgecad 0.9.15 → 0.9.16

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 (70) hide show
  1. package/dist/assets/{AdminPage-CDyGUinA.js → AdminPage-CXvls4-J.js} +1 -1
  2. package/dist/assets/{BenchmarkPage-DfPMY_-d.js → BenchmarkPage-B27zk8xL.js} +1 -1
  3. package/dist/assets/{BlogPage-kF0fkdJT.js → BlogPage-CMAVvgQL.js} +1 -1
  4. package/dist/assets/{DocsPage-B954L3YN.js → DocsPage-knf4I4h7.js} +1 -1
  5. package/dist/assets/EditorApp-BHMQlJ-D.js +14686 -0
  6. package/dist/assets/{EditorApp-CuDLxKqL.css → EditorApp-BpjZgzk0.css} +148 -0
  7. package/dist/assets/{EmbedViewer-C77B-TrF.js → EmbedViewer-D7ZGlFjx.js} +2 -2
  8. package/dist/assets/{LandingPageProofDriven-Cr6fXMDj.js → LandingPageProofDriven-CnevhTE8.js} +2 -2
  9. package/dist/assets/{LegalPage-Dzklqmmg.js → LegalPage-BPTUmqeg.js} +1 -1
  10. package/dist/assets/{PricingPage-zWXkvlwl.js → PricingPage-B0D4goG_.js} +1 -1
  11. package/dist/assets/{SettingsPage-Bz0of4KQ.js → SettingsPage-CFF-UgjI.js} +1 -1
  12. package/dist/assets/{app-D3kDkggg.js → app-T0pDcSX4.js} +1184 -218
  13. package/dist/assets/cli/{render-DSY3mMQa.js → render-C5pcIISc.js} +144 -26
  14. package/dist/assets/{constructionHistoryWorker-gpDo-uH2.js → constructionHistoryWorker-Ba2Hm58b.js} +1 -0
  15. package/dist/assets/{evalWorker-CU0Ke6DP.js → evalWorker-vkx310U2.js} +1380 -2173
  16. package/dist/assets/{inspectWorker-COyp8XXA.js → inspectWorker-BuTJDVX6.js} +252 -30
  17. package/dist/assets/{targets-B9sGB5nB.js → jointPose-B_Cgedn9.js} +71 -3
  18. package/dist/assets/{manifold-DNkrUWpA.js → manifold-BWgsjmAM.js} +1 -1
  19. package/dist/assets/{manifold-C-3h2M7p.js → manifold-D6IFSkhH.js} +2 -2
  20. package/dist/assets/{manifold-BRI5prcH.js → manifold-rZexZI0G.js} +1 -1
  21. package/dist/assets/{reportWorker-CdBz5bNg.js → reportWorker-0AGij1Ru.js} +1373 -2166
  22. package/dist/assets/{scalar-sampling-budget-wJF98aY9.js → scalar-sampling-budget-J5cuzxT1.js} +1494 -2251
  23. package/dist/assets/{scanProxyWorker-B-9VbLIs.js → scanProxyWorker-Vl4Wxa1y.js} +18 -5
  24. package/dist/cli/render.html +1 -1
  25. package/dist/docs/index.html +1 -1
  26. package/dist/docs-raw/AI/usage.md +2 -0
  27. package/dist/docs-raw/CLI.md +4 -0
  28. package/dist/docs-raw/generated/assembly.md +104 -6
  29. package/dist/docs-raw/generated/concepts.md +14 -4
  30. package/dist/docs-raw/generated/lib.md +2 -18
  31. package/dist/docs-raw/generated/output.md +14 -4
  32. package/dist/docs-raw/generated/runtime-names.md +27 -19
  33. package/dist/docs-raw/skills/forgecad-make-a-model.md +39 -38
  34. package/dist/docs-raw/skills/forgecad-project.md +2 -0
  35. package/dist/docs-raw/welcome.md +2 -0
  36. package/dist/index.html +1 -1
  37. package/dist/sitemap.xml +13 -13
  38. package/dist-cli/{check-compiler-SDX5QIXI.js → check-compiler-SYQ2PWOB.js} +1 -1
  39. package/dist-cli/{check-query-propagation-EAYEFT77.js → check-query-propagation-HIAGV62W.js} +1 -1
  40. package/dist-cli/{chunk-N4O47JLF.js → chunk-SPZE3DUY.js} +1591 -2356
  41. package/dist-cli/forgecad.js +1698 -487
  42. package/dist-skill/CONTEXT.md +117 -46
  43. package/dist-skill/docs/CLI.md +4 -0
  44. package/dist-skill/docs/generated/assembly.md +83 -5
  45. package/dist-skill/docs/generated/lib.md +2 -18
  46. package/dist-skill/docs/generated/output.md +14 -4
  47. package/dist-skill/docs/generated/runtime-names.md +18 -19
  48. package/dist-skill/library/forgecad-make-a-model/SKILL.md +39 -38
  49. package/dist-skill/library/forgecad-project/SKILL.md +2 -0
  50. package/examples/api/helix-basics.forge.js +2 -2
  51. package/examples/api/route3d-elbow.forge.js +3 -0
  52. package/examples/api/variable-sweep-test.forge.js +3 -1
  53. package/package.json +4 -1
  54. package/dist/assets/EditorApp-Beb-IZ0y.js +0 -14014
  55. package/examples/api/bolted-service-cover.forge.js +0 -17
  56. package/examples/api/cable-gland-anchor.forge.js +0 -14
  57. package/examples/api/captured-cartridge-guide.forge.js +0 -14
  58. package/examples/api/captured-linear-slide.forge.js +0 -13
  59. package/examples/api/clevis-pin-joint.forge.js +0 -13
  60. package/examples/api/datum-enclosure.forge.js +0 -16
  61. package/examples/api/hose-barb-port.forge.js +0 -14
  62. package/examples/api/knuckled-hinge-assembly.forge.js +0 -15
  63. package/examples/api/living-hinge-cover.forge.js +0 -14
  64. package/examples/api/pcb-terminal-block.forge.js +0 -22
  65. package/examples/api/pinned-lever-pivot-stack.forge.js +0 -14
  66. package/examples/api/retained-shaft-knob-stack.forge.js +0 -15
  67. package/examples/api/routed-tube-clip.forge.js +0 -15
  68. package/examples/api/seated-bearing-stack.forge.js +0 -30
  69. package/examples/api/snap-latch-cover.forge.js +0 -14
  70. package/examples/api/thumb-screw-clamp.forge.js +0 -15
@@ -1381,7 +1381,7 @@ function requireFiniteVec2(value, label) {
1381
1381
  }
1382
1382
  return [requireFiniteNumber$1(value[0], `${label}[0]`), requireFiniteNumber$1(value[1], `${label}[1]`)];
1383
1383
  }
1384
- function requireFiniteVec3$2(value, label) {
1384
+ function requireFiniteVec3$4(value, label) {
1385
1385
  if (!Array.isArray(value) || value.length !== 3) {
1386
1386
  throw new Error(`${label} must be [x, y, z] with finite numbers, got ${formatValidationValue(value)}`);
1387
1387
  }
@@ -1397,7 +1397,7 @@ function requireNonZeroFiniteVec2(value, label) {
1397
1397
  return v;
1398
1398
  }
1399
1399
  function requireNonZeroFiniteVec3(value, label) {
1400
- const v = requireFiniteVec3$2(value, label);
1400
+ const v = requireFiniteVec3$4(value, label);
1401
1401
  if (v[0] === 0 && v[1] === 0 && v[2] === 0) throw new Error(`${label} must not be [0, 0, 0]`);
1402
1402
  return v;
1403
1403
  }
@@ -1415,7 +1415,7 @@ function requireNonZeroFiniteScale2(value, label) {
1415
1415
  return scale2;
1416
1416
  }
1417
1417
  function requireNonZeroFiniteScale3(value, label) {
1418
- const scale2 = typeof value === "number" ? [requireFiniteNumber$1(value, label), requireFiniteNumber$1(value, label), requireFiniteNumber$1(value, label)] : requireFiniteVec3$2(value, label);
1418
+ const scale2 = typeof value === "number" ? [requireFiniteNumber$1(value, label), requireFiniteNumber$1(value, label), requireFiniteNumber$1(value, label)] : requireFiniteVec3$4(value, label);
1419
1419
  if (Math.abs(scale2[0]) < 1e-12 || Math.abs(scale2[1]) < 1e-12 || Math.abs(scale2[2]) < 1e-12) {
1420
1420
  throw new Error(`${label} must have finite non-zero components, got ${formatValidationValue(value)}`);
1421
1421
  }
@@ -1610,7 +1610,7 @@ class Transform {
1610
1610
  static rotationAxis(axis, angleDeg, pivot = [0, 0, 0]) {
1611
1611
  const [ux, uy, uz] = normalizeVec3$6(requireNonZeroFiniteVec3(axis, "Transform.rotationAxis() axis"));
1612
1612
  const degrees2 = requireFiniteNumber$1(angleDeg, "Transform.rotationAxis() angleDeg");
1613
- const [px, py, pz] = requireFiniteVec3$2(pivot, "Transform.rotationAxis() pivot");
1613
+ const [px, py, pz] = requireFiniteVec3$4(pivot, "Transform.rotationAxis() pivot");
1614
1614
  const rad = degrees2 * Math.PI / 180;
1615
1615
  const cos2 = Math.cos(rad);
1616
1616
  const sin2 = Math.sin(rad);
@@ -1631,9 +1631,9 @@ class Transform {
1631
1631
  /** Solve the rotation needed to move one point onto a target line or plane. */
1632
1632
  static rotateAroundTo(axis, pivot, movingPoint, targetPoint, options = {}) {
1633
1633
  const rotateAxis = requireNonZeroFiniteVec3(axis, "Transform.rotateAroundTo() axis");
1634
- const rotatePivot = requireFiniteVec3$2(pivot, "Transform.rotateAroundTo() pivot");
1635
- const moving = requireFiniteVec3$2(movingPoint, "Transform.rotateAroundTo() movingPoint");
1636
- const target = requireFiniteVec3$2(targetPoint, "Transform.rotateAroundTo() targetPoint");
1634
+ const rotatePivot = requireFiniteVec3$4(pivot, "Transform.rotateAroundTo() pivot");
1635
+ const moving = requireFiniteVec3$4(movingPoint, "Transform.rotateAroundTo() movingPoint");
1636
+ const target = requireFiniteVec3$4(targetPoint, "Transform.rotateAroundTo() targetPoint");
1637
1637
  const angleDeg = solveRotateAroundAngle(rotateAxis, rotatePivot, moving, target, options);
1638
1638
  return Transform.rotationAxis(rotateAxis, angleDeg, rotatePivot);
1639
1639
  }
@@ -1663,6 +1663,7 @@ class Transform {
1663
1663
  return this.rotateAxis([0, 0, 1], angleDeg, pivot);
1664
1664
  }
1665
1665
  /** Scale after the current transform. */
1666
+ // biome-ignore lint/suspicious/useAdjacentOverloadSignatures: Static Transform.scale() and chainable instance scale() intentionally share the CAD API name.
1666
1667
  scale(v) {
1667
1668
  return this.mul(Transform.scale(v));
1668
1669
  }
@@ -1672,11 +1673,11 @@ class Transform {
1672
1673
  }
1673
1674
  /** Transform a point using homogeneous coordinates. */
1674
1675
  point(p2) {
1675
- return transformPoint$1(this.m, requireFiniteVec3$2(p2, "Transform.point() point"), 1);
1676
+ return transformPoint$1(this.m, requireFiniteVec3$4(p2, "Transform.point() point"), 1);
1676
1677
  }
1677
1678
  /** Transform a direction vector without translation. */
1678
1679
  vector(v) {
1679
- return transformPoint$1(this.m, requireFiniteVec3$2(v, "Transform.vector() vector"), 0);
1680
+ return transformPoint$1(this.m, requireFiniteVec3$4(v, "Transform.vector() vector"), 0);
1680
1681
  }
1681
1682
  /** Return the transform as a raw 4x4 matrix array. */
1682
1683
  toArray() {
@@ -3421,7 +3422,7 @@ const EPSILON$4 = 1e-9;
3421
3422
  function add$7(a2, b) {
3422
3423
  return [a2[0] + b[0], a2[1] + b[1], a2[2] + b[2]];
3423
3424
  }
3424
- function scale$7(v, factor) {
3425
+ function scale$8(v, factor) {
3425
3426
  return [v[0] * factor, v[1] * factor, v[2] * factor];
3426
3427
  }
3427
3428
  function sub$9(a2, b) {
@@ -3436,9 +3437,9 @@ function cross$a(a2, b) {
3436
3437
  function rotateAroundAxis(v, axis, angleRad) {
3437
3438
  const c2 = Math.cos(angleRad);
3438
3439
  const s = Math.sin(angleRad);
3439
- const term1 = scale$7(v, c2);
3440
- const term2 = scale$7(cross$a(axis, v), s);
3441
- const term3 = scale$7(axis, dot$9(axis, v) * (1 - c2));
3440
+ const term1 = scale$8(v, c2);
3441
+ const term2 = scale$8(cross$a(axis, v), s);
3442
+ const term3 = scale$8(axis, dot$9(axis, v) * (1 - c2));
3442
3443
  return add$7(add$7(term1, term2), term3);
3443
3444
  }
3444
3445
  function arcPointAt(segment, t) {
@@ -3466,7 +3467,7 @@ function routePointAt(plan, t) {
3466
3467
  const next = station + segment.length;
3467
3468
  if (target <= next || segment === plan.segments[plan.segments.length - 1]) {
3468
3469
  const localT = segment.length <= EPSILON$4 ? 1 : Math.max(0, Math.min(1, (target - station) / segment.length));
3469
- if (segment.kind === "line") return add$7(segment.from, scale$7(sub$9(segment.to, segment.from), localT));
3470
+ if (segment.kind === "line") return add$7(segment.from, scale$8(sub$9(segment.to, segment.from), localT));
3470
3471
  return arcPointAt(segment, localT);
3471
3472
  }
3472
3473
  station = next;
@@ -3486,7 +3487,7 @@ function sampleRoute3DCompilePlan(plan, options) {
3486
3487
  pushUnique(points, start);
3487
3488
  for (let i = 1; i <= intervals; i += 1) {
3488
3489
  if (segment.kind === "line") {
3489
- pushUnique(points, add$7(segment.from, scale$7(sub$9(segment.to, segment.from), i / intervals)));
3490
+ pushUnique(points, add$7(segment.from, scale$8(sub$9(segment.to, segment.from), i / intervals)));
3490
3491
  } else {
3491
3492
  pushUnique(points, arcPointAt(segment, i / intervals));
3492
3493
  }
@@ -6519,11 +6520,11 @@ function distSq$1(a2, b) {
6519
6520
  const dz = a2[2] - b[2];
6520
6521
  return dx * dx + dy * dy + dz * dz;
6521
6522
  }
6522
- function vecLength$3(v) {
6523
+ function vecLength$4(v) {
6523
6524
  return Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
6524
6525
  }
6525
6526
  function normalize$6(v) {
6526
- const len2 = vecLength$3(v);
6527
+ const len2 = vecLength$4(v);
6527
6528
  if (len2 < 1e-12) return [0, 0, 0];
6528
6529
  return [v[0] / len2, v[1] / len2, v[2] / len2];
6529
6530
  }
@@ -7253,14 +7254,14 @@ function resolveSdfMeshingSettings(tree, bounds, options = {}) {
7253
7254
  const quality = options.quality ?? "preview";
7254
7255
  const minEdgeLength = positiveOrDefault(options.minEdgeLength, DEFAULT_MIN_EDGE_LENGTH);
7255
7256
  const maxGridPoints = positiveOrDefault(options.maxGridPoints, DEFAULT_MAX_GRID_POINTS);
7256
- const tolerance = options.tolerance !== void 0 ? requirePositiveFinite$3(options.tolerance, "SDF tolerance") : void 0;
7257
- const minFeatureSize = options.minFeatureSize !== void 0 ? requirePositiveFinite$3(options.minFeatureSize, "SDF minFeatureSize") : void 0;
7258
- const maxTriangles = options.maxTriangles !== void 0 ? Math.floor(requirePositiveFinite$3(options.maxTriangles, "SDF maxTriangles")) : void 0;
7257
+ const tolerance = options.tolerance !== void 0 ? requirePositiveFinite$4(options.tolerance, "SDF tolerance") : void 0;
7258
+ const minFeatureSize = options.minFeatureSize !== void 0 ? requirePositiveFinite$4(options.minFeatureSize, "SDF minFeatureSize") : void 0;
7259
+ const maxTriangles = options.maxTriangles !== void 0 ? Math.floor(requirePositiveFinite$4(options.maxTriangles, "SDF maxTriangles")) : void 0;
7259
7260
  const analysis = analyzeSdfTree(tree);
7260
7261
  const warnings = [];
7261
7262
  let edgeLength2;
7262
7263
  if (options.edgeLength !== void 0) {
7263
- edgeLength2 = requirePositiveFinite$3(options.edgeLength, "SDF edgeLength");
7264
+ edgeLength2 = requirePositiveFinite$4(options.edgeLength, "SDF edgeLength");
7264
7265
  if (edgeLength2 < minEdgeLength) {
7265
7266
  warnings.push(`edgeLength ${formatMm(edgeLength2)} was clamped to minimum ${formatMm(minEdgeLength)}.`);
7266
7267
  edgeLength2 = minEdgeLength;
@@ -7353,8 +7354,8 @@ function resolveDefaultEdgeLength(bounds, quality, minEdgeLength, analysis, opti
7353
7354
  const maxDim = Math.max(dx, dy, dz, minEdgeLength);
7354
7355
  const divisor = quality === "draft" ? 60 : quality === "export" ? 160 : 100;
7355
7356
  const candidates = [maxDim / divisor];
7356
- if (options.tolerance !== void 0) candidates.push(requirePositiveFinite$3(options.tolerance, "SDF tolerance") * 2);
7357
- if (options.minFeatureSize !== void 0) candidates.push(requirePositiveFinite$3(options.minFeatureSize, "SDF minFeatureSize") / 2.5);
7357
+ if (options.tolerance !== void 0) candidates.push(requirePositiveFinite$4(options.tolerance, "SDF tolerance") * 2);
7358
+ if (options.minFeatureSize !== void 0) candidates.push(requirePositiveFinite$4(options.minFeatureSize, "SDF minFeatureSize") / 2.5);
7358
7359
  if (analysis.minMetricTpmsThickness < Infinity) candidates.push(analysis.minMetricTpmsThickness / 2);
7359
7360
  if (analysis.minTpmsCellSize < Infinity) candidates.push(analysis.minTpmsCellSize / 10);
7360
7361
  if (analysis.minRepeatSpacing < Infinity) candidates.push(analysis.minRepeatSpacing / 8);
@@ -7494,9 +7495,9 @@ function visitSdfNode(node, analysis) {
7494
7495
  }
7495
7496
  function positiveOrDefault(value, fallback) {
7496
7497
  if (value === void 0) return fallback;
7497
- return requirePositiveFinite$3(value, "SDF meshing option");
7498
+ return requirePositiveFinite$4(value, "SDF meshing option");
7498
7499
  }
7499
- function requirePositiveFinite$3(value, name) {
7500
+ function requirePositiveFinite$4(value, name) {
7500
7501
  if (!Number.isFinite(value) || value <= 0) {
7501
7502
  throw new Error(`${name} must be a positive finite number.`);
7502
7503
  }
@@ -9480,7 +9481,7 @@ function midpoint$3(a2, b) {
9480
9481
  function add$6(a2, b) {
9481
9482
  return [a2[0] + b[0], a2[1] + b[1], a2[2] + b[2]];
9482
9483
  }
9483
- function scale$6(v, s) {
9484
+ function scale$7(v, s) {
9484
9485
  return [v[0] * s, v[1] * s, v[2] * s];
9485
9486
  }
9486
9487
  function sub$8(a2, b) {
@@ -9509,7 +9510,7 @@ function average$1(points) {
9509
9510
  for (const point2 of points) {
9510
9511
  acc = add$6(acc, point2);
9511
9512
  }
9512
- return scale$6(acc, 1 / points.length);
9513
+ return scale$7(acc, 1 / points.length);
9513
9514
  }
9514
9515
  function buildSurfaceSheetTopology(boundaries, options = {}) {
9515
9516
  var _a3, _b3, _c2, _d2;
@@ -9655,7 +9656,7 @@ function edgeCurveFaceName(curve) {
9655
9656
  function dotVec3$4(a2, b) {
9656
9657
  return a2[0] * b[0] + a2[1] * b[1] + a2[2] * b[2];
9657
9658
  }
9658
- function vecLength$2(v) {
9659
+ function vecLength$3(v) {
9659
9660
  return Math.hypot(v[0], v[1], v[2]);
9660
9661
  }
9661
9662
  function sameScalar$2(a2, b) {
@@ -9665,9 +9666,9 @@ function uniformDistanceScale(matrix) {
9665
9666
  const xAxis = [matrix[0], matrix[1], matrix[2]];
9666
9667
  const yAxis = [matrix[4], matrix[5], matrix[6]];
9667
9668
  const zAxis = [matrix[8], matrix[9], matrix[10]];
9668
- const sx = vecLength$2(xAxis);
9669
- const sy = vecLength$2(yAxis);
9670
- const sz = vecLength$2(zAxis);
9669
+ const sx = vecLength$3(xAxis);
9670
+ const sy = vecLength$3(yAxis);
9671
+ const sz = vecLength$3(zAxis);
9671
9672
  if (sx <= 1e-9 || sy <= 1e-9 || sz <= 1e-9) return null;
9672
9673
  if (!sameScalar$2(sx, sy) || !sameScalar$2(sx, sz)) return null;
9673
9674
  const orthoTolerance = 1e-9 * Math.max(1, sx * sx);
@@ -9678,7 +9679,7 @@ function uniformDistanceScale(matrix) {
9678
9679
  }
9679
9680
  function transformSurfaceAxis(tx, axis) {
9680
9681
  const transformed = tx.vector(axis);
9681
- const length4 = vecLength$2(transformed);
9682
+ const length4 = vecLength$3(transformed);
9682
9683
  return length4 > 1e-9 ? [transformed[0] / length4, transformed[1] / length4, transformed[2] / length4] : null;
9683
9684
  }
9684
9685
  function transformFaceSurface(surface, tx) {
@@ -10916,7 +10917,7 @@ function rotateVector$1(v, axis, cosAngle, sinAngle) {
10916
10917
  v[2] * cosAngle + axisCross[2] * sinAngle + axis[2] * axisDot * (1 - cosAngle)
10917
10918
  ];
10918
10919
  }
10919
- function orthonormalizeFrame(xCandidate, tangent, fallbackUp) {
10920
+ function orthonormalizeFrame$1(xCandidate, tangent, fallbackUp) {
10920
10921
  let x2 = vec3Sub$1(xCandidate, vec3Scale(tangent, vec3Dot$2(xCandidate, tangent)));
10921
10922
  if (vec3Len$2(x2) < 1e-8) {
10922
10923
  return makeSweepFrame(tangent, fallbackUp);
@@ -10964,7 +10965,7 @@ function computeParallelTransportFramesFromSamples(samples, preferredUp) {
10964
10965
  xCandidate = previous.x;
10965
10966
  yCandidate = previous.y;
10966
10967
  }
10967
- ({ x: x2, y: y2 } = orthonormalizeFrame(xCandidate, tangent, yCandidate));
10968
+ ({ x: x2, y: y2 } = orthonormalizeFrame$1(xCandidate, tangent, yCandidate));
10968
10969
  frames.push({ origin: samples[index2].origin, x: x2, y: y2, t: tangent });
10969
10970
  }
10970
10971
  return frames;
@@ -11027,7 +11028,7 @@ function interpolateSweepFrame(segment, alpha, origin) {
11027
11028
  const tangent = vec3Len$2(tangentCandidate) > 1e-8 ? vec3Norm$1(tangentCandidate) : segment.t;
11028
11029
  const xCandidate = vec3Lerp(segment.frameA.x, segment.frameB.x, alpha);
11029
11030
  const fallbackUp = vec3Lerp(segment.frameA.y, segment.frameB.y, alpha);
11030
- const { x: x2, y: y2 } = orthonormalizeFrame(xCandidate, tangent, fallbackUp);
11031
+ const { x: x2, y: y2 } = orthonormalizeFrame$1(xCandidate, tangent, fallbackUp);
11031
11032
  return { origin, x: x2, y: y2, t: tangent };
11032
11033
  }
11033
11034
  function findNearestSweepPoint(point2, segments) {
@@ -11168,7 +11169,7 @@ function buildCatmullRomSweepPathRuntime(path2, preferredUp, edgeLength2) {
11168
11169
  const tangent2 = vec3Norm$1(catmullRom3DTangentAtNormalizedT(path2.controlPoints, path2.closed, path2.tension, wrapped));
11169
11170
  const xCandidate2 = vec3Lerp(frames[base2].x, frames[next].x, alpha2);
11170
11171
  const fallbackUp2 = vec3Lerp(frames[base2].y, frames[next].y, alpha2);
11171
- const { x: x22, y: y22 } = orthonormalizeFrame(xCandidate2, tangent2, fallbackUp2);
11172
+ const { x: x22, y: y22 } = orthonormalizeFrame$1(xCandidate2, tangent2, fallbackUp2);
11172
11173
  return { origin, x: x22, y: y22, t: tangent2 };
11173
11174
  }
11174
11175
  const clamped = clamp$8(tParam, 0, 1);
@@ -11178,7 +11179,7 @@ function buildCatmullRomSweepPathRuntime(path2, preferredUp, edgeLength2) {
11178
11179
  const tangent = vec3Norm$1(catmullRom3DTangentAtNormalizedT(path2.controlPoints, path2.closed, path2.tension, clamped));
11179
11180
  const xCandidate = vec3Lerp(frames[base].x, frames[base + 1].x, alpha);
11180
11181
  const fallbackUp = vec3Lerp(frames[base].y, frames[base + 1].y, alpha);
11181
- const { x: x2, y: y2 } = orthonormalizeFrame(xCandidate, tangent, fallbackUp);
11182
+ const { x: x2, y: y2 } = orthonormalizeFrame$1(xCandidate, tangent, fallbackUp);
11182
11183
  return { origin, x: x2, y: y2, t: tangent };
11183
11184
  }
11184
11185
  function evaluatePoint(tParam) {
@@ -11723,7 +11724,7 @@ let _wasm$1 = null;
11723
11724
  async function initManifoldWasm() {
11724
11725
  if (_wasm$1) return _wasm$1;
11725
11726
  performance.mark("manifold:start");
11726
- const Module = (await import("./manifold-DNkrUWpA.js")).default;
11727
+ const Module = (await import("./manifold-BWgsjmAM.js")).default;
11727
11728
  performance.mark("manifold:imported");
11728
11729
  const wasm = await Module();
11729
11730
  wasm.setup();
@@ -11976,7 +11977,7 @@ function computeParallelTransportFrames(path2, preferredUp) {
11976
11977
  const vLen = length$3(v);
11977
11978
  const c2 = dot$8(prevT, nextT);
11978
11979
  if (vLen > 1e-10) {
11979
- const axis = scale$5(v, 1 / vLen);
11980
+ const axis = scale$6(v, 1 / vLen);
11980
11981
  x2 = rotateVector(frames[i - 1].x, axis, c2, vLen);
11981
11982
  y2 = rotateVector(frames[i - 1].y, axis, c2, vLen);
11982
11983
  } else {
@@ -12054,7 +12055,7 @@ function sub$7(a2, b) {
12054
12055
  function add$5(a2, b) {
12055
12056
  return [a2[0] + b[0], a2[1] + b[1], a2[2] + b[2]];
12056
12057
  }
12057
- function scale$5(v, s) {
12058
+ function scale$6(v, s) {
12058
12059
  return [v[0] * s, v[1] * s, v[2] * s];
12059
12060
  }
12060
12061
  function dot$8(a2, b) {
@@ -25221,7 +25222,7 @@ function add$4(a2, b) {
25221
25222
  function sub$6(a2, b) {
25222
25223
  return [a2[0] - b[0], a2[1] - b[1], a2[2] - b[2]];
25223
25224
  }
25224
- function scale$4(v, scalar) {
25225
+ function scale$5(v, scalar) {
25225
25226
  return [v[0] * scalar, v[1] * scalar, v[2] * scalar];
25226
25227
  }
25227
25228
  function cross$6(a2, b) {
@@ -25470,7 +25471,7 @@ function createBoundedHalfSpace(bounds, normal, originOffset) {
25470
25471
  const n = [normal[0] / normalLength, normal[1] / normalLength, normal[2] / normalLength];
25471
25472
  const signedOffset = originOffset / normalLength;
25472
25473
  const corners2 = boundsCorners(bounds);
25473
- const planeOrigin = scale$4(n, signedOffset);
25474
+ const planeOrigin = scale$5(n, signedOffset);
25474
25475
  const { uAxis, vAxis } = perpendicularAxes(n);
25475
25476
  const diagonal = Math.hypot(bounds.max[0] - bounds.min[0], bounds.max[1] - bounds.min[1], bounds.max[2] - bounds.min[2]);
25476
25477
  const margin = Math.max(diagonal * 0.05, 1e-3);
@@ -25495,7 +25496,7 @@ function createBoundedHalfSpace(bounds, normal, originOffset) {
25495
25496
  const height = Math.max(maxDistance + margin, margin);
25496
25497
  const centerU = (minU + maxU) / 2;
25497
25498
  const centerV = (minV + maxV) / 2;
25498
- const translation = add$4(add$4(planeOrigin, scale$4(uAxis, centerU)), scale$4(vAxis, centerV));
25499
+ const translation = add$4(add$4(planeOrigin, scale$5(uAxis, centerU)), scale$5(vAxis, centerV));
25499
25500
  const matrix = [
25500
25501
  uAxis[0],
25501
25502
  uAxis[1],
@@ -34683,7 +34684,7 @@ function placementReferenceNames(refs, kind) {
34683
34684
  (entryKind) => Object.keys(refs[entryKind]).sort().map((name) => `${entryKind}.${name}`)
34684
34685
  );
34685
34686
  }
34686
- function requireFiniteVec3$1(v, label) {
34687
+ function requireFiniteVec3$3(v, label) {
34687
34688
  const [x2, y2, z2] = v;
34688
34689
  if (!Number.isFinite(x2) || !Number.isFinite(y2) || !Number.isFinite(z2)) {
34689
34690
  throw new Error(`${label} must contain finite numbers, got [${x2}, ${y2}, ${z2}]`);
@@ -34723,8 +34724,8 @@ function normalizePortInput(input) {
34723
34724
  const hasStartEnd = input.start != null && input.end != null;
34724
34725
  const hasOriginAxis = input.origin != null && input.axis != null;
34725
34726
  if (hasStartEnd) {
34726
- const start = requireFiniteVec3$1(input.start, "port start");
34727
- const end = requireFiniteVec3$1(input.end, "port end");
34727
+ const start = requireFiniteVec3$3(input.start, "port start");
34728
+ const end = requireFiniteVec3$3(input.end, "port end");
34728
34729
  origin = [(start[0] + end[0]) / 2, (start[1] + end[1]) / 2, (start[2] + end[2]) / 2];
34729
34730
  const dir = [end[0] - start[0], end[1] - start[1], end[2] - start[2]];
34730
34731
  const dirLen = len3$2(dir);
@@ -34733,11 +34734,11 @@ function normalizePortInput(input) {
34733
34734
  }
34734
34735
  axis = normalize3$4(dir);
34735
34736
  extent = dirLen / 2;
34736
- if (input.origin != null) origin = requireFiniteVec3$1(input.origin, "port origin");
34737
- if (input.axis != null) axis = normalize3$4(requireFiniteVec3$1(input.axis, "port axis"));
34737
+ if (input.origin != null) origin = requireFiniteVec3$3(input.origin, "port origin");
34738
+ if (input.axis != null) axis = normalize3$4(requireFiniteVec3$3(input.axis, "port axis"));
34738
34739
  } else if (hasOriginAxis) {
34739
- origin = requireFiniteVec3$1(input.origin, "port origin");
34740
- const rawAxis = requireFiniteVec3$1(input.axis, "port axis");
34740
+ origin = requireFiniteVec3$3(input.origin, "port origin");
34741
+ const rawAxis = requireFiniteVec3$3(input.axis, "port axis");
34741
34742
  if (len3$2(rawAxis) < 1e-10) {
34742
34743
  throw new Error("Port axis must be non-zero");
34743
34744
  }
@@ -34746,14 +34747,14 @@ function normalizePortInput(input) {
34746
34747
  extent = input.extent;
34747
34748
  }
34748
34749
  } else if (input.origin != null) {
34749
- origin = requireFiniteVec3$1(input.origin, "port origin");
34750
+ origin = requireFiniteVec3$3(input.origin, "port origin");
34750
34751
  axis = [0, 0, 1];
34751
34752
  } else {
34752
34753
  throw new Error("Port requires either { origin, axis } or { start, end }");
34753
34754
  }
34754
34755
  let up;
34755
34756
  if (input.up != null) {
34756
- const rawUp = requireFiniteVec3$1(input.up, "port up");
34757
+ const rawUp = requireFiniteVec3$3(input.up, "port up");
34757
34758
  if (len3$2(rawUp) < 1e-10) {
34758
34759
  throw new Error("Port up vector must be non-zero");
34759
34760
  }
@@ -36176,7 +36177,7 @@ class ShapeGroup {
36176
36177
  return { min: bb.min, max: bb.max };
36177
36178
  }
36178
36179
  resolveRotatePoint(point2) {
36179
- if (Array.isArray(point2)) return requireFiniteVec3$2(point2, "ShapeGroup.rotateAroundTo() point");
36180
+ if (Array.isArray(point2)) return requireFiniteVec3$4(point2, "ShapeGroup.rotateAroundTo() point");
36180
36181
  const bb = this._bbox();
36181
36182
  return resolveAnchor3D(bb.min, bb.max, point2);
36182
36183
  }
@@ -36229,7 +36230,7 @@ class ShapeGroup {
36229
36230
  const sp = resolveAnchor3D(sbb.min, sbb.max, selfAnchor);
36230
36231
  let dx = tp[0] - sp[0], dy = tp[1] - sp[1], dz = tp[2] - sp[2];
36231
36232
  if (offset2) {
36232
- const offsetPoint = requireFiniteVec3$2(offset2, "ShapeGroup.attachTo() offset");
36233
+ const offsetPoint = requireFiniteVec3$4(offset2, "ShapeGroup.attachTo() offset");
36233
36234
  dx += offsetPoint[0];
36234
36235
  dy += offsetPoint[1];
36235
36236
  dz += offsetPoint[2];
@@ -36284,7 +36285,7 @@ class ShapeGroup {
36284
36285
  rotateAroundAxis(axis, angleDeg, pivot = [0, 0, 0]) {
36285
36286
  const rotateAxis = requireNonZeroFiniteVec3(axis, "ShapeGroup.rotateAroundAxis() axis");
36286
36287
  const degrees2 = requireFiniteNumber$1(angleDeg, "ShapeGroup.rotateAroundAxis() angleDeg");
36287
- const rotatePivot = requireFiniteVec3$2(pivot, "ShapeGroup.rotateAroundAxis() pivot");
36288
+ const rotatePivot = requireFiniteVec3$4(pivot, "ShapeGroup.rotateAroundAxis() pivot");
36288
36289
  return this.transform(Transform.rotationAxis(rotateAxis, degrees2, rotatePivot));
36289
36290
  }
36290
36291
  /**
@@ -36293,7 +36294,7 @@ class ShapeGroup {
36293
36294
  */
36294
36295
  rotateAroundTo(axis, pivot, movingPoint, targetPoint, options = {}) {
36295
36296
  const rotateAxis = requireNonZeroFiniteVec3(axis, "ShapeGroup.rotateAroundTo() axis");
36296
- const rotatePivot = requireFiniteVec3$2(pivot, "ShapeGroup.rotateAroundTo() pivot");
36297
+ const rotatePivot = requireFiniteVec3$4(pivot, "ShapeGroup.rotateAroundTo() pivot");
36297
36298
  return this.transform(
36298
36299
  Transform.rotateAroundTo(
36299
36300
  rotateAxis,
@@ -36344,7 +36345,7 @@ class ShapeGroup {
36344
36345
  /** Scale uniformly or per-axis from an explicit pivot point. */
36345
36346
  scaleAround(pivot, v) {
36346
36347
  const scale2 = requireNonZeroFiniteScale3(v, "ShapeGroup.scaleAround() scale");
36347
- const scalePivot = requireFiniteVec3$2(pivot, "ShapeGroup.scaleAround() pivot");
36348
+ const scalePivot = requireFiniteVec3$4(pivot, "ShapeGroup.scaleAround() pivot");
36348
36349
  const matrix = Transform.scale(scale2).toArray();
36349
36350
  if (scalePivot[0] === 0 && scalePivot[1] === 0 && scalePivot[2] === 0) {
36350
36351
  return this.mapChildrenTransform((c2) => {
@@ -36367,7 +36368,7 @@ class ShapeGroup {
36367
36368
  }
36368
36369
  /** Mirror across a plane through an explicit point. */
36369
36370
  mirrorThrough(point2, normal) {
36370
- const mirrorPoint = requireFiniteVec3$2(point2, "ShapeGroup.mirrorThrough() point");
36371
+ const mirrorPoint = requireFiniteVec3$4(point2, "ShapeGroup.mirrorThrough() point");
36371
36372
  const mirrorNormal = requireNonZeroFiniteVec3(normal, "ShapeGroup.mirrorThrough() normal");
36372
36373
  const matrix = mirrorPlaneMatrix(mirrorNormal);
36373
36374
  if (mirrorPoint[0] === 0 && mirrorPoint[1] === 0 && mirrorPoint[2] === 0) {
@@ -36455,8 +36456,8 @@ class ShapeGroup {
36455
36456
  * ```
36456
36457
  */
36457
36458
  placeReference(ref, target, offset2) {
36458
- const targetPoint = requireFiniteVec3$2(target, "ShapeGroup.placeReference() target");
36459
- const offsetPoint = offset2 === void 0 ? void 0 : requireFiniteVec3$2(offset2, "ShapeGroup.placeReference() offset");
36459
+ const targetPoint = requireFiniteVec3$4(target, "ShapeGroup.placeReference() target");
36460
+ const offsetPoint = offset2 === void 0 ? void 0 : requireFiniteVec3$4(offset2, "ShapeGroup.placeReference() offset");
36460
36461
  const sourcePoint = this.referencePoint(ref);
36461
36462
  let dx = targetPoint[0] - sourcePoint[0];
36462
36463
  let dy = targetPoint[1] - sourcePoint[1];
@@ -39013,7 +39014,7 @@ function buildSdfFunctionDefinition(source, options) {
39013
39014
  jsExpression: expression,
39014
39015
  ...shader.ok ? { shaderExpression: shader.expression } : { shaderUnsupportedReason: shader.reason },
39015
39016
  raymarchStepLimit: resolveRaymarchStepLimit(options.bounds, options.maxStep),
39016
- ...options.lipschitz !== void 0 ? { raymarchLipschitz: requirePositiveFinite$2(options.lipschitz, "sdf.fromFunction() lipschitz") } : {}
39017
+ ...options.lipschitz !== void 0 ? { raymarchLipschitz: requirePositiveFinite$3(options.lipschitz, "sdf.fromFunction() lipschitz") } : {}
39017
39018
  };
39018
39019
  }
39019
39020
  function extractSdfExpression(source) {
@@ -39181,7 +39182,7 @@ function formatNumericLiteralsForGlsl(source) {
39181
39182
  return result;
39182
39183
  }
39183
39184
  function resolveRaymarchStepLimit(bounds, maxStep) {
39184
- if (maxStep !== void 0) return requirePositiveFinite$2(maxStep, "sdf.fromFunction() maxStep");
39185
+ if (maxStep !== void 0) return requirePositiveFinite$3(maxStep, "sdf.fromFunction() maxStep");
39185
39186
  const dx = bounds.max[0] - bounds.min[0];
39186
39187
  const dy = bounds.max[1] - bounds.min[1];
39187
39188
  const dz = bounds.max[2] - bounds.min[2];
@@ -39189,7 +39190,7 @@ function resolveRaymarchStepLimit(bounds, maxStep) {
39189
39190
  if (!Number.isFinite(diagonal) || diagonal <= 0) return 0.1;
39190
39191
  return Math.max(0.025, Math.min(0.5, diagonal / 240));
39191
39192
  }
39192
- function requirePositiveFinite$2(value, label) {
39193
+ function requirePositiveFinite$3(value, label) {
39193
39194
  if (!Number.isFinite(value) || value <= 0) throw new Error(`${label} must be a positive finite number.`);
39194
39195
  return value;
39195
39196
  }
@@ -39302,7 +39303,7 @@ class Pattern2DBuilder {
39302
39303
  return new Pattern2DImpl({
39303
39304
  kind: "surfacePattern:sineWave",
39304
39305
  direction: normalizeDirection$1(options.direction ?? [1, 0], "sdf.pattern2d().sineWave() direction"),
39305
- wavelength: requirePositiveFinite$1(options.wavelength, "sdf.pattern2d().sineWave() wavelength"),
39306
+ wavelength: requirePositiveFinite$2(options.wavelength, "sdf.pattern2d().sineWave() wavelength"),
39306
39307
  amplitude: requireFinite$a(options.amplitude ?? 1, "sdf.pattern2d().sineWave() amplitude"),
39307
39308
  phase: requireFinite$a(options.phase ?? 0, "sdf.pattern2d().sineWave() phase"),
39308
39309
  bias: requireFinite$a(options.bias ?? 0, "sdf.pattern2d().sineWave() bias")
@@ -39313,19 +39314,19 @@ class Pattern2DBuilder {
39313
39314
  return new Pattern2DImpl({
39314
39315
  kind: "surfacePattern:stripes",
39315
39316
  direction: normalizeDirection$1(options.direction ?? [1, 0], "sdf.pattern2d().stripes() direction"),
39316
- spacing: requirePositiveFinite$1(options.spacing, "sdf.pattern2d().stripes() spacing"),
39317
- width: requirePositiveFinite$1(options.width, "sdf.pattern2d().stripes() width"),
39318
- depth: requireNonNegativeFinite$1(options.depth ?? 1, "sdf.pattern2d().stripes() depth")
39317
+ spacing: requirePositiveFinite$2(options.spacing, "sdf.pattern2d().stripes() spacing"),
39318
+ width: requirePositiveFinite$2(options.width, "sdf.pattern2d().stripes() width"),
39319
+ depth: requireNonNegativeFinite$2(options.depth ?? 1, "sdf.pattern2d().stripes() depth")
39319
39320
  });
39320
39321
  }
39321
39322
  /** Create an over-under woven relief pattern in UV space. */
39322
39323
  overUnderWeave(options) {
39323
39324
  return new Pattern2DImpl({
39324
39325
  kind: "surfacePattern:overUnderWeave",
39325
- spacing: normalizeVec2(options.spacing, "sdf.pattern2d().overUnderWeave() spacing", requirePositiveFinite$1),
39326
- threadWidth: normalizeVec2(options.threadWidth, "sdf.pattern2d().overUnderWeave() threadWidth", requirePositiveFinite$1),
39327
- depth: requireNonNegativeFinite$1(options.depth ?? 0.8, "sdf.pattern2d().overUnderWeave() depth"),
39328
- underScale: requireNonNegativeFinite$1(options.underScale ?? 0.15, "sdf.pattern2d().overUnderWeave() underScale")
39326
+ spacing: normalizeVec2(options.spacing, "sdf.pattern2d().overUnderWeave() spacing", requirePositiveFinite$2),
39327
+ threadWidth: normalizeVec2(options.threadWidth, "sdf.pattern2d().overUnderWeave() threadWidth", requirePositiveFinite$2),
39328
+ depth: requireNonNegativeFinite$2(options.depth ?? 0.8, "sdf.pattern2d().overUnderWeave() depth"),
39329
+ underScale: requireNonNegativeFinite$2(options.underScale ?? 0.15, "sdf.pattern2d().overUnderWeave() underScale")
39329
39330
  });
39330
39331
  }
39331
39332
  }
@@ -39348,13 +39349,13 @@ function requireFinite$a(value, label) {
39348
39349
  }
39349
39350
  return value;
39350
39351
  }
39351
- function requirePositiveFinite$1(value, label) {
39352
+ function requirePositiveFinite$2(value, label) {
39352
39353
  if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
39353
39354
  throw new Error(`${label} must be a positive finite number. Received: ${String(value)}`);
39354
39355
  }
39355
39356
  return value;
39356
39357
  }
39357
- function requireNonNegativeFinite$1(value, label) {
39358
+ function requireNonNegativeFinite$2(value, label) {
39358
39359
  if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
39359
39360
  throw new Error(`${label} must be a non-negative finite number. Received: ${String(value)}`);
39360
39361
  }
@@ -39472,7 +39473,7 @@ Fix: use a known preset or pass material props directly.`
39472
39473
  material: validateMaterialProps(material)
39473
39474
  };
39474
39475
  }
39475
- function requirePositiveFinite(value, label) {
39476
+ function requirePositiveFinite$1(value, label) {
39476
39477
  if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
39477
39478
  throw new Error(`${label} must be a positive finite number. Received: ${String(value)}`);
39478
39479
  }
@@ -39484,15 +39485,15 @@ function requirePositiveInteger(value, label) {
39484
39485
  }
39485
39486
  return value;
39486
39487
  }
39487
- function requireNonNegativeFinite(value, label) {
39488
+ function requireNonNegativeFinite$1(value, label) {
39488
39489
  if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
39489
39490
  throw new Error(`${label} must be a non-negative finite number. Received: ${String(value)}`);
39490
39491
  }
39491
39492
  return value;
39492
39493
  }
39493
39494
  function resolveBlendRadius(input, label, fallback = 4) {
39494
- if (typeof input === "number") return requirePositiveFinite(input, `${label} radius`);
39495
- if ((input == null ? void 0 : input.radius) !== void 0) return requirePositiveFinite(input.radius, `${label} radius`);
39495
+ if (typeof input === "number") return requirePositiveFinite$1(input, `${label} radius`);
39496
+ if ((input == null ? void 0 : input.radius) !== void 0) return requirePositiveFinite$1(input.radius, `${label} radius`);
39496
39497
  return fallback;
39497
39498
  }
39498
39499
  function formatExpressionNumber(value) {
@@ -39502,7 +39503,7 @@ function formatExpressionNumber(value) {
39502
39503
  }
39503
39504
  function roundedBoxNode(halfExtents, radius) {
39504
39505
  const minHalf = Math.min(halfExtents[0], halfExtents[1], halfExtents[2]);
39505
- const r = Math.min(requirePositiveFinite(radius, "SdfShape.round() radius"), Math.max(1e-3, minHalf - 1e-3));
39506
+ const r = Math.min(requirePositiveFinite$1(radius, "SdfShape.round() radius"), Math.max(1e-3, minHalf - 1e-3));
39506
39507
  const inner = [halfExtents[0] - r, halfExtents[1] - r, halfExtents[2] - r];
39507
39508
  const [ix, iy, iz] = inner.map(formatExpressionNumber);
39508
39509
  const rr = formatExpressionNumber(r);
@@ -39745,9 +39746,9 @@ class SdfShape {
39745
39746
  clipBox(x2, y2, z2) {
39746
39747
  return this.intersect(
39747
39748
  box$1(
39748
- requirePositiveFinite(x2, "SdfShape.clipBox() x"),
39749
- requirePositiveFinite(y2, "SdfShape.clipBox() y"),
39750
- requirePositiveFinite(z2, "SdfShape.clipBox() z")
39749
+ requirePositiveFinite$1(x2, "SdfShape.clipBox() x"),
39750
+ requirePositiveFinite$1(y2, "SdfShape.clipBox() y"),
39751
+ requirePositiveFinite$1(z2, "SdfShape.clipBox() z")
39751
39752
  )
39752
39753
  );
39753
39754
  }
@@ -39779,7 +39780,7 @@ class SdfShape {
39779
39780
  return this.withNode({
39780
39781
  kind: "sdf:smoothUnion",
39781
39782
  children: [this._node, other._node],
39782
- radius: requirePositiveFinite(radius, "SdfShape.smoothUnion() radius")
39783
+ radius: requirePositiveFinite$1(radius, "SdfShape.smoothUnion() radius")
39783
39784
  });
39784
39785
  }
39785
39786
  /** Smooth difference — smoothly carves other from this. */
@@ -39787,7 +39788,7 @@ class SdfShape {
39787
39788
  return this.withNode({
39788
39789
  kind: "sdf:smoothDifference",
39789
39790
  children: [this._node, other._node],
39790
- radius: requirePositiveFinite(radius, "SdfShape.smoothSubtract() radius")
39791
+ radius: requirePositiveFinite$1(radius, "SdfShape.smoothSubtract() radius")
39791
39792
  });
39792
39793
  }
39793
39794
  /** Smooth intersection — smoothly intersects. */
@@ -39795,7 +39796,7 @@ class SdfShape {
39795
39796
  return this.withNode({
39796
39797
  kind: "sdf:smoothIntersection",
39797
39798
  children: [this._node, other._node],
39798
- radius: requirePositiveFinite(radius, "SdfShape.smoothIntersect() radius")
39799
+ radius: requirePositiveFinite$1(radius, "SdfShape.smoothIntersect() radius")
39799
39800
  });
39800
39801
  }
39801
39802
  /** Morph between this shape and another. t=0 → this, t=1 → other. */
@@ -39853,7 +39854,7 @@ class SdfShape {
39853
39854
  }
39854
39855
  /** Uniformly scale this SDF around the origin. */
39855
39856
  scale(factor) {
39856
- return this.withNode({ kind: "sdf:scale", child: this._node, factor: requirePositiveFinite(factor, "SdfShape.scale() factor") });
39857
+ return this.withNode({ kind: "sdf:scale", child: this._node, factor: requirePositiveFinite$1(factor, "SdfShape.scale() factor") });
39857
39858
  }
39858
39859
  // ── Domain operations ──
39859
39860
  /** Twist around the Z axis. */
@@ -39866,15 +39867,15 @@ class SdfShape {
39866
39867
  }
39867
39868
  /** Bend around the Z axis with given radius. */
39868
39869
  bend(radius) {
39869
- return this.withNode({ kind: "sdf:bend", child: this._node, radius: requirePositiveFinite(radius, "SdfShape.bend() radius") });
39870
+ return this.withNode({ kind: "sdf:bend", child: this._node, radius: requirePositiveFinite$1(radius, "SdfShape.bend() radius") });
39870
39871
  }
39871
39872
  /** Repeat in space. Spacing of 0 on an axis means no repetition. Count of 0 = infinite. */
39872
39873
  repeat(spacing, count) {
39873
39874
  return this.withNode({
39874
39875
  kind: "sdf:repeat",
39875
39876
  child: this._node,
39876
- spacing: requireFiniteVec3$2(spacing, "SdfShape.repeat() spacing"),
39877
- count: count === void 0 ? [0, 0, 0] : requireFiniteVec3$2(count, "SdfShape.repeat() count")
39877
+ spacing: requireFiniteVec3$4(spacing, "SdfShape.repeat() spacing"),
39878
+ count: count === void 0 ? [0, 0, 0] : requireFiniteVec3$4(count, "SdfShape.repeat() count")
39878
39879
  });
39879
39880
  }
39880
39881
  /**
@@ -39889,7 +39890,7 @@ class SdfShape {
39889
39890
  kind: "sdf:circularArray",
39890
39891
  child: this._node,
39891
39892
  count: requirePositiveInteger(count, "SdfShape.circularArray() count"),
39892
- offset: requireNonNegativeFinite(offset2, "SdfShape.circularArray() offset")
39893
+ offset: requireNonNegativeFinite$1(offset2, "SdfShape.circularArray() offset")
39893
39894
  });
39894
39895
  }
39895
39896
  /** Hollow out, keeping only a shell of given thickness. */
@@ -39897,7 +39898,7 @@ class SdfShape {
39897
39898
  return this.withNode({
39898
39899
  kind: "sdf:shell",
39899
39900
  child: this._node,
39900
- thickness: requirePositiveFinite(thickness, "SdfShape.shell() thickness")
39901
+ thickness: requirePositiveFinite$1(thickness, "SdfShape.shell() thickness")
39901
39902
  });
39902
39903
  }
39903
39904
  /**
@@ -39977,70 +39978,70 @@ class SdfShape {
39977
39978
  kind: "sdf:onion",
39978
39979
  child: this._node,
39979
39980
  layers: requirePositiveInteger(layers, "SdfShape.onion() layers"),
39980
- thickness: requirePositiveFinite(thickness, "SdfShape.onion() thickness")
39981
+ thickness: requirePositiveFinite$1(thickness, "SdfShape.onion() thickness")
39981
39982
  });
39982
39983
  }
39983
39984
  }
39984
39985
  function sphere$1(radius) {
39985
- return new SdfShape({ kind: "sdf:sphere", radius: requirePositiveFinite(radius, "sdf.sphere() radius") });
39986
+ return new SdfShape({ kind: "sdf:sphere", radius: requirePositiveFinite$1(radius, "sdf.sphere() radius") });
39986
39987
  }
39987
39988
  function box$1(x2, y2, z2) {
39988
39989
  return new SdfShape({
39989
39990
  kind: "sdf:box",
39990
39991
  halfExtents: [
39991
- requirePositiveFinite(x2, "sdf.box() x") / 2,
39992
- requirePositiveFinite(y2, "sdf.box() y") / 2,
39993
- requirePositiveFinite(z2, "sdf.box() z") / 2
39992
+ requirePositiveFinite$1(x2, "sdf.box() x") / 2,
39993
+ requirePositiveFinite$1(y2, "sdf.box() y") / 2,
39994
+ requirePositiveFinite$1(z2, "sdf.box() z") / 2
39994
39995
  ]
39995
39996
  });
39996
39997
  }
39997
39998
  function cylinder$1(height, radius) {
39998
39999
  return new SdfShape({
39999
40000
  kind: "sdf:cylinder",
40000
- height: requirePositiveFinite(height, "sdf.cylinder() height"),
40001
- radius: requirePositiveFinite(radius, "sdf.cylinder() radius")
40001
+ height: requirePositiveFinite$1(height, "sdf.cylinder() height"),
40002
+ radius: requirePositiveFinite$1(radius, "sdf.cylinder() radius")
40002
40003
  });
40003
40004
  }
40004
40005
  function torus$1(majorRadius, minorRadius) {
40005
40006
  return new SdfShape({
40006
40007
  kind: "sdf:torus",
40007
- majorRadius: requirePositiveFinite(majorRadius, "sdf.torus() majorRadius"),
40008
- minorRadius: requirePositiveFinite(minorRadius, "sdf.torus() minorRadius")
40008
+ majorRadius: requirePositiveFinite$1(majorRadius, "sdf.torus() majorRadius"),
40009
+ minorRadius: requirePositiveFinite$1(minorRadius, "sdf.torus() minorRadius")
40009
40010
  });
40010
40011
  }
40011
40012
  function capsule(height, radius) {
40012
40013
  return new SdfShape({
40013
40014
  kind: "sdf:capsule",
40014
- height: requirePositiveFinite(height, "sdf.capsule() height"),
40015
- radius: requirePositiveFinite(radius, "sdf.capsule() radius")
40015
+ height: requirePositiveFinite$1(height, "sdf.capsule() height"),
40016
+ radius: requirePositiveFinite$1(radius, "sdf.capsule() radius")
40016
40017
  });
40017
40018
  }
40018
40019
  function cone(height, radius) {
40019
40020
  return new SdfShape({
40020
40021
  kind: "sdf:cone",
40021
- height: requirePositiveFinite(height, "sdf.cone() height"),
40022
- radius: requirePositiveFinite(radius, "sdf.cone() radius")
40022
+ height: requirePositiveFinite$1(height, "sdf.cone() height"),
40023
+ radius: requirePositiveFinite$1(radius, "sdf.cone() radius")
40023
40024
  });
40024
40025
  }
40025
40026
  function smoothUnion(a2, b, options) {
40026
40027
  return new SdfShape({
40027
40028
  kind: "sdf:smoothUnion",
40028
40029
  children: [a2._node, b._node],
40029
- radius: requirePositiveFinite(options.radius, "sdf.smoothUnion() radius")
40030
+ radius: requirePositiveFinite$1(options.radius, "sdf.smoothUnion() radius")
40030
40031
  });
40031
40032
  }
40032
40033
  function smoothDifference(a2, b, options) {
40033
40034
  return new SdfShape({
40034
40035
  kind: "sdf:smoothDifference",
40035
40036
  children: [a2._node, b._node],
40036
- radius: requirePositiveFinite(options.radius, "sdf.smoothDifference() radius")
40037
+ radius: requirePositiveFinite$1(options.radius, "sdf.smoothDifference() radius")
40037
40038
  });
40038
40039
  }
40039
40040
  function smoothIntersection(a2, b, options) {
40040
40041
  return new SdfShape({
40041
40042
  kind: "sdf:smoothIntersection",
40042
40043
  children: [a2._node, b._node],
40043
- radius: requirePositiveFinite(options.radius, "sdf.smoothIntersection() radius")
40044
+ radius: requirePositiveFinite$1(options.radius, "sdf.smoothIntersection() radius")
40044
40045
  });
40045
40046
  }
40046
40047
  function morph(a2, b, t) {
@@ -40223,9 +40224,9 @@ function weave(options) {
40223
40224
  });
40224
40225
  }
40225
40226
  function basketWeave(options) {
40226
- const SP = requirePositiveFinite((options == null ? void 0 : options.spacing) ?? 3, "sdf.basketWeave() spacing");
40227
- const TW = requirePositiveFinite((options == null ? void 0 : options.threadWidth) ?? 1.5, "sdf.basketWeave() threadWidth");
40228
- const D2 = requireNonNegativeFinite((options == null ? void 0 : options.depth) ?? 0.8, "sdf.basketWeave() depth");
40227
+ const SP = requirePositiveFinite$1((options == null ? void 0 : options.spacing) ?? 3, "sdf.basketWeave() spacing");
40228
+ const TW = requirePositiveFinite$1((options == null ? void 0 : options.threadWidth) ?? 1.5, "sdf.basketWeave() threadWidth");
40229
+ const D2 = requireNonNegativeFinite$1((options == null ? void 0 : options.depth) ?? 0.8, "sdf.basketWeave() depth");
40229
40230
  return pattern2d().overUnderWeave({ spacing: SP, threadWidth: TW, depth: D2 });
40230
40231
  }
40231
40232
  function fromFunction(fn, options) {
@@ -40254,18 +40255,18 @@ function twist(shape, degreesPerUnit) {
40254
40255
  return shape.twist(requireFiniteNumber$1(degreesPerUnit, "sdf.twist() degreesPerUnit"));
40255
40256
  }
40256
40257
  function bend(shape, radius) {
40257
- return shape.bend(requirePositiveFinite(radius, "sdf.bend() radius"));
40258
+ return shape.bend(requirePositiveFinite$1(radius, "sdf.bend() radius"));
40258
40259
  }
40259
40260
  function repeat(shape, spacing, count) {
40260
40261
  return shape.repeat(
40261
- requireFiniteVec3$2(spacing, "sdf.repeat() spacing"),
40262
- count === void 0 ? void 0 : requireFiniteVec3$2(count, "sdf.repeat() count")
40262
+ requireFiniteVec3$4(spacing, "sdf.repeat() spacing"),
40263
+ count === void 0 ? void 0 : requireFiniteVec3$4(count, "sdf.repeat() count")
40263
40264
  );
40264
40265
  }
40265
40266
  function circularArray(shape, count, offset2 = 0) {
40266
40267
  return shape.circularArray(
40267
40268
  requirePositiveInteger(count, "sdf.circularArray() count"),
40268
- requireNonNegativeFinite(offset2, "sdf.circularArray() offset")
40269
+ requireNonNegativeFinite$1(offset2, "sdf.circularArray() offset")
40269
40270
  );
40270
40271
  }
40271
40272
  function resolveTpmsOptions(options) {
@@ -41065,11 +41066,11 @@ function mergeTopology(base, overlay) {
41065
41066
  return merged;
41066
41067
  }
41067
41068
  const ANALYTIC_SURFACE_EPS = 1e-9;
41068
- function vecLength$1(v) {
41069
+ function vecLength$2(v) {
41069
41070
  return Math.hypot(v[0], v[1], v[2]);
41070
41071
  }
41071
41072
  function normalizeVec3OrNull(v) {
41072
- const length4 = vecLength$1(v);
41073
+ const length4 = vecLength$2(v);
41073
41074
  return length4 > ANALYTIC_SURFACE_EPS ? [v[0] / length4, v[1] / length4, v[2] / length4] : null;
41074
41075
  }
41075
41076
  function dotVec3$1(a2, b) {
@@ -41121,9 +41122,9 @@ function matrixUniformDistanceScale(matrix) {
41121
41122
  const xAxis = [matrix[0], matrix[1], matrix[2]];
41122
41123
  const yAxis = [matrix[4], matrix[5], matrix[6]];
41123
41124
  const zAxis = [matrix[8], matrix[9], matrix[10]];
41124
- const sx = vecLength$1(xAxis);
41125
- const sy = vecLength$1(yAxis);
41126
- const sz = vecLength$1(zAxis);
41125
+ const sx = vecLength$2(xAxis);
41126
+ const sy = vecLength$2(yAxis);
41127
+ const sz = vecLength$2(zAxis);
41127
41128
  if (sx <= ANALYTIC_SURFACE_EPS || sy <= ANALYTIC_SURFACE_EPS || sz <= ANALYTIC_SURFACE_EPS) return null;
41128
41129
  if (!sameScalar(sx, sy) || !sameScalar(sx, sz)) return null;
41129
41130
  const orthoTolerance = ANALYTIC_SURFACE_EPS * Math.max(1, sx * sx);
@@ -41811,7 +41812,7 @@ function withBaseDimensionsAndMergedSourceSpans(base, sources, out, preserveOutp
41811
41812
  return result;
41812
41813
  }
41813
41814
  function resolveRotationPoint(shape, point2) {
41814
- if (Array.isArray(point2)) return requireFiniteVec3$2(point2, "rotateAroundTo(): point");
41815
+ if (Array.isArray(point2)) return requireFiniteVec3$4(point2, "rotateAroundTo(): point");
41815
41816
  return shape.referencePoint(point2);
41816
41817
  }
41817
41818
  function setShapeDimensions(shape, dims, options = {}) {
@@ -42731,8 +42732,8 @@ class Shape {
42731
42732
  * ```
42732
42733
  */
42733
42734
  placeReference(ref, target, offset2) {
42734
- const targetPoint = requireFiniteVec3$2(target, "Shape.placeReference() target");
42735
- const offsetPoint = offset2 === void 0 ? void 0 : requireFiniteVec3$2(offset2, "Shape.placeReference() offset");
42735
+ const targetPoint = requireFiniteVec3$4(target, "Shape.placeReference() target");
42736
+ const offsetPoint = offset2 === void 0 ? void 0 : requireFiniteVec3$4(offset2, "Shape.placeReference() offset");
42736
42737
  const sourcePoint = this.referencePoint(ref);
42737
42738
  let dx = targetPoint[0] - sourcePoint[0];
42738
42739
  let dy = targetPoint[1] - sourcePoint[1];
@@ -42837,7 +42838,7 @@ class Shape {
42837
42838
  /** Scale the shape uniformly or per-axis from an explicit pivot point. */
42838
42839
  scaleAround(pivot, v) {
42839
42840
  const scale2 = requireNonZeroFiniteScale3(v, "Shape.scaleAround() scale");
42840
- const scalePivot = requireFiniteVec3$2(pivot, "Shape.scaleAround() pivot");
42841
+ const scalePivot = requireFiniteVec3$4(pivot, "Shape.scaleAround() pivot");
42841
42842
  if (scalePivot[0] === 0 && scalePivot[1] === 0 && scalePivot[2] === 0) {
42842
42843
  const nextPlan2 = appendShapeCompileTransform(getShapeCompilePlanInternal(this), {
42843
42844
  kind: "scale",
@@ -42875,7 +42876,7 @@ class Shape {
42875
42876
  }
42876
42877
  /** Mirror across a plane through an explicit point, defined by its normal vector. */
42877
42878
  mirrorThrough(point2, normal) {
42878
- const mirrorPoint = requireFiniteVec3$2(point2, "Shape.mirrorThrough() point");
42879
+ const mirrorPoint = requireFiniteVec3$4(point2, "Shape.mirrorThrough() point");
42879
42880
  const mirrorNormal = requireNonZeroFiniteVec3(normal, "Shape.mirrorThrough() normal");
42880
42881
  if (mirrorPoint[0] === 0 && mirrorPoint[1] === 0 && mirrorPoint[2] === 0) {
42881
42882
  const transformedPlan2 = appendShapeCompileTransform(getShapeCompilePlanInternal(this), {
@@ -42933,7 +42934,7 @@ class Shape {
42933
42934
  rotateAroundAxis(axis, angleDeg, pivot = [0, 0, 0]) {
42934
42935
  const rotateAxis = requireNonZeroFiniteVec3(axis, "Shape.rotateAroundAxis() axis");
42935
42936
  const degrees2 = requireFiniteNumber$1(angleDeg, "Shape.rotateAroundAxis() angleDeg");
42936
- const rotatePivot = requireFiniteVec3$2(pivot, "Shape.rotateAroundAxis() pivot");
42937
+ const rotatePivot = requireFiniteVec3$4(pivot, "Shape.rotateAroundAxis() pivot");
42937
42938
  const len2 = Math.sqrt(rotateAxis[0] ** 2 + rotateAxis[1] ** 2 + rotateAxis[2] ** 2) || 1;
42938
42939
  const normalizedAxis = [rotateAxis[0] / len2, rotateAxis[1] / len2, rotateAxis[2] / len2];
42939
42940
  const matrix = rotationAroundAxisMatrix(normalizedAxis, degrees2, rotatePivot);
@@ -42958,7 +42959,7 @@ class Shape {
42958
42959
  */
42959
42960
  rotateAroundTo(axis, pivot, movingPoint, targetPoint, options = {}) {
42960
42961
  const rotateAxis = requireNonZeroFiniteVec3(axis, "Shape.rotateAroundTo() axis");
42961
- const rotatePivot = requireFiniteVec3$2(pivot, "Shape.rotateAroundTo() pivot");
42962
+ const rotatePivot = requireFiniteVec3$4(pivot, "Shape.rotateAroundTo() pivot");
42962
42963
  const moving = resolveRotationPoint(this, movingPoint);
42963
42964
  const target = resolveRotationPoint(this, targetPoint);
42964
42965
  const angleDeg = solveRotateAroundAngle(rotateAxis, rotatePivot, moving, target, options);
@@ -43281,7 +43282,7 @@ class Shape {
43281
43282
  const sp = this.referencePoint(selfAnchor);
43282
43283
  let dx = tp[0] - sp[0], dy = tp[1] - sp[1], dz = tp[2] - sp[2];
43283
43284
  if (offset2) {
43284
- const offsetPoint = requireFiniteVec3$2(offset2, "Shape.attachTo() offset");
43285
+ const offsetPoint = requireFiniteVec3$4(offset2, "Shape.attachTo() offset");
43285
43286
  dx += offsetPoint[0];
43286
43287
  dy += offsetPoint[1];
43287
43288
  dz += offsetPoint[2];
@@ -45533,6 +45534,107 @@ function collectJointsView(options = {}, base = null, motionSource) {
45533
45534
  function jointsView(options = {}) {
45534
45535
  _collected$8 = collectJointsView(options, _collected$8);
45535
45536
  }
45537
+ function requireFiniteVec3$2(input, label) {
45538
+ if (!Array.isArray(input) || input.length !== 3) throw new Error(`${label} must be a [x, y, z] tuple`);
45539
+ const out = [input[0], input[1], input[2]];
45540
+ if (!Number.isFinite(out[0]) || !Number.isFinite(out[1]) || !Number.isFinite(out[2])) {
45541
+ throw new Error(`${label} must contain finite numbers`);
45542
+ }
45543
+ return out;
45544
+ }
45545
+ function vecDot$1(a2, b) {
45546
+ return a2[0] * b[0] + a2[1] * b[1] + a2[2] * b[2];
45547
+ }
45548
+ function vecCross$1(a2, b) {
45549
+ 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]];
45550
+ }
45551
+ function vecLength$1(v) {
45552
+ return Math.hypot(v[0], v[1], v[2]);
45553
+ }
45554
+ function vecNormalize$1(v, label) {
45555
+ const length4 = vecLength$1(v);
45556
+ if (length4 < 1e-10) throw new Error(`${label} must be non-zero`);
45557
+ return [v[0] / length4, v[1] / length4, v[2] / length4];
45558
+ }
45559
+ function orthonormalizeFrame(axisInput, upInput) {
45560
+ const axis = vecNormalize$1(axisInput, "frame axis");
45561
+ if (vecLength$1(upInput) < 1e-10) throw new Error("frame up vector must be non-zero");
45562
+ const upProjection = vecDot$1(upInput, axis);
45563
+ const upOrtho = [upInput[0] - upProjection * axis[0], upInput[1] - upProjection * axis[1], upInput[2] - upProjection * axis[2]];
45564
+ if (vecLength$1(upOrtho) < 1e-10) throw new Error("frame axis and up must not be parallel");
45565
+ const up = vecNormalize$1(upOrtho, "frame up vector");
45566
+ const right = vecNormalize$1(vecCross$1(up, axis), "frame right vector");
45567
+ return { axis, up, right };
45568
+ }
45569
+ function frameTransform(origin, axisInput, upInput) {
45570
+ const { axis, up, right } = orthonormalizeFrame(axisInput, upInput);
45571
+ return Transform.from([
45572
+ right[0],
45573
+ right[1],
45574
+ right[2],
45575
+ 0,
45576
+ up[0],
45577
+ up[1],
45578
+ up[2],
45579
+ 0,
45580
+ axis[0],
45581
+ axis[1],
45582
+ axis[2],
45583
+ 0,
45584
+ origin[0],
45585
+ origin[1],
45586
+ origin[2],
45587
+ 1
45588
+ ]);
45589
+ }
45590
+ function normalizeAssemblyFrame(name, input) {
45591
+ if (!input || typeof input !== "object") throw new Error(`frame("${name}") options are required`);
45592
+ const origin = requireFiniteVec3$2(input.origin, `frame("${name}") origin`);
45593
+ const rawAxis = requireFiniteVec3$2(input.axis, `frame("${name}") axis`);
45594
+ const rawUp = requireFiniteVec3$2(input.up, `frame("${name}") up`);
45595
+ const { axis, up } = orthonormalizeFrame(rawAxis, rawUp);
45596
+ return {
45597
+ name,
45598
+ origin,
45599
+ axis,
45600
+ up,
45601
+ fixed: input.fixed ?? false,
45602
+ metadata: input.metadata ? { ...input.metadata } : void 0
45603
+ };
45604
+ }
45605
+ function cloneAssemblyFrame(frame) {
45606
+ return {
45607
+ name: frame.name,
45608
+ origin: [...frame.origin],
45609
+ axis: [...frame.axis],
45610
+ up: [...frame.up],
45611
+ fixed: frame.fixed,
45612
+ metadata: frame.metadata ? { ...frame.metadata } : void 0
45613
+ };
45614
+ }
45615
+ function cloneAssemblyFrameEdge(edge) {
45616
+ return {
45617
+ name: edge.name,
45618
+ a: edge.a,
45619
+ b: edge.b,
45620
+ metadata: edge.metadata ? { ...edge.metadata } : void 0
45621
+ };
45622
+ }
45623
+ function frameDefTransform(frame) {
45624
+ return frameTransform(frame.origin, frame.axis, frame.up);
45625
+ }
45626
+ function frameFromTransform(frame, transform) {
45627
+ return {
45628
+ ...cloneAssemblyFrame(frame),
45629
+ origin: transform.point([0, 0, 0]),
45630
+ up: vecNormalize$1(transform.vector([0, 1, 0]), `solved frame "${frame.name}" up`),
45631
+ axis: vecNormalize$1(transform.vector([0, 0, 1]), `solved frame "${frame.name}" axis`),
45632
+ transform
45633
+ };
45634
+ }
45635
+ function relativeFrameTransform(parent, child) {
45636
+ return composeChain(frameDefTransform(child), frameDefTransform(parent).inverse());
45637
+ }
45536
45638
  var define_process_env_default$1 = {};
45537
45639
  let collectedAssemblies = [];
45538
45640
  function resetCollectedAssemblies() {
@@ -45666,12 +45768,22 @@ function motionTransform(joint2, value) {
45666
45768
  const dz = joint2.axis[2] * value;
45667
45769
  return Transform.identity().translate(dx, dy, dz);
45668
45770
  }
45771
+ function frameJointMotionTransform(joint2, value) {
45772
+ if (joint2.type === "fixed") return Transform.identity();
45773
+ if (joint2.type === "revolute") return Transform.rotationAxis([0, 0, 1], value);
45774
+ return Transform.translation(0, 0, value);
45775
+ }
45669
45776
  function clampJointValue(joint2, value) {
45670
45777
  let clamped = Number.isFinite(value) ? value : joint2.defaultValue;
45671
45778
  if (joint2.min != null) clamped = Math.max(joint2.min, clamped);
45672
45779
  if (joint2.max != null) clamped = Math.min(joint2.max, clamped);
45673
45780
  return { value: clamped, wasClamped: clamped !== value };
45674
45781
  }
45782
+ function clampFrameJointDefault(jointName, value, min2, max2) {
45783
+ if (min2 !== void 0 && value < min2) throw new Error(`Frame joint "${jointName}" default must be >= min`);
45784
+ if (max2 !== void 0 && value > max2) throw new Error(`Frame joint "${jointName}" default must be <= max`);
45785
+ return value;
45786
+ }
45675
45787
  function finiteVec3(input, label) {
45676
45788
  if (!Array.isArray(input) || input.length !== 3) throw new Error(`${label} must be a [x, y, z] tuple`);
45677
45789
  const out = [input[0], input[1], input[2]];
@@ -45683,6 +45795,9 @@ function finiteVec3(input, label) {
45683
45795
  function cloneVec3$3(v) {
45684
45796
  return [v[0], v[1], v[2]];
45685
45797
  }
45798
+ function cloneAngleReference(reference) {
45799
+ return reference ? { kind: "worldDirection", direction: cloneVec3$3(reference.direction) } : void 0;
45800
+ }
45686
45801
  function vecDistance(a2, b) {
45687
45802
  return Math.hypot(b[0] - a2[0], b[1] - a2[1], b[2] - a2[2]);
45688
45803
  }
@@ -45702,12 +45817,10 @@ function vecNormalize(a2) {
45702
45817
  const len2 = vecLength(a2);
45703
45818
  return len2 < 1e-12 ? [0, 0, 0] : [a2[0] / len2, a2[1] / len2, a2[2] / len2];
45704
45819
  }
45705
- function signedAngleAboutAxis(a2, b, c2, axisUnit) {
45706
- const ba = vecSub(a2, b);
45707
- const bc = vecSub(c2, b);
45708
- const cross2 = vecCross(ba, bc);
45820
+ function signedAngleBetweenVectorsAboutAxis(reference, target, axisUnit) {
45821
+ const cross2 = vecCross(reference, target);
45709
45822
  const sin2 = vecDot(cross2, axisUnit);
45710
- const cos2 = vecDot(ba, bc);
45823
+ const cos2 = vecDot(reference, target);
45711
45824
  return Math.atan2(sin2, cos2) * 180 / Math.PI;
45712
45825
  }
45713
45826
  function normalizeAngleDeltaDeg(value) {
@@ -45828,7 +45941,7 @@ function deriveExplodeHintsFromMates(constraints, result, bodies, ctx) {
45828
45941
  return hints;
45829
45942
  }
45830
45943
  class SolvedAssembly {
45831
- constructor(name, parts, transforms, jointValues, solveWarnings, _mateMetadata = null, _kinematics = null, usedPortRefs) {
45944
+ constructor(name, parts, transforms, jointValues, solveWarnings, _mateMetadata = null, _kinematics = null, _frames = /* @__PURE__ */ new Map(), usedPortRefs) {
45832
45945
  __publicField(this, "_usedPortRefs");
45833
45946
  this.name = name;
45834
45947
  this.parts = parts;
@@ -45837,6 +45950,7 @@ class SolvedAssembly {
45837
45950
  this.solveWarnings = solveWarnings;
45838
45951
  this._mateMetadata = _mateMetadata;
45839
45952
  this._kinematics = _kinematics;
45953
+ this._frames = _frames;
45840
45954
  this._usedPortRefs = usedPortRefs ?? /* @__PURE__ */ new Set();
45841
45955
  }
45842
45956
  /** Return any warnings generated during solve (clamped joints, unconverged mates, etc.). */
@@ -45862,7 +45976,7 @@ class SolvedAssembly {
45862
45976
  var _a3;
45863
45977
  return ((_a3 = this._mateMetadata) == null ? void 0 : _a3.converged) ?? null;
45864
45978
  }
45865
- /** Solved assembly-native kinematic graph, or null when no links were declared. */
45979
+ /** Solved assembly-native kinematic or frame-edge overlay data, or null when no rig overlay data was declared. */
45866
45980
  get kinematics() {
45867
45981
  return this._kinematics;
45868
45982
  }
@@ -45873,6 +45987,19 @@ class SolvedAssembly {
45873
45987
  if (!link) throw new Error(`Unknown kinematic link "${linkName}"`);
45874
45988
  return cloneVec3$3(link.position);
45875
45989
  }
45990
+ /** Return the solved world transform for a named rig frame. */
45991
+ getFrame(frameName) {
45992
+ const frame = this._frames.get(frameName);
45993
+ if (!frame) throw new Error(`Unknown frame "${frameName}"`);
45994
+ return frame.transform;
45995
+ }
45996
+ /** Return solved rig frames, including origin, axis, up, and transform. */
45997
+ get frames() {
45998
+ return [...this._frames.values()].map((frame) => ({
45999
+ ...cloneAssemblyFrame(frame),
46000
+ transform: frame.transform
46001
+ }));
46002
+ }
45876
46003
  /**
45877
46004
  * Return the world-space `Transform` for the named part at the solved pose.
45878
46005
  *
@@ -46125,14 +46252,19 @@ class Assembly {
46125
46252
  __publicField(this, "parts", /* @__PURE__ */ new Map());
46126
46253
  __publicField(this, "joints", /* @__PURE__ */ new Map());
46127
46254
  __publicField(this, "jointCouplings", /* @__PURE__ */ new Map());
46255
+ __publicField(this, "frames", /* @__PURE__ */ new Map());
46256
+ __publicField(this, "frameJoints", /* @__PURE__ */ new Map());
46257
+ __publicField(this, "frameEdges", /* @__PURE__ */ new Map());
46128
46258
  __publicField(this, "links", /* @__PURE__ */ new Map());
46129
46259
  __publicField(this, "linkEdges", /* @__PURE__ */ new Map());
46130
46260
  __publicField(this, "linkAngles", /* @__PURE__ */ new Map());
46261
+ __publicField(this, "derivedLinks", /* @__PURE__ */ new Map());
46131
46262
  __publicField(this, "_mateFns", []);
46132
46263
  __publicField(this, "_refs", createPlacementReferences());
46133
46264
  __publicField(this, "_portsByPart", /* @__PURE__ */ new Map());
46134
46265
  __publicField(this, "_usedPortRefs", /* @__PURE__ */ new Set());
46135
46266
  __publicField(this, "_connectCounter", 0);
46267
+ __publicField(this, "_frameEdgeCounter", 0);
46136
46268
  __publicField(this, "_linkEdgeCounter", 0);
46137
46269
  __publicField(this, "_linkAngleCounter", 0);
46138
46270
  this.name = name;
@@ -46142,6 +46274,7 @@ class Assembly {
46142
46274
  const next = new Assembly(this.name);
46143
46275
  next._refs = clonePlacementReferences(this._refs);
46144
46276
  next._connectCounter = this._connectCounter;
46277
+ next._frameEdgeCounter = this._frameEdgeCounter;
46145
46278
  next._linkEdgeCounter = this._linkEdgeCounter;
46146
46279
  next._linkAngleCounter = this._linkAngleCounter;
46147
46280
  next._mateFns.push(...this._mateFns);
@@ -46151,7 +46284,8 @@ class Assembly {
46151
46284
  part: record.part,
46152
46285
  base: record.base,
46153
46286
  metadata: record.metadata ? { ...record.metadata } : void 0,
46154
- mates: record.mates.map((mate) => ({ ...mate }))
46287
+ mates: record.mates.map((mate) => ({ ...mate })),
46288
+ bindToFrame: record.bindToFrame
46155
46289
  });
46156
46290
  }
46157
46291
  for (const [name, joint2] of this.joints) {
@@ -46180,6 +46314,27 @@ class Assembly {
46180
46314
  offset: coupling.offset
46181
46315
  });
46182
46316
  }
46317
+ for (const [name, frame] of this.frames) {
46318
+ next.frames.set(name, cloneAssemblyFrame(frame));
46319
+ }
46320
+ for (const [name, joint2] of this.frameJoints) {
46321
+ next.frameJoints.set(name, {
46322
+ name: joint2.name,
46323
+ type: joint2.type,
46324
+ parent: joint2.parent,
46325
+ child: joint2.child,
46326
+ rest: joint2.rest,
46327
+ min: joint2.min,
46328
+ max: joint2.max,
46329
+ defaultValue: joint2.defaultValue,
46330
+ unit: joint2.unit,
46331
+ control: joint2.control,
46332
+ metadata: joint2.metadata ? { ...joint2.metadata } : void 0
46333
+ });
46334
+ }
46335
+ for (const [name, edge] of this.frameEdges) {
46336
+ next.frameEdges.set(name, cloneAssemblyFrameEdge(edge));
46337
+ }
46183
46338
  for (const [name, link] of this.links) {
46184
46339
  next.links.set(name, {
46185
46340
  name: link.name,
@@ -46207,6 +46362,7 @@ class Assembly {
46207
46362
  a: angle.a,
46208
46363
  b: angle.b,
46209
46364
  c: angle.c,
46365
+ reference: cloneAngleReference(angle.reference),
46210
46366
  target: angle.target,
46211
46367
  min: angle.min,
46212
46368
  max: angle.max,
@@ -46214,6 +46370,9 @@ class Assembly {
46214
46370
  metadata: angle.metadata ? { ...angle.metadata } : void 0
46215
46371
  });
46216
46372
  }
46373
+ for (const [name, derived] of this.derivedLinks) {
46374
+ next.derivedLinks.set(name, { ...derived });
46375
+ }
46217
46376
  for (const [partName, ports] of this._portsByPart) {
46218
46377
  next._portsByPart.set(partName, clonePortMap(ports));
46219
46378
  }
@@ -46351,6 +46510,129 @@ class Assembly {
46351
46510
  port: resolved.connector
46352
46511
  };
46353
46512
  }
46513
+ /**
46514
+ * Add a named rig frame to the assembly.
46515
+ *
46516
+ * A frame is a solved pose: `origin` plus orientation. `axis` is the frame's
46517
+ * primary direction and `up` fixes roll around that axis. Use frames for
46518
+ * robot links, joint axes, and parts that must carry orientation. Use
46519
+ * `link()` for solved points in distance/angle graphs.
46520
+ *
46521
+ * @category Assembly
46522
+ */
46523
+ frame(name, options) {
46524
+ const id = typeof name === "string" ? name.trim() : "";
46525
+ if (!id) throw new Error("frame() name must be non-empty");
46526
+ if (this.frames.has(id)) throw new Error(`Frame "${id}" already exists`);
46527
+ this.frames.set(id, normalizeAssemblyFrame(id, options));
46528
+ return this;
46529
+ }
46530
+ resolveFrameJointEndpoint(jointName, value, role) {
46531
+ const frameName = typeof value === "string" ? value.trim() : "";
46532
+ if (!frameName) throw new Error(`Frame joint "${jointName}" ${role} must be non-empty`);
46533
+ const frame = this.frames.get(frameName);
46534
+ if (!frame) throw new Error(`Frame joint "${jointName}" unknown ${role} frame "${frameName}"`);
46535
+ return frame;
46536
+ }
46537
+ addFrameJoint(type, name, options) {
46538
+ const id = typeof name === "string" ? name.trim() : "";
46539
+ if (!id) throw new Error(`${type}Joint() name must be non-empty`);
46540
+ if (!options || typeof options !== "object") throw new Error(`${type}Joint("${id}") options are required`);
46541
+ if (this.frameJoints.has(id) || this.joints.has(id)) throw new Error(`Joint "${id}" already exists`);
46542
+ const parent = this.resolveFrameJointEndpoint(id, options.parent, "parent");
46543
+ const child = this.resolveFrameJointEndpoint(id, options.child, "child");
46544
+ if (parent.name === child.name) throw new Error(`Frame joint "${id}" cannot connect a frame to itself`);
46545
+ if (child.fixed) throw new Error(`Frame joint "${id}" cannot drive fixed frame "${child.name}"`);
46546
+ for (const joint2 of this.frameJoints.values()) {
46547
+ if (joint2.child === child.name) throw new Error(`Frame "${child.name}" already has parent joint "${joint2.name}"`);
46548
+ }
46549
+ const movingOptions = options;
46550
+ const min2 = movingOptions.min;
46551
+ const max2 = movingOptions.max;
46552
+ if (min2 !== void 0 && !Number.isFinite(min2)) throw new Error(`Frame joint "${id}" min must be finite`);
46553
+ if (max2 !== void 0 && !Number.isFinite(max2)) throw new Error(`Frame joint "${id}" max must be finite`);
46554
+ if (min2 !== void 0 && max2 !== void 0 && min2 > max2) throw new Error(`Frame joint "${id}" min must be <= max`);
46555
+ const defaultValue = type === "fixed" ? 0 : movingOptions.default ?? 0;
46556
+ if (!Number.isFinite(defaultValue)) throw new Error(`Frame joint "${id}" default must be finite`);
46557
+ this.frameJoints.set(id, {
46558
+ name: id,
46559
+ type,
46560
+ parent: parent.name,
46561
+ child: child.name,
46562
+ rest: relativeFrameTransform(parent, child),
46563
+ min: min2,
46564
+ max: max2,
46565
+ defaultValue: clampFrameJointDefault(id, defaultValue, min2, max2),
46566
+ unit: movingOptions.unit,
46567
+ control: type !== "fixed" && movingOptions.control !== false,
46568
+ metadata: options.metadata ? { ...options.metadata } : void 0
46569
+ });
46570
+ return this;
46571
+ }
46572
+ /**
46573
+ * Rigidly attach a child rig frame to a parent rig frame.
46574
+ *
46575
+ * Fixed joints carry frame hierarchy but do not expose a Motion control.
46576
+ *
46577
+ * @category Assembly
46578
+ */
46579
+ fixedJoint(name, options) {
46580
+ return this.addFrameJoint("fixed", name, options);
46581
+ }
46582
+ /**
46583
+ * Add a revolute rig-frame joint.
46584
+ *
46585
+ * The child frame rotates around the parent frame's `axis` direction. Moving
46586
+ * frame joints appear in Motion by default; pass `control: false` to keep the
46587
+ * joint solved at its default value without showing a Motion control.
46588
+ *
46589
+ * @category Assembly
46590
+ */
46591
+ revoluteJoint(name, options) {
46592
+ return this.addFrameJoint("revolute", name, options);
46593
+ }
46594
+ /**
46595
+ * Add a prismatic rig-frame joint.
46596
+ *
46597
+ * The child frame translates along the parent frame's `axis` direction. Moving
46598
+ * frame joints appear in Motion by default; pass `control: false` to keep the
46599
+ * joint solved at its default value without showing a Motion control.
46600
+ *
46601
+ * @category Assembly
46602
+ */
46603
+ prismaticJoint(name, options) {
46604
+ return this.addFrameJoint("prismatic", name, options);
46605
+ }
46606
+ /**
46607
+ * Add a visual skeleton edge between two rig frame origins.
46608
+ *
46609
+ * Frame edges follow the solved frame poses produced by `fixedJoint()`,
46610
+ * `revoluteJoint()`, and `prismaticJoint()`. They do not add constraints,
46611
+ * degrees of freedom, parts, or geometry; use them to make a frame-only rig
46612
+ * readable in the Motion/rig inspection overlay.
46613
+ *
46614
+ * @category Assembly
46615
+ */
46616
+ edgeBetweenFrames(a2, b, options = {}) {
46617
+ const frameA = typeof a2 === "string" ? a2.trim() : "";
46618
+ const frameB = typeof b === "string" ? b.trim() : "";
46619
+ if (!frameA) throw new Error("edgeBetweenFrames() first frame must be non-empty");
46620
+ if (!frameB) throw new Error("edgeBetweenFrames() second frame must be non-empty");
46621
+ if (!this.frames.has(frameA)) throw new Error(`edgeBetweenFrames() unknown frame "${frameA}"`);
46622
+ if (!this.frames.has(frameB)) throw new Error(`edgeBetweenFrames() unknown frame "${frameB}"`);
46623
+ if (frameA === frameB) throw new Error("edgeBetweenFrames() requires two different frames");
46624
+ if (options.name !== void 0 && typeof options.name !== "string") throw new Error("edgeBetweenFrames() name must be a string");
46625
+ const name = (options.name ?? `${frameA}_${frameB}_${this._frameEdgeCounter++}`).trim();
46626
+ if (!name) throw new Error("edgeBetweenFrames() name must be non-empty");
46627
+ if (this.frameEdges.has(name)) throw new Error(`Frame edge "${name}" already exists`);
46628
+ this.frameEdges.set(name, {
46629
+ name,
46630
+ a: frameA,
46631
+ b: frameB,
46632
+ metadata: options.metadata ? { ...options.metadata } : void 0
46633
+ });
46634
+ return this;
46635
+ }
46354
46636
  /**
46355
46637
  * Add a named kinematic link to the assembly graph.
46356
46638
  *
@@ -46398,6 +46680,11 @@ class Assembly {
46398
46680
  if (options.min !== void 0 && options.max !== void 0 && options.min > options.max) {
46399
46681
  throw new Error(`Link edge "${name}" min must be <= max`);
46400
46682
  }
46683
+ const isStructural = !options.visualOnly && options.length !== "free";
46684
+ if (isStructural) {
46685
+ this.assertNotDerivedStructuralLink(a2, "edgeBetweenLinks()");
46686
+ this.assertNotDerivedStructuralLink(b, "edgeBetweenLinks()");
46687
+ }
46401
46688
  let length4;
46402
46689
  if (options.visualOnly || options.length === "free") {
46403
46690
  length4 = null;
@@ -46406,6 +46693,11 @@ class Assembly {
46406
46693
  length4 = options.length;
46407
46694
  } else {
46408
46695
  length4 = vecDistance(linkA.at, linkB.at);
46696
+ if (length4 <= 1e-10) {
46697
+ throw new Error(
46698
+ `edgeBetweenLinks("${a2}", "${b}") captured a zero structural length. Pass an explicit numeric length, move the links apart with link(..., { at }), or use { length: "free" } / { visualOnly: true }.`
46699
+ );
46700
+ }
46409
46701
  }
46410
46702
  this.linkEdges.set(name, {
46411
46703
  name,
@@ -46430,13 +46722,135 @@ class Assembly {
46430
46722
  * @category Assembly
46431
46723
  */
46432
46724
  addAngleBetweenLinks(a2, b, c2, options = {}) {
46433
- var _a3, _b3;
46434
46725
  for (const id of [a2, b, c2]) {
46435
46726
  if (!this.links.has(id)) throw new Error(`addAngleBetweenLinks() unknown link "${id}"`);
46727
+ this.assertNotDerivedStructuralLink(id, "addAngleBetweenLinks()");
46436
46728
  }
46437
46729
  if (a2 === b || b === c2 || a2 === c2) throw new Error("addAngleBetweenLinks() requires three different links");
46438
46730
  const name = options.name ?? `${a2}_${b}_${c2}_${this._linkAngleCounter++}`;
46439
46731
  if (this.linkAngles.has(name)) throw new Error(`Link angle "${name}" already exists`);
46732
+ const normalized = this.normalizeAngleOptions(name, options);
46733
+ this.linkAngles.set(name, {
46734
+ name,
46735
+ a: a2,
46736
+ b,
46737
+ c: c2,
46738
+ ...normalized
46739
+ });
46740
+ return this;
46741
+ }
46742
+ /**
46743
+ * Add an absolute angle relationship from a world direction to a link segment.
46744
+ *
46745
+ * The first link is the vertex/pivot and the second link is the moving point.
46746
+ * A value of `0` places `fromLink -> toLink` along `direction` in the
46747
+ * mechanism plane; positive angles rotate counter-clockwise in that plane.
46748
+ *
46749
+ * Use `Points.polar(1, angleDeg)` when the reference direction is planar and
46750
+ * angle-based instead of axis-aligned.
46751
+ *
46752
+ * @category Assembly
46753
+ */
46754
+ addAngleBetweenLinkSegmentAndWorldDirection(fromLink, toLink, direction2, options = {}) {
46755
+ const caller = "addAngleBetweenLinkSegmentAndWorldDirection()";
46756
+ for (const id of [fromLink, toLink]) {
46757
+ if (!this.links.has(id)) throw new Error(`${caller} unknown link "${id}"`);
46758
+ this.assertNotDerivedStructuralLink(id, caller);
46759
+ }
46760
+ if (fromLink === toLink) throw new Error(`${caller} requires two different links`);
46761
+ const referenceDirection = finiteVec3(direction2, `${caller} direction`);
46762
+ if (vecLength(referenceDirection) <= 1e-10) throw new Error(`${caller} direction must be non-zero`);
46763
+ const name = options.name ?? `${fromLink}_${toLink}_from_direction_${this._linkAngleCounter++}`;
46764
+ if (this.linkAngles.has(name)) throw new Error(`Link angle "${name}" already exists`);
46765
+ this.linkAngles.set(name, {
46766
+ name,
46767
+ b: fromLink,
46768
+ c: toLink,
46769
+ reference: { kind: "worldDirection", direction: vecNormalize(referenceDirection) },
46770
+ ...this.normalizeAngleOptions(name, options)
46771
+ });
46772
+ return this;
46773
+ }
46774
+ /**
46775
+ * @deprecated Use `addAngleBetweenLinkSegmentAndWorldDirection(fromLink, toLink, [1, 0, 0], options)`.
46776
+ * @skillSuppress Compatibility-only renamed API. Use `addAngleBetweenLinkSegmentAndWorldDirection()` instead.
46777
+ */
46778
+ addAngleOfLinkSegmentFromXAxis(fromLink, toLink, options = {}) {
46779
+ throw new Error(
46780
+ "addAngleOfLinkSegmentFromXAxis() has been replaced by addAngleBetweenLinkSegmentAndWorldDirection(fromLink, toLink, [1, 0, 0], options). Update your script."
46781
+ );
46782
+ }
46783
+ /**
46784
+ * @deprecated Use `addAngleBetweenLinkSegmentAndWorldDirection(fromLink, toLink, [0, 1, 0], options)`.
46785
+ * @skillSuppress Compatibility-only renamed API. Use `addAngleBetweenLinkSegmentAndWorldDirection()` instead.
46786
+ */
46787
+ addAngleOfLinkSegmentFromYAxis(fromLink, toLink, options = {}) {
46788
+ throw new Error(
46789
+ "addAngleOfLinkSegmentFromYAxis() has been replaced by addAngleBetweenLinkSegmentAndWorldDirection(fromLink, toLink, [0, 1, 0], options). Update your script."
46790
+ );
46791
+ }
46792
+ assertNotDerivedStructuralLink(name, caller) {
46793
+ if (!this.derivedLinks.has(name)) return;
46794
+ throw new Error(
46795
+ `${caller} cannot structurally constrain derived link "${name}". Derived links are recomputed after the primary link solve.`
46796
+ );
46797
+ }
46798
+ assertCanBecomeDerivedLink(name) {
46799
+ const link = this.links.get(name);
46800
+ if (link == null ? void 0 : link.fixed) throw new Error(`Derived link "${name}" cannot be fixed`);
46801
+ for (const edge of this.linkEdges.values()) {
46802
+ if (edge.visualOnly || edge.length == null) continue;
46803
+ if (edge.a === name || edge.b === name) throw new Error(`Derived link "${name}" cannot already have structural edge "${edge.name}"`);
46804
+ }
46805
+ for (const angle of this.linkAngles.values()) {
46806
+ if (angle.a === name || angle.b === name || angle.c === name) {
46807
+ throw new Error(`Derived link "${name}" cannot already participate in angle "${angle.name}"`);
46808
+ }
46809
+ }
46810
+ }
46811
+ addDerivedLink(direction2, name, fromLink, referenceLink, distance2) {
46812
+ const id = typeof name === "string" ? name.trim() : "";
46813
+ if (!id) throw new Error(`${direction2 === "toward" ? "linkToward" : "linkAwayFrom"}() name must be non-empty`);
46814
+ if (!this.links.has(fromLink)) throw new Error(`Derived link "${id}" unknown fromLink "${fromLink}"`);
46815
+ if (!this.links.has(referenceLink)) throw new Error(`Derived link "${id}" unknown reference link "${referenceLink}"`);
46816
+ if (id === fromLink || id === referenceLink || fromLink === referenceLink) {
46817
+ throw new Error(`Derived link "${id}" requires three different links`);
46818
+ }
46819
+ if (!Number.isFinite(distance2) || distance2 < 0) {
46820
+ throw new Error(`Derived link "${id}" distance must be a finite value >= 0`);
46821
+ }
46822
+ if (!this.links.has(id)) {
46823
+ this.link(id);
46824
+ }
46825
+ this.assertCanBecomeDerivedLink(id);
46826
+ this.derivedLinks.set(id, { name: id, fromLink, referenceLink, distance: distance2, direction: direction2 });
46827
+ return this;
46828
+ }
46829
+ /**
46830
+ * Create a derived link at a fixed distance from `fromLink` toward `towardLink`.
46831
+ *
46832
+ * Derived links are trace/reference points. They are recomputed after the
46833
+ * primary link solve and cannot participate in structural edges or angle
46834
+ * constraints.
46835
+ *
46836
+ * @category Assembly
46837
+ */
46838
+ linkToward(name, fromLink, towardLink, distance2) {
46839
+ return this.addDerivedLink("toward", name, fromLink, towardLink, distance2);
46840
+ }
46841
+ /**
46842
+ * Create a derived link at a fixed distance from `fromLink` away from `awayFromLink`.
46843
+ *
46844
+ * Use this for coupler trace/extension points such as the Chebyshev lambda
46845
+ * linkage's point beyond the rocker joint.
46846
+ *
46847
+ * @category Assembly
46848
+ */
46849
+ linkAwayFrom(name, fromLink, awayFromLink, distance2) {
46850
+ return this.addDerivedLink("awayFrom", name, fromLink, awayFromLink, distance2);
46851
+ }
46852
+ normalizeAngleOptions(name, options) {
46853
+ var _a3, _b3;
46440
46854
  const control = options.control === true ? { min: options.min, max: options.max, default: options.value } : options.control ? { ...options.control } : void 0;
46441
46855
  const min2 = ((_a3 = options.limit) == null ? void 0 : _a3.min) ?? options.min;
46442
46856
  const max2 = ((_b3 = options.limit) == null ? void 0 : _b3.max) ?? options.max;
@@ -46445,18 +46859,7 @@ class Assembly {
46445
46859
  if (min2 !== void 0 && max2 !== void 0 && min2 > max2) throw new Error(`Link angle "${name}" min must be <= max`);
46446
46860
  const target = options.value ?? (control == null ? void 0 : control.default);
46447
46861
  if (target !== void 0 && !Number.isFinite(target)) throw new Error(`Link angle "${name}" value must be finite`);
46448
- this.linkAngles.set(name, {
46449
- name,
46450
- a: a2,
46451
- b,
46452
- c: c2,
46453
- target,
46454
- min: min2,
46455
- max: max2,
46456
- control,
46457
- metadata: options.metadata ? { ...options.metadata } : void 0
46458
- });
46459
- return this;
46862
+ return { target, min: min2, max: max2, control, metadata: options.metadata ? { ...options.metadata } : void 0 };
46460
46863
  }
46461
46864
  /** Return the assembly-native kinematic graph definition. */
46462
46865
  describeKinematics() {
@@ -46483,30 +46886,23 @@ class Assembly {
46483
46886
  a: angle.a,
46484
46887
  b: angle.b,
46485
46888
  c: angle.c,
46889
+ reference: cloneAngleReference(angle.reference),
46486
46890
  target: angle.target,
46487
46891
  min: angle.min,
46488
46892
  max: angle.max,
46489
46893
  control: angle.control ? { ...angle.control } : void 0,
46490
46894
  metadata: angle.metadata ? { ...angle.metadata } : void 0
46491
- }))
46895
+ })),
46896
+ derivedLinks: [...this.derivedLinks.values()].map((derived) => ({ ...derived }))
46492
46897
  };
46493
46898
  }
46494
46899
  /**
46495
- * Add a virtual reference frame (no geometry) to the assembly graph.
46496
- *
46497
- * **Details**
46498
- *
46499
- * Useful when you need a named pivot point or coordinate frame that has no
46500
- * visual geometry. Acts like a zero-volume part and can be connected to
46501
- * other parts via joints.
46502
- *
46503
- * @param name - Unique part name for the frame in the assembly graph
46504
- * @param options - Optional transform and metadata
46505
- * @returns `this` for chaining
46506
- * @category Assembly
46900
+ * @deprecated `addFrame()` has been removed. Use `frame()` for rig frames, or
46901
+ * `addPart(name, group())` for an empty placeholder part.
46902
+ * @internal
46507
46903
  */
46508
- addFrame(name, options = {}) {
46509
- return this.addPart(name, group(), options);
46904
+ addFrame(_name2, _options = {}) {
46905
+ throw new Error("addFrame() has been removed. Use frame() for rig frames, or addPart(name, group()) for an empty placeholder part.");
46510
46906
  }
46511
46907
  /**
46512
46908
  * Add a named part to the assembly.
@@ -46546,6 +46942,16 @@ class Assembly {
46546
46942
  addPart(name, part, options = {}) {
46547
46943
  if (this.parts.has(name)) throw new Error(`Part "${name}" already exists`);
46548
46944
  const mates = options.mate == null ? [] : Array.isArray(options.mate) ? options.mate : [options.mate];
46945
+ if (options.bindToFrame != null && typeof options.bindToFrame !== "string") {
46946
+ throw new Error(`addPart("${name}") bindToFrame must be a frame name string`);
46947
+ }
46948
+ const bindToFrame = typeof options.bindToFrame === "string" ? options.bindToFrame.trim() : "";
46949
+ if (bindToFrame && mates.length > 0) {
46950
+ throw new Error(`addPart("${name}") cannot use both bindToFrame and mate`);
46951
+ }
46952
+ if (bindToFrame && !this.frames.has(bindToFrame)) {
46953
+ throw new Error(`addPart("${name}") bindToFrame references unknown frame "${bindToFrame}"`);
46954
+ }
46549
46955
  this.parts.set(name, {
46550
46956
  name,
46551
46957
  part,
@@ -46563,7 +46969,8 @@ class Assembly {
46563
46969
  if (aimLink === toLink) throw new Error(`addPart("${name}") mate[${index2}] aimLink must differ from toLink`);
46564
46970
  }
46565
46971
  return aimLink ? { connector, toLink, aimLink } : { connector, toLink };
46566
- })
46972
+ }),
46973
+ bindToFrame: bindToFrame || void 0
46567
46974
  });
46568
46975
  let ports = {};
46569
46976
  if (part instanceof Shape) {
@@ -46609,10 +47016,15 @@ class Assembly {
46609
47016
  * @internal
46610
47017
  */
46611
47018
  addJoint(name, type, parent, child, options = {}) {
46612
- if (this.joints.has(name)) throw new Error(`Joint "${name}" already exists`);
47019
+ if (this.joints.has(name) || this.frameJoints.has(name)) throw new Error(`Joint "${name}" already exists`);
46613
47020
  if (!this.parts.has(parent)) throw new Error(`Unknown parent part "${parent}"`);
46614
47021
  if (!this.parts.has(child)) throw new Error(`Unknown child part "${child}"`);
46615
47022
  if (parent === child) throw new Error(`Joint "${name}" cannot connect a part to itself`);
47023
+ const parentRecord = this.parts.get(parent);
47024
+ const childRecord = this.parts.get(child);
47025
+ if (parentRecord.bindToFrame || childRecord.bindToFrame) {
47026
+ throw new Error(`Joint "${name}" cannot connect frame-bound parts. Use frame joints for parts added with bindToFrame.`);
47027
+ }
46616
47028
  if (options.frame && options.origin) {
46617
47029
  throw new Error(`Joint "${name}" cannot have both frame and origin`);
46618
47030
  }
@@ -46889,7 +47301,578 @@ class Assembly {
46889
47301
  "addGearCoupling() has been removed from the modeling API. Express gear behavior through assembly kinematic constraints instead."
46890
47302
  );
46891
47303
  }
47304
+ solveFrames(state) {
47305
+ const transforms = /* @__PURE__ */ new Map();
47306
+ const frames = /* @__PURE__ */ new Map();
47307
+ const jointValues = {};
47308
+ const warnings = [];
47309
+ if (this.frames.size === 0) return { frames, transforms, jointValues, warnings };
47310
+ const incoming = /* @__PURE__ */ new Map();
47311
+ const jointsByParent = /* @__PURE__ */ new Map();
47312
+ for (const joint2 of this.frameJoints.values()) {
47313
+ if (incoming.has(joint2.child)) {
47314
+ throw new Error(`Frame "${joint2.child}" has multiple parent joints`);
47315
+ }
47316
+ incoming.set(joint2.child, joint2.name);
47317
+ const list = jointsByParent.get(joint2.parent) ?? [];
47318
+ list.push(joint2);
47319
+ jointsByParent.set(joint2.parent, list);
47320
+ }
47321
+ const roots = [...this.frames.keys()].filter((name) => !incoming.has(name));
47322
+ if (roots.length === 0) throw new Error("Frame graph has no root frame");
47323
+ const visiting = /* @__PURE__ */ new Set();
47324
+ const visited = /* @__PURE__ */ new Set();
47325
+ const resolveJointValue = (joint2) => {
47326
+ if (joint2.type === "fixed") {
47327
+ jointValues[joint2.name] = 0;
47328
+ return 0;
47329
+ }
47330
+ const hasStateValue = Object.prototype.hasOwnProperty.call(state, joint2.name);
47331
+ let raw = joint2.defaultValue;
47332
+ if (joint2.control === false) {
47333
+ if (hasStateValue) {
47334
+ warnings.push(`Frame joint "${joint2.name}" state override ignored because control is false`);
47335
+ }
47336
+ } else {
47337
+ raw = state[joint2.name] ?? joint2.defaultValue;
47338
+ }
47339
+ const finite = Number.isFinite(raw) ? raw : joint2.defaultValue;
47340
+ let clamped = finite;
47341
+ if (joint2.min != null) clamped = Math.max(joint2.min, clamped);
47342
+ if (joint2.max != null) clamped = Math.min(joint2.max, clamped);
47343
+ if (clamped !== raw) {
47344
+ warnings.push(`Frame joint "${joint2.name}" clamped from ${raw} to ${clamped}${joint2.unit ?? ""}`);
47345
+ }
47346
+ jointValues[joint2.name] = clamped;
47347
+ return clamped;
47348
+ };
47349
+ const visit = (frameName, worldTransform) => {
47350
+ if (visiting.has(frameName)) throw new Error(`Frame joint cycle detected at "${frameName}"`);
47351
+ const frame = this.frames.get(frameName);
47352
+ if (!frame) throw new Error(`Unknown frame "${frameName}"`);
47353
+ visiting.add(frameName);
47354
+ transforms.set(frameName, worldTransform);
47355
+ frames.set(frameName, frameFromTransform(frame, worldTransform));
47356
+ visited.add(frameName);
47357
+ for (const joint2 of jointsByParent.get(frameName) ?? []) {
47358
+ const value = resolveJointValue(joint2);
47359
+ const childWorld = composeChain(joint2.rest, frameJointMotionTransform(joint2, value), worldTransform);
47360
+ visit(joint2.child, childWorld);
47361
+ }
47362
+ visiting.delete(frameName);
47363
+ };
47364
+ for (const rootName of roots) {
47365
+ const root = this.frames.get(rootName);
47366
+ visit(rootName, frameDefTransform(root));
47367
+ }
47368
+ if (visited.size !== this.frames.size) {
47369
+ const missing = [...this.frames.keys()].filter((name) => !visited.has(name));
47370
+ throw new Error(`Frame graph unresolved for frames: ${missing.join(", ")}`);
47371
+ }
47372
+ return { frames, transforms, jointValues, warnings };
47373
+ }
47374
+ solveFrameEdges(frames) {
47375
+ const solved = [];
47376
+ for (const edge of this.frameEdges.values()) {
47377
+ const a2 = frames.get(edge.a);
47378
+ const b = frames.get(edge.b);
47379
+ if (!a2) throw new Error(`Frame edge "${edge.name}" references unresolved frame "${edge.a}"`);
47380
+ if (!b) throw new Error(`Frame edge "${edge.name}" references unresolved frame "${edge.b}"`);
47381
+ const start = cloneVec3$3(a2.origin);
47382
+ const end = cloneVec3$3(b.origin);
47383
+ solved.push({
47384
+ ...cloneAssemblyFrameEdge(edge),
47385
+ start,
47386
+ end,
47387
+ solvedLength: vecDistance(start, end)
47388
+ });
47389
+ }
47390
+ return solved;
47391
+ }
47392
+ attachFrameEdgesToKinematics(metadata, frameEdges) {
47393
+ if (frameEdges.length === 0) return metadata;
47394
+ if (metadata) return { ...metadata, frameEdges };
47395
+ return {
47396
+ links: [],
47397
+ edges: [],
47398
+ angles: [],
47399
+ derivedLinks: [],
47400
+ frameEdges,
47401
+ controls: {},
47402
+ floatingComponents: [],
47403
+ diagnostics: [],
47404
+ maxResidual: 0,
47405
+ converged: true
47406
+ };
47407
+ }
46892
47408
  solveKinematicLinks(state) {
47409
+ if (this.links.size === 0) {
47410
+ return { metadata: null, linkPositions: /* @__PURE__ */ new Map(), warnings: [] };
47411
+ }
47412
+ const plane = this.buildKinematicWorkplane();
47413
+ if (!plane) {
47414
+ return this.solveKinematicLinksLegacy(state);
47415
+ }
47416
+ return this.solveKinematicLinksPlanar(state, plane);
47417
+ }
47418
+ buildKinematicWorkplane() {
47419
+ const derivedNames = new Set(this.derivedLinks.keys());
47420
+ const primaryLinks = [...this.links.values()].filter((link) => !derivedNames.has(link.name));
47421
+ const originLink = primaryLinks.find((link) => link.fixed) ?? primaryLinks[0];
47422
+ const origin = originLink ? cloneVec3$3(originLink.at) : [0, 0, 0];
47423
+ let normal = null;
47424
+ for (const angle of this.linkAngles.values()) {
47425
+ const b = this.links.get(angle.b).at;
47426
+ const c2 = this.links.get(angle.c).at;
47427
+ const reference = angle.reference ? angle.reference.direction : angle.a ? vecSub(this.links.get(angle.a).at, b) : null;
47428
+ if (!reference) continue;
47429
+ const n = vecCross(reference, vecSub(c2, b));
47430
+ if (vecLength(n) < 1e-9) continue;
47431
+ const unit = vecNormalize(n);
47432
+ if (!normal) {
47433
+ normal = unit;
47434
+ } else if (Math.abs(vecDot(normal, unit)) < 0.999) {
47435
+ return null;
47436
+ }
47437
+ }
47438
+ normal ?? (normal = [0, 0, 1]);
47439
+ const projectDir = (v) => {
47440
+ const d2 = vecDot(v, normal);
47441
+ const projected = [v[0] - normal[0] * d2, v[1] - normal[1] * d2, v[2] - normal[2] * d2];
47442
+ const len2 = vecLength(projected);
47443
+ return len2 > 1e-9 ? [projected[0] / len2, projected[1] / len2, projected[2] / len2] : null;
47444
+ };
47445
+ let xAxis = null;
47446
+ const fixed = primaryLinks.filter((link) => link.fixed);
47447
+ for (let i = 0; i < fixed.length && !xAxis; i++) {
47448
+ for (let j = i + 1; j < fixed.length && !xAxis; j++) {
47449
+ xAxis = projectDir(vecSub(fixed[j].at, fixed[i].at));
47450
+ }
47451
+ }
47452
+ for (const edge of this.linkEdges.values()) {
47453
+ if (xAxis) break;
47454
+ if (derivedNames.has(edge.a) || derivedNames.has(edge.b)) continue;
47455
+ const a2 = this.links.get(edge.a);
47456
+ const b = this.links.get(edge.b);
47457
+ if (a2 && b) xAxis = projectDir(vecSub(b.at, a2.at));
47458
+ }
47459
+ for (const link of primaryLinks) {
47460
+ if (xAxis) break;
47461
+ xAxis = projectDir(vecSub(link.at, origin));
47462
+ }
47463
+ xAxis ?? (xAxis = projectDir(Math.abs(normal[0]) < 0.9 ? [1, 0, 0] : [0, 1, 0]) ?? [1, 0, 0]);
47464
+ const yAxis = vecNormalize(vecCross(normal, xAxis));
47465
+ for (const link of primaryLinks) {
47466
+ const offPlane = Math.abs(vecDot(vecSub(link.at, origin), normal));
47467
+ if (offPlane > 1e-6) return null;
47468
+ }
47469
+ return { origin, xAxis, yAxis, normal };
47470
+ }
47471
+ solveKinematicLinksPlanar(state, plane) {
47472
+ var _a3, _b3, _c2, _d2, _e2, _f2, _g2;
47473
+ const derivedNames = new Set(this.derivedLinks.keys());
47474
+ const primaryLinks = [...this.links.values()].filter((link) => !derivedNames.has(link.name));
47475
+ const controls = {};
47476
+ const diagnosticSet = /* @__PURE__ */ new Set();
47477
+ const diagnostics = [];
47478
+ const addDiagnostic = (message) => {
47479
+ if (diagnosticSet.has(message)) return;
47480
+ diagnosticSet.add(message);
47481
+ diagnostics.push(message);
47482
+ };
47483
+ const project = (p2) => {
47484
+ const rel = vecSub(p2, plane.origin);
47485
+ return [vecDot(rel, plane.xAxis), vecDot(rel, plane.yAxis)];
47486
+ };
47487
+ const unproject = (p2) => [
47488
+ plane.origin[0] + plane.xAxis[0] * p2[0] + plane.yAxis[0] * p2[1],
47489
+ plane.origin[1] + plane.xAxis[1] * p2[0] + plane.yAxis[1] * p2[1],
47490
+ plane.origin[2] + plane.xAxis[2] * p2[0] + plane.yAxis[2] * p2[1]
47491
+ ];
47492
+ const unprojectVector = (v) => [
47493
+ plane.xAxis[0] * v[0] + plane.yAxis[0] * v[1],
47494
+ plane.xAxis[1] * v[0] + plane.yAxis[1] * v[1],
47495
+ plane.xAxis[2] * v[0] + plane.yAxis[2] * v[1]
47496
+ ];
47497
+ const projectVector = (v) => [vecDot(v, plane.xAxis), vecDot(v, plane.yAxis)];
47498
+ const initialPositions2d = /* @__PURE__ */ new Map();
47499
+ for (const link of primaryLinks) initialPositions2d.set(link.name, project(link.at));
47500
+ const neighbors = /* @__PURE__ */ new Map();
47501
+ for (const link of primaryLinks) neighbors.set(link.name, /* @__PURE__ */ new Set());
47502
+ for (const edge of this.linkEdges.values()) {
47503
+ if (derivedNames.has(edge.a) || derivedNames.has(edge.b)) continue;
47504
+ (_a3 = neighbors.get(edge.a)) == null ? void 0 : _a3.add(edge.b);
47505
+ (_b3 = neighbors.get(edge.b)) == null ? void 0 : _b3.add(edge.a);
47506
+ }
47507
+ for (const angle of this.linkAngles.values()) {
47508
+ if (angle.a) {
47509
+ (_c2 = neighbors.get(angle.a)) == null ? void 0 : _c2.add(angle.b);
47510
+ (_d2 = neighbors.get(angle.b)) == null ? void 0 : _d2.add(angle.a);
47511
+ }
47512
+ (_e2 = neighbors.get(angle.b)) == null ? void 0 : _e2.add(angle.c);
47513
+ (_f2 = neighbors.get(angle.c)) == null ? void 0 : _f2.add(angle.b);
47514
+ }
47515
+ const floatingComponents = [];
47516
+ const gaugeFixed = /* @__PURE__ */ new Set();
47517
+ const seen = /* @__PURE__ */ new Set();
47518
+ for (const link of primaryLinks.map((link2) => link2.name)) {
47519
+ if (seen.has(link)) continue;
47520
+ const stack = [link];
47521
+ const component = [];
47522
+ seen.add(link);
47523
+ while (stack.length > 0) {
47524
+ const next = stack.pop();
47525
+ component.push(next);
47526
+ for (const neighbor of neighbors.get(next) ?? []) {
47527
+ if (seen.has(neighbor)) continue;
47528
+ seen.add(neighbor);
47529
+ stack.push(neighbor);
47530
+ }
47531
+ }
47532
+ if (!component.some((name) => {
47533
+ var _a4;
47534
+ return (_a4 = this.links.get(name)) == null ? void 0 : _a4.fixed;
47535
+ })) {
47536
+ const sorted = [...component].sort();
47537
+ const gaugeLink = sorted[0];
47538
+ gaugeFixed.add(gaugeLink);
47539
+ floatingComponents.push({ links: sorted, gaugeLink });
47540
+ }
47541
+ }
47542
+ const angleValueForState = (angle, inputState, recordControl) => {
47543
+ var _a4, _b4, _c3, _d3;
47544
+ if (!angle.control && angle.target === void 0) return null;
47545
+ const raw = inputState[angle.name] ?? angle.target ?? ((_a4 = angle.control) == null ? void 0 : _a4.default) ?? 0;
47546
+ let value = Number.isFinite(raw) ? raw : ((_b4 = angle.control) == null ? void 0 : _b4.default) ?? angle.target ?? 0;
47547
+ const min2 = ((_c3 = angle.control) == null ? void 0 : _c3.min) ?? angle.min;
47548
+ const max2 = ((_d3 = angle.control) == null ? void 0 : _d3.max) ?? angle.max;
47549
+ if (min2 != null) value = Math.max(min2, value);
47550
+ if (max2 != null) value = Math.min(max2, value);
47551
+ if (recordControl) controls[angle.name] = value;
47552
+ return value;
47553
+ };
47554
+ const controlledAngleValue = (angle) => angleValueForState(angle, state, true);
47555
+ const edgeLengthBetween = (x2, y2) => {
47556
+ for (const edge of this.linkEdges.values()) {
47557
+ if (edge.visualOnly || edge.length == null) continue;
47558
+ if (edge.a === x2 && edge.b === y2 || edge.a === y2 && edge.b === x2) return edge.length;
47559
+ }
47560
+ return null;
47561
+ };
47562
+ const seedDirection = (index2) => {
47563
+ const angle = index2 * Math.PI * (3 - Math.sqrt(5));
47564
+ return [Math.cos(angle), Math.sin(angle)];
47565
+ };
47566
+ const rotate2d = (v, angleDeg) => {
47567
+ const rad = angleDeg * Math.PI / 180;
47568
+ const c2 = Math.cos(rad);
47569
+ const s = Math.sin(rad);
47570
+ return [v[0] * c2 - v[1] * s, v[0] * s + v[1] * c2];
47571
+ };
47572
+ const angleReferenceVector2d = (angle, seedPositions) => {
47573
+ if (angle.reference) return projectVector(angle.reference.direction);
47574
+ if (!angle.a) return null;
47575
+ const a2 = seedPositions.get(angle.a);
47576
+ const b = seedPositions.get(angle.b);
47577
+ if (!a2 || !b) return null;
47578
+ return [a2[0] - b[0], a2[1] - b[1]];
47579
+ };
47580
+ const angleReferenceLabel = (angle) => angle.reference ? `world direction [${angle.reference.direction.join(", ")}]` : `reference links "${angle.a}" and "${angle.b}"`;
47581
+ const signedArea2d2 = (a2, b, p2) => (b[0] - a2[0]) * (p2[1] - a2[1]) - (b[1] - a2[1]) * (p2[0] - a2[0]);
47582
+ const circleCircleCandidates = (a2, radiusA, b, radiusB) => {
47583
+ const dx = b[0] - a2[0];
47584
+ const dy = b[1] - a2[1];
47585
+ const d2 = Math.hypot(dx, dy);
47586
+ if (d2 <= 1e-10) return [];
47587
+ const along = (radiusA * radiusA - radiusB * radiusB + d2 * d2) / (2 * d2);
47588
+ const h2 = radiusA * radiusA - along * along;
47589
+ if (h2 < -1e-7) return [];
47590
+ const h = Math.sqrt(Math.max(0, h2));
47591
+ const ux = dx / d2;
47592
+ const uy = dy / d2;
47593
+ const base = [a2[0] + ux * along, a2[1] + uy * along];
47594
+ const offset2 = [-uy * h, ux * h];
47595
+ return [
47596
+ [base[0] + offset2[0], base[1] + offset2[1]],
47597
+ [base[0] - offset2[0], base[1] - offset2[1]]
47598
+ ];
47599
+ };
47600
+ const chooseCandidate = (targetName, anchors, candidates, seedPositions, branchReference2) => {
47601
+ if (candidates.length === 0) return null;
47602
+ const anchorAName = anchors[0].name;
47603
+ const anchorBName = anchors[1].name;
47604
+ const anchorA = seedPositions.get(anchorAName);
47605
+ const anchorB = seedPositions.get(anchorBName);
47606
+ let referenceSign = 0;
47607
+ if (branchReference2) {
47608
+ const refA = branchReference2.get(anchorAName);
47609
+ const refB = branchReference2.get(anchorBName);
47610
+ const refTarget = branchReference2.get(targetName);
47611
+ if (refA && refB && refTarget) {
47612
+ const area2 = signedArea2d2(refA, refB, refTarget);
47613
+ if (Math.abs(area2) > 1e-7) referenceSign = Math.sign(area2);
47614
+ }
47615
+ }
47616
+ const authored = initialPositions2d.get(targetName) ?? [0, 0];
47617
+ let best = null;
47618
+ let bestScore = Number.POSITIVE_INFINITY;
47619
+ for (const candidate of candidates) {
47620
+ const candidateSign = Math.sign(signedArea2d2(anchorA, anchorB, candidate));
47621
+ const branchPenalty = referenceSign !== 0 && candidateSign !== referenceSign ? 1e12 : 0;
47622
+ const authoredDistance = Math.hypot(candidate[0] - authored[0], candidate[1] - authored[1]);
47623
+ const constraintResidual = anchors.reduce((sum2, anchor) => {
47624
+ const p2 = seedPositions.get(anchor.name);
47625
+ return sum2 + Math.abs(Math.hypot(candidate[0] - p2[0], candidate[1] - p2[1]) - anchor.length);
47626
+ }, 0);
47627
+ const score = branchPenalty + constraintResidual * 1e6 + authoredDistance;
47628
+ if (score < bestScore) {
47629
+ bestScore = score;
47630
+ best = candidate;
47631
+ }
47632
+ }
47633
+ return best;
47634
+ };
47635
+ const buildSeedPositions = (inputState, branchReference2, diagnosticPrefix = "") => {
47636
+ const seedPositions = new Map(initialPositions2d);
47637
+ const seeded = /* @__PURE__ */ new Set();
47638
+ const angleDriven = /* @__PURE__ */ new Set();
47639
+ const isLocked = (name) => {
47640
+ var _a4;
47641
+ return ((_a4 = this.links.get(name)) == null ? void 0 : _a4.fixed) === true || gaugeFixed.has(name);
47642
+ };
47643
+ for (const link of primaryLinks) {
47644
+ const p2 = seedPositions.get(link.name);
47645
+ if (link.fixed || gaugeFixed.has(link.name) || Math.hypot(p2[0], p2[1]) > 1e-9) seeded.add(link.name);
47646
+ }
47647
+ if (seeded.size === 0 && primaryLinks.length > 0) seeded.add(primaryLinks[0].name);
47648
+ let seedIndex = 1;
47649
+ let changed = true;
47650
+ for (let pass = 0; pass < primaryLinks.length * 3 && changed; pass++) {
47651
+ changed = false;
47652
+ for (const angle of this.linkAngles.values()) {
47653
+ const target = angleValueForState(angle, inputState, false);
47654
+ const radius = edgeLengthBetween(angle.b, angle.c);
47655
+ const hasReference = !!angle.reference || angle.a !== void 0 && seeded.has(angle.a);
47656
+ if (target == null || radius == null || isLocked(angle.c) || !hasReference || !seeded.has(angle.b)) continue;
47657
+ const b = seedPositions.get(angle.b);
47658
+ const ref = angleReferenceVector2d(angle, seedPositions);
47659
+ if (!ref) continue;
47660
+ const refLen = Math.hypot(ref[0], ref[1]);
47661
+ if (refLen <= 1e-10) {
47662
+ addDiagnostic(
47663
+ `${diagnosticPrefix}Angle "${angle.name}" cannot drive link "${angle.c}" because ${angleReferenceLabel(angle)} has no usable direction in the mechanism plane.`
47664
+ );
47665
+ continue;
47666
+ }
47667
+ const dir = rotate2d([ref[0] / refLen, ref[1] / refLen], target);
47668
+ seedPositions.set(angle.c, [b[0] + dir[0] * radius, b[1] + dir[1] * radius]);
47669
+ seeded.add(angle.c);
47670
+ angleDriven.add(angle.c);
47671
+ changed = true;
47672
+ }
47673
+ for (const link of primaryLinks) {
47674
+ if (isLocked(link.name) || angleDriven.has(link.name)) continue;
47675
+ const anchors = [];
47676
+ for (const edge of this.linkEdges.values()) {
47677
+ if (edge.visualOnly || edge.length == null || derivedNames.has(edge.a) || derivedNames.has(edge.b)) continue;
47678
+ if (edge.a === link.name && seeded.has(edge.b)) anchors.push({ name: edge.b, length: edge.length });
47679
+ if (edge.b === link.name && seeded.has(edge.a)) anchors.push({ name: edge.a, length: edge.length });
47680
+ }
47681
+ if (anchors.length < 2) continue;
47682
+ const a2 = anchors[0];
47683
+ const b = anchors[1];
47684
+ const candidates = circleCircleCandidates(seedPositions.get(a2.name), a2.length, seedPositions.get(b.name), b.length);
47685
+ const chosen = chooseCandidate(link.name, anchors, candidates, seedPositions, branchReference2);
47686
+ if (!chosen) {
47687
+ const pa = seedPositions.get(a2.name);
47688
+ const pb = seedPositions.get(b.name);
47689
+ const centerDistance = Math.hypot(pb[0] - pa[0], pb[1] - pa[1]);
47690
+ const minDistance = Math.abs(a2.length - b.length);
47691
+ const maxDistance = a2.length + b.length;
47692
+ addDiagnostic(
47693
+ `${diagnosticPrefix}Link "${link.name}" cannot be placed from anchors "${a2.name}" and "${b.name}": distance between anchors is ${centerDistance.toFixed(6)}, but required circle intersection range is ${minDistance.toFixed(6)}..${maxDistance.toFixed(6)}. This control state is outside the selected assembly mode, or the model needs more initial link positions to select a different mode.`
47694
+ );
47695
+ continue;
47696
+ }
47697
+ seedPositions.set(link.name, chosen);
47698
+ seeded.add(link.name);
47699
+ changed = true;
47700
+ }
47701
+ for (const edge of this.linkEdges.values()) {
47702
+ if (edge.visualOnly || edge.length == null || derivedNames.has(edge.a) || derivedNames.has(edge.b)) continue;
47703
+ const aSeeded = seeded.has(edge.a);
47704
+ const bSeeded = seeded.has(edge.b);
47705
+ if (aSeeded === bSeeded) continue;
47706
+ const anchorName = aSeeded ? edge.a : edge.b;
47707
+ const targetName = aSeeded ? edge.b : edge.a;
47708
+ if (isLocked(targetName) || angleDriven.has(targetName)) continue;
47709
+ const anchor = seedPositions.get(anchorName);
47710
+ const current = seedPositions.get(targetName);
47711
+ const delta = [current[0] - anchor[0], current[1] - anchor[1]];
47712
+ const deltaLen = Math.hypot(delta[0], delta[1]);
47713
+ const dir = deltaLen > 1e-9 ? [delta[0] / deltaLen, delta[1] / deltaLen] : seedDirection(seedIndex++);
47714
+ seedPositions.set(targetName, [anchor[0] + dir[0] * edge.length, anchor[1] + dir[1] * edge.length]);
47715
+ seeded.add(targetName);
47716
+ changed = true;
47717
+ }
47718
+ }
47719
+ const refLength = Math.max(1, ...[...this.linkEdges.values()].map((edge) => edge.length ?? 0));
47720
+ for (const link of primaryLinks) {
47721
+ if (seeded.has(link.name)) continue;
47722
+ if (this.linkEdges.size > 0 || this.linkAngles.size > 0) {
47723
+ addDiagnostic(
47724
+ `${diagnosticPrefix}Link "${link.name}" could not be constructed from the current kinematic graph. Give it an initial position, add a structural distance to an already-placed link, or add a controlling angle.`
47725
+ );
47726
+ } else {
47727
+ const dir = seedDirection(seedIndex++);
47728
+ seedPositions.set(link.name, [dir[0] * refLength, dir[1] * refLength]);
47729
+ seeded.add(link.name);
47730
+ }
47731
+ }
47732
+ return seedPositions;
47733
+ };
47734
+ const defaultControlState = {};
47735
+ let hasDefaultControlState = false;
47736
+ for (const angle of this.linkAngles.values()) {
47737
+ if (!angle.control && angle.target === void 0) continue;
47738
+ const value = ((_g2 = angle.control) == null ? void 0 : _g2.default) ?? angle.target;
47739
+ if (Number.isFinite(value)) {
47740
+ defaultControlState[angle.name] = value;
47741
+ hasDefaultControlState = true;
47742
+ }
47743
+ }
47744
+ const branchReference = hasDefaultControlState ? buildSeedPositions(defaultControlState, void 0, "Default assembly mode: ") : void 0;
47745
+ const positions2d = buildSeedPositions(state, branchReference);
47746
+ const positions = /* @__PURE__ */ new Map();
47747
+ for (const link of primaryLinks) {
47748
+ const point2 = positions2d.get(link.name);
47749
+ if (!point2) {
47750
+ addDiagnostic(`Link "${link.name}" has no solved planar position after kinematic construction.`);
47751
+ continue;
47752
+ }
47753
+ positions.set(link.name, unproject(point2));
47754
+ }
47755
+ const solvedDerivedLinks = this.evaluateDerivedLinks(positions);
47756
+ for (const derived of solvedDerivedLinks) positions.set(derived.name, cloneVec3$3(derived.position));
47757
+ const solvedEdges = [];
47758
+ const solvedAngles = [];
47759
+ let maxResidual = diagnostics.length > 0 ? Number.POSITIVE_INFINITY : 0;
47760
+ for (const edge of this.linkEdges.values()) {
47761
+ const edgeA = positions.get(edge.a);
47762
+ const edgeB = positions.get(edge.b);
47763
+ const solvedLength = edgeA && edgeB ? vecDistance(edgeA, edgeB) : Number.NaN;
47764
+ let residual = 0;
47765
+ if (!edgeA || !edgeB) {
47766
+ residual = Number.POSITIVE_INFINITY;
47767
+ addDiagnostic(`Edge "${edge.name}" could not be evaluated because one of its links has no solved position.`);
47768
+ } else {
47769
+ if (edge.length != null) residual = solvedLength - edge.length;
47770
+ if (edge.min != null && solvedLength < edge.min) residual = Math.min(residual, solvedLength - edge.min);
47771
+ if (edge.max != null && solvedLength > edge.max) residual = Math.max(residual, solvedLength - edge.max);
47772
+ }
47773
+ maxResidual = Math.max(maxResidual, Math.abs(residual));
47774
+ solvedEdges.push({
47775
+ name: edge.name,
47776
+ a: edge.a,
47777
+ b: edge.b,
47778
+ length: edge.length,
47779
+ solvedLength,
47780
+ residual,
47781
+ visualOnly: edge.visualOnly
47782
+ });
47783
+ }
47784
+ for (const angle of this.linkAngles.values()) {
47785
+ const angleB = positions.get(angle.b);
47786
+ const angleC = positions.get(angle.c);
47787
+ const reference = angleReferenceVector2d(angle, positions2d);
47788
+ const solvedValue = reference && angleB && angleC ? signedAngleBetweenVectorsAboutAxis(unprojectVector(reference), vecSub(angleC, angleB), plane.normal) : Number.NaN;
47789
+ const target = controlledAngleValue(angle) ?? angle.target;
47790
+ let residual = target == null ? 0 : normalizeAngleDeltaDeg(solvedValue - target);
47791
+ if (!reference || !angleB || !angleC) {
47792
+ residual = Number.POSITIVE_INFINITY;
47793
+ addDiagnostic(`Angle "${angle.name}" could not be evaluated because its reference or one of its links has no solved position.`);
47794
+ } else {
47795
+ if (angle.min != null && solvedValue < angle.min) residual = Math.min(residual, solvedValue - angle.min);
47796
+ if (angle.max != null && solvedValue > angle.max) residual = Math.max(residual, solvedValue - angle.max);
47797
+ }
47798
+ maxResidual = Math.max(maxResidual, Math.abs(residual));
47799
+ solvedAngles.push({
47800
+ name: angle.name,
47801
+ a: angle.a,
47802
+ b: angle.b,
47803
+ c: angle.c,
47804
+ reference: cloneAngleReference(angle.reference),
47805
+ target,
47806
+ solvedValue,
47807
+ residual,
47808
+ control: angle.control ? { ...angle.control } : void 0
47809
+ });
47810
+ }
47811
+ const warnings = [];
47812
+ for (const component of floatingComponents) {
47813
+ warnings.push(`Kinematic component is floating; using link "${component.gaugeLink}" as a numerical display gauge`);
47814
+ }
47815
+ if (maxResidual > 1e-3) {
47816
+ warnings.push(`Kinematic graph residual ${maxResidual.toFixed(6)} exceeds tolerance`);
47817
+ }
47818
+ return {
47819
+ metadata: {
47820
+ links: [...this.links.values()].map((link) => ({
47821
+ name: link.name,
47822
+ position: cloneVec3$3(positions.get(link.name) ?? link.at),
47823
+ fixed: link.fixed,
47824
+ gaugeFixed: gaugeFixed.has(link.name) || void 0
47825
+ })),
47826
+ edges: solvedEdges,
47827
+ angles: solvedAngles,
47828
+ derivedLinks: solvedDerivedLinks,
47829
+ controls,
47830
+ floatingComponents,
47831
+ diagnostics,
47832
+ maxResidual,
47833
+ converged: diagnostics.length === 0 && maxResidual <= 1e-3
47834
+ },
47835
+ linkPositions: positions,
47836
+ warnings
47837
+ };
47838
+ }
47839
+ evaluateDerivedLinks(basePositions) {
47840
+ const output = /* @__PURE__ */ new Map();
47841
+ const visiting = /* @__PURE__ */ new Set();
47842
+ const resolve = (name) => {
47843
+ const existing = basePositions.get(name);
47844
+ if (existing && !this.derivedLinks.has(name)) return existing;
47845
+ const cached = output.get(name);
47846
+ if (cached) return cached.position;
47847
+ const derived = this.derivedLinks.get(name);
47848
+ if (!derived) {
47849
+ const position2 = basePositions.get(name);
47850
+ if (!position2) throw new Error(`Derived link references unknown solved link "${name}"`);
47851
+ return position2;
47852
+ }
47853
+ if (visiting.has(name)) throw new Error(`Derived link cycle detected at "${name}"`);
47854
+ visiting.add(name);
47855
+ const from = resolve(derived.fromLink);
47856
+ const reference = resolve(derived.referenceLink);
47857
+ const rawDir = derived.direction === "toward" ? vecSub(reference, from) : vecSub(from, reference);
47858
+ const dir = vecNormalize(rawDir);
47859
+ if (vecLength(dir) < 1e-10) {
47860
+ throw new Error(`Derived link "${name}" cannot be solved because "${derived.fromLink}" and "${derived.referenceLink}" coincide`);
47861
+ }
47862
+ const position = [
47863
+ from[0] + dir[0] * derived.distance,
47864
+ from[1] + dir[1] * derived.distance,
47865
+ from[2] + dir[2] * derived.distance
47866
+ ];
47867
+ visiting.delete(name);
47868
+ const solved = { ...derived, position };
47869
+ output.set(name, solved);
47870
+ return position;
47871
+ };
47872
+ for (const name of this.derivedLinks.keys()) resolve(name);
47873
+ return [...output.values()];
47874
+ }
47875
+ solveKinematicLinksLegacy(state) {
46893
47876
  var _a3, _b3, _c2, _d2, _e2, _f2;
46894
47877
  if (this.links.size === 0) {
46895
47878
  return { metadata: null, linkPositions: /* @__PURE__ */ new Map(), warnings: [] };
@@ -46905,8 +47888,10 @@ class Assembly {
46905
47888
  (_b3 = neighbors.get(edge.b)) == null ? void 0 : _b3.add(edge.a);
46906
47889
  }
46907
47890
  for (const angle of this.linkAngles.values()) {
46908
- (_c2 = neighbors.get(angle.a)) == null ? void 0 : _c2.add(angle.b);
46909
- (_d2 = neighbors.get(angle.b)) == null ? void 0 : _d2.add(angle.a);
47891
+ if (angle.a) {
47892
+ (_c2 = neighbors.get(angle.a)) == null ? void 0 : _c2.add(angle.b);
47893
+ (_d2 = neighbors.get(angle.b)) == null ? void 0 : _d2.add(angle.a);
47894
+ }
46910
47895
  (_e2 = neighbors.get(angle.b)) == null ? void 0 : _e2.add(angle.c);
46911
47896
  (_f2 = neighbors.get(angle.c)) == null ? void 0 : _f2.add(angle.b);
46912
47897
  }
@@ -46961,27 +47946,36 @@ class Assembly {
46961
47946
  const link = this.links.get(name);
46962
47947
  return !link.fixed && !gaugeFixed.has(name) && !controlledLinks.has(name);
46963
47948
  };
47949
+ const angleReferenceVector = (angle, source) => {
47950
+ if (angle.reference) return cloneVec3$3(angle.reference.direction);
47951
+ if (!angle.a) return null;
47952
+ const a2 = source.get(angle.a);
47953
+ const b = source.get(angle.b);
47954
+ if (!a2 || !b) return null;
47955
+ return vecSub(a2, b);
47956
+ };
46964
47957
  const anglePlaneNormal = (angle) => {
46965
- const a2 = this.links.get(angle.a).at;
46966
47958
  const b = this.links.get(angle.b).at;
46967
47959
  const c2 = this.links.get(angle.c).at;
46968
- const n = vecCross(vecSub(a2, b), vecSub(c2, b));
47960
+ const reference = angle.reference ? angle.reference.direction : angle.a ? vecSub(this.links.get(angle.a).at, b) : null;
47961
+ if (!reference) return [0, 0, 1];
47962
+ const n = vecCross(reference, vecSub(c2, b));
46969
47963
  return vecLength(n) < 1e-9 ? [0, 0, 1] : vecNormalize(n);
46970
47964
  };
46971
47965
  const applyAngleControls = () => {
46972
47966
  for (const angle of this.linkAngles.values()) {
46973
47967
  const target = controlledAngleValue(angle);
46974
47968
  if (target == null) continue;
46975
- const a2 = positions.get(angle.a);
46976
47969
  const b = positions.get(angle.b);
46977
47970
  const c2 = positions.get(angle.c);
46978
47971
  if (!isMovable(angle.c) && !controlledLinks.has(angle.c)) continue;
46979
47972
  const radius = edgeLengthBetween(angle.b, angle.c) ?? vecDistance(b, c2);
46980
47973
  if (radius <= 1e-10) continue;
46981
- const ba = vecSub(a2, b);
46982
- const len2 = vecLength(ba);
47974
+ const reference = angleReferenceVector(angle, positions);
47975
+ if (!reference) continue;
47976
+ const len2 = vecLength(reference);
46983
47977
  if (len2 <= 1e-10) continue;
46984
- const baUnit = [ba[0] / len2, ba[1] / len2, ba[2] / len2];
47978
+ const baUnit = [reference[0] / len2, reference[1] / len2, reference[2] / len2];
46985
47979
  const rotated = rotateAboutAxis(baUnit, anglePlaneNormal(angle), target);
46986
47980
  positions.set(angle.c, [b[0] + rotated[0] * radius, b[1] + rotated[1] * radius, b[2] + rotated[2] * radius]);
46987
47981
  controlledLinks.add(angle.c);
@@ -47011,6 +48005,8 @@ class Assembly {
47011
48005
  }
47012
48006
  controlledLinks.clear();
47013
48007
  applyAngleControls();
48008
+ const solvedDerivedLinks = this.evaluateDerivedLinks(positions);
48009
+ for (const derived of solvedDerivedLinks) positions.set(derived.name, cloneVec3$3(derived.position));
47014
48010
  const solvedEdges = [];
47015
48011
  const solvedAngles = [];
47016
48012
  let maxResidual = 0;
@@ -47032,14 +48028,13 @@ class Assembly {
47032
48028
  });
47033
48029
  }
47034
48030
  for (const angle of this.linkAngles.values()) {
47035
- const solvedValue = signedAngleAboutAxis(
47036
- positions.get(angle.a),
47037
- positions.get(angle.b),
47038
- positions.get(angle.c),
47039
- anglePlaneNormal(angle)
47040
- );
48031
+ const reference = angleReferenceVector(angle, positions);
48032
+ const b = positions.get(angle.b);
48033
+ const c2 = positions.get(angle.c);
48034
+ const solvedValue = reference ? signedAngleBetweenVectorsAboutAxis(reference, vecSub(c2, b), anglePlaneNormal(angle)) : Number.NaN;
47041
48035
  const target = controlledAngleValue(angle) ?? angle.target;
47042
48036
  let residual = target == null ? 0 : normalizeAngleDeltaDeg(solvedValue - target);
48037
+ if (!reference) residual = Number.POSITIVE_INFINITY;
47043
48038
  if (angle.min != null && solvedValue < angle.min) residual = Math.min(residual, solvedValue - angle.min);
47044
48039
  if (angle.max != null && solvedValue > angle.max) residual = Math.max(residual, solvedValue - angle.max);
47045
48040
  maxResidual = Math.max(maxResidual, Math.abs(residual));
@@ -47048,6 +48043,7 @@ class Assembly {
47048
48043
  a: angle.a,
47049
48044
  b: angle.b,
47050
48045
  c: angle.c,
48046
+ reference: cloneAngleReference(angle.reference),
47051
48047
  target,
47052
48048
  solvedValue,
47053
48049
  residual,
@@ -47071,8 +48067,10 @@ class Assembly {
47071
48067
  })),
47072
48068
  edges: solvedEdges,
47073
48069
  angles: solvedAngles,
48070
+ derivedLinks: solvedDerivedLinks,
47074
48071
  controls,
47075
48072
  floatingComponents,
48073
+ diagnostics: [],
47076
48074
  maxResidual,
47077
48075
  converged: maxResidual <= 1e-3
47078
48076
  },
@@ -47120,8 +48118,12 @@ class Assembly {
47120
48118
  const incoming = /* @__PURE__ */ new Map();
47121
48119
  const jointsByParent = /* @__PURE__ */ new Map();
47122
48120
  const warnings = [];
48121
+ const frameSolve = this.solveFrames(state);
48122
+ warnings.push(...frameSolve.warnings);
47123
48123
  const kinematicSolve = this.solveKinematicLinks(state);
47124
48124
  warnings.push(...kinematicSolve.warnings);
48125
+ const solvedFrameEdges = this.solveFrameEdges(frameSolve.frames);
48126
+ const kinematicMetadata = this.attachFrameEdgesToKinematics(kinematicSolve.metadata, solvedFrameEdges);
47125
48127
  for (const joint2 of this.joints.values()) {
47126
48128
  if (incoming.has(joint2.child)) {
47127
48129
  throw new Error(`Part "${joint2.child}" has multiple parent joints`);
@@ -47131,8 +48133,9 @@ class Assembly {
47131
48133
  list.push(joint2);
47132
48134
  jointsByParent.set(joint2.parent, list);
47133
48135
  }
47134
- const roots = [...this.parts.keys()].filter((name) => !incoming.has(name));
47135
- if (roots.length === 0 && this.parts.size > 0) {
48136
+ const unboundPartNames = [...this.parts.values()].filter((part) => !part.bindToFrame).map((part) => part.name);
48137
+ const roots = unboundPartNames.filter((name) => !incoming.has(name));
48138
+ if (roots.length === 0 && unboundPartNames.length > 0) {
47136
48139
  throw new Error("Assembly has no root part (cyclic joint graph)");
47137
48140
  }
47138
48141
  const mateBaseOverrides = /* @__PURE__ */ new Map();
@@ -47235,7 +48238,7 @@ class Assembly {
47235
48238
  const world = /* @__PURE__ */ new Map();
47236
48239
  const visiting = /* @__PURE__ */ new Set();
47237
48240
  const visited = /* @__PURE__ */ new Set();
47238
- const jointValues = {};
48241
+ const jointValues = { ...frameSolve.jointValues };
47239
48242
  const resolvingJointValues = /* @__PURE__ */ new Set();
47240
48243
  const resolveJointValue = (jointName) => {
47241
48244
  const cached = jointValues[jointName];
@@ -47285,6 +48288,13 @@ class Assembly {
47285
48288
  const rootBase = mateBaseOverrides.get(rootName) ?? kinematicBaseForPart(root) ?? root.base;
47286
48289
  dfs(rootName, rootBase);
47287
48290
  }
48291
+ for (const rec of this.parts.values()) {
48292
+ if (!rec.bindToFrame) continue;
48293
+ const frameTransform2 = frameSolve.transforms.get(rec.bindToFrame);
48294
+ if (!frameTransform2) throw new Error(`Part "${rec.name}" references unresolved frame "${rec.bindToFrame}"`);
48295
+ world.set(rec.name, composeChain(rec.base, frameTransform2));
48296
+ visited.add(rec.name);
48297
+ }
47288
48298
  if (visited.size !== this.parts.size) {
47289
48299
  const missing = [...this.parts.keys()].filter((name) => !visited.has(name));
47290
48300
  throw new Error(`Assembly graph unresolved for parts: ${missing.join(", ")}`);
@@ -47340,7 +48350,8 @@ class Assembly {
47340
48350
  jointValues,
47341
48351
  warnings,
47342
48352
  mateMetadata,
47343
- kinematicSolve.metadata,
48353
+ kinematicMetadata,
48354
+ frameSolve.frames,
47344
48355
  this._usedPortRefs
47345
48356
  );
47346
48357
  }
@@ -47453,7 +48464,7 @@ class Assembly {
47453
48464
  return collectJointsView(this.buildJointsViewOptions(options, state, false), null, "assembly");
47454
48465
  }
47455
48466
  buildJointsViewOptions(options, state, relativeLinkControls) {
47456
- var _a3, _b3, _c2, _d2;
48467
+ var _a3, _b3, _c2, _d2, _e2, _f2;
47457
48468
  const solved = this.solve(state);
47458
48469
  const def = this.describe();
47459
48470
  const joints = [];
@@ -47497,7 +48508,31 @@ class Assembly {
47497
48508
  }
47498
48509
  joints.push(entry);
47499
48510
  }
47500
- if (this.linkAngles.size > 0 && this.parts.size > 0) {
48511
+ for (const j of def.frameJoints) {
48512
+ if (j.type === "fixed" || j.control === false) continue;
48513
+ const parentWorld = solved.getFrame(j.parent);
48514
+ const pivot = parentWorld.point([0, 0, 0]);
48515
+ const axisWorld = parentWorld.vector([0, 0, 1]);
48516
+ const axisLen = Math.hypot(axisWorld[0], axisWorld[1], axisWorld[2]);
48517
+ const normalizedAxis = axisLen > 1e-10 ? [axisWorld[0] / axisLen, axisWorld[1] / axisLen, axisWorld[2] / axisLen] : [0, 0, 1];
48518
+ const entry = {
48519
+ name: j.name,
48520
+ child: j.child,
48521
+ parent: j.parent,
48522
+ type: j.type,
48523
+ axis: normalizedAxis,
48524
+ pivot,
48525
+ min: j.min,
48526
+ max: j.max,
48527
+ default: ((_c2 = options.defaults) == null ? void 0 : _c2[j.name]) ?? j.defaultValue,
48528
+ unit: j.unit
48529
+ };
48530
+ if ((_d2 = options.overrides) == null ? void 0 : _d2[j.name]) {
48531
+ Object.assign(entry, options.overrides[j.name]);
48532
+ }
48533
+ joints.push(entry);
48534
+ }
48535
+ if (this.linkAngles.size > 0) {
47501
48536
  const mateLinksOf = /* @__PURE__ */ new Map();
47502
48537
  for (const part of this.parts.values()) {
47503
48538
  if (part.mates.length > 0) mateLinksOf.set(part.name, new Set(part.mates.map((m2) => m2.toLink)));
@@ -47518,18 +48553,20 @@ class Assembly {
47518
48553
  for (const angle of this.linkAngles.values()) {
47519
48554
  if (angle.control) controlMoving.set(angle.c, angle);
47520
48555
  }
48556
+ const controlledAngleChild = (angle) => spanPart(angle.b, angle.c) ?? firstPartMatedTo(angle.c) ?? angle.c;
47521
48557
  const normalAtSolvedPose = (angle) => {
47522
- const a2 = solved.getLinkPosition(angle.a);
47523
48558
  const b = solved.getLinkPosition(angle.b);
47524
48559
  const c2 = solved.getLinkPosition(angle.c);
47525
- const n = vecCross(vecSub(a2, b), vecSub(c2, b));
48560
+ const reference = angle.reference ? angle.reference.direction : angle.a ? vecSub(solved.getLinkPosition(angle.a), b) : null;
48561
+ if (!reference) return [0, 0, 1];
48562
+ const n = vecCross(reference, vecSub(c2, b));
47526
48563
  return vecLength(n) < 1e-9 ? [0, 0, 1] : vecNormalize(n);
47527
48564
  };
47528
48565
  for (const angle of this.linkAngles.values()) {
47529
48566
  if (!angle.control) continue;
47530
- const child = spanPart(angle.b, angle.c) ?? firstPartMatedTo(angle.c) ?? angle.c;
48567
+ const child = controlledAngleChild(angle);
47531
48568
  const parentAngle = controlMoving.get(angle.b);
47532
- const parent = parentAngle ? spanPart(parentAngle.b, parentAngle.c) ?? firstPartMatedTo(parentAngle.c) : firstPartMatedTo(angle.b);
48569
+ const parent = parentAngle ? controlledAngleChild(parentAngle) : firstPartMatedTo(angle.b);
47533
48570
  const defValue = angle.control.default ?? angle.target ?? 0;
47534
48571
  const absMin = angle.control.min ?? angle.min;
47535
48572
  const absMax = angle.control.max ?? angle.max;
@@ -47542,10 +48579,10 @@ class Assembly {
47542
48579
  pivot: solved.getLinkPosition(angle.b),
47543
48580
  min: absMin !== void 0 && relativeLinkControls ? absMin - defValue : absMin,
47544
48581
  max: absMax !== void 0 && relativeLinkControls ? absMax - defValue : absMax,
47545
- default: relativeLinkControls ? 0 : ((_c2 = options.defaults) == null ? void 0 : _c2[angle.name]) ?? defValue,
48582
+ default: relativeLinkControls ? 0 : ((_e2 = options.defaults) == null ? void 0 : _e2[angle.name]) ?? defValue,
47546
48583
  unit: angle.control.unit
47547
48584
  };
47548
- if ((_d2 = options.overrides) == null ? void 0 : _d2[angle.name]) {
48585
+ if ((_f2 = options.overrides) == null ? void 0 : _f2[angle.name]) {
47549
48586
  Object.assign(entry, options.overrides[angle.name]);
47550
48587
  }
47551
48588
  joints.push(entry);
@@ -47607,7 +48644,8 @@ class Assembly {
47607
48644
  part: part.part,
47608
48645
  base: part.base,
47609
48646
  metadata: part.metadata ? { ...part.metadata } : void 0,
47610
- mates: part.mates.map((mate) => ({ ...mate }))
48647
+ mates: part.mates.map((mate) => ({ ...mate })),
48648
+ bindToFrame: part.bindToFrame
47611
48649
  })),
47612
48650
  joints: [...this.joints.values()].map((joint2) => ({
47613
48651
  name: joint2.name,
@@ -47631,7 +48669,22 @@ class Assembly {
47631
48669
  terms: coupling.terms.map((term) => ({ joint: term.joint, ratio: term.ratio })),
47632
48670
  offset: coupling.offset
47633
48671
  })),
47634
- kinematics: this.describeKinematics()
48672
+ kinematics: this.describeKinematics(),
48673
+ frames: [...this.frames.values()].map((frame) => cloneAssemblyFrame(frame)),
48674
+ frameJoints: [...this.frameJoints.values()].map((joint2) => ({
48675
+ name: joint2.name,
48676
+ type: joint2.type,
48677
+ parent: joint2.parent,
48678
+ child: joint2.child,
48679
+ rest: joint2.rest,
48680
+ min: joint2.min,
48681
+ max: joint2.max,
48682
+ defaultValue: joint2.defaultValue,
48683
+ unit: joint2.unit,
48684
+ control: joint2.control,
48685
+ metadata: joint2.metadata ? { ...joint2.metadata } : void 0
48686
+ })),
48687
+ frameEdges: [...this.frameEdges.values()].map((edge) => cloneAssemblyFrameEdge(edge))
47635
48688
  };
47636
48689
  }
47637
48690
  }
@@ -47858,6 +48911,9 @@ class ImportedAssembly {
47858
48911
  mergeInto(parent, options) {
47859
48912
  const def = this._assembly.describe();
47860
48913
  const pfx = options.prefix ? `${options.prefix}.` : "";
48914
+ if (def.frames.length > 0 || def.frameJoints.length > 0 || def.frameEdges.length > 0) {
48915
+ throw new Error("mergeInto() does not yet support rig frames. Import and solve the frame-based sub-assembly directly for now.");
48916
+ }
47861
48917
  const childSet = new Set(def.joints.map((j) => j.child));
47862
48918
  const roots = def.parts.filter((p2) => !childSet.has(p2.name));
47863
48919
  if (roots.length === 0) {
@@ -47888,14 +48944,26 @@ class ImportedAssembly {
47888
48944
  });
47889
48945
  }
47890
48946
  for (const angle of def.kinematics.angles) {
47891
- parent.addAngleBetweenLinks(`${pfx}${angle.a}`, `${pfx}${angle.b}`, `${pfx}${angle.c}`, {
48947
+ const options2 = {
47892
48948
  name: `${pfx}${angle.name}`,
47893
48949
  value: angle.target,
47894
48950
  min: angle.min,
47895
48951
  max: angle.max,
47896
48952
  control: angle.control,
47897
48953
  metadata: angle.metadata
47898
- });
48954
+ };
48955
+ if (angle.reference) {
48956
+ parent.addAngleBetweenLinkSegmentAndWorldDirection(`${pfx}${angle.b}`, `${pfx}${angle.c}`, angle.reference.direction, options2);
48957
+ } else if (angle.a) {
48958
+ parent.addAngleBetweenLinks(`${pfx}${angle.a}`, `${pfx}${angle.b}`, `${pfx}${angle.c}`, options2);
48959
+ }
48960
+ }
48961
+ for (const derived of def.kinematics.derivedLinks) {
48962
+ if (derived.direction === "toward") {
48963
+ parent.linkToward(`${pfx}${derived.name}`, `${pfx}${derived.fromLink}`, `${pfx}${derived.referenceLink}`, derived.distance);
48964
+ } else {
48965
+ parent.linkAwayFrom(`${pfx}${derived.name}`, `${pfx}${derived.fromLink}`, `${pfx}${derived.referenceLink}`, derived.distance);
48966
+ }
47899
48967
  }
47900
48968
  for (const p2 of def.parts) {
47901
48969
  parent.addPart(`${pfx}${p2.name}`, p2.part, {
@@ -52662,7 +53730,7 @@ const EPSILON$2 = 1e-9;
52662
53730
  function add$3(a2, b) {
52663
53731
  return [a2[0] + b[0], a2[1] + b[1], a2[2] + b[2]];
52664
53732
  }
52665
- function scale$3(v, factor) {
53733
+ function scale$4(v, factor) {
52666
53734
  return [v[0] * factor, v[1] * factor, v[2] * factor];
52667
53735
  }
52668
53736
  function sub$5(a2, b) {
@@ -52737,9 +53805,9 @@ function buildBend(points, directions, cornerIndex, radius) {
52737
53805
  const angle = Math.acos(turnDot);
52738
53806
  const trim = radius * Math.tan(angle / 2);
52739
53807
  const axis = normalize$3(cross$4(dIn, dOut), `corner ${cornerIndex} bend axis`);
52740
- const start = add$3(points[cornerIndex], scale$3(dIn, -trim));
52741
- const end = add$3(points[cornerIndex], scale$3(dOut, trim));
52742
- const center = add$3(start, scale$3(normalize$3(cross$4(axis, dIn), `corner ${cornerIndex} bend normal`), radius));
53808
+ const start = add$3(points[cornerIndex], scale$4(dIn, -trim));
53809
+ const end = add$3(points[cornerIndex], scale$4(dOut, trim));
53810
+ const center = add$3(start, scale$4(normalize$3(cross$4(axis, dIn), `corner ${cornerIndex} bend normal`), radius));
52743
53811
  return { cornerIndex, trim, start, end, center, axis, sweepDeg: angle * 180 / Math.PI, length: radius * angle };
52744
53812
  }
52745
53813
  function assertBendFits(bend2, radius, segmentLengths, bends) {
@@ -52770,7 +53838,7 @@ function buildRoute3DPlanFromPolyline(pointsInput, options = {}) {
52770
53838
  const segmentLengths = points.slice(1).map((point2, index2) => length$1(sub$5(point2, points[index2])));
52771
53839
  const directions = segmentLengths.map((segmentLength, index2) => {
52772
53840
  if (segmentLength <= EPSILON$2) throw new Error(`Curve.Route.fromPolyline: segment ${index2} is zero length.`);
52773
- return scale$3(sub$5(points[index2 + 1], points[index2]), 1 / segmentLength);
53841
+ return scale$4(sub$5(points[index2 + 1], points[index2]), 1 / segmentLength);
52774
53842
  });
52775
53843
  const bends = new Array(points.length).fill(null);
52776
53844
  if (radius > EPSILON$2) {
@@ -52803,7 +53871,7 @@ function buildRoute3DPlanFromPolyline(pointsInput, options = {}) {
52803
53871
  segments,
52804
53872
  length: routeLength,
52805
53873
  ports: {
52806
- [startPort]: portFrame(startPort, points[0], scale$3(directions[0], -1), 0, up),
53874
+ [startPort]: portFrame(startPort, points[0], scale$4(directions[0], -1), 0, up),
52807
53875
  [endPort]: portFrame(endPort, points[points.length - 1], directions[directions.length - 1], routeLength, up)
52808
53876
  }
52809
53877
  };
@@ -53311,7 +54379,7 @@ function variableSweep(spine, sections, options = {}) {
53311
54379
  sources: isTruckVariableSweep ? ["sweep"] : ["sweep", "level-set"]
53312
54380
  });
53313
54381
  }
53314
- function requirePositive$9(value, label) {
54382
+ function requirePositive$8(value, label) {
53315
54383
  if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
53316
54384
  throw new Error(`${label} must be a finite number > 0, got ${JSON.stringify(value)}`);
53317
54385
  }
@@ -53330,10 +54398,10 @@ function requireIntegerAtLeast(value, label, min2) {
53330
54398
  return value;
53331
54399
  }
53332
54400
  function normalizeHelixOptions(options, apiName) {
53333
- const radius = requirePositive$9(options.radius, `${apiName}.radius`);
53334
- const pitch = options.pitch == null ? void 0 : requirePositive$9(options.pitch, `${apiName}.pitch`);
53335
- const turns = options.turns == null ? void 0 : requirePositive$9(options.turns, `${apiName}.turns`);
53336
- const height = options.height == null ? void 0 : requirePositive$9(options.height, `${apiName}.height`);
54401
+ const radius = requirePositive$8(options.radius, `${apiName}.radius`);
54402
+ const pitch = options.pitch == null ? void 0 : requirePositive$8(options.pitch, `${apiName}.pitch`);
54403
+ const turns = options.turns == null ? void 0 : requirePositive$8(options.turns, `${apiName}.turns`);
54404
+ const height = options.height == null ? void 0 : requirePositive$8(options.height, `${apiName}.height`);
53337
54405
  const provided = [pitch != null, turns != null, height != null].filter(Boolean).length;
53338
54406
  if (provided < 2) {
53339
54407
  throw new Error(`${apiName}: provide any two of pitch, turns, and height so the third can be derived.`);
@@ -53419,7 +54487,7 @@ function buildHelixCoil(profileOrOptions, maybeOptions) {
53419
54487
  if (!options) throw new Error("Helix.coil: options are required.");
53420
54488
  const spec2 = normalizeHelixOptions(options, "Helix.coil");
53421
54489
  const profile = hasCustomProfile ? profileOrOptions : circle2d(
53422
- requirePositive$9(options.wireRadius, "Helix.coil.wireRadius"),
54490
+ requirePositive$8(options.wireRadius, "Helix.coil.wireRadius"),
53423
54491
  requireIntegerAtLeast(options.profileSegments ?? 24, "Helix.coil.profileSegments", 8)
53424
54492
  );
53425
54493
  if (profile.isEmpty()) throw new Error("Helix.coil: profile must not be empty.");
@@ -54002,6 +55070,12 @@ const Curve = {
54002
55070
  function sub$4(a2, b) {
54003
55071
  return [a2[0] - b[0], a2[1] - b[1], a2[2] - b[2]];
54004
55072
  }
55073
+ function addVec(a2, b) {
55074
+ return [a2[0] + b[0], a2[1] + b[1], a2[2] + b[2]];
55075
+ }
55076
+ function scale$3(v, s) {
55077
+ return [v[0] * s, v[1] * s, v[2] * s];
55078
+ }
54005
55079
  function dot$4(a2, b) {
54006
55080
  return a2[0] * b[0] + a2[1] * b[1] + a2[2] * b[2];
54007
55081
  }
@@ -54018,6 +55092,56 @@ function normalize$2(v) {
54018
55092
  function clampDot(d2) {
54019
55093
  return Math.max(-1, Math.min(1, d2));
54020
55094
  }
55095
+ function requirePositiveFinite(value, label) {
55096
+ if (!Number.isFinite(value) || value <= 0) throw new Error(`${label} must be a finite number > 0`);
55097
+ return value;
55098
+ }
55099
+ function requireNonNegativeFinite(value, label) {
55100
+ if (!Number.isFinite(value) || value < 0) throw new Error(`${label} must be a finite number >= 0`);
55101
+ return value;
55102
+ }
55103
+ function requireFiniteVec3$1(value, label) {
55104
+ if (value == null) return void 0;
55105
+ if (!Array.isArray(value) || value.length !== 3) throw new Error(`${label} must be [x, y, z]`);
55106
+ const result = [Number(value[0]), Number(value[1]), Number(value[2])];
55107
+ if (!Number.isFinite(result[0]) || !Number.isFinite(result[1]) || !Number.isFinite(result[2])) {
55108
+ throw new Error(`${label} must contain finite coordinates`);
55109
+ }
55110
+ return result;
55111
+ }
55112
+ function normalizedDirection(value, label) {
55113
+ const len2 = vecLen(value);
55114
+ if (len2 < 1e-12) throw new Error(`${label} must not be the zero vector`);
55115
+ return [value[0] / len2, value[1] / len2, value[2] / len2];
55116
+ }
55117
+ function stablePerpendicularAxis$1(direction2) {
55118
+ const seed = Math.abs(direction2[1]) < 0.9 ? [0, 1, 0] : [1, 0, 0];
55119
+ return normalizedDirection(sub$4(seed, scale$3(direction2, dot$4(direction2, seed))), "elbow: turn plane");
55120
+ }
55121
+ function rotateDirection(v, axis, angleRad) {
55122
+ const c2 = Math.cos(angleRad);
55123
+ const s = Math.sin(angleRad);
55124
+ const axisDot = dot$4(axis, v);
55125
+ const axisCross = cross$3(axis, v);
55126
+ return [
55127
+ v[0] * c2 + axisCross[0] * s + axis[0] * axisDot * (1 - c2),
55128
+ v[1] * c2 + axisCross[1] * s + axis[1] * axisDot * (1 - c2),
55129
+ v[2] * c2 + axisCross[2] * s + axis[2] * axisDot * (1 - c2)
55130
+ ];
55131
+ }
55132
+ function angleBetweenDirections(from, to) {
55133
+ return Math.acos(clampDot(dot$4(from, to)));
55134
+ }
55135
+ function turnAxisForDirections(from, to) {
55136
+ const axis = cross$3(from, to);
55137
+ if (vecLen(axis) >= 1e-12) return normalizedDirection(axis, "elbow: turn axis");
55138
+ if (dot$4(from, to) > 0) throw new Error("elbow: from and to directions are collinear; angle too small");
55139
+ return stablePerpendicularAxis$1(from);
55140
+ }
55141
+ function profileForPipeRadius(pipeRadius, wall, segments) {
55142
+ const outer = circle2d(pipeRadius, segments);
55143
+ return wall != null && wall > 0 ? difference2d(outer, circle2d(pipeRadius - wall, segments)) : outer;
55144
+ }
54021
55145
  function pipeRoute(points, radius, options) {
54022
55146
  if (points.length < 2) throw new Error("pipeRoute needs at least 2 points");
54023
55147
  const bendR = (options == null ? void 0 : options.bendRadius) ?? radius * 4;
@@ -54049,88 +55173,40 @@ function simplifyStraightPipeRoutePoints(points) {
54049
55173
  return simplified;
54050
55174
  }
54051
55175
  function elbow(pipeRadius, bendRadius, angle, options) {
54052
- let angleDeg;
54053
- let wall;
54054
- let segs;
54055
- let fromDir;
54056
- let toDir;
54057
- if (typeof angle === "object" && angle !== null) {
54058
- angleDeg = 90;
54059
- wall = angle.wall;
54060
- segs = angle.segments ?? 32;
54061
- fromDir = angle.from;
54062
- toDir = angle.to;
55176
+ requirePositiveFinite(pipeRadius, "elbow: pipeRadius");
55177
+ requirePositiveFinite(bendRadius, "elbow: bendRadius");
55178
+ const opts = typeof angle === "object" && angle !== null ? angle : options;
55179
+ const wall = opts == null ? void 0 : opts.wall;
55180
+ const segments = (opts == null ? void 0 : opts.segments) ?? 32;
55181
+ if (!Number.isFinite(segments) || segments < 3) throw new Error("elbow: segments must be a finite number >= 3");
55182
+ if (wall != null && requireNonNegativeFinite(wall, "elbow: wall") >= pipeRadius) {
55183
+ throw new Error("elbow: wall must be smaller than pipeRadius");
55184
+ }
55185
+ const from = normalizedDirection(requireFiniteVec3$1(opts == null ? void 0 : opts.from, "elbow: from") ?? [0, 0, 1], "elbow: from");
55186
+ const toInput = requireFiniteVec3$1(opts == null ? void 0 : opts.to, "elbow: to");
55187
+ const angleDegInput = typeof angle === "number" ? angle : void 0;
55188
+ const angleRadInput = (angleDegInput ?? 90) * Math.PI / 180;
55189
+ let angleRad;
55190
+ let turnAxis;
55191
+ if (toInput) {
55192
+ const to = normalizedDirection(toInput, "elbow: to");
55193
+ angleRad = angleBetweenDirections(from, to);
55194
+ turnAxis = turnAxisForDirections(from, to);
54063
55195
  } else {
54064
- angleDeg = angle ?? 90;
54065
- wall = options == null ? void 0 : options.wall;
54066
- segs = (options == null ? void 0 : options.segments) ?? 32;
54067
- fromDir = options == null ? void 0 : options.from;
54068
- toDir = options == null ? void 0 : options.to;
54069
- }
54070
- if (fromDir && toDir) {
54071
- const nFrom = normalize$2(fromDir);
54072
- const nTo = normalize$2(toDir);
54073
- const d2 = clampDot(dot$4(nFrom, nTo));
54074
- angleDeg = Math.acos(d2) * 180 / Math.PI;
54075
- }
54076
- if (angleDeg < 0.01) throw new Error("elbow: angle too small");
54077
- const circlePts = [];
54078
- for (let i = 0; i < segs; i++) {
54079
- const a2 = i / segs * Math.PI * 2;
54080
- circlePts.push([bendRadius + pipeRadius * Math.cos(a2), pipeRadius * Math.sin(a2)]);
54081
- }
54082
- const bendSegs = Math.max(4, Math.ceil(segs * angleDeg / 360));
54083
- const outerPlan = {
54084
- kind: "revolve",
54085
- profile: { kind: "polygon", points: circlePts, transforms: [] },
54086
- degrees: angleDeg,
54087
- segments: bendSegs
54088
- };
54089
- let bendShape = buildShapeFromCompilePlan(outerPlan);
54090
- if (wall != null && wall > 0) {
54091
- const innerPts = [];
54092
- const innerR = pipeRadius - wall;
54093
- for (let i = 0; i < segs; i++) {
54094
- const a2 = i / segs * Math.PI * 2;
54095
- innerPts.push([bendRadius + innerR * Math.cos(a2), innerR * Math.sin(a2)]);
54096
- }
54097
- const innerPlan = {
54098
- kind: "revolve",
54099
- profile: { kind: "polygon", points: innerPts, transforms: [] },
54100
- degrees: angleDeg,
54101
- segments: bendSegs
54102
- };
54103
- const innerBend = buildShapeFromCompilePlan(innerPlan);
54104
- bendShape = bendShape.subtract(innerBend);
54105
- }
54106
- if (fromDir && toDir) {
54107
- const nFrom = normalize$2(fromDir);
54108
- const nTo = normalize$2(toDir);
54109
- const crossVec = cross$3(nFrom, nTo);
54110
- const crossLen = vecLen(crossVec);
54111
- if (crossLen < 1e-10) return bendShape;
54112
- const axis = normalize$2(crossVec);
54113
- const perpDir = cross$3(axis, nFrom);
54114
- bendShape = bendShape.transform([
54115
- perpDir[0],
54116
- perpDir[1],
54117
- perpDir[2],
54118
- 0,
54119
- nFrom[0],
54120
- nFrom[1],
54121
- nFrom[2],
54122
- 0,
54123
- axis[0],
54124
- axis[1],
54125
- axis[2],
54126
- 0,
54127
- 0,
54128
- 0,
54129
- 0,
54130
- 1
54131
- ]);
55196
+ if (!Number.isFinite(angleRadInput) || angleRadInput <= 1e-6 || angleRadInput > Math.PI) {
55197
+ throw new Error("elbow: angle must be finite and in the range (0, 180] degrees");
55198
+ }
55199
+ turnAxis = stablePerpendicularAxis$1(from);
55200
+ angleRad = angleRadInput;
54132
55201
  }
54133
- return bendShape;
55202
+ if (angleRad <= 1e-6) throw new Error("elbow: angle too small");
55203
+ const inward = normalizedDirection(cross$3(turnAxis, from), "elbow: bend normal");
55204
+ const center = scale$3(inward, bendRadius);
55205
+ const endRadial = rotateDirection(scale$3(inward, -bendRadius), turnAxis, angleRad);
55206
+ const end = addVec(center, endRadial);
55207
+ const profile = profileForPipeRadius(pipeRadius, wall, Math.floor(segments));
55208
+ const pathSamples = Math.max(6, Math.ceil(segments * (angleRad * 180 / Math.PI) / 360) + 1);
55209
+ return sweep(profile, Curve.Arc({ start: [0, 0, 0], end, tangent: from }), { samples: pathSamples, up: turnAxis });
54134
55210
  }
54135
55211
  const EPS$7 = 1e-9;
54136
55212
  function assertFinitePositive(apiName, name, value) {
@@ -54629,7 +55705,7 @@ function spurGear(options) {
54629
55705
  });
54630
55706
  return attachGearMeta(shapeWithConnectors, meta2);
54631
55707
  }
54632
- function requirePositive$8(scope, name, value) {
55708
+ function requirePositive$7(scope, name, value) {
54633
55709
  if (!isFinitePositive(value)) throw new Error(`${scope}: "${name}" must be > 0`);
54634
55710
  }
54635
55711
  function requireOptionalBore(scope, boreDiameter, maxDiameter) {
@@ -54651,8 +55727,8 @@ function cutBore$1(shape, boreDiameter) {
54651
55727
  return shape.subtract(cutter);
54652
55728
  }
54653
55729
  function gearBodyDisk(options) {
54654
- requirePositive$8("gearBodyDisk", "outerRadius", options.outerRadius);
54655
- requirePositive$8("gearBodyDisk", "faceWidth", options.faceWidth);
55730
+ requirePositive$7("gearBodyDisk", "outerRadius", options.outerRadius);
55731
+ requirePositive$7("gearBodyDisk", "faceWidth", options.faceWidth);
54656
55732
  const bore = requireOptionalBore("gearBodyDisk", options.boreDiameter, options.outerRadius * 2);
54657
55733
  const segments = resolveSegments(options.segments);
54658
55734
  const outer = circle2d(options.outerRadius, segments);
@@ -54660,14 +55736,14 @@ function gearBodyDisk(options) {
54660
55736
  return sketchExtrude(profile, options.faceWidth);
54661
55737
  }
54662
55738
  function gearBodyDiskWithHub(options) {
54663
- requirePositive$8("gearBodyDiskWithHub", "hubDiameter", options.hubDiameter);
55739
+ requirePositive$7("gearBodyDiskWithHub", "hubDiameter", options.hubDiameter);
54664
55740
  if (options.hubDiameter >= options.outerRadius * 2) {
54665
55741
  throw new Error('gearBodyDiskWithHub: "hubDiameter" must be smaller than the outer diameter');
54666
55742
  }
54667
55743
  const bore = requireOptionalBore("gearBodyDiskWithHub", options.boreDiameter, options.hubDiameter);
54668
55744
  const base = gearBodyDisk({ ...options, boreDiameter: 0 });
54669
55745
  const hubFaceWidth = options.hubFaceWidth ?? options.faceWidth * 1.5;
54670
- requirePositive$8("gearBodyDiskWithHub", "hubFaceWidth", hubFaceWidth);
55746
+ requirePositive$7("gearBodyDiskWithHub", "hubFaceWidth", hubFaceWidth);
54671
55747
  const hub = cylinder(hubFaceWidth, options.hubDiameter * 0.5, void 0, options.segments).translate(
54672
55748
  0,
54673
55749
  0,
@@ -54676,11 +55752,11 @@ function gearBodyDiskWithHub(options) {
54676
55752
  return cutBore$1(base.add(hub), bore);
54677
55753
  }
54678
55754
  function gearBodySpoked(options) {
54679
- requirePositive$8("gearBodySpoked", "outerRadius", options.outerRadius);
54680
- requirePositive$8("gearBodySpoked", "faceWidth", options.faceWidth);
54681
- requirePositive$8("gearBodySpoked", "rimWidth", options.rimWidth);
54682
- requirePositive$8("gearBodySpoked", "hubDiameter", options.hubDiameter);
54683
- requirePositive$8("gearBodySpoked", "spokeWidth", options.spokeWidth);
55755
+ requirePositive$7("gearBodySpoked", "outerRadius", options.outerRadius);
55756
+ requirePositive$7("gearBodySpoked", "faceWidth", options.faceWidth);
55757
+ requirePositive$7("gearBodySpoked", "rimWidth", options.rimWidth);
55758
+ requirePositive$7("gearBodySpoked", "hubDiameter", options.hubDiameter);
55759
+ requirePositive$7("gearBodySpoked", "spokeWidth", options.spokeWidth);
54684
55760
  if (!Number.isInteger(options.spokeCount) || options.spokeCount < 2) {
54685
55761
  throw new Error('gearBodySpoked: "spokeCount" must be an integer >= 2');
54686
55762
  }
@@ -54703,12 +55779,12 @@ function gearBodySpoked(options) {
54703
55779
  }
54704
55780
  function gearBodyFromProfile(profile, options) {
54705
55781
  if (!(profile instanceof Sketch)) throw new Error('gearBodyFromProfile: "profile" must be a Sketch');
54706
- requirePositive$8("gearBodyFromProfile", "faceWidth", options.faceWidth);
55782
+ requirePositive$7("gearBodyFromProfile", "faceWidth", options.faceWidth);
54707
55783
  const bore = options.boreDiameter ?? 0;
54708
55784
  if (!Number.isFinite(bore) || bore < 0) throw new Error('gearBodyFromProfile: "boreDiameter" must be >= 0');
54709
55785
  return cutBore$1(sketchExtrude(profile, options.faceWidth), bore);
54710
55786
  }
54711
- function requirePositive$7(scope, name, value) {
55787
+ function requirePositive$6(scope, name, value) {
54712
55788
  if (!isFinitePositive(value)) throw new Error(`${scope}: "${name}" must be > 0`);
54713
55789
  }
54714
55790
  function requireFiniteAngle(scope, name, value) {
@@ -54770,7 +55846,7 @@ function buildSpurTeethRegion(options, name, faceWidth) {
54770
55846
  }
54771
55847
  function buildSolidArcRegion(options, name, faceWidth) {
54772
55848
  const scope = "driveWheel.addSolidArcBetween";
54773
- requirePositive$7(scope, "outerRadius", options.outerRadius);
55849
+ requirePositive$6(scope, "outerRadius", options.outerRadius);
54774
55850
  const innerRadius = options.innerRadius ?? 0;
54775
55851
  if (!Number.isFinite(innerRadius) || innerRadius < 0) throw new Error(`${scope}: "innerRadius" must be >= 0`);
54776
55852
  if (innerRadius >= options.outerRadius) throw new Error(`${scope}: "innerRadius" must be smaller than "outerRadius"`);
@@ -54836,7 +55912,7 @@ class DriveWheelBuilder {
54836
55912
  __publicField(this, "boreDiameter");
54837
55913
  __publicField(this, "regions", []);
54838
55914
  if (options.body !== void 0 && !(options.body instanceof Shape)) throw new Error('driveWheel: "body" must be a Shape');
54839
- if (options.faceWidth !== void 0) requirePositive$7("driveWheel", "faceWidth", options.faceWidth);
55915
+ if (options.faceWidth !== void 0) requirePositive$6("driveWheel", "faceWidth", options.faceWidth);
54840
55916
  const boreDiameter = options.boreDiameter ?? 0;
54841
55917
  if (!Number.isFinite(boreDiameter) || boreDiameter < 0) throw new Error('driveWheel: "boreDiameter" must be >= 0');
54842
55918
  this.body = options.body;
@@ -54871,7 +55947,7 @@ class DriveWheelBuilder {
54871
55947
  if (options.innerRadius !== void 0 && (!Number.isFinite(options.innerRadius) || options.innerRadius < 0)) {
54872
55948
  throw new Error(`${scope}: "innerRadius" must be >= 0`);
54873
55949
  }
54874
- if (options.outerRadius !== void 0) requirePositive$7(scope, "outerRadius", options.outerRadius);
55950
+ if (options.outerRadius !== void 0) requirePositive$6(scope, "outerRadius", options.outerRadius);
54875
55951
  this.regions.push({
54876
55952
  shape: shape.clone(),
54877
55953
  meta: {
@@ -54937,7 +56013,7 @@ class DriveWheelBuilder {
54937
56013
  resolveFaceWidth(scope, localFaceWidth) {
54938
56014
  const faceWidth = localFaceWidth ?? this.faceWidth;
54939
56015
  if (faceWidth === void 0) throw new Error(`${scope}: "faceWidth" is required unless driveWheel({ faceWidth }) was set`);
54940
- requirePositive$7(scope, "faceWidth", faceWidth);
56016
+ requirePositive$6(scope, "faceWidth", faceWidth);
54941
56017
  if (this.faceWidth !== void 0 && localFaceWidth !== void 0 && Math.abs(this.faceWidth - localFaceWidth) > EPSILON$1) {
54942
56018
  throw new Error(`${scope}: region faceWidth must match driveWheel faceWidth`);
54943
56019
  }
@@ -56090,1860 +57166,6 @@ function washer(size, options) {
56090
57166
  const bore = cylinder(dims.t + 1, dims.id / 2, void 0, segs);
56091
57167
  return outer.subtract(bore);
56092
57168
  }
56093
- function requirePositive$6(value, name) {
56094
- if (!Number.isFinite(value) || value <= 0) throw new Error(`${name} must be a positive finite number`);
56095
- return value;
56096
- }
56097
- function requireNonNegative(value, name) {
56098
- if (!Number.isFinite(value) || value < 0) throw new Error(`${name} must be a non-negative finite number`);
56099
- return value;
56100
- }
56101
- function metricWasherSizeForPin(pinDiameter) {
56102
- if (pinDiameter <= 2) return "M2";
56103
- if (pinDiameter <= 2.5) return "M2.5";
56104
- if (pinDiameter <= 3) return "M3";
56105
- if (pinDiameter <= 4) return "M4";
56106
- if (pinDiameter <= 5) return "M5";
56107
- if (pinDiameter <= 6) return "M6";
56108
- if (pinDiameter <= 8) return "M8";
56109
- return "M10";
56110
- }
56111
- function cylinderAlongX(length4, radius, xCenter, segments) {
56112
- return cylinder(length4, radius, void 0, segments).pointAlong([1, 0, 0]).translate(xCenter - length4 / 2, 0, 0);
56113
- }
56114
- function tubeAlongX(length4, outerRadius, innerRadius, xCenter, segments) {
56115
- return cylinderAlongX(length4, outerRadius, xCenter, segments).subtract(cylinderAlongX(length4 + 0.4, innerRadius, xCenter, segments));
56116
- }
56117
- function cylinderAlongY(length4, radius, yCenter, segments) {
56118
- return cylinder(length4, radius, void 0, segments).pointAlong([0, 1, 0]).translate(0, yCenter - length4 / 2, 0);
56119
- }
56120
- function tubeAlongY(length4, outerRadius, innerRadius, yCenter, segments) {
56121
- return cylinderAlongY(length4, outerRadius, yCenter, segments).subtract(cylinderAlongY(length4 + 0.4, innerRadius, yCenter, segments));
56122
- }
56123
- function tubeAlongZ(height, outerRadius, innerRadius, segments) {
56124
- return cylinder(height, outerRadius, void 0, segments).subtract(
56125
- cylinder(height + 0.4, innerRadius, void 0, segments).translate(0, 0, -0.2)
56126
- );
56127
- }
56128
- function washerAlongX(size, xCenter, segments) {
56129
- const dims = WASHER_TABLE[size];
56130
- return washer(size, { segments }).pointAlong([1, 0, 0]).translate(xCenter - dims.t / 2, 0, 0);
56131
- }
56132
- function resolveBoltInset(raw, fallback) {
56133
- if (raw === void 0) return [fallback, fallback];
56134
- if (typeof raw === "number") return [requirePositive$6(raw, "boltInset"), requirePositive$6(raw, "boltInset")];
56135
- if (raw.length !== 2) throw new Error("boltInset tuple must be [x, y]");
56136
- return [requirePositive$6(raw[0], "boltInset[0]"), requirePositive$6(raw[1], "boltInset[1]")];
56137
- }
56138
- function validateBoltPositionsForServiceCover(args) {
56139
- args.positions.forEach(([x2, y2], index2) => {
56140
- if (!Number.isFinite(x2) || !Number.isFinite(y2)) {
56141
- throw new Error(`boltedServiceCover: boltPositions[${index2}] must contain finite numbers`);
56142
- }
56143
- if (Math.abs(x2) + args.holeRadius >= args.coverWidth / 2 || Math.abs(y2) + args.holeRadius >= args.coverDepth / 2) {
56144
- throw new Error(`boltedServiceCover: boltPositions[${index2}] is too close to the cover edge`);
56145
- }
56146
- const overlapsOpening = Math.abs(x2) - args.holeRadius <= args.openingWidth / 2 && Math.abs(y2) - args.holeRadius <= args.openingDepth / 2;
56147
- if (overlapsOpening) {
56148
- throw new Error(
56149
- `boltedServiceCover: boltPositions[${index2}] lands over the service opening; decrease boltInset, increase ledgeWidth, or provide a smaller opening`
56150
- );
56151
- }
56152
- });
56153
- }
56154
- function placeCutterAtPositions(cutter, positions, z2) {
56155
- return union(...positions.map(([x2, y2]) => cutter.translate(x2, y2, z2)));
56156
- }
56157
- function boltedServiceCover(options) {
56158
- const width = requirePositive$6(options.width, "width");
56159
- const depth = requirePositive$6(options.depth, "depth");
56160
- const coverThickness = requirePositive$6(options.coverThickness ?? 3, "coverThickness");
56161
- const parentThickness = requirePositive$6(options.parentThickness ?? 8, "parentThickness");
56162
- const ledgeWidth = requirePositive$6(options.ledgeWidth ?? 8, "ledgeWidth");
56163
- const gasketThickness = Math.max(0, options.gasketThickness ?? 0.8);
56164
- const gasketInset = Math.max(0, options.gasketInset ?? 2);
56165
- const screwSize = options.screwSize ?? "M4";
56166
- const segments = options.segments ?? 36;
56167
- const sizeData = METRIC_HOLE_TABLE[screwSize];
56168
- if (!sizeData) throw new Error(`boltedServiceCover: unsupported screwSize "${screwSize}"`);
56169
- const screwLength = requirePositive$6(options.screwLength ?? parentThickness + gasketThickness + coverThickness + 4, "screwLength");
56170
- const coverFit = options.coverFit ?? "normal";
56171
- const counterboreEnabled = options.counterbore ?? true;
56172
- const [insetX, insetY] = resolveBoltInset(options.boltInset, Math.max(ledgeWidth * 0.65, sizeData.head * 0.75));
56173
- if (insetX * 2 >= width || insetY * 2 >= depth) {
56174
- throw new Error("boltedServiceCover: boltInset leaves no room for a four-corner bolt pattern");
56175
- }
56176
- const boltPositions = options.boltPositions ?? [
56177
- [-width / 2 + insetX, -depth / 2 + insetY],
56178
- [width / 2 - insetX, -depth / 2 + insetY],
56179
- [-width / 2 + insetX, depth / 2 - insetY],
56180
- [width / 2 - insetX, depth / 2 - insetY]
56181
- ];
56182
- if (boltPositions.length === 0) throw new Error("boltedServiceCover: boltPositions must contain at least one point");
56183
- const parentWidth = width + ledgeWidth * 2;
56184
- const parentDepth = depth + ledgeWidth * 2;
56185
- const openingWidth = Math.max(1, width - ledgeWidth * 2);
56186
- const openingDepth = Math.max(1, depth - ledgeWidth * 2);
56187
- validateBoltPositionsForServiceCover({
56188
- positions: boltPositions,
56189
- coverWidth: width,
56190
- coverDepth: depth,
56191
- openingWidth,
56192
- openingDepth,
56193
- holeRadius: sizeData[coverFit] / 2
56194
- });
56195
- const coverHole = fastenerHole({
56196
- size: screwSize,
56197
- fit: coverFit,
56198
- depth: coverThickness + 0.6,
56199
- center: true,
56200
- segments,
56201
- ...counterboreEnabled ? { counterbore: { depth: Math.min(coverThickness * 0.6, Math.max(0.6, coverThickness - 0.4)) } } : {}
56202
- });
56203
- const parentTap = fastenerHole({ size: screwSize, fit: "tap", depth: parentThickness + 0.6, center: true, segments });
56204
- const parentThreadEnvelope = fastenerHole({
56205
- size: screwSize,
56206
- fit: "close",
56207
- depth: parentThickness + 0.6,
56208
- center: true,
56209
- segments
56210
- });
56211
- const openingCutter = box(openingWidth, openingDepth, parentThickness + 1).translate(0, 0, -0.5);
56212
- const parentTappedPattern = placeCutterAtPositions(parentTap, boltPositions, parentThickness / 2);
56213
- const parentThreadEnvelopePattern = placeCutterAtPositions(parentThreadEnvelope, boltPositions, parentThickness / 2);
56214
- const parent = box(parentWidth, parentDepth, parentThickness).subtract(openingCutter).subtract(parentThreadEnvelopePattern).color("#4b5563");
56215
- let coverBlank = box(width, depth, coverThickness);
56216
- if (options.pullTabs ?? true) {
56217
- const tabWidth = Math.min(width * 0.18, Math.max(sizeData.head * 1.6, 12));
56218
- const tabDepth = Math.max(4, coverThickness * 1.4);
56219
- const tabOverlap = Math.min(0.5, tabDepth * 0.25);
56220
- const tabY = -depth / 2 - tabDepth / 2 + tabOverlap;
56221
- const tabX = width * 0.23;
56222
- coverBlank = union(
56223
- coverBlank,
56224
- box(tabWidth, tabDepth, coverThickness).translate(-tabX, tabY, 0),
56225
- box(tabWidth, tabDepth, coverThickness).translate(tabX, tabY, 0)
56226
- );
56227
- }
56228
- const coverClearancePattern = placeCutterAtPositions(coverHole, boltPositions, coverThickness / 2);
56229
- const cover = coverBlank.subtract(coverClearancePattern).translate(0, 0, parentThickness + gasketThickness).color("#334155");
56230
- 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;
56231
- const hardware = fastenerSet(screwSize, screwLength, { washerUnderHead: false, washerUnderNut: false, fit: coverFit, segments });
56232
- const screwOriginZ = parentThickness + gasketThickness + coverThickness;
56233
- const screws = boltPositions.map(([x2, y2]) => hardware.bolt.translate(x2, y2, screwOriginZ).color("#94a3b8"));
56234
- const parts = [
56235
- { name: "service cover parent ledge with threaded hole envelopes", shape: parent },
56236
- ...gasket ? [{ name: "service cover gasket seated on ledge", shape: gasket }] : [],
56237
- { name: "bolted service cover plate with fused pull tabs", shape: cover },
56238
- ...screws.map((shape, index2) => ({ name: `installed ${screwSize} cover screw ${index2 + 1}`, shape }))
56239
- ];
56240
- return {
56241
- parts,
56242
- parent,
56243
- cover,
56244
- gasket,
56245
- screws,
56246
- boltPositions,
56247
- cutters: {
56248
- coverClearance: coverClearancePattern,
56249
- parentTapped: parentTappedPattern,
56250
- parentThreadEnvelope: parentThreadEnvelopePattern
56251
- },
56252
- dims: {
56253
- width,
56254
- depth,
56255
- coverThickness,
56256
- parentThickness,
56257
- ledgeWidth,
56258
- gasketThickness,
56259
- screwSize,
56260
- screwLength,
56261
- clearanceDia: sizeData[coverFit],
56262
- tapDia: sizeData.tap,
56263
- threadEnvelopeDia: sizeData.close
56264
- }
56265
- };
56266
- }
56267
- function datumEnclosureAssembly(options) {
56268
- const width = requirePositive$6(options.width, "width");
56269
- const depth = requirePositive$6(options.depth, "depth");
56270
- const height = requirePositive$6(options.height, "height");
56271
- const wallThickness = requirePositive$6(options.wallThickness ?? 2.4, "wallThickness");
56272
- const baseThickness = requirePositive$6(options.baseThickness ?? wallThickness, "baseThickness");
56273
- const coverThickness = requirePositive$6(options.coverThickness ?? 2.4, "coverThickness");
56274
- const ledgeWidth = requirePositive$6(options.ledgeWidth ?? Math.max(3.6, wallThickness * 1.35), "ledgeWidth");
56275
- const gasketThickness = requireNonNegative(options.gasketThickness ?? 0.8, "gasketThickness");
56276
- const faceClearance = requirePositive$6(options.faceClearance ?? 0.04, "faceClearance");
56277
- const screwSize = options.screwSize ?? "M3";
56278
- const coverFit = options.coverFit ?? "normal";
56279
- const segments = options.segments ?? 32;
56280
- const sizeData = METRIC_HOLE_TABLE[screwSize];
56281
- if (!sizeData) throw new Error(`datumEnclosureAssembly: unsupported screwSize "${screwSize}"`);
56282
- const innerWidth = width - wallThickness * 2;
56283
- const innerDepth = depth - wallThickness * 2;
56284
- if (innerWidth <= ledgeWidth * 2 + 8 || innerDepth <= ledgeWidth * 2 + 8) {
56285
- throw new Error("datumEnclosureAssembly: wallThickness and ledgeWidth leave too little internal opening");
56286
- }
56287
- if (height <= baseThickness + coverThickness + 4) {
56288
- throw new Error("datumEnclosureAssembly: height must leave room for internal ribs and standoffs");
56289
- }
56290
- const standoffDiameter = requirePositive$6(
56291
- options.standoffDiameter ?? Math.max(sizeData.head * 1.65, sizeData.close * 2.2),
56292
- "standoffDiameter"
56293
- );
56294
- const minInset = wallThickness + Math.max(ledgeWidth, standoffDiameter / 2 + 1.2);
56295
- const [insetX, insetY] = resolveBoltInset(options.screwInset, minInset);
56296
- if (insetX * 2 >= width || insetY * 2 >= depth) {
56297
- throw new Error("datumEnclosureAssembly: screwInset leaves no room for the standoff datum");
56298
- }
56299
- const screwPositions = options.screwPositions ?? [
56300
- [-width / 2 + insetX, -depth / 2 + insetY],
56301
- [width / 2 - insetX, -depth / 2 + insetY],
56302
- [-width / 2 + insetX, depth / 2 - insetY],
56303
- [width / 2 - insetX, depth / 2 - insetY]
56304
- ];
56305
- if (screwPositions.length === 0) throw new Error("datumEnclosureAssembly: screwPositions must contain at least one point");
56306
- for (const [index2, [x2, y2]] of screwPositions.entries()) {
56307
- if (!Number.isFinite(x2) || !Number.isFinite(y2)) {
56308
- throw new Error(`datumEnclosureAssembly: screwPositions[${index2}] must contain finite numbers`);
56309
- }
56310
- if (Math.abs(x2) + standoffDiameter / 2 > innerWidth / 2 || Math.abs(y2) + standoffDiameter / 2 > innerDepth / 2) {
56311
- throw new Error(`datumEnclosureAssembly: screwPositions[${index2}] does not fit inside the enclosure walls`);
56312
- }
56313
- }
56314
- const ribHeight = requirePositive$6(options.ribHeight ?? Math.min(height * 0.24, Math.max(2.4, baseThickness * 1.4)), "ribHeight");
56315
- const ribThickness = requirePositive$6(options.ribThickness ?? Math.max(1.2, wallThickness * 0.75), "ribThickness");
56316
- const portWidth = requirePositive$6(options.portWidth ?? Math.min(innerWidth * 0.28, Math.max(12, width * 0.16)), "portWidth");
56317
- const portHeight = requirePositive$6(options.portHeight ?? Math.min(height * 0.42, Math.max(5, height * 0.28)), "portHeight");
56318
- if (portWidth >= innerWidth - ledgeWidth * 2) {
56319
- throw new Error("datumEnclosureAssembly: portWidth must fit between internal ledges and standoffs");
56320
- }
56321
- if (portHeight >= height - baseThickness - 1) {
56322
- throw new Error("datumEnclosureAssembly: portHeight must leave material above and below the service port");
56323
- }
56324
- const screwLength = requirePositive$6(options.screwLength ?? coverThickness + gasketThickness + Math.max(6, height * 0.45), "screwLength");
56325
- const coverHole = fastenerHole({
56326
- size: screwSize,
56327
- fit: coverFit,
56328
- depth: coverThickness + 0.6,
56329
- center: true,
56330
- segments,
56331
- counterbore: { depth: Math.min(coverThickness * 0.6, Math.max(0.6, coverThickness - 0.35)) }
56332
- });
56333
- const standoffTap = fastenerHole({ size: screwSize, fit: "tap", depth: height + 0.8, center: true, segments });
56334
- const standoffThreadEnvelope = fastenerHole({ size: screwSize, fit: "close", depth: height + 0.8, center: true, segments });
56335
- const coverClearance = placeCutterAtPositions(coverHole, screwPositions, coverThickness / 2);
56336
- const standoffTappedPattern = placeCutterAtPositions(standoffTap, screwPositions, height / 2);
56337
- const standoffThreadEnvelopePattern = placeCutterAtPositions(standoffThreadEnvelope, screwPositions, height / 2);
56338
- const fuseOverlap = Math.min(0.06, Math.max(0.02, wallThickness * 0.02));
56339
- const ledgeThickness = Math.min(Math.max(1.1, coverThickness * 0.45), height * 0.2);
56340
- const sideX = width / 2 - wallThickness / 2;
56341
- const sideY = depth / 2 - wallThickness / 2;
56342
- const ledgeZ = height - ledgeThickness;
56343
- const baseSolids = [
56344
- box(width, depth, baseThickness),
56345
- box(wallThickness, depth, height).translate(sideX, 0, 0),
56346
- box(wallThickness, depth, height).translate(-sideX, 0, 0),
56347
- box(width, wallThickness, height).translate(0, sideY, 0),
56348
- box(width, wallThickness, height).translate(0, -sideY, 0),
56349
- box(ledgeWidth, innerDepth, ledgeThickness).translate(-width / 2 + wallThickness + ledgeWidth / 2, 0, ledgeZ),
56350
- box(ledgeWidth, innerDepth, ledgeThickness).translate(width / 2 - wallThickness - ledgeWidth / 2, 0, ledgeZ),
56351
- box(innerWidth, ledgeWidth, ledgeThickness).translate(0, -depth / 2 + wallThickness + ledgeWidth / 2, ledgeZ),
56352
- box(innerWidth, ledgeWidth, ledgeThickness).translate(0, depth / 2 - wallThickness - ledgeWidth / 2, ledgeZ),
56353
- box(Math.max(1, innerWidth - standoffDiameter * 1.8), ribThickness, ribHeight + fuseOverlap).translate(
56354
- 0,
56355
- 0,
56356
- baseThickness - fuseOverlap
56357
- ),
56358
- box(ribThickness, Math.max(1, innerDepth - standoffDiameter * 1.8), ribHeight + fuseOverlap).translate(
56359
- 0,
56360
- 0,
56361
- baseThickness - fuseOverlap
56362
- ),
56363
- ...screwPositions.map(
56364
- ([x2, y2]) => cylinder(height - baseThickness + fuseOverlap, standoffDiameter / 2, void 0, segments).translate(
56365
- x2,
56366
- y2,
56367
- baseThickness - fuseOverlap
56368
- )
56369
- )
56370
- ];
56371
- const servicePort = box(portWidth, wallThickness + 1, portHeight).translate(
56372
- 0,
56373
- -depth / 2 + wallThickness / 2,
56374
- baseThickness + Math.max(0.8, (height - baseThickness - portHeight) * 0.35)
56375
- );
56376
- const base = union(...baseSolids).subtract(standoffThreadEnvelopePattern).subtract(servicePort).color("#475569");
56377
- const gasketFrameCutter = box(Math.max(1, width - ledgeWidth * 2), Math.max(1, depth - ledgeWidth * 2), gasketThickness + 0.6).translate(
56378
- 0,
56379
- 0,
56380
- -0.3
56381
- );
56382
- const gasket = gasketThickness > 0 ? box(width, depth, gasketThickness).subtract(gasketFrameCutter).subtract(placeCutterAtPositions(coverHole, screwPositions, gasketThickness / 2)).translate(0, 0, height + faceClearance).color("#111827") : null;
56383
- const coverZ = height + faceClearance + (gasket ? gasketThickness + faceClearance : 0);
56384
- const cover = box(width, depth, coverThickness).subtract(coverClearance).translate(0, 0, coverZ).color("#334155");
56385
- const hardware = fastenerSet(screwSize, screwLength, { washerUnderHead: false, washerUnderNut: false, fit: coverFit, segments });
56386
- const screwOriginZ = coverZ + coverThickness;
56387
- const screws = screwPositions.map(([x2, y2]) => hardware.bolt.translate(x2, y2, screwOriginZ).color("#94a3b8"));
56388
- const parts = [
56389
- { name: "datum enclosure base tray with walls ribs standoffs and service port", shape: base },
56390
- ...gasket ? [{ name: "datum enclosure gasket seated on continuous ledge", shape: gasket }] : [],
56391
- { name: "datum enclosure cover plate with matched screw pattern", shape: cover },
56392
- ...screws.map((shape, index2) => ({ name: `installed ${screwSize} enclosure screw ${index2 + 1}`, shape }))
56393
- ];
56394
- return {
56395
- parts,
56396
- base,
56397
- cover,
56398
- gasket,
56399
- screws,
56400
- screwPositions,
56401
- cutters: {
56402
- coverClearance,
56403
- standoffTapped: standoffTappedPattern,
56404
- standoffThreadEnvelope: standoffThreadEnvelopePattern,
56405
- servicePort
56406
- },
56407
- dims: {
56408
- width,
56409
- depth,
56410
- height,
56411
- innerWidth,
56412
- innerDepth,
56413
- wallThickness,
56414
- baseThickness,
56415
- coverThickness,
56416
- ledgeWidth,
56417
- gasketThickness,
56418
- faceClearance,
56419
- screwSize,
56420
- screwLength,
56421
- standoffDiameter,
56422
- ribHeight,
56423
- ribThickness,
56424
- portWidth,
56425
- portHeight,
56426
- clearanceDia: sizeData[coverFit],
56427
- tapDia: sizeData.tap,
56428
- threadEnvelopeDia: sizeData.close
56429
- }
56430
- };
56431
- }
56432
- function snapLatchCoverAssembly(options) {
56433
- const width = requirePositive$6(options.width, "width");
56434
- const depth = requirePositive$6(options.depth, "depth");
56435
- const coverThickness = requirePositive$6(options.coverThickness ?? 2.4, "coverThickness");
56436
- const parentThickness = requirePositive$6(options.parentThickness ?? 6, "parentThickness");
56437
- const ledgeWidth = requirePositive$6(options.ledgeWidth ?? 8, "ledgeWidth");
56438
- const runningClearance = requirePositive$6(options.runningClearance ?? 0.25, "runningClearance");
56439
- const faceClearance = requirePositive$6(options.faceClearance ?? 0.04, "faceClearance");
56440
- const latchWidth = requirePositive$6(options.latchWidth ?? Math.min(width * 0.22, Math.max(12, width * 0.16)), "latchWidth");
56441
- const latchThickness = requirePositive$6(options.latchThickness ?? 1.6, "latchThickness");
56442
- const hookThrow = requirePositive$6(options.hookThrow ?? 3.2, "hookThrow");
56443
- const hookThickness = requirePositive$6(options.hookThickness ?? 1.6, "hookThickness");
56444
- const openingWidth = width - ledgeWidth * 2;
56445
- const openingDepth = depth - ledgeWidth * 2;
56446
- if (openingWidth <= Math.max(8, latchWidth * 0.8) || openingDepth <= 8) {
56447
- throw new Error("snapLatchCoverAssembly: ledgeWidth leaves too little service opening under the cover");
56448
- }
56449
- if (latchWidth >= openingWidth) {
56450
- throw new Error("snapLatchCoverAssembly: latchWidth must fit along the receiver opening");
56451
- }
56452
- if (latchThickness + runningClearance * 2 >= ledgeWidth) {
56453
- throw new Error("snapLatchCoverAssembly: latchThickness and clearance must fit inside the receiver ledge");
56454
- }
56455
- if (hookThrow + latchThickness / 2 + runningClearance >= ledgeWidth * 1.5) {
56456
- throw new Error("snapLatchCoverAssembly: hookThrow is too large for the available underside catch land");
56457
- }
56458
- const parentWidth = width + ledgeWidth * 2;
56459
- const parentDepth = depth + ledgeWidth * 2;
56460
- const fuseOverlap = Math.min(0.04, faceClearance * 0.7);
56461
- const hookClearance = Math.min(0.08, runningClearance * 0.32);
56462
- const coverMinZ = parentThickness + faceClearance;
56463
- const stemMinZ = -hookClearance - hookThickness;
56464
- const stemHeight = coverMinZ + fuseOverlap - stemMinZ;
56465
- const slotY = openingDepth / 2 + ledgeWidth / 2;
56466
- const latchWindow = (sign2) => box(latchWidth + runningClearance * 2, latchThickness + runningClearance * 2, parentThickness + 0.8).translate(0, sign2 * slotY, -0.4);
56467
- const latchWindows = union(latchWindow(1), latchWindow(-1));
56468
- const serviceOpening = box(openingWidth, openingDepth, parentThickness + 1).translate(0, 0, -0.5);
56469
- const parent = box(parentWidth, parentDepth, parentThickness).subtract(serviceOpening).subtract(latchWindows).color("#475569");
56470
- const coverPlate = box(width, depth, coverThickness).translate(0, 0, coverMinZ);
56471
- const snapHook = (sign2) => {
56472
- const y2 = sign2 * slotY;
56473
- const stem = box(latchWidth, latchThickness, stemHeight).translate(0, y2, stemMinZ);
56474
- const barb = box(latchWidth, latchThickness + hookThrow, hookThickness).translate(0, y2 + sign2 * (hookThrow / 2), stemMinZ);
56475
- const rootRib = box(latchWidth, Math.max(latchThickness, hookThrow * 0.55), coverThickness * 0.65).translate(
56476
- 0,
56477
- y2 - sign2 * (ledgeWidth * 0.18),
56478
- coverMinZ
56479
- );
56480
- return union(stem, barb, rootRib);
56481
- };
56482
- const cover = union(coverPlate, snapHook(1), snapHook(-1)).color("#111827");
56483
- const parts = [
56484
- { name: "snap cover receiver frame with latch windows and catch lands", shape: parent },
56485
- { name: "one-piece snap cover with fused hooks and underside barbs", shape: cover }
56486
- ];
56487
- return {
56488
- parts,
56489
- parent,
56490
- cover,
56491
- cutters: {
56492
- serviceOpening,
56493
- latchWindows
56494
- },
56495
- dims: {
56496
- width,
56497
- depth,
56498
- parentWidth,
56499
- parentDepth,
56500
- openingWidth,
56501
- openingDepth,
56502
- coverThickness,
56503
- parentThickness,
56504
- ledgeWidth,
56505
- latchWidth,
56506
- latchThickness,
56507
- hookThrow,
56508
- hookThickness,
56509
- runningClearance,
56510
- faceClearance
56511
- }
56512
- };
56513
- }
56514
- function pinnedLeverAssembly(options) {
56515
- const armLength = requirePositive$6(options.armLength, "armLength");
56516
- const armWidth = requirePositive$6(options.armWidth ?? 10, "armWidth");
56517
- const leverThickness = requirePositive$6(options.leverThickness ?? 5, "leverThickness");
56518
- const pinDiameter = requirePositive$6(options.pinDiameter ?? 5, "pinDiameter");
56519
- const pinClearance = requireNonNegative(options.pinClearance ?? 0.25, "pinClearance");
56520
- const boreDiameter = pinDiameter + pinClearance;
56521
- const hubRadius = requirePositive$6(options.hubRadius ?? Math.max(armWidth * 0.85, pinDiameter * 1.8), "hubRadius");
56522
- const supportThickness = requirePositive$6(options.supportThickness ?? Math.max(6, pinDiameter * 1.4), "supportThickness");
56523
- const supportWidth = requirePositive$6(options.supportWidth ?? hubRadius * 2 + 18, "supportWidth");
56524
- const supportDepth = requirePositive$6(options.supportDepth ?? Math.max(armWidth + 18, hubRadius * 2 + 10), "supportDepth");
56525
- const washerSize = options.washerSize ?? metricWasherSizeForPin(pinDiameter);
56526
- const washerDims = WASHER_TABLE[washerSize];
56527
- if (!washerDims) throw new Error(`pinnedLeverAssembly: unsupported washerSize "${washerSize}"`);
56528
- if (washerDims.id <= pinDiameter) {
56529
- throw new Error(`pinnedLeverAssembly: ${washerSize} washer inner diameter is too small for a ${pinDiameter} mm pin`);
56530
- }
56531
- if (hubRadius <= boreDiameter / 2 + Math.max(1, pinDiameter * 0.25)) {
56532
- throw new Error("pinnedLeverAssembly: hubRadius leaves too little material around the pivot bore");
56533
- }
56534
- if (supportWidth <= boreDiameter + 4 || supportDepth <= boreDiameter + 4) {
56535
- throw new Error("pinnedLeverAssembly: support dimensions leave too little material around the pivot bore");
56536
- }
56537
- const segments = options.segments ?? 40;
56538
- const gripLength = requirePositive$6(options.gripLength ?? Math.min(armLength * 0.32, Math.max(16, armWidth * 2.4)), "gripLength");
56539
- const gripWidth = requirePositive$6(options.gripWidth ?? armWidth * 1.55, "gripWidth");
56540
- if (gripLength >= armLength) throw new Error("pinnedLeverAssembly: gripLength must be shorter than armLength");
56541
- const armOverlap = Math.min(hubRadius * 0.65, armLength * 0.25);
56542
- const armStartX = hubRadius - armOverlap;
56543
- const armCenterX = armStartX + armLength / 2;
56544
- const gripCenterX = armStartX + armLength - gripLength / 2;
56545
- const runningClearance = 0.03;
56546
- const lowerWasherZ = supportThickness + runningClearance;
56547
- const leverZ = lowerWasherZ + washerDims.t + runningClearance;
56548
- const upperWasherZ = leverZ + leverThickness + runningClearance;
56549
- const stackHeight = upperWasherZ + washerDims.t;
56550
- const pinHeadThickness = Math.max(washerDims.t, pinDiameter * 0.35);
56551
- const pinHeadRadius = Math.max(washerDims.od * 0.42, pinDiameter * 0.8);
56552
- const supportBore = cylinder(supportThickness + 1, boreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
56553
- let supportBlank = box(supportWidth, supportDepth, supportThickness);
56554
- if (options.stopBlock ?? true) {
56555
- const stopLength = Math.min(armLength * 0.22, Math.max(10, armWidth * 1.4));
56556
- const stopWidth = Math.max(4, pinDiameter * 0.7);
56557
- const stopHeight = supportThickness;
56558
- const stopX = hubRadius + stopLength / 2;
56559
- const stopY = armWidth / 2 + stopWidth / 2 + runningClearance;
56560
- supportBlank = union(supportBlank, box(stopLength, stopWidth, stopHeight).translate(stopX, stopY, 0));
56561
- }
56562
- const support = supportBlank.subtract(supportBore).color("#475569");
56563
- const hub = cylinder(leverThickness, hubRadius, void 0, segments);
56564
- const arm = box(armLength, armWidth, leverThickness).translate(armCenterX, 0, 0);
56565
- const grip = box(gripLength, gripWidth, leverThickness).translate(gripCenterX, 0, 0);
56566
- const leverSolids = [hub, arm, grip];
56567
- if (options.detentBoss ?? true) {
56568
- const bossRadius = Math.min(armWidth * 0.42, hubRadius * 0.42);
56569
- const bossX = hubRadius + Math.min(armLength * 0.22, armWidth * 2);
56570
- const bossY = -armWidth / 2 - bossRadius * 0.45;
56571
- leverSolids.push(cylinder(leverThickness, bossRadius, void 0, segments).translate(bossX, bossY, 0));
56572
- }
56573
- const leverBore = cylinder(leverThickness + 1, boreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
56574
- const lever = union(...leverSolids).subtract(leverBore).translate(0, 0, leverZ).color("#7f1d1d");
56575
- const lowerWasher = washer(washerSize, { segments }).translate(0, 0, lowerWasherZ).color("#94a3b8");
56576
- const upperWasher = washer(washerSize, { segments }).translate(0, 0, upperWasherZ).color("#94a3b8");
56577
- const shaft = cylinder(stackHeight, pinDiameter / 2, void 0, segments);
56578
- const lowerRetainer = cylinder(pinHeadThickness, pinHeadRadius, void 0, segments).translate(
56579
- 0,
56580
- 0,
56581
- -pinHeadThickness - runningClearance
56582
- );
56583
- const upperHead = cylinder(pinHeadThickness, pinHeadRadius, void 0, segments).translate(0, 0, stackHeight + runningClearance);
56584
- const pin = union(shaft, lowerRetainer, upperHead).color("#cbd5e1");
56585
- const pivotBore = cylinder(stackHeight + 1, boreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
56586
- const parts = [
56587
- { name: "pivot support block with bearing bore and low stop land", shape: support },
56588
- { name: "lower thrust washer under pinned lever", shape: lowerWasher },
56589
- { name: "fused pinned lever with hub arm grip and detent boss", shape: lever },
56590
- { name: "upper thrust washer over pinned lever", shape: upperWasher },
56591
- { name: "retained pivot pin through lever stack", shape: pin }
56592
- ];
56593
- return {
56594
- parts,
56595
- support,
56596
- lever,
56597
- pin,
56598
- washers: {
56599
- lower: lowerWasher,
56600
- upper: upperWasher
56601
- },
56602
- cutters: {
56603
- pivotBore
56604
- },
56605
- dims: {
56606
- armLength,
56607
- armWidth,
56608
- leverThickness,
56609
- hubRadius,
56610
- pinDiameter,
56611
- boreDiameter,
56612
- supportWidth,
56613
- supportDepth,
56614
- supportThickness,
56615
- washerSize,
56616
- washerThickness: washerDims.t,
56617
- stackHeight
56618
- }
56619
- };
56620
- }
56621
- function retainedShaftAssembly(options) {
56622
- const supportSpacing = requirePositive$6(options.supportSpacing, "supportSpacing");
56623
- const shaftDiameter = requirePositive$6(options.shaftDiameter ?? 8, "shaftDiameter");
56624
- const boreClearance = requireNonNegative(options.boreClearance ?? 0.35, "boreClearance");
56625
- const boreDiameter = shaftDiameter + boreClearance;
56626
- const supportThickness = requirePositive$6(options.supportThickness ?? Math.max(5, shaftDiameter * 0.75), "supportThickness");
56627
- const washerSize = options.washerSize ?? metricWasherSizeForPin(shaftDiameter);
56628
- const washerDims = WASHER_TABLE[washerSize];
56629
- if (!washerDims) throw new Error(`retainedShaftAssembly: unsupported washerSize "${washerSize}"`);
56630
- if (washerDims.id <= shaftDiameter) {
56631
- throw new Error(`retainedShaftAssembly: ${washerSize} washer inner diameter is too small for a ${shaftDiameter} mm shaft`);
56632
- }
56633
- const knobDiameter = requirePositive$6(options.knobDiameter ?? shaftDiameter * 3, "knobDiameter");
56634
- const knobThickness = requirePositive$6(options.knobThickness ?? Math.max(8, shaftDiameter), "knobThickness");
56635
- const retainerThickness = requirePositive$6(options.retainerThickness ?? Math.max(washerDims.t, shaftDiameter * 0.35), "retainerThickness");
56636
- const runningClearance = requireNonNegative(options.runningClearance ?? 0.05, "runningClearance");
56637
- const supportWidth = requirePositive$6(options.supportWidth ?? Math.max(28, knobDiameter * 1.25), "supportWidth");
56638
- const supportHeight = requirePositive$6(options.supportHeight ?? Math.max(34, knobDiameter * 1.45), "supportHeight");
56639
- const segments = options.segments ?? 40;
56640
- if (supportSpacing <= supportThickness) {
56641
- throw new Error("retainedShaftAssembly: supportSpacing must leave a gap between support cheeks");
56642
- }
56643
- if (supportWidth <= boreDiameter + 4 || supportHeight <= boreDiameter + 4) {
56644
- throw new Error("retainedShaftAssembly: support dimensions leave too little material around the shaft bore");
56645
- }
56646
- const leftSupportX = -supportSpacing / 2;
56647
- const rightSupportX = supportSpacing / 2;
56648
- const leftOuterFaceX = leftSupportX - supportThickness / 2;
56649
- const rightOuterFaceX = rightSupportX + supportThickness / 2;
56650
- const leftWasherX = leftOuterFaceX - runningClearance - washerDims.t / 2;
56651
- const rightWasherX = rightOuterFaceX + runningClearance + washerDims.t / 2;
56652
- const leftKnobX = leftOuterFaceX - runningClearance * 2 - washerDims.t - knobThickness / 2;
56653
- const rightKnobX = rightOuterFaceX + runningClearance * 2 + washerDims.t + knobThickness / 2;
56654
- const leftStackOuterX = leftKnobX - knobThickness / 2;
56655
- const rightStackOuterX = rightKnobX + knobThickness / 2;
56656
- const minimumShaftLength = rightStackOuterX - leftStackOuterX + retainerThickness * 2 + runningClearance * 2;
56657
- const shaftLength = requirePositive$6(options.shaftLength ?? minimumShaftLength, "shaftLength");
56658
- if (shaftLength < minimumShaftLength) {
56659
- throw new Error("retainedShaftAssembly: shaftLength is too short to retain both supports, washers, and knobs");
56660
- }
56661
- const supportBore = cylinderAlongX(supportThickness + 1, boreDiameter / 2, 0, segments);
56662
- const makeSupport = (x2) => box(supportThickness, supportWidth, supportHeight).translate(x2, 0, -supportHeight / 2).subtract(supportBore.translate(x2, 0, 0)).color("#334155");
56663
- const knobBore = cylinder(knobThickness + 1, boreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
56664
- const makeKnob = (x2) => cylinder(knobThickness, knobDiameter / 2, void 0, 18).subtract(knobBore).pointAlong([1, 0, 0]).translate(x2 - knobThickness / 2, 0, 0).color("#111827");
56665
- const retainerRadius = Math.max(shaftDiameter * 0.85, knobDiameter * 0.36);
56666
- const shaftCore = cylinderAlongX(shaftLength, shaftDiameter / 2, 0, segments);
56667
- const leftRetainer = cylinderAlongX(retainerThickness, retainerRadius, -shaftLength / 2 + retainerThickness / 2, segments);
56668
- const rightRetainer = cylinderAlongX(retainerThickness, retainerRadius, shaftLength / 2 - retainerThickness / 2, segments);
56669
- const shaft = union(shaftCore, leftRetainer, rightRetainer).color("#cbd5e1");
56670
- const leftSupport = makeSupport(leftSupportX);
56671
- const rightSupport = makeSupport(rightSupportX);
56672
- const leftWasher = washerAlongX(washerSize, leftWasherX, segments).color("#94a3b8");
56673
- const rightWasher = washerAlongX(washerSize, rightWasherX, segments).color("#94a3b8");
56674
- const leftKnob = makeKnob(leftKnobX);
56675
- const rightKnob = makeKnob(rightKnobX);
56676
- const shaftBore = cylinderAlongX(supportThickness + knobThickness + 2, boreDiameter / 2, 0, segments);
56677
- const parts = [
56678
- { name: "left bored support cheek for retained shaft", shape: leftSupport },
56679
- { name: "right bored support cheek for retained shaft", shape: rightSupport },
56680
- { name: "retained through shaft with end heads", shape: shaft },
56681
- { name: `left ${washerSize} thrust washer on shaft`, shape: leftWasher },
56682
- { name: `right ${washerSize} thrust washer on shaft`, shape: rightWasher },
56683
- { name: "left retained hand knob with shaft bore", shape: leftKnob },
56684
- { name: "right retained hand knob with shaft bore", shape: rightKnob }
56685
- ];
56686
- return {
56687
- parts,
56688
- supports: {
56689
- left: leftSupport,
56690
- right: rightSupport
56691
- },
56692
- shaft,
56693
- washers: {
56694
- left: leftWasher,
56695
- right: rightWasher
56696
- },
56697
- knobs: {
56698
- left: leftKnob,
56699
- right: rightKnob
56700
- },
56701
- cutters: {
56702
- shaftBore
56703
- },
56704
- dims: {
56705
- supportSpacing,
56706
- supportThickness,
56707
- supportWidth,
56708
- supportHeight,
56709
- shaftDiameter,
56710
- shaftLength,
56711
- boreDiameter,
56712
- washerSize,
56713
- washerThickness: washerDims.t,
56714
- knobDiameter,
56715
- knobThickness,
56716
- retainerThickness,
56717
- runningClearance
56718
- }
56719
- };
56720
- }
56721
- function capturedLinearSlide(options) {
56722
- const length4 = requirePositive$6(options.length, "length");
56723
- const railWidth = requirePositive$6(options.railWidth ?? 38, "railWidth");
56724
- const baseThickness = requirePositive$6(options.baseThickness ?? 2.4, "baseThickness");
56725
- const wallThickness = requirePositive$6(options.wallThickness ?? 2, "wallThickness");
56726
- const wallHeight = requirePositive$6(options.wallHeight ?? 9, "wallHeight");
56727
- const lipWidth = requirePositive$6(options.lipWidth ?? 4, "lipWidth");
56728
- const lipThickness = requirePositive$6(options.lipThickness ?? 1.8, "lipThickness");
56729
- const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
56730
- const endStopLength = requirePositive$6(options.endStopLength ?? 6, "endStopLength");
56731
- const carriageLength = requirePositive$6(options.carriageLength ?? length4 * 0.32, "carriageLength");
56732
- const innerWidth = railWidth - wallThickness * 2;
56733
- const throatWidth = innerWidth - lipWidth * 2;
56734
- if (innerWidth <= 0) throw new Error("capturedLinearSlide: wallThickness leaves no inner rail width");
56735
- if (throatWidth <= 0) throw new Error("capturedLinearSlide: lipWidth closes the rail throat");
56736
- const carriageWidth = requirePositive$6(options.carriageWidth ?? innerWidth - runningClearance * 2, "carriageWidth");
56737
- const carriageThickness = requirePositive$6(options.carriageThickness ?? 4, "carriageThickness");
56738
- if (carriageWidth >= innerWidth - runningClearance) {
56739
- throw new Error("capturedLinearSlide: carriageWidth leaves too little side clearance inside the rail");
56740
- }
56741
- if (carriageWidth <= throatWidth + runningClearance) {
56742
- throw new Error("capturedLinearSlide: carriageWidth must be wider than the lip throat so the rail actually captures it");
56743
- }
56744
- if (carriageThickness + runningClearance * 2 >= wallHeight) {
56745
- throw new Error("capturedLinearSlide: carriage is too tall to clear the return lips");
56746
- }
56747
- const maxTravel = length4 - endStopLength * 2 - carriageLength;
56748
- if (maxTravel <= 0) {
56749
- throw new Error("capturedLinearSlide: rail length, end stops, and carriage length leave no travel");
56750
- }
56751
- const travel = options.travel ?? maxTravel / 2;
56752
- if (!Number.isFinite(travel) || travel < 0 || travel > maxTravel) {
56753
- throw new Error(`capturedLinearSlide: travel must be between 0 and ${maxTravel}`);
56754
- }
56755
- const carriageCenterX = -maxTravel / 2 + travel;
56756
- const fuseOverlap = Math.min(0.04, runningClearance * 0.1);
56757
- const sideY = railWidth / 2 - wallThickness / 2;
56758
- const lipY = railWidth / 2 - wallThickness - lipWidth / 2 + fuseOverlap / 2;
56759
- const stopZ = baseThickness - fuseOverlap;
56760
- const rail2 = union(
56761
- box(length4, railWidth, baseThickness),
56762
- box(length4, wallThickness, wallHeight + fuseOverlap).translate(0, sideY, baseThickness - fuseOverlap),
56763
- box(length4, wallThickness, wallHeight + fuseOverlap).translate(0, -sideY, baseThickness - fuseOverlap),
56764
- box(length4, lipWidth, lipThickness + fuseOverlap).translate(0, lipY, baseThickness + wallHeight - fuseOverlap),
56765
- box(length4, lipWidth, lipThickness + fuseOverlap).translate(0, -lipY, baseThickness + wallHeight - fuseOverlap),
56766
- box(endStopLength, throatWidth, carriageThickness + fuseOverlap).translate(-length4 / 2 + endStopLength / 2, 0, stopZ),
56767
- box(endStopLength, throatWidth, carriageThickness + fuseOverlap).translate(length4 / 2 - endStopLength / 2, 0, stopZ)
56768
- ).color("#475569");
56769
- const carriage = union(
56770
- box(carriageLength, carriageWidth, carriageThickness),
56771
- box(carriageLength * 0.78, throatWidth - runningClearance * 2, Math.max(1, carriageThickness * 0.38)).translate(
56772
- 0,
56773
- 0,
56774
- carriageThickness
56775
- )
56776
- ).translate(carriageCenterX, 0, baseThickness + runningClearance).color("#111827");
56777
- const parts = [
56778
- { name: "captured linear rail with return lips and end stops", shape: rail2 },
56779
- { name: "sliding carriage captured under rail lips", shape: carriage }
56780
- ];
56781
- return {
56782
- parts,
56783
- rail: rail2,
56784
- carriage,
56785
- dims: {
56786
- length: length4,
56787
- railWidth,
56788
- innerWidth,
56789
- throatWidth,
56790
- baseThickness,
56791
- wallThickness,
56792
- wallHeight,
56793
- lipWidth,
56794
- lipThickness,
56795
- carriageLength,
56796
- carriageWidth,
56797
- carriageThickness,
56798
- endStopLength,
56799
- runningClearance,
56800
- maxTravel,
56801
- travel,
56802
- carriageCenterX
56803
- }
56804
- };
56805
- }
56806
- function capturedCartridgeGuideAssembly(options) {
56807
- const length4 = requirePositive$6(options.length, "length");
56808
- const guideWidth = requirePositive$6(options.guideWidth ?? 42, "guideWidth");
56809
- const baseThickness = requirePositive$6(options.baseThickness ?? 3, "baseThickness");
56810
- const wallThickness = requirePositive$6(options.wallThickness ?? 2.5, "wallThickness");
56811
- const wallHeight = requirePositive$6(options.wallHeight ?? 12, "wallHeight");
56812
- const lipWidth = requirePositive$6(options.lipWidth ?? 4, "lipWidth");
56813
- const lipThickness = requirePositive$6(options.lipThickness ?? 2, "lipThickness");
56814
- const rearStopLength = requirePositive$6(options.rearStopLength ?? 7, "rearStopLength");
56815
- const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
56816
- const cartridgeLength = requirePositive$6(options.cartridgeLength ?? length4 * 0.58, "cartridgeLength");
56817
- const cartridgeHeight = requirePositive$6(options.cartridgeHeight ?? 10, "cartridgeHeight");
56818
- const flangeThickness = requirePositive$6(options.flangeThickness ?? 3, "flangeThickness");
56819
- const pullTabLength = requirePositive$6(options.pullTabLength ?? 10, "pullTabLength");
56820
- const innerWidth = guideWidth - wallThickness * 2;
56821
- const throatWidth = innerWidth - lipWidth * 2;
56822
- if (innerWidth <= 0) throw new Error("capturedCartridgeGuideAssembly: wallThickness leaves no inner guide width");
56823
- if (throatWidth <= 0) throw new Error("capturedCartridgeGuideAssembly: lipWidth closes the guide throat");
56824
- if (wallHeight <= lipThickness + flangeThickness + runningClearance * 2) {
56825
- throw new Error("capturedCartridgeGuideAssembly: wallHeight leaves too little vertical capture clearance");
56826
- }
56827
- const cartridgeWidth = requirePositive$6(options.cartridgeWidth ?? innerWidth - runningClearance * 2, "cartridgeWidth");
56828
- const cartridgeBodyWidth = throatWidth - runningClearance * 2;
56829
- if (cartridgeBodyWidth <= 0) {
56830
- throw new Error("capturedCartridgeGuideAssembly: throatWidth and runningClearance leave no cartridge body width");
56831
- }
56832
- if (cartridgeWidth >= innerWidth - runningClearance) {
56833
- throw new Error("capturedCartridgeGuideAssembly: cartridgeWidth leaves too little side clearance inside the guide");
56834
- }
56835
- if (cartridgeWidth <= throatWidth + runningClearance) {
56836
- throw new Error("capturedCartridgeGuideAssembly: cartridge flange must be wider than the guide throat so the cartridge is captured");
56837
- }
56838
- const maxInsertion = length4 - rearStopLength - cartridgeLength;
56839
- if (maxInsertion <= 0) {
56840
- throw new Error("capturedCartridgeGuideAssembly: length, rearStopLength, and cartridgeLength leave no insertion travel");
56841
- }
56842
- const insertion = options.insertion ?? maxInsertion * 0.4;
56843
- if (!Number.isFinite(insertion) || insertion < 0 || insertion > maxInsertion) {
56844
- throw new Error(`capturedCartridgeGuideAssembly: insertion must be between 0 and ${maxInsertion}`);
56845
- }
56846
- const cartridgeCenterX = -length4 / 2 + cartridgeLength / 2 + insertion;
56847
- const fuseOverlap = Math.min(0.04, runningClearance * 0.1);
56848
- const sideY = guideWidth / 2 - wallThickness / 2;
56849
- const lipY = guideWidth / 2 - wallThickness - lipWidth / 2 + fuseOverlap / 2;
56850
- const guide = union(
56851
- box(length4, guideWidth, baseThickness),
56852
- box(length4, wallThickness, wallHeight + fuseOverlap).translate(0, sideY, baseThickness - fuseOverlap),
56853
- box(length4, wallThickness, wallHeight + fuseOverlap).translate(0, -sideY, baseThickness - fuseOverlap),
56854
- box(length4, lipWidth, lipThickness + fuseOverlap).translate(0, lipY, baseThickness + wallHeight - fuseOverlap),
56855
- box(length4, lipWidth, lipThickness + fuseOverlap).translate(0, -lipY, baseThickness + wallHeight - fuseOverlap),
56856
- box(rearStopLength, throatWidth, Math.max(flangeThickness + runningClearance, 4)).translate(
56857
- length4 / 2 - rearStopLength / 2,
56858
- 0,
56859
- baseThickness - fuseOverlap
56860
- )
56861
- ).color("#475569");
56862
- const flangeZ = baseThickness + runningClearance;
56863
- const bodyHeight = Math.max(1, cartridgeHeight - flangeThickness);
56864
- const bodyZ = flangeZ + flangeThickness;
56865
- const tabOverlap = Math.min(0.6, pullTabLength * 0.15);
56866
- const pullTabX = cartridgeCenterX - cartridgeLength / 2 - pullTabLength / 2 + tabOverlap;
56867
- const pullTabWidth = Math.max(cartridgeBodyWidth * 0.55, 12);
56868
- const cartridge = union(
56869
- box(cartridgeLength, cartridgeWidth, flangeThickness).translate(cartridgeCenterX, 0, flangeZ),
56870
- box(cartridgeLength * 0.88, cartridgeBodyWidth, bodyHeight).translate(cartridgeCenterX, 0, bodyZ),
56871
- box(pullTabLength, pullTabWidth, Math.max(flangeThickness, 3)).translate(pullTabX, 0, flangeZ)
56872
- ).color("#111827");
56873
- const parts = [
56874
- { name: "captured cartridge guide with return lips and rear stop", shape: guide },
56875
- { name: "removable cartridge with captured flange and pull tab", shape: cartridge }
56876
- ];
56877
- return {
56878
- parts,
56879
- guide,
56880
- cartridge,
56881
- dims: {
56882
- length: length4,
56883
- guideWidth,
56884
- innerWidth,
56885
- throatWidth,
56886
- baseThickness,
56887
- wallThickness,
56888
- wallHeight,
56889
- lipWidth,
56890
- lipThickness,
56891
- rearStopLength,
56892
- cartridgeLength,
56893
- cartridgeWidth,
56894
- cartridgeBodyWidth,
56895
- cartridgeHeight,
56896
- flangeThickness,
56897
- pullTabLength,
56898
- runningClearance,
56899
- maxInsertion,
56900
- insertion,
56901
- cartridgeCenterX
56902
- }
56903
- };
56904
- }
56905
- function livingHingeCoverAssembly(options) {
56906
- const width = requirePositive$6(options.width, "width");
56907
- const coverDepth = requirePositive$6(options.coverDepth ?? 42, "coverDepth");
56908
- const fixedLeafDepth = requirePositive$6(options.fixedLeafDepth ?? 18, "fixedLeafDepth");
56909
- const leafThickness = requirePositive$6(options.leafThickness ?? 2, "leafThickness");
56910
- const hingeWebWidth = requirePositive$6(options.hingeWebWidth ?? 3.2, "hingeWebWidth");
56911
- const hingeWebThickness = requirePositive$6(options.hingeWebThickness ?? 0.45, "hingeWebThickness");
56912
- const pullLipDepth = requirePositive$6(options.pullLipDepth ?? 5, "pullLipDepth");
56913
- const snapBarbWidth = requirePositive$6(options.snapBarbWidth ?? width * 0.35, "snapBarbWidth");
56914
- const snapBarbDepth = requirePositive$6(options.snapBarbDepth ?? 2.4, "snapBarbDepth");
56915
- const snapBarbHeight = requirePositive$6(options.snapBarbHeight ?? 1.4, "snapBarbHeight");
56916
- const catchLandDepth = requirePositive$6(options.catchLandDepth ?? 2.4, "catchLandDepth");
56917
- if (hingeWebThickness >= leafThickness * 0.55) {
56918
- throw new Error("livingHingeCoverAssembly: hingeWebThickness must be much thinner than the rigid leaves");
56919
- }
56920
- if (hingeWebWidth >= Math.min(coverDepth, fixedLeafDepth) * 0.45) {
56921
- throw new Error("livingHingeCoverAssembly: hingeWebWidth is too wide for the selected leaves");
56922
- }
56923
- if (snapBarbWidth >= width - 2) {
56924
- throw new Error("livingHingeCoverAssembly: snapBarbWidth must leave side material on the cover leaf");
56925
- }
56926
- const fuseOverlap = Math.min(0.04, hingeWebWidth * 0.02);
56927
- const fixedCenterY = -hingeWebWidth / 2 - fixedLeafDepth / 2 + fuseOverlap / 2;
56928
- const coverCenterY = hingeWebWidth / 2 + coverDepth / 2 - fuseOverlap / 2;
56929
- const fixedLeaf = box(width, fixedLeafDepth + fuseOverlap, leafThickness).translate(0, fixedCenterY, 0);
56930
- const movingLeaf = box(width, coverDepth + fuseOverlap, leafThickness).translate(0, coverCenterY, 0);
56931
- const hingeWeb = box(width, hingeWebWidth + fuseOverlap * 2, hingeWebThickness).translate(0, 0, 0);
56932
- const pullLip = box(width * 0.92, pullLipDepth, leafThickness).translate(
56933
- 0,
56934
- coverCenterY + coverDepth / 2 + pullLipDepth / 2 - fuseOverlap,
56935
- 0
56936
- );
56937
- const snapBarb = box(snapBarbWidth, snapBarbDepth, snapBarbHeight).translate(
56938
- 0,
56939
- coverCenterY + coverDepth / 2 - snapBarbDepth / 2,
56940
- leafThickness
56941
- );
56942
- const catchLand = box(width * 0.55, catchLandDepth, Math.max(0.8, leafThickness * 0.45)).translate(
56943
- 0,
56944
- fixedCenterY - fixedLeafDepth / 2 + catchLandDepth / 2,
56945
- leafThickness
56946
- );
56947
- const cover = union(fixedLeaf, movingLeaf, hingeWeb, pullLip, snapBarb, catchLand).color("#0f766e");
56948
- const overallDepth = fixedLeafDepth + hingeWebWidth + coverDepth + pullLipDepth;
56949
- const flexRatio = leafThickness / hingeWebThickness;
56950
- return {
56951
- parts: [{ name: "one-piece molded living hinge cover with snap barb", shape: cover }],
56952
- cover,
56953
- fixedLeaf,
56954
- movingLeaf,
56955
- hingeWeb,
56956
- snapBarb,
56957
- catchLand,
56958
- dims: {
56959
- width,
56960
- coverDepth,
56961
- fixedLeafDepth,
56962
- leafThickness,
56963
- hingeWebWidth,
56964
- hingeWebThickness,
56965
- pullLipDepth,
56966
- snapBarbWidth,
56967
- snapBarbDepth,
56968
- snapBarbHeight,
56969
- catchLandDepth,
56970
- flexRatio,
56971
- overallDepth
56972
- }
56973
- };
56974
- }
56975
- function knuckledHingeAssembly(options) {
56976
- const length4 = requirePositive$6(options.length, "length");
56977
- const leafLength = requirePositive$6(options.leafLength ?? 36, "leafLength");
56978
- const leafThickness = requirePositive$6(options.leafThickness ?? 1.6, "leafThickness");
56979
- const barrelOuterRadius = requirePositive$6(options.barrelOuterRadius ?? 3, "barrelOuterRadius");
56980
- const pinDiameter = requirePositive$6(options.pinDiameter ?? 2, "pinDiameter");
56981
- const pinClearance = requireNonNegative(options.pinClearance ?? 0.25, "pinClearance");
56982
- const boreDiameter = pinDiameter + pinClearance;
56983
- const knuckleGap = requireNonNegative(options.knuckleGap ?? 0.45, "knuckleGap");
56984
- const openAngleDeg = Number.isFinite(options.openAngleDeg ?? 35) ? options.openAngleDeg ?? 35 : 35;
56985
- const retainerThickness = requirePositive$6(options.retainerThickness ?? Math.max(leafThickness, pinDiameter * 0.7), "retainerThickness");
56986
- const segments = options.segments ?? 36;
56987
- const knuckleCount = options.knuckleCount ?? 5;
56988
- if (!Number.isInteger(knuckleCount) || knuckleCount < 3 || knuckleCount % 2 === 0) {
56989
- throw new Error("knuckledHingeAssembly: knuckleCount must be an odd integer >= 3");
56990
- }
56991
- if (barrelOuterRadius <= boreDiameter / 2 + Math.max(0.35, pinDiameter * 0.18)) {
56992
- throw new Error("knuckledHingeAssembly: barrelOuterRadius leaves too little wall around the pin bore");
56993
- }
56994
- const knuckleLength = (length4 - knuckleGap * (knuckleCount - 1)) / knuckleCount;
56995
- if (knuckleLength <= pinDiameter * 1.4) {
56996
- throw new Error("knuckledHingeAssembly: length, knuckleCount, and knuckleGap make knuckles too short");
56997
- }
56998
- const leafRootClearance = Math.max(0.12, Math.min(knuckleGap * 0.35, 0.35));
56999
- const barrelLeafOverlap = Math.min(barrelOuterRadius * 0.18, leafThickness * 0.35);
57000
- const bridgeDepth = leafRootClearance + barrelLeafOverlap + 0.2;
57001
- const fixedLeafPlate = box(length4, leafLength, leafThickness).translate(
57002
- 0,
57003
- barrelOuterRadius + leafRootClearance + leafLength / 2,
57004
- -leafThickness / 2
57005
- );
57006
- const movingLeafPlate = box(length4, leafLength, leafThickness).translate(
57007
- 0,
57008
- -barrelOuterRadius - leafRootClearance - leafLength / 2,
57009
- -leafThickness / 2
57010
- );
57011
- const fixedKnuckles = [];
57012
- const movingKnuckles = [];
57013
- const fixedBridges = [];
57014
- const movingBridges = [];
57015
- for (let index2 = 0; index2 < knuckleCount; index2 += 1) {
57016
- const xStart = -length4 / 2 + index2 * (knuckleLength + knuckleGap);
57017
- const xCenter = xStart + knuckleLength / 2;
57018
- const knuckle = tubeAlongX(knuckleLength, barrelOuterRadius, boreDiameter / 2, xCenter, segments);
57019
- if (index2 % 2 === 0) {
57020
- fixedKnuckles.push(knuckle);
57021
- fixedBridges.push(
57022
- box(knuckleLength, bridgeDepth, leafThickness).translate(
57023
- xCenter,
57024
- barrelOuterRadius - barrelLeafOverlap + bridgeDepth / 2,
57025
- -leafThickness / 2
57026
- )
57027
- );
57028
- } else {
57029
- movingKnuckles.push(knuckle);
57030
- movingBridges.push(
57031
- box(knuckleLength, bridgeDepth, leafThickness).translate(
57032
- xCenter,
57033
- -barrelOuterRadius + barrelLeafOverlap - bridgeDepth / 2,
57034
- -leafThickness / 2
57035
- )
57036
- );
57037
- }
57038
- }
57039
- const fixedLeaf = union(fixedLeafPlate, ...fixedKnuckles, ...fixedBridges).color("#475569");
57040
- const movingLeaf = union(movingLeafPlate, ...movingKnuckles, ...movingBridges).rotateX(openAngleDeg).color("#111827");
57041
- const pinCore = cylinderAlongX(length4 + retainerThickness * 2, pinDiameter / 2, 0, segments);
57042
- const retainerRadius = Math.max(barrelOuterRadius * 0.85, pinDiameter);
57043
- const leftHead = cylinderAlongX(retainerThickness, retainerRadius, -length4 / 2 - retainerThickness / 2, segments);
57044
- const rightHead = cylinderAlongX(retainerThickness, retainerRadius, length4 / 2 + retainerThickness / 2, segments);
57045
- const pin = union(pinCore, leftHead, rightHead).color("#cbd5e1");
57046
- const pinBore = cylinderAlongX(length4 + retainerThickness * 2, boreDiameter / 2, 0, segments);
57047
- const parts = [
57048
- { name: "fixed hinge leaf with alternating knuckles", shape: fixedLeaf },
57049
- { name: "moving hinge leaf with alternating knuckles", shape: movingLeaf },
57050
- { name: "retained hinge pin through knuckle stack", shape: pin }
57051
- ];
57052
- return {
57053
- parts,
57054
- fixedLeaf,
57055
- movingLeaf,
57056
- pin,
57057
- cutters: {
57058
- pinBore
57059
- },
57060
- dims: {
57061
- length: length4,
57062
- leafLength,
57063
- leafThickness,
57064
- barrelOuterRadius,
57065
- pinDiameter,
57066
- boreDiameter,
57067
- knuckleGap,
57068
- knuckleCount,
57069
- knuckleLength,
57070
- openAngleDeg,
57071
- retainerThickness
57072
- }
57073
- };
57074
- }
57075
- function clevisPinJointAssembly(options = {}) {
57076
- const pinDiameter = requirePositive$6(options.pinDiameter ?? 4, "pinDiameter");
57077
- const pinClearance = requireNonNegative(options.pinClearance ?? 0.3, "pinClearance");
57078
- const boreDiameter = pinDiameter + pinClearance;
57079
- const linkThickness = requirePositive$6(options.linkThickness ?? Math.max(5, pinDiameter * 1.5), "linkThickness");
57080
- const earThickness = requirePositive$6(options.earThickness ?? Math.max(3.5, pinDiameter), "earThickness");
57081
- const runningClearance = requireNonNegative(options.runningClearance ?? 0.25, "runningClearance");
57082
- const linkArmWidth = requirePositive$6(options.linkArmWidth ?? pinDiameter * 2.4, "linkArmWidth");
57083
- const eyeOuterRadius = requirePositive$6(options.eyeOuterRadius ?? Math.max(pinDiameter * 1.8, linkArmWidth / 2 + 1.4), "eyeOuterRadius");
57084
- const earLength = requirePositive$6(options.earLength ?? Math.max(eyeOuterRadius * 2.55, pinDiameter * 4.2), "earLength");
57085
- const earHeight = requirePositive$6(options.earHeight ?? Math.max(eyeOuterRadius * 2.25, pinDiameter * 4.4), "earHeight");
57086
- const linkArmLength = requirePositive$6(options.linkArmLength ?? 34, "linkArmLength");
57087
- const retainerThickness = requirePositive$6(options.retainerThickness ?? Math.max(1.2, pinDiameter * 0.35), "retainerThickness");
57088
- const segments = options.segments ?? 40;
57089
- if (eyeOuterRadius <= boreDiameter / 2 + Math.max(0.8, pinDiameter * 0.25)) {
57090
- throw new Error("clevisPinJointAssembly: eyeOuterRadius leaves too little material around the pin bore");
57091
- }
57092
- if (earHeight <= boreDiameter + Math.max(3, pinDiameter)) {
57093
- throw new Error("clevisPinJointAssembly: earHeight leaves too little material around the pin bore");
57094
- }
57095
- if (earLength / 2 <= eyeOuterRadius + runningClearance) {
57096
- throw new Error("clevisPinJointAssembly: earLength must extend behind the link eye for a rear clevis bridge");
57097
- }
57098
- const clevisGap = linkThickness + runningClearance * 2;
57099
- const earCenterY = clevisGap / 2 + earThickness / 2;
57100
- const totalStackY = clevisGap + earThickness * 2;
57101
- const pinLength = totalStackY + retainerThickness * 2 + runningClearance * 2;
57102
- const bridgeClearX = -eyeOuterRadius - runningClearance;
57103
- const bridgeLength = Math.max(pinDiameter * 2.2, 4);
57104
- const bridgeHeight = Math.min(earHeight * 0.48, Math.max(pinDiameter * 1.4, eyeOuterRadius * 0.75));
57105
- const bridgeCenterX = bridgeClearX - bridgeLength / 2;
57106
- const bridgeCenterZ = -earHeight / 2 + bridgeHeight / 2;
57107
- const pinBore = cylinderAlongY(totalStackY + 0.8, boreDiameter / 2, 0, segments);
57108
- const clevisBlank = union(
57109
- box(earLength, earThickness, earHeight).translate(0, earCenterY, -earHeight / 2),
57110
- box(earLength, earThickness, earHeight).translate(0, -earCenterY, -earHeight / 2),
57111
- box(bridgeLength, totalStackY, bridgeHeight).translate(bridgeCenterX, 0, bridgeCenterZ)
57112
- );
57113
- const clevis = clevisBlank.subtract(pinBore).color("#475569");
57114
- const eye = tubeAlongY(linkThickness, eyeOuterRadius, boreDiameter / 2, 0, segments);
57115
- const armOverlap = Math.min(eyeOuterRadius * 0.65, linkArmLength * 0.25);
57116
- const armCenterX = eyeOuterRadius - armOverlap + linkArmLength / 2;
57117
- const linkArm = box(linkArmLength, linkThickness, linkArmWidth).translate(armCenterX, 0, -linkArmWidth / 2);
57118
- const link = union(eye, linkArm).color("#111827");
57119
- const pinCore = cylinderAlongY(pinLength, pinDiameter / 2, 0, segments);
57120
- const headRadius = Math.max(pinDiameter * 0.9, boreDiameter / 2 + 0.8);
57121
- const headY = totalStackY / 2 + runningClearance + retainerThickness / 2;
57122
- const headA = cylinderAlongY(retainerThickness, headRadius, headY, segments);
57123
- const headB = cylinderAlongY(retainerThickness, headRadius, -headY, segments);
57124
- const pin = union(pinCore, headA, headB).color("#cbd5e1");
57125
- const cutter = cylinderAlongY(pinLength + 1, boreDiameter / 2, 0, segments);
57126
- const parts = [
57127
- { name: "bored clevis yoke with rear bridge", shape: clevis },
57128
- { name: "center link eye captured in clevis", shape: link },
57129
- { name: "retained clevis pin through link eye", shape: pin }
57130
- ];
57131
- return {
57132
- parts,
57133
- clevis,
57134
- link,
57135
- pin,
57136
- cutters: {
57137
- pinBore: cutter
57138
- },
57139
- dims: {
57140
- pinDiameter,
57141
- boreDiameter,
57142
- linkThickness,
57143
- earThickness,
57144
- runningClearance,
57145
- earLength,
57146
- earHeight,
57147
- linkArmLength,
57148
- linkArmWidth,
57149
- eyeOuterRadius,
57150
- retainerThickness,
57151
- pinLength,
57152
- clevisGap
57153
- }
57154
- };
57155
- }
57156
- function seatedBearingAssembly(options) {
57157
- const bearingOuterDiameter = requirePositive$6(options.bearingOuterDiameter, "bearingOuterDiameter");
57158
- const bearingInnerDiameter = requirePositive$6(options.bearingInnerDiameter, "bearingInnerDiameter");
57159
- const bearingWidth = requirePositive$6(options.bearingWidth, "bearingWidth");
57160
- const shaftDiameter = requirePositive$6(options.shaftDiameter ?? Math.max(1, bearingInnerDiameter - 0.4), "shaftDiameter");
57161
- const pocketClearance = requireNonNegative(options.pocketClearance ?? 0.2, "pocketClearance");
57162
- const shaftClearance = requireNonNegative(options.shaftClearance ?? 0.35, "shaftClearance");
57163
- const runningClearance = requireNonNegative(options.runningClearance ?? 0.05, "runningClearance");
57164
- const housingThickness = requirePositive$6(options.housingThickness ?? bearingWidth + 5, "housingThickness");
57165
- const bossHeight = requirePositive$6(options.bossHeight ?? Math.max(2, bearingWidth * 0.45), "bossHeight");
57166
- const bossOuterDiameter = requirePositive$6(
57167
- options.bossOuterDiameter ?? bearingOuterDiameter + Math.max(8, bearingOuterDiameter * 0.36),
57168
- "bossOuterDiameter"
57169
- );
57170
- const housingWidth = requirePositive$6(
57171
- options.housingWidth ?? Math.max(bossOuterDiameter + 12, bearingOuterDiameter * 2.1),
57172
- "housingWidth"
57173
- );
57174
- const housingDepth = requirePositive$6(
57175
- options.housingDepth ?? Math.max(bossOuterDiameter + 12, bearingOuterDiameter * 1.8),
57176
- "housingDepth"
57177
- );
57178
- const shaftOverhang = requirePositive$6(options.shaftOverhang ?? Math.max(8, bearingOuterDiameter * 0.45), "shaftOverhang");
57179
- const shoulderDiameter = requirePositive$6(
57180
- options.shoulderDiameter ?? Math.max(shaftDiameter * 1.65, bearingInnerDiameter + 2),
57181
- "shoulderDiameter"
57182
- );
57183
- const shoulderThickness = requirePositive$6(options.shoulderThickness ?? Math.max(1.5, shaftDiameter * 0.32), "shoulderThickness");
57184
- const segments = options.segments ?? 48;
57185
- if (bearingOuterDiameter <= bearingInnerDiameter + Math.max(1, bearingOuterDiameter * 0.08)) {
57186
- throw new Error("seatedBearingAssembly: bearingOuterDiameter leaves too little bearing wall around the bore");
57187
- }
57188
- if (shaftDiameter + shaftClearance >= bearingInnerDiameter) {
57189
- throw new Error("seatedBearingAssembly: shaftDiameter plus shaftClearance must fit inside the bearing bore");
57190
- }
57191
- if (shoulderDiameter >= bearingOuterDiameter - runningClearance * 2) {
57192
- throw new Error("seatedBearingAssembly: shoulderDiameter must stay smaller than the bearing outer race");
57193
- }
57194
- const pocketDiameter = bearingOuterDiameter + pocketClearance;
57195
- const shaftBoreDiameter = shaftDiameter + shaftClearance;
57196
- const totalHousingHeight = housingThickness + bossHeight;
57197
- const pocketDepth = bearingWidth + runningClearance * 2;
57198
- if (pocketDepth >= totalHousingHeight - runningClearance) {
57199
- throw new Error("seatedBearingAssembly: housingThickness and bossHeight must leave a shoulder below the bearing pocket");
57200
- }
57201
- if (bossOuterDiameter <= pocketDiameter + Math.max(2, bearingOuterDiameter * 0.12)) {
57202
- throw new Error("seatedBearingAssembly: bossOuterDiameter leaves too little wall around the bearing pocket");
57203
- }
57204
- if (housingWidth <= pocketDiameter + 6 || housingDepth <= pocketDiameter + 6) {
57205
- throw new Error("seatedBearingAssembly: housing dimensions leave too little material around the bearing pocket");
57206
- }
57207
- if (shoulderThickness * 2 + runningClearance * 2 >= shaftOverhang) {
57208
- throw new Error("seatedBearingAssembly: shaftOverhang must leave room for retaining collars outside the housing");
57209
- }
57210
- const pocketBottomZ = totalHousingHeight - pocketDepth;
57211
- const bearingZ = pocketBottomZ + runningClearance;
57212
- const lowerShoulderZ = -runningClearance - shoulderThickness;
57213
- const upperShoulderZ = totalHousingHeight + runningClearance;
57214
- const shaftLength = totalHousingHeight + shaftOverhang * 2;
57215
- const bossFuseOverlap = Math.min(0.08, Math.max(0.02, bossHeight * 0.03));
57216
- const bearingPocket = cylinder(pocketDepth + 0.4, pocketDiameter / 2, void 0, segments).translate(0, 0, pocketBottomZ - 0.2);
57217
- const shaftBore = cylinder(totalHousingHeight + 1, shaftBoreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
57218
- const housingBase = box(housingWidth, housingDepth, housingThickness).subtract(bearingPocket).subtract(shaftBore);
57219
- const housingBoss = cylinder(bossHeight + bossFuseOverlap, bossOuterDiameter / 2, void 0, segments).translate(0, 0, housingThickness - bossFuseOverlap).subtract(bearingPocket);
57220
- const housing = union(housingBase, housingBoss).color("#475569");
57221
- const bearingRing = tubeAlongZ(bearingWidth, bearingOuterDiameter / 2, bearingInnerDiameter / 2, segments);
57222
- const shieldInset = Math.min(bearingWidth * 0.18, 0.7);
57223
- const shieldOuterRadius = bearingOuterDiameter / 2 - Math.max(0.45, (bearingOuterDiameter - bearingInnerDiameter) * 0.08);
57224
- const shieldInnerRadius = bearingInnerDiameter / 2 + Math.max(0.2, (bearingOuterDiameter - bearingInnerDiameter) * 0.035);
57225
- const bearingShield = shieldOuterRadius > shieldInnerRadius + 0.2 ? union(
57226
- tubeAlongZ(Math.min(0.35, bearingWidth * 0.08), shieldOuterRadius, shieldInnerRadius, segments).translate(0, 0, shieldInset),
57227
- tubeAlongZ(Math.min(0.35, bearingWidth * 0.08), shieldOuterRadius, shieldInnerRadius, segments).translate(
57228
- 0,
57229
- 0,
57230
- bearingWidth - shieldInset - Math.min(0.35, bearingWidth * 0.08)
57231
- )
57232
- ) : null;
57233
- const bearing = (bearingShield ? union(bearingRing, bearingShield) : bearingRing).translate(0, 0, bearingZ).color("#111827");
57234
- const shaftCore = cylinder(shaftLength, shaftDiameter / 2, void 0, segments).translate(0, 0, -shaftOverhang);
57235
- const lowerShoulder = cylinder(shoulderThickness, shoulderDiameter / 2, void 0, segments).translate(0, 0, lowerShoulderZ);
57236
- const upperShoulder = cylinder(shoulderThickness, shoulderDiameter / 2, void 0, segments).translate(0, 0, upperShoulderZ);
57237
- const shaft = union(shaftCore, lowerShoulder, upperShoulder).color("#cbd5e1");
57238
- const parts = [
57239
- { name: "bearing housing with counterbore pocket and shoulder", shape: housing },
57240
- { name: "purchased radial bearing seated in counterbore", shape: bearing },
57241
- { name: "shaft through bearing bore with retaining collars", shape: shaft }
57242
- ];
57243
- return {
57244
- parts,
57245
- housing,
57246
- bearing,
57247
- shaft,
57248
- cutters: {
57249
- bearingPocket,
57250
- shaftBore
57251
- },
57252
- dims: {
57253
- bearingOuterDiameter,
57254
- bearingInnerDiameter,
57255
- bearingWidth,
57256
- shaftDiameter,
57257
- housingWidth,
57258
- housingDepth,
57259
- housingThickness,
57260
- bossOuterDiameter,
57261
- bossHeight,
57262
- totalHousingHeight,
57263
- pocketDiameter,
57264
- pocketDepth,
57265
- shaftBoreDiameter,
57266
- runningClearance,
57267
- shaftLength,
57268
- shoulderDiameter,
57269
- shoulderThickness
57270
- }
57271
- };
57272
- }
57273
- function cableGlandAnchorAssembly(options) {
57274
- const cableDiameter = requirePositive$6(options.cableDiameter, "cableDiameter");
57275
- const panelThickness = requirePositive$6(options.panelThickness ?? 3, "panelThickness");
57276
- const panelWidth = requirePositive$6(options.panelWidth ?? Math.max(54, cableDiameter * 7), "panelWidth");
57277
- const panelHeight = requirePositive$6(options.panelHeight ?? Math.max(38, cableDiameter * 5), "panelHeight");
57278
- const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
57279
- const panelHoleClearance = requirePositive$6(options.panelHoleClearance ?? 0.25, "panelHoleClearance");
57280
- const cableBoreDiameter = cableDiameter + runningClearance * 2;
57281
- const glandOuterDiameter = requirePositive$6(
57282
- options.glandOuterDiameter ?? cableDiameter + Math.max(6, cableDiameter * 0.9),
57283
- "glandOuterDiameter"
57284
- );
57285
- const nutOuterDiameter = requirePositive$6(
57286
- options.nutOuterDiameter ?? glandOuterDiameter + Math.max(6, cableDiameter * 0.8),
57287
- "nutOuterDiameter"
57288
- );
57289
- const nutThickness = requirePositive$6(options.nutThickness ?? Math.max(4, cableDiameter * 0.8), "nutThickness");
57290
- const flangeDiameter = requirePositive$6(options.flangeDiameter ?? glandOuterDiameter + Math.max(5, cableDiameter * 0.7), "flangeDiameter");
57291
- const flangeThickness = requirePositive$6(options.flangeThickness ?? Math.max(2, panelThickness * 0.45), "flangeThickness");
57292
- const minGlandLength = panelThickness + nutThickness + flangeThickness + runningClearance * 4;
57293
- const glandLength = requirePositive$6(options.glandLength ?? minGlandLength + Math.max(8, cableDiameter), "glandLength");
57294
- const cableLength = requirePositive$6(options.cableLength ?? glandLength + Math.max(36, cableDiameter * 5), "cableLength");
57295
- const segments = options.segments ?? 40;
57296
- if (glandOuterDiameter <= cableBoreDiameter + Math.max(1.2, cableDiameter * 0.18)) {
57297
- throw new Error("cableGlandAnchorAssembly: glandOuterDiameter leaves too little wall around the cable bore");
57298
- }
57299
- if (nutOuterDiameter <= glandOuterDiameter + Math.max(1.5, cableDiameter * 0.2)) {
57300
- throw new Error("cableGlandAnchorAssembly: nutOuterDiameter must leave material around the gland body");
57301
- }
57302
- if (flangeDiameter <= glandOuterDiameter + Math.max(1.2, cableDiameter * 0.16)) {
57303
- throw new Error("cableGlandAnchorAssembly: flangeDiameter must be larger than the gland body");
57304
- }
57305
- if (panelWidth <= flangeDiameter + 8 || panelHeight <= flangeDiameter + 8) {
57306
- throw new Error("cableGlandAnchorAssembly: panel dimensions leave too little material around the gland hole");
57307
- }
57308
- if (glandLength <= minGlandLength) {
57309
- throw new Error("cableGlandAnchorAssembly: glandLength must span the panel, flange, compression nut, and clearances");
57310
- }
57311
- if (cableLength <= glandLength + runningClearance * 2) {
57312
- throw new Error("cableGlandAnchorAssembly: cableLength must extend beyond the gland body");
57313
- }
57314
- const panelHoleDiameter = glandOuterDiameter + panelHoleClearance * 2;
57315
- const glandOuterRadius = glandOuterDiameter / 2;
57316
- const cableBoreRadius = cableBoreDiameter / 2;
57317
- const faceClearance = Math.min(0.05, runningClearance * 0.15);
57318
- const flangePocketDepth = Math.min(Math.max(0.35, panelThickness * 0.18), panelThickness * 0.4, flangeThickness * 0.55);
57319
- const panelHole = cylinderAlongX(panelThickness + 0.8, panelHoleDiameter / 2, 0, segments);
57320
- const flangeSeatPocket = cylinderAlongX(
57321
- flangePocketDepth + 0.2,
57322
- flangeDiameter / 2 + panelHoleClearance,
57323
- panelThickness / 2 - flangePocketDepth / 2,
57324
- segments
57325
- );
57326
- const cableBore = cylinderAlongX(glandLength + 0.8, cableBoreRadius, 0, segments);
57327
- const panel = box(panelThickness, panelWidth, panelHeight).translate(0, 0, -panelHeight / 2).subtract(panelHole).subtract(flangeSeatPocket).color("#475569");
57328
- const glandBody = tubeAlongX(glandLength, glandOuterRadius, cableBoreRadius, 0, segments);
57329
- const flangeCenterX = panelThickness / 2 - flangePocketDepth + faceClearance + flangeThickness / 2;
57330
- const flange = tubeAlongX(flangeThickness, flangeDiameter / 2, cableBoreRadius, flangeCenterX, segments);
57331
- const gland = union(glandBody, flange).color("#94a3b8");
57332
- const nutInnerRadius = glandOuterRadius + Math.min(0.12, runningClearance * 0.4);
57333
- const nutCenterX = -panelThickness / 2 - faceClearance - nutThickness / 2;
57334
- const compressionNut = tubeAlongX(nutThickness, nutOuterDiameter / 2, nutInnerRadius, nutCenterX, segments).color("#cbd5e1");
57335
- const cable = cylinderAlongX(cableLength, cableDiameter / 2, 0, segments).color("#111827");
57336
- const parts = [
57337
- { name: "panel with gland clearance hole", shape: panel },
57338
- { name: "hollow cable gland body with panel flange", shape: gland },
57339
- { name: "compression nut around gland body", shape: compressionNut },
57340
- { name: "routed cable through gland bore", shape: cable }
57341
- ];
57342
- return {
57343
- parts,
57344
- panel,
57345
- gland,
57346
- compressionNut,
57347
- cable,
57348
- cutters: {
57349
- panelHole,
57350
- flangeSeatPocket,
57351
- cableBore
57352
- },
57353
- dims: {
57354
- cableDiameter,
57355
- cableBoreDiameter,
57356
- panelThickness,
57357
- panelWidth,
57358
- panelHeight,
57359
- glandOuterDiameter,
57360
- glandLength,
57361
- nutOuterDiameter,
57362
- nutThickness,
57363
- flangeDiameter,
57364
- flangeThickness,
57365
- runningClearance,
57366
- faceClearance,
57367
- flangePocketDepth,
57368
- panelHoleDiameter,
57369
- cableLength
57370
- }
57371
- };
57372
- }
57373
- function hoseBarbPortAssembly(options) {
57374
- const hoseInnerDiameter = requirePositive$6(options.hoseInnerDiameter, "hoseInnerDiameter");
57375
- const runningClearance = requirePositive$6(options.runningClearance ?? 0.18, "runningClearance");
57376
- const faceClearance = requirePositive$6(options.faceClearance ?? 0.04, "faceClearance");
57377
- const barbRootDiameter = requirePositive$6(
57378
- options.barbRootDiameter ?? Math.max(1, hoseInnerDiameter - Math.max(0.25, hoseInnerDiameter * 0.06)),
57379
- "barbRootDiameter"
57380
- );
57381
- const barbPeakDiameter = requirePositive$6(
57382
- options.barbPeakDiameter ?? hoseInnerDiameter + Math.max(0.65, hoseInnerDiameter * 0.12),
57383
- "barbPeakDiameter"
57384
- );
57385
- const installedHoseBoreDiameter = barbPeakDiameter + runningClearance * 2;
57386
- const hoseOuterDiameter = requirePositive$6(
57387
- options.hoseOuterDiameter ?? Math.max(installedHoseBoreDiameter + 2.4, hoseInnerDiameter + Math.max(3, hoseInnerDiameter * 0.55)),
57388
- "hoseOuterDiameter"
57389
- );
57390
- const fluidBoreDiameter = requirePositive$6(options.fluidBoreDiameter ?? hoseInnerDiameter * 0.65, "fluidBoreDiameter");
57391
- const blockThickness = requirePositive$6(options.blockThickness ?? Math.max(7, hoseInnerDiameter * 1.2), "blockThickness");
57392
- const barbCount = options.barbCount ?? 3;
57393
- const barbLength = requirePositive$6(options.barbLength ?? Math.max(2.6, hoseInnerDiameter * 0.55), "barbLength");
57394
- const barbStackLength = barbCount * barbLength;
57395
- const shoulderDiameter = requirePositive$6(
57396
- options.shoulderDiameter ?? barbPeakDiameter + Math.max(4, hoseInnerDiameter * 0.65),
57397
- "shoulderDiameter"
57398
- );
57399
- const shoulderThickness = requirePositive$6(options.shoulderThickness ?? Math.max(2, hoseInnerDiameter * 0.35), "shoulderThickness");
57400
- const bossDiameter = requirePositive$6(options.bossDiameter ?? shoulderDiameter + Math.max(4, hoseInnerDiameter * 0.6), "bossDiameter");
57401
- const bossHeight = requirePositive$6(options.bossHeight ?? Math.max(2.4, hoseInnerDiameter * 0.45), "bossHeight");
57402
- const blockWidth = requirePositive$6(options.blockWidth ?? bossDiameter + Math.max(14, hoseInnerDiameter * 2.4), "blockWidth");
57403
- const blockHeight = requirePositive$6(options.blockHeight ?? bossDiameter + Math.max(12, hoseInnerDiameter * 2.1), "blockHeight");
57404
- const hoseLength = requirePositive$6(options.hoseLength ?? barbStackLength + Math.max(32, hoseInnerDiameter * 5), "hoseLength");
57405
- const clampWidth = requirePositive$6(options.clampWidth ?? Math.max(4, hoseOuterDiameter * 0.45), "clampWidth");
57406
- const clampThickness = requirePositive$6(options.clampThickness ?? 0.9, "clampThickness");
57407
- const segments = options.segments ?? 40;
57408
- if (!Number.isInteger(barbCount) || barbCount < 1 || barbCount > 8) {
57409
- throw new Error("hoseBarbPortAssembly: barbCount must be an integer from 1 to 8");
57410
- }
57411
- if (barbPeakDiameter <= hoseInnerDiameter) {
57412
- throw new Error("hoseBarbPortAssembly: barbPeakDiameter must exceed hoseInnerDiameter so the barb retains the hose");
57413
- }
57414
- if (barbRootDiameter >= barbPeakDiameter - Math.max(0.25, hoseInnerDiameter * 0.04)) {
57415
- throw new Error("hoseBarbPortAssembly: barbRootDiameter must leave a visible barb rise");
57416
- }
57417
- if (fluidBoreDiameter >= barbRootDiameter - Math.max(0.8, hoseInnerDiameter * 0.12)) {
57418
- throw new Error("hoseBarbPortAssembly: fluidBoreDiameter leaves too little wall in the barb fitting");
57419
- }
57420
- if (hoseOuterDiameter <= installedHoseBoreDiameter + Math.max(1.2, hoseInnerDiameter * 0.16)) {
57421
- throw new Error("hoseBarbPortAssembly: hoseOuterDiameter leaves too little hose wall around the installed barb envelope");
57422
- }
57423
- if (shoulderDiameter <= barbPeakDiameter + Math.max(1.5, hoseInnerDiameter * 0.2)) {
57424
- throw new Error("hoseBarbPortAssembly: shoulderDiameter must be larger than the barb peaks");
57425
- }
57426
- if (bossDiameter <= shoulderDiameter + Math.max(1.5, hoseInnerDiameter * 0.2)) {
57427
- throw new Error("hoseBarbPortAssembly: bossDiameter must leave material around the shoulder seat");
57428
- }
57429
- if (blockWidth <= bossDiameter + 8 || blockHeight <= bossDiameter + 8) {
57430
- throw new Error("hoseBarbPortAssembly: receiver block dimensions leave too little material around the port boss");
57431
- }
57432
- const portBoreDiameter = barbRootDiameter + runningClearance * 2;
57433
- const portBore = cylinderAlongX(blockThickness + bossHeight + 0.8, portBoreDiameter / 2, bossHeight / 2, segments);
57434
- const fuseOverlap = Math.min(0.04, faceClearance * 0.7);
57435
- const bossCenterX = blockThickness / 2 + bossHeight / 2 - fuseOverlap;
57436
- const receiver = union(
57437
- box(blockThickness, blockWidth, blockHeight).translate(0, 0, -blockHeight / 2),
57438
- cylinderAlongX(bossHeight + fuseOverlap, bossDiameter / 2, bossCenterX, segments)
57439
- ).subtract(portBore).color("#475569");
57440
- const bossFaceX = blockThickness / 2 + bossHeight;
57441
- const shoulderCenterX = bossFaceX + faceClearance + shoulderThickness / 2;
57442
- const barbStartX = shoulderCenterX + shoulderThickness / 2;
57443
- const fittingStartX = -blockThickness / 2 - runningClearance;
57444
- const fittingEndX = barbStartX + barbStackLength;
57445
- const fittingCore = tubeAlongX(
57446
- fittingEndX - fittingStartX,
57447
- barbRootDiameter / 2,
57448
- fluidBoreDiameter / 2,
57449
- (fittingStartX + fittingEndX) / 2,
57450
- segments
57451
- );
57452
- const shoulder = tubeAlongX(shoulderThickness, shoulderDiameter / 2, fluidBoreDiameter / 2, shoulderCenterX, segments);
57453
- const barbSolids = [];
57454
- const ridgeLength = Math.max(0.8, Math.min(barbLength * 0.45, hoseInnerDiameter * 0.28));
57455
- for (let index2 = 0; index2 < barbCount; index2 += 1) {
57456
- const startX = barbStartX + index2 * barbLength;
57457
- const ridgeCenterX = startX + barbLength - ridgeLength / 2;
57458
- barbSolids.push(tubeAlongX(ridgeLength, barbPeakDiameter / 2, fluidBoreDiameter / 2, ridgeCenterX, segments));
57459
- }
57460
- const fitting = union(fittingCore, shoulder, ...barbSolids).color("#94a3b8");
57461
- const hoseStartX = barbStartX + faceClearance;
57462
- const hoseCenterX = hoseStartX + hoseLength / 2;
57463
- const installedHoseBore = cylinderAlongX(hoseLength + 0.8, installedHoseBoreDiameter / 2, hoseCenterX, segments);
57464
- const hose = tubeAlongX(hoseLength, hoseOuterDiameter / 2, installedHoseBoreDiameter / 2, hoseCenterX, segments).color("#111827");
57465
- const clampCenterX = barbStartX + Math.min(barbStackLength * 0.55, Math.max(barbLength, clampWidth));
57466
- const clamp2 = tubeAlongX(
57467
- clampWidth,
57468
- hoseOuterDiameter / 2 + clampThickness,
57469
- hoseOuterDiameter / 2 + Math.min(0.08, runningClearance * 0.45),
57470
- clampCenterX,
57471
- segments
57472
- ).color("#cbd5e1");
57473
- const parts = [
57474
- { name: "bored pump or filter body with raised hose-port boss", shape: receiver },
57475
- { name: "hollow hose barb fitting with shoulder and retention ridges", shape: fitting },
57476
- { name: "installed flexible hose over barb tail", shape: hose },
57477
- { name: "clamp band over hose and barb ridges", shape: clamp2 }
57478
- ];
57479
- return {
57480
- parts,
57481
- receiver,
57482
- fitting,
57483
- hose,
57484
- clamp: clamp2,
57485
- cutters: {
57486
- portBore,
57487
- installedHoseBore
57488
- },
57489
- dims: {
57490
- hoseInnerDiameter,
57491
- hoseOuterDiameter,
57492
- installedHoseBoreDiameter,
57493
- blockThickness,
57494
- blockWidth,
57495
- blockHeight,
57496
- bossDiameter,
57497
- bossHeight,
57498
- fluidBoreDiameter,
57499
- barbRootDiameter,
57500
- barbPeakDiameter,
57501
- barbCount,
57502
- barbLength,
57503
- barbStackLength,
57504
- shoulderDiameter,
57505
- shoulderThickness,
57506
- hoseLength,
57507
- clampWidth,
57508
- clampThickness,
57509
- runningClearance,
57510
- faceClearance
57511
- }
57512
- };
57513
- }
57514
- function routedTubeClipAssembly(options) {
57515
- const tubeDiameter = requirePositive$6(options.tubeDiameter, "tubeDiameter");
57516
- const tubeLength = requirePositive$6(options.tubeLength ?? 120, "tubeLength");
57517
- const panelThickness = requirePositive$6(options.panelThickness ?? 3, "panelThickness");
57518
- const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
57519
- const screwSize = options.screwSize ?? "M3";
57520
- const segments = options.segments ?? 32;
57521
- const sizeData = METRIC_HOLE_TABLE[screwSize];
57522
- if (!sizeData) throw new Error(`routedTubeClipAssembly: unsupported screwSize "${screwSize}"`);
57523
- const clipCount = options.clipCount ?? 3;
57524
- if (!Number.isInteger(clipCount) || clipCount < 1 || clipCount > 8) {
57525
- throw new Error("routedTubeClipAssembly: clipCount must be an integer from 1 to 8");
57526
- }
57527
- const screwDiameter = parseFloat(screwSize.replace("M", ""));
57528
- const screwHeadDiameter = sizeData.head;
57529
- const tubeBoreDiameter = tubeDiameter + runningClearance * 2;
57530
- const clipWallThickness = requirePositive$6(
57531
- options.clipWallThickness ?? Math.max(screwHeadDiameter + 1.2, tubeDiameter * 0.45, 5),
57532
- "clipWallThickness"
57533
- );
57534
- const clipWidth = requirePositive$6(options.clipWidth ?? Math.max(screwHeadDiameter + 3, tubeDiameter * 1.4, 10), "clipWidth");
57535
- const clipDepth = tubeBoreDiameter + clipWallThickness * 2;
57536
- const bottomWall = Math.max(1.2, clipWallThickness * 0.35);
57537
- const topWall = Math.max(2, clipWallThickness * 0.45);
57538
- const clipHeight = bottomWall + tubeBoreDiameter + topWall;
57539
- const tubeCenterZ = panelThickness + bottomWall + tubeBoreDiameter / 2;
57540
- const panelLength = requirePositive$6(options.panelLength ?? tubeLength + 24, "panelLength");
57541
- const panelWidth = requirePositive$6(options.panelWidth ?? clipDepth + Math.max(14, screwHeadDiameter * 2), "panelWidth");
57542
- if (tubeLength <= clipWidth + 8) {
57543
- throw new Error("routedTubeClipAssembly: tubeLength must leave visible tube beyond the clip body");
57544
- }
57545
- const defaultSpacing = clipCount === 1 ? 0 : Math.max(clipWidth + 8, (tubeLength - clipWidth * 2) / (clipCount - 1));
57546
- const clipSpacing = options.clipSpacing === void 0 ? defaultSpacing : requirePositive$6(options.clipSpacing, "clipSpacing");
57547
- const clipCenters = Array.from({ length: clipCount }, (_2, index2) => (index2 - (clipCount - 1) / 2) * clipSpacing);
57548
- const maxClipExtent = Math.max(...clipCenters.map((x2) => Math.abs(x2) + clipWidth / 2));
57549
- if (maxClipExtent > tubeLength / 2 - 2) {
57550
- throw new Error("routedTubeClipAssembly: clipSpacing places a clip beyond the routed tube length");
57551
- }
57552
- if (maxClipExtent > panelLength / 2 - 2) {
57553
- throw new Error("routedTubeClipAssembly: panelLength is too short for the clip pattern");
57554
- }
57555
- const boreRadius = tubeBoreDiameter / 2;
57556
- const screwY = boreRadius + clipWallThickness / 2;
57557
- if (screwY + screwHeadDiameter / 2 > clipDepth / 2 - 0.2) {
57558
- throw new Error("routedTubeClipAssembly: clipWallThickness leaves too little land for screw heads");
57559
- }
57560
- if (clipDepth > panelWidth - Math.max(4, screwHeadDiameter * 0.5)) {
57561
- throw new Error("routedTubeClipAssembly: panelWidth leaves too little material beside the clips");
57562
- }
57563
- const screwPositions = clipCenters.flatMap((x2) => [[x2, -screwY], [x2, screwY]]);
57564
- const screwClearanceDiameter = Math.max(sizeData.loose, screwDiameter + 0.8);
57565
- const panelThreadEnvelopeDiameter = screwClearanceDiameter;
57566
- const clipTopZ = panelThickness + clipHeight;
57567
- const clipTubeBores = union(
57568
- ...clipCenters.map((x2) => cylinderAlongX(clipWidth + 0.8, boreRadius, x2, segments).translate(0, 0, tubeCenterZ))
57569
- );
57570
- const clipScrewClearances = union(
57571
- ...screwPositions.map(
57572
- ([x2, y2]) => cylinder(clipHeight + 0.8, screwClearanceDiameter / 2, void 0, segments).translate(x2, y2, panelThickness - 0.4)
57573
- )
57574
- );
57575
- const panelThreadEnvelopes = union(
57576
- ...screwPositions.map(
57577
- ([x2, y2]) => cylinder(panelThickness + 0.8, panelThreadEnvelopeDiameter / 2, void 0, segments).translate(x2, y2, -0.4)
57578
- )
57579
- );
57580
- const panel = box(panelLength, panelWidth, panelThickness).subtract(panelThreadEnvelopes).color("#475569");
57581
- const tube2 = cylinderAlongX(tubeLength, tubeDiameter / 2, 0, segments).translate(0, 0, tubeCenterZ).color("#0f172a");
57582
- const clips = clipCenters.map((x2) => {
57583
- const body = box(clipWidth, clipDepth, clipHeight).translate(x2, 0, panelThickness);
57584
- const tubeBore = cylinderAlongX(clipWidth + 0.8, boreRadius, x2, segments).translate(0, 0, tubeCenterZ);
57585
- const screwHoles = union(
57586
- cylinder(clipHeight + 0.8, screwClearanceDiameter / 2, void 0, segments).translate(x2, -screwY, panelThickness - 0.4),
57587
- cylinder(clipHeight + 0.8, screwClearanceDiameter / 2, void 0, segments).translate(x2, screwY, panelThickness - 0.4)
57588
- );
57589
- return body.subtract(tubeBore).subtract(screwHoles).color("#94a3b8");
57590
- });
57591
- const screwLength = clipHeight + panelThickness * 0.65;
57592
- const screwHeadHeight = Math.max(1.2, screwDiameter * 0.55);
57593
- const screwBlank = union(
57594
- cylinder(screwLength, screwDiameter / 2, void 0, segments).translate(0, 0, clipTopZ - screwLength),
57595
- cylinder(screwHeadHeight, screwHeadDiameter / 2, void 0, segments).translate(0, 0, clipTopZ)
57596
- ).color("#cbd5e1");
57597
- const screws = screwPositions.map(([x2, y2]) => screwBlank.translate(x2, y2, 0));
57598
- const parts = [
57599
- { name: "panel with tube-clip screw receiving holes", shape: panel },
57600
- { name: "routed flexible tube through retained clip bores", shape: tube2 },
57601
- ...clips.map((shape, index2) => ({ name: `saddle tube clip ${index2 + 1} with through-bore`, shape })),
57602
- ...screws.map((shape, index2) => ({ name: `installed ${screwSize} tube clip screw ${index2 + 1}`, shape }))
57603
- ];
57604
- return {
57605
- parts,
57606
- panel,
57607
- tube: tube2,
57608
- clips,
57609
- screws,
57610
- clipCenters,
57611
- screwPositions,
57612
- cutters: {
57613
- clipTubeBores,
57614
- clipScrewClearances,
57615
- panelThreadEnvelopes
57616
- },
57617
- dims: {
57618
- tubeDiameter,
57619
- tubeLength,
57620
- tubeBoreDiameter,
57621
- panelLength,
57622
- panelWidth,
57623
- panelThickness,
57624
- clipCount,
57625
- clipWidth,
57626
- clipDepth,
57627
- clipHeight,
57628
- clipWallThickness,
57629
- tubeCenterZ,
57630
- screwSize,
57631
- screwDiameter,
57632
- screwHeadDiameter,
57633
- screwLength,
57634
- screwClearanceDiameter,
57635
- panelThreadEnvelopeDiameter,
57636
- runningClearance
57637
- }
57638
- };
57639
- }
57640
- function pcbTerminalBlockAssembly(options = {}) {
57641
- const terminalCount = options.terminalCount ?? 4;
57642
- if (!Number.isInteger(terminalCount) || terminalCount < 1 || terminalCount > 24) {
57643
- throw new Error("pcbTerminalBlockAssembly: terminalCount must be an integer from 1 to 24");
57644
- }
57645
- const terminalPitch = requirePositive$6(options.terminalPitch ?? 5.08, "terminalPitch");
57646
- const terminalBlockWidth = terminalPitch * terminalCount + 3;
57647
- const boardWidth = requirePositive$6(options.boardWidth ?? Math.max(50, terminalBlockWidth + 28), "boardWidth");
57648
- const boardDepth = requirePositive$6(options.boardDepth ?? 38, "boardDepth");
57649
- const boardThickness = requirePositive$6(options.boardThickness ?? 1.6, "boardThickness");
57650
- const backplateThickness = requirePositive$6(options.backplateThickness ?? 3, "backplateThickness");
57651
- const backplateMargin = requirePositive$6(options.backplateMargin ?? 5, "backplateMargin");
57652
- const standoffHeight = requirePositive$6(options.standoffHeight ?? 6, "standoffHeight");
57653
- const screwSize = options.screwSize ?? "M3";
57654
- const segments = options.segments ?? 28;
57655
- const sizeData = METRIC_HOLE_TABLE[screwSize];
57656
- if (!sizeData) throw new Error(`pcbTerminalBlockAssembly: unsupported screwSize "${screwSize}"`);
57657
- const screwDiameter = parseFloat(screwSize.replace("M", ""));
57658
- const screwHeadDiameter = sizeData.head;
57659
- const screwHeadHeight = Math.max(1.2, screwDiameter * 0.55);
57660
- const standoffDiameter = requirePositive$6(
57661
- options.standoffDiameter ?? Math.max(screwHeadDiameter * 1.45, sizeData.normal + 3),
57662
- "standoffDiameter"
57663
- );
57664
- const [mountInsetX, mountInsetY] = resolveBoltInset(
57665
- options.mountingInset,
57666
- Math.max(standoffDiameter / 2 + 1.2, screwHeadDiameter * 0.75)
57667
- );
57668
- if (mountInsetX * 2 >= boardWidth || mountInsetY * 2 >= boardDepth) {
57669
- throw new Error("pcbTerminalBlockAssembly: mountingInset leaves no room for the PCB mounting pattern");
57670
- }
57671
- const terminalBlockDepth = requirePositive$6(options.terminalBlockDepth ?? 10, "terminalBlockDepth");
57672
- const terminalBlockHeight = requirePositive$6(options.terminalBlockHeight ?? 9, "terminalBlockHeight");
57673
- const terminalEdgeInset = requirePositive$6(options.terminalEdgeInset ?? 5, "terminalEdgeInset");
57674
- const pinDiameter = requirePositive$6(options.pinDiameter ?? 0.9, "pinDiameter");
57675
- const pinClearance = requirePositive$6(options.pinClearance ?? 0.25, "pinClearance");
57676
- const pinTailLength = requireNonNegative(options.pinTailLength ?? 0, "pinTailLength");
57677
- const wirePortDiameter = requirePositive$6(options.wirePortDiameter ?? 2.6, "wirePortDiameter");
57678
- const pinHoleDiameter = pinDiameter + pinClearance;
57679
- const terminalCenterY = -boardDepth / 2 + terminalEdgeInset + terminalBlockDepth / 2;
57680
- const pinY = terminalCenterY + terminalBlockDepth * 0.24;
57681
- const firstPinX = -((terminalCount - 1) * terminalPitch) / 2;
57682
- const pinPositions = Array.from({ length: terminalCount }, (_2, index2) => [firstPinX + index2 * terminalPitch, pinY]);
57683
- const mountingPositions = [
57684
- [-boardWidth / 2 + mountInsetX, -boardDepth / 2 + mountInsetY],
57685
- [boardWidth / 2 - mountInsetX, -boardDepth / 2 + mountInsetY],
57686
- [-boardWidth / 2 + mountInsetX, boardDepth / 2 - mountInsetY],
57687
- [boardWidth / 2 - mountInsetX, boardDepth / 2 - mountInsetY]
57688
- ];
57689
- if (terminalBlockWidth >= boardWidth - mountInsetX * 2) {
57690
- throw new Error("pcbTerminalBlockAssembly: terminal block is too wide for the PCB mounting pattern");
57691
- }
57692
- if (terminalEdgeInset + terminalBlockDepth >= boardDepth - mountInsetY * 2) {
57693
- throw new Error("pcbTerminalBlockAssembly: terminal block depth collides with the rear mounting datum");
57694
- }
57695
- if (pinHoleDiameter >= terminalPitch * 0.55) {
57696
- throw new Error("pcbTerminalBlockAssembly: pinDiameter and pinClearance leave too little PCB web between terminal holes");
57697
- }
57698
- if (wirePortDiameter >= Math.min(terminalPitch * 0.72, terminalBlockHeight * 0.65)) {
57699
- throw new Error("pcbTerminalBlockAssembly: wirePortDiameter is too large for the terminal pitch or body height");
57700
- }
57701
- for (const [index2, [x2, y2]] of [...mountingPositions, ...pinPositions].entries()) {
57702
- if (!Number.isFinite(x2) || !Number.isFinite(y2)) {
57703
- throw new Error(`pcbTerminalBlockAssembly: generated datum position ${index2} is not finite`);
57704
- }
57705
- }
57706
- const backplateWidth = boardWidth + backplateMargin * 2;
57707
- const backplateDepth = boardDepth + backplateMargin * 2;
57708
- const boardBottomZ = backplateThickness + standoffHeight;
57709
- const boardTopZ = boardBottomZ + boardThickness;
57710
- const standoffOverlap = Math.min(0.08, standoffHeight * 0.03);
57711
- const standoffThreadEnvelopeDiameter = Math.max(sizeData.loose, screwDiameter + 1);
57712
- const standoffThreadEnvelope = cylinder(standoffHeight + 0.8, standoffThreadEnvelopeDiameter / 2, void 0, segments).translate(
57713
- 0,
57714
- 0,
57715
- backplateThickness - 0.4
57716
- );
57717
- const standoffThreadEnvelopes = union(...mountingPositions.map(([x2, y2]) => standoffThreadEnvelope.translate(x2, y2, 0)));
57718
- const standoff = cylinder(standoffHeight + standoffOverlap, standoffDiameter / 2, void 0, segments).translate(0, 0, backplateThickness - standoffOverlap).subtract(standoffThreadEnvelope);
57719
- const standoffs = union(...mountingPositions.map(([x2, y2]) => standoff.translate(x2, y2, 0)));
57720
- const backplate = union(box(backplateWidth, backplateDepth, backplateThickness), standoffs).color("#475569");
57721
- const boardMountingHoleDiameter = sizeData.normal;
57722
- const boardMountHole = cylinder(boardThickness + 0.8, boardMountingHoleDiameter / 2, void 0, segments).translate(
57723
- 0,
57724
- 0,
57725
- boardBottomZ - 0.4
57726
- );
57727
- const pcbMountingHoles = union(...mountingPositions.map(([x2, y2]) => boardMountHole.translate(x2, y2, 0)));
57728
- const pinHole = cylinder(boardThickness + 0.8, pinHoleDiameter / 2, void 0, segments).translate(0, 0, boardBottomZ - 0.4);
57729
- const pcbPinHoles = union(...pinPositions.map(([x2, y2]) => pinHole.translate(x2, y2, 0)));
57730
- const pcb = box(boardWidth, boardDepth, boardThickness).translate(0, 0, boardBottomZ).subtract(pcbMountingHoles).subtract(pcbPinHoles).color("#166534");
57731
- const terminalBodyBlank = box(terminalBlockWidth, terminalBlockDepth, terminalBlockHeight).translate(0, terminalCenterY, boardTopZ);
57732
- const wirePort = cylinderAlongY(terminalBlockDepth + 0.8, wirePortDiameter / 2, terminalCenterY, segments).translate(
57733
- 0,
57734
- 0,
57735
- boardTopZ + terminalBlockHeight * 0.42
57736
- );
57737
- const wirePorts = union(...pinPositions.map(([x2]) => wirePort.translate(x2, 0, 0)));
57738
- const clampScrewPockets = union(
57739
- ...pinPositions.map(
57740
- ([x2]) => cylinder(
57741
- Math.max(0.6, terminalBlockHeight * 0.22),
57742
- Math.min(terminalPitch * 0.22, wirePortDiameter * 0.42),
57743
- void 0,
57744
- segments
57745
- ).translate(x2, terminalCenterY + terminalBlockDepth * 0.12, boardTopZ + terminalBlockHeight * 0.76)
57746
- )
57747
- );
57748
- const pinLength = boardThickness + pinTailLength + Math.min(0.6, terminalBlockHeight * 0.08);
57749
- const pinStartZ = boardBottomZ - pinTailLength;
57750
- const pins = union(...pinPositions.map(([x2, y2]) => cylinder(pinLength, pinDiameter / 2, void 0, segments).translate(x2, y2, pinStartZ)));
57751
- const terminalBlock = union(terminalBodyBlank.subtract(wirePorts).subtract(clampScrewPockets), pins).color("#16a34a");
57752
- const screwShaftLength = boardThickness + standoffHeight * 0.85;
57753
- const mountingHardware = fastenerSet(screwSize, screwShaftLength, {
57754
- washerUnderHead: false,
57755
- washerUnderNut: false,
57756
- fit: "normal",
57757
- segments
57758
- });
57759
- const screws = mountingPositions.map(([x2, y2]) => mountingHardware.bolt.translate(x2, y2, boardTopZ).color("#cbd5e1"));
57760
- const parts = [
57761
- { name: "electronics backplate with fused PCB standoffs", shape: backplate },
57762
- { name: "PCB with mounting holes and terminal pin clearances", shape: pcb },
57763
- { name: "seated purchased terminal block with through-board pins", shape: terminalBlock },
57764
- ...screws.map((shape, index2) => ({ name: `installed ${screwSize} PCB mounting screw ${index2 + 1}`, shape }))
57765
- ];
57766
- return {
57767
- parts,
57768
- backplate,
57769
- pcb,
57770
- terminalBlock,
57771
- screws,
57772
- mountingPositions,
57773
- pinPositions,
57774
- cutters: {
57775
- pcbMountingHoles,
57776
- pcbPinHoles,
57777
- standoffThreadEnvelopes
57778
- },
57779
- dims: {
57780
- terminalCount,
57781
- terminalPitch,
57782
- boardWidth,
57783
- boardDepth,
57784
- boardThickness,
57785
- backplateWidth,
57786
- backplateDepth,
57787
- backplateThickness,
57788
- standoffHeight,
57789
- standoffDiameter,
57790
- screwSize,
57791
- screwDiameter,
57792
- screwHeadDiameter,
57793
- screwHeadHeight,
57794
- screwShaftLength,
57795
- boardMountingHoleDiameter,
57796
- standoffThreadEnvelopeDiameter,
57797
- terminalBlockWidth,
57798
- terminalBlockDepth,
57799
- terminalBlockHeight,
57800
- terminalEdgeInset,
57801
- pinDiameter,
57802
- pinClearance,
57803
- pinHoleDiameter,
57804
- pinTailLength,
57805
- wirePortDiameter
57806
- }
57807
- };
57808
- }
57809
- function thumbScrewClampAssembly(options = {}) {
57810
- const screwSize = options.screwSize ?? "M6";
57811
- const segments = options.segments ?? 36;
57812
- const sizeData = METRIC_HOLE_TABLE[screwSize];
57813
- if (!sizeData) throw new Error(`thumbScrewClampAssembly: unsupported screwSize "${screwSize}"`);
57814
- const screwDiameter = parseFloat(screwSize.replace("M", ""));
57815
- const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
57816
- const faceClearance = requireNonNegative(options.faceClearance ?? 0, "faceClearance");
57817
- const threadEnvelopeDiameter = Math.max(sizeData.normal, screwDiameter + runningClearance * 2);
57818
- const pressurePadDiameter = requirePositive$6(options.pressurePadDiameter ?? Math.max(screwDiameter * 3.2, 18), "pressurePadDiameter");
57819
- const pressurePadThickness = requirePositive$6(options.pressurePadThickness ?? Math.max(screwDiameter * 0.72, 4), "pressurePadThickness");
57820
- const knobDiameter = requirePositive$6(options.knobDiameter ?? Math.max(screwDiameter * 4.2, 24), "knobDiameter");
57821
- const knobThickness = requirePositive$6(options.knobThickness ?? Math.max(screwDiameter * 0.9, 7), "knobThickness");
57822
- const workpieceThickness = requirePositive$6(options.workpieceThickness ?? 18, "workpieceThickness");
57823
- const workpieceDepth = requirePositive$6(options.workpieceDepth ?? Math.max(46, pressurePadDiameter * 1.5), "workpieceDepth");
57824
- const workpieceHeight = requirePositive$6(options.workpieceHeight ?? Math.max(pressurePadDiameter * 1.35, 24), "workpieceHeight");
57825
- const frameDepth = requirePositive$6(options.frameDepth ?? Math.max(workpieceDepth + 12, pressurePadDiameter + 16), "frameDepth");
57826
- const baseThickness = requirePositive$6(options.baseThickness ?? Math.max(screwDiameter, 6), "baseThickness");
57827
- const jawThickness = requirePositive$6(options.jawThickness ?? Math.max(screwDiameter * 1.35, 9), "jawThickness");
57828
- const supportThickness = requirePositive$6(options.supportThickness ?? Math.max(screwDiameter * 1.8, 12), "supportThickness");
57829
- const bossLength = requirePositive$6(options.bossLength ?? Math.max(screwDiameter * 1.1, 8), "bossLength");
57830
- const bossDiameter = requirePositive$6(options.bossDiameter ?? Math.max(threadEnvelopeDiameter + 5, screwDiameter * 2.5), "bossDiameter");
57831
- const exposedScrewLength = requirePositive$6(
57832
- options.exposedScrewLength ?? Math.max(pressurePadDiameter * 0.45, screwDiameter * 2.2),
57833
- "exposedScrewLength"
57834
- );
57835
- const screwCenterZ = baseThickness + Math.max(workpieceHeight * 0.52, pressurePadDiameter * 0.68);
57836
- const frameHeight = requirePositive$6(
57837
- options.frameHeight ?? screwCenterZ - baseThickness + pressurePadDiameter / 2 + Math.max(baseThickness, 7),
57838
- "frameHeight"
57839
- );
57840
- if (workpieceDepth > frameDepth - 6) {
57841
- throw new Error("thumbScrewClampAssembly: frameDepth must leave side material around the clamped workpiece");
57842
- }
57843
- if (pressurePadDiameter > frameDepth - 4) {
57844
- throw new Error("thumbScrewClampAssembly: pressurePadDiameter is too large for the frame depth");
57845
- }
57846
- if (bossDiameter > frameDepth - 4) {
57847
- throw new Error("thumbScrewClampAssembly: bossDiameter is too large for the frame depth");
57848
- }
57849
- if (screwCenterZ - pressurePadDiameter / 2 <= baseThickness + 0.5) {
57850
- throw new Error("thumbScrewClampAssembly: pressure pad collides with the base bridge");
57851
- }
57852
- if (baseThickness + frameHeight - screwCenterZ <= pressurePadDiameter / 2 + 2) {
57853
- throw new Error("thumbScrewClampAssembly: frameHeight leaves too little material above the screw axis");
57854
- }
57855
- if (threadEnvelopeDiameter + 4 > Math.min(frameDepth, frameHeight)) {
57856
- throw new Error("thumbScrewClampAssembly: threaded boss bore leaves too little surrounding frame material");
57857
- }
57858
- const workpieceLeftFaceX = -workpieceThickness / 2;
57859
- const workpieceRightFaceX = workpieceThickness / 2;
57860
- const anvilOverlap = Math.min(0.35, pressurePadThickness * 0.18);
57861
- const anvilPadCenterX = workpieceLeftFaceX - faceClearance - pressurePadThickness / 2;
57862
- const pressurePadCenterX = workpieceRightFaceX + faceClearance + pressurePadThickness / 2;
57863
- const fixedJawRightFaceX = anvilPadCenterX - pressurePadThickness / 2 + anvilOverlap;
57864
- const fixedJawCenterX = fixedJawRightFaceX - jawThickness / 2;
57865
- const pressurePadRightFaceX = pressurePadCenterX + pressurePadThickness / 2;
57866
- const supportInnerFaceX = pressurePadRightFaceX + exposedScrewLength;
57867
- const supportCenterX = supportInnerFaceX + supportThickness / 2;
57868
- const supportOuterFaceX = supportInnerFaceX + supportThickness;
57869
- const frameLeftFaceX = fixedJawCenterX - jawThickness / 2;
57870
- const frameRightFaceX = supportOuterFaceX;
57871
- const baseLength = frameRightFaceX - frameLeftFaceX;
57872
- if (baseLength <= 0 || !Number.isFinite(baseLength)) {
57873
- throw new Error("thumbScrewClampAssembly: generated clamp frame length is invalid");
57874
- }
57875
- const bossCenterX = supportInnerFaceX + (supportThickness + bossLength) / 2;
57876
- const threadedBossBore = cylinderAlongX(supportThickness + bossLength + 1, threadEnvelopeDiameter / 2, bossCenterX, segments).translate(
57877
- 0,
57878
- 0,
57879
- screwCenterZ
57880
- );
57881
- const frameOverlap = Math.min(0.12, baseThickness * 0.04);
57882
- const base = box(baseLength, frameDepth, baseThickness).translate((frameLeftFaceX + frameRightFaceX) / 2, 0, 0);
57883
- const fixedJaw = box(jawThickness, frameDepth, frameHeight + frameOverlap).translate(fixedJawCenterX, 0, baseThickness - frameOverlap);
57884
- const support = box(supportThickness, frameDepth, frameHeight + frameOverlap).translate(supportCenterX, 0, baseThickness - frameOverlap);
57885
- const boss2 = cylinderAlongX(supportThickness + bossLength, bossDiameter / 2, bossCenterX, segments).translate(0, 0, screwCenterZ);
57886
- const anvilPad = cylinderAlongX(pressurePadThickness, pressurePadDiameter / 2, anvilPadCenterX, segments).translate(0, 0, screwCenterZ);
57887
- const frame = union(base, fixedJaw, support, boss2, anvilPad).subtract(threadedBossBore).color("#475569");
57888
- const workpieceBottomZ = screwCenterZ - workpieceHeight / 2;
57889
- const workpiece = box(workpieceThickness, workpieceDepth, workpieceHeight).translate(0, 0, workpieceBottomZ).color("#a16207");
57890
- const pressurePad = cylinderAlongX(pressurePadThickness, pressurePadDiameter / 2, pressurePadCenterX, segments).translate(
57891
- 0,
57892
- 0,
57893
- screwCenterZ
57894
- );
57895
- const knobCenterX = supportOuterFaceX + bossLength + runningClearance + knobThickness / 2;
57896
- const knob = cylinderAlongX(knobThickness, knobDiameter / 2, knobCenterX, segments).translate(0, 0, screwCenterZ);
57897
- const shaftLeftX = pressurePadRightFaceX - Math.min(pressurePadThickness * 0.45, screwDiameter * 0.45);
57898
- const shaftRightX = knobCenterX + knobThickness / 2;
57899
- const shaftLength = shaftRightX - shaftLeftX;
57900
- if (shaftLength <= supportThickness + bossLength) {
57901
- throw new Error("thumbScrewClampAssembly: generated screw length is too short for the threaded support");
57902
- }
57903
- const shaft = cylinderAlongX(shaftLength, screwDiameter / 2, (shaftLeftX + shaftRightX) / 2, segments).translate(0, 0, screwCenterZ);
57904
- const clampScrew = union(shaft, pressurePad, knob).color("#cbd5e1");
57905
- const workpieceEnvelope = box(workpieceThickness, workpieceDepth, workpieceHeight).translate(0, 0, workpieceBottomZ);
57906
- return {
57907
- parts: [
57908
- { name: "thumb-screw clamp frame with fixed anvil and threaded boss", shape: frame },
57909
- { name: "representative clamped workpiece between pads", shape: workpiece },
57910
- { name: "installed thumb screw with captive pressure pad and hand knob", shape: clampScrew }
57911
- ],
57912
- frame,
57913
- workpiece,
57914
- clampScrew,
57915
- cutters: {
57916
- threadedBossBore,
57917
- workpieceEnvelope
57918
- },
57919
- dims: {
57920
- screwSize,
57921
- screwDiameter,
57922
- threadEnvelopeDiameter,
57923
- workpieceThickness,
57924
- workpieceDepth,
57925
- workpieceHeight,
57926
- frameDepth,
57927
- frameHeight,
57928
- baseThickness,
57929
- jawThickness,
57930
- supportThickness,
57931
- bossLength,
57932
- bossDiameter,
57933
- exposedScrewLength,
57934
- pressurePadDiameter,
57935
- pressurePadThickness,
57936
- knobDiameter,
57937
- knobThickness,
57938
- screwCenterZ,
57939
- fixedAnvilFaceX: workpieceLeftFaceX - faceClearance,
57940
- pressurePadFaceX: workpieceRightFaceX + faceClearance,
57941
- supportInnerFaceX,
57942
- runningClearance,
57943
- faceClearance
57944
- }
57945
- };
57946
- }
57947
57169
  function fastenerSet(size, boltLength, options) {
57948
57170
  const sizeData = METRIC_HOLE_TABLE[size];
57949
57171
  if (!sizeData) throw new Error(`fastenerSet: unsupported size "${size}"`);
@@ -58004,22 +57226,6 @@ const partLibrary = {
58004
57226
  nut,
58005
57227
  washer,
58006
57228
  fastenerSet,
58007
- boltedServiceCover,
58008
- datumEnclosureAssembly,
58009
- snapLatchCoverAssembly,
58010
- pinnedLeverAssembly,
58011
- retainedShaftAssembly,
58012
- capturedLinearSlide,
58013
- capturedCartridgeGuideAssembly,
58014
- livingHingeCoverAssembly,
58015
- knuckledHingeAssembly,
58016
- clevisPinJointAssembly,
58017
- seatedBearingAssembly,
58018
- cableGlandAnchorAssembly,
58019
- hoseBarbPortAssembly,
58020
- routedTubeClipAssembly,
58021
- pcbTerminalBlockAssembly,
58022
- thumbScrewClampAssembly,
58023
57229
  pipeRoute,
58024
57230
  elbow,
58025
57231
  beltDrive,
@@ -304893,7 +304099,8 @@ function describeKinematicConvergenceError(kinematics) {
304893
304099
  const edgeResidual = edge ? Math.abs(edge.residual) : 0;
304894
304100
  const angleResidual = angle ? Math.abs(angle.residual) : 0;
304895
304101
  const worst = edgeResidual >= angleResidual && edge ? `edge "${edge.name}"` : angle ? `angle "${angle.name}"` : "constraint";
304896
- return `Assembly kinematic solve did not converge (max residual ${kinematics.maxResidual.toFixed(6)}, worst ${worst}). The requested control state is outside the mechanism's valid range; rejecting best-effort stretched geometry.`;
304102
+ const diagnostic = kinematics.diagnostics.length > 0 ? ` Diagnostic: ${kinematics.diagnostics[0]}` : "";
304103
+ return `Assembly kinematic solve did not converge (max residual ${kinematics.maxResidual.toFixed(6)}, worst ${worst}).${diagnostic} The requested control state is outside the selected mechanism mode or valid range; rejecting best-effort stretched geometry.`;
304897
304104
  }
304898
304105
  function mapScriptResultToScene(args) {
304899
304106
  var _a3;