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
@@ -1220,7 +1220,7 @@ function requireFiniteVec2(value, label) {
1220
1220
  }
1221
1221
  return [requireFiniteNumber$1(value[0], `${label}[0]`), requireFiniteNumber$1(value[1], `${label}[1]`)];
1222
1222
  }
1223
- function requireFiniteVec3$2(value, label) {
1223
+ function requireFiniteVec3$4(value, label) {
1224
1224
  if (!Array.isArray(value) || value.length !== 3) {
1225
1225
  throw new Error(`${label} must be [x, y, z] with finite numbers, got ${formatValidationValue(value)}`);
1226
1226
  }
@@ -1236,7 +1236,7 @@ function requireNonZeroFiniteVec2(value, label) {
1236
1236
  return v;
1237
1237
  }
1238
1238
  function requireNonZeroFiniteVec3(value, label) {
1239
- const v = requireFiniteVec3$2(value, label);
1239
+ const v = requireFiniteVec3$4(value, label);
1240
1240
  if (v[0] === 0 && v[1] === 0 && v[2] === 0) throw new Error(`${label} must not be [0, 0, 0]`);
1241
1241
  return v;
1242
1242
  }
@@ -1254,7 +1254,7 @@ function requireNonZeroFiniteScale2(value, label) {
1254
1254
  return scale2;
1255
1255
  }
1256
1256
  function requireNonZeroFiniteScale3(value, label) {
1257
- const scale2 = typeof value === "number" ? [requireFiniteNumber$1(value, label), requireFiniteNumber$1(value, label), requireFiniteNumber$1(value, label)] : requireFiniteVec3$2(value, label);
1257
+ const scale2 = typeof value === "number" ? [requireFiniteNumber$1(value, label), requireFiniteNumber$1(value, label), requireFiniteNumber$1(value, label)] : requireFiniteVec3$4(value, label);
1258
1258
  if (Math.abs(scale2[0]) < 1e-12 || Math.abs(scale2[1]) < 1e-12 || Math.abs(scale2[2]) < 1e-12) {
1259
1259
  throw new Error(`${label} must have finite non-zero components, got ${formatValidationValue(value)}`);
1260
1260
  }
@@ -1449,7 +1449,7 @@ class Transform {
1449
1449
  static rotationAxis(axis, angleDeg, pivot = [0, 0, 0]) {
1450
1450
  const [ux, uy, uz] = normalizeVec3$4(requireNonZeroFiniteVec3(axis, "Transform.rotationAxis() axis"));
1451
1451
  const degrees2 = requireFiniteNumber$1(angleDeg, "Transform.rotationAxis() angleDeg");
1452
- const [px, py, pz] = requireFiniteVec3$2(pivot, "Transform.rotationAxis() pivot");
1452
+ const [px, py, pz] = requireFiniteVec3$4(pivot, "Transform.rotationAxis() pivot");
1453
1453
  const rad = degrees2 * Math.PI / 180;
1454
1454
  const cos2 = Math.cos(rad);
1455
1455
  const sin2 = Math.sin(rad);
@@ -1470,9 +1470,9 @@ class Transform {
1470
1470
  /** Solve the rotation needed to move one point onto a target line or plane. */
1471
1471
  static rotateAroundTo(axis, pivot, movingPoint, targetPoint, options = {}) {
1472
1472
  const rotateAxis = requireNonZeroFiniteVec3(axis, "Transform.rotateAroundTo() axis");
1473
- const rotatePivot = requireFiniteVec3$2(pivot, "Transform.rotateAroundTo() pivot");
1474
- const moving = requireFiniteVec3$2(movingPoint, "Transform.rotateAroundTo() movingPoint");
1475
- const target = requireFiniteVec3$2(targetPoint, "Transform.rotateAroundTo() targetPoint");
1473
+ const rotatePivot = requireFiniteVec3$4(pivot, "Transform.rotateAroundTo() pivot");
1474
+ const moving = requireFiniteVec3$4(movingPoint, "Transform.rotateAroundTo() movingPoint");
1475
+ const target = requireFiniteVec3$4(targetPoint, "Transform.rotateAroundTo() targetPoint");
1476
1476
  const angleDeg = solveRotateAroundAngle(rotateAxis, rotatePivot, moving, target, options);
1477
1477
  return Transform.rotationAxis(rotateAxis, angleDeg, rotatePivot);
1478
1478
  }
@@ -1502,6 +1502,7 @@ class Transform {
1502
1502
  return this.rotateAxis([0, 0, 1], angleDeg, pivot);
1503
1503
  }
1504
1504
  /** Scale after the current transform. */
1505
+ // biome-ignore lint/suspicious/useAdjacentOverloadSignatures: Static Transform.scale() and chainable instance scale() intentionally share the CAD API name.
1505
1506
  scale(v) {
1506
1507
  return this.mul(Transform.scale(v));
1507
1508
  }
@@ -1511,11 +1512,11 @@ class Transform {
1511
1512
  }
1512
1513
  /** Transform a point using homogeneous coordinates. */
1513
1514
  point(p2) {
1514
- return transformPoint$1(this.m, requireFiniteVec3$2(p2, "Transform.point() point"), 1);
1515
+ return transformPoint$1(this.m, requireFiniteVec3$4(p2, "Transform.point() point"), 1);
1515
1516
  }
1516
1517
  /** Transform a direction vector without translation. */
1517
1518
  vector(v) {
1518
- return transformPoint$1(this.m, requireFiniteVec3$2(v, "Transform.vector() vector"), 0);
1519
+ return transformPoint$1(this.m, requireFiniteVec3$4(v, "Transform.vector() vector"), 0);
1519
1520
  }
1520
1521
  /** Return the transform as a raw 4x4 matrix array. */
1521
1522
  toArray() {
@@ -3245,7 +3246,7 @@ const EPSILON$4 = 1e-9;
3245
3246
  function add$6(a2, b) {
3246
3247
  return [a2[0] + b[0], a2[1] + b[1], a2[2] + b[2]];
3247
3248
  }
3248
- function scale$6(v, factor) {
3249
+ function scale$7(v, factor) {
3249
3250
  return [v[0] * factor, v[1] * factor, v[2] * factor];
3250
3251
  }
3251
3252
  function sub$8(a2, b) {
@@ -3260,9 +3261,9 @@ function cross$8(a2, b) {
3260
3261
  function rotateAroundAxis(v, axis, angleRad) {
3261
3262
  const c2 = Math.cos(angleRad);
3262
3263
  const s = Math.sin(angleRad);
3263
- const term1 = scale$6(v, c2);
3264
- const term2 = scale$6(cross$8(axis, v), s);
3265
- const term3 = scale$6(axis, dot$8(axis, v) * (1 - c2));
3264
+ const term1 = scale$7(v, c2);
3265
+ const term2 = scale$7(cross$8(axis, v), s);
3266
+ const term3 = scale$7(axis, dot$8(axis, v) * (1 - c2));
3266
3267
  return add$6(add$6(term1, term2), term3);
3267
3268
  }
3268
3269
  function arcPointAt(segment, t) {
@@ -3290,7 +3291,7 @@ function routePointAt(plan, t) {
3290
3291
  const next = station + segment.length;
3291
3292
  if (target <= next || segment === plan.segments[plan.segments.length - 1]) {
3292
3293
  const localT = segment.length <= EPSILON$4 ? 1 : Math.max(0, Math.min(1, (target - station) / segment.length));
3293
- if (segment.kind === "line") return add$6(segment.from, scale$6(sub$8(segment.to, segment.from), localT));
3294
+ if (segment.kind === "line") return add$6(segment.from, scale$7(sub$8(segment.to, segment.from), localT));
3294
3295
  return arcPointAt(segment, localT);
3295
3296
  }
3296
3297
  station = next;
@@ -3310,7 +3311,7 @@ function sampleRoute3DCompilePlan(plan, options) {
3310
3311
  pushUnique(points, start);
3311
3312
  for (let i = 1; i <= intervals; i += 1) {
3312
3313
  if (segment.kind === "line") {
3313
- pushUnique(points, add$6(segment.from, scale$6(sub$8(segment.to, segment.from), i / intervals)));
3314
+ pushUnique(points, add$6(segment.from, scale$7(sub$8(segment.to, segment.from), i / intervals)));
3314
3315
  } else {
3315
3316
  pushUnique(points, arcPointAt(segment, i / intervals));
3316
3317
  }
@@ -5637,11 +5638,11 @@ function distSq$1(a2, b) {
5637
5638
  const dz = a2[2] - b[2];
5638
5639
  return dx * dx + dy * dy + dz * dz;
5639
5640
  }
5640
- function vecLength$3(v) {
5641
+ function vecLength$4(v) {
5641
5642
  return Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
5642
5643
  }
5643
5644
  function normalize$6(v) {
5644
- const len2 = vecLength$3(v);
5645
+ const len2 = vecLength$4(v);
5645
5646
  if (len2 < 1e-12) return [0, 0, 0];
5646
5647
  return [v[0] / len2, v[1] / len2, v[2] / len2];
5647
5648
  }
@@ -6371,14 +6372,14 @@ function resolveSdfMeshingSettings(tree, bounds, options = {}) {
6371
6372
  const quality = options.quality ?? "preview";
6372
6373
  const minEdgeLength = positiveOrDefault(options.minEdgeLength, DEFAULT_MIN_EDGE_LENGTH);
6373
6374
  const maxGridPoints = positiveOrDefault(options.maxGridPoints, DEFAULT_MAX_GRID_POINTS);
6374
- const tolerance = options.tolerance !== void 0 ? requirePositiveFinite$3(options.tolerance, "SDF tolerance") : void 0;
6375
- const minFeatureSize = options.minFeatureSize !== void 0 ? requirePositiveFinite$3(options.minFeatureSize, "SDF minFeatureSize") : void 0;
6376
- const maxTriangles = options.maxTriangles !== void 0 ? Math.floor(requirePositiveFinite$3(options.maxTriangles, "SDF maxTriangles")) : void 0;
6375
+ const tolerance = options.tolerance !== void 0 ? requirePositiveFinite$4(options.tolerance, "SDF tolerance") : void 0;
6376
+ const minFeatureSize = options.minFeatureSize !== void 0 ? requirePositiveFinite$4(options.minFeatureSize, "SDF minFeatureSize") : void 0;
6377
+ const maxTriangles = options.maxTriangles !== void 0 ? Math.floor(requirePositiveFinite$4(options.maxTriangles, "SDF maxTriangles")) : void 0;
6377
6378
  const analysis = analyzeSdfTree(tree);
6378
6379
  const warnings = [];
6379
6380
  let edgeLength;
6380
6381
  if (options.edgeLength !== void 0) {
6381
- edgeLength = requirePositiveFinite$3(options.edgeLength, "SDF edgeLength");
6382
+ edgeLength = requirePositiveFinite$4(options.edgeLength, "SDF edgeLength");
6382
6383
  if (edgeLength < minEdgeLength) {
6383
6384
  warnings.push(`edgeLength ${formatMm(edgeLength)} was clamped to minimum ${formatMm(minEdgeLength)}.`);
6384
6385
  edgeLength = minEdgeLength;
@@ -6471,8 +6472,8 @@ function resolveDefaultEdgeLength(bounds, quality, minEdgeLength, analysis, opti
6471
6472
  const maxDim = Math.max(dx, dy, dz, minEdgeLength);
6472
6473
  const divisor = quality === "draft" ? 60 : quality === "export" ? 160 : 100;
6473
6474
  const candidates = [maxDim / divisor];
6474
- if (options.tolerance !== void 0) candidates.push(requirePositiveFinite$3(options.tolerance, "SDF tolerance") * 2);
6475
- if (options.minFeatureSize !== void 0) candidates.push(requirePositiveFinite$3(options.minFeatureSize, "SDF minFeatureSize") / 2.5);
6475
+ if (options.tolerance !== void 0) candidates.push(requirePositiveFinite$4(options.tolerance, "SDF tolerance") * 2);
6476
+ if (options.minFeatureSize !== void 0) candidates.push(requirePositiveFinite$4(options.minFeatureSize, "SDF minFeatureSize") / 2.5);
6476
6477
  if (analysis.minMetricTpmsThickness < Infinity) candidates.push(analysis.minMetricTpmsThickness / 2);
6477
6478
  if (analysis.minTpmsCellSize < Infinity) candidates.push(analysis.minTpmsCellSize / 10);
6478
6479
  if (analysis.minRepeatSpacing < Infinity) candidates.push(analysis.minRepeatSpacing / 8);
@@ -6612,9 +6613,9 @@ function visitSdfNode(node, analysis) {
6612
6613
  }
6613
6614
  function positiveOrDefault(value, fallback) {
6614
6615
  if (value === void 0) return fallback;
6615
- return requirePositiveFinite$3(value, "SDF meshing option");
6616
+ return requirePositiveFinite$4(value, "SDF meshing option");
6616
6617
  }
6617
- function requirePositiveFinite$3(value, name) {
6618
+ function requirePositiveFinite$4(value, name) {
6618
6619
  if (!Number.isFinite(value) || value <= 0) {
6619
6620
  throw new Error(`${name} must be a positive finite number.`);
6620
6621
  }
@@ -8598,7 +8599,7 @@ function midpoint$3(a2, b) {
8598
8599
  function add$5(a2, b) {
8599
8600
  return [a2[0] + b[0], a2[1] + b[1], a2[2] + b[2]];
8600
8601
  }
8601
- function scale$5(v, s) {
8602
+ function scale$6(v, s) {
8602
8603
  return [v[0] * s, v[1] * s, v[2] * s];
8603
8604
  }
8604
8605
  function sub$7(a2, b) {
@@ -8627,7 +8628,7 @@ function average$1(points) {
8627
8628
  for (const point2 of points) {
8628
8629
  acc = add$5(acc, point2);
8629
8630
  }
8630
- return scale$5(acc, 1 / points.length);
8631
+ return scale$6(acc, 1 / points.length);
8631
8632
  }
8632
8633
  function buildSurfaceSheetTopology(boundaries, options = {}) {
8633
8634
  var _a3, _b3, _c2, _d2;
@@ -8773,7 +8774,7 @@ function edgeCurveFaceName(curve) {
8773
8774
  function dotVec3$3(a2, b) {
8774
8775
  return a2[0] * b[0] + a2[1] * b[1] + a2[2] * b[2];
8775
8776
  }
8776
- function vecLength$2(v) {
8777
+ function vecLength$3(v) {
8777
8778
  return Math.hypot(v[0], v[1], v[2]);
8778
8779
  }
8779
8780
  function sameScalar$2(a2, b) {
@@ -8783,9 +8784,9 @@ function uniformDistanceScale(matrix) {
8783
8784
  const xAxis = [matrix[0], matrix[1], matrix[2]];
8784
8785
  const yAxis = [matrix[4], matrix[5], matrix[6]];
8785
8786
  const zAxis = [matrix[8], matrix[9], matrix[10]];
8786
- const sx = vecLength$2(xAxis);
8787
- const sy = vecLength$2(yAxis);
8788
- const sz = vecLength$2(zAxis);
8787
+ const sx = vecLength$3(xAxis);
8788
+ const sy = vecLength$3(yAxis);
8789
+ const sz = vecLength$3(zAxis);
8789
8790
  if (sx <= 1e-9 || sy <= 1e-9 || sz <= 1e-9) return null;
8790
8791
  if (!sameScalar$2(sx, sy) || !sameScalar$2(sx, sz)) return null;
8791
8792
  const orthoTolerance = 1e-9 * Math.max(1, sx * sx);
@@ -8796,7 +8797,7 @@ function uniformDistanceScale(matrix) {
8796
8797
  }
8797
8798
  function transformSurfaceAxis(tx, axis) {
8798
8799
  const transformed = tx.vector(axis);
8799
- const length4 = vecLength$2(transformed);
8800
+ const length4 = vecLength$3(transformed);
8800
8801
  return length4 > 1e-9 ? [transformed[0] / length4, transformed[1] / length4, transformed[2] / length4] : null;
8801
8802
  }
8802
8803
  function transformFaceSurface(surface, tx) {
@@ -9755,7 +9756,7 @@ let _wasm$1 = null;
9755
9756
  async function initManifoldWasm() {
9756
9757
  if (_wasm$1) return _wasm$1;
9757
9758
  performance.mark("manifold:start");
9758
- const Module = (await import("./manifold-BRI5prcH.js")).default;
9759
+ const Module = (await import("./manifold-rZexZI0G.js")).default;
9759
9760
  performance.mark("manifold:imported");
9760
9761
  const wasm = await Module();
9761
9762
  wasm.setup();
@@ -18408,7 +18409,7 @@ function add$4(a2, b) {
18408
18409
  function sub$6(a2, b) {
18409
18410
  return [a2[0] - b[0], a2[1] - b[1], a2[2] - b[2]];
18410
18411
  }
18411
- function scale$4(v, scalar) {
18412
+ function scale$5(v, scalar) {
18412
18413
  return [v[0] * scalar, v[1] * scalar, v[2] * scalar];
18413
18414
  }
18414
18415
  function cross$6(a2, b) {
@@ -18657,7 +18658,7 @@ function createBoundedHalfSpace(bounds, normal, originOffset) {
18657
18658
  const n = [normal[0] / normalLength, normal[1] / normalLength, normal[2] / normalLength];
18658
18659
  const signedOffset = originOffset / normalLength;
18659
18660
  const corners = boundsCorners(bounds);
18660
- const planeOrigin = scale$4(n, signedOffset);
18661
+ const planeOrigin = scale$5(n, signedOffset);
18661
18662
  const { uAxis, vAxis } = perpendicularAxes(n);
18662
18663
  const diagonal = Math.hypot(bounds.max[0] - bounds.min[0], bounds.max[1] - bounds.min[1], bounds.max[2] - bounds.min[2]);
18663
18664
  const margin = Math.max(diagonal * 0.05, 1e-3);
@@ -18682,7 +18683,7 @@ function createBoundedHalfSpace(bounds, normal, originOffset) {
18682
18683
  const height = Math.max(maxDistance + margin, margin);
18683
18684
  const centerU = (minU + maxU) / 2;
18684
18685
  const centerV = (minV + maxV) / 2;
18685
- const translation = add$4(add$4(planeOrigin, scale$4(uAxis, centerU)), scale$4(vAxis, centerV));
18686
+ const translation = add$4(add$4(planeOrigin, scale$5(uAxis, centerU)), scale$5(vAxis, centerV));
18686
18687
  const matrix = [
18687
18688
  uAxis[0],
18688
18689
  uAxis[1],
@@ -27870,7 +27871,7 @@ function placementReferenceNames(refs, kind) {
27870
27871
  (entryKind) => Object.keys(refs[entryKind]).sort().map((name) => `${entryKind}.${name}`)
27871
27872
  );
27872
27873
  }
27873
- function requireFiniteVec3$1(v, label) {
27874
+ function requireFiniteVec3$3(v, label) {
27874
27875
  const [x2, y2, z2] = v;
27875
27876
  if (!Number.isFinite(x2) || !Number.isFinite(y2) || !Number.isFinite(z2)) {
27876
27877
  throw new Error(`${label} must contain finite numbers, got [${x2}, ${y2}, ${z2}]`);
@@ -27910,8 +27911,8 @@ function normalizePortInput(input) {
27910
27911
  const hasStartEnd = input.start != null && input.end != null;
27911
27912
  const hasOriginAxis = input.origin != null && input.axis != null;
27912
27913
  if (hasStartEnd) {
27913
- const start = requireFiniteVec3$1(input.start, "port start");
27914
- const end = requireFiniteVec3$1(input.end, "port end");
27914
+ const start = requireFiniteVec3$3(input.start, "port start");
27915
+ const end = requireFiniteVec3$3(input.end, "port end");
27915
27916
  origin = [(start[0] + end[0]) / 2, (start[1] + end[1]) / 2, (start[2] + end[2]) / 2];
27916
27917
  const dir = [end[0] - start[0], end[1] - start[1], end[2] - start[2]];
27917
27918
  const dirLen = len3$2(dir);
@@ -27920,11 +27921,11 @@ function normalizePortInput(input) {
27920
27921
  }
27921
27922
  axis = normalize3$2(dir);
27922
27923
  extent = dirLen / 2;
27923
- if (input.origin != null) origin = requireFiniteVec3$1(input.origin, "port origin");
27924
- if (input.axis != null) axis = normalize3$2(requireFiniteVec3$1(input.axis, "port axis"));
27924
+ if (input.origin != null) origin = requireFiniteVec3$3(input.origin, "port origin");
27925
+ if (input.axis != null) axis = normalize3$2(requireFiniteVec3$3(input.axis, "port axis"));
27925
27926
  } else if (hasOriginAxis) {
27926
- origin = requireFiniteVec3$1(input.origin, "port origin");
27927
- const rawAxis = requireFiniteVec3$1(input.axis, "port axis");
27927
+ origin = requireFiniteVec3$3(input.origin, "port origin");
27928
+ const rawAxis = requireFiniteVec3$3(input.axis, "port axis");
27928
27929
  if (len3$2(rawAxis) < 1e-10) {
27929
27930
  throw new Error("Port axis must be non-zero");
27930
27931
  }
@@ -27933,14 +27934,14 @@ function normalizePortInput(input) {
27933
27934
  extent = input.extent;
27934
27935
  }
27935
27936
  } else if (input.origin != null) {
27936
- origin = requireFiniteVec3$1(input.origin, "port origin");
27937
+ origin = requireFiniteVec3$3(input.origin, "port origin");
27937
27938
  axis = [0, 0, 1];
27938
27939
  } else {
27939
27940
  throw new Error("Port requires either { origin, axis } or { start, end }");
27940
27941
  }
27941
27942
  let up;
27942
27943
  if (input.up != null) {
27943
- const rawUp = requireFiniteVec3$1(input.up, "port up");
27944
+ const rawUp = requireFiniteVec3$3(input.up, "port up");
27944
27945
  if (len3$2(rawUp) < 1e-10) {
27945
27946
  throw new Error("Port up vector must be non-zero");
27946
27947
  }
@@ -29315,7 +29316,7 @@ class ShapeGroup {
29315
29316
  return { min: bb.min, max: bb.max };
29316
29317
  }
29317
29318
  resolveRotatePoint(point2) {
29318
- if (Array.isArray(point2)) return requireFiniteVec3$2(point2, "ShapeGroup.rotateAroundTo() point");
29319
+ if (Array.isArray(point2)) return requireFiniteVec3$4(point2, "ShapeGroup.rotateAroundTo() point");
29319
29320
  const bb = this._bbox();
29320
29321
  return resolveAnchor3D(bb.min, bb.max, point2);
29321
29322
  }
@@ -29368,7 +29369,7 @@ class ShapeGroup {
29368
29369
  const sp = resolveAnchor3D(sbb.min, sbb.max, selfAnchor);
29369
29370
  let dx = tp[0] - sp[0], dy = tp[1] - sp[1], dz = tp[2] - sp[2];
29370
29371
  if (offset2) {
29371
- const offsetPoint = requireFiniteVec3$2(offset2, "ShapeGroup.attachTo() offset");
29372
+ const offsetPoint = requireFiniteVec3$4(offset2, "ShapeGroup.attachTo() offset");
29372
29373
  dx += offsetPoint[0];
29373
29374
  dy += offsetPoint[1];
29374
29375
  dz += offsetPoint[2];
@@ -29423,7 +29424,7 @@ class ShapeGroup {
29423
29424
  rotateAroundAxis(axis, angleDeg, pivot = [0, 0, 0]) {
29424
29425
  const rotateAxis = requireNonZeroFiniteVec3(axis, "ShapeGroup.rotateAroundAxis() axis");
29425
29426
  const degrees2 = requireFiniteNumber$1(angleDeg, "ShapeGroup.rotateAroundAxis() angleDeg");
29426
- const rotatePivot = requireFiniteVec3$2(pivot, "ShapeGroup.rotateAroundAxis() pivot");
29427
+ const rotatePivot = requireFiniteVec3$4(pivot, "ShapeGroup.rotateAroundAxis() pivot");
29427
29428
  return this.transform(Transform.rotationAxis(rotateAxis, degrees2, rotatePivot));
29428
29429
  }
29429
29430
  /**
@@ -29432,7 +29433,7 @@ class ShapeGroup {
29432
29433
  */
29433
29434
  rotateAroundTo(axis, pivot, movingPoint, targetPoint, options = {}) {
29434
29435
  const rotateAxis = requireNonZeroFiniteVec3(axis, "ShapeGroup.rotateAroundTo() axis");
29435
- const rotatePivot = requireFiniteVec3$2(pivot, "ShapeGroup.rotateAroundTo() pivot");
29436
+ const rotatePivot = requireFiniteVec3$4(pivot, "ShapeGroup.rotateAroundTo() pivot");
29436
29437
  return this.transform(
29437
29438
  Transform.rotateAroundTo(
29438
29439
  rotateAxis,
@@ -29483,7 +29484,7 @@ class ShapeGroup {
29483
29484
  /** Scale uniformly or per-axis from an explicit pivot point. */
29484
29485
  scaleAround(pivot, v) {
29485
29486
  const scale2 = requireNonZeroFiniteScale3(v, "ShapeGroup.scaleAround() scale");
29486
- const scalePivot = requireFiniteVec3$2(pivot, "ShapeGroup.scaleAround() pivot");
29487
+ const scalePivot = requireFiniteVec3$4(pivot, "ShapeGroup.scaleAround() pivot");
29487
29488
  const matrix = Transform.scale(scale2).toArray();
29488
29489
  if (scalePivot[0] === 0 && scalePivot[1] === 0 && scalePivot[2] === 0) {
29489
29490
  return this.mapChildrenTransform((c2) => {
@@ -29506,7 +29507,7 @@ class ShapeGroup {
29506
29507
  }
29507
29508
  /** Mirror across a plane through an explicit point. */
29508
29509
  mirrorThrough(point2, normal) {
29509
- const mirrorPoint = requireFiniteVec3$2(point2, "ShapeGroup.mirrorThrough() point");
29510
+ const mirrorPoint = requireFiniteVec3$4(point2, "ShapeGroup.mirrorThrough() point");
29510
29511
  const mirrorNormal = requireNonZeroFiniteVec3(normal, "ShapeGroup.mirrorThrough() normal");
29511
29512
  const matrix = mirrorPlaneMatrix(mirrorNormal);
29512
29513
  if (mirrorPoint[0] === 0 && mirrorPoint[1] === 0 && mirrorPoint[2] === 0) {
@@ -29594,8 +29595,8 @@ class ShapeGroup {
29594
29595
  * ```
29595
29596
  */
29596
29597
  placeReference(ref, target, offset2) {
29597
- const targetPoint = requireFiniteVec3$2(target, "ShapeGroup.placeReference() target");
29598
- const offsetPoint = offset2 === void 0 ? void 0 : requireFiniteVec3$2(offset2, "ShapeGroup.placeReference() offset");
29598
+ const targetPoint = requireFiniteVec3$4(target, "ShapeGroup.placeReference() target");
29599
+ const offsetPoint = offset2 === void 0 ? void 0 : requireFiniteVec3$4(offset2, "ShapeGroup.placeReference() offset");
29599
29600
  const sourcePoint = this.referencePoint(ref);
29600
29601
  let dx = targetPoint[0] - sourcePoint[0];
29601
29602
  let dy = targetPoint[1] - sourcePoint[1];
@@ -31785,7 +31786,7 @@ function buildSdfFunctionDefinition(source, options) {
31785
31786
  jsExpression: expression,
31786
31787
  ...shader.ok ? { shaderExpression: shader.expression } : { shaderUnsupportedReason: shader.reason },
31787
31788
  raymarchStepLimit: resolveRaymarchStepLimit(options.bounds, options.maxStep),
31788
- ...options.lipschitz !== void 0 ? { raymarchLipschitz: requirePositiveFinite$2(options.lipschitz, "sdf.fromFunction() lipschitz") } : {}
31789
+ ...options.lipschitz !== void 0 ? { raymarchLipschitz: requirePositiveFinite$3(options.lipschitz, "sdf.fromFunction() lipschitz") } : {}
31789
31790
  };
31790
31791
  }
31791
31792
  function extractSdfExpression(source) {
@@ -31953,7 +31954,7 @@ function formatNumericLiteralsForGlsl(source) {
31953
31954
  return result;
31954
31955
  }
31955
31956
  function resolveRaymarchStepLimit(bounds, maxStep) {
31956
- if (maxStep !== void 0) return requirePositiveFinite$2(maxStep, "sdf.fromFunction() maxStep");
31957
+ if (maxStep !== void 0) return requirePositiveFinite$3(maxStep, "sdf.fromFunction() maxStep");
31957
31958
  const dx = bounds.max[0] - bounds.min[0];
31958
31959
  const dy = bounds.max[1] - bounds.min[1];
31959
31960
  const dz = bounds.max[2] - bounds.min[2];
@@ -31961,7 +31962,7 @@ function resolveRaymarchStepLimit(bounds, maxStep) {
31961
31962
  if (!Number.isFinite(diagonal) || diagonal <= 0) return 0.1;
31962
31963
  return Math.max(0.025, Math.min(0.5, diagonal / 240));
31963
31964
  }
31964
- function requirePositiveFinite$2(value, label) {
31965
+ function requirePositiveFinite$3(value, label) {
31965
31966
  if (!Number.isFinite(value) || value <= 0) throw new Error(`${label} must be a positive finite number.`);
31966
31967
  return value;
31967
31968
  }
@@ -32074,7 +32075,7 @@ class Pattern2DBuilder {
32074
32075
  return new Pattern2DImpl({
32075
32076
  kind: "surfacePattern:sineWave",
32076
32077
  direction: normalizeDirection$1(options.direction ?? [1, 0], "sdf.pattern2d().sineWave() direction"),
32077
- wavelength: requirePositiveFinite$1(options.wavelength, "sdf.pattern2d().sineWave() wavelength"),
32078
+ wavelength: requirePositiveFinite$2(options.wavelength, "sdf.pattern2d().sineWave() wavelength"),
32078
32079
  amplitude: requireFinite$a(options.amplitude ?? 1, "sdf.pattern2d().sineWave() amplitude"),
32079
32080
  phase: requireFinite$a(options.phase ?? 0, "sdf.pattern2d().sineWave() phase"),
32080
32081
  bias: requireFinite$a(options.bias ?? 0, "sdf.pattern2d().sineWave() bias")
@@ -32085,19 +32086,19 @@ class Pattern2DBuilder {
32085
32086
  return new Pattern2DImpl({
32086
32087
  kind: "surfacePattern:stripes",
32087
32088
  direction: normalizeDirection$1(options.direction ?? [1, 0], "sdf.pattern2d().stripes() direction"),
32088
- spacing: requirePositiveFinite$1(options.spacing, "sdf.pattern2d().stripes() spacing"),
32089
- width: requirePositiveFinite$1(options.width, "sdf.pattern2d().stripes() width"),
32090
- depth: requireNonNegativeFinite$1(options.depth ?? 1, "sdf.pattern2d().stripes() depth")
32089
+ spacing: requirePositiveFinite$2(options.spacing, "sdf.pattern2d().stripes() spacing"),
32090
+ width: requirePositiveFinite$2(options.width, "sdf.pattern2d().stripes() width"),
32091
+ depth: requireNonNegativeFinite$2(options.depth ?? 1, "sdf.pattern2d().stripes() depth")
32091
32092
  });
32092
32093
  }
32093
32094
  /** Create an over-under woven relief pattern in UV space. */
32094
32095
  overUnderWeave(options) {
32095
32096
  return new Pattern2DImpl({
32096
32097
  kind: "surfacePattern:overUnderWeave",
32097
- spacing: normalizeVec2(options.spacing, "sdf.pattern2d().overUnderWeave() spacing", requirePositiveFinite$1),
32098
- threadWidth: normalizeVec2(options.threadWidth, "sdf.pattern2d().overUnderWeave() threadWidth", requirePositiveFinite$1),
32099
- depth: requireNonNegativeFinite$1(options.depth ?? 0.8, "sdf.pattern2d().overUnderWeave() depth"),
32100
- underScale: requireNonNegativeFinite$1(options.underScale ?? 0.15, "sdf.pattern2d().overUnderWeave() underScale")
32098
+ spacing: normalizeVec2(options.spacing, "sdf.pattern2d().overUnderWeave() spacing", requirePositiveFinite$2),
32099
+ threadWidth: normalizeVec2(options.threadWidth, "sdf.pattern2d().overUnderWeave() threadWidth", requirePositiveFinite$2),
32100
+ depth: requireNonNegativeFinite$2(options.depth ?? 0.8, "sdf.pattern2d().overUnderWeave() depth"),
32101
+ underScale: requireNonNegativeFinite$2(options.underScale ?? 0.15, "sdf.pattern2d().overUnderWeave() underScale")
32101
32102
  });
32102
32103
  }
32103
32104
  }
@@ -32120,13 +32121,13 @@ function requireFinite$a(value, label) {
32120
32121
  }
32121
32122
  return value;
32122
32123
  }
32123
- function requirePositiveFinite$1(value, label) {
32124
+ function requirePositiveFinite$2(value, label) {
32124
32125
  if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
32125
32126
  throw new Error(`${label} must be a positive finite number. Received: ${String(value)}`);
32126
32127
  }
32127
32128
  return value;
32128
32129
  }
32129
- function requireNonNegativeFinite$1(value, label) {
32130
+ function requireNonNegativeFinite$2(value, label) {
32130
32131
  if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
32131
32132
  throw new Error(`${label} must be a non-negative finite number. Received: ${String(value)}`);
32132
32133
  }
@@ -32244,7 +32245,7 @@ Fix: use a known preset or pass material props directly.`
32244
32245
  material: validateMaterialProps(material)
32245
32246
  };
32246
32247
  }
32247
- function requirePositiveFinite(value, label) {
32248
+ function requirePositiveFinite$1(value, label) {
32248
32249
  if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
32249
32250
  throw new Error(`${label} must be a positive finite number. Received: ${String(value)}`);
32250
32251
  }
@@ -32256,15 +32257,15 @@ function requirePositiveInteger(value, label) {
32256
32257
  }
32257
32258
  return value;
32258
32259
  }
32259
- function requireNonNegativeFinite(value, label) {
32260
+ function requireNonNegativeFinite$1(value, label) {
32260
32261
  if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
32261
32262
  throw new Error(`${label} must be a non-negative finite number. Received: ${String(value)}`);
32262
32263
  }
32263
32264
  return value;
32264
32265
  }
32265
32266
  function resolveBlendRadius(input, label, fallback = 4) {
32266
- if (typeof input === "number") return requirePositiveFinite(input, `${label} radius`);
32267
- if ((input == null ? void 0 : input.radius) !== void 0) return requirePositiveFinite(input.radius, `${label} radius`);
32267
+ if (typeof input === "number") return requirePositiveFinite$1(input, `${label} radius`);
32268
+ if ((input == null ? void 0 : input.radius) !== void 0) return requirePositiveFinite$1(input.radius, `${label} radius`);
32268
32269
  return fallback;
32269
32270
  }
32270
32271
  function formatExpressionNumber(value) {
@@ -32274,7 +32275,7 @@ function formatExpressionNumber(value) {
32274
32275
  }
32275
32276
  function roundedBoxNode(halfExtents, radius) {
32276
32277
  const minHalf = Math.min(halfExtents[0], halfExtents[1], halfExtents[2]);
32277
- const r = Math.min(requirePositiveFinite(radius, "SdfShape.round() radius"), Math.max(1e-3, minHalf - 1e-3));
32278
+ const r = Math.min(requirePositiveFinite$1(radius, "SdfShape.round() radius"), Math.max(1e-3, minHalf - 1e-3));
32278
32279
  const inner = [halfExtents[0] - r, halfExtents[1] - r, halfExtents[2] - r];
32279
32280
  const [ix, iy, iz] = inner.map(formatExpressionNumber);
32280
32281
  const rr = formatExpressionNumber(r);
@@ -32517,9 +32518,9 @@ class SdfShape {
32517
32518
  clipBox(x2, y2, z2) {
32518
32519
  return this.intersect(
32519
32520
  box$1(
32520
- requirePositiveFinite(x2, "SdfShape.clipBox() x"),
32521
- requirePositiveFinite(y2, "SdfShape.clipBox() y"),
32522
- requirePositiveFinite(z2, "SdfShape.clipBox() z")
32521
+ requirePositiveFinite$1(x2, "SdfShape.clipBox() x"),
32522
+ requirePositiveFinite$1(y2, "SdfShape.clipBox() y"),
32523
+ requirePositiveFinite$1(z2, "SdfShape.clipBox() z")
32523
32524
  )
32524
32525
  );
32525
32526
  }
@@ -32551,7 +32552,7 @@ class SdfShape {
32551
32552
  return this.withNode({
32552
32553
  kind: "sdf:smoothUnion",
32553
32554
  children: [this._node, other._node],
32554
- radius: requirePositiveFinite(radius, "SdfShape.smoothUnion() radius")
32555
+ radius: requirePositiveFinite$1(radius, "SdfShape.smoothUnion() radius")
32555
32556
  });
32556
32557
  }
32557
32558
  /** Smooth difference — smoothly carves other from this. */
@@ -32559,7 +32560,7 @@ class SdfShape {
32559
32560
  return this.withNode({
32560
32561
  kind: "sdf:smoothDifference",
32561
32562
  children: [this._node, other._node],
32562
- radius: requirePositiveFinite(radius, "SdfShape.smoothSubtract() radius")
32563
+ radius: requirePositiveFinite$1(radius, "SdfShape.smoothSubtract() radius")
32563
32564
  });
32564
32565
  }
32565
32566
  /** Smooth intersection — smoothly intersects. */
@@ -32567,7 +32568,7 @@ class SdfShape {
32567
32568
  return this.withNode({
32568
32569
  kind: "sdf:smoothIntersection",
32569
32570
  children: [this._node, other._node],
32570
- radius: requirePositiveFinite(radius, "SdfShape.smoothIntersect() radius")
32571
+ radius: requirePositiveFinite$1(radius, "SdfShape.smoothIntersect() radius")
32571
32572
  });
32572
32573
  }
32573
32574
  /** Morph between this shape and another. t=0 → this, t=1 → other. */
@@ -32625,7 +32626,7 @@ class SdfShape {
32625
32626
  }
32626
32627
  /** Uniformly scale this SDF around the origin. */
32627
32628
  scale(factor) {
32628
- return this.withNode({ kind: "sdf:scale", child: this._node, factor: requirePositiveFinite(factor, "SdfShape.scale() factor") });
32629
+ return this.withNode({ kind: "sdf:scale", child: this._node, factor: requirePositiveFinite$1(factor, "SdfShape.scale() factor") });
32629
32630
  }
32630
32631
  // ── Domain operations ──
32631
32632
  /** Twist around the Z axis. */
@@ -32638,15 +32639,15 @@ class SdfShape {
32638
32639
  }
32639
32640
  /** Bend around the Z axis with given radius. */
32640
32641
  bend(radius) {
32641
- return this.withNode({ kind: "sdf:bend", child: this._node, radius: requirePositiveFinite(radius, "SdfShape.bend() radius") });
32642
+ return this.withNode({ kind: "sdf:bend", child: this._node, radius: requirePositiveFinite$1(radius, "SdfShape.bend() radius") });
32642
32643
  }
32643
32644
  /** Repeat in space. Spacing of 0 on an axis means no repetition. Count of 0 = infinite. */
32644
32645
  repeat(spacing, count) {
32645
32646
  return this.withNode({
32646
32647
  kind: "sdf:repeat",
32647
32648
  child: this._node,
32648
- spacing: requireFiniteVec3$2(spacing, "SdfShape.repeat() spacing"),
32649
- count: count === void 0 ? [0, 0, 0] : requireFiniteVec3$2(count, "SdfShape.repeat() count")
32649
+ spacing: requireFiniteVec3$4(spacing, "SdfShape.repeat() spacing"),
32650
+ count: count === void 0 ? [0, 0, 0] : requireFiniteVec3$4(count, "SdfShape.repeat() count")
32650
32651
  });
32651
32652
  }
32652
32653
  /**
@@ -32661,7 +32662,7 @@ class SdfShape {
32661
32662
  kind: "sdf:circularArray",
32662
32663
  child: this._node,
32663
32664
  count: requirePositiveInteger(count, "SdfShape.circularArray() count"),
32664
- offset: requireNonNegativeFinite(offset2, "SdfShape.circularArray() offset")
32665
+ offset: requireNonNegativeFinite$1(offset2, "SdfShape.circularArray() offset")
32665
32666
  });
32666
32667
  }
32667
32668
  /** Hollow out, keeping only a shell of given thickness. */
@@ -32669,7 +32670,7 @@ class SdfShape {
32669
32670
  return this.withNode({
32670
32671
  kind: "sdf:shell",
32671
32672
  child: this._node,
32672
- thickness: requirePositiveFinite(thickness, "SdfShape.shell() thickness")
32673
+ thickness: requirePositiveFinite$1(thickness, "SdfShape.shell() thickness")
32673
32674
  });
32674
32675
  }
32675
32676
  /**
@@ -32749,70 +32750,70 @@ class SdfShape {
32749
32750
  kind: "sdf:onion",
32750
32751
  child: this._node,
32751
32752
  layers: requirePositiveInteger(layers, "SdfShape.onion() layers"),
32752
- thickness: requirePositiveFinite(thickness, "SdfShape.onion() thickness")
32753
+ thickness: requirePositiveFinite$1(thickness, "SdfShape.onion() thickness")
32753
32754
  });
32754
32755
  }
32755
32756
  }
32756
32757
  function sphere$1(radius) {
32757
- return new SdfShape({ kind: "sdf:sphere", radius: requirePositiveFinite(radius, "sdf.sphere() radius") });
32758
+ return new SdfShape({ kind: "sdf:sphere", radius: requirePositiveFinite$1(radius, "sdf.sphere() radius") });
32758
32759
  }
32759
32760
  function box$1(x2, y2, z2) {
32760
32761
  return new SdfShape({
32761
32762
  kind: "sdf:box",
32762
32763
  halfExtents: [
32763
- requirePositiveFinite(x2, "sdf.box() x") / 2,
32764
- requirePositiveFinite(y2, "sdf.box() y") / 2,
32765
- requirePositiveFinite(z2, "sdf.box() z") / 2
32764
+ requirePositiveFinite$1(x2, "sdf.box() x") / 2,
32765
+ requirePositiveFinite$1(y2, "sdf.box() y") / 2,
32766
+ requirePositiveFinite$1(z2, "sdf.box() z") / 2
32766
32767
  ]
32767
32768
  });
32768
32769
  }
32769
32770
  function cylinder$1(height, radius) {
32770
32771
  return new SdfShape({
32771
32772
  kind: "sdf:cylinder",
32772
- height: requirePositiveFinite(height, "sdf.cylinder() height"),
32773
- radius: requirePositiveFinite(radius, "sdf.cylinder() radius")
32773
+ height: requirePositiveFinite$1(height, "sdf.cylinder() height"),
32774
+ radius: requirePositiveFinite$1(radius, "sdf.cylinder() radius")
32774
32775
  });
32775
32776
  }
32776
32777
  function torus$1(majorRadius, minorRadius) {
32777
32778
  return new SdfShape({
32778
32779
  kind: "sdf:torus",
32779
- majorRadius: requirePositiveFinite(majorRadius, "sdf.torus() majorRadius"),
32780
- minorRadius: requirePositiveFinite(minorRadius, "sdf.torus() minorRadius")
32780
+ majorRadius: requirePositiveFinite$1(majorRadius, "sdf.torus() majorRadius"),
32781
+ minorRadius: requirePositiveFinite$1(minorRadius, "sdf.torus() minorRadius")
32781
32782
  });
32782
32783
  }
32783
32784
  function capsule(height, radius) {
32784
32785
  return new SdfShape({
32785
32786
  kind: "sdf:capsule",
32786
- height: requirePositiveFinite(height, "sdf.capsule() height"),
32787
- radius: requirePositiveFinite(radius, "sdf.capsule() radius")
32787
+ height: requirePositiveFinite$1(height, "sdf.capsule() height"),
32788
+ radius: requirePositiveFinite$1(radius, "sdf.capsule() radius")
32788
32789
  });
32789
32790
  }
32790
32791
  function cone(height, radius) {
32791
32792
  return new SdfShape({
32792
32793
  kind: "sdf:cone",
32793
- height: requirePositiveFinite(height, "sdf.cone() height"),
32794
- radius: requirePositiveFinite(radius, "sdf.cone() radius")
32794
+ height: requirePositiveFinite$1(height, "sdf.cone() height"),
32795
+ radius: requirePositiveFinite$1(radius, "sdf.cone() radius")
32795
32796
  });
32796
32797
  }
32797
32798
  function smoothUnion(a2, b, options) {
32798
32799
  return new SdfShape({
32799
32800
  kind: "sdf:smoothUnion",
32800
32801
  children: [a2._node, b._node],
32801
- radius: requirePositiveFinite(options.radius, "sdf.smoothUnion() radius")
32802
+ radius: requirePositiveFinite$1(options.radius, "sdf.smoothUnion() radius")
32802
32803
  });
32803
32804
  }
32804
32805
  function smoothDifference(a2, b, options) {
32805
32806
  return new SdfShape({
32806
32807
  kind: "sdf:smoothDifference",
32807
32808
  children: [a2._node, b._node],
32808
- radius: requirePositiveFinite(options.radius, "sdf.smoothDifference() radius")
32809
+ radius: requirePositiveFinite$1(options.radius, "sdf.smoothDifference() radius")
32809
32810
  });
32810
32811
  }
32811
32812
  function smoothIntersection(a2, b, options) {
32812
32813
  return new SdfShape({
32813
32814
  kind: "sdf:smoothIntersection",
32814
32815
  children: [a2._node, b._node],
32815
- radius: requirePositiveFinite(options.radius, "sdf.smoothIntersection() radius")
32816
+ radius: requirePositiveFinite$1(options.radius, "sdf.smoothIntersection() radius")
32816
32817
  });
32817
32818
  }
32818
32819
  function morph(a2, b, t) {
@@ -32995,9 +32996,9 @@ function weave(options) {
32995
32996
  });
32996
32997
  }
32997
32998
  function basketWeave(options) {
32998
- const SP = requirePositiveFinite((options == null ? void 0 : options.spacing) ?? 3, "sdf.basketWeave() spacing");
32999
- const TW = requirePositiveFinite((options == null ? void 0 : options.threadWidth) ?? 1.5, "sdf.basketWeave() threadWidth");
33000
- const D2 = requireNonNegativeFinite((options == null ? void 0 : options.depth) ?? 0.8, "sdf.basketWeave() depth");
32999
+ const SP = requirePositiveFinite$1((options == null ? void 0 : options.spacing) ?? 3, "sdf.basketWeave() spacing");
33000
+ const TW = requirePositiveFinite$1((options == null ? void 0 : options.threadWidth) ?? 1.5, "sdf.basketWeave() threadWidth");
33001
+ const D2 = requireNonNegativeFinite$1((options == null ? void 0 : options.depth) ?? 0.8, "sdf.basketWeave() depth");
33001
33002
  return pattern2d().overUnderWeave({ spacing: SP, threadWidth: TW, depth: D2 });
33002
33003
  }
33003
33004
  function fromFunction(fn, options) {
@@ -33026,18 +33027,18 @@ function twist(shape, degreesPerUnit) {
33026
33027
  return shape.twist(requireFiniteNumber$1(degreesPerUnit, "sdf.twist() degreesPerUnit"));
33027
33028
  }
33028
33029
  function bend(shape, radius) {
33029
- return shape.bend(requirePositiveFinite(radius, "sdf.bend() radius"));
33030
+ return shape.bend(requirePositiveFinite$1(radius, "sdf.bend() radius"));
33030
33031
  }
33031
33032
  function repeat(shape, spacing, count) {
33032
33033
  return shape.repeat(
33033
- requireFiniteVec3$2(spacing, "sdf.repeat() spacing"),
33034
- count === void 0 ? void 0 : requireFiniteVec3$2(count, "sdf.repeat() count")
33034
+ requireFiniteVec3$4(spacing, "sdf.repeat() spacing"),
33035
+ count === void 0 ? void 0 : requireFiniteVec3$4(count, "sdf.repeat() count")
33035
33036
  );
33036
33037
  }
33037
33038
  function circularArray(shape, count, offset2 = 0) {
33038
33039
  return shape.circularArray(
33039
33040
  requirePositiveInteger(count, "sdf.circularArray() count"),
33040
- requireNonNegativeFinite(offset2, "sdf.circularArray() offset")
33041
+ requireNonNegativeFinite$1(offset2, "sdf.circularArray() offset")
33041
33042
  );
33042
33043
  }
33043
33044
  function resolveTpmsOptions(options) {
@@ -33818,11 +33819,11 @@ function mergeTopology(base, overlay) {
33818
33819
  return merged;
33819
33820
  }
33820
33821
  const ANALYTIC_SURFACE_EPS = 1e-9;
33821
- function vecLength$1(v) {
33822
+ function vecLength$2(v) {
33822
33823
  return Math.hypot(v[0], v[1], v[2]);
33823
33824
  }
33824
33825
  function normalizeVec3OrNull(v) {
33825
- const length4 = vecLength$1(v);
33826
+ const length4 = vecLength$2(v);
33826
33827
  return length4 > ANALYTIC_SURFACE_EPS ? [v[0] / length4, v[1] / length4, v[2] / length4] : null;
33827
33828
  }
33828
33829
  function dotVec3$1(a2, b) {
@@ -33874,9 +33875,9 @@ function matrixUniformDistanceScale(matrix) {
33874
33875
  const xAxis = [matrix[0], matrix[1], matrix[2]];
33875
33876
  const yAxis = [matrix[4], matrix[5], matrix[6]];
33876
33877
  const zAxis = [matrix[8], matrix[9], matrix[10]];
33877
- const sx = vecLength$1(xAxis);
33878
- const sy = vecLength$1(yAxis);
33879
- const sz = vecLength$1(zAxis);
33878
+ const sx = vecLength$2(xAxis);
33879
+ const sy = vecLength$2(yAxis);
33880
+ const sz = vecLength$2(zAxis);
33880
33881
  if (sx <= ANALYTIC_SURFACE_EPS || sy <= ANALYTIC_SURFACE_EPS || sz <= ANALYTIC_SURFACE_EPS) return null;
33881
33882
  if (!sameScalar(sx, sy) || !sameScalar(sx, sz)) return null;
33882
33883
  const orthoTolerance = ANALYTIC_SURFACE_EPS * Math.max(1, sx * sx);
@@ -34564,7 +34565,7 @@ function withBaseDimensionsAndMergedSourceSpans(base, sources, out, preserveOutp
34564
34565
  return result;
34565
34566
  }
34566
34567
  function resolveRotationPoint(shape, point2) {
34567
- if (Array.isArray(point2)) return requireFiniteVec3$2(point2, "rotateAroundTo(): point");
34568
+ if (Array.isArray(point2)) return requireFiniteVec3$4(point2, "rotateAroundTo(): point");
34568
34569
  return shape.referencePoint(point2);
34569
34570
  }
34570
34571
  function setShapeDimensions(shape, dims, options = {}) {
@@ -35456,8 +35457,8 @@ class Shape {
35456
35457
  * ```
35457
35458
  */
35458
35459
  placeReference(ref, target, offset2) {
35459
- const targetPoint = requireFiniteVec3$2(target, "Shape.placeReference() target");
35460
- const offsetPoint = offset2 === void 0 ? void 0 : requireFiniteVec3$2(offset2, "Shape.placeReference() offset");
35460
+ const targetPoint = requireFiniteVec3$4(target, "Shape.placeReference() target");
35461
+ const offsetPoint = offset2 === void 0 ? void 0 : requireFiniteVec3$4(offset2, "Shape.placeReference() offset");
35461
35462
  const sourcePoint = this.referencePoint(ref);
35462
35463
  let dx = targetPoint[0] - sourcePoint[0];
35463
35464
  let dy = targetPoint[1] - sourcePoint[1];
@@ -35562,7 +35563,7 @@ class Shape {
35562
35563
  /** Scale the shape uniformly or per-axis from an explicit pivot point. */
35563
35564
  scaleAround(pivot, v) {
35564
35565
  const scale2 = requireNonZeroFiniteScale3(v, "Shape.scaleAround() scale");
35565
- const scalePivot = requireFiniteVec3$2(pivot, "Shape.scaleAround() pivot");
35566
+ const scalePivot = requireFiniteVec3$4(pivot, "Shape.scaleAround() pivot");
35566
35567
  if (scalePivot[0] === 0 && scalePivot[1] === 0 && scalePivot[2] === 0) {
35567
35568
  const nextPlan2 = appendShapeCompileTransform(getShapeCompilePlanInternal(this), {
35568
35569
  kind: "scale",
@@ -35600,7 +35601,7 @@ class Shape {
35600
35601
  }
35601
35602
  /** Mirror across a plane through an explicit point, defined by its normal vector. */
35602
35603
  mirrorThrough(point2, normal) {
35603
- const mirrorPoint = requireFiniteVec3$2(point2, "Shape.mirrorThrough() point");
35604
+ const mirrorPoint = requireFiniteVec3$4(point2, "Shape.mirrorThrough() point");
35604
35605
  const mirrorNormal = requireNonZeroFiniteVec3(normal, "Shape.mirrorThrough() normal");
35605
35606
  if (mirrorPoint[0] === 0 && mirrorPoint[1] === 0 && mirrorPoint[2] === 0) {
35606
35607
  const transformedPlan2 = appendShapeCompileTransform(getShapeCompilePlanInternal(this), {
@@ -35658,7 +35659,7 @@ class Shape {
35658
35659
  rotateAroundAxis(axis, angleDeg, pivot = [0, 0, 0]) {
35659
35660
  const rotateAxis = requireNonZeroFiniteVec3(axis, "Shape.rotateAroundAxis() axis");
35660
35661
  const degrees2 = requireFiniteNumber$1(angleDeg, "Shape.rotateAroundAxis() angleDeg");
35661
- const rotatePivot = requireFiniteVec3$2(pivot, "Shape.rotateAroundAxis() pivot");
35662
+ const rotatePivot = requireFiniteVec3$4(pivot, "Shape.rotateAroundAxis() pivot");
35662
35663
  const len2 = Math.sqrt(rotateAxis[0] ** 2 + rotateAxis[1] ** 2 + rotateAxis[2] ** 2) || 1;
35663
35664
  const normalizedAxis = [rotateAxis[0] / len2, rotateAxis[1] / len2, rotateAxis[2] / len2];
35664
35665
  const matrix = rotationAroundAxisMatrix(normalizedAxis, degrees2, rotatePivot);
@@ -35683,7 +35684,7 @@ class Shape {
35683
35684
  */
35684
35685
  rotateAroundTo(axis, pivot, movingPoint, targetPoint, options = {}) {
35685
35686
  const rotateAxis = requireNonZeroFiniteVec3(axis, "Shape.rotateAroundTo() axis");
35686
- const rotatePivot = requireFiniteVec3$2(pivot, "Shape.rotateAroundTo() pivot");
35687
+ const rotatePivot = requireFiniteVec3$4(pivot, "Shape.rotateAroundTo() pivot");
35687
35688
  const moving = resolveRotationPoint(this, movingPoint);
35688
35689
  const target = resolveRotationPoint(this, targetPoint);
35689
35690
  const angleDeg = solveRotateAroundAngle(rotateAxis, rotatePivot, moving, target, options);
@@ -36006,7 +36007,7 @@ class Shape {
36006
36007
  const sp = this.referencePoint(selfAnchor);
36007
36008
  let dx = tp[0] - sp[0], dy = tp[1] - sp[1], dz = tp[2] - sp[2];
36008
36009
  if (offset2) {
36009
- const offsetPoint = requireFiniteVec3$2(offset2, "Shape.attachTo() offset");
36010
+ const offsetPoint = requireFiniteVec3$4(offset2, "Shape.attachTo() offset");
36010
36011
  dx += offsetPoint[0];
36011
36012
  dy += offsetPoint[1];
36012
36013
  dz += offsetPoint[2];
@@ -38244,6 +38245,107 @@ function collectJointsView(options = {}, base = null, motionSource) {
38244
38245
  function jointsView(options = {}) {
38245
38246
  _collected$8 = collectJointsView(options, _collected$8);
38246
38247
  }
38248
+ function requireFiniteVec3$2(input, label) {
38249
+ if (!Array.isArray(input) || input.length !== 3) throw new Error(`${label} must be a [x, y, z] tuple`);
38250
+ const out = [input[0], input[1], input[2]];
38251
+ if (!Number.isFinite(out[0]) || !Number.isFinite(out[1]) || !Number.isFinite(out[2])) {
38252
+ throw new Error(`${label} must contain finite numbers`);
38253
+ }
38254
+ return out;
38255
+ }
38256
+ function vecDot$1(a2, b) {
38257
+ return a2[0] * b[0] + a2[1] * b[1] + a2[2] * b[2];
38258
+ }
38259
+ function vecCross$1(a2, b) {
38260
+ 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]];
38261
+ }
38262
+ function vecLength$1(v) {
38263
+ return Math.hypot(v[0], v[1], v[2]);
38264
+ }
38265
+ function vecNormalize$1(v, label) {
38266
+ const length4 = vecLength$1(v);
38267
+ if (length4 < 1e-10) throw new Error(`${label} must be non-zero`);
38268
+ return [v[0] / length4, v[1] / length4, v[2] / length4];
38269
+ }
38270
+ function orthonormalizeFrame(axisInput, upInput) {
38271
+ const axis = vecNormalize$1(axisInput, "frame axis");
38272
+ if (vecLength$1(upInput) < 1e-10) throw new Error("frame up vector must be non-zero");
38273
+ const upProjection = vecDot$1(upInput, axis);
38274
+ const upOrtho = [upInput[0] - upProjection * axis[0], upInput[1] - upProjection * axis[1], upInput[2] - upProjection * axis[2]];
38275
+ if (vecLength$1(upOrtho) < 1e-10) throw new Error("frame axis and up must not be parallel");
38276
+ const up = vecNormalize$1(upOrtho, "frame up vector");
38277
+ const right = vecNormalize$1(vecCross$1(up, axis), "frame right vector");
38278
+ return { axis, up, right };
38279
+ }
38280
+ function frameTransform(origin, axisInput, upInput) {
38281
+ const { axis, up, right } = orthonormalizeFrame(axisInput, upInput);
38282
+ return Transform.from([
38283
+ right[0],
38284
+ right[1],
38285
+ right[2],
38286
+ 0,
38287
+ up[0],
38288
+ up[1],
38289
+ up[2],
38290
+ 0,
38291
+ axis[0],
38292
+ axis[1],
38293
+ axis[2],
38294
+ 0,
38295
+ origin[0],
38296
+ origin[1],
38297
+ origin[2],
38298
+ 1
38299
+ ]);
38300
+ }
38301
+ function normalizeAssemblyFrame(name, input) {
38302
+ if (!input || typeof input !== "object") throw new Error(`frame("${name}") options are required`);
38303
+ const origin = requireFiniteVec3$2(input.origin, `frame("${name}") origin`);
38304
+ const rawAxis = requireFiniteVec3$2(input.axis, `frame("${name}") axis`);
38305
+ const rawUp = requireFiniteVec3$2(input.up, `frame("${name}") up`);
38306
+ const { axis, up } = orthonormalizeFrame(rawAxis, rawUp);
38307
+ return {
38308
+ name,
38309
+ origin,
38310
+ axis,
38311
+ up,
38312
+ fixed: input.fixed ?? false,
38313
+ metadata: input.metadata ? { ...input.metadata } : void 0
38314
+ };
38315
+ }
38316
+ function cloneAssemblyFrame(frame) {
38317
+ return {
38318
+ name: frame.name,
38319
+ origin: [...frame.origin],
38320
+ axis: [...frame.axis],
38321
+ up: [...frame.up],
38322
+ fixed: frame.fixed,
38323
+ metadata: frame.metadata ? { ...frame.metadata } : void 0
38324
+ };
38325
+ }
38326
+ function cloneAssemblyFrameEdge(edge) {
38327
+ return {
38328
+ name: edge.name,
38329
+ a: edge.a,
38330
+ b: edge.b,
38331
+ metadata: edge.metadata ? { ...edge.metadata } : void 0
38332
+ };
38333
+ }
38334
+ function frameDefTransform(frame) {
38335
+ return frameTransform(frame.origin, frame.axis, frame.up);
38336
+ }
38337
+ function frameFromTransform(frame, transform) {
38338
+ return {
38339
+ ...cloneAssemblyFrame(frame),
38340
+ origin: transform.point([0, 0, 0]),
38341
+ up: vecNormalize$1(transform.vector([0, 1, 0]), `solved frame "${frame.name}" up`),
38342
+ axis: vecNormalize$1(transform.vector([0, 0, 1]), `solved frame "${frame.name}" axis`),
38343
+ transform
38344
+ };
38345
+ }
38346
+ function relativeFrameTransform(parent, child) {
38347
+ return composeChain(frameDefTransform(child), frameDefTransform(parent).inverse());
38348
+ }
38247
38349
  var define_process_env_default$1 = {};
38248
38350
  let collectedAssemblies = [];
38249
38351
  function resetCollectedAssemblies() {
@@ -38377,12 +38479,22 @@ function motionTransform(joint2, value) {
38377
38479
  const dz = joint2.axis[2] * value;
38378
38480
  return Transform.identity().translate(dx, dy, dz);
38379
38481
  }
38482
+ function frameJointMotionTransform(joint2, value) {
38483
+ if (joint2.type === "fixed") return Transform.identity();
38484
+ if (joint2.type === "revolute") return Transform.rotationAxis([0, 0, 1], value);
38485
+ return Transform.translation(0, 0, value);
38486
+ }
38380
38487
  function clampJointValue(joint2, value) {
38381
38488
  let clamped = Number.isFinite(value) ? value : joint2.defaultValue;
38382
38489
  if (joint2.min != null) clamped = Math.max(joint2.min, clamped);
38383
38490
  if (joint2.max != null) clamped = Math.min(joint2.max, clamped);
38384
38491
  return { value: clamped, wasClamped: clamped !== value };
38385
38492
  }
38493
+ function clampFrameJointDefault(jointName, value, min2, max2) {
38494
+ if (min2 !== void 0 && value < min2) throw new Error(`Frame joint "${jointName}" default must be >= min`);
38495
+ if (max2 !== void 0 && value > max2) throw new Error(`Frame joint "${jointName}" default must be <= max`);
38496
+ return value;
38497
+ }
38386
38498
  function finiteVec3(input, label) {
38387
38499
  if (!Array.isArray(input) || input.length !== 3) throw new Error(`${label} must be a [x, y, z] tuple`);
38388
38500
  const out = [input[0], input[1], input[2]];
@@ -38394,6 +38506,9 @@ function finiteVec3(input, label) {
38394
38506
  function cloneVec3$3(v) {
38395
38507
  return [v[0], v[1], v[2]];
38396
38508
  }
38509
+ function cloneAngleReference(reference) {
38510
+ return reference ? { kind: "worldDirection", direction: cloneVec3$3(reference.direction) } : void 0;
38511
+ }
38397
38512
  function vecDistance(a2, b) {
38398
38513
  return Math.hypot(b[0] - a2[0], b[1] - a2[1], b[2] - a2[2]);
38399
38514
  }
@@ -38413,12 +38528,10 @@ function vecNormalize(a2) {
38413
38528
  const len2 = vecLength(a2);
38414
38529
  return len2 < 1e-12 ? [0, 0, 0] : [a2[0] / len2, a2[1] / len2, a2[2] / len2];
38415
38530
  }
38416
- function signedAngleAboutAxis(a2, b, c2, axisUnit) {
38417
- const ba = vecSub(a2, b);
38418
- const bc = vecSub(c2, b);
38419
- const cross2 = vecCross(ba, bc);
38531
+ function signedAngleBetweenVectorsAboutAxis(reference, target, axisUnit) {
38532
+ const cross2 = vecCross(reference, target);
38420
38533
  const sin2 = vecDot(cross2, axisUnit);
38421
- const cos2 = vecDot(ba, bc);
38534
+ const cos2 = vecDot(reference, target);
38422
38535
  return Math.atan2(sin2, cos2) * 180 / Math.PI;
38423
38536
  }
38424
38537
  function normalizeAngleDeltaDeg(value) {
@@ -38539,7 +38652,7 @@ function deriveExplodeHintsFromMates(constraints, result, bodies, ctx) {
38539
38652
  return hints;
38540
38653
  }
38541
38654
  class SolvedAssembly {
38542
- constructor(name, parts, transforms, jointValues, solveWarnings, _mateMetadata = null, _kinematics = null, usedPortRefs) {
38655
+ constructor(name, parts, transforms, jointValues, solveWarnings, _mateMetadata = null, _kinematics = null, _frames = /* @__PURE__ */ new Map(), usedPortRefs) {
38543
38656
  __publicField(this, "_usedPortRefs");
38544
38657
  this.name = name;
38545
38658
  this.parts = parts;
@@ -38548,6 +38661,7 @@ class SolvedAssembly {
38548
38661
  this.solveWarnings = solveWarnings;
38549
38662
  this._mateMetadata = _mateMetadata;
38550
38663
  this._kinematics = _kinematics;
38664
+ this._frames = _frames;
38551
38665
  this._usedPortRefs = usedPortRefs ?? /* @__PURE__ */ new Set();
38552
38666
  }
38553
38667
  /** Return any warnings generated during solve (clamped joints, unconverged mates, etc.). */
@@ -38573,7 +38687,7 @@ class SolvedAssembly {
38573
38687
  var _a3;
38574
38688
  return ((_a3 = this._mateMetadata) == null ? void 0 : _a3.converged) ?? null;
38575
38689
  }
38576
- /** Solved assembly-native kinematic graph, or null when no links were declared. */
38690
+ /** Solved assembly-native kinematic or frame-edge overlay data, or null when no rig overlay data was declared. */
38577
38691
  get kinematics() {
38578
38692
  return this._kinematics;
38579
38693
  }
@@ -38584,6 +38698,19 @@ class SolvedAssembly {
38584
38698
  if (!link) throw new Error(`Unknown kinematic link "${linkName}"`);
38585
38699
  return cloneVec3$3(link.position);
38586
38700
  }
38701
+ /** Return the solved world transform for a named rig frame. */
38702
+ getFrame(frameName) {
38703
+ const frame = this._frames.get(frameName);
38704
+ if (!frame) throw new Error(`Unknown frame "${frameName}"`);
38705
+ return frame.transform;
38706
+ }
38707
+ /** Return solved rig frames, including origin, axis, up, and transform. */
38708
+ get frames() {
38709
+ return [...this._frames.values()].map((frame) => ({
38710
+ ...cloneAssemblyFrame(frame),
38711
+ transform: frame.transform
38712
+ }));
38713
+ }
38587
38714
  /**
38588
38715
  * Return the world-space `Transform` for the named part at the solved pose.
38589
38716
  *
@@ -38836,14 +38963,19 @@ class Assembly {
38836
38963
  __publicField(this, "parts", /* @__PURE__ */ new Map());
38837
38964
  __publicField(this, "joints", /* @__PURE__ */ new Map());
38838
38965
  __publicField(this, "jointCouplings", /* @__PURE__ */ new Map());
38966
+ __publicField(this, "frames", /* @__PURE__ */ new Map());
38967
+ __publicField(this, "frameJoints", /* @__PURE__ */ new Map());
38968
+ __publicField(this, "frameEdges", /* @__PURE__ */ new Map());
38839
38969
  __publicField(this, "links", /* @__PURE__ */ new Map());
38840
38970
  __publicField(this, "linkEdges", /* @__PURE__ */ new Map());
38841
38971
  __publicField(this, "linkAngles", /* @__PURE__ */ new Map());
38972
+ __publicField(this, "derivedLinks", /* @__PURE__ */ new Map());
38842
38973
  __publicField(this, "_mateFns", []);
38843
38974
  __publicField(this, "_refs", createPlacementReferences());
38844
38975
  __publicField(this, "_portsByPart", /* @__PURE__ */ new Map());
38845
38976
  __publicField(this, "_usedPortRefs", /* @__PURE__ */ new Set());
38846
38977
  __publicField(this, "_connectCounter", 0);
38978
+ __publicField(this, "_frameEdgeCounter", 0);
38847
38979
  __publicField(this, "_linkEdgeCounter", 0);
38848
38980
  __publicField(this, "_linkAngleCounter", 0);
38849
38981
  this.name = name;
@@ -38853,6 +38985,7 @@ class Assembly {
38853
38985
  const next = new Assembly(this.name);
38854
38986
  next._refs = clonePlacementReferences(this._refs);
38855
38987
  next._connectCounter = this._connectCounter;
38988
+ next._frameEdgeCounter = this._frameEdgeCounter;
38856
38989
  next._linkEdgeCounter = this._linkEdgeCounter;
38857
38990
  next._linkAngleCounter = this._linkAngleCounter;
38858
38991
  next._mateFns.push(...this._mateFns);
@@ -38862,7 +38995,8 @@ class Assembly {
38862
38995
  part: record.part,
38863
38996
  base: record.base,
38864
38997
  metadata: record.metadata ? { ...record.metadata } : void 0,
38865
- mates: record.mates.map((mate) => ({ ...mate }))
38998
+ mates: record.mates.map((mate) => ({ ...mate })),
38999
+ bindToFrame: record.bindToFrame
38866
39000
  });
38867
39001
  }
38868
39002
  for (const [name, joint2] of this.joints) {
@@ -38891,6 +39025,27 @@ class Assembly {
38891
39025
  offset: coupling.offset
38892
39026
  });
38893
39027
  }
39028
+ for (const [name, frame] of this.frames) {
39029
+ next.frames.set(name, cloneAssemblyFrame(frame));
39030
+ }
39031
+ for (const [name, joint2] of this.frameJoints) {
39032
+ next.frameJoints.set(name, {
39033
+ name: joint2.name,
39034
+ type: joint2.type,
39035
+ parent: joint2.parent,
39036
+ child: joint2.child,
39037
+ rest: joint2.rest,
39038
+ min: joint2.min,
39039
+ max: joint2.max,
39040
+ defaultValue: joint2.defaultValue,
39041
+ unit: joint2.unit,
39042
+ control: joint2.control,
39043
+ metadata: joint2.metadata ? { ...joint2.metadata } : void 0
39044
+ });
39045
+ }
39046
+ for (const [name, edge] of this.frameEdges) {
39047
+ next.frameEdges.set(name, cloneAssemblyFrameEdge(edge));
39048
+ }
38894
39049
  for (const [name, link] of this.links) {
38895
39050
  next.links.set(name, {
38896
39051
  name: link.name,
@@ -38918,6 +39073,7 @@ class Assembly {
38918
39073
  a: angle.a,
38919
39074
  b: angle.b,
38920
39075
  c: angle.c,
39076
+ reference: cloneAngleReference(angle.reference),
38921
39077
  target: angle.target,
38922
39078
  min: angle.min,
38923
39079
  max: angle.max,
@@ -38925,6 +39081,9 @@ class Assembly {
38925
39081
  metadata: angle.metadata ? { ...angle.metadata } : void 0
38926
39082
  });
38927
39083
  }
39084
+ for (const [name, derived] of this.derivedLinks) {
39085
+ next.derivedLinks.set(name, { ...derived });
39086
+ }
38928
39087
  for (const [partName, ports] of this._portsByPart) {
38929
39088
  next._portsByPart.set(partName, clonePortMap(ports));
38930
39089
  }
@@ -39062,6 +39221,129 @@ class Assembly {
39062
39221
  port: resolved.connector
39063
39222
  };
39064
39223
  }
39224
+ /**
39225
+ * Add a named rig frame to the assembly.
39226
+ *
39227
+ * A frame is a solved pose: `origin` plus orientation. `axis` is the frame's
39228
+ * primary direction and `up` fixes roll around that axis. Use frames for
39229
+ * robot links, joint axes, and parts that must carry orientation. Use
39230
+ * `link()` for solved points in distance/angle graphs.
39231
+ *
39232
+ * @category Assembly
39233
+ */
39234
+ frame(name, options) {
39235
+ const id = typeof name === "string" ? name.trim() : "";
39236
+ if (!id) throw new Error("frame() name must be non-empty");
39237
+ if (this.frames.has(id)) throw new Error(`Frame "${id}" already exists`);
39238
+ this.frames.set(id, normalizeAssemblyFrame(id, options));
39239
+ return this;
39240
+ }
39241
+ resolveFrameJointEndpoint(jointName, value, role) {
39242
+ const frameName = typeof value === "string" ? value.trim() : "";
39243
+ if (!frameName) throw new Error(`Frame joint "${jointName}" ${role} must be non-empty`);
39244
+ const frame = this.frames.get(frameName);
39245
+ if (!frame) throw new Error(`Frame joint "${jointName}" unknown ${role} frame "${frameName}"`);
39246
+ return frame;
39247
+ }
39248
+ addFrameJoint(type, name, options) {
39249
+ const id = typeof name === "string" ? name.trim() : "";
39250
+ if (!id) throw new Error(`${type}Joint() name must be non-empty`);
39251
+ if (!options || typeof options !== "object") throw new Error(`${type}Joint("${id}") options are required`);
39252
+ if (this.frameJoints.has(id) || this.joints.has(id)) throw new Error(`Joint "${id}" already exists`);
39253
+ const parent = this.resolveFrameJointEndpoint(id, options.parent, "parent");
39254
+ const child = this.resolveFrameJointEndpoint(id, options.child, "child");
39255
+ if (parent.name === child.name) throw new Error(`Frame joint "${id}" cannot connect a frame to itself`);
39256
+ if (child.fixed) throw new Error(`Frame joint "${id}" cannot drive fixed frame "${child.name}"`);
39257
+ for (const joint2 of this.frameJoints.values()) {
39258
+ if (joint2.child === child.name) throw new Error(`Frame "${child.name}" already has parent joint "${joint2.name}"`);
39259
+ }
39260
+ const movingOptions = options;
39261
+ const min2 = movingOptions.min;
39262
+ const max2 = movingOptions.max;
39263
+ if (min2 !== void 0 && !Number.isFinite(min2)) throw new Error(`Frame joint "${id}" min must be finite`);
39264
+ if (max2 !== void 0 && !Number.isFinite(max2)) throw new Error(`Frame joint "${id}" max must be finite`);
39265
+ if (min2 !== void 0 && max2 !== void 0 && min2 > max2) throw new Error(`Frame joint "${id}" min must be <= max`);
39266
+ const defaultValue = type === "fixed" ? 0 : movingOptions.default ?? 0;
39267
+ if (!Number.isFinite(defaultValue)) throw new Error(`Frame joint "${id}" default must be finite`);
39268
+ this.frameJoints.set(id, {
39269
+ name: id,
39270
+ type,
39271
+ parent: parent.name,
39272
+ child: child.name,
39273
+ rest: relativeFrameTransform(parent, child),
39274
+ min: min2,
39275
+ max: max2,
39276
+ defaultValue: clampFrameJointDefault(id, defaultValue, min2, max2),
39277
+ unit: movingOptions.unit,
39278
+ control: type !== "fixed" && movingOptions.control !== false,
39279
+ metadata: options.metadata ? { ...options.metadata } : void 0
39280
+ });
39281
+ return this;
39282
+ }
39283
+ /**
39284
+ * Rigidly attach a child rig frame to a parent rig frame.
39285
+ *
39286
+ * Fixed joints carry frame hierarchy but do not expose a Motion control.
39287
+ *
39288
+ * @category Assembly
39289
+ */
39290
+ fixedJoint(name, options) {
39291
+ return this.addFrameJoint("fixed", name, options);
39292
+ }
39293
+ /**
39294
+ * Add a revolute rig-frame joint.
39295
+ *
39296
+ * The child frame rotates around the parent frame's `axis` direction. Moving
39297
+ * frame joints appear in Motion by default; pass `control: false` to keep the
39298
+ * joint solved at its default value without showing a Motion control.
39299
+ *
39300
+ * @category Assembly
39301
+ */
39302
+ revoluteJoint(name, options) {
39303
+ return this.addFrameJoint("revolute", name, options);
39304
+ }
39305
+ /**
39306
+ * Add a prismatic rig-frame joint.
39307
+ *
39308
+ * The child frame translates along the parent frame's `axis` direction. Moving
39309
+ * frame joints appear in Motion by default; pass `control: false` to keep the
39310
+ * joint solved at its default value without showing a Motion control.
39311
+ *
39312
+ * @category Assembly
39313
+ */
39314
+ prismaticJoint(name, options) {
39315
+ return this.addFrameJoint("prismatic", name, options);
39316
+ }
39317
+ /**
39318
+ * Add a visual skeleton edge between two rig frame origins.
39319
+ *
39320
+ * Frame edges follow the solved frame poses produced by `fixedJoint()`,
39321
+ * `revoluteJoint()`, and `prismaticJoint()`. They do not add constraints,
39322
+ * degrees of freedom, parts, or geometry; use them to make a frame-only rig
39323
+ * readable in the Motion/rig inspection overlay.
39324
+ *
39325
+ * @category Assembly
39326
+ */
39327
+ edgeBetweenFrames(a2, b, options = {}) {
39328
+ const frameA = typeof a2 === "string" ? a2.trim() : "";
39329
+ const frameB = typeof b === "string" ? b.trim() : "";
39330
+ if (!frameA) throw new Error("edgeBetweenFrames() first frame must be non-empty");
39331
+ if (!frameB) throw new Error("edgeBetweenFrames() second frame must be non-empty");
39332
+ if (!this.frames.has(frameA)) throw new Error(`edgeBetweenFrames() unknown frame "${frameA}"`);
39333
+ if (!this.frames.has(frameB)) throw new Error(`edgeBetweenFrames() unknown frame "${frameB}"`);
39334
+ if (frameA === frameB) throw new Error("edgeBetweenFrames() requires two different frames");
39335
+ if (options.name !== void 0 && typeof options.name !== "string") throw new Error("edgeBetweenFrames() name must be a string");
39336
+ const name = (options.name ?? `${frameA}_${frameB}_${this._frameEdgeCounter++}`).trim();
39337
+ if (!name) throw new Error("edgeBetweenFrames() name must be non-empty");
39338
+ if (this.frameEdges.has(name)) throw new Error(`Frame edge "${name}" already exists`);
39339
+ this.frameEdges.set(name, {
39340
+ name,
39341
+ a: frameA,
39342
+ b: frameB,
39343
+ metadata: options.metadata ? { ...options.metadata } : void 0
39344
+ });
39345
+ return this;
39346
+ }
39065
39347
  /**
39066
39348
  * Add a named kinematic link to the assembly graph.
39067
39349
  *
@@ -39109,6 +39391,11 @@ class Assembly {
39109
39391
  if (options.min !== void 0 && options.max !== void 0 && options.min > options.max) {
39110
39392
  throw new Error(`Link edge "${name}" min must be <= max`);
39111
39393
  }
39394
+ const isStructural = !options.visualOnly && options.length !== "free";
39395
+ if (isStructural) {
39396
+ this.assertNotDerivedStructuralLink(a2, "edgeBetweenLinks()");
39397
+ this.assertNotDerivedStructuralLink(b, "edgeBetweenLinks()");
39398
+ }
39112
39399
  let length4;
39113
39400
  if (options.visualOnly || options.length === "free") {
39114
39401
  length4 = null;
@@ -39117,6 +39404,11 @@ class Assembly {
39117
39404
  length4 = options.length;
39118
39405
  } else {
39119
39406
  length4 = vecDistance(linkA.at, linkB.at);
39407
+ if (length4 <= 1e-10) {
39408
+ throw new Error(
39409
+ `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 }.`
39410
+ );
39411
+ }
39120
39412
  }
39121
39413
  this.linkEdges.set(name, {
39122
39414
  name,
@@ -39141,13 +39433,135 @@ class Assembly {
39141
39433
  * @category Assembly
39142
39434
  */
39143
39435
  addAngleBetweenLinks(a2, b, c2, options = {}) {
39144
- var _a3, _b3;
39145
39436
  for (const id of [a2, b, c2]) {
39146
39437
  if (!this.links.has(id)) throw new Error(`addAngleBetweenLinks() unknown link "${id}"`);
39438
+ this.assertNotDerivedStructuralLink(id, "addAngleBetweenLinks()");
39147
39439
  }
39148
39440
  if (a2 === b || b === c2 || a2 === c2) throw new Error("addAngleBetweenLinks() requires three different links");
39149
39441
  const name = options.name ?? `${a2}_${b}_${c2}_${this._linkAngleCounter++}`;
39150
39442
  if (this.linkAngles.has(name)) throw new Error(`Link angle "${name}" already exists`);
39443
+ const normalized = this.normalizeAngleOptions(name, options);
39444
+ this.linkAngles.set(name, {
39445
+ name,
39446
+ a: a2,
39447
+ b,
39448
+ c: c2,
39449
+ ...normalized
39450
+ });
39451
+ return this;
39452
+ }
39453
+ /**
39454
+ * Add an absolute angle relationship from a world direction to a link segment.
39455
+ *
39456
+ * The first link is the vertex/pivot and the second link is the moving point.
39457
+ * A value of `0` places `fromLink -> toLink` along `direction` in the
39458
+ * mechanism plane; positive angles rotate counter-clockwise in that plane.
39459
+ *
39460
+ * Use `Points.polar(1, angleDeg)` when the reference direction is planar and
39461
+ * angle-based instead of axis-aligned.
39462
+ *
39463
+ * @category Assembly
39464
+ */
39465
+ addAngleBetweenLinkSegmentAndWorldDirection(fromLink, toLink, direction2, options = {}) {
39466
+ const caller = "addAngleBetweenLinkSegmentAndWorldDirection()";
39467
+ for (const id of [fromLink, toLink]) {
39468
+ if (!this.links.has(id)) throw new Error(`${caller} unknown link "${id}"`);
39469
+ this.assertNotDerivedStructuralLink(id, caller);
39470
+ }
39471
+ if (fromLink === toLink) throw new Error(`${caller} requires two different links`);
39472
+ const referenceDirection = finiteVec3(direction2, `${caller} direction`);
39473
+ if (vecLength(referenceDirection) <= 1e-10) throw new Error(`${caller} direction must be non-zero`);
39474
+ const name = options.name ?? `${fromLink}_${toLink}_from_direction_${this._linkAngleCounter++}`;
39475
+ if (this.linkAngles.has(name)) throw new Error(`Link angle "${name}" already exists`);
39476
+ this.linkAngles.set(name, {
39477
+ name,
39478
+ b: fromLink,
39479
+ c: toLink,
39480
+ reference: { kind: "worldDirection", direction: vecNormalize(referenceDirection) },
39481
+ ...this.normalizeAngleOptions(name, options)
39482
+ });
39483
+ return this;
39484
+ }
39485
+ /**
39486
+ * @deprecated Use `addAngleBetweenLinkSegmentAndWorldDirection(fromLink, toLink, [1, 0, 0], options)`.
39487
+ * @skillSuppress Compatibility-only renamed API. Use `addAngleBetweenLinkSegmentAndWorldDirection()` instead.
39488
+ */
39489
+ addAngleOfLinkSegmentFromXAxis(fromLink, toLink, options = {}) {
39490
+ throw new Error(
39491
+ "addAngleOfLinkSegmentFromXAxis() has been replaced by addAngleBetweenLinkSegmentAndWorldDirection(fromLink, toLink, [1, 0, 0], options). Update your script."
39492
+ );
39493
+ }
39494
+ /**
39495
+ * @deprecated Use `addAngleBetweenLinkSegmentAndWorldDirection(fromLink, toLink, [0, 1, 0], options)`.
39496
+ * @skillSuppress Compatibility-only renamed API. Use `addAngleBetweenLinkSegmentAndWorldDirection()` instead.
39497
+ */
39498
+ addAngleOfLinkSegmentFromYAxis(fromLink, toLink, options = {}) {
39499
+ throw new Error(
39500
+ "addAngleOfLinkSegmentFromYAxis() has been replaced by addAngleBetweenLinkSegmentAndWorldDirection(fromLink, toLink, [0, 1, 0], options). Update your script."
39501
+ );
39502
+ }
39503
+ assertNotDerivedStructuralLink(name, caller) {
39504
+ if (!this.derivedLinks.has(name)) return;
39505
+ throw new Error(
39506
+ `${caller} cannot structurally constrain derived link "${name}". Derived links are recomputed after the primary link solve.`
39507
+ );
39508
+ }
39509
+ assertCanBecomeDerivedLink(name) {
39510
+ const link = this.links.get(name);
39511
+ if (link == null ? void 0 : link.fixed) throw new Error(`Derived link "${name}" cannot be fixed`);
39512
+ for (const edge of this.linkEdges.values()) {
39513
+ if (edge.visualOnly || edge.length == null) continue;
39514
+ if (edge.a === name || edge.b === name) throw new Error(`Derived link "${name}" cannot already have structural edge "${edge.name}"`);
39515
+ }
39516
+ for (const angle of this.linkAngles.values()) {
39517
+ if (angle.a === name || angle.b === name || angle.c === name) {
39518
+ throw new Error(`Derived link "${name}" cannot already participate in angle "${angle.name}"`);
39519
+ }
39520
+ }
39521
+ }
39522
+ addDerivedLink(direction2, name, fromLink, referenceLink, distance2) {
39523
+ const id = typeof name === "string" ? name.trim() : "";
39524
+ if (!id) throw new Error(`${direction2 === "toward" ? "linkToward" : "linkAwayFrom"}() name must be non-empty`);
39525
+ if (!this.links.has(fromLink)) throw new Error(`Derived link "${id}" unknown fromLink "${fromLink}"`);
39526
+ if (!this.links.has(referenceLink)) throw new Error(`Derived link "${id}" unknown reference link "${referenceLink}"`);
39527
+ if (id === fromLink || id === referenceLink || fromLink === referenceLink) {
39528
+ throw new Error(`Derived link "${id}" requires three different links`);
39529
+ }
39530
+ if (!Number.isFinite(distance2) || distance2 < 0) {
39531
+ throw new Error(`Derived link "${id}" distance must be a finite value >= 0`);
39532
+ }
39533
+ if (!this.links.has(id)) {
39534
+ this.link(id);
39535
+ }
39536
+ this.assertCanBecomeDerivedLink(id);
39537
+ this.derivedLinks.set(id, { name: id, fromLink, referenceLink, distance: distance2, direction: direction2 });
39538
+ return this;
39539
+ }
39540
+ /**
39541
+ * Create a derived link at a fixed distance from `fromLink` toward `towardLink`.
39542
+ *
39543
+ * Derived links are trace/reference points. They are recomputed after the
39544
+ * primary link solve and cannot participate in structural edges or angle
39545
+ * constraints.
39546
+ *
39547
+ * @category Assembly
39548
+ */
39549
+ linkToward(name, fromLink, towardLink, distance2) {
39550
+ return this.addDerivedLink("toward", name, fromLink, towardLink, distance2);
39551
+ }
39552
+ /**
39553
+ * Create a derived link at a fixed distance from `fromLink` away from `awayFromLink`.
39554
+ *
39555
+ * Use this for coupler trace/extension points such as the Chebyshev lambda
39556
+ * linkage's point beyond the rocker joint.
39557
+ *
39558
+ * @category Assembly
39559
+ */
39560
+ linkAwayFrom(name, fromLink, awayFromLink, distance2) {
39561
+ return this.addDerivedLink("awayFrom", name, fromLink, awayFromLink, distance2);
39562
+ }
39563
+ normalizeAngleOptions(name, options) {
39564
+ var _a3, _b3;
39151
39565
  const control = options.control === true ? { min: options.min, max: options.max, default: options.value } : options.control ? { ...options.control } : void 0;
39152
39566
  const min2 = ((_a3 = options.limit) == null ? void 0 : _a3.min) ?? options.min;
39153
39567
  const max2 = ((_b3 = options.limit) == null ? void 0 : _b3.max) ?? options.max;
@@ -39156,18 +39570,7 @@ class Assembly {
39156
39570
  if (min2 !== void 0 && max2 !== void 0 && min2 > max2) throw new Error(`Link angle "${name}" min must be <= max`);
39157
39571
  const target = options.value ?? (control == null ? void 0 : control.default);
39158
39572
  if (target !== void 0 && !Number.isFinite(target)) throw new Error(`Link angle "${name}" value must be finite`);
39159
- this.linkAngles.set(name, {
39160
- name,
39161
- a: a2,
39162
- b,
39163
- c: c2,
39164
- target,
39165
- min: min2,
39166
- max: max2,
39167
- control,
39168
- metadata: options.metadata ? { ...options.metadata } : void 0
39169
- });
39170
- return this;
39573
+ return { target, min: min2, max: max2, control, metadata: options.metadata ? { ...options.metadata } : void 0 };
39171
39574
  }
39172
39575
  /** Return the assembly-native kinematic graph definition. */
39173
39576
  describeKinematics() {
@@ -39194,30 +39597,23 @@ class Assembly {
39194
39597
  a: angle.a,
39195
39598
  b: angle.b,
39196
39599
  c: angle.c,
39600
+ reference: cloneAngleReference(angle.reference),
39197
39601
  target: angle.target,
39198
39602
  min: angle.min,
39199
39603
  max: angle.max,
39200
39604
  control: angle.control ? { ...angle.control } : void 0,
39201
39605
  metadata: angle.metadata ? { ...angle.metadata } : void 0
39202
- }))
39606
+ })),
39607
+ derivedLinks: [...this.derivedLinks.values()].map((derived) => ({ ...derived }))
39203
39608
  };
39204
39609
  }
39205
39610
  /**
39206
- * Add a virtual reference frame (no geometry) to the assembly graph.
39207
- *
39208
- * **Details**
39209
- *
39210
- * Useful when you need a named pivot point or coordinate frame that has no
39211
- * visual geometry. Acts like a zero-volume part and can be connected to
39212
- * other parts via joints.
39213
- *
39214
- * @param name - Unique part name for the frame in the assembly graph
39215
- * @param options - Optional transform and metadata
39216
- * @returns `this` for chaining
39217
- * @category Assembly
39611
+ * @deprecated `addFrame()` has been removed. Use `frame()` for rig frames, or
39612
+ * `addPart(name, group())` for an empty placeholder part.
39613
+ * @internal
39218
39614
  */
39219
- addFrame(name, options = {}) {
39220
- return this.addPart(name, group(), options);
39615
+ addFrame(_name2, _options = {}) {
39616
+ throw new Error("addFrame() has been removed. Use frame() for rig frames, or addPart(name, group()) for an empty placeholder part.");
39221
39617
  }
39222
39618
  /**
39223
39619
  * Add a named part to the assembly.
@@ -39257,6 +39653,16 @@ class Assembly {
39257
39653
  addPart(name, part, options = {}) {
39258
39654
  if (this.parts.has(name)) throw new Error(`Part "${name}" already exists`);
39259
39655
  const mates = options.mate == null ? [] : Array.isArray(options.mate) ? options.mate : [options.mate];
39656
+ if (options.bindToFrame != null && typeof options.bindToFrame !== "string") {
39657
+ throw new Error(`addPart("${name}") bindToFrame must be a frame name string`);
39658
+ }
39659
+ const bindToFrame = typeof options.bindToFrame === "string" ? options.bindToFrame.trim() : "";
39660
+ if (bindToFrame && mates.length > 0) {
39661
+ throw new Error(`addPart("${name}") cannot use both bindToFrame and mate`);
39662
+ }
39663
+ if (bindToFrame && !this.frames.has(bindToFrame)) {
39664
+ throw new Error(`addPart("${name}") bindToFrame references unknown frame "${bindToFrame}"`);
39665
+ }
39260
39666
  this.parts.set(name, {
39261
39667
  name,
39262
39668
  part,
@@ -39274,7 +39680,8 @@ class Assembly {
39274
39680
  if (aimLink === toLink) throw new Error(`addPart("${name}") mate[${index2}] aimLink must differ from toLink`);
39275
39681
  }
39276
39682
  return aimLink ? { connector, toLink, aimLink } : { connector, toLink };
39277
- })
39683
+ }),
39684
+ bindToFrame: bindToFrame || void 0
39278
39685
  });
39279
39686
  let ports = {};
39280
39687
  if (part instanceof Shape) {
@@ -39320,10 +39727,15 @@ class Assembly {
39320
39727
  * @internal
39321
39728
  */
39322
39729
  addJoint(name, type, parent, child, options = {}) {
39323
- if (this.joints.has(name)) throw new Error(`Joint "${name}" already exists`);
39730
+ if (this.joints.has(name) || this.frameJoints.has(name)) throw new Error(`Joint "${name}" already exists`);
39324
39731
  if (!this.parts.has(parent)) throw new Error(`Unknown parent part "${parent}"`);
39325
39732
  if (!this.parts.has(child)) throw new Error(`Unknown child part "${child}"`);
39326
39733
  if (parent === child) throw new Error(`Joint "${name}" cannot connect a part to itself`);
39734
+ const parentRecord = this.parts.get(parent);
39735
+ const childRecord = this.parts.get(child);
39736
+ if (parentRecord.bindToFrame || childRecord.bindToFrame) {
39737
+ throw new Error(`Joint "${name}" cannot connect frame-bound parts. Use frame joints for parts added with bindToFrame.`);
39738
+ }
39327
39739
  if (options.frame && options.origin) {
39328
39740
  throw new Error(`Joint "${name}" cannot have both frame and origin`);
39329
39741
  }
@@ -39600,7 +40012,578 @@ class Assembly {
39600
40012
  "addGearCoupling() has been removed from the modeling API. Express gear behavior through assembly kinematic constraints instead."
39601
40013
  );
39602
40014
  }
40015
+ solveFrames(state) {
40016
+ const transforms = /* @__PURE__ */ new Map();
40017
+ const frames = /* @__PURE__ */ new Map();
40018
+ const jointValues = {};
40019
+ const warnings = [];
40020
+ if (this.frames.size === 0) return { frames, transforms, jointValues, warnings };
40021
+ const incoming = /* @__PURE__ */ new Map();
40022
+ const jointsByParent = /* @__PURE__ */ new Map();
40023
+ for (const joint2 of this.frameJoints.values()) {
40024
+ if (incoming.has(joint2.child)) {
40025
+ throw new Error(`Frame "${joint2.child}" has multiple parent joints`);
40026
+ }
40027
+ incoming.set(joint2.child, joint2.name);
40028
+ const list = jointsByParent.get(joint2.parent) ?? [];
40029
+ list.push(joint2);
40030
+ jointsByParent.set(joint2.parent, list);
40031
+ }
40032
+ const roots = [...this.frames.keys()].filter((name) => !incoming.has(name));
40033
+ if (roots.length === 0) throw new Error("Frame graph has no root frame");
40034
+ const visiting = /* @__PURE__ */ new Set();
40035
+ const visited = /* @__PURE__ */ new Set();
40036
+ const resolveJointValue = (joint2) => {
40037
+ if (joint2.type === "fixed") {
40038
+ jointValues[joint2.name] = 0;
40039
+ return 0;
40040
+ }
40041
+ const hasStateValue = Object.prototype.hasOwnProperty.call(state, joint2.name);
40042
+ let raw = joint2.defaultValue;
40043
+ if (joint2.control === false) {
40044
+ if (hasStateValue) {
40045
+ warnings.push(`Frame joint "${joint2.name}" state override ignored because control is false`);
40046
+ }
40047
+ } else {
40048
+ raw = state[joint2.name] ?? joint2.defaultValue;
40049
+ }
40050
+ const finite = Number.isFinite(raw) ? raw : joint2.defaultValue;
40051
+ let clamped = finite;
40052
+ if (joint2.min != null) clamped = Math.max(joint2.min, clamped);
40053
+ if (joint2.max != null) clamped = Math.min(joint2.max, clamped);
40054
+ if (clamped !== raw) {
40055
+ warnings.push(`Frame joint "${joint2.name}" clamped from ${raw} to ${clamped}${joint2.unit ?? ""}`);
40056
+ }
40057
+ jointValues[joint2.name] = clamped;
40058
+ return clamped;
40059
+ };
40060
+ const visit = (frameName, worldTransform) => {
40061
+ if (visiting.has(frameName)) throw new Error(`Frame joint cycle detected at "${frameName}"`);
40062
+ const frame = this.frames.get(frameName);
40063
+ if (!frame) throw new Error(`Unknown frame "${frameName}"`);
40064
+ visiting.add(frameName);
40065
+ transforms.set(frameName, worldTransform);
40066
+ frames.set(frameName, frameFromTransform(frame, worldTransform));
40067
+ visited.add(frameName);
40068
+ for (const joint2 of jointsByParent.get(frameName) ?? []) {
40069
+ const value = resolveJointValue(joint2);
40070
+ const childWorld = composeChain(joint2.rest, frameJointMotionTransform(joint2, value), worldTransform);
40071
+ visit(joint2.child, childWorld);
40072
+ }
40073
+ visiting.delete(frameName);
40074
+ };
40075
+ for (const rootName of roots) {
40076
+ const root = this.frames.get(rootName);
40077
+ visit(rootName, frameDefTransform(root));
40078
+ }
40079
+ if (visited.size !== this.frames.size) {
40080
+ const missing = [...this.frames.keys()].filter((name) => !visited.has(name));
40081
+ throw new Error(`Frame graph unresolved for frames: ${missing.join(", ")}`);
40082
+ }
40083
+ return { frames, transforms, jointValues, warnings };
40084
+ }
40085
+ solveFrameEdges(frames) {
40086
+ const solved = [];
40087
+ for (const edge of this.frameEdges.values()) {
40088
+ const a2 = frames.get(edge.a);
40089
+ const b = frames.get(edge.b);
40090
+ if (!a2) throw new Error(`Frame edge "${edge.name}" references unresolved frame "${edge.a}"`);
40091
+ if (!b) throw new Error(`Frame edge "${edge.name}" references unresolved frame "${edge.b}"`);
40092
+ const start = cloneVec3$3(a2.origin);
40093
+ const end = cloneVec3$3(b.origin);
40094
+ solved.push({
40095
+ ...cloneAssemblyFrameEdge(edge),
40096
+ start,
40097
+ end,
40098
+ solvedLength: vecDistance(start, end)
40099
+ });
40100
+ }
40101
+ return solved;
40102
+ }
40103
+ attachFrameEdgesToKinematics(metadata, frameEdges) {
40104
+ if (frameEdges.length === 0) return metadata;
40105
+ if (metadata) return { ...metadata, frameEdges };
40106
+ return {
40107
+ links: [],
40108
+ edges: [],
40109
+ angles: [],
40110
+ derivedLinks: [],
40111
+ frameEdges,
40112
+ controls: {},
40113
+ floatingComponents: [],
40114
+ diagnostics: [],
40115
+ maxResidual: 0,
40116
+ converged: true
40117
+ };
40118
+ }
39603
40119
  solveKinematicLinks(state) {
40120
+ if (this.links.size === 0) {
40121
+ return { metadata: null, linkPositions: /* @__PURE__ */ new Map(), warnings: [] };
40122
+ }
40123
+ const plane = this.buildKinematicWorkplane();
40124
+ if (!plane) {
40125
+ return this.solveKinematicLinksLegacy(state);
40126
+ }
40127
+ return this.solveKinematicLinksPlanar(state, plane);
40128
+ }
40129
+ buildKinematicWorkplane() {
40130
+ const derivedNames = new Set(this.derivedLinks.keys());
40131
+ const primaryLinks = [...this.links.values()].filter((link) => !derivedNames.has(link.name));
40132
+ const originLink = primaryLinks.find((link) => link.fixed) ?? primaryLinks[0];
40133
+ const origin = originLink ? cloneVec3$3(originLink.at) : [0, 0, 0];
40134
+ let normal = null;
40135
+ for (const angle of this.linkAngles.values()) {
40136
+ const b = this.links.get(angle.b).at;
40137
+ const c2 = this.links.get(angle.c).at;
40138
+ const reference = angle.reference ? angle.reference.direction : angle.a ? vecSub(this.links.get(angle.a).at, b) : null;
40139
+ if (!reference) continue;
40140
+ const n = vecCross(reference, vecSub(c2, b));
40141
+ if (vecLength(n) < 1e-9) continue;
40142
+ const unit = vecNormalize(n);
40143
+ if (!normal) {
40144
+ normal = unit;
40145
+ } else if (Math.abs(vecDot(normal, unit)) < 0.999) {
40146
+ return null;
40147
+ }
40148
+ }
40149
+ normal ?? (normal = [0, 0, 1]);
40150
+ const projectDir = (v) => {
40151
+ const d2 = vecDot(v, normal);
40152
+ const projected = [v[0] - normal[0] * d2, v[1] - normal[1] * d2, v[2] - normal[2] * d2];
40153
+ const len2 = vecLength(projected);
40154
+ return len2 > 1e-9 ? [projected[0] / len2, projected[1] / len2, projected[2] / len2] : null;
40155
+ };
40156
+ let xAxis = null;
40157
+ const fixed = primaryLinks.filter((link) => link.fixed);
40158
+ for (let i = 0; i < fixed.length && !xAxis; i++) {
40159
+ for (let j = i + 1; j < fixed.length && !xAxis; j++) {
40160
+ xAxis = projectDir(vecSub(fixed[j].at, fixed[i].at));
40161
+ }
40162
+ }
40163
+ for (const edge of this.linkEdges.values()) {
40164
+ if (xAxis) break;
40165
+ if (derivedNames.has(edge.a) || derivedNames.has(edge.b)) continue;
40166
+ const a2 = this.links.get(edge.a);
40167
+ const b = this.links.get(edge.b);
40168
+ if (a2 && b) xAxis = projectDir(vecSub(b.at, a2.at));
40169
+ }
40170
+ for (const link of primaryLinks) {
40171
+ if (xAxis) break;
40172
+ xAxis = projectDir(vecSub(link.at, origin));
40173
+ }
40174
+ xAxis ?? (xAxis = projectDir(Math.abs(normal[0]) < 0.9 ? [1, 0, 0] : [0, 1, 0]) ?? [1, 0, 0]);
40175
+ const yAxis = vecNormalize(vecCross(normal, xAxis));
40176
+ for (const link of primaryLinks) {
40177
+ const offPlane = Math.abs(vecDot(vecSub(link.at, origin), normal));
40178
+ if (offPlane > 1e-6) return null;
40179
+ }
40180
+ return { origin, xAxis, yAxis, normal };
40181
+ }
40182
+ solveKinematicLinksPlanar(state, plane) {
40183
+ var _a3, _b3, _c2, _d2, _e2, _f, _g;
40184
+ const derivedNames = new Set(this.derivedLinks.keys());
40185
+ const primaryLinks = [...this.links.values()].filter((link) => !derivedNames.has(link.name));
40186
+ const controls = {};
40187
+ const diagnosticSet = /* @__PURE__ */ new Set();
40188
+ const diagnostics = [];
40189
+ const addDiagnostic = (message) => {
40190
+ if (diagnosticSet.has(message)) return;
40191
+ diagnosticSet.add(message);
40192
+ diagnostics.push(message);
40193
+ };
40194
+ const project = (p2) => {
40195
+ const rel = vecSub(p2, plane.origin);
40196
+ return [vecDot(rel, plane.xAxis), vecDot(rel, plane.yAxis)];
40197
+ };
40198
+ const unproject = (p2) => [
40199
+ plane.origin[0] + plane.xAxis[0] * p2[0] + plane.yAxis[0] * p2[1],
40200
+ plane.origin[1] + plane.xAxis[1] * p2[0] + plane.yAxis[1] * p2[1],
40201
+ plane.origin[2] + plane.xAxis[2] * p2[0] + plane.yAxis[2] * p2[1]
40202
+ ];
40203
+ const unprojectVector = (v) => [
40204
+ plane.xAxis[0] * v[0] + plane.yAxis[0] * v[1],
40205
+ plane.xAxis[1] * v[0] + plane.yAxis[1] * v[1],
40206
+ plane.xAxis[2] * v[0] + plane.yAxis[2] * v[1]
40207
+ ];
40208
+ const projectVector = (v) => [vecDot(v, plane.xAxis), vecDot(v, plane.yAxis)];
40209
+ const initialPositions2d = /* @__PURE__ */ new Map();
40210
+ for (const link of primaryLinks) initialPositions2d.set(link.name, project(link.at));
40211
+ const neighbors = /* @__PURE__ */ new Map();
40212
+ for (const link of primaryLinks) neighbors.set(link.name, /* @__PURE__ */ new Set());
40213
+ for (const edge of this.linkEdges.values()) {
40214
+ if (derivedNames.has(edge.a) || derivedNames.has(edge.b)) continue;
40215
+ (_a3 = neighbors.get(edge.a)) == null ? void 0 : _a3.add(edge.b);
40216
+ (_b3 = neighbors.get(edge.b)) == null ? void 0 : _b3.add(edge.a);
40217
+ }
40218
+ for (const angle of this.linkAngles.values()) {
40219
+ if (angle.a) {
40220
+ (_c2 = neighbors.get(angle.a)) == null ? void 0 : _c2.add(angle.b);
40221
+ (_d2 = neighbors.get(angle.b)) == null ? void 0 : _d2.add(angle.a);
40222
+ }
40223
+ (_e2 = neighbors.get(angle.b)) == null ? void 0 : _e2.add(angle.c);
40224
+ (_f = neighbors.get(angle.c)) == null ? void 0 : _f.add(angle.b);
40225
+ }
40226
+ const floatingComponents = [];
40227
+ const gaugeFixed = /* @__PURE__ */ new Set();
40228
+ const seen = /* @__PURE__ */ new Set();
40229
+ for (const link of primaryLinks.map((link2) => link2.name)) {
40230
+ if (seen.has(link)) continue;
40231
+ const stack = [link];
40232
+ const component = [];
40233
+ seen.add(link);
40234
+ while (stack.length > 0) {
40235
+ const next = stack.pop();
40236
+ component.push(next);
40237
+ for (const neighbor of neighbors.get(next) ?? []) {
40238
+ if (seen.has(neighbor)) continue;
40239
+ seen.add(neighbor);
40240
+ stack.push(neighbor);
40241
+ }
40242
+ }
40243
+ if (!component.some((name) => {
40244
+ var _a4;
40245
+ return (_a4 = this.links.get(name)) == null ? void 0 : _a4.fixed;
40246
+ })) {
40247
+ const sorted = [...component].sort();
40248
+ const gaugeLink = sorted[0];
40249
+ gaugeFixed.add(gaugeLink);
40250
+ floatingComponents.push({ links: sorted, gaugeLink });
40251
+ }
40252
+ }
40253
+ const angleValueForState = (angle, inputState, recordControl) => {
40254
+ var _a4, _b4, _c3, _d3;
40255
+ if (!angle.control && angle.target === void 0) return null;
40256
+ const raw = inputState[angle.name] ?? angle.target ?? ((_a4 = angle.control) == null ? void 0 : _a4.default) ?? 0;
40257
+ let value = Number.isFinite(raw) ? raw : ((_b4 = angle.control) == null ? void 0 : _b4.default) ?? angle.target ?? 0;
40258
+ const min2 = ((_c3 = angle.control) == null ? void 0 : _c3.min) ?? angle.min;
40259
+ const max2 = ((_d3 = angle.control) == null ? void 0 : _d3.max) ?? angle.max;
40260
+ if (min2 != null) value = Math.max(min2, value);
40261
+ if (max2 != null) value = Math.min(max2, value);
40262
+ if (recordControl) controls[angle.name] = value;
40263
+ return value;
40264
+ };
40265
+ const controlledAngleValue = (angle) => angleValueForState(angle, state, true);
40266
+ const edgeLengthBetween = (x2, y2) => {
40267
+ for (const edge of this.linkEdges.values()) {
40268
+ if (edge.visualOnly || edge.length == null) continue;
40269
+ if (edge.a === x2 && edge.b === y2 || edge.a === y2 && edge.b === x2) return edge.length;
40270
+ }
40271
+ return null;
40272
+ };
40273
+ const seedDirection = (index2) => {
40274
+ const angle = index2 * Math.PI * (3 - Math.sqrt(5));
40275
+ return [Math.cos(angle), Math.sin(angle)];
40276
+ };
40277
+ const rotate2d = (v, angleDeg) => {
40278
+ const rad = angleDeg * Math.PI / 180;
40279
+ const c2 = Math.cos(rad);
40280
+ const s = Math.sin(rad);
40281
+ return [v[0] * c2 - v[1] * s, v[0] * s + v[1] * c2];
40282
+ };
40283
+ const angleReferenceVector2d = (angle, seedPositions) => {
40284
+ if (angle.reference) return projectVector(angle.reference.direction);
40285
+ if (!angle.a) return null;
40286
+ const a2 = seedPositions.get(angle.a);
40287
+ const b = seedPositions.get(angle.b);
40288
+ if (!a2 || !b) return null;
40289
+ return [a2[0] - b[0], a2[1] - b[1]];
40290
+ };
40291
+ const angleReferenceLabel = (angle) => angle.reference ? `world direction [${angle.reference.direction.join(", ")}]` : `reference links "${angle.a}" and "${angle.b}"`;
40292
+ const signedArea2d2 = (a2, b, p2) => (b[0] - a2[0]) * (p2[1] - a2[1]) - (b[1] - a2[1]) * (p2[0] - a2[0]);
40293
+ const circleCircleCandidates = (a2, radiusA, b, radiusB) => {
40294
+ const dx = b[0] - a2[0];
40295
+ const dy = b[1] - a2[1];
40296
+ const d2 = Math.hypot(dx, dy);
40297
+ if (d2 <= 1e-10) return [];
40298
+ const along = (radiusA * radiusA - radiusB * radiusB + d2 * d2) / (2 * d2);
40299
+ const h2 = radiusA * radiusA - along * along;
40300
+ if (h2 < -1e-7) return [];
40301
+ const h = Math.sqrt(Math.max(0, h2));
40302
+ const ux = dx / d2;
40303
+ const uy = dy / d2;
40304
+ const base = [a2[0] + ux * along, a2[1] + uy * along];
40305
+ const offset2 = [-uy * h, ux * h];
40306
+ return [
40307
+ [base[0] + offset2[0], base[1] + offset2[1]],
40308
+ [base[0] - offset2[0], base[1] - offset2[1]]
40309
+ ];
40310
+ };
40311
+ const chooseCandidate = (targetName, anchors, candidates, seedPositions, branchReference2) => {
40312
+ if (candidates.length === 0) return null;
40313
+ const anchorAName = anchors[0].name;
40314
+ const anchorBName = anchors[1].name;
40315
+ const anchorA = seedPositions.get(anchorAName);
40316
+ const anchorB = seedPositions.get(anchorBName);
40317
+ let referenceSign = 0;
40318
+ if (branchReference2) {
40319
+ const refA = branchReference2.get(anchorAName);
40320
+ const refB = branchReference2.get(anchorBName);
40321
+ const refTarget = branchReference2.get(targetName);
40322
+ if (refA && refB && refTarget) {
40323
+ const area2 = signedArea2d2(refA, refB, refTarget);
40324
+ if (Math.abs(area2) > 1e-7) referenceSign = Math.sign(area2);
40325
+ }
40326
+ }
40327
+ const authored = initialPositions2d.get(targetName) ?? [0, 0];
40328
+ let best = null;
40329
+ let bestScore = Number.POSITIVE_INFINITY;
40330
+ for (const candidate of candidates) {
40331
+ const candidateSign = Math.sign(signedArea2d2(anchorA, anchorB, candidate));
40332
+ const branchPenalty = referenceSign !== 0 && candidateSign !== referenceSign ? 1e12 : 0;
40333
+ const authoredDistance = Math.hypot(candidate[0] - authored[0], candidate[1] - authored[1]);
40334
+ const constraintResidual = anchors.reduce((sum2, anchor) => {
40335
+ const p2 = seedPositions.get(anchor.name);
40336
+ return sum2 + Math.abs(Math.hypot(candidate[0] - p2[0], candidate[1] - p2[1]) - anchor.length);
40337
+ }, 0);
40338
+ const score = branchPenalty + constraintResidual * 1e6 + authoredDistance;
40339
+ if (score < bestScore) {
40340
+ bestScore = score;
40341
+ best = candidate;
40342
+ }
40343
+ }
40344
+ return best;
40345
+ };
40346
+ const buildSeedPositions = (inputState, branchReference2, diagnosticPrefix = "") => {
40347
+ const seedPositions = new Map(initialPositions2d);
40348
+ const seeded = /* @__PURE__ */ new Set();
40349
+ const angleDriven = /* @__PURE__ */ new Set();
40350
+ const isLocked = (name) => {
40351
+ var _a4;
40352
+ return ((_a4 = this.links.get(name)) == null ? void 0 : _a4.fixed) === true || gaugeFixed.has(name);
40353
+ };
40354
+ for (const link of primaryLinks) {
40355
+ const p2 = seedPositions.get(link.name);
40356
+ if (link.fixed || gaugeFixed.has(link.name) || Math.hypot(p2[0], p2[1]) > 1e-9) seeded.add(link.name);
40357
+ }
40358
+ if (seeded.size === 0 && primaryLinks.length > 0) seeded.add(primaryLinks[0].name);
40359
+ let seedIndex = 1;
40360
+ let changed = true;
40361
+ for (let pass = 0; pass < primaryLinks.length * 3 && changed; pass++) {
40362
+ changed = false;
40363
+ for (const angle of this.linkAngles.values()) {
40364
+ const target = angleValueForState(angle, inputState, false);
40365
+ const radius = edgeLengthBetween(angle.b, angle.c);
40366
+ const hasReference = !!angle.reference || angle.a !== void 0 && seeded.has(angle.a);
40367
+ if (target == null || radius == null || isLocked(angle.c) || !hasReference || !seeded.has(angle.b)) continue;
40368
+ const b = seedPositions.get(angle.b);
40369
+ const ref = angleReferenceVector2d(angle, seedPositions);
40370
+ if (!ref) continue;
40371
+ const refLen = Math.hypot(ref[0], ref[1]);
40372
+ if (refLen <= 1e-10) {
40373
+ addDiagnostic(
40374
+ `${diagnosticPrefix}Angle "${angle.name}" cannot drive link "${angle.c}" because ${angleReferenceLabel(angle)} has no usable direction in the mechanism plane.`
40375
+ );
40376
+ continue;
40377
+ }
40378
+ const dir = rotate2d([ref[0] / refLen, ref[1] / refLen], target);
40379
+ seedPositions.set(angle.c, [b[0] + dir[0] * radius, b[1] + dir[1] * radius]);
40380
+ seeded.add(angle.c);
40381
+ angleDriven.add(angle.c);
40382
+ changed = true;
40383
+ }
40384
+ for (const link of primaryLinks) {
40385
+ if (isLocked(link.name) || angleDriven.has(link.name)) continue;
40386
+ const anchors = [];
40387
+ for (const edge of this.linkEdges.values()) {
40388
+ if (edge.visualOnly || edge.length == null || derivedNames.has(edge.a) || derivedNames.has(edge.b)) continue;
40389
+ if (edge.a === link.name && seeded.has(edge.b)) anchors.push({ name: edge.b, length: edge.length });
40390
+ if (edge.b === link.name && seeded.has(edge.a)) anchors.push({ name: edge.a, length: edge.length });
40391
+ }
40392
+ if (anchors.length < 2) continue;
40393
+ const a2 = anchors[0];
40394
+ const b = anchors[1];
40395
+ const candidates = circleCircleCandidates(seedPositions.get(a2.name), a2.length, seedPositions.get(b.name), b.length);
40396
+ const chosen = chooseCandidate(link.name, anchors, candidates, seedPositions, branchReference2);
40397
+ if (!chosen) {
40398
+ const pa = seedPositions.get(a2.name);
40399
+ const pb = seedPositions.get(b.name);
40400
+ const centerDistance = Math.hypot(pb[0] - pa[0], pb[1] - pa[1]);
40401
+ const minDistance = Math.abs(a2.length - b.length);
40402
+ const maxDistance = a2.length + b.length;
40403
+ addDiagnostic(
40404
+ `${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.`
40405
+ );
40406
+ continue;
40407
+ }
40408
+ seedPositions.set(link.name, chosen);
40409
+ seeded.add(link.name);
40410
+ changed = true;
40411
+ }
40412
+ for (const edge of this.linkEdges.values()) {
40413
+ if (edge.visualOnly || edge.length == null || derivedNames.has(edge.a) || derivedNames.has(edge.b)) continue;
40414
+ const aSeeded = seeded.has(edge.a);
40415
+ const bSeeded = seeded.has(edge.b);
40416
+ if (aSeeded === bSeeded) continue;
40417
+ const anchorName = aSeeded ? edge.a : edge.b;
40418
+ const targetName = aSeeded ? edge.b : edge.a;
40419
+ if (isLocked(targetName) || angleDriven.has(targetName)) continue;
40420
+ const anchor = seedPositions.get(anchorName);
40421
+ const current = seedPositions.get(targetName);
40422
+ const delta = [current[0] - anchor[0], current[1] - anchor[1]];
40423
+ const deltaLen = Math.hypot(delta[0], delta[1]);
40424
+ const dir = deltaLen > 1e-9 ? [delta[0] / deltaLen, delta[1] / deltaLen] : seedDirection(seedIndex++);
40425
+ seedPositions.set(targetName, [anchor[0] + dir[0] * edge.length, anchor[1] + dir[1] * edge.length]);
40426
+ seeded.add(targetName);
40427
+ changed = true;
40428
+ }
40429
+ }
40430
+ const refLength = Math.max(1, ...[...this.linkEdges.values()].map((edge) => edge.length ?? 0));
40431
+ for (const link of primaryLinks) {
40432
+ if (seeded.has(link.name)) continue;
40433
+ if (this.linkEdges.size > 0 || this.linkAngles.size > 0) {
40434
+ addDiagnostic(
40435
+ `${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.`
40436
+ );
40437
+ } else {
40438
+ const dir = seedDirection(seedIndex++);
40439
+ seedPositions.set(link.name, [dir[0] * refLength, dir[1] * refLength]);
40440
+ seeded.add(link.name);
40441
+ }
40442
+ }
40443
+ return seedPositions;
40444
+ };
40445
+ const defaultControlState = {};
40446
+ let hasDefaultControlState = false;
40447
+ for (const angle of this.linkAngles.values()) {
40448
+ if (!angle.control && angle.target === void 0) continue;
40449
+ const value = ((_g = angle.control) == null ? void 0 : _g.default) ?? angle.target;
40450
+ if (Number.isFinite(value)) {
40451
+ defaultControlState[angle.name] = value;
40452
+ hasDefaultControlState = true;
40453
+ }
40454
+ }
40455
+ const branchReference = hasDefaultControlState ? buildSeedPositions(defaultControlState, void 0, "Default assembly mode: ") : void 0;
40456
+ const positions2d = buildSeedPositions(state, branchReference);
40457
+ const positions = /* @__PURE__ */ new Map();
40458
+ for (const link of primaryLinks) {
40459
+ const point2 = positions2d.get(link.name);
40460
+ if (!point2) {
40461
+ addDiagnostic(`Link "${link.name}" has no solved planar position after kinematic construction.`);
40462
+ continue;
40463
+ }
40464
+ positions.set(link.name, unproject(point2));
40465
+ }
40466
+ const solvedDerivedLinks = this.evaluateDerivedLinks(positions);
40467
+ for (const derived of solvedDerivedLinks) positions.set(derived.name, cloneVec3$3(derived.position));
40468
+ const solvedEdges = [];
40469
+ const solvedAngles = [];
40470
+ let maxResidual = diagnostics.length > 0 ? Number.POSITIVE_INFINITY : 0;
40471
+ for (const edge of this.linkEdges.values()) {
40472
+ const edgeA = positions.get(edge.a);
40473
+ const edgeB = positions.get(edge.b);
40474
+ const solvedLength = edgeA && edgeB ? vecDistance(edgeA, edgeB) : Number.NaN;
40475
+ let residual = 0;
40476
+ if (!edgeA || !edgeB) {
40477
+ residual = Number.POSITIVE_INFINITY;
40478
+ addDiagnostic(`Edge "${edge.name}" could not be evaluated because one of its links has no solved position.`);
40479
+ } else {
40480
+ if (edge.length != null) residual = solvedLength - edge.length;
40481
+ if (edge.min != null && solvedLength < edge.min) residual = Math.min(residual, solvedLength - edge.min);
40482
+ if (edge.max != null && solvedLength > edge.max) residual = Math.max(residual, solvedLength - edge.max);
40483
+ }
40484
+ maxResidual = Math.max(maxResidual, Math.abs(residual));
40485
+ solvedEdges.push({
40486
+ name: edge.name,
40487
+ a: edge.a,
40488
+ b: edge.b,
40489
+ length: edge.length,
40490
+ solvedLength,
40491
+ residual,
40492
+ visualOnly: edge.visualOnly
40493
+ });
40494
+ }
40495
+ for (const angle of this.linkAngles.values()) {
40496
+ const angleB = positions.get(angle.b);
40497
+ const angleC = positions.get(angle.c);
40498
+ const reference = angleReferenceVector2d(angle, positions2d);
40499
+ const solvedValue = reference && angleB && angleC ? signedAngleBetweenVectorsAboutAxis(unprojectVector(reference), vecSub(angleC, angleB), plane.normal) : Number.NaN;
40500
+ const target = controlledAngleValue(angle) ?? angle.target;
40501
+ let residual = target == null ? 0 : normalizeAngleDeltaDeg(solvedValue - target);
40502
+ if (!reference || !angleB || !angleC) {
40503
+ residual = Number.POSITIVE_INFINITY;
40504
+ addDiagnostic(`Angle "${angle.name}" could not be evaluated because its reference or one of its links has no solved position.`);
40505
+ } else {
40506
+ if (angle.min != null && solvedValue < angle.min) residual = Math.min(residual, solvedValue - angle.min);
40507
+ if (angle.max != null && solvedValue > angle.max) residual = Math.max(residual, solvedValue - angle.max);
40508
+ }
40509
+ maxResidual = Math.max(maxResidual, Math.abs(residual));
40510
+ solvedAngles.push({
40511
+ name: angle.name,
40512
+ a: angle.a,
40513
+ b: angle.b,
40514
+ c: angle.c,
40515
+ reference: cloneAngleReference(angle.reference),
40516
+ target,
40517
+ solvedValue,
40518
+ residual,
40519
+ control: angle.control ? { ...angle.control } : void 0
40520
+ });
40521
+ }
40522
+ const warnings = [];
40523
+ for (const component of floatingComponents) {
40524
+ warnings.push(`Kinematic component is floating; using link "${component.gaugeLink}" as a numerical display gauge`);
40525
+ }
40526
+ if (maxResidual > 1e-3) {
40527
+ warnings.push(`Kinematic graph residual ${maxResidual.toFixed(6)} exceeds tolerance`);
40528
+ }
40529
+ return {
40530
+ metadata: {
40531
+ links: [...this.links.values()].map((link) => ({
40532
+ name: link.name,
40533
+ position: cloneVec3$3(positions.get(link.name) ?? link.at),
40534
+ fixed: link.fixed,
40535
+ gaugeFixed: gaugeFixed.has(link.name) || void 0
40536
+ })),
40537
+ edges: solvedEdges,
40538
+ angles: solvedAngles,
40539
+ derivedLinks: solvedDerivedLinks,
40540
+ controls,
40541
+ floatingComponents,
40542
+ diagnostics,
40543
+ maxResidual,
40544
+ converged: diagnostics.length === 0 && maxResidual <= 1e-3
40545
+ },
40546
+ linkPositions: positions,
40547
+ warnings
40548
+ };
40549
+ }
40550
+ evaluateDerivedLinks(basePositions) {
40551
+ const output = /* @__PURE__ */ new Map();
40552
+ const visiting = /* @__PURE__ */ new Set();
40553
+ const resolve = (name) => {
40554
+ const existing = basePositions.get(name);
40555
+ if (existing && !this.derivedLinks.has(name)) return existing;
40556
+ const cached = output.get(name);
40557
+ if (cached) return cached.position;
40558
+ const derived = this.derivedLinks.get(name);
40559
+ if (!derived) {
40560
+ const position2 = basePositions.get(name);
40561
+ if (!position2) throw new Error(`Derived link references unknown solved link "${name}"`);
40562
+ return position2;
40563
+ }
40564
+ if (visiting.has(name)) throw new Error(`Derived link cycle detected at "${name}"`);
40565
+ visiting.add(name);
40566
+ const from = resolve(derived.fromLink);
40567
+ const reference = resolve(derived.referenceLink);
40568
+ const rawDir = derived.direction === "toward" ? vecSub(reference, from) : vecSub(from, reference);
40569
+ const dir = vecNormalize(rawDir);
40570
+ if (vecLength(dir) < 1e-10) {
40571
+ throw new Error(`Derived link "${name}" cannot be solved because "${derived.fromLink}" and "${derived.referenceLink}" coincide`);
40572
+ }
40573
+ const position = [
40574
+ from[0] + dir[0] * derived.distance,
40575
+ from[1] + dir[1] * derived.distance,
40576
+ from[2] + dir[2] * derived.distance
40577
+ ];
40578
+ visiting.delete(name);
40579
+ const solved = { ...derived, position };
40580
+ output.set(name, solved);
40581
+ return position;
40582
+ };
40583
+ for (const name of this.derivedLinks.keys()) resolve(name);
40584
+ return [...output.values()];
40585
+ }
40586
+ solveKinematicLinksLegacy(state) {
39604
40587
  var _a3, _b3, _c2, _d2, _e2, _f;
39605
40588
  if (this.links.size === 0) {
39606
40589
  return { metadata: null, linkPositions: /* @__PURE__ */ new Map(), warnings: [] };
@@ -39616,8 +40599,10 @@ class Assembly {
39616
40599
  (_b3 = neighbors.get(edge.b)) == null ? void 0 : _b3.add(edge.a);
39617
40600
  }
39618
40601
  for (const angle of this.linkAngles.values()) {
39619
- (_c2 = neighbors.get(angle.a)) == null ? void 0 : _c2.add(angle.b);
39620
- (_d2 = neighbors.get(angle.b)) == null ? void 0 : _d2.add(angle.a);
40602
+ if (angle.a) {
40603
+ (_c2 = neighbors.get(angle.a)) == null ? void 0 : _c2.add(angle.b);
40604
+ (_d2 = neighbors.get(angle.b)) == null ? void 0 : _d2.add(angle.a);
40605
+ }
39621
40606
  (_e2 = neighbors.get(angle.b)) == null ? void 0 : _e2.add(angle.c);
39622
40607
  (_f = neighbors.get(angle.c)) == null ? void 0 : _f.add(angle.b);
39623
40608
  }
@@ -39672,27 +40657,36 @@ class Assembly {
39672
40657
  const link = this.links.get(name);
39673
40658
  return !link.fixed && !gaugeFixed.has(name) && !controlledLinks.has(name);
39674
40659
  };
40660
+ const angleReferenceVector = (angle, source) => {
40661
+ if (angle.reference) return cloneVec3$3(angle.reference.direction);
40662
+ if (!angle.a) return null;
40663
+ const a2 = source.get(angle.a);
40664
+ const b = source.get(angle.b);
40665
+ if (!a2 || !b) return null;
40666
+ return vecSub(a2, b);
40667
+ };
39675
40668
  const anglePlaneNormal = (angle) => {
39676
- const a2 = this.links.get(angle.a).at;
39677
40669
  const b = this.links.get(angle.b).at;
39678
40670
  const c2 = this.links.get(angle.c).at;
39679
- const n = vecCross(vecSub(a2, b), vecSub(c2, b));
40671
+ const reference = angle.reference ? angle.reference.direction : angle.a ? vecSub(this.links.get(angle.a).at, b) : null;
40672
+ if (!reference) return [0, 0, 1];
40673
+ const n = vecCross(reference, vecSub(c2, b));
39680
40674
  return vecLength(n) < 1e-9 ? [0, 0, 1] : vecNormalize(n);
39681
40675
  };
39682
40676
  const applyAngleControls = () => {
39683
40677
  for (const angle of this.linkAngles.values()) {
39684
40678
  const target = controlledAngleValue(angle);
39685
40679
  if (target == null) continue;
39686
- const a2 = positions.get(angle.a);
39687
40680
  const b = positions.get(angle.b);
39688
40681
  const c2 = positions.get(angle.c);
39689
40682
  if (!isMovable(angle.c) && !controlledLinks.has(angle.c)) continue;
39690
40683
  const radius = edgeLengthBetween(angle.b, angle.c) ?? vecDistance(b, c2);
39691
40684
  if (radius <= 1e-10) continue;
39692
- const ba = vecSub(a2, b);
39693
- const len2 = vecLength(ba);
40685
+ const reference = angleReferenceVector(angle, positions);
40686
+ if (!reference) continue;
40687
+ const len2 = vecLength(reference);
39694
40688
  if (len2 <= 1e-10) continue;
39695
- const baUnit = [ba[0] / len2, ba[1] / len2, ba[2] / len2];
40689
+ const baUnit = [reference[0] / len2, reference[1] / len2, reference[2] / len2];
39696
40690
  const rotated = rotateAboutAxis(baUnit, anglePlaneNormal(angle), target);
39697
40691
  positions.set(angle.c, [b[0] + rotated[0] * radius, b[1] + rotated[1] * radius, b[2] + rotated[2] * radius]);
39698
40692
  controlledLinks.add(angle.c);
@@ -39722,6 +40716,8 @@ class Assembly {
39722
40716
  }
39723
40717
  controlledLinks.clear();
39724
40718
  applyAngleControls();
40719
+ const solvedDerivedLinks = this.evaluateDerivedLinks(positions);
40720
+ for (const derived of solvedDerivedLinks) positions.set(derived.name, cloneVec3$3(derived.position));
39725
40721
  const solvedEdges = [];
39726
40722
  const solvedAngles = [];
39727
40723
  let maxResidual = 0;
@@ -39743,14 +40739,13 @@ class Assembly {
39743
40739
  });
39744
40740
  }
39745
40741
  for (const angle of this.linkAngles.values()) {
39746
- const solvedValue = signedAngleAboutAxis(
39747
- positions.get(angle.a),
39748
- positions.get(angle.b),
39749
- positions.get(angle.c),
39750
- anglePlaneNormal(angle)
39751
- );
40742
+ const reference = angleReferenceVector(angle, positions);
40743
+ const b = positions.get(angle.b);
40744
+ const c2 = positions.get(angle.c);
40745
+ const solvedValue = reference ? signedAngleBetweenVectorsAboutAxis(reference, vecSub(c2, b), anglePlaneNormal(angle)) : Number.NaN;
39752
40746
  const target = controlledAngleValue(angle) ?? angle.target;
39753
40747
  let residual = target == null ? 0 : normalizeAngleDeltaDeg(solvedValue - target);
40748
+ if (!reference) residual = Number.POSITIVE_INFINITY;
39754
40749
  if (angle.min != null && solvedValue < angle.min) residual = Math.min(residual, solvedValue - angle.min);
39755
40750
  if (angle.max != null && solvedValue > angle.max) residual = Math.max(residual, solvedValue - angle.max);
39756
40751
  maxResidual = Math.max(maxResidual, Math.abs(residual));
@@ -39759,6 +40754,7 @@ class Assembly {
39759
40754
  a: angle.a,
39760
40755
  b: angle.b,
39761
40756
  c: angle.c,
40757
+ reference: cloneAngleReference(angle.reference),
39762
40758
  target,
39763
40759
  solvedValue,
39764
40760
  residual,
@@ -39782,8 +40778,10 @@ class Assembly {
39782
40778
  })),
39783
40779
  edges: solvedEdges,
39784
40780
  angles: solvedAngles,
40781
+ derivedLinks: solvedDerivedLinks,
39785
40782
  controls,
39786
40783
  floatingComponents,
40784
+ diagnostics: [],
39787
40785
  maxResidual,
39788
40786
  converged: maxResidual <= 1e-3
39789
40787
  },
@@ -39831,8 +40829,12 @@ class Assembly {
39831
40829
  const incoming = /* @__PURE__ */ new Map();
39832
40830
  const jointsByParent = /* @__PURE__ */ new Map();
39833
40831
  const warnings = [];
40832
+ const frameSolve = this.solveFrames(state);
40833
+ warnings.push(...frameSolve.warnings);
39834
40834
  const kinematicSolve = this.solveKinematicLinks(state);
39835
40835
  warnings.push(...kinematicSolve.warnings);
40836
+ const solvedFrameEdges = this.solveFrameEdges(frameSolve.frames);
40837
+ const kinematicMetadata = this.attachFrameEdgesToKinematics(kinematicSolve.metadata, solvedFrameEdges);
39836
40838
  for (const joint2 of this.joints.values()) {
39837
40839
  if (incoming.has(joint2.child)) {
39838
40840
  throw new Error(`Part "${joint2.child}" has multiple parent joints`);
@@ -39842,8 +40844,9 @@ class Assembly {
39842
40844
  list.push(joint2);
39843
40845
  jointsByParent.set(joint2.parent, list);
39844
40846
  }
39845
- const roots = [...this.parts.keys()].filter((name) => !incoming.has(name));
39846
- if (roots.length === 0 && this.parts.size > 0) {
40847
+ const unboundPartNames = [...this.parts.values()].filter((part) => !part.bindToFrame).map((part) => part.name);
40848
+ const roots = unboundPartNames.filter((name) => !incoming.has(name));
40849
+ if (roots.length === 0 && unboundPartNames.length > 0) {
39847
40850
  throw new Error("Assembly has no root part (cyclic joint graph)");
39848
40851
  }
39849
40852
  const mateBaseOverrides = /* @__PURE__ */ new Map();
@@ -39946,7 +40949,7 @@ class Assembly {
39946
40949
  const world = /* @__PURE__ */ new Map();
39947
40950
  const visiting = /* @__PURE__ */ new Set();
39948
40951
  const visited = /* @__PURE__ */ new Set();
39949
- const jointValues = {};
40952
+ const jointValues = { ...frameSolve.jointValues };
39950
40953
  const resolvingJointValues = /* @__PURE__ */ new Set();
39951
40954
  const resolveJointValue = (jointName) => {
39952
40955
  const cached = jointValues[jointName];
@@ -39996,6 +40999,13 @@ class Assembly {
39996
40999
  const rootBase = mateBaseOverrides.get(rootName) ?? kinematicBaseForPart(root) ?? root.base;
39997
41000
  dfs(rootName, rootBase);
39998
41001
  }
41002
+ for (const rec of this.parts.values()) {
41003
+ if (!rec.bindToFrame) continue;
41004
+ const frameTransform2 = frameSolve.transforms.get(rec.bindToFrame);
41005
+ if (!frameTransform2) throw new Error(`Part "${rec.name}" references unresolved frame "${rec.bindToFrame}"`);
41006
+ world.set(rec.name, composeChain(rec.base, frameTransform2));
41007
+ visited.add(rec.name);
41008
+ }
39999
41009
  if (visited.size !== this.parts.size) {
40000
41010
  const missing = [...this.parts.keys()].filter((name) => !visited.has(name));
40001
41011
  throw new Error(`Assembly graph unresolved for parts: ${missing.join(", ")}`);
@@ -40051,7 +41061,8 @@ class Assembly {
40051
41061
  jointValues,
40052
41062
  warnings,
40053
41063
  mateMetadata,
40054
- kinematicSolve.metadata,
41064
+ kinematicMetadata,
41065
+ frameSolve.frames,
40055
41066
  this._usedPortRefs
40056
41067
  );
40057
41068
  }
@@ -40164,7 +41175,7 @@ class Assembly {
40164
41175
  return collectJointsView(this.buildJointsViewOptions(options, state, false), null, "assembly");
40165
41176
  }
40166
41177
  buildJointsViewOptions(options, state, relativeLinkControls) {
40167
- var _a3, _b3, _c2, _d2;
41178
+ var _a3, _b3, _c2, _d2, _e2, _f;
40168
41179
  const solved = this.solve(state);
40169
41180
  const def = this.describe();
40170
41181
  const joints = [];
@@ -40208,7 +41219,31 @@ class Assembly {
40208
41219
  }
40209
41220
  joints.push(entry);
40210
41221
  }
40211
- if (this.linkAngles.size > 0 && this.parts.size > 0) {
41222
+ for (const j of def.frameJoints) {
41223
+ if (j.type === "fixed" || j.control === false) continue;
41224
+ const parentWorld = solved.getFrame(j.parent);
41225
+ const pivot = parentWorld.point([0, 0, 0]);
41226
+ const axisWorld = parentWorld.vector([0, 0, 1]);
41227
+ const axisLen = Math.hypot(axisWorld[0], axisWorld[1], axisWorld[2]);
41228
+ const normalizedAxis = axisLen > 1e-10 ? [axisWorld[0] / axisLen, axisWorld[1] / axisLen, axisWorld[2] / axisLen] : [0, 0, 1];
41229
+ const entry = {
41230
+ name: j.name,
41231
+ child: j.child,
41232
+ parent: j.parent,
41233
+ type: j.type,
41234
+ axis: normalizedAxis,
41235
+ pivot,
41236
+ min: j.min,
41237
+ max: j.max,
41238
+ default: ((_c2 = options.defaults) == null ? void 0 : _c2[j.name]) ?? j.defaultValue,
41239
+ unit: j.unit
41240
+ };
41241
+ if ((_d2 = options.overrides) == null ? void 0 : _d2[j.name]) {
41242
+ Object.assign(entry, options.overrides[j.name]);
41243
+ }
41244
+ joints.push(entry);
41245
+ }
41246
+ if (this.linkAngles.size > 0) {
40212
41247
  const mateLinksOf = /* @__PURE__ */ new Map();
40213
41248
  for (const part of this.parts.values()) {
40214
41249
  if (part.mates.length > 0) mateLinksOf.set(part.name, new Set(part.mates.map((m2) => m2.toLink)));
@@ -40229,18 +41264,20 @@ class Assembly {
40229
41264
  for (const angle of this.linkAngles.values()) {
40230
41265
  if (angle.control) controlMoving.set(angle.c, angle);
40231
41266
  }
41267
+ const controlledAngleChild = (angle) => spanPart(angle.b, angle.c) ?? firstPartMatedTo(angle.c) ?? angle.c;
40232
41268
  const normalAtSolvedPose = (angle) => {
40233
- const a2 = solved.getLinkPosition(angle.a);
40234
41269
  const b = solved.getLinkPosition(angle.b);
40235
41270
  const c2 = solved.getLinkPosition(angle.c);
40236
- const n = vecCross(vecSub(a2, b), vecSub(c2, b));
41271
+ const reference = angle.reference ? angle.reference.direction : angle.a ? vecSub(solved.getLinkPosition(angle.a), b) : null;
41272
+ if (!reference) return [0, 0, 1];
41273
+ const n = vecCross(reference, vecSub(c2, b));
40237
41274
  return vecLength(n) < 1e-9 ? [0, 0, 1] : vecNormalize(n);
40238
41275
  };
40239
41276
  for (const angle of this.linkAngles.values()) {
40240
41277
  if (!angle.control) continue;
40241
- const child = spanPart(angle.b, angle.c) ?? firstPartMatedTo(angle.c) ?? angle.c;
41278
+ const child = controlledAngleChild(angle);
40242
41279
  const parentAngle = controlMoving.get(angle.b);
40243
- const parent = parentAngle ? spanPart(parentAngle.b, parentAngle.c) ?? firstPartMatedTo(parentAngle.c) : firstPartMatedTo(angle.b);
41280
+ const parent = parentAngle ? controlledAngleChild(parentAngle) : firstPartMatedTo(angle.b);
40244
41281
  const defValue = angle.control.default ?? angle.target ?? 0;
40245
41282
  const absMin = angle.control.min ?? angle.min;
40246
41283
  const absMax = angle.control.max ?? angle.max;
@@ -40253,10 +41290,10 @@ class Assembly {
40253
41290
  pivot: solved.getLinkPosition(angle.b),
40254
41291
  min: absMin !== void 0 && relativeLinkControls ? absMin - defValue : absMin,
40255
41292
  max: absMax !== void 0 && relativeLinkControls ? absMax - defValue : absMax,
40256
- default: relativeLinkControls ? 0 : ((_c2 = options.defaults) == null ? void 0 : _c2[angle.name]) ?? defValue,
41293
+ default: relativeLinkControls ? 0 : ((_e2 = options.defaults) == null ? void 0 : _e2[angle.name]) ?? defValue,
40257
41294
  unit: angle.control.unit
40258
41295
  };
40259
- if ((_d2 = options.overrides) == null ? void 0 : _d2[angle.name]) {
41296
+ if ((_f = options.overrides) == null ? void 0 : _f[angle.name]) {
40260
41297
  Object.assign(entry, options.overrides[angle.name]);
40261
41298
  }
40262
41299
  joints.push(entry);
@@ -40318,7 +41355,8 @@ class Assembly {
40318
41355
  part: part.part,
40319
41356
  base: part.base,
40320
41357
  metadata: part.metadata ? { ...part.metadata } : void 0,
40321
- mates: part.mates.map((mate) => ({ ...mate }))
41358
+ mates: part.mates.map((mate) => ({ ...mate })),
41359
+ bindToFrame: part.bindToFrame
40322
41360
  })),
40323
41361
  joints: [...this.joints.values()].map((joint2) => ({
40324
41362
  name: joint2.name,
@@ -40342,7 +41380,22 @@ class Assembly {
40342
41380
  terms: coupling.terms.map((term) => ({ joint: term.joint, ratio: term.ratio })),
40343
41381
  offset: coupling.offset
40344
41382
  })),
40345
- kinematics: this.describeKinematics()
41383
+ kinematics: this.describeKinematics(),
41384
+ frames: [...this.frames.values()].map((frame) => cloneAssemblyFrame(frame)),
41385
+ frameJoints: [...this.frameJoints.values()].map((joint2) => ({
41386
+ name: joint2.name,
41387
+ type: joint2.type,
41388
+ parent: joint2.parent,
41389
+ child: joint2.child,
41390
+ rest: joint2.rest,
41391
+ min: joint2.min,
41392
+ max: joint2.max,
41393
+ defaultValue: joint2.defaultValue,
41394
+ unit: joint2.unit,
41395
+ control: joint2.control,
41396
+ metadata: joint2.metadata ? { ...joint2.metadata } : void 0
41397
+ })),
41398
+ frameEdges: [...this.frameEdges.values()].map((edge) => cloneAssemblyFrameEdge(edge))
40346
41399
  };
40347
41400
  }
40348
41401
  }
@@ -40569,6 +41622,9 @@ class ImportedAssembly {
40569
41622
  mergeInto(parent, options) {
40570
41623
  const def = this._assembly.describe();
40571
41624
  const pfx = options.prefix ? `${options.prefix}.` : "";
41625
+ if (def.frames.length > 0 || def.frameJoints.length > 0 || def.frameEdges.length > 0) {
41626
+ throw new Error("mergeInto() does not yet support rig frames. Import and solve the frame-based sub-assembly directly for now.");
41627
+ }
40572
41628
  const childSet = new Set(def.joints.map((j) => j.child));
40573
41629
  const roots = def.parts.filter((p2) => !childSet.has(p2.name));
40574
41630
  if (roots.length === 0) {
@@ -40599,14 +41655,26 @@ class ImportedAssembly {
40599
41655
  });
40600
41656
  }
40601
41657
  for (const angle of def.kinematics.angles) {
40602
- parent.addAngleBetweenLinks(`${pfx}${angle.a}`, `${pfx}${angle.b}`, `${pfx}${angle.c}`, {
41658
+ const options2 = {
40603
41659
  name: `${pfx}${angle.name}`,
40604
41660
  value: angle.target,
40605
41661
  min: angle.min,
40606
41662
  max: angle.max,
40607
41663
  control: angle.control,
40608
41664
  metadata: angle.metadata
40609
- });
41665
+ };
41666
+ if (angle.reference) {
41667
+ parent.addAngleBetweenLinkSegmentAndWorldDirection(`${pfx}${angle.b}`, `${pfx}${angle.c}`, angle.reference.direction, options2);
41668
+ } else if (angle.a) {
41669
+ parent.addAngleBetweenLinks(`${pfx}${angle.a}`, `${pfx}${angle.b}`, `${pfx}${angle.c}`, options2);
41670
+ }
41671
+ }
41672
+ for (const derived of def.kinematics.derivedLinks) {
41673
+ if (derived.direction === "toward") {
41674
+ parent.linkToward(`${pfx}${derived.name}`, `${pfx}${derived.fromLink}`, `${pfx}${derived.referenceLink}`, derived.distance);
41675
+ } else {
41676
+ parent.linkAwayFrom(`${pfx}${derived.name}`, `${pfx}${derived.fromLink}`, `${pfx}${derived.referenceLink}`, derived.distance);
41677
+ }
40610
41678
  }
40611
41679
  for (const p2 of def.parts) {
40612
41680
  parent.addPart(`${pfx}${p2.name}`, p2.part, {
@@ -45122,7 +46190,7 @@ const EPSILON$2 = 1e-9;
45122
46190
  function add$3(a2, b) {
45123
46191
  return [a2[0] + b[0], a2[1] + b[1], a2[2] + b[2]];
45124
46192
  }
45125
- function scale$3(v, factor) {
46193
+ function scale$4(v, factor) {
45126
46194
  return [v[0] * factor, v[1] * factor, v[2] * factor];
45127
46195
  }
45128
46196
  function sub$5(a2, b) {
@@ -45197,9 +46265,9 @@ function buildBend(points, directions, cornerIndex, radius) {
45197
46265
  const angle = Math.acos(turnDot);
45198
46266
  const trim = radius * Math.tan(angle / 2);
45199
46267
  const axis = normalize$4(cross$4(dIn, dOut), `corner ${cornerIndex} bend axis`);
45200
- const start = add$3(points[cornerIndex], scale$3(dIn, -trim));
45201
- const end = add$3(points[cornerIndex], scale$3(dOut, trim));
45202
- const center = add$3(start, scale$3(normalize$4(cross$4(axis, dIn), `corner ${cornerIndex} bend normal`), radius));
46268
+ const start = add$3(points[cornerIndex], scale$4(dIn, -trim));
46269
+ const end = add$3(points[cornerIndex], scale$4(dOut, trim));
46270
+ const center = add$3(start, scale$4(normalize$4(cross$4(axis, dIn), `corner ${cornerIndex} bend normal`), radius));
45203
46271
  return { cornerIndex, trim, start, end, center, axis, sweepDeg: angle * 180 / Math.PI, length: radius * angle };
45204
46272
  }
45205
46273
  function assertBendFits(bend2, radius, segmentLengths, bends) {
@@ -45230,7 +46298,7 @@ function buildRoute3DPlanFromPolyline(pointsInput, options = {}) {
45230
46298
  const segmentLengths = points.slice(1).map((point2, index2) => length$1(sub$5(point2, points[index2])));
45231
46299
  const directions = segmentLengths.map((segmentLength, index2) => {
45232
46300
  if (segmentLength <= EPSILON$2) throw new Error(`Curve.Route.fromPolyline: segment ${index2} is zero length.`);
45233
- return scale$3(sub$5(points[index2 + 1], points[index2]), 1 / segmentLength);
46301
+ return scale$4(sub$5(points[index2 + 1], points[index2]), 1 / segmentLength);
45234
46302
  });
45235
46303
  const bends = new Array(points.length).fill(null);
45236
46304
  if (radius > EPSILON$2) {
@@ -45263,7 +46331,7 @@ function buildRoute3DPlanFromPolyline(pointsInput, options = {}) {
45263
46331
  segments,
45264
46332
  length: routeLength,
45265
46333
  ports: {
45266
- [startPort]: portFrame(startPort, points[0], scale$3(directions[0], -1), 0, up),
46334
+ [startPort]: portFrame(startPort, points[0], scale$4(directions[0], -1), 0, up),
45267
46335
  [endPort]: portFrame(endPort, points[points.length - 1], directions[directions.length - 1], routeLength, up)
45268
46336
  }
45269
46337
  };
@@ -45766,7 +46834,7 @@ function variableSweep(spine, sections, options = {}) {
45766
46834
  sources: ["sweep"]
45767
46835
  });
45768
46836
  }
45769
- function requirePositive$9(value, label) {
46837
+ function requirePositive$8(value, label) {
45770
46838
  if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
45771
46839
  throw new Error(`${label} must be a finite number > 0, got ${JSON.stringify(value)}`);
45772
46840
  }
@@ -45785,10 +46853,10 @@ function requireIntegerAtLeast(value, label, min2) {
45785
46853
  return value;
45786
46854
  }
45787
46855
  function normalizeHelixOptions(options, apiName) {
45788
- const radius = requirePositive$9(options.radius, `${apiName}.radius`);
45789
- const pitch = options.pitch == null ? void 0 : requirePositive$9(options.pitch, `${apiName}.pitch`);
45790
- const turns = options.turns == null ? void 0 : requirePositive$9(options.turns, `${apiName}.turns`);
45791
- const height = options.height == null ? void 0 : requirePositive$9(options.height, `${apiName}.height`);
46856
+ const radius = requirePositive$8(options.radius, `${apiName}.radius`);
46857
+ const pitch = options.pitch == null ? void 0 : requirePositive$8(options.pitch, `${apiName}.pitch`);
46858
+ const turns = options.turns == null ? void 0 : requirePositive$8(options.turns, `${apiName}.turns`);
46859
+ const height = options.height == null ? void 0 : requirePositive$8(options.height, `${apiName}.height`);
45792
46860
  const provided = [pitch != null, turns != null, height != null].filter(Boolean).length;
45793
46861
  if (provided < 2) {
45794
46862
  throw new Error(`${apiName}: provide any two of pitch, turns, and height so the third can be derived.`);
@@ -45874,7 +46942,7 @@ function buildHelixCoil(profileOrOptions, maybeOptions) {
45874
46942
  if (!options) throw new Error("Helix.coil: options are required.");
45875
46943
  const spec2 = normalizeHelixOptions(options, "Helix.coil");
45876
46944
  const profile = hasCustomProfile ? profileOrOptions : circle2d(
45877
- requirePositive$9(options.wireRadius, "Helix.coil.wireRadius"),
46945
+ requirePositive$8(options.wireRadius, "Helix.coil.wireRadius"),
45878
46946
  requireIntegerAtLeast(options.profileSegments ?? 24, "Helix.coil.profileSegments", 8)
45879
46947
  );
45880
46948
  if (profile.isEmpty()) throw new Error("Helix.coil: profile must not be empty.");
@@ -46457,6 +47525,12 @@ const Curve = {
46457
47525
  function sub$4(a2, b) {
46458
47526
  return [a2[0] - b[0], a2[1] - b[1], a2[2] - b[2]];
46459
47527
  }
47528
+ function addVec(a2, b) {
47529
+ return [a2[0] + b[0], a2[1] + b[1], a2[2] + b[2]];
47530
+ }
47531
+ function scale$3(v, s) {
47532
+ return [v[0] * s, v[1] * s, v[2] * s];
47533
+ }
46460
47534
  function dot$4(a2, b) {
46461
47535
  return a2[0] * b[0] + a2[1] * b[1] + a2[2] * b[2];
46462
47536
  }
@@ -46473,6 +47547,56 @@ function normalize$3(v) {
46473
47547
  function clampDot(d2) {
46474
47548
  return Math.max(-1, Math.min(1, d2));
46475
47549
  }
47550
+ function requirePositiveFinite(value, label) {
47551
+ if (!Number.isFinite(value) || value <= 0) throw new Error(`${label} must be a finite number > 0`);
47552
+ return value;
47553
+ }
47554
+ function requireNonNegativeFinite(value, label) {
47555
+ if (!Number.isFinite(value) || value < 0) throw new Error(`${label} must be a finite number >= 0`);
47556
+ return value;
47557
+ }
47558
+ function requireFiniteVec3$1(value, label) {
47559
+ if (value == null) return void 0;
47560
+ if (!Array.isArray(value) || value.length !== 3) throw new Error(`${label} must be [x, y, z]`);
47561
+ const result = [Number(value[0]), Number(value[1]), Number(value[2])];
47562
+ if (!Number.isFinite(result[0]) || !Number.isFinite(result[1]) || !Number.isFinite(result[2])) {
47563
+ throw new Error(`${label} must contain finite coordinates`);
47564
+ }
47565
+ return result;
47566
+ }
47567
+ function normalizedDirection(value, label) {
47568
+ const len2 = vecLen(value);
47569
+ if (len2 < 1e-12) throw new Error(`${label} must not be the zero vector`);
47570
+ return [value[0] / len2, value[1] / len2, value[2] / len2];
47571
+ }
47572
+ function stablePerpendicularAxis$1(direction2) {
47573
+ const seed = Math.abs(direction2[1]) < 0.9 ? [0, 1, 0] : [1, 0, 0];
47574
+ return normalizedDirection(sub$4(seed, scale$3(direction2, dot$4(direction2, seed))), "elbow: turn plane");
47575
+ }
47576
+ function rotateDirection(v, axis, angleRad) {
47577
+ const c2 = Math.cos(angleRad);
47578
+ const s = Math.sin(angleRad);
47579
+ const axisDot = dot$4(axis, v);
47580
+ const axisCross = cross$3(axis, v);
47581
+ return [
47582
+ v[0] * c2 + axisCross[0] * s + axis[0] * axisDot * (1 - c2),
47583
+ v[1] * c2 + axisCross[1] * s + axis[1] * axisDot * (1 - c2),
47584
+ v[2] * c2 + axisCross[2] * s + axis[2] * axisDot * (1 - c2)
47585
+ ];
47586
+ }
47587
+ function angleBetweenDirections(from, to) {
47588
+ return Math.acos(clampDot(dot$4(from, to)));
47589
+ }
47590
+ function turnAxisForDirections(from, to) {
47591
+ const axis = cross$3(from, to);
47592
+ if (vecLen(axis) >= 1e-12) return normalizedDirection(axis, "elbow: turn axis");
47593
+ if (dot$4(from, to) > 0) throw new Error("elbow: from and to directions are collinear; angle too small");
47594
+ return stablePerpendicularAxis$1(from);
47595
+ }
47596
+ function profileForPipeRadius(pipeRadius, wall, segments) {
47597
+ const outer = circle2d(pipeRadius, segments);
47598
+ return wall != null && wall > 0 ? difference2d(outer, circle2d(pipeRadius - wall, segments)) : outer;
47599
+ }
46476
47600
  function pipeRoute(points, radius, options) {
46477
47601
  if (points.length < 2) throw new Error("pipeRoute needs at least 2 points");
46478
47602
  const bendR = (options == null ? void 0 : options.bendRadius) ?? radius * 4;
@@ -46504,88 +47628,40 @@ function simplifyStraightPipeRoutePoints(points) {
46504
47628
  return simplified;
46505
47629
  }
46506
47630
  function elbow(pipeRadius, bendRadius, angle, options) {
46507
- let angleDeg;
46508
- let wall;
46509
- let segs;
46510
- let fromDir;
46511
- let toDir;
46512
- if (typeof angle === "object" && angle !== null) {
46513
- angleDeg = 90;
46514
- wall = angle.wall;
46515
- segs = angle.segments ?? 32;
46516
- fromDir = angle.from;
46517
- toDir = angle.to;
47631
+ requirePositiveFinite(pipeRadius, "elbow: pipeRadius");
47632
+ requirePositiveFinite(bendRadius, "elbow: bendRadius");
47633
+ const opts = typeof angle === "object" && angle !== null ? angle : options;
47634
+ const wall = opts == null ? void 0 : opts.wall;
47635
+ const segments = (opts == null ? void 0 : opts.segments) ?? 32;
47636
+ if (!Number.isFinite(segments) || segments < 3) throw new Error("elbow: segments must be a finite number >= 3");
47637
+ if (wall != null && requireNonNegativeFinite(wall, "elbow: wall") >= pipeRadius) {
47638
+ throw new Error("elbow: wall must be smaller than pipeRadius");
47639
+ }
47640
+ const from = normalizedDirection(requireFiniteVec3$1(opts == null ? void 0 : opts.from, "elbow: from") ?? [0, 0, 1], "elbow: from");
47641
+ const toInput = requireFiniteVec3$1(opts == null ? void 0 : opts.to, "elbow: to");
47642
+ const angleDegInput = typeof angle === "number" ? angle : void 0;
47643
+ const angleRadInput = (angleDegInput ?? 90) * Math.PI / 180;
47644
+ let angleRad;
47645
+ let turnAxis;
47646
+ if (toInput) {
47647
+ const to = normalizedDirection(toInput, "elbow: to");
47648
+ angleRad = angleBetweenDirections(from, to);
47649
+ turnAxis = turnAxisForDirections(from, to);
46518
47650
  } else {
46519
- angleDeg = angle ?? 90;
46520
- wall = options == null ? void 0 : options.wall;
46521
- segs = (options == null ? void 0 : options.segments) ?? 32;
46522
- fromDir = options == null ? void 0 : options.from;
46523
- toDir = options == null ? void 0 : options.to;
46524
- }
46525
- if (fromDir && toDir) {
46526
- const nFrom = normalize$3(fromDir);
46527
- const nTo = normalize$3(toDir);
46528
- const d2 = clampDot(dot$4(nFrom, nTo));
46529
- angleDeg = Math.acos(d2) * 180 / Math.PI;
46530
- }
46531
- if (angleDeg < 0.01) throw new Error("elbow: angle too small");
46532
- const circlePts = [];
46533
- for (let i = 0; i < segs; i++) {
46534
- const a2 = i / segs * Math.PI * 2;
46535
- circlePts.push([bendRadius + pipeRadius * Math.cos(a2), pipeRadius * Math.sin(a2)]);
46536
- }
46537
- const bendSegs = Math.max(4, Math.ceil(segs * angleDeg / 360));
46538
- const outerPlan = {
46539
- kind: "revolve",
46540
- profile: { kind: "polygon", points: circlePts, transforms: [] },
46541
- degrees: angleDeg,
46542
- segments: bendSegs
46543
- };
46544
- let bendShape = buildShapeFromCompilePlan(outerPlan);
46545
- if (wall != null && wall > 0) {
46546
- const innerPts = [];
46547
- const innerR = pipeRadius - wall;
46548
- for (let i = 0; i < segs; i++) {
46549
- const a2 = i / segs * Math.PI * 2;
46550
- innerPts.push([bendRadius + innerR * Math.cos(a2), innerR * Math.sin(a2)]);
46551
- }
46552
- const innerPlan = {
46553
- kind: "revolve",
46554
- profile: { kind: "polygon", points: innerPts, transforms: [] },
46555
- degrees: angleDeg,
46556
- segments: bendSegs
46557
- };
46558
- const innerBend = buildShapeFromCompilePlan(innerPlan);
46559
- bendShape = bendShape.subtract(innerBend);
46560
- }
46561
- if (fromDir && toDir) {
46562
- const nFrom = normalize$3(fromDir);
46563
- const nTo = normalize$3(toDir);
46564
- const crossVec = cross$3(nFrom, nTo);
46565
- const crossLen = vecLen(crossVec);
46566
- if (crossLen < 1e-10) return bendShape;
46567
- const axis = normalize$3(crossVec);
46568
- const perpDir = cross$3(axis, nFrom);
46569
- bendShape = bendShape.transform([
46570
- perpDir[0],
46571
- perpDir[1],
46572
- perpDir[2],
46573
- 0,
46574
- nFrom[0],
46575
- nFrom[1],
46576
- nFrom[2],
46577
- 0,
46578
- axis[0],
46579
- axis[1],
46580
- axis[2],
46581
- 0,
46582
- 0,
46583
- 0,
46584
- 0,
46585
- 1
46586
- ]);
47651
+ if (!Number.isFinite(angleRadInput) || angleRadInput <= 1e-6 || angleRadInput > Math.PI) {
47652
+ throw new Error("elbow: angle must be finite and in the range (0, 180] degrees");
47653
+ }
47654
+ turnAxis = stablePerpendicularAxis$1(from);
47655
+ angleRad = angleRadInput;
46587
47656
  }
46588
- return bendShape;
47657
+ if (angleRad <= 1e-6) throw new Error("elbow: angle too small");
47658
+ const inward = normalizedDirection(cross$3(turnAxis, from), "elbow: bend normal");
47659
+ const center = scale$3(inward, bendRadius);
47660
+ const endRadial = rotateDirection(scale$3(inward, -bendRadius), turnAxis, angleRad);
47661
+ const end = addVec(center, endRadial);
47662
+ const profile = profileForPipeRadius(pipeRadius, wall, Math.floor(segments));
47663
+ const pathSamples = Math.max(6, Math.ceil(segments * (angleRad * 180 / Math.PI) / 360) + 1);
47664
+ return sweep(profile, Curve.Arc({ start: [0, 0, 0], end, tangent: from }), { samples: pathSamples, up: turnAxis });
46589
47665
  }
46590
47666
  const EPS$7 = 1e-9;
46591
47667
  function assertFinitePositive(apiName, name, value) {
@@ -47084,7 +48160,7 @@ function spurGear(options) {
47084
48160
  });
47085
48161
  return attachGearMeta(shapeWithConnectors, meta2);
47086
48162
  }
47087
- function requirePositive$8(scope, name, value) {
48163
+ function requirePositive$7(scope, name, value) {
47088
48164
  if (!isFinitePositive(value)) throw new Error(`${scope}: "${name}" must be > 0`);
47089
48165
  }
47090
48166
  function requireOptionalBore(scope, boreDiameter, maxDiameter) {
@@ -47106,8 +48182,8 @@ function cutBore$1(shape, boreDiameter) {
47106
48182
  return shape.subtract(cutter);
47107
48183
  }
47108
48184
  function gearBodyDisk(options) {
47109
- requirePositive$8("gearBodyDisk", "outerRadius", options.outerRadius);
47110
- requirePositive$8("gearBodyDisk", "faceWidth", options.faceWidth);
48185
+ requirePositive$7("gearBodyDisk", "outerRadius", options.outerRadius);
48186
+ requirePositive$7("gearBodyDisk", "faceWidth", options.faceWidth);
47111
48187
  const bore = requireOptionalBore("gearBodyDisk", options.boreDiameter, options.outerRadius * 2);
47112
48188
  const segments = resolveSegments(options.segments);
47113
48189
  const outer = circle2d(options.outerRadius, segments);
@@ -47115,14 +48191,14 @@ function gearBodyDisk(options) {
47115
48191
  return sketchExtrude(profile, options.faceWidth);
47116
48192
  }
47117
48193
  function gearBodyDiskWithHub(options) {
47118
- requirePositive$8("gearBodyDiskWithHub", "hubDiameter", options.hubDiameter);
48194
+ requirePositive$7("gearBodyDiskWithHub", "hubDiameter", options.hubDiameter);
47119
48195
  if (options.hubDiameter >= options.outerRadius * 2) {
47120
48196
  throw new Error('gearBodyDiskWithHub: "hubDiameter" must be smaller than the outer diameter');
47121
48197
  }
47122
48198
  const bore = requireOptionalBore("gearBodyDiskWithHub", options.boreDiameter, options.hubDiameter);
47123
48199
  const base = gearBodyDisk({ ...options, boreDiameter: 0 });
47124
48200
  const hubFaceWidth = options.hubFaceWidth ?? options.faceWidth * 1.5;
47125
- requirePositive$8("gearBodyDiskWithHub", "hubFaceWidth", hubFaceWidth);
48201
+ requirePositive$7("gearBodyDiskWithHub", "hubFaceWidth", hubFaceWidth);
47126
48202
  const hub = cylinder(hubFaceWidth, options.hubDiameter * 0.5, void 0, options.segments).translate(
47127
48203
  0,
47128
48204
  0,
@@ -47131,11 +48207,11 @@ function gearBodyDiskWithHub(options) {
47131
48207
  return cutBore$1(base.add(hub), bore);
47132
48208
  }
47133
48209
  function gearBodySpoked(options) {
47134
- requirePositive$8("gearBodySpoked", "outerRadius", options.outerRadius);
47135
- requirePositive$8("gearBodySpoked", "faceWidth", options.faceWidth);
47136
- requirePositive$8("gearBodySpoked", "rimWidth", options.rimWidth);
47137
- requirePositive$8("gearBodySpoked", "hubDiameter", options.hubDiameter);
47138
- requirePositive$8("gearBodySpoked", "spokeWidth", options.spokeWidth);
48210
+ requirePositive$7("gearBodySpoked", "outerRadius", options.outerRadius);
48211
+ requirePositive$7("gearBodySpoked", "faceWidth", options.faceWidth);
48212
+ requirePositive$7("gearBodySpoked", "rimWidth", options.rimWidth);
48213
+ requirePositive$7("gearBodySpoked", "hubDiameter", options.hubDiameter);
48214
+ requirePositive$7("gearBodySpoked", "spokeWidth", options.spokeWidth);
47139
48215
  if (!Number.isInteger(options.spokeCount) || options.spokeCount < 2) {
47140
48216
  throw new Error('gearBodySpoked: "spokeCount" must be an integer >= 2');
47141
48217
  }
@@ -47158,12 +48234,12 @@ function gearBodySpoked(options) {
47158
48234
  }
47159
48235
  function gearBodyFromProfile(profile, options) {
47160
48236
  if (!(profile instanceof Sketch)) throw new Error('gearBodyFromProfile: "profile" must be a Sketch');
47161
- requirePositive$8("gearBodyFromProfile", "faceWidth", options.faceWidth);
48237
+ requirePositive$7("gearBodyFromProfile", "faceWidth", options.faceWidth);
47162
48238
  const bore = options.boreDiameter ?? 0;
47163
48239
  if (!Number.isFinite(bore) || bore < 0) throw new Error('gearBodyFromProfile: "boreDiameter" must be >= 0');
47164
48240
  return cutBore$1(sketchExtrude(profile, options.faceWidth), bore);
47165
48241
  }
47166
- function requirePositive$7(scope, name, value) {
48242
+ function requirePositive$6(scope, name, value) {
47167
48243
  if (!isFinitePositive(value)) throw new Error(`${scope}: "${name}" must be > 0`);
47168
48244
  }
47169
48245
  function requireFiniteAngle(scope, name, value) {
@@ -47225,7 +48301,7 @@ function buildSpurTeethRegion(options, name, faceWidth) {
47225
48301
  }
47226
48302
  function buildSolidArcRegion(options, name, faceWidth) {
47227
48303
  const scope = "driveWheel.addSolidArcBetween";
47228
- requirePositive$7(scope, "outerRadius", options.outerRadius);
48304
+ requirePositive$6(scope, "outerRadius", options.outerRadius);
47229
48305
  const innerRadius = options.innerRadius ?? 0;
47230
48306
  if (!Number.isFinite(innerRadius) || innerRadius < 0) throw new Error(`${scope}: "innerRadius" must be >= 0`);
47231
48307
  if (innerRadius >= options.outerRadius) throw new Error(`${scope}: "innerRadius" must be smaller than "outerRadius"`);
@@ -47291,7 +48367,7 @@ class DriveWheelBuilder {
47291
48367
  __publicField(this, "boreDiameter");
47292
48368
  __publicField(this, "regions", []);
47293
48369
  if (options.body !== void 0 && !(options.body instanceof Shape)) throw new Error('driveWheel: "body" must be a Shape');
47294
- if (options.faceWidth !== void 0) requirePositive$7("driveWheel", "faceWidth", options.faceWidth);
48370
+ if (options.faceWidth !== void 0) requirePositive$6("driveWheel", "faceWidth", options.faceWidth);
47295
48371
  const boreDiameter = options.boreDiameter ?? 0;
47296
48372
  if (!Number.isFinite(boreDiameter) || boreDiameter < 0) throw new Error('driveWheel: "boreDiameter" must be >= 0');
47297
48373
  this.body = options.body;
@@ -47326,7 +48402,7 @@ class DriveWheelBuilder {
47326
48402
  if (options.innerRadius !== void 0 && (!Number.isFinite(options.innerRadius) || options.innerRadius < 0)) {
47327
48403
  throw new Error(`${scope}: "innerRadius" must be >= 0`);
47328
48404
  }
47329
- if (options.outerRadius !== void 0) requirePositive$7(scope, "outerRadius", options.outerRadius);
48405
+ if (options.outerRadius !== void 0) requirePositive$6(scope, "outerRadius", options.outerRadius);
47330
48406
  this.regions.push({
47331
48407
  shape: shape.clone(),
47332
48408
  meta: {
@@ -47392,7 +48468,7 @@ class DriveWheelBuilder {
47392
48468
  resolveFaceWidth(scope, localFaceWidth) {
47393
48469
  const faceWidth = localFaceWidth ?? this.faceWidth;
47394
48470
  if (faceWidth === void 0) throw new Error(`${scope}: "faceWidth" is required unless driveWheel({ faceWidth }) was set`);
47395
- requirePositive$7(scope, "faceWidth", faceWidth);
48471
+ requirePositive$6(scope, "faceWidth", faceWidth);
47396
48472
  if (this.faceWidth !== void 0 && localFaceWidth !== void 0 && Math.abs(this.faceWidth - localFaceWidth) > EPSILON$1) {
47397
48473
  throw new Error(`${scope}: region faceWidth must match driveWheel faceWidth`);
47398
48474
  }
@@ -48545,1860 +49621,6 @@ function washer(size, options) {
48545
49621
  const bore = cylinder(dims.t + 1, dims.id / 2, void 0, segs);
48546
49622
  return outer.subtract(bore);
48547
49623
  }
48548
- function requirePositive$6(value, name) {
48549
- if (!Number.isFinite(value) || value <= 0) throw new Error(`${name} must be a positive finite number`);
48550
- return value;
48551
- }
48552
- function requireNonNegative(value, name) {
48553
- if (!Number.isFinite(value) || value < 0) throw new Error(`${name} must be a non-negative finite number`);
48554
- return value;
48555
- }
48556
- function metricWasherSizeForPin(pinDiameter) {
48557
- if (pinDiameter <= 2) return "M2";
48558
- if (pinDiameter <= 2.5) return "M2.5";
48559
- if (pinDiameter <= 3) return "M3";
48560
- if (pinDiameter <= 4) return "M4";
48561
- if (pinDiameter <= 5) return "M5";
48562
- if (pinDiameter <= 6) return "M6";
48563
- if (pinDiameter <= 8) return "M8";
48564
- return "M10";
48565
- }
48566
- function cylinderAlongX(length4, radius, xCenter, segments) {
48567
- return cylinder(length4, radius, void 0, segments).pointAlong([1, 0, 0]).translate(xCenter - length4 / 2, 0, 0);
48568
- }
48569
- function tubeAlongX(length4, outerRadius, innerRadius, xCenter, segments) {
48570
- return cylinderAlongX(length4, outerRadius, xCenter, segments).subtract(cylinderAlongX(length4 + 0.4, innerRadius, xCenter, segments));
48571
- }
48572
- function cylinderAlongY(length4, radius, yCenter, segments) {
48573
- return cylinder(length4, radius, void 0, segments).pointAlong([0, 1, 0]).translate(0, yCenter - length4 / 2, 0);
48574
- }
48575
- function tubeAlongY(length4, outerRadius, innerRadius, yCenter, segments) {
48576
- return cylinderAlongY(length4, outerRadius, yCenter, segments).subtract(cylinderAlongY(length4 + 0.4, innerRadius, yCenter, segments));
48577
- }
48578
- function tubeAlongZ(height, outerRadius, innerRadius, segments) {
48579
- return cylinder(height, outerRadius, void 0, segments).subtract(
48580
- cylinder(height + 0.4, innerRadius, void 0, segments).translate(0, 0, -0.2)
48581
- );
48582
- }
48583
- function washerAlongX(size, xCenter, segments) {
48584
- const dims = WASHER_TABLE[size];
48585
- return washer(size, { segments }).pointAlong([1, 0, 0]).translate(xCenter - dims.t / 2, 0, 0);
48586
- }
48587
- function resolveBoltInset(raw, fallback) {
48588
- if (raw === void 0) return [fallback, fallback];
48589
- if (typeof raw === "number") return [requirePositive$6(raw, "boltInset"), requirePositive$6(raw, "boltInset")];
48590
- if (raw.length !== 2) throw new Error("boltInset tuple must be [x, y]");
48591
- return [requirePositive$6(raw[0], "boltInset[0]"), requirePositive$6(raw[1], "boltInset[1]")];
48592
- }
48593
- function validateBoltPositionsForServiceCover(args) {
48594
- args.positions.forEach(([x2, y2], index2) => {
48595
- if (!Number.isFinite(x2) || !Number.isFinite(y2)) {
48596
- throw new Error(`boltedServiceCover: boltPositions[${index2}] must contain finite numbers`);
48597
- }
48598
- if (Math.abs(x2) + args.holeRadius >= args.coverWidth / 2 || Math.abs(y2) + args.holeRadius >= args.coverDepth / 2) {
48599
- throw new Error(`boltedServiceCover: boltPositions[${index2}] is too close to the cover edge`);
48600
- }
48601
- const overlapsOpening = Math.abs(x2) - args.holeRadius <= args.openingWidth / 2 && Math.abs(y2) - args.holeRadius <= args.openingDepth / 2;
48602
- if (overlapsOpening) {
48603
- throw new Error(
48604
- `boltedServiceCover: boltPositions[${index2}] lands over the service opening; decrease boltInset, increase ledgeWidth, or provide a smaller opening`
48605
- );
48606
- }
48607
- });
48608
- }
48609
- function placeCutterAtPositions(cutter, positions, z2) {
48610
- return union(...positions.map(([x2, y2]) => cutter.translate(x2, y2, z2)));
48611
- }
48612
- function boltedServiceCover(options) {
48613
- const width = requirePositive$6(options.width, "width");
48614
- const depth = requirePositive$6(options.depth, "depth");
48615
- const coverThickness = requirePositive$6(options.coverThickness ?? 3, "coverThickness");
48616
- const parentThickness = requirePositive$6(options.parentThickness ?? 8, "parentThickness");
48617
- const ledgeWidth = requirePositive$6(options.ledgeWidth ?? 8, "ledgeWidth");
48618
- const gasketThickness = Math.max(0, options.gasketThickness ?? 0.8);
48619
- const gasketInset = Math.max(0, options.gasketInset ?? 2);
48620
- const screwSize = options.screwSize ?? "M4";
48621
- const segments = options.segments ?? 36;
48622
- const sizeData = METRIC_HOLE_TABLE[screwSize];
48623
- if (!sizeData) throw new Error(`boltedServiceCover: unsupported screwSize "${screwSize}"`);
48624
- const screwLength = requirePositive$6(options.screwLength ?? parentThickness + gasketThickness + coverThickness + 4, "screwLength");
48625
- const coverFit = options.coverFit ?? "normal";
48626
- const counterboreEnabled = options.counterbore ?? true;
48627
- const [insetX, insetY] = resolveBoltInset(options.boltInset, Math.max(ledgeWidth * 0.65, sizeData.head * 0.75));
48628
- if (insetX * 2 >= width || insetY * 2 >= depth) {
48629
- throw new Error("boltedServiceCover: boltInset leaves no room for a four-corner bolt pattern");
48630
- }
48631
- const boltPositions = options.boltPositions ?? [
48632
- [-width / 2 + insetX, -depth / 2 + insetY],
48633
- [width / 2 - insetX, -depth / 2 + insetY],
48634
- [-width / 2 + insetX, depth / 2 - insetY],
48635
- [width / 2 - insetX, depth / 2 - insetY]
48636
- ];
48637
- if (boltPositions.length === 0) throw new Error("boltedServiceCover: boltPositions must contain at least one point");
48638
- const parentWidth = width + ledgeWidth * 2;
48639
- const parentDepth = depth + ledgeWidth * 2;
48640
- const openingWidth = Math.max(1, width - ledgeWidth * 2);
48641
- const openingDepth = Math.max(1, depth - ledgeWidth * 2);
48642
- validateBoltPositionsForServiceCover({
48643
- positions: boltPositions,
48644
- coverWidth: width,
48645
- coverDepth: depth,
48646
- openingWidth,
48647
- openingDepth,
48648
- holeRadius: sizeData[coverFit] / 2
48649
- });
48650
- const coverHole = fastenerHole({
48651
- size: screwSize,
48652
- fit: coverFit,
48653
- depth: coverThickness + 0.6,
48654
- center: true,
48655
- segments,
48656
- ...counterboreEnabled ? { counterbore: { depth: Math.min(coverThickness * 0.6, Math.max(0.6, coverThickness - 0.4)) } } : {}
48657
- });
48658
- const parentTap = fastenerHole({ size: screwSize, fit: "tap", depth: parentThickness + 0.6, center: true, segments });
48659
- const parentThreadEnvelope = fastenerHole({
48660
- size: screwSize,
48661
- fit: "close",
48662
- depth: parentThickness + 0.6,
48663
- center: true,
48664
- segments
48665
- });
48666
- const openingCutter = box(openingWidth, openingDepth, parentThickness + 1).translate(0, 0, -0.5);
48667
- const parentTappedPattern = placeCutterAtPositions(parentTap, boltPositions, parentThickness / 2);
48668
- const parentThreadEnvelopePattern = placeCutterAtPositions(parentThreadEnvelope, boltPositions, parentThickness / 2);
48669
- const parent = box(parentWidth, parentDepth, parentThickness).subtract(openingCutter).subtract(parentThreadEnvelopePattern).color("#4b5563");
48670
- let coverBlank = box(width, depth, coverThickness);
48671
- if (options.pullTabs ?? true) {
48672
- const tabWidth = Math.min(width * 0.18, Math.max(sizeData.head * 1.6, 12));
48673
- const tabDepth = Math.max(4, coverThickness * 1.4);
48674
- const tabOverlap = Math.min(0.5, tabDepth * 0.25);
48675
- const tabY = -depth / 2 - tabDepth / 2 + tabOverlap;
48676
- const tabX = width * 0.23;
48677
- coverBlank = union(
48678
- coverBlank,
48679
- box(tabWidth, tabDepth, coverThickness).translate(-tabX, tabY, 0),
48680
- box(tabWidth, tabDepth, coverThickness).translate(tabX, tabY, 0)
48681
- );
48682
- }
48683
- const coverClearancePattern = placeCutterAtPositions(coverHole, boltPositions, coverThickness / 2);
48684
- const cover = coverBlank.subtract(coverClearancePattern).translate(0, 0, parentThickness + gasketThickness).color("#334155");
48685
- 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;
48686
- const hardware = fastenerSet(screwSize, screwLength, { washerUnderHead: false, washerUnderNut: false, fit: coverFit, segments });
48687
- const screwOriginZ = parentThickness + gasketThickness + coverThickness;
48688
- const screws = boltPositions.map(([x2, y2]) => hardware.bolt.translate(x2, y2, screwOriginZ).color("#94a3b8"));
48689
- const parts = [
48690
- { name: "service cover parent ledge with threaded hole envelopes", shape: parent },
48691
- ...gasket ? [{ name: "service cover gasket seated on ledge", shape: gasket }] : [],
48692
- { name: "bolted service cover plate with fused pull tabs", shape: cover },
48693
- ...screws.map((shape, index2) => ({ name: `installed ${screwSize} cover screw ${index2 + 1}`, shape }))
48694
- ];
48695
- return {
48696
- parts,
48697
- parent,
48698
- cover,
48699
- gasket,
48700
- screws,
48701
- boltPositions,
48702
- cutters: {
48703
- coverClearance: coverClearancePattern,
48704
- parentTapped: parentTappedPattern,
48705
- parentThreadEnvelope: parentThreadEnvelopePattern
48706
- },
48707
- dims: {
48708
- width,
48709
- depth,
48710
- coverThickness,
48711
- parentThickness,
48712
- ledgeWidth,
48713
- gasketThickness,
48714
- screwSize,
48715
- screwLength,
48716
- clearanceDia: sizeData[coverFit],
48717
- tapDia: sizeData.tap,
48718
- threadEnvelopeDia: sizeData.close
48719
- }
48720
- };
48721
- }
48722
- function datumEnclosureAssembly(options) {
48723
- const width = requirePositive$6(options.width, "width");
48724
- const depth = requirePositive$6(options.depth, "depth");
48725
- const height = requirePositive$6(options.height, "height");
48726
- const wallThickness = requirePositive$6(options.wallThickness ?? 2.4, "wallThickness");
48727
- const baseThickness = requirePositive$6(options.baseThickness ?? wallThickness, "baseThickness");
48728
- const coverThickness = requirePositive$6(options.coverThickness ?? 2.4, "coverThickness");
48729
- const ledgeWidth = requirePositive$6(options.ledgeWidth ?? Math.max(3.6, wallThickness * 1.35), "ledgeWidth");
48730
- const gasketThickness = requireNonNegative(options.gasketThickness ?? 0.8, "gasketThickness");
48731
- const faceClearance = requirePositive$6(options.faceClearance ?? 0.04, "faceClearance");
48732
- const screwSize = options.screwSize ?? "M3";
48733
- const coverFit = options.coverFit ?? "normal";
48734
- const segments = options.segments ?? 32;
48735
- const sizeData = METRIC_HOLE_TABLE[screwSize];
48736
- if (!sizeData) throw new Error(`datumEnclosureAssembly: unsupported screwSize "${screwSize}"`);
48737
- const innerWidth = width - wallThickness * 2;
48738
- const innerDepth = depth - wallThickness * 2;
48739
- if (innerWidth <= ledgeWidth * 2 + 8 || innerDepth <= ledgeWidth * 2 + 8) {
48740
- throw new Error("datumEnclosureAssembly: wallThickness and ledgeWidth leave too little internal opening");
48741
- }
48742
- if (height <= baseThickness + coverThickness + 4) {
48743
- throw new Error("datumEnclosureAssembly: height must leave room for internal ribs and standoffs");
48744
- }
48745
- const standoffDiameter = requirePositive$6(
48746
- options.standoffDiameter ?? Math.max(sizeData.head * 1.65, sizeData.close * 2.2),
48747
- "standoffDiameter"
48748
- );
48749
- const minInset = wallThickness + Math.max(ledgeWidth, standoffDiameter / 2 + 1.2);
48750
- const [insetX, insetY] = resolveBoltInset(options.screwInset, minInset);
48751
- if (insetX * 2 >= width || insetY * 2 >= depth) {
48752
- throw new Error("datumEnclosureAssembly: screwInset leaves no room for the standoff datum");
48753
- }
48754
- const screwPositions = options.screwPositions ?? [
48755
- [-width / 2 + insetX, -depth / 2 + insetY],
48756
- [width / 2 - insetX, -depth / 2 + insetY],
48757
- [-width / 2 + insetX, depth / 2 - insetY],
48758
- [width / 2 - insetX, depth / 2 - insetY]
48759
- ];
48760
- if (screwPositions.length === 0) throw new Error("datumEnclosureAssembly: screwPositions must contain at least one point");
48761
- for (const [index2, [x2, y2]] of screwPositions.entries()) {
48762
- if (!Number.isFinite(x2) || !Number.isFinite(y2)) {
48763
- throw new Error(`datumEnclosureAssembly: screwPositions[${index2}] must contain finite numbers`);
48764
- }
48765
- if (Math.abs(x2) + standoffDiameter / 2 > innerWidth / 2 || Math.abs(y2) + standoffDiameter / 2 > innerDepth / 2) {
48766
- throw new Error(`datumEnclosureAssembly: screwPositions[${index2}] does not fit inside the enclosure walls`);
48767
- }
48768
- }
48769
- const ribHeight = requirePositive$6(options.ribHeight ?? Math.min(height * 0.24, Math.max(2.4, baseThickness * 1.4)), "ribHeight");
48770
- const ribThickness = requirePositive$6(options.ribThickness ?? Math.max(1.2, wallThickness * 0.75), "ribThickness");
48771
- const portWidth = requirePositive$6(options.portWidth ?? Math.min(innerWidth * 0.28, Math.max(12, width * 0.16)), "portWidth");
48772
- const portHeight = requirePositive$6(options.portHeight ?? Math.min(height * 0.42, Math.max(5, height * 0.28)), "portHeight");
48773
- if (portWidth >= innerWidth - ledgeWidth * 2) {
48774
- throw new Error("datumEnclosureAssembly: portWidth must fit between internal ledges and standoffs");
48775
- }
48776
- if (portHeight >= height - baseThickness - 1) {
48777
- throw new Error("datumEnclosureAssembly: portHeight must leave material above and below the service port");
48778
- }
48779
- const screwLength = requirePositive$6(options.screwLength ?? coverThickness + gasketThickness + Math.max(6, height * 0.45), "screwLength");
48780
- const coverHole = fastenerHole({
48781
- size: screwSize,
48782
- fit: coverFit,
48783
- depth: coverThickness + 0.6,
48784
- center: true,
48785
- segments,
48786
- counterbore: { depth: Math.min(coverThickness * 0.6, Math.max(0.6, coverThickness - 0.35)) }
48787
- });
48788
- const standoffTap = fastenerHole({ size: screwSize, fit: "tap", depth: height + 0.8, center: true, segments });
48789
- const standoffThreadEnvelope = fastenerHole({ size: screwSize, fit: "close", depth: height + 0.8, center: true, segments });
48790
- const coverClearance = placeCutterAtPositions(coverHole, screwPositions, coverThickness / 2);
48791
- const standoffTappedPattern = placeCutterAtPositions(standoffTap, screwPositions, height / 2);
48792
- const standoffThreadEnvelopePattern = placeCutterAtPositions(standoffThreadEnvelope, screwPositions, height / 2);
48793
- const fuseOverlap = Math.min(0.06, Math.max(0.02, wallThickness * 0.02));
48794
- const ledgeThickness = Math.min(Math.max(1.1, coverThickness * 0.45), height * 0.2);
48795
- const sideX = width / 2 - wallThickness / 2;
48796
- const sideY = depth / 2 - wallThickness / 2;
48797
- const ledgeZ = height - ledgeThickness;
48798
- const baseSolids = [
48799
- box(width, depth, baseThickness),
48800
- box(wallThickness, depth, height).translate(sideX, 0, 0),
48801
- box(wallThickness, depth, height).translate(-sideX, 0, 0),
48802
- box(width, wallThickness, height).translate(0, sideY, 0),
48803
- box(width, wallThickness, height).translate(0, -sideY, 0),
48804
- box(ledgeWidth, innerDepth, ledgeThickness).translate(-width / 2 + wallThickness + ledgeWidth / 2, 0, ledgeZ),
48805
- box(ledgeWidth, innerDepth, ledgeThickness).translate(width / 2 - wallThickness - ledgeWidth / 2, 0, ledgeZ),
48806
- box(innerWidth, ledgeWidth, ledgeThickness).translate(0, -depth / 2 + wallThickness + ledgeWidth / 2, ledgeZ),
48807
- box(innerWidth, ledgeWidth, ledgeThickness).translate(0, depth / 2 - wallThickness - ledgeWidth / 2, ledgeZ),
48808
- box(Math.max(1, innerWidth - standoffDiameter * 1.8), ribThickness, ribHeight + fuseOverlap).translate(
48809
- 0,
48810
- 0,
48811
- baseThickness - fuseOverlap
48812
- ),
48813
- box(ribThickness, Math.max(1, innerDepth - standoffDiameter * 1.8), ribHeight + fuseOverlap).translate(
48814
- 0,
48815
- 0,
48816
- baseThickness - fuseOverlap
48817
- ),
48818
- ...screwPositions.map(
48819
- ([x2, y2]) => cylinder(height - baseThickness + fuseOverlap, standoffDiameter / 2, void 0, segments).translate(
48820
- x2,
48821
- y2,
48822
- baseThickness - fuseOverlap
48823
- )
48824
- )
48825
- ];
48826
- const servicePort = box(portWidth, wallThickness + 1, portHeight).translate(
48827
- 0,
48828
- -depth / 2 + wallThickness / 2,
48829
- baseThickness + Math.max(0.8, (height - baseThickness - portHeight) * 0.35)
48830
- );
48831
- const base = union(...baseSolids).subtract(standoffThreadEnvelopePattern).subtract(servicePort).color("#475569");
48832
- const gasketFrameCutter = box(Math.max(1, width - ledgeWidth * 2), Math.max(1, depth - ledgeWidth * 2), gasketThickness + 0.6).translate(
48833
- 0,
48834
- 0,
48835
- -0.3
48836
- );
48837
- const gasket = gasketThickness > 0 ? box(width, depth, gasketThickness).subtract(gasketFrameCutter).subtract(placeCutterAtPositions(coverHole, screwPositions, gasketThickness / 2)).translate(0, 0, height + faceClearance).color("#111827") : null;
48838
- const coverZ = height + faceClearance + (gasket ? gasketThickness + faceClearance : 0);
48839
- const cover = box(width, depth, coverThickness).subtract(coverClearance).translate(0, 0, coverZ).color("#334155");
48840
- const hardware = fastenerSet(screwSize, screwLength, { washerUnderHead: false, washerUnderNut: false, fit: coverFit, segments });
48841
- const screwOriginZ = coverZ + coverThickness;
48842
- const screws = screwPositions.map(([x2, y2]) => hardware.bolt.translate(x2, y2, screwOriginZ).color("#94a3b8"));
48843
- const parts = [
48844
- { name: "datum enclosure base tray with walls ribs standoffs and service port", shape: base },
48845
- ...gasket ? [{ name: "datum enclosure gasket seated on continuous ledge", shape: gasket }] : [],
48846
- { name: "datum enclosure cover plate with matched screw pattern", shape: cover },
48847
- ...screws.map((shape, index2) => ({ name: `installed ${screwSize} enclosure screw ${index2 + 1}`, shape }))
48848
- ];
48849
- return {
48850
- parts,
48851
- base,
48852
- cover,
48853
- gasket,
48854
- screws,
48855
- screwPositions,
48856
- cutters: {
48857
- coverClearance,
48858
- standoffTapped: standoffTappedPattern,
48859
- standoffThreadEnvelope: standoffThreadEnvelopePattern,
48860
- servicePort
48861
- },
48862
- dims: {
48863
- width,
48864
- depth,
48865
- height,
48866
- innerWidth,
48867
- innerDepth,
48868
- wallThickness,
48869
- baseThickness,
48870
- coverThickness,
48871
- ledgeWidth,
48872
- gasketThickness,
48873
- faceClearance,
48874
- screwSize,
48875
- screwLength,
48876
- standoffDiameter,
48877
- ribHeight,
48878
- ribThickness,
48879
- portWidth,
48880
- portHeight,
48881
- clearanceDia: sizeData[coverFit],
48882
- tapDia: sizeData.tap,
48883
- threadEnvelopeDia: sizeData.close
48884
- }
48885
- };
48886
- }
48887
- function snapLatchCoverAssembly(options) {
48888
- const width = requirePositive$6(options.width, "width");
48889
- const depth = requirePositive$6(options.depth, "depth");
48890
- const coverThickness = requirePositive$6(options.coverThickness ?? 2.4, "coverThickness");
48891
- const parentThickness = requirePositive$6(options.parentThickness ?? 6, "parentThickness");
48892
- const ledgeWidth = requirePositive$6(options.ledgeWidth ?? 8, "ledgeWidth");
48893
- const runningClearance = requirePositive$6(options.runningClearance ?? 0.25, "runningClearance");
48894
- const faceClearance = requirePositive$6(options.faceClearance ?? 0.04, "faceClearance");
48895
- const latchWidth = requirePositive$6(options.latchWidth ?? Math.min(width * 0.22, Math.max(12, width * 0.16)), "latchWidth");
48896
- const latchThickness = requirePositive$6(options.latchThickness ?? 1.6, "latchThickness");
48897
- const hookThrow = requirePositive$6(options.hookThrow ?? 3.2, "hookThrow");
48898
- const hookThickness = requirePositive$6(options.hookThickness ?? 1.6, "hookThickness");
48899
- const openingWidth = width - ledgeWidth * 2;
48900
- const openingDepth = depth - ledgeWidth * 2;
48901
- if (openingWidth <= Math.max(8, latchWidth * 0.8) || openingDepth <= 8) {
48902
- throw new Error("snapLatchCoverAssembly: ledgeWidth leaves too little service opening under the cover");
48903
- }
48904
- if (latchWidth >= openingWidth) {
48905
- throw new Error("snapLatchCoverAssembly: latchWidth must fit along the receiver opening");
48906
- }
48907
- if (latchThickness + runningClearance * 2 >= ledgeWidth) {
48908
- throw new Error("snapLatchCoverAssembly: latchThickness and clearance must fit inside the receiver ledge");
48909
- }
48910
- if (hookThrow + latchThickness / 2 + runningClearance >= ledgeWidth * 1.5) {
48911
- throw new Error("snapLatchCoverAssembly: hookThrow is too large for the available underside catch land");
48912
- }
48913
- const parentWidth = width + ledgeWidth * 2;
48914
- const parentDepth = depth + ledgeWidth * 2;
48915
- const fuseOverlap = Math.min(0.04, faceClearance * 0.7);
48916
- const hookClearance = Math.min(0.08, runningClearance * 0.32);
48917
- const coverMinZ = parentThickness + faceClearance;
48918
- const stemMinZ = -hookClearance - hookThickness;
48919
- const stemHeight = coverMinZ + fuseOverlap - stemMinZ;
48920
- const slotY = openingDepth / 2 + ledgeWidth / 2;
48921
- const latchWindow = (sign2) => box(latchWidth + runningClearance * 2, latchThickness + runningClearance * 2, parentThickness + 0.8).translate(0, sign2 * slotY, -0.4);
48922
- const latchWindows = union(latchWindow(1), latchWindow(-1));
48923
- const serviceOpening = box(openingWidth, openingDepth, parentThickness + 1).translate(0, 0, -0.5);
48924
- const parent = box(parentWidth, parentDepth, parentThickness).subtract(serviceOpening).subtract(latchWindows).color("#475569");
48925
- const coverPlate = box(width, depth, coverThickness).translate(0, 0, coverMinZ);
48926
- const snapHook = (sign2) => {
48927
- const y2 = sign2 * slotY;
48928
- const stem = box(latchWidth, latchThickness, stemHeight).translate(0, y2, stemMinZ);
48929
- const barb = box(latchWidth, latchThickness + hookThrow, hookThickness).translate(0, y2 + sign2 * (hookThrow / 2), stemMinZ);
48930
- const rootRib = box(latchWidth, Math.max(latchThickness, hookThrow * 0.55), coverThickness * 0.65).translate(
48931
- 0,
48932
- y2 - sign2 * (ledgeWidth * 0.18),
48933
- coverMinZ
48934
- );
48935
- return union(stem, barb, rootRib);
48936
- };
48937
- const cover = union(coverPlate, snapHook(1), snapHook(-1)).color("#111827");
48938
- const parts = [
48939
- { name: "snap cover receiver frame with latch windows and catch lands", shape: parent },
48940
- { name: "one-piece snap cover with fused hooks and underside barbs", shape: cover }
48941
- ];
48942
- return {
48943
- parts,
48944
- parent,
48945
- cover,
48946
- cutters: {
48947
- serviceOpening,
48948
- latchWindows
48949
- },
48950
- dims: {
48951
- width,
48952
- depth,
48953
- parentWidth,
48954
- parentDepth,
48955
- openingWidth,
48956
- openingDepth,
48957
- coverThickness,
48958
- parentThickness,
48959
- ledgeWidth,
48960
- latchWidth,
48961
- latchThickness,
48962
- hookThrow,
48963
- hookThickness,
48964
- runningClearance,
48965
- faceClearance
48966
- }
48967
- };
48968
- }
48969
- function pinnedLeverAssembly(options) {
48970
- const armLength = requirePositive$6(options.armLength, "armLength");
48971
- const armWidth = requirePositive$6(options.armWidth ?? 10, "armWidth");
48972
- const leverThickness = requirePositive$6(options.leverThickness ?? 5, "leverThickness");
48973
- const pinDiameter = requirePositive$6(options.pinDiameter ?? 5, "pinDiameter");
48974
- const pinClearance = requireNonNegative(options.pinClearance ?? 0.25, "pinClearance");
48975
- const boreDiameter = pinDiameter + pinClearance;
48976
- const hubRadius = requirePositive$6(options.hubRadius ?? Math.max(armWidth * 0.85, pinDiameter * 1.8), "hubRadius");
48977
- const supportThickness = requirePositive$6(options.supportThickness ?? Math.max(6, pinDiameter * 1.4), "supportThickness");
48978
- const supportWidth = requirePositive$6(options.supportWidth ?? hubRadius * 2 + 18, "supportWidth");
48979
- const supportDepth = requirePositive$6(options.supportDepth ?? Math.max(armWidth + 18, hubRadius * 2 + 10), "supportDepth");
48980
- const washerSize = options.washerSize ?? metricWasherSizeForPin(pinDiameter);
48981
- const washerDims = WASHER_TABLE[washerSize];
48982
- if (!washerDims) throw new Error(`pinnedLeverAssembly: unsupported washerSize "${washerSize}"`);
48983
- if (washerDims.id <= pinDiameter) {
48984
- throw new Error(`pinnedLeverAssembly: ${washerSize} washer inner diameter is too small for a ${pinDiameter} mm pin`);
48985
- }
48986
- if (hubRadius <= boreDiameter / 2 + Math.max(1, pinDiameter * 0.25)) {
48987
- throw new Error("pinnedLeverAssembly: hubRadius leaves too little material around the pivot bore");
48988
- }
48989
- if (supportWidth <= boreDiameter + 4 || supportDepth <= boreDiameter + 4) {
48990
- throw new Error("pinnedLeverAssembly: support dimensions leave too little material around the pivot bore");
48991
- }
48992
- const segments = options.segments ?? 40;
48993
- const gripLength = requirePositive$6(options.gripLength ?? Math.min(armLength * 0.32, Math.max(16, armWidth * 2.4)), "gripLength");
48994
- const gripWidth = requirePositive$6(options.gripWidth ?? armWidth * 1.55, "gripWidth");
48995
- if (gripLength >= armLength) throw new Error("pinnedLeverAssembly: gripLength must be shorter than armLength");
48996
- const armOverlap = Math.min(hubRadius * 0.65, armLength * 0.25);
48997
- const armStartX = hubRadius - armOverlap;
48998
- const armCenterX = armStartX + armLength / 2;
48999
- const gripCenterX = armStartX + armLength - gripLength / 2;
49000
- const runningClearance = 0.03;
49001
- const lowerWasherZ = supportThickness + runningClearance;
49002
- const leverZ = lowerWasherZ + washerDims.t + runningClearance;
49003
- const upperWasherZ = leverZ + leverThickness + runningClearance;
49004
- const stackHeight = upperWasherZ + washerDims.t;
49005
- const pinHeadThickness = Math.max(washerDims.t, pinDiameter * 0.35);
49006
- const pinHeadRadius = Math.max(washerDims.od * 0.42, pinDiameter * 0.8);
49007
- const supportBore = cylinder(supportThickness + 1, boreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
49008
- let supportBlank = box(supportWidth, supportDepth, supportThickness);
49009
- if (options.stopBlock ?? true) {
49010
- const stopLength = Math.min(armLength * 0.22, Math.max(10, armWidth * 1.4));
49011
- const stopWidth = Math.max(4, pinDiameter * 0.7);
49012
- const stopHeight = supportThickness;
49013
- const stopX = hubRadius + stopLength / 2;
49014
- const stopY = armWidth / 2 + stopWidth / 2 + runningClearance;
49015
- supportBlank = union(supportBlank, box(stopLength, stopWidth, stopHeight).translate(stopX, stopY, 0));
49016
- }
49017
- const support = supportBlank.subtract(supportBore).color("#475569");
49018
- const hub = cylinder(leverThickness, hubRadius, void 0, segments);
49019
- const arm = box(armLength, armWidth, leverThickness).translate(armCenterX, 0, 0);
49020
- const grip = box(gripLength, gripWidth, leverThickness).translate(gripCenterX, 0, 0);
49021
- const leverSolids = [hub, arm, grip];
49022
- if (options.detentBoss ?? true) {
49023
- const bossRadius = Math.min(armWidth * 0.42, hubRadius * 0.42);
49024
- const bossX = hubRadius + Math.min(armLength * 0.22, armWidth * 2);
49025
- const bossY = -armWidth / 2 - bossRadius * 0.45;
49026
- leverSolids.push(cylinder(leverThickness, bossRadius, void 0, segments).translate(bossX, bossY, 0));
49027
- }
49028
- const leverBore = cylinder(leverThickness + 1, boreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
49029
- const lever = union(...leverSolids).subtract(leverBore).translate(0, 0, leverZ).color("#7f1d1d");
49030
- const lowerWasher = washer(washerSize, { segments }).translate(0, 0, lowerWasherZ).color("#94a3b8");
49031
- const upperWasher = washer(washerSize, { segments }).translate(0, 0, upperWasherZ).color("#94a3b8");
49032
- const shaft = cylinder(stackHeight, pinDiameter / 2, void 0, segments);
49033
- const lowerRetainer = cylinder(pinHeadThickness, pinHeadRadius, void 0, segments).translate(
49034
- 0,
49035
- 0,
49036
- -pinHeadThickness - runningClearance
49037
- );
49038
- const upperHead = cylinder(pinHeadThickness, pinHeadRadius, void 0, segments).translate(0, 0, stackHeight + runningClearance);
49039
- const pin = union(shaft, lowerRetainer, upperHead).color("#cbd5e1");
49040
- const pivotBore = cylinder(stackHeight + 1, boreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
49041
- const parts = [
49042
- { name: "pivot support block with bearing bore and low stop land", shape: support },
49043
- { name: "lower thrust washer under pinned lever", shape: lowerWasher },
49044
- { name: "fused pinned lever with hub arm grip and detent boss", shape: lever },
49045
- { name: "upper thrust washer over pinned lever", shape: upperWasher },
49046
- { name: "retained pivot pin through lever stack", shape: pin }
49047
- ];
49048
- return {
49049
- parts,
49050
- support,
49051
- lever,
49052
- pin,
49053
- washers: {
49054
- lower: lowerWasher,
49055
- upper: upperWasher
49056
- },
49057
- cutters: {
49058
- pivotBore
49059
- },
49060
- dims: {
49061
- armLength,
49062
- armWidth,
49063
- leverThickness,
49064
- hubRadius,
49065
- pinDiameter,
49066
- boreDiameter,
49067
- supportWidth,
49068
- supportDepth,
49069
- supportThickness,
49070
- washerSize,
49071
- washerThickness: washerDims.t,
49072
- stackHeight
49073
- }
49074
- };
49075
- }
49076
- function retainedShaftAssembly(options) {
49077
- const supportSpacing = requirePositive$6(options.supportSpacing, "supportSpacing");
49078
- const shaftDiameter = requirePositive$6(options.shaftDiameter ?? 8, "shaftDiameter");
49079
- const boreClearance = requireNonNegative(options.boreClearance ?? 0.35, "boreClearance");
49080
- const boreDiameter = shaftDiameter + boreClearance;
49081
- const supportThickness = requirePositive$6(options.supportThickness ?? Math.max(5, shaftDiameter * 0.75), "supportThickness");
49082
- const washerSize = options.washerSize ?? metricWasherSizeForPin(shaftDiameter);
49083
- const washerDims = WASHER_TABLE[washerSize];
49084
- if (!washerDims) throw new Error(`retainedShaftAssembly: unsupported washerSize "${washerSize}"`);
49085
- if (washerDims.id <= shaftDiameter) {
49086
- throw new Error(`retainedShaftAssembly: ${washerSize} washer inner diameter is too small for a ${shaftDiameter} mm shaft`);
49087
- }
49088
- const knobDiameter = requirePositive$6(options.knobDiameter ?? shaftDiameter * 3, "knobDiameter");
49089
- const knobThickness = requirePositive$6(options.knobThickness ?? Math.max(8, shaftDiameter), "knobThickness");
49090
- const retainerThickness = requirePositive$6(options.retainerThickness ?? Math.max(washerDims.t, shaftDiameter * 0.35), "retainerThickness");
49091
- const runningClearance = requireNonNegative(options.runningClearance ?? 0.05, "runningClearance");
49092
- const supportWidth = requirePositive$6(options.supportWidth ?? Math.max(28, knobDiameter * 1.25), "supportWidth");
49093
- const supportHeight = requirePositive$6(options.supportHeight ?? Math.max(34, knobDiameter * 1.45), "supportHeight");
49094
- const segments = options.segments ?? 40;
49095
- if (supportSpacing <= supportThickness) {
49096
- throw new Error("retainedShaftAssembly: supportSpacing must leave a gap between support cheeks");
49097
- }
49098
- if (supportWidth <= boreDiameter + 4 || supportHeight <= boreDiameter + 4) {
49099
- throw new Error("retainedShaftAssembly: support dimensions leave too little material around the shaft bore");
49100
- }
49101
- const leftSupportX = -supportSpacing / 2;
49102
- const rightSupportX = supportSpacing / 2;
49103
- const leftOuterFaceX = leftSupportX - supportThickness / 2;
49104
- const rightOuterFaceX = rightSupportX + supportThickness / 2;
49105
- const leftWasherX = leftOuterFaceX - runningClearance - washerDims.t / 2;
49106
- const rightWasherX = rightOuterFaceX + runningClearance + washerDims.t / 2;
49107
- const leftKnobX = leftOuterFaceX - runningClearance * 2 - washerDims.t - knobThickness / 2;
49108
- const rightKnobX = rightOuterFaceX + runningClearance * 2 + washerDims.t + knobThickness / 2;
49109
- const leftStackOuterX = leftKnobX - knobThickness / 2;
49110
- const rightStackOuterX = rightKnobX + knobThickness / 2;
49111
- const minimumShaftLength = rightStackOuterX - leftStackOuterX + retainerThickness * 2 + runningClearance * 2;
49112
- const shaftLength = requirePositive$6(options.shaftLength ?? minimumShaftLength, "shaftLength");
49113
- if (shaftLength < minimumShaftLength) {
49114
- throw new Error("retainedShaftAssembly: shaftLength is too short to retain both supports, washers, and knobs");
49115
- }
49116
- const supportBore = cylinderAlongX(supportThickness + 1, boreDiameter / 2, 0, segments);
49117
- const makeSupport = (x2) => box(supportThickness, supportWidth, supportHeight).translate(x2, 0, -supportHeight / 2).subtract(supportBore.translate(x2, 0, 0)).color("#334155");
49118
- const knobBore = cylinder(knobThickness + 1, boreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
49119
- const makeKnob = (x2) => cylinder(knobThickness, knobDiameter / 2, void 0, 18).subtract(knobBore).pointAlong([1, 0, 0]).translate(x2 - knobThickness / 2, 0, 0).color("#111827");
49120
- const retainerRadius = Math.max(shaftDiameter * 0.85, knobDiameter * 0.36);
49121
- const shaftCore = cylinderAlongX(shaftLength, shaftDiameter / 2, 0, segments);
49122
- const leftRetainer = cylinderAlongX(retainerThickness, retainerRadius, -shaftLength / 2 + retainerThickness / 2, segments);
49123
- const rightRetainer = cylinderAlongX(retainerThickness, retainerRadius, shaftLength / 2 - retainerThickness / 2, segments);
49124
- const shaft = union(shaftCore, leftRetainer, rightRetainer).color("#cbd5e1");
49125
- const leftSupport = makeSupport(leftSupportX);
49126
- const rightSupport = makeSupport(rightSupportX);
49127
- const leftWasher = washerAlongX(washerSize, leftWasherX, segments).color("#94a3b8");
49128
- const rightWasher = washerAlongX(washerSize, rightWasherX, segments).color("#94a3b8");
49129
- const leftKnob = makeKnob(leftKnobX);
49130
- const rightKnob = makeKnob(rightKnobX);
49131
- const shaftBore = cylinderAlongX(supportThickness + knobThickness + 2, boreDiameter / 2, 0, segments);
49132
- const parts = [
49133
- { name: "left bored support cheek for retained shaft", shape: leftSupport },
49134
- { name: "right bored support cheek for retained shaft", shape: rightSupport },
49135
- { name: "retained through shaft with end heads", shape: shaft },
49136
- { name: `left ${washerSize} thrust washer on shaft`, shape: leftWasher },
49137
- { name: `right ${washerSize} thrust washer on shaft`, shape: rightWasher },
49138
- { name: "left retained hand knob with shaft bore", shape: leftKnob },
49139
- { name: "right retained hand knob with shaft bore", shape: rightKnob }
49140
- ];
49141
- return {
49142
- parts,
49143
- supports: {
49144
- left: leftSupport,
49145
- right: rightSupport
49146
- },
49147
- shaft,
49148
- washers: {
49149
- left: leftWasher,
49150
- right: rightWasher
49151
- },
49152
- knobs: {
49153
- left: leftKnob,
49154
- right: rightKnob
49155
- },
49156
- cutters: {
49157
- shaftBore
49158
- },
49159
- dims: {
49160
- supportSpacing,
49161
- supportThickness,
49162
- supportWidth,
49163
- supportHeight,
49164
- shaftDiameter,
49165
- shaftLength,
49166
- boreDiameter,
49167
- washerSize,
49168
- washerThickness: washerDims.t,
49169
- knobDiameter,
49170
- knobThickness,
49171
- retainerThickness,
49172
- runningClearance
49173
- }
49174
- };
49175
- }
49176
- function capturedLinearSlide(options) {
49177
- const length4 = requirePositive$6(options.length, "length");
49178
- const railWidth = requirePositive$6(options.railWidth ?? 38, "railWidth");
49179
- const baseThickness = requirePositive$6(options.baseThickness ?? 2.4, "baseThickness");
49180
- const wallThickness = requirePositive$6(options.wallThickness ?? 2, "wallThickness");
49181
- const wallHeight = requirePositive$6(options.wallHeight ?? 9, "wallHeight");
49182
- const lipWidth = requirePositive$6(options.lipWidth ?? 4, "lipWidth");
49183
- const lipThickness = requirePositive$6(options.lipThickness ?? 1.8, "lipThickness");
49184
- const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
49185
- const endStopLength = requirePositive$6(options.endStopLength ?? 6, "endStopLength");
49186
- const carriageLength = requirePositive$6(options.carriageLength ?? length4 * 0.32, "carriageLength");
49187
- const innerWidth = railWidth - wallThickness * 2;
49188
- const throatWidth = innerWidth - lipWidth * 2;
49189
- if (innerWidth <= 0) throw new Error("capturedLinearSlide: wallThickness leaves no inner rail width");
49190
- if (throatWidth <= 0) throw new Error("capturedLinearSlide: lipWidth closes the rail throat");
49191
- const carriageWidth = requirePositive$6(options.carriageWidth ?? innerWidth - runningClearance * 2, "carriageWidth");
49192
- const carriageThickness = requirePositive$6(options.carriageThickness ?? 4, "carriageThickness");
49193
- if (carriageWidth >= innerWidth - runningClearance) {
49194
- throw new Error("capturedLinearSlide: carriageWidth leaves too little side clearance inside the rail");
49195
- }
49196
- if (carriageWidth <= throatWidth + runningClearance) {
49197
- throw new Error("capturedLinearSlide: carriageWidth must be wider than the lip throat so the rail actually captures it");
49198
- }
49199
- if (carriageThickness + runningClearance * 2 >= wallHeight) {
49200
- throw new Error("capturedLinearSlide: carriage is too tall to clear the return lips");
49201
- }
49202
- const maxTravel = length4 - endStopLength * 2 - carriageLength;
49203
- if (maxTravel <= 0) {
49204
- throw new Error("capturedLinearSlide: rail length, end stops, and carriage length leave no travel");
49205
- }
49206
- const travel = options.travel ?? maxTravel / 2;
49207
- if (!Number.isFinite(travel) || travel < 0 || travel > maxTravel) {
49208
- throw new Error(`capturedLinearSlide: travel must be between 0 and ${maxTravel}`);
49209
- }
49210
- const carriageCenterX = -maxTravel / 2 + travel;
49211
- const fuseOverlap = Math.min(0.04, runningClearance * 0.1);
49212
- const sideY = railWidth / 2 - wallThickness / 2;
49213
- const lipY = railWidth / 2 - wallThickness - lipWidth / 2 + fuseOverlap / 2;
49214
- const stopZ = baseThickness - fuseOverlap;
49215
- const rail2 = union(
49216
- box(length4, railWidth, baseThickness),
49217
- box(length4, wallThickness, wallHeight + fuseOverlap).translate(0, sideY, baseThickness - fuseOverlap),
49218
- box(length4, wallThickness, wallHeight + fuseOverlap).translate(0, -sideY, baseThickness - fuseOverlap),
49219
- box(length4, lipWidth, lipThickness + fuseOverlap).translate(0, lipY, baseThickness + wallHeight - fuseOverlap),
49220
- box(length4, lipWidth, lipThickness + fuseOverlap).translate(0, -lipY, baseThickness + wallHeight - fuseOverlap),
49221
- box(endStopLength, throatWidth, carriageThickness + fuseOverlap).translate(-length4 / 2 + endStopLength / 2, 0, stopZ),
49222
- box(endStopLength, throatWidth, carriageThickness + fuseOverlap).translate(length4 / 2 - endStopLength / 2, 0, stopZ)
49223
- ).color("#475569");
49224
- const carriage = union(
49225
- box(carriageLength, carriageWidth, carriageThickness),
49226
- box(carriageLength * 0.78, throatWidth - runningClearance * 2, Math.max(1, carriageThickness * 0.38)).translate(
49227
- 0,
49228
- 0,
49229
- carriageThickness
49230
- )
49231
- ).translate(carriageCenterX, 0, baseThickness + runningClearance).color("#111827");
49232
- const parts = [
49233
- { name: "captured linear rail with return lips and end stops", shape: rail2 },
49234
- { name: "sliding carriage captured under rail lips", shape: carriage }
49235
- ];
49236
- return {
49237
- parts,
49238
- rail: rail2,
49239
- carriage,
49240
- dims: {
49241
- length: length4,
49242
- railWidth,
49243
- innerWidth,
49244
- throatWidth,
49245
- baseThickness,
49246
- wallThickness,
49247
- wallHeight,
49248
- lipWidth,
49249
- lipThickness,
49250
- carriageLength,
49251
- carriageWidth,
49252
- carriageThickness,
49253
- endStopLength,
49254
- runningClearance,
49255
- maxTravel,
49256
- travel,
49257
- carriageCenterX
49258
- }
49259
- };
49260
- }
49261
- function capturedCartridgeGuideAssembly(options) {
49262
- const length4 = requirePositive$6(options.length, "length");
49263
- const guideWidth = requirePositive$6(options.guideWidth ?? 42, "guideWidth");
49264
- const baseThickness = requirePositive$6(options.baseThickness ?? 3, "baseThickness");
49265
- const wallThickness = requirePositive$6(options.wallThickness ?? 2.5, "wallThickness");
49266
- const wallHeight = requirePositive$6(options.wallHeight ?? 12, "wallHeight");
49267
- const lipWidth = requirePositive$6(options.lipWidth ?? 4, "lipWidth");
49268
- const lipThickness = requirePositive$6(options.lipThickness ?? 2, "lipThickness");
49269
- const rearStopLength = requirePositive$6(options.rearStopLength ?? 7, "rearStopLength");
49270
- const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
49271
- const cartridgeLength = requirePositive$6(options.cartridgeLength ?? length4 * 0.58, "cartridgeLength");
49272
- const cartridgeHeight = requirePositive$6(options.cartridgeHeight ?? 10, "cartridgeHeight");
49273
- const flangeThickness = requirePositive$6(options.flangeThickness ?? 3, "flangeThickness");
49274
- const pullTabLength = requirePositive$6(options.pullTabLength ?? 10, "pullTabLength");
49275
- const innerWidth = guideWidth - wallThickness * 2;
49276
- const throatWidth = innerWidth - lipWidth * 2;
49277
- if (innerWidth <= 0) throw new Error("capturedCartridgeGuideAssembly: wallThickness leaves no inner guide width");
49278
- if (throatWidth <= 0) throw new Error("capturedCartridgeGuideAssembly: lipWidth closes the guide throat");
49279
- if (wallHeight <= lipThickness + flangeThickness + runningClearance * 2) {
49280
- throw new Error("capturedCartridgeGuideAssembly: wallHeight leaves too little vertical capture clearance");
49281
- }
49282
- const cartridgeWidth = requirePositive$6(options.cartridgeWidth ?? innerWidth - runningClearance * 2, "cartridgeWidth");
49283
- const cartridgeBodyWidth = throatWidth - runningClearance * 2;
49284
- if (cartridgeBodyWidth <= 0) {
49285
- throw new Error("capturedCartridgeGuideAssembly: throatWidth and runningClearance leave no cartridge body width");
49286
- }
49287
- if (cartridgeWidth >= innerWidth - runningClearance) {
49288
- throw new Error("capturedCartridgeGuideAssembly: cartridgeWidth leaves too little side clearance inside the guide");
49289
- }
49290
- if (cartridgeWidth <= throatWidth + runningClearance) {
49291
- throw new Error("capturedCartridgeGuideAssembly: cartridge flange must be wider than the guide throat so the cartridge is captured");
49292
- }
49293
- const maxInsertion = length4 - rearStopLength - cartridgeLength;
49294
- if (maxInsertion <= 0) {
49295
- throw new Error("capturedCartridgeGuideAssembly: length, rearStopLength, and cartridgeLength leave no insertion travel");
49296
- }
49297
- const insertion = options.insertion ?? maxInsertion * 0.4;
49298
- if (!Number.isFinite(insertion) || insertion < 0 || insertion > maxInsertion) {
49299
- throw new Error(`capturedCartridgeGuideAssembly: insertion must be between 0 and ${maxInsertion}`);
49300
- }
49301
- const cartridgeCenterX = -length4 / 2 + cartridgeLength / 2 + insertion;
49302
- const fuseOverlap = Math.min(0.04, runningClearance * 0.1);
49303
- const sideY = guideWidth / 2 - wallThickness / 2;
49304
- const lipY = guideWidth / 2 - wallThickness - lipWidth / 2 + fuseOverlap / 2;
49305
- const guide = union(
49306
- box(length4, guideWidth, baseThickness),
49307
- box(length4, wallThickness, wallHeight + fuseOverlap).translate(0, sideY, baseThickness - fuseOverlap),
49308
- box(length4, wallThickness, wallHeight + fuseOverlap).translate(0, -sideY, baseThickness - fuseOverlap),
49309
- box(length4, lipWidth, lipThickness + fuseOverlap).translate(0, lipY, baseThickness + wallHeight - fuseOverlap),
49310
- box(length4, lipWidth, lipThickness + fuseOverlap).translate(0, -lipY, baseThickness + wallHeight - fuseOverlap),
49311
- box(rearStopLength, throatWidth, Math.max(flangeThickness + runningClearance, 4)).translate(
49312
- length4 / 2 - rearStopLength / 2,
49313
- 0,
49314
- baseThickness - fuseOverlap
49315
- )
49316
- ).color("#475569");
49317
- const flangeZ = baseThickness + runningClearance;
49318
- const bodyHeight = Math.max(1, cartridgeHeight - flangeThickness);
49319
- const bodyZ = flangeZ + flangeThickness;
49320
- const tabOverlap = Math.min(0.6, pullTabLength * 0.15);
49321
- const pullTabX = cartridgeCenterX - cartridgeLength / 2 - pullTabLength / 2 + tabOverlap;
49322
- const pullTabWidth = Math.max(cartridgeBodyWidth * 0.55, 12);
49323
- const cartridge = union(
49324
- box(cartridgeLength, cartridgeWidth, flangeThickness).translate(cartridgeCenterX, 0, flangeZ),
49325
- box(cartridgeLength * 0.88, cartridgeBodyWidth, bodyHeight).translate(cartridgeCenterX, 0, bodyZ),
49326
- box(pullTabLength, pullTabWidth, Math.max(flangeThickness, 3)).translate(pullTabX, 0, flangeZ)
49327
- ).color("#111827");
49328
- const parts = [
49329
- { name: "captured cartridge guide with return lips and rear stop", shape: guide },
49330
- { name: "removable cartridge with captured flange and pull tab", shape: cartridge }
49331
- ];
49332
- return {
49333
- parts,
49334
- guide,
49335
- cartridge,
49336
- dims: {
49337
- length: length4,
49338
- guideWidth,
49339
- innerWidth,
49340
- throatWidth,
49341
- baseThickness,
49342
- wallThickness,
49343
- wallHeight,
49344
- lipWidth,
49345
- lipThickness,
49346
- rearStopLength,
49347
- cartridgeLength,
49348
- cartridgeWidth,
49349
- cartridgeBodyWidth,
49350
- cartridgeHeight,
49351
- flangeThickness,
49352
- pullTabLength,
49353
- runningClearance,
49354
- maxInsertion,
49355
- insertion,
49356
- cartridgeCenterX
49357
- }
49358
- };
49359
- }
49360
- function livingHingeCoverAssembly(options) {
49361
- const width = requirePositive$6(options.width, "width");
49362
- const coverDepth = requirePositive$6(options.coverDepth ?? 42, "coverDepth");
49363
- const fixedLeafDepth = requirePositive$6(options.fixedLeafDepth ?? 18, "fixedLeafDepth");
49364
- const leafThickness = requirePositive$6(options.leafThickness ?? 2, "leafThickness");
49365
- const hingeWebWidth = requirePositive$6(options.hingeWebWidth ?? 3.2, "hingeWebWidth");
49366
- const hingeWebThickness = requirePositive$6(options.hingeWebThickness ?? 0.45, "hingeWebThickness");
49367
- const pullLipDepth = requirePositive$6(options.pullLipDepth ?? 5, "pullLipDepth");
49368
- const snapBarbWidth = requirePositive$6(options.snapBarbWidth ?? width * 0.35, "snapBarbWidth");
49369
- const snapBarbDepth = requirePositive$6(options.snapBarbDepth ?? 2.4, "snapBarbDepth");
49370
- const snapBarbHeight = requirePositive$6(options.snapBarbHeight ?? 1.4, "snapBarbHeight");
49371
- const catchLandDepth = requirePositive$6(options.catchLandDepth ?? 2.4, "catchLandDepth");
49372
- if (hingeWebThickness >= leafThickness * 0.55) {
49373
- throw new Error("livingHingeCoverAssembly: hingeWebThickness must be much thinner than the rigid leaves");
49374
- }
49375
- if (hingeWebWidth >= Math.min(coverDepth, fixedLeafDepth) * 0.45) {
49376
- throw new Error("livingHingeCoverAssembly: hingeWebWidth is too wide for the selected leaves");
49377
- }
49378
- if (snapBarbWidth >= width - 2) {
49379
- throw new Error("livingHingeCoverAssembly: snapBarbWidth must leave side material on the cover leaf");
49380
- }
49381
- const fuseOverlap = Math.min(0.04, hingeWebWidth * 0.02);
49382
- const fixedCenterY = -hingeWebWidth / 2 - fixedLeafDepth / 2 + fuseOverlap / 2;
49383
- const coverCenterY = hingeWebWidth / 2 + coverDepth / 2 - fuseOverlap / 2;
49384
- const fixedLeaf = box(width, fixedLeafDepth + fuseOverlap, leafThickness).translate(0, fixedCenterY, 0);
49385
- const movingLeaf = box(width, coverDepth + fuseOverlap, leafThickness).translate(0, coverCenterY, 0);
49386
- const hingeWeb = box(width, hingeWebWidth + fuseOverlap * 2, hingeWebThickness).translate(0, 0, 0);
49387
- const pullLip = box(width * 0.92, pullLipDepth, leafThickness).translate(
49388
- 0,
49389
- coverCenterY + coverDepth / 2 + pullLipDepth / 2 - fuseOverlap,
49390
- 0
49391
- );
49392
- const snapBarb = box(snapBarbWidth, snapBarbDepth, snapBarbHeight).translate(
49393
- 0,
49394
- coverCenterY + coverDepth / 2 - snapBarbDepth / 2,
49395
- leafThickness
49396
- );
49397
- const catchLand = box(width * 0.55, catchLandDepth, Math.max(0.8, leafThickness * 0.45)).translate(
49398
- 0,
49399
- fixedCenterY - fixedLeafDepth / 2 + catchLandDepth / 2,
49400
- leafThickness
49401
- );
49402
- const cover = union(fixedLeaf, movingLeaf, hingeWeb, pullLip, snapBarb, catchLand).color("#0f766e");
49403
- const overallDepth = fixedLeafDepth + hingeWebWidth + coverDepth + pullLipDepth;
49404
- const flexRatio = leafThickness / hingeWebThickness;
49405
- return {
49406
- parts: [{ name: "one-piece molded living hinge cover with snap barb", shape: cover }],
49407
- cover,
49408
- fixedLeaf,
49409
- movingLeaf,
49410
- hingeWeb,
49411
- snapBarb,
49412
- catchLand,
49413
- dims: {
49414
- width,
49415
- coverDepth,
49416
- fixedLeafDepth,
49417
- leafThickness,
49418
- hingeWebWidth,
49419
- hingeWebThickness,
49420
- pullLipDepth,
49421
- snapBarbWidth,
49422
- snapBarbDepth,
49423
- snapBarbHeight,
49424
- catchLandDepth,
49425
- flexRatio,
49426
- overallDepth
49427
- }
49428
- };
49429
- }
49430
- function knuckledHingeAssembly(options) {
49431
- const length4 = requirePositive$6(options.length, "length");
49432
- const leafLength = requirePositive$6(options.leafLength ?? 36, "leafLength");
49433
- const leafThickness = requirePositive$6(options.leafThickness ?? 1.6, "leafThickness");
49434
- const barrelOuterRadius = requirePositive$6(options.barrelOuterRadius ?? 3, "barrelOuterRadius");
49435
- const pinDiameter = requirePositive$6(options.pinDiameter ?? 2, "pinDiameter");
49436
- const pinClearance = requireNonNegative(options.pinClearance ?? 0.25, "pinClearance");
49437
- const boreDiameter = pinDiameter + pinClearance;
49438
- const knuckleGap = requireNonNegative(options.knuckleGap ?? 0.45, "knuckleGap");
49439
- const openAngleDeg = Number.isFinite(options.openAngleDeg ?? 35) ? options.openAngleDeg ?? 35 : 35;
49440
- const retainerThickness = requirePositive$6(options.retainerThickness ?? Math.max(leafThickness, pinDiameter * 0.7), "retainerThickness");
49441
- const segments = options.segments ?? 36;
49442
- const knuckleCount = options.knuckleCount ?? 5;
49443
- if (!Number.isInteger(knuckleCount) || knuckleCount < 3 || knuckleCount % 2 === 0) {
49444
- throw new Error("knuckledHingeAssembly: knuckleCount must be an odd integer >= 3");
49445
- }
49446
- if (barrelOuterRadius <= boreDiameter / 2 + Math.max(0.35, pinDiameter * 0.18)) {
49447
- throw new Error("knuckledHingeAssembly: barrelOuterRadius leaves too little wall around the pin bore");
49448
- }
49449
- const knuckleLength = (length4 - knuckleGap * (knuckleCount - 1)) / knuckleCount;
49450
- if (knuckleLength <= pinDiameter * 1.4) {
49451
- throw new Error("knuckledHingeAssembly: length, knuckleCount, and knuckleGap make knuckles too short");
49452
- }
49453
- const leafRootClearance = Math.max(0.12, Math.min(knuckleGap * 0.35, 0.35));
49454
- const barrelLeafOverlap = Math.min(barrelOuterRadius * 0.18, leafThickness * 0.35);
49455
- const bridgeDepth = leafRootClearance + barrelLeafOverlap + 0.2;
49456
- const fixedLeafPlate = box(length4, leafLength, leafThickness).translate(
49457
- 0,
49458
- barrelOuterRadius + leafRootClearance + leafLength / 2,
49459
- -leafThickness / 2
49460
- );
49461
- const movingLeafPlate = box(length4, leafLength, leafThickness).translate(
49462
- 0,
49463
- -barrelOuterRadius - leafRootClearance - leafLength / 2,
49464
- -leafThickness / 2
49465
- );
49466
- const fixedKnuckles = [];
49467
- const movingKnuckles = [];
49468
- const fixedBridges = [];
49469
- const movingBridges = [];
49470
- for (let index2 = 0; index2 < knuckleCount; index2 += 1) {
49471
- const xStart = -length4 / 2 + index2 * (knuckleLength + knuckleGap);
49472
- const xCenter = xStart + knuckleLength / 2;
49473
- const knuckle = tubeAlongX(knuckleLength, barrelOuterRadius, boreDiameter / 2, xCenter, segments);
49474
- if (index2 % 2 === 0) {
49475
- fixedKnuckles.push(knuckle);
49476
- fixedBridges.push(
49477
- box(knuckleLength, bridgeDepth, leafThickness).translate(
49478
- xCenter,
49479
- barrelOuterRadius - barrelLeafOverlap + bridgeDepth / 2,
49480
- -leafThickness / 2
49481
- )
49482
- );
49483
- } else {
49484
- movingKnuckles.push(knuckle);
49485
- movingBridges.push(
49486
- box(knuckleLength, bridgeDepth, leafThickness).translate(
49487
- xCenter,
49488
- -barrelOuterRadius + barrelLeafOverlap - bridgeDepth / 2,
49489
- -leafThickness / 2
49490
- )
49491
- );
49492
- }
49493
- }
49494
- const fixedLeaf = union(fixedLeafPlate, ...fixedKnuckles, ...fixedBridges).color("#475569");
49495
- const movingLeaf = union(movingLeafPlate, ...movingKnuckles, ...movingBridges).rotateX(openAngleDeg).color("#111827");
49496
- const pinCore = cylinderAlongX(length4 + retainerThickness * 2, pinDiameter / 2, 0, segments);
49497
- const retainerRadius = Math.max(barrelOuterRadius * 0.85, pinDiameter);
49498
- const leftHead = cylinderAlongX(retainerThickness, retainerRadius, -length4 / 2 - retainerThickness / 2, segments);
49499
- const rightHead = cylinderAlongX(retainerThickness, retainerRadius, length4 / 2 + retainerThickness / 2, segments);
49500
- const pin = union(pinCore, leftHead, rightHead).color("#cbd5e1");
49501
- const pinBore = cylinderAlongX(length4 + retainerThickness * 2, boreDiameter / 2, 0, segments);
49502
- const parts = [
49503
- { name: "fixed hinge leaf with alternating knuckles", shape: fixedLeaf },
49504
- { name: "moving hinge leaf with alternating knuckles", shape: movingLeaf },
49505
- { name: "retained hinge pin through knuckle stack", shape: pin }
49506
- ];
49507
- return {
49508
- parts,
49509
- fixedLeaf,
49510
- movingLeaf,
49511
- pin,
49512
- cutters: {
49513
- pinBore
49514
- },
49515
- dims: {
49516
- length: length4,
49517
- leafLength,
49518
- leafThickness,
49519
- barrelOuterRadius,
49520
- pinDiameter,
49521
- boreDiameter,
49522
- knuckleGap,
49523
- knuckleCount,
49524
- knuckleLength,
49525
- openAngleDeg,
49526
- retainerThickness
49527
- }
49528
- };
49529
- }
49530
- function clevisPinJointAssembly(options = {}) {
49531
- const pinDiameter = requirePositive$6(options.pinDiameter ?? 4, "pinDiameter");
49532
- const pinClearance = requireNonNegative(options.pinClearance ?? 0.3, "pinClearance");
49533
- const boreDiameter = pinDiameter + pinClearance;
49534
- const linkThickness = requirePositive$6(options.linkThickness ?? Math.max(5, pinDiameter * 1.5), "linkThickness");
49535
- const earThickness = requirePositive$6(options.earThickness ?? Math.max(3.5, pinDiameter), "earThickness");
49536
- const runningClearance = requireNonNegative(options.runningClearance ?? 0.25, "runningClearance");
49537
- const linkArmWidth = requirePositive$6(options.linkArmWidth ?? pinDiameter * 2.4, "linkArmWidth");
49538
- const eyeOuterRadius = requirePositive$6(options.eyeOuterRadius ?? Math.max(pinDiameter * 1.8, linkArmWidth / 2 + 1.4), "eyeOuterRadius");
49539
- const earLength = requirePositive$6(options.earLength ?? Math.max(eyeOuterRadius * 2.55, pinDiameter * 4.2), "earLength");
49540
- const earHeight = requirePositive$6(options.earHeight ?? Math.max(eyeOuterRadius * 2.25, pinDiameter * 4.4), "earHeight");
49541
- const linkArmLength = requirePositive$6(options.linkArmLength ?? 34, "linkArmLength");
49542
- const retainerThickness = requirePositive$6(options.retainerThickness ?? Math.max(1.2, pinDiameter * 0.35), "retainerThickness");
49543
- const segments = options.segments ?? 40;
49544
- if (eyeOuterRadius <= boreDiameter / 2 + Math.max(0.8, pinDiameter * 0.25)) {
49545
- throw new Error("clevisPinJointAssembly: eyeOuterRadius leaves too little material around the pin bore");
49546
- }
49547
- if (earHeight <= boreDiameter + Math.max(3, pinDiameter)) {
49548
- throw new Error("clevisPinJointAssembly: earHeight leaves too little material around the pin bore");
49549
- }
49550
- if (earLength / 2 <= eyeOuterRadius + runningClearance) {
49551
- throw new Error("clevisPinJointAssembly: earLength must extend behind the link eye for a rear clevis bridge");
49552
- }
49553
- const clevisGap = linkThickness + runningClearance * 2;
49554
- const earCenterY = clevisGap / 2 + earThickness / 2;
49555
- const totalStackY = clevisGap + earThickness * 2;
49556
- const pinLength = totalStackY + retainerThickness * 2 + runningClearance * 2;
49557
- const bridgeClearX = -eyeOuterRadius - runningClearance;
49558
- const bridgeLength = Math.max(pinDiameter * 2.2, 4);
49559
- const bridgeHeight = Math.min(earHeight * 0.48, Math.max(pinDiameter * 1.4, eyeOuterRadius * 0.75));
49560
- const bridgeCenterX = bridgeClearX - bridgeLength / 2;
49561
- const bridgeCenterZ = -earHeight / 2 + bridgeHeight / 2;
49562
- const pinBore = cylinderAlongY(totalStackY + 0.8, boreDiameter / 2, 0, segments);
49563
- const clevisBlank = union(
49564
- box(earLength, earThickness, earHeight).translate(0, earCenterY, -earHeight / 2),
49565
- box(earLength, earThickness, earHeight).translate(0, -earCenterY, -earHeight / 2),
49566
- box(bridgeLength, totalStackY, bridgeHeight).translate(bridgeCenterX, 0, bridgeCenterZ)
49567
- );
49568
- const clevis = clevisBlank.subtract(pinBore).color("#475569");
49569
- const eye = tubeAlongY(linkThickness, eyeOuterRadius, boreDiameter / 2, 0, segments);
49570
- const armOverlap = Math.min(eyeOuterRadius * 0.65, linkArmLength * 0.25);
49571
- const armCenterX = eyeOuterRadius - armOverlap + linkArmLength / 2;
49572
- const linkArm = box(linkArmLength, linkThickness, linkArmWidth).translate(armCenterX, 0, -linkArmWidth / 2);
49573
- const link = union(eye, linkArm).color("#111827");
49574
- const pinCore = cylinderAlongY(pinLength, pinDiameter / 2, 0, segments);
49575
- const headRadius = Math.max(pinDiameter * 0.9, boreDiameter / 2 + 0.8);
49576
- const headY = totalStackY / 2 + runningClearance + retainerThickness / 2;
49577
- const headA = cylinderAlongY(retainerThickness, headRadius, headY, segments);
49578
- const headB = cylinderAlongY(retainerThickness, headRadius, -headY, segments);
49579
- const pin = union(pinCore, headA, headB).color("#cbd5e1");
49580
- const cutter = cylinderAlongY(pinLength + 1, boreDiameter / 2, 0, segments);
49581
- const parts = [
49582
- { name: "bored clevis yoke with rear bridge", shape: clevis },
49583
- { name: "center link eye captured in clevis", shape: link },
49584
- { name: "retained clevis pin through link eye", shape: pin }
49585
- ];
49586
- return {
49587
- parts,
49588
- clevis,
49589
- link,
49590
- pin,
49591
- cutters: {
49592
- pinBore: cutter
49593
- },
49594
- dims: {
49595
- pinDiameter,
49596
- boreDiameter,
49597
- linkThickness,
49598
- earThickness,
49599
- runningClearance,
49600
- earLength,
49601
- earHeight,
49602
- linkArmLength,
49603
- linkArmWidth,
49604
- eyeOuterRadius,
49605
- retainerThickness,
49606
- pinLength,
49607
- clevisGap
49608
- }
49609
- };
49610
- }
49611
- function seatedBearingAssembly(options) {
49612
- const bearingOuterDiameter = requirePositive$6(options.bearingOuterDiameter, "bearingOuterDiameter");
49613
- const bearingInnerDiameter = requirePositive$6(options.bearingInnerDiameter, "bearingInnerDiameter");
49614
- const bearingWidth = requirePositive$6(options.bearingWidth, "bearingWidth");
49615
- const shaftDiameter = requirePositive$6(options.shaftDiameter ?? Math.max(1, bearingInnerDiameter - 0.4), "shaftDiameter");
49616
- const pocketClearance = requireNonNegative(options.pocketClearance ?? 0.2, "pocketClearance");
49617
- const shaftClearance = requireNonNegative(options.shaftClearance ?? 0.35, "shaftClearance");
49618
- const runningClearance = requireNonNegative(options.runningClearance ?? 0.05, "runningClearance");
49619
- const housingThickness = requirePositive$6(options.housingThickness ?? bearingWidth + 5, "housingThickness");
49620
- const bossHeight = requirePositive$6(options.bossHeight ?? Math.max(2, bearingWidth * 0.45), "bossHeight");
49621
- const bossOuterDiameter = requirePositive$6(
49622
- options.bossOuterDiameter ?? bearingOuterDiameter + Math.max(8, bearingOuterDiameter * 0.36),
49623
- "bossOuterDiameter"
49624
- );
49625
- const housingWidth = requirePositive$6(
49626
- options.housingWidth ?? Math.max(bossOuterDiameter + 12, bearingOuterDiameter * 2.1),
49627
- "housingWidth"
49628
- );
49629
- const housingDepth = requirePositive$6(
49630
- options.housingDepth ?? Math.max(bossOuterDiameter + 12, bearingOuterDiameter * 1.8),
49631
- "housingDepth"
49632
- );
49633
- const shaftOverhang = requirePositive$6(options.shaftOverhang ?? Math.max(8, bearingOuterDiameter * 0.45), "shaftOverhang");
49634
- const shoulderDiameter = requirePositive$6(
49635
- options.shoulderDiameter ?? Math.max(shaftDiameter * 1.65, bearingInnerDiameter + 2),
49636
- "shoulderDiameter"
49637
- );
49638
- const shoulderThickness = requirePositive$6(options.shoulderThickness ?? Math.max(1.5, shaftDiameter * 0.32), "shoulderThickness");
49639
- const segments = options.segments ?? 48;
49640
- if (bearingOuterDiameter <= bearingInnerDiameter + Math.max(1, bearingOuterDiameter * 0.08)) {
49641
- throw new Error("seatedBearingAssembly: bearingOuterDiameter leaves too little bearing wall around the bore");
49642
- }
49643
- if (shaftDiameter + shaftClearance >= bearingInnerDiameter) {
49644
- throw new Error("seatedBearingAssembly: shaftDiameter plus shaftClearance must fit inside the bearing bore");
49645
- }
49646
- if (shoulderDiameter >= bearingOuterDiameter - runningClearance * 2) {
49647
- throw new Error("seatedBearingAssembly: shoulderDiameter must stay smaller than the bearing outer race");
49648
- }
49649
- const pocketDiameter = bearingOuterDiameter + pocketClearance;
49650
- const shaftBoreDiameter = shaftDiameter + shaftClearance;
49651
- const totalHousingHeight = housingThickness + bossHeight;
49652
- const pocketDepth = bearingWidth + runningClearance * 2;
49653
- if (pocketDepth >= totalHousingHeight - runningClearance) {
49654
- throw new Error("seatedBearingAssembly: housingThickness and bossHeight must leave a shoulder below the bearing pocket");
49655
- }
49656
- if (bossOuterDiameter <= pocketDiameter + Math.max(2, bearingOuterDiameter * 0.12)) {
49657
- throw new Error("seatedBearingAssembly: bossOuterDiameter leaves too little wall around the bearing pocket");
49658
- }
49659
- if (housingWidth <= pocketDiameter + 6 || housingDepth <= pocketDiameter + 6) {
49660
- throw new Error("seatedBearingAssembly: housing dimensions leave too little material around the bearing pocket");
49661
- }
49662
- if (shoulderThickness * 2 + runningClearance * 2 >= shaftOverhang) {
49663
- throw new Error("seatedBearingAssembly: shaftOverhang must leave room for retaining collars outside the housing");
49664
- }
49665
- const pocketBottomZ = totalHousingHeight - pocketDepth;
49666
- const bearingZ = pocketBottomZ + runningClearance;
49667
- const lowerShoulderZ = -runningClearance - shoulderThickness;
49668
- const upperShoulderZ = totalHousingHeight + runningClearance;
49669
- const shaftLength = totalHousingHeight + shaftOverhang * 2;
49670
- const bossFuseOverlap = Math.min(0.08, Math.max(0.02, bossHeight * 0.03));
49671
- const bearingPocket = cylinder(pocketDepth + 0.4, pocketDiameter / 2, void 0, segments).translate(0, 0, pocketBottomZ - 0.2);
49672
- const shaftBore = cylinder(totalHousingHeight + 1, shaftBoreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
49673
- const housingBase = box(housingWidth, housingDepth, housingThickness).subtract(bearingPocket).subtract(shaftBore);
49674
- const housingBoss = cylinder(bossHeight + bossFuseOverlap, bossOuterDiameter / 2, void 0, segments).translate(0, 0, housingThickness - bossFuseOverlap).subtract(bearingPocket);
49675
- const housing = union(housingBase, housingBoss).color("#475569");
49676
- const bearingRing = tubeAlongZ(bearingWidth, bearingOuterDiameter / 2, bearingInnerDiameter / 2, segments);
49677
- const shieldInset = Math.min(bearingWidth * 0.18, 0.7);
49678
- const shieldOuterRadius = bearingOuterDiameter / 2 - Math.max(0.45, (bearingOuterDiameter - bearingInnerDiameter) * 0.08);
49679
- const shieldInnerRadius = bearingInnerDiameter / 2 + Math.max(0.2, (bearingOuterDiameter - bearingInnerDiameter) * 0.035);
49680
- const bearingShield = shieldOuterRadius > shieldInnerRadius + 0.2 ? union(
49681
- tubeAlongZ(Math.min(0.35, bearingWidth * 0.08), shieldOuterRadius, shieldInnerRadius, segments).translate(0, 0, shieldInset),
49682
- tubeAlongZ(Math.min(0.35, bearingWidth * 0.08), shieldOuterRadius, shieldInnerRadius, segments).translate(
49683
- 0,
49684
- 0,
49685
- bearingWidth - shieldInset - Math.min(0.35, bearingWidth * 0.08)
49686
- )
49687
- ) : null;
49688
- const bearing = (bearingShield ? union(bearingRing, bearingShield) : bearingRing).translate(0, 0, bearingZ).color("#111827");
49689
- const shaftCore = cylinder(shaftLength, shaftDiameter / 2, void 0, segments).translate(0, 0, -shaftOverhang);
49690
- const lowerShoulder = cylinder(shoulderThickness, shoulderDiameter / 2, void 0, segments).translate(0, 0, lowerShoulderZ);
49691
- const upperShoulder = cylinder(shoulderThickness, shoulderDiameter / 2, void 0, segments).translate(0, 0, upperShoulderZ);
49692
- const shaft = union(shaftCore, lowerShoulder, upperShoulder).color("#cbd5e1");
49693
- const parts = [
49694
- { name: "bearing housing with counterbore pocket and shoulder", shape: housing },
49695
- { name: "purchased radial bearing seated in counterbore", shape: bearing },
49696
- { name: "shaft through bearing bore with retaining collars", shape: shaft }
49697
- ];
49698
- return {
49699
- parts,
49700
- housing,
49701
- bearing,
49702
- shaft,
49703
- cutters: {
49704
- bearingPocket,
49705
- shaftBore
49706
- },
49707
- dims: {
49708
- bearingOuterDiameter,
49709
- bearingInnerDiameter,
49710
- bearingWidth,
49711
- shaftDiameter,
49712
- housingWidth,
49713
- housingDepth,
49714
- housingThickness,
49715
- bossOuterDiameter,
49716
- bossHeight,
49717
- totalHousingHeight,
49718
- pocketDiameter,
49719
- pocketDepth,
49720
- shaftBoreDiameter,
49721
- runningClearance,
49722
- shaftLength,
49723
- shoulderDiameter,
49724
- shoulderThickness
49725
- }
49726
- };
49727
- }
49728
- function cableGlandAnchorAssembly(options) {
49729
- const cableDiameter = requirePositive$6(options.cableDiameter, "cableDiameter");
49730
- const panelThickness = requirePositive$6(options.panelThickness ?? 3, "panelThickness");
49731
- const panelWidth = requirePositive$6(options.panelWidth ?? Math.max(54, cableDiameter * 7), "panelWidth");
49732
- const panelHeight = requirePositive$6(options.panelHeight ?? Math.max(38, cableDiameter * 5), "panelHeight");
49733
- const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
49734
- const panelHoleClearance = requirePositive$6(options.panelHoleClearance ?? 0.25, "panelHoleClearance");
49735
- const cableBoreDiameter = cableDiameter + runningClearance * 2;
49736
- const glandOuterDiameter = requirePositive$6(
49737
- options.glandOuterDiameter ?? cableDiameter + Math.max(6, cableDiameter * 0.9),
49738
- "glandOuterDiameter"
49739
- );
49740
- const nutOuterDiameter = requirePositive$6(
49741
- options.nutOuterDiameter ?? glandOuterDiameter + Math.max(6, cableDiameter * 0.8),
49742
- "nutOuterDiameter"
49743
- );
49744
- const nutThickness = requirePositive$6(options.nutThickness ?? Math.max(4, cableDiameter * 0.8), "nutThickness");
49745
- const flangeDiameter = requirePositive$6(options.flangeDiameter ?? glandOuterDiameter + Math.max(5, cableDiameter * 0.7), "flangeDiameter");
49746
- const flangeThickness = requirePositive$6(options.flangeThickness ?? Math.max(2, panelThickness * 0.45), "flangeThickness");
49747
- const minGlandLength = panelThickness + nutThickness + flangeThickness + runningClearance * 4;
49748
- const glandLength = requirePositive$6(options.glandLength ?? minGlandLength + Math.max(8, cableDiameter), "glandLength");
49749
- const cableLength = requirePositive$6(options.cableLength ?? glandLength + Math.max(36, cableDiameter * 5), "cableLength");
49750
- const segments = options.segments ?? 40;
49751
- if (glandOuterDiameter <= cableBoreDiameter + Math.max(1.2, cableDiameter * 0.18)) {
49752
- throw new Error("cableGlandAnchorAssembly: glandOuterDiameter leaves too little wall around the cable bore");
49753
- }
49754
- if (nutOuterDiameter <= glandOuterDiameter + Math.max(1.5, cableDiameter * 0.2)) {
49755
- throw new Error("cableGlandAnchorAssembly: nutOuterDiameter must leave material around the gland body");
49756
- }
49757
- if (flangeDiameter <= glandOuterDiameter + Math.max(1.2, cableDiameter * 0.16)) {
49758
- throw new Error("cableGlandAnchorAssembly: flangeDiameter must be larger than the gland body");
49759
- }
49760
- if (panelWidth <= flangeDiameter + 8 || panelHeight <= flangeDiameter + 8) {
49761
- throw new Error("cableGlandAnchorAssembly: panel dimensions leave too little material around the gland hole");
49762
- }
49763
- if (glandLength <= minGlandLength) {
49764
- throw new Error("cableGlandAnchorAssembly: glandLength must span the panel, flange, compression nut, and clearances");
49765
- }
49766
- if (cableLength <= glandLength + runningClearance * 2) {
49767
- throw new Error("cableGlandAnchorAssembly: cableLength must extend beyond the gland body");
49768
- }
49769
- const panelHoleDiameter = glandOuterDiameter + panelHoleClearance * 2;
49770
- const glandOuterRadius = glandOuterDiameter / 2;
49771
- const cableBoreRadius = cableBoreDiameter / 2;
49772
- const faceClearance = Math.min(0.05, runningClearance * 0.15);
49773
- const flangePocketDepth = Math.min(Math.max(0.35, panelThickness * 0.18), panelThickness * 0.4, flangeThickness * 0.55);
49774
- const panelHole = cylinderAlongX(panelThickness + 0.8, panelHoleDiameter / 2, 0, segments);
49775
- const flangeSeatPocket = cylinderAlongX(
49776
- flangePocketDepth + 0.2,
49777
- flangeDiameter / 2 + panelHoleClearance,
49778
- panelThickness / 2 - flangePocketDepth / 2,
49779
- segments
49780
- );
49781
- const cableBore = cylinderAlongX(glandLength + 0.8, cableBoreRadius, 0, segments);
49782
- const panel = box(panelThickness, panelWidth, panelHeight).translate(0, 0, -panelHeight / 2).subtract(panelHole).subtract(flangeSeatPocket).color("#475569");
49783
- const glandBody = tubeAlongX(glandLength, glandOuterRadius, cableBoreRadius, 0, segments);
49784
- const flangeCenterX = panelThickness / 2 - flangePocketDepth + faceClearance + flangeThickness / 2;
49785
- const flange = tubeAlongX(flangeThickness, flangeDiameter / 2, cableBoreRadius, flangeCenterX, segments);
49786
- const gland = union(glandBody, flange).color("#94a3b8");
49787
- const nutInnerRadius = glandOuterRadius + Math.min(0.12, runningClearance * 0.4);
49788
- const nutCenterX = -panelThickness / 2 - faceClearance - nutThickness / 2;
49789
- const compressionNut = tubeAlongX(nutThickness, nutOuterDiameter / 2, nutInnerRadius, nutCenterX, segments).color("#cbd5e1");
49790
- const cable = cylinderAlongX(cableLength, cableDiameter / 2, 0, segments).color("#111827");
49791
- const parts = [
49792
- { name: "panel with gland clearance hole", shape: panel },
49793
- { name: "hollow cable gland body with panel flange", shape: gland },
49794
- { name: "compression nut around gland body", shape: compressionNut },
49795
- { name: "routed cable through gland bore", shape: cable }
49796
- ];
49797
- return {
49798
- parts,
49799
- panel,
49800
- gland,
49801
- compressionNut,
49802
- cable,
49803
- cutters: {
49804
- panelHole,
49805
- flangeSeatPocket,
49806
- cableBore
49807
- },
49808
- dims: {
49809
- cableDiameter,
49810
- cableBoreDiameter,
49811
- panelThickness,
49812
- panelWidth,
49813
- panelHeight,
49814
- glandOuterDiameter,
49815
- glandLength,
49816
- nutOuterDiameter,
49817
- nutThickness,
49818
- flangeDiameter,
49819
- flangeThickness,
49820
- runningClearance,
49821
- faceClearance,
49822
- flangePocketDepth,
49823
- panelHoleDiameter,
49824
- cableLength
49825
- }
49826
- };
49827
- }
49828
- function hoseBarbPortAssembly(options) {
49829
- const hoseInnerDiameter = requirePositive$6(options.hoseInnerDiameter, "hoseInnerDiameter");
49830
- const runningClearance = requirePositive$6(options.runningClearance ?? 0.18, "runningClearance");
49831
- const faceClearance = requirePositive$6(options.faceClearance ?? 0.04, "faceClearance");
49832
- const barbRootDiameter = requirePositive$6(
49833
- options.barbRootDiameter ?? Math.max(1, hoseInnerDiameter - Math.max(0.25, hoseInnerDiameter * 0.06)),
49834
- "barbRootDiameter"
49835
- );
49836
- const barbPeakDiameter = requirePositive$6(
49837
- options.barbPeakDiameter ?? hoseInnerDiameter + Math.max(0.65, hoseInnerDiameter * 0.12),
49838
- "barbPeakDiameter"
49839
- );
49840
- const installedHoseBoreDiameter = barbPeakDiameter + runningClearance * 2;
49841
- const hoseOuterDiameter = requirePositive$6(
49842
- options.hoseOuterDiameter ?? Math.max(installedHoseBoreDiameter + 2.4, hoseInnerDiameter + Math.max(3, hoseInnerDiameter * 0.55)),
49843
- "hoseOuterDiameter"
49844
- );
49845
- const fluidBoreDiameter = requirePositive$6(options.fluidBoreDiameter ?? hoseInnerDiameter * 0.65, "fluidBoreDiameter");
49846
- const blockThickness = requirePositive$6(options.blockThickness ?? Math.max(7, hoseInnerDiameter * 1.2), "blockThickness");
49847
- const barbCount = options.barbCount ?? 3;
49848
- const barbLength = requirePositive$6(options.barbLength ?? Math.max(2.6, hoseInnerDiameter * 0.55), "barbLength");
49849
- const barbStackLength = barbCount * barbLength;
49850
- const shoulderDiameter = requirePositive$6(
49851
- options.shoulderDiameter ?? barbPeakDiameter + Math.max(4, hoseInnerDiameter * 0.65),
49852
- "shoulderDiameter"
49853
- );
49854
- const shoulderThickness = requirePositive$6(options.shoulderThickness ?? Math.max(2, hoseInnerDiameter * 0.35), "shoulderThickness");
49855
- const bossDiameter = requirePositive$6(options.bossDiameter ?? shoulderDiameter + Math.max(4, hoseInnerDiameter * 0.6), "bossDiameter");
49856
- const bossHeight = requirePositive$6(options.bossHeight ?? Math.max(2.4, hoseInnerDiameter * 0.45), "bossHeight");
49857
- const blockWidth = requirePositive$6(options.blockWidth ?? bossDiameter + Math.max(14, hoseInnerDiameter * 2.4), "blockWidth");
49858
- const blockHeight = requirePositive$6(options.blockHeight ?? bossDiameter + Math.max(12, hoseInnerDiameter * 2.1), "blockHeight");
49859
- const hoseLength = requirePositive$6(options.hoseLength ?? barbStackLength + Math.max(32, hoseInnerDiameter * 5), "hoseLength");
49860
- const clampWidth = requirePositive$6(options.clampWidth ?? Math.max(4, hoseOuterDiameter * 0.45), "clampWidth");
49861
- const clampThickness = requirePositive$6(options.clampThickness ?? 0.9, "clampThickness");
49862
- const segments = options.segments ?? 40;
49863
- if (!Number.isInteger(barbCount) || barbCount < 1 || barbCount > 8) {
49864
- throw new Error("hoseBarbPortAssembly: barbCount must be an integer from 1 to 8");
49865
- }
49866
- if (barbPeakDiameter <= hoseInnerDiameter) {
49867
- throw new Error("hoseBarbPortAssembly: barbPeakDiameter must exceed hoseInnerDiameter so the barb retains the hose");
49868
- }
49869
- if (barbRootDiameter >= barbPeakDiameter - Math.max(0.25, hoseInnerDiameter * 0.04)) {
49870
- throw new Error("hoseBarbPortAssembly: barbRootDiameter must leave a visible barb rise");
49871
- }
49872
- if (fluidBoreDiameter >= barbRootDiameter - Math.max(0.8, hoseInnerDiameter * 0.12)) {
49873
- throw new Error("hoseBarbPortAssembly: fluidBoreDiameter leaves too little wall in the barb fitting");
49874
- }
49875
- if (hoseOuterDiameter <= installedHoseBoreDiameter + Math.max(1.2, hoseInnerDiameter * 0.16)) {
49876
- throw new Error("hoseBarbPortAssembly: hoseOuterDiameter leaves too little hose wall around the installed barb envelope");
49877
- }
49878
- if (shoulderDiameter <= barbPeakDiameter + Math.max(1.5, hoseInnerDiameter * 0.2)) {
49879
- throw new Error("hoseBarbPortAssembly: shoulderDiameter must be larger than the barb peaks");
49880
- }
49881
- if (bossDiameter <= shoulderDiameter + Math.max(1.5, hoseInnerDiameter * 0.2)) {
49882
- throw new Error("hoseBarbPortAssembly: bossDiameter must leave material around the shoulder seat");
49883
- }
49884
- if (blockWidth <= bossDiameter + 8 || blockHeight <= bossDiameter + 8) {
49885
- throw new Error("hoseBarbPortAssembly: receiver block dimensions leave too little material around the port boss");
49886
- }
49887
- const portBoreDiameter = barbRootDiameter + runningClearance * 2;
49888
- const portBore = cylinderAlongX(blockThickness + bossHeight + 0.8, portBoreDiameter / 2, bossHeight / 2, segments);
49889
- const fuseOverlap = Math.min(0.04, faceClearance * 0.7);
49890
- const bossCenterX = blockThickness / 2 + bossHeight / 2 - fuseOverlap;
49891
- const receiver = union(
49892
- box(blockThickness, blockWidth, blockHeight).translate(0, 0, -blockHeight / 2),
49893
- cylinderAlongX(bossHeight + fuseOverlap, bossDiameter / 2, bossCenterX, segments)
49894
- ).subtract(portBore).color("#475569");
49895
- const bossFaceX = blockThickness / 2 + bossHeight;
49896
- const shoulderCenterX = bossFaceX + faceClearance + shoulderThickness / 2;
49897
- const barbStartX = shoulderCenterX + shoulderThickness / 2;
49898
- const fittingStartX = -blockThickness / 2 - runningClearance;
49899
- const fittingEndX = barbStartX + barbStackLength;
49900
- const fittingCore = tubeAlongX(
49901
- fittingEndX - fittingStartX,
49902
- barbRootDiameter / 2,
49903
- fluidBoreDiameter / 2,
49904
- (fittingStartX + fittingEndX) / 2,
49905
- segments
49906
- );
49907
- const shoulder = tubeAlongX(shoulderThickness, shoulderDiameter / 2, fluidBoreDiameter / 2, shoulderCenterX, segments);
49908
- const barbSolids = [];
49909
- const ridgeLength = Math.max(0.8, Math.min(barbLength * 0.45, hoseInnerDiameter * 0.28));
49910
- for (let index2 = 0; index2 < barbCount; index2 += 1) {
49911
- const startX = barbStartX + index2 * barbLength;
49912
- const ridgeCenterX = startX + barbLength - ridgeLength / 2;
49913
- barbSolids.push(tubeAlongX(ridgeLength, barbPeakDiameter / 2, fluidBoreDiameter / 2, ridgeCenterX, segments));
49914
- }
49915
- const fitting = union(fittingCore, shoulder, ...barbSolids).color("#94a3b8");
49916
- const hoseStartX = barbStartX + faceClearance;
49917
- const hoseCenterX = hoseStartX + hoseLength / 2;
49918
- const installedHoseBore = cylinderAlongX(hoseLength + 0.8, installedHoseBoreDiameter / 2, hoseCenterX, segments);
49919
- const hose = tubeAlongX(hoseLength, hoseOuterDiameter / 2, installedHoseBoreDiameter / 2, hoseCenterX, segments).color("#111827");
49920
- const clampCenterX = barbStartX + Math.min(barbStackLength * 0.55, Math.max(barbLength, clampWidth));
49921
- const clamp2 = tubeAlongX(
49922
- clampWidth,
49923
- hoseOuterDiameter / 2 + clampThickness,
49924
- hoseOuterDiameter / 2 + Math.min(0.08, runningClearance * 0.45),
49925
- clampCenterX,
49926
- segments
49927
- ).color("#cbd5e1");
49928
- const parts = [
49929
- { name: "bored pump or filter body with raised hose-port boss", shape: receiver },
49930
- { name: "hollow hose barb fitting with shoulder and retention ridges", shape: fitting },
49931
- { name: "installed flexible hose over barb tail", shape: hose },
49932
- { name: "clamp band over hose and barb ridges", shape: clamp2 }
49933
- ];
49934
- return {
49935
- parts,
49936
- receiver,
49937
- fitting,
49938
- hose,
49939
- clamp: clamp2,
49940
- cutters: {
49941
- portBore,
49942
- installedHoseBore
49943
- },
49944
- dims: {
49945
- hoseInnerDiameter,
49946
- hoseOuterDiameter,
49947
- installedHoseBoreDiameter,
49948
- blockThickness,
49949
- blockWidth,
49950
- blockHeight,
49951
- bossDiameter,
49952
- bossHeight,
49953
- fluidBoreDiameter,
49954
- barbRootDiameter,
49955
- barbPeakDiameter,
49956
- barbCount,
49957
- barbLength,
49958
- barbStackLength,
49959
- shoulderDiameter,
49960
- shoulderThickness,
49961
- hoseLength,
49962
- clampWidth,
49963
- clampThickness,
49964
- runningClearance,
49965
- faceClearance
49966
- }
49967
- };
49968
- }
49969
- function routedTubeClipAssembly(options) {
49970
- const tubeDiameter = requirePositive$6(options.tubeDiameter, "tubeDiameter");
49971
- const tubeLength = requirePositive$6(options.tubeLength ?? 120, "tubeLength");
49972
- const panelThickness = requirePositive$6(options.panelThickness ?? 3, "panelThickness");
49973
- const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
49974
- const screwSize = options.screwSize ?? "M3";
49975
- const segments = options.segments ?? 32;
49976
- const sizeData = METRIC_HOLE_TABLE[screwSize];
49977
- if (!sizeData) throw new Error(`routedTubeClipAssembly: unsupported screwSize "${screwSize}"`);
49978
- const clipCount = options.clipCount ?? 3;
49979
- if (!Number.isInteger(clipCount) || clipCount < 1 || clipCount > 8) {
49980
- throw new Error("routedTubeClipAssembly: clipCount must be an integer from 1 to 8");
49981
- }
49982
- const screwDiameter = parseFloat(screwSize.replace("M", ""));
49983
- const screwHeadDiameter = sizeData.head;
49984
- const tubeBoreDiameter = tubeDiameter + runningClearance * 2;
49985
- const clipWallThickness = requirePositive$6(
49986
- options.clipWallThickness ?? Math.max(screwHeadDiameter + 1.2, tubeDiameter * 0.45, 5),
49987
- "clipWallThickness"
49988
- );
49989
- const clipWidth = requirePositive$6(options.clipWidth ?? Math.max(screwHeadDiameter + 3, tubeDiameter * 1.4, 10), "clipWidth");
49990
- const clipDepth = tubeBoreDiameter + clipWallThickness * 2;
49991
- const bottomWall = Math.max(1.2, clipWallThickness * 0.35);
49992
- const topWall = Math.max(2, clipWallThickness * 0.45);
49993
- const clipHeight = bottomWall + tubeBoreDiameter + topWall;
49994
- const tubeCenterZ = panelThickness + bottomWall + tubeBoreDiameter / 2;
49995
- const panelLength = requirePositive$6(options.panelLength ?? tubeLength + 24, "panelLength");
49996
- const panelWidth = requirePositive$6(options.panelWidth ?? clipDepth + Math.max(14, screwHeadDiameter * 2), "panelWidth");
49997
- if (tubeLength <= clipWidth + 8) {
49998
- throw new Error("routedTubeClipAssembly: tubeLength must leave visible tube beyond the clip body");
49999
- }
50000
- const defaultSpacing = clipCount === 1 ? 0 : Math.max(clipWidth + 8, (tubeLength - clipWidth * 2) / (clipCount - 1));
50001
- const clipSpacing = options.clipSpacing === void 0 ? defaultSpacing : requirePositive$6(options.clipSpacing, "clipSpacing");
50002
- const clipCenters = Array.from({ length: clipCount }, (_2, index2) => (index2 - (clipCount - 1) / 2) * clipSpacing);
50003
- const maxClipExtent = Math.max(...clipCenters.map((x2) => Math.abs(x2) + clipWidth / 2));
50004
- if (maxClipExtent > tubeLength / 2 - 2) {
50005
- throw new Error("routedTubeClipAssembly: clipSpacing places a clip beyond the routed tube length");
50006
- }
50007
- if (maxClipExtent > panelLength / 2 - 2) {
50008
- throw new Error("routedTubeClipAssembly: panelLength is too short for the clip pattern");
50009
- }
50010
- const boreRadius = tubeBoreDiameter / 2;
50011
- const screwY = boreRadius + clipWallThickness / 2;
50012
- if (screwY + screwHeadDiameter / 2 > clipDepth / 2 - 0.2) {
50013
- throw new Error("routedTubeClipAssembly: clipWallThickness leaves too little land for screw heads");
50014
- }
50015
- if (clipDepth > panelWidth - Math.max(4, screwHeadDiameter * 0.5)) {
50016
- throw new Error("routedTubeClipAssembly: panelWidth leaves too little material beside the clips");
50017
- }
50018
- const screwPositions = clipCenters.flatMap((x2) => [[x2, -screwY], [x2, screwY]]);
50019
- const screwClearanceDiameter = Math.max(sizeData.loose, screwDiameter + 0.8);
50020
- const panelThreadEnvelopeDiameter = screwClearanceDiameter;
50021
- const clipTopZ = panelThickness + clipHeight;
50022
- const clipTubeBores = union(
50023
- ...clipCenters.map((x2) => cylinderAlongX(clipWidth + 0.8, boreRadius, x2, segments).translate(0, 0, tubeCenterZ))
50024
- );
50025
- const clipScrewClearances = union(
50026
- ...screwPositions.map(
50027
- ([x2, y2]) => cylinder(clipHeight + 0.8, screwClearanceDiameter / 2, void 0, segments).translate(x2, y2, panelThickness - 0.4)
50028
- )
50029
- );
50030
- const panelThreadEnvelopes = union(
50031
- ...screwPositions.map(
50032
- ([x2, y2]) => cylinder(panelThickness + 0.8, panelThreadEnvelopeDiameter / 2, void 0, segments).translate(x2, y2, -0.4)
50033
- )
50034
- );
50035
- const panel = box(panelLength, panelWidth, panelThickness).subtract(panelThreadEnvelopes).color("#475569");
50036
- const tube2 = cylinderAlongX(tubeLength, tubeDiameter / 2, 0, segments).translate(0, 0, tubeCenterZ).color("#0f172a");
50037
- const clips = clipCenters.map((x2) => {
50038
- const body = box(clipWidth, clipDepth, clipHeight).translate(x2, 0, panelThickness);
50039
- const tubeBore = cylinderAlongX(clipWidth + 0.8, boreRadius, x2, segments).translate(0, 0, tubeCenterZ);
50040
- const screwHoles = union(
50041
- cylinder(clipHeight + 0.8, screwClearanceDiameter / 2, void 0, segments).translate(x2, -screwY, panelThickness - 0.4),
50042
- cylinder(clipHeight + 0.8, screwClearanceDiameter / 2, void 0, segments).translate(x2, screwY, panelThickness - 0.4)
50043
- );
50044
- return body.subtract(tubeBore).subtract(screwHoles).color("#94a3b8");
50045
- });
50046
- const screwLength = clipHeight + panelThickness * 0.65;
50047
- const screwHeadHeight = Math.max(1.2, screwDiameter * 0.55);
50048
- const screwBlank = union(
50049
- cylinder(screwLength, screwDiameter / 2, void 0, segments).translate(0, 0, clipTopZ - screwLength),
50050
- cylinder(screwHeadHeight, screwHeadDiameter / 2, void 0, segments).translate(0, 0, clipTopZ)
50051
- ).color("#cbd5e1");
50052
- const screws = screwPositions.map(([x2, y2]) => screwBlank.translate(x2, y2, 0));
50053
- const parts = [
50054
- { name: "panel with tube-clip screw receiving holes", shape: panel },
50055
- { name: "routed flexible tube through retained clip bores", shape: tube2 },
50056
- ...clips.map((shape, index2) => ({ name: `saddle tube clip ${index2 + 1} with through-bore`, shape })),
50057
- ...screws.map((shape, index2) => ({ name: `installed ${screwSize} tube clip screw ${index2 + 1}`, shape }))
50058
- ];
50059
- return {
50060
- parts,
50061
- panel,
50062
- tube: tube2,
50063
- clips,
50064
- screws,
50065
- clipCenters,
50066
- screwPositions,
50067
- cutters: {
50068
- clipTubeBores,
50069
- clipScrewClearances,
50070
- panelThreadEnvelopes
50071
- },
50072
- dims: {
50073
- tubeDiameter,
50074
- tubeLength,
50075
- tubeBoreDiameter,
50076
- panelLength,
50077
- panelWidth,
50078
- panelThickness,
50079
- clipCount,
50080
- clipWidth,
50081
- clipDepth,
50082
- clipHeight,
50083
- clipWallThickness,
50084
- tubeCenterZ,
50085
- screwSize,
50086
- screwDiameter,
50087
- screwHeadDiameter,
50088
- screwLength,
50089
- screwClearanceDiameter,
50090
- panelThreadEnvelopeDiameter,
50091
- runningClearance
50092
- }
50093
- };
50094
- }
50095
- function pcbTerminalBlockAssembly(options = {}) {
50096
- const terminalCount = options.terminalCount ?? 4;
50097
- if (!Number.isInteger(terminalCount) || terminalCount < 1 || terminalCount > 24) {
50098
- throw new Error("pcbTerminalBlockAssembly: terminalCount must be an integer from 1 to 24");
50099
- }
50100
- const terminalPitch = requirePositive$6(options.terminalPitch ?? 5.08, "terminalPitch");
50101
- const terminalBlockWidth = terminalPitch * terminalCount + 3;
50102
- const boardWidth = requirePositive$6(options.boardWidth ?? Math.max(50, terminalBlockWidth + 28), "boardWidth");
50103
- const boardDepth = requirePositive$6(options.boardDepth ?? 38, "boardDepth");
50104
- const boardThickness = requirePositive$6(options.boardThickness ?? 1.6, "boardThickness");
50105
- const backplateThickness = requirePositive$6(options.backplateThickness ?? 3, "backplateThickness");
50106
- const backplateMargin = requirePositive$6(options.backplateMargin ?? 5, "backplateMargin");
50107
- const standoffHeight = requirePositive$6(options.standoffHeight ?? 6, "standoffHeight");
50108
- const screwSize = options.screwSize ?? "M3";
50109
- const segments = options.segments ?? 28;
50110
- const sizeData = METRIC_HOLE_TABLE[screwSize];
50111
- if (!sizeData) throw new Error(`pcbTerminalBlockAssembly: unsupported screwSize "${screwSize}"`);
50112
- const screwDiameter = parseFloat(screwSize.replace("M", ""));
50113
- const screwHeadDiameter = sizeData.head;
50114
- const screwHeadHeight = Math.max(1.2, screwDiameter * 0.55);
50115
- const standoffDiameter = requirePositive$6(
50116
- options.standoffDiameter ?? Math.max(screwHeadDiameter * 1.45, sizeData.normal + 3),
50117
- "standoffDiameter"
50118
- );
50119
- const [mountInsetX, mountInsetY] = resolveBoltInset(
50120
- options.mountingInset,
50121
- Math.max(standoffDiameter / 2 + 1.2, screwHeadDiameter * 0.75)
50122
- );
50123
- if (mountInsetX * 2 >= boardWidth || mountInsetY * 2 >= boardDepth) {
50124
- throw new Error("pcbTerminalBlockAssembly: mountingInset leaves no room for the PCB mounting pattern");
50125
- }
50126
- const terminalBlockDepth = requirePositive$6(options.terminalBlockDepth ?? 10, "terminalBlockDepth");
50127
- const terminalBlockHeight = requirePositive$6(options.terminalBlockHeight ?? 9, "terminalBlockHeight");
50128
- const terminalEdgeInset = requirePositive$6(options.terminalEdgeInset ?? 5, "terminalEdgeInset");
50129
- const pinDiameter = requirePositive$6(options.pinDiameter ?? 0.9, "pinDiameter");
50130
- const pinClearance = requirePositive$6(options.pinClearance ?? 0.25, "pinClearance");
50131
- const pinTailLength = requireNonNegative(options.pinTailLength ?? 0, "pinTailLength");
50132
- const wirePortDiameter = requirePositive$6(options.wirePortDiameter ?? 2.6, "wirePortDiameter");
50133
- const pinHoleDiameter = pinDiameter + pinClearance;
50134
- const terminalCenterY = -boardDepth / 2 + terminalEdgeInset + terminalBlockDepth / 2;
50135
- const pinY = terminalCenterY + terminalBlockDepth * 0.24;
50136
- const firstPinX = -((terminalCount - 1) * terminalPitch) / 2;
50137
- const pinPositions = Array.from({ length: terminalCount }, (_2, index2) => [firstPinX + index2 * terminalPitch, pinY]);
50138
- const mountingPositions = [
50139
- [-boardWidth / 2 + mountInsetX, -boardDepth / 2 + mountInsetY],
50140
- [boardWidth / 2 - mountInsetX, -boardDepth / 2 + mountInsetY],
50141
- [-boardWidth / 2 + mountInsetX, boardDepth / 2 - mountInsetY],
50142
- [boardWidth / 2 - mountInsetX, boardDepth / 2 - mountInsetY]
50143
- ];
50144
- if (terminalBlockWidth >= boardWidth - mountInsetX * 2) {
50145
- throw new Error("pcbTerminalBlockAssembly: terminal block is too wide for the PCB mounting pattern");
50146
- }
50147
- if (terminalEdgeInset + terminalBlockDepth >= boardDepth - mountInsetY * 2) {
50148
- throw new Error("pcbTerminalBlockAssembly: terminal block depth collides with the rear mounting datum");
50149
- }
50150
- if (pinHoleDiameter >= terminalPitch * 0.55) {
50151
- throw new Error("pcbTerminalBlockAssembly: pinDiameter and pinClearance leave too little PCB web between terminal holes");
50152
- }
50153
- if (wirePortDiameter >= Math.min(terminalPitch * 0.72, terminalBlockHeight * 0.65)) {
50154
- throw new Error("pcbTerminalBlockAssembly: wirePortDiameter is too large for the terminal pitch or body height");
50155
- }
50156
- for (const [index2, [x2, y2]] of [...mountingPositions, ...pinPositions].entries()) {
50157
- if (!Number.isFinite(x2) || !Number.isFinite(y2)) {
50158
- throw new Error(`pcbTerminalBlockAssembly: generated datum position ${index2} is not finite`);
50159
- }
50160
- }
50161
- const backplateWidth = boardWidth + backplateMargin * 2;
50162
- const backplateDepth = boardDepth + backplateMargin * 2;
50163
- const boardBottomZ = backplateThickness + standoffHeight;
50164
- const boardTopZ = boardBottomZ + boardThickness;
50165
- const standoffOverlap = Math.min(0.08, standoffHeight * 0.03);
50166
- const standoffThreadEnvelopeDiameter = Math.max(sizeData.loose, screwDiameter + 1);
50167
- const standoffThreadEnvelope = cylinder(standoffHeight + 0.8, standoffThreadEnvelopeDiameter / 2, void 0, segments).translate(
50168
- 0,
50169
- 0,
50170
- backplateThickness - 0.4
50171
- );
50172
- const standoffThreadEnvelopes = union(...mountingPositions.map(([x2, y2]) => standoffThreadEnvelope.translate(x2, y2, 0)));
50173
- const standoff = cylinder(standoffHeight + standoffOverlap, standoffDiameter / 2, void 0, segments).translate(0, 0, backplateThickness - standoffOverlap).subtract(standoffThreadEnvelope);
50174
- const standoffs = union(...mountingPositions.map(([x2, y2]) => standoff.translate(x2, y2, 0)));
50175
- const backplate = union(box(backplateWidth, backplateDepth, backplateThickness), standoffs).color("#475569");
50176
- const boardMountingHoleDiameter = sizeData.normal;
50177
- const boardMountHole = cylinder(boardThickness + 0.8, boardMountingHoleDiameter / 2, void 0, segments).translate(
50178
- 0,
50179
- 0,
50180
- boardBottomZ - 0.4
50181
- );
50182
- const pcbMountingHoles = union(...mountingPositions.map(([x2, y2]) => boardMountHole.translate(x2, y2, 0)));
50183
- const pinHole = cylinder(boardThickness + 0.8, pinHoleDiameter / 2, void 0, segments).translate(0, 0, boardBottomZ - 0.4);
50184
- const pcbPinHoles = union(...pinPositions.map(([x2, y2]) => pinHole.translate(x2, y2, 0)));
50185
- const pcb = box(boardWidth, boardDepth, boardThickness).translate(0, 0, boardBottomZ).subtract(pcbMountingHoles).subtract(pcbPinHoles).color("#166534");
50186
- const terminalBodyBlank = box(terminalBlockWidth, terminalBlockDepth, terminalBlockHeight).translate(0, terminalCenterY, boardTopZ);
50187
- const wirePort = cylinderAlongY(terminalBlockDepth + 0.8, wirePortDiameter / 2, terminalCenterY, segments).translate(
50188
- 0,
50189
- 0,
50190
- boardTopZ + terminalBlockHeight * 0.42
50191
- );
50192
- const wirePorts = union(...pinPositions.map(([x2]) => wirePort.translate(x2, 0, 0)));
50193
- const clampScrewPockets = union(
50194
- ...pinPositions.map(
50195
- ([x2]) => cylinder(
50196
- Math.max(0.6, terminalBlockHeight * 0.22),
50197
- Math.min(terminalPitch * 0.22, wirePortDiameter * 0.42),
50198
- void 0,
50199
- segments
50200
- ).translate(x2, terminalCenterY + terminalBlockDepth * 0.12, boardTopZ + terminalBlockHeight * 0.76)
50201
- )
50202
- );
50203
- const pinLength = boardThickness + pinTailLength + Math.min(0.6, terminalBlockHeight * 0.08);
50204
- const pinStartZ = boardBottomZ - pinTailLength;
50205
- const pins = union(...pinPositions.map(([x2, y2]) => cylinder(pinLength, pinDiameter / 2, void 0, segments).translate(x2, y2, pinStartZ)));
50206
- const terminalBlock = union(terminalBodyBlank.subtract(wirePorts).subtract(clampScrewPockets), pins).color("#16a34a");
50207
- const screwShaftLength = boardThickness + standoffHeight * 0.85;
50208
- const mountingHardware = fastenerSet(screwSize, screwShaftLength, {
50209
- washerUnderHead: false,
50210
- washerUnderNut: false,
50211
- fit: "normal",
50212
- segments
50213
- });
50214
- const screws = mountingPositions.map(([x2, y2]) => mountingHardware.bolt.translate(x2, y2, boardTopZ).color("#cbd5e1"));
50215
- const parts = [
50216
- { name: "electronics backplate with fused PCB standoffs", shape: backplate },
50217
- { name: "PCB with mounting holes and terminal pin clearances", shape: pcb },
50218
- { name: "seated purchased terminal block with through-board pins", shape: terminalBlock },
50219
- ...screws.map((shape, index2) => ({ name: `installed ${screwSize} PCB mounting screw ${index2 + 1}`, shape }))
50220
- ];
50221
- return {
50222
- parts,
50223
- backplate,
50224
- pcb,
50225
- terminalBlock,
50226
- screws,
50227
- mountingPositions,
50228
- pinPositions,
50229
- cutters: {
50230
- pcbMountingHoles,
50231
- pcbPinHoles,
50232
- standoffThreadEnvelopes
50233
- },
50234
- dims: {
50235
- terminalCount,
50236
- terminalPitch,
50237
- boardWidth,
50238
- boardDepth,
50239
- boardThickness,
50240
- backplateWidth,
50241
- backplateDepth,
50242
- backplateThickness,
50243
- standoffHeight,
50244
- standoffDiameter,
50245
- screwSize,
50246
- screwDiameter,
50247
- screwHeadDiameter,
50248
- screwHeadHeight,
50249
- screwShaftLength,
50250
- boardMountingHoleDiameter,
50251
- standoffThreadEnvelopeDiameter,
50252
- terminalBlockWidth,
50253
- terminalBlockDepth,
50254
- terminalBlockHeight,
50255
- terminalEdgeInset,
50256
- pinDiameter,
50257
- pinClearance,
50258
- pinHoleDiameter,
50259
- pinTailLength,
50260
- wirePortDiameter
50261
- }
50262
- };
50263
- }
50264
- function thumbScrewClampAssembly(options = {}) {
50265
- const screwSize = options.screwSize ?? "M6";
50266
- const segments = options.segments ?? 36;
50267
- const sizeData = METRIC_HOLE_TABLE[screwSize];
50268
- if (!sizeData) throw new Error(`thumbScrewClampAssembly: unsupported screwSize "${screwSize}"`);
50269
- const screwDiameter = parseFloat(screwSize.replace("M", ""));
50270
- const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
50271
- const faceClearance = requireNonNegative(options.faceClearance ?? 0, "faceClearance");
50272
- const threadEnvelopeDiameter = Math.max(sizeData.normal, screwDiameter + runningClearance * 2);
50273
- const pressurePadDiameter = requirePositive$6(options.pressurePadDiameter ?? Math.max(screwDiameter * 3.2, 18), "pressurePadDiameter");
50274
- const pressurePadThickness = requirePositive$6(options.pressurePadThickness ?? Math.max(screwDiameter * 0.72, 4), "pressurePadThickness");
50275
- const knobDiameter = requirePositive$6(options.knobDiameter ?? Math.max(screwDiameter * 4.2, 24), "knobDiameter");
50276
- const knobThickness = requirePositive$6(options.knobThickness ?? Math.max(screwDiameter * 0.9, 7), "knobThickness");
50277
- const workpieceThickness = requirePositive$6(options.workpieceThickness ?? 18, "workpieceThickness");
50278
- const workpieceDepth = requirePositive$6(options.workpieceDepth ?? Math.max(46, pressurePadDiameter * 1.5), "workpieceDepth");
50279
- const workpieceHeight = requirePositive$6(options.workpieceHeight ?? Math.max(pressurePadDiameter * 1.35, 24), "workpieceHeight");
50280
- const frameDepth = requirePositive$6(options.frameDepth ?? Math.max(workpieceDepth + 12, pressurePadDiameter + 16), "frameDepth");
50281
- const baseThickness = requirePositive$6(options.baseThickness ?? Math.max(screwDiameter, 6), "baseThickness");
50282
- const jawThickness = requirePositive$6(options.jawThickness ?? Math.max(screwDiameter * 1.35, 9), "jawThickness");
50283
- const supportThickness = requirePositive$6(options.supportThickness ?? Math.max(screwDiameter * 1.8, 12), "supportThickness");
50284
- const bossLength = requirePositive$6(options.bossLength ?? Math.max(screwDiameter * 1.1, 8), "bossLength");
50285
- const bossDiameter = requirePositive$6(options.bossDiameter ?? Math.max(threadEnvelopeDiameter + 5, screwDiameter * 2.5), "bossDiameter");
50286
- const exposedScrewLength = requirePositive$6(
50287
- options.exposedScrewLength ?? Math.max(pressurePadDiameter * 0.45, screwDiameter * 2.2),
50288
- "exposedScrewLength"
50289
- );
50290
- const screwCenterZ = baseThickness + Math.max(workpieceHeight * 0.52, pressurePadDiameter * 0.68);
50291
- const frameHeight = requirePositive$6(
50292
- options.frameHeight ?? screwCenterZ - baseThickness + pressurePadDiameter / 2 + Math.max(baseThickness, 7),
50293
- "frameHeight"
50294
- );
50295
- if (workpieceDepth > frameDepth - 6) {
50296
- throw new Error("thumbScrewClampAssembly: frameDepth must leave side material around the clamped workpiece");
50297
- }
50298
- if (pressurePadDiameter > frameDepth - 4) {
50299
- throw new Error("thumbScrewClampAssembly: pressurePadDiameter is too large for the frame depth");
50300
- }
50301
- if (bossDiameter > frameDepth - 4) {
50302
- throw new Error("thumbScrewClampAssembly: bossDiameter is too large for the frame depth");
50303
- }
50304
- if (screwCenterZ - pressurePadDiameter / 2 <= baseThickness + 0.5) {
50305
- throw new Error("thumbScrewClampAssembly: pressure pad collides with the base bridge");
50306
- }
50307
- if (baseThickness + frameHeight - screwCenterZ <= pressurePadDiameter / 2 + 2) {
50308
- throw new Error("thumbScrewClampAssembly: frameHeight leaves too little material above the screw axis");
50309
- }
50310
- if (threadEnvelopeDiameter + 4 > Math.min(frameDepth, frameHeight)) {
50311
- throw new Error("thumbScrewClampAssembly: threaded boss bore leaves too little surrounding frame material");
50312
- }
50313
- const workpieceLeftFaceX = -workpieceThickness / 2;
50314
- const workpieceRightFaceX = workpieceThickness / 2;
50315
- const anvilOverlap = Math.min(0.35, pressurePadThickness * 0.18);
50316
- const anvilPadCenterX = workpieceLeftFaceX - faceClearance - pressurePadThickness / 2;
50317
- const pressurePadCenterX = workpieceRightFaceX + faceClearance + pressurePadThickness / 2;
50318
- const fixedJawRightFaceX = anvilPadCenterX - pressurePadThickness / 2 + anvilOverlap;
50319
- const fixedJawCenterX = fixedJawRightFaceX - jawThickness / 2;
50320
- const pressurePadRightFaceX = pressurePadCenterX + pressurePadThickness / 2;
50321
- const supportInnerFaceX = pressurePadRightFaceX + exposedScrewLength;
50322
- const supportCenterX = supportInnerFaceX + supportThickness / 2;
50323
- const supportOuterFaceX = supportInnerFaceX + supportThickness;
50324
- const frameLeftFaceX = fixedJawCenterX - jawThickness / 2;
50325
- const frameRightFaceX = supportOuterFaceX;
50326
- const baseLength = frameRightFaceX - frameLeftFaceX;
50327
- if (baseLength <= 0 || !Number.isFinite(baseLength)) {
50328
- throw new Error("thumbScrewClampAssembly: generated clamp frame length is invalid");
50329
- }
50330
- const bossCenterX = supportInnerFaceX + (supportThickness + bossLength) / 2;
50331
- const threadedBossBore = cylinderAlongX(supportThickness + bossLength + 1, threadEnvelopeDiameter / 2, bossCenterX, segments).translate(
50332
- 0,
50333
- 0,
50334
- screwCenterZ
50335
- );
50336
- const frameOverlap = Math.min(0.12, baseThickness * 0.04);
50337
- const base = box(baseLength, frameDepth, baseThickness).translate((frameLeftFaceX + frameRightFaceX) / 2, 0, 0);
50338
- const fixedJaw = box(jawThickness, frameDepth, frameHeight + frameOverlap).translate(fixedJawCenterX, 0, baseThickness - frameOverlap);
50339
- const support = box(supportThickness, frameDepth, frameHeight + frameOverlap).translate(supportCenterX, 0, baseThickness - frameOverlap);
50340
- const boss2 = cylinderAlongX(supportThickness + bossLength, bossDiameter / 2, bossCenterX, segments).translate(0, 0, screwCenterZ);
50341
- const anvilPad = cylinderAlongX(pressurePadThickness, pressurePadDiameter / 2, anvilPadCenterX, segments).translate(0, 0, screwCenterZ);
50342
- const frame = union(base, fixedJaw, support, boss2, anvilPad).subtract(threadedBossBore).color("#475569");
50343
- const workpieceBottomZ = screwCenterZ - workpieceHeight / 2;
50344
- const workpiece = box(workpieceThickness, workpieceDepth, workpieceHeight).translate(0, 0, workpieceBottomZ).color("#a16207");
50345
- const pressurePad = cylinderAlongX(pressurePadThickness, pressurePadDiameter / 2, pressurePadCenterX, segments).translate(
50346
- 0,
50347
- 0,
50348
- screwCenterZ
50349
- );
50350
- const knobCenterX = supportOuterFaceX + bossLength + runningClearance + knobThickness / 2;
50351
- const knob = cylinderAlongX(knobThickness, knobDiameter / 2, knobCenterX, segments).translate(0, 0, screwCenterZ);
50352
- const shaftLeftX = pressurePadRightFaceX - Math.min(pressurePadThickness * 0.45, screwDiameter * 0.45);
50353
- const shaftRightX = knobCenterX + knobThickness / 2;
50354
- const shaftLength = shaftRightX - shaftLeftX;
50355
- if (shaftLength <= supportThickness + bossLength) {
50356
- throw new Error("thumbScrewClampAssembly: generated screw length is too short for the threaded support");
50357
- }
50358
- const shaft = cylinderAlongX(shaftLength, screwDiameter / 2, (shaftLeftX + shaftRightX) / 2, segments).translate(0, 0, screwCenterZ);
50359
- const clampScrew = union(shaft, pressurePad, knob).color("#cbd5e1");
50360
- const workpieceEnvelope = box(workpieceThickness, workpieceDepth, workpieceHeight).translate(0, 0, workpieceBottomZ);
50361
- return {
50362
- parts: [
50363
- { name: "thumb-screw clamp frame with fixed anvil and threaded boss", shape: frame },
50364
- { name: "representative clamped workpiece between pads", shape: workpiece },
50365
- { name: "installed thumb screw with captive pressure pad and hand knob", shape: clampScrew }
50366
- ],
50367
- frame,
50368
- workpiece,
50369
- clampScrew,
50370
- cutters: {
50371
- threadedBossBore,
50372
- workpieceEnvelope
50373
- },
50374
- dims: {
50375
- screwSize,
50376
- screwDiameter,
50377
- threadEnvelopeDiameter,
50378
- workpieceThickness,
50379
- workpieceDepth,
50380
- workpieceHeight,
50381
- frameDepth,
50382
- frameHeight,
50383
- baseThickness,
50384
- jawThickness,
50385
- supportThickness,
50386
- bossLength,
50387
- bossDiameter,
50388
- exposedScrewLength,
50389
- pressurePadDiameter,
50390
- pressurePadThickness,
50391
- knobDiameter,
50392
- knobThickness,
50393
- screwCenterZ,
50394
- fixedAnvilFaceX: workpieceLeftFaceX - faceClearance,
50395
- pressurePadFaceX: workpieceRightFaceX + faceClearance,
50396
- supportInnerFaceX,
50397
- runningClearance,
50398
- faceClearance
50399
- }
50400
- };
50401
- }
50402
49624
  function fastenerSet(size, boltLength, options) {
50403
49625
  const sizeData = METRIC_HOLE_TABLE[size];
50404
49626
  if (!sizeData) throw new Error(`fastenerSet: unsupported size "${size}"`);
@@ -50459,22 +49681,6 @@ const partLibrary = {
50459
49681
  nut,
50460
49682
  washer,
50461
49683
  fastenerSet,
50462
- boltedServiceCover,
50463
- datumEnclosureAssembly,
50464
- snapLatchCoverAssembly,
50465
- pinnedLeverAssembly,
50466
- retainedShaftAssembly,
50467
- capturedLinearSlide,
50468
- capturedCartridgeGuideAssembly,
50469
- livingHingeCoverAssembly,
50470
- knuckledHingeAssembly,
50471
- clevisPinJointAssembly,
50472
- seatedBearingAssembly,
50473
- cableGlandAnchorAssembly,
50474
- hoseBarbPortAssembly,
50475
- routedTubeClipAssembly,
50476
- pcbTerminalBlockAssembly,
50477
- thumbScrewClampAssembly,
50478
49684
  pipeRoute,
50479
49685
  elbow,
50480
49686
  beltDrive,
@@ -306313,7 +305519,8 @@ function describeKinematicConvergenceError(kinematics) {
306313
305519
  const edgeResidual = edge ? Math.abs(edge.residual) : 0;
306314
305520
  const angleResidual = angle ? Math.abs(angle.residual) : 0;
306315
305521
  const worst = edgeResidual >= angleResidual && edge ? `edge "${edge.name}"` : angle ? `angle "${angle.name}"` : "constraint";
306316
- 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.`;
305522
+ const diagnostic = kinematics.diagnostics.length > 0 ? ` Diagnostic: ${kinematics.diagnostics[0]}` : "";
305523
+ 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.`;
306317
305524
  }
306318
305525
  function mapScriptResultToScene(args) {
306319
305526
  var _a3;