forgecad 0.10.2 → 0.10.3

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 (95) hide show
  1. package/README.md +7 -6
  2. package/dist/assets/{AdminPage-CHY6ZN-p.js → AdminPage-CK7ObBz3.js} +1 -1
  3. package/dist/assets/{BenchmarkPage-BcRT5iGN.js → BenchmarkPage-Ds7Z2doN.js} +1 -1
  4. package/dist/assets/{BlogPage-BssBbnb-.js → BlogPage-DlPbpt6A.js} +1 -1
  5. package/dist/assets/{DocsPage-DsvdiRNK.js → DocsPage-vZb3b3Y0.js} +9 -14
  6. package/dist/assets/{EditorApp-BpjZgzk0.css → EditorApp-C5f24ZN9.css} +8 -0
  7. package/dist/assets/{EditorApp-Bfd3jbtC.js → EditorApp-HLoKfe15.js} +141 -12
  8. package/dist/assets/{EmbedViewer-D5t8WamV.js → EmbedViewer--KnqBKrJ.js} +2 -2
  9. package/dist/assets/{LandingPageProofDriven-DbN7o-Be.js → LandingPageProofDriven-C_LssmnA.js} +1 -1
  10. package/dist/assets/{LegalPage-DNGrrY0p.js → LegalPage-DGsyo4n1.js} +1 -1
  11. package/dist/assets/{PricingPage-Nczr3pRz.js → PricingPage-BOE27B-R.js} +1 -1
  12. package/dist/assets/{SettingsPage-DZlyu4d4.js → SettingsPage-f47cnk39.js} +1 -1
  13. package/dist/assets/{app-C9ct2hRD.js → app-D6ccu2Xx.js} +6854 -7373
  14. package/dist/assets/{backendInit-ymjonyQp.js → backendInit-DbTkQN9J.js} +2557 -809
  15. package/dist/assets/cli/{render-B_0lQwKU.js → render-BsngirjC.js} +114 -9
  16. package/dist/assets/{constructionHistoryWorker-CZ42Dksy.js → constructionHistoryWorker-PCwXrTDB.js} +175 -36
  17. package/dist/assets/{evalWorker-C2pm8LHP.js → evalWorker-CS63PfZu.js} +1125 -447
  18. package/dist/assets/{forgecad_geometry-BlMtqluF.js → forgecad_geometry-CZ_IfuvA.js} +1 -9
  19. package/dist/assets/{forgecad_geometry_bg-BllP_WiL.wasm → forgecad_geometry_bg-C3rQHfwg.wasm} +0 -0
  20. package/dist/assets/{inspectWorker-D5T5VbfK.js → inspectWorker-Y4cOzNyA.js} +4345 -373
  21. package/dist/assets/{jointPose-4r8ed8_5.js → jointPose-AMvCywzS.js} +1 -1
  22. package/dist/assets/{manifold-C4r6B-XY.js → manifold-CBry38ly.js} +2 -2
  23. package/dist/assets/{manifold-5PP1eGLN.js → manifold-Crd_F2qx.js} +1 -1
  24. package/dist/assets/{manifold-DjBkyIc8.js → manifold-k2kRcc85.js} +1 -1
  25. package/dist/assets/{reportWorker-CwenM7wB.js → reportWorker-CWvn0CEv.js} +1095 -400
  26. package/dist/cli/render.html +1 -1
  27. package/dist/docs/index.html +2 -2
  28. package/dist/docs-raw/AI/usage.md +2 -4
  29. package/dist/docs-raw/CLI.md +9 -7
  30. package/dist/docs-raw/README.md +1 -1
  31. package/dist/docs-raw/component-model.md +1 -1
  32. package/dist/docs-raw/generated/assembly.md +1 -1
  33. package/dist/docs-raw/generated/concepts.md +5 -3
  34. package/dist/docs-raw/generated/core.md +70 -1
  35. package/dist/docs-raw/generated/curves.md +8 -1
  36. package/dist/docs-raw/generated/output.md +0 -64
  37. package/dist/docs-raw/generated/runtime-names.md +6 -6
  38. package/dist/docs-raw/generated/viewport.md +3 -12
  39. package/dist/docs-raw/guides/inspection-bundles.md +1 -1
  40. package/dist/docs-raw/simulation-workflow.md +58 -0
  41. package/dist/docs-raw/skills/forgecad-blockout-model.md +1 -1
  42. package/dist/docs-raw/skills/forgecad-image-replicator.md +2 -2
  43. package/dist/docs-raw/skills/forgecad-mujoco-verify.md +78 -0
  44. package/dist/docs-raw/skills/forgecad-spec-by-walking-through-it.md +145 -0
  45. package/dist/docs-raw/skills/forgecad-visual-spec.md +1 -1
  46. package/dist/docs-raw/skills/forgecad.md +24 -24
  47. package/dist/docs-raw/skills/index.md +2 -3
  48. package/dist/index.html +1 -1
  49. package/dist/sitemap.xml +15 -15
  50. package/dist-cli/{check-compiler-SP7FAL7R.js → check-compiler-HPF2T2FS.js} +1 -1
  51. package/dist-cli/{check-query-propagation-BRLSHP22.js → check-query-propagation-HYSLTXAB.js} +1 -1
  52. package/dist-cli/{chunk-RQQ42YCP.js → chunk-WLUKAW3H.js} +1025 -158
  53. package/dist-cli/forgecad.js +2621 -232
  54. package/dist-cli/{forgecad_geometry-7TVSNVUB.js → forgecad_geometry-2IMYCUWW.js} +0 -8
  55. package/dist-cli/forgecad_geometry_bg.wasm +0 -0
  56. package/dist-skill/CONTEXT.md +85 -73
  57. package/dist-skill/SKILL.md +1 -1
  58. package/dist-skill/docs/CLI.md +9 -7
  59. package/dist-skill/docs/generated/assembly.md +1 -1
  60. package/dist-skill/docs/generated/core.md +70 -1
  61. package/dist-skill/docs/generated/curves.md +8 -1
  62. package/dist-skill/docs/generated/output.md +0 -64
  63. package/dist-skill/docs/generated/runtime-names.md +6 -6
  64. package/dist-skill/docs/generated/viewport.md +3 -12
  65. package/dist-skill/docs/guides/inspection-bundles.md +1 -1
  66. package/dist-skill/library/README.md +2 -3
  67. package/dist-skill/library/forgecad-blockout-model/SKILL.md +1 -1
  68. package/dist-skill/library/forgecad-image-replicator/SKILL.md +2 -2
  69. package/dist-skill/library/forgecad-mujoco-verify/SKILL.md +66 -0
  70. package/dist-skill/library/forgecad-mujoco-verify/scripts/mujoco_verify.py +385 -0
  71. package/dist-skill/library/forgecad-spec-by-walking-through-it/SKILL.md +132 -0
  72. package/dist-skill/library/forgecad-visual-spec/SKILL.md +1 -1
  73. package/dist-skill/website/skills/forgecad-blockout-model.md +1 -1
  74. package/dist-skill/website/skills/forgecad-image-replicator.md +2 -2
  75. package/dist-skill/website/skills/forgecad-mujoco-verify.md +78 -0
  76. package/dist-skill/website/skills/forgecad-spec-by-walking-through-it.md +145 -0
  77. package/dist-skill/website/skills/forgecad-visual-spec.md +1 -1
  78. package/dist-skill/website/skills/forgecad.md +24 -24
  79. package/dist-skill/website/skills/index.md +2 -3
  80. package/examples/analysis/clearance-fit.forge.js +31 -0
  81. package/examples/analysis/lever-arm-actuator.forge.js +43 -0
  82. package/examples/analysis/tipping-tripod.forge.js +35 -0
  83. package/examples/products/sportscar.forge.js +77 -0
  84. package/package.json +1 -3
  85. package/dist/docs-raw/skills/forgecad-high-level-spec.md +0 -101
  86. package/dist/docs-raw/skills/forgecad-lld.md +0 -41
  87. package/dist/docs-raw/skills/forgecad-prepare-prompt.md +0 -63
  88. package/dist-skill/library/forgecad-high-level-spec/SKILL.md +0 -94
  89. package/dist-skill/library/forgecad-lld/SKILL.md +0 -34
  90. package/dist-skill/library/forgecad-prepare-prompt/SKILL.md +0 -50
  91. package/dist-skill/website/skills/forgecad-high-level-spec.md +0 -101
  92. package/dist-skill/website/skills/forgecad-lld.md +0 -41
  93. package/dist-skill/website/skills/forgecad-prepare-prompt.md +0 -63
  94. /package/dist-skill/library/{forgecad-prepare-prompt → forgecad-spec-by-walking-through-it}/references/default-profiles.md +0 -0
  95. /package/dist-skill/library/{forgecad-prepare-prompt → forgecad-spec-by-walking-through-it}/references/master-prompt.md +0 -0
@@ -58,7 +58,6 @@ import {
58
58
  getCollectedIntentionalOverlaps,
59
59
  getCollectedJointsView,
60
60
  getCollectedPhysicalComponentExpectations,
61
- getCollectedRobotExport,
62
61
  getCompilerRegressionCorpusPart,
63
62
  getDesignTraceInputs,
64
63
  getDesignTraceUsedBy,
@@ -135,11 +134,11 @@ import {
135
134
  updateConstraintValue,
136
135
  validateSimulationModel,
137
136
  wrapOCCTShapeBackend
138
- } from "./chunk-RQQ42YCP.js";
137
+ } from "./chunk-WLUKAW3H.js";
139
138
 
140
139
  // cli/forgecad.ts
141
140
  import { Command, Flags, Help, flush as flushOclif, handle as handleOclif, run as runOclif } from "@oclif/core";
142
- import { readFileSync as readFileSync26 } from "fs";
141
+ import { readFileSync as readFileSync28 } from "fs";
143
142
 
144
143
  // cli/check-api-contracts.ts
145
144
  import assert from "assert/strict";
@@ -5065,12 +5064,6 @@ function applyNonPartExpectations(entryPath, result, expectations) {
5065
5064
  assertMinimum(entryPath, result.cutPlanes.length, expectations.minCutPlanes, "cut plane(s)");
5066
5065
  assertMinimum(entryPath, jointCount, expectations.minJoints, "joint control(s)");
5067
5066
  assertMinimum(entryPath, animationCount, expectations.minAnimations, "joint animation(s)");
5068
- if (expectations.requireRobotExport || expectations.minRobotParts != null || expectations.minRobotJoints != null) {
5069
- assert4(result.robotExport, `${entryPath}: expected robotExport(...) data to stay available to the example gate`);
5070
- if (!result.robotExport) return;
5071
- assertMinimum(entryPath, result.robotExport.assembly.parts.length, expectations.minRobotParts, "robot part(s)");
5072
- assertMinimum(entryPath, result.robotExport.assembly.joints.length, expectations.minRobotJoints, "robot joint(s)");
5073
- }
5074
5067
  }
5075
5068
  function validateAssemblyEntry(entry) {
5076
5069
  const result = executeExample(entry);
@@ -5536,8 +5529,8 @@ function requireSingleInputForOutputPath(inputPaths, outputPath, flag = "--outpu
5536
5529
  throw new Error(`Cannot use ${flag} <${valueKind}> with multiple inputs. Omit ${flag} to use per-input defaults.`);
5537
5530
  }
5538
5531
  }
5539
- function batchOutputPath(inputPath, explicitOutputPath, defaultOutputPath12) {
5540
- return explicitOutputPath ?? defaultOutputPath12(inputPath);
5532
+ function batchOutputPath(inputPath, explicitOutputPath, defaultOutputPath13) {
5533
+ return explicitOutputPath ?? defaultOutputPath13(inputPath);
5541
5534
  }
5542
5535
  function printBatchHeader(inputPath, index, total) {
5543
5536
  if (total <= 1) return;
@@ -14743,10 +14736,10 @@ function polygonCentroid(points) {
14743
14736
  for (let i = 0; i < points.length; i += 1) {
14744
14737
  const [x1, y1] = points[i];
14745
14738
  const [x2, y2] = points[(i + 1) % points.length];
14746
- const cross7 = x1 * y2 - x2 * y1;
14747
- a2 += cross7;
14748
- cx += (x1 + x2) * cross7;
14749
- cy += (y1 + y2) * cross7;
14739
+ const cross8 = x1 * y2 - x2 * y1;
14740
+ a2 += cross8;
14741
+ cx += (x1 + x2) * cross8;
14742
+ cy += (y1 + y2) * cross8;
14750
14743
  }
14751
14744
  if (Math.abs(a2) < EPS5) {
14752
14745
  return points.length > 0 ? [points.reduce((sum2, p) => sum2 + p[0], 0) / points.length, points.reduce((sum2, p) => sum2 + p[1], 0) / points.length] : [0, 0];
@@ -17080,7 +17073,6 @@ var ESTABLISHED_LOWERCASE_RUNTIME_GLOBALS = /* @__PURE__ */ new Set([
17080
17073
  "sketchToDxf",
17081
17074
  "bom",
17082
17075
  "sheetStock",
17083
- "robotExport",
17084
17076
  "group",
17085
17077
  "console",
17086
17078
  "cutPlane",
@@ -17293,7 +17285,7 @@ function checkRuntimeNamesDoc(runtimeNames) {
17293
17285
  const compatibilityList = renderRuntimeNameList(collisionReservedNames.filter((name) => SKILL_SUPPRESSED_RUNTIME_NAMES.has(name)));
17294
17286
  let doc;
17295
17287
  try {
17296
- doc = readText("docs/permanent/generated/runtime-names.md");
17288
+ doc = readText("docs/skill/generated/runtime-names.md");
17297
17289
  } catch {
17298
17290
  return "Generated runtime names doc is missing. Run npm run gen:docs.";
17299
17291
  }
@@ -17372,6 +17364,7 @@ import { readFileSync as readFileSync10 } from "fs";
17372
17364
  import { resolve as resolve12 } from "path";
17373
17365
  function parseArgs4(argv) {
17374
17366
  const inputPaths = [];
17367
+ const paramOverrides = {};
17375
17368
  const jointOverrides = {};
17376
17369
  let json = false;
17377
17370
  let compact = false;
@@ -17397,14 +17390,24 @@ function parseArgs4(argv) {
17397
17390
  i += 1;
17398
17391
  continue;
17399
17392
  }
17393
+ if (arg === "--param" || arg === "-p") {
17394
+ const value = argv[i + 1];
17395
+ if (!value || value.startsWith("--")) throw new Error(`${arg} requires Key=Value`);
17396
+ addCliParamOverride(value, paramOverrides);
17397
+ i += 1;
17398
+ continue;
17399
+ }
17400
17400
  if (arg.startsWith("--")) {
17401
17401
  throw new Error(`Unknown flag: ${arg}`);
17402
17402
  }
17403
17403
  inputPaths.push(arg);
17404
17404
  }
17405
- requireInputPaths(inputPaths, "Usage: forgecad check simready <script.forge.js> [input ...] [--json] [--joint JointName=Value]");
17405
+ requireInputPaths(
17406
+ inputPaths,
17407
+ "Usage: forgecad check simready <script.forge.js> [input ...] [--json] [--param Key=Value] [--joint JointName=Value]"
17408
+ );
17406
17409
  requireScriptInputPaths(inputPaths);
17407
- return { inputPaths, json, compact, noFailExit, jointOverrides };
17410
+ return { inputPaths, json, compact, noFailExit, paramOverrides, jointOverrides };
17408
17411
  }
17409
17412
  function finding2(code, message, path5) {
17410
17413
  return { level: "error", code, message, path: path5 };
@@ -17422,10 +17425,6 @@ function failedReport(source, modelName, errorMessage2, code = "SIM.CHECK.ERROR"
17422
17425
  };
17423
17426
  }
17424
17427
  function resolveSimulationModel(result, jointOverrides) {
17425
- const legacy = getCollectedRobotExport();
17426
- if (legacy) {
17427
- return { ...legacy, state: { ...legacy.state, ...jointOverrides } };
17428
- }
17429
17428
  const assembly2 = getRunResultAssemblySource(result);
17430
17429
  if (!assembly2) return null;
17431
17430
  return collectSimulationModel(assembly2.describe(), { state: jointOverrides });
@@ -17477,6 +17476,7 @@ function runCheckSimReadyInput(options, scriptPath) {
17477
17476
  const scriptAbs = resolve12(scriptPath);
17478
17477
  const code = readFileSync10(scriptAbs, "utf-8");
17479
17478
  const { allFiles, fileName } = collectProjectFiles(scriptAbs);
17479
+ setParamOverrides(options.paramOverrides);
17480
17480
  const runResult = applyCliJointOverrides(
17481
17481
  runScript(code, fileName, allFiles, { assemblyState: options.jointOverrides }),
17482
17482
  options.jointOverrides
@@ -17488,12 +17488,7 @@ function runCheckSimReadyInput(options, scriptPath) {
17488
17488
  try {
17489
17489
  const model = resolveSimulationModel(runResult, options.jointOverrides);
17490
17490
  if (!model) {
17491
- report = failedReport(
17492
- scriptPath,
17493
- scriptPath,
17494
- "Model must return assembly(...).withSimulation(...), or call legacy robotExport({...}).",
17495
- "SIM.SIMULATION.MISSING"
17496
- );
17491
+ report = failedReport(scriptPath, scriptPath, "Model must return assembly(...).withSimulation(...).", "SIM.SIMULATION.MISSING");
17497
17492
  } else {
17498
17493
  report = { source: scriptPath, ...validateSimulationModel(model) };
17499
17494
  }
@@ -17504,6 +17499,647 @@ function runCheckSimReadyInput(options, scriptPath) {
17504
17499
  return report;
17505
17500
  }
17506
17501
 
17502
+ // src/forge/analysis/massProperties.ts
17503
+ function meshSurfaceAreaMm2(mesh) {
17504
+ const { numProp, numTri, triVerts, vertProperties } = mesh;
17505
+ let area = 0;
17506
+ for (let t = 0; t < numTri; t += 1) {
17507
+ const i0 = triVerts[t * 3] * numProp;
17508
+ const i1 = triVerts[t * 3 + 1] * numProp;
17509
+ const i2 = triVerts[t * 3 + 2] * numProp;
17510
+ const e1x = vertProperties[i1] - vertProperties[i0];
17511
+ const e1y = vertProperties[i1 + 1] - vertProperties[i0 + 1];
17512
+ const e1z = vertProperties[i1 + 2] - vertProperties[i0 + 2];
17513
+ const e2x = vertProperties[i2] - vertProperties[i0];
17514
+ const e2y = vertProperties[i2 + 1] - vertProperties[i0 + 1];
17515
+ const e2z = vertProperties[i2 + 2] - vertProperties[i0 + 2];
17516
+ const cx = e1y * e2z - e1z * e2y;
17517
+ const cy = e1z * e2x - e1x * e2z;
17518
+ const cz = e1x * e2y - e1y * e2x;
17519
+ area += 0.5 * Math.sqrt(cx * cx + cy * cy + cz * cz);
17520
+ }
17521
+ return area;
17522
+ }
17523
+ function accumulateBounds(mesh, min, max) {
17524
+ const { numProp, vertProperties } = mesh;
17525
+ const numVerts = vertProperties.length / numProp;
17526
+ for (let v = 0; v < numVerts; v += 1) {
17527
+ for (let axis = 0; axis < 3; axis += 1) {
17528
+ const value = vertProperties[v * numProp + axis];
17529
+ if (value < min[axis]) min[axis] = value;
17530
+ if (value > max[axis]) max[axis] = value;
17531
+ }
17532
+ }
17533
+ }
17534
+ function symmetricEigen3(m) {
17535
+ const a = [
17536
+ [m[0][0], m[0][1], m[0][2]],
17537
+ [m[1][0], m[1][1], m[1][2]],
17538
+ [m[2][0], m[2][1], m[2][2]]
17539
+ ];
17540
+ const v = [
17541
+ [1, 0, 0],
17542
+ [0, 1, 0],
17543
+ [0, 0, 1]
17544
+ ];
17545
+ for (let sweep = 0; sweep < 100; sweep += 1) {
17546
+ const off = Math.abs(a[0][1]) + Math.abs(a[0][2]) + Math.abs(a[1][2]);
17547
+ if (off < 1e-20) break;
17548
+ for (let p = 0; p < 2; p += 1) {
17549
+ for (let q = p + 1; q < 3; q += 1) {
17550
+ if (Math.abs(a[p][q]) < 1e-300) continue;
17551
+ const theta = (a[q][q] - a[p][p]) / (2 * a[p][q]);
17552
+ const t = Math.sign(theta || 1) / (Math.abs(theta) + Math.sqrt(theta * theta + 1));
17553
+ const c = 1 / Math.sqrt(t * t + 1);
17554
+ const s = t * c;
17555
+ for (let i = 0; i < 3; i += 1) {
17556
+ const aip = a[i][p];
17557
+ const aiq = a[i][q];
17558
+ a[i][p] = c * aip - s * aiq;
17559
+ a[i][q] = s * aip + c * aiq;
17560
+ }
17561
+ for (let i = 0; i < 3; i += 1) {
17562
+ const api = a[p][i];
17563
+ const aqi = a[q][i];
17564
+ a[p][i] = c * api - s * aqi;
17565
+ a[q][i] = s * api + c * aqi;
17566
+ }
17567
+ for (let i = 0; i < 3; i += 1) {
17568
+ const vip = v[i][p];
17569
+ const viq = v[i][q];
17570
+ v[i][p] = c * vip - s * viq;
17571
+ v[i][q] = s * vip + c * viq;
17572
+ }
17573
+ }
17574
+ }
17575
+ }
17576
+ const eigen = [
17577
+ { value: a[0][0], vector: [v[0][0], v[1][0], v[2][0]] },
17578
+ { value: a[1][1], vector: [v[0][1], v[1][1], v[2][1]] },
17579
+ { value: a[2][2], vector: [v[0][2], v[1][2], v[2][2]] }
17580
+ ];
17581
+ eigen.sort((x, y) => y.value - x.value);
17582
+ return {
17583
+ values: [eigen[0].value, eigen[1].value, eigen[2].value],
17584
+ vectors: [eigen[0].vector, eigen[1].vector, eigen[2].vector]
17585
+ };
17586
+ }
17587
+ function analyzeMassProperties(objects) {
17588
+ if (objects.length === 0) {
17589
+ throw new Error("analyzeMassProperties requires at least one object");
17590
+ }
17591
+ const perObject = [];
17592
+ let surfaceAreaMm2 = 0;
17593
+ const min = [Infinity, Infinity, Infinity];
17594
+ const max = [-Infinity, -Infinity, -Infinity];
17595
+ const inertiaAboutOwnCom = [];
17596
+ for (const obj of objects) {
17597
+ const massPlaceholder = 1;
17598
+ const probe = computeMeshInertia(obj.mesh, massPlaceholder);
17599
+ const volumeMm3 = probe.volumeMm3;
17600
+ const volumeM3 = volumeMm3 * 1e-9;
17601
+ const massKg = obj.densityKgM3 * volumeM3;
17602
+ inertiaAboutOwnCom.push({
17603
+ ixx: probe.ixx * massKg,
17604
+ iyy: probe.iyy * massKg,
17605
+ izz: probe.izz * massKg,
17606
+ ixy: probe.ixy * massKg,
17607
+ ixz: probe.ixz * massKg,
17608
+ iyz: probe.iyz * massKg
17609
+ });
17610
+ perObject.push({
17611
+ name: obj.name,
17612
+ volumeMm3,
17613
+ massKg,
17614
+ densityKgM3: obj.densityKgM3,
17615
+ centerOfMassMm: probe.centerOfMass
17616
+ });
17617
+ surfaceAreaMm2 += meshSurfaceAreaMm2(obj.mesh);
17618
+ accumulateBounds(obj.mesh, min, max);
17619
+ }
17620
+ const totalVolumeMm3 = perObject.reduce((sum2, o) => sum2 + o.volumeMm3, 0);
17621
+ const totalMassKg = perObject.reduce((sum2, o) => sum2 + o.massKg, 0);
17622
+ const weights = totalMassKg > 0 ? perObject.map((o) => o.massKg) : perObject.map((o) => o.volumeMm3);
17623
+ const weightTotal = weights.reduce((a, b) => a + b, 0) || 1;
17624
+ const com = [0, 0, 0];
17625
+ perObject.forEach((o, i) => {
17626
+ com[0] += weights[i] * o.centerOfMassMm[0] / weightTotal;
17627
+ com[1] += weights[i] * o.centerOfMassMm[1] / weightTotal;
17628
+ com[2] += weights[i] * o.centerOfMassMm[2] / weightTotal;
17629
+ });
17630
+ const tensor = { ixx: 0, iyy: 0, izz: 0, ixy: 0, ixz: 0, iyz: 0 };
17631
+ perObject.forEach((o, i) => {
17632
+ const own = inertiaAboutOwnCom[i];
17633
+ const dx = (o.centerOfMassMm[0] - com[0]) * 1e-3;
17634
+ const dy = (o.centerOfMassMm[1] - com[1]) * 1e-3;
17635
+ const dz = (o.centerOfMassMm[2] - com[2]) * 1e-3;
17636
+ const m = o.massKg;
17637
+ tensor.ixx += own.ixx + m * (dy * dy + dz * dz);
17638
+ tensor.iyy += own.iyy + m * (dx * dx + dz * dz);
17639
+ tensor.izz += own.izz + m * (dx * dx + dy * dy);
17640
+ tensor.ixy += own.ixy - m * dx * dy;
17641
+ tensor.ixz += own.ixz - m * dx * dz;
17642
+ tensor.iyz += own.iyz - m * dy * dz;
17643
+ });
17644
+ const eig = symmetricEigen3([
17645
+ [tensor.ixx, tensor.ixy, tensor.ixz],
17646
+ [tensor.ixy, tensor.iyy, tensor.iyz],
17647
+ [tensor.ixz, tensor.iyz, tensor.izz]
17648
+ ]);
17649
+ const principalMomentsKgM2 = eig.values;
17650
+ const radiusOfGyrationM = totalMassKg > 0 ? [
17651
+ Math.sqrt(Math.max(0, principalMomentsKgM2[0]) / totalMassKg),
17652
+ Math.sqrt(Math.max(0, principalMomentsKgM2[1]) / totalMassKg),
17653
+ Math.sqrt(Math.max(0, principalMomentsKgM2[2]) / totalMassKg)
17654
+ ] : [0, 0, 0];
17655
+ const size = [max[0] - min[0], max[1] - min[1], max[2] - min[2]];
17656
+ return {
17657
+ totalVolumeMm3,
17658
+ totalMassKg,
17659
+ centerOfMassMm: com,
17660
+ inertiaTensorKgM2: tensor,
17661
+ principalMomentsKgM2,
17662
+ principalAxes: eig.vectors,
17663
+ radiusOfGyrationM,
17664
+ surfaceAreaMm2,
17665
+ boundingBoxMm: { minMm: min, maxMm: max, sizeMm: size },
17666
+ perObject
17667
+ };
17668
+ }
17669
+
17670
+ // src/forge/analysis/stability.ts
17671
+ function axisIndex(up) {
17672
+ switch (up) {
17673
+ case "x":
17674
+ return { upIdx: 0, planeA: 1, planeB: 2 };
17675
+ case "y":
17676
+ return { upIdx: 1, planeA: 2, planeB: 0 };
17677
+ default:
17678
+ return { upIdx: 2, planeA: 0, planeB: 1 };
17679
+ }
17680
+ }
17681
+ function convexHull2D(points) {
17682
+ const pts = points.slice().sort((p, q) => p[0] === q[0] ? p[1] - q[1] : p[0] - q[0]);
17683
+ const unique = [];
17684
+ for (const p of pts) {
17685
+ const last = unique[unique.length - 1];
17686
+ if (!last || last[0] !== p[0] || last[1] !== p[1]) unique.push(p);
17687
+ }
17688
+ if (unique.length < 3) return unique;
17689
+ const cross8 = (o, a, b) => (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
17690
+ const lower2 = [];
17691
+ for (const p of unique) {
17692
+ while (lower2.length >= 2 && cross8(lower2[lower2.length - 2], lower2[lower2.length - 1], p) <= 0) lower2.pop();
17693
+ lower2.push(p);
17694
+ }
17695
+ const upper = [];
17696
+ for (let i = unique.length - 1; i >= 0; i -= 1) {
17697
+ const p = unique[i];
17698
+ while (upper.length >= 2 && cross8(upper[upper.length - 2], upper[upper.length - 1], p) <= 0) upper.pop();
17699
+ upper.push(p);
17700
+ }
17701
+ lower2.pop();
17702
+ upper.pop();
17703
+ return lower2.concat(upper);
17704
+ }
17705
+ function distancePointToSegment(p, a, b) {
17706
+ const dx = b[0] - a[0];
17707
+ const dy = b[1] - a[1];
17708
+ const lenSq = dx * dx + dy * dy;
17709
+ if (lenSq < 1e-18) return Math.hypot(p[0] - a[0], p[1] - a[1]);
17710
+ let t = ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / lenSq;
17711
+ t = Math.max(0, Math.min(1, t));
17712
+ return Math.hypot(p[0] - (a[0] + t * dx), p[1] - (a[1] + t * dy));
17713
+ }
17714
+ function signedDistanceToPolygon(p, poly) {
17715
+ let minDist = Infinity;
17716
+ let nearestEdge = 0;
17717
+ let inside = true;
17718
+ for (let i = 0; i < poly.length; i += 1) {
17719
+ const a = poly[i];
17720
+ const b = poly[(i + 1) % poly.length];
17721
+ const cross8 = (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0]);
17722
+ if (cross8 < 0) inside = false;
17723
+ const d = distancePointToSegment(p, a, b);
17724
+ if (d < minDist) {
17725
+ minDist = d;
17726
+ nearestEdge = i;
17727
+ }
17728
+ }
17729
+ return { distance: inside ? minDist : -minDist, nearestEdge };
17730
+ }
17731
+ function analyzeStability(meshes, centerOfMassMm, options = {}) {
17732
+ const up = options.up ?? "z";
17733
+ const { upIdx, planeA, planeB } = axisIndex(up);
17734
+ const minMarginMm = options.minMarginMm ?? 0;
17735
+ let groundLevelMm = Infinity;
17736
+ for (const mesh of meshes) {
17737
+ const numVerts = mesh.vertProperties.length / mesh.numProp;
17738
+ for (let v = 0; v < numVerts; v += 1) {
17739
+ const value = mesh.vertProperties[v * mesh.numProp + upIdx];
17740
+ if (value < groundLevelMm) groundLevelMm = value;
17741
+ }
17742
+ }
17743
+ if (!Number.isFinite(groundLevelMm)) {
17744
+ throw new Error("analyzeStability: geometry has no vertices");
17745
+ }
17746
+ let contactToleranceMm = options.contactToleranceMm ?? 0;
17747
+ if (!options.contactToleranceMm) {
17748
+ let span = 0;
17749
+ for (const mesh of meshes) {
17750
+ const numVerts = mesh.vertProperties.length / mesh.numProp;
17751
+ for (let v = 0; v < numVerts; v += 1) {
17752
+ const value = mesh.vertProperties[v * mesh.numProp + upIdx];
17753
+ span = Math.max(span, value - groundLevelMm);
17754
+ }
17755
+ }
17756
+ contactToleranceMm = Math.max(0.05, span * 2e-3);
17757
+ }
17758
+ const contactPoints = [];
17759
+ for (const mesh of meshes) {
17760
+ const numVerts = mesh.vertProperties.length / mesh.numProp;
17761
+ for (let v = 0; v < numVerts; v += 1) {
17762
+ const base = v * mesh.numProp;
17763
+ if (mesh.vertProperties[base + upIdx] <= groundLevelMm + contactToleranceMm) {
17764
+ contactPoints.push([mesh.vertProperties[base + planeA], mesh.vertProperties[base + planeB]]);
17765
+ }
17766
+ }
17767
+ }
17768
+ const supportPolygonMm = convexHull2D(contactPoints);
17769
+ const comProjectionMm = [centerOfMassMm[planeA], centerOfMassMm[planeB]];
17770
+ const comHeightMm = centerOfMassMm[upIdx] - groundLevelMm;
17771
+ const degenerate = supportPolygonMm.length < 3;
17772
+ if (degenerate) {
17773
+ return {
17774
+ up,
17775
+ groundLevelMm,
17776
+ centerOfMassMm,
17777
+ comProjectionMm,
17778
+ comHeightMm,
17779
+ supportPolygonMm,
17780
+ contactVertexCount: contactPoints.length,
17781
+ comInsideSupport: false,
17782
+ tipOverMarginMm: 0,
17783
+ tipOverAngleDeg: 0,
17784
+ criticalEdge: null,
17785
+ tipDirection: null,
17786
+ comShiftToTargetMm: null,
17787
+ degenerate: true
17788
+ };
17789
+ }
17790
+ const { distance: distance2, nearestEdge } = signedDistanceToPolygon(comProjectionMm, supportPolygonMm);
17791
+ const a = supportPolygonMm[nearestEdge];
17792
+ const b = supportPolygonMm[(nearestEdge + 1) % supportPolygonMm.length];
17793
+ const ex = b[0] - a[0];
17794
+ const ey = b[1] - a[1];
17795
+ const edgeLen = Math.hypot(ex, ey) || 1;
17796
+ const nx = ey / edgeLen;
17797
+ const ny = -ex / edgeLen;
17798
+ const tipDirection = [nx, ny];
17799
+ const tipOverAngleDeg = comHeightMm > 1e-9 ? Math.atan2(distance2, comHeightMm) * 180 / Math.PI : distance2 >= 0 ? 90 : -90;
17800
+ let comShiftToTargetMm = null;
17801
+ if (distance2 < minMarginMm) {
17802
+ const deficit = minMarginMm - distance2;
17803
+ comShiftToTargetMm = [-nx * deficit, -ny * deficit];
17804
+ }
17805
+ return {
17806
+ up,
17807
+ groundLevelMm,
17808
+ centerOfMassMm,
17809
+ comProjectionMm,
17810
+ comHeightMm,
17811
+ supportPolygonMm,
17812
+ contactVertexCount: contactPoints.length,
17813
+ comInsideSupport: distance2 >= 0,
17814
+ tipOverMarginMm: distance2,
17815
+ tipOverAngleDeg,
17816
+ criticalEdge: { a, b },
17817
+ tipDirection,
17818
+ comShiftToTargetMm,
17819
+ degenerate: false
17820
+ };
17821
+ }
17822
+
17823
+ // src/forge/analysis/feedback.ts
17824
+ var FEEDBACK_SCHEMA = "forgecad.feedback/v1";
17825
+ function evaluateTarget(value, target) {
17826
+ switch (target.op) {
17827
+ case ">=":
17828
+ return value >= target.value;
17829
+ case "<=":
17830
+ return value <= target.value;
17831
+ case ">":
17832
+ return value > target.value;
17833
+ case "<":
17834
+ return value < target.value;
17835
+ case "==":
17836
+ return value === target.value;
17837
+ default:
17838
+ return false;
17839
+ }
17840
+ }
17841
+ function makeMetric(key, label, value, options) {
17842
+ const { unit, target, status: forced, warnFraction = 0.1 } = options;
17843
+ let status = forced ?? "info";
17844
+ if (!forced && target && value !== null && Number.isFinite(value)) {
17845
+ const passed = evaluateTarget(value, target);
17846
+ if (!passed) {
17847
+ status = "fail";
17848
+ } else {
17849
+ const span = Math.abs(target.value) || 1;
17850
+ const slack = Math.abs(value - target.value) / span;
17851
+ status = slack < warnFraction ? "warn" : "pass";
17852
+ }
17853
+ } else if (!forced && value === null) {
17854
+ status = "info";
17855
+ }
17856
+ return target ? { key, label, value, unit, status, target } : { key, label, value, unit, status };
17857
+ }
17858
+ function reportOk(metrics, findings) {
17859
+ if (metrics.some((m) => m.status === "fail")) return false;
17860
+ if (findings.some((f2) => f2.level === "error")) return false;
17861
+ return true;
17862
+ }
17863
+ function statusGlyph(status) {
17864
+ switch (status) {
17865
+ case "pass":
17866
+ return "\u2713";
17867
+ case "warn":
17868
+ return "\u26A0";
17869
+ case "fail":
17870
+ return "\u2717";
17871
+ default:
17872
+ return "\xB7";
17873
+ }
17874
+ }
17875
+ function formatValue(value) {
17876
+ if (value === null || !Number.isFinite(value)) return "n/a";
17877
+ const abs = Math.abs(value);
17878
+ if (abs !== 0 && (abs < 1e-3 || abs >= 1e6)) return value.toExponential(3);
17879
+ return Number.parseFloat(value.toPrecision(6)).toString();
17880
+ }
17881
+ function renderFeedbackText(report) {
17882
+ const lines = [];
17883
+ const head = report.ok ? "\u2713" : "\u2717";
17884
+ lines.push(`${head} ${report.category}: ${report.source}`);
17885
+ lines.push(` ${report.summary}`);
17886
+ for (const metric of report.metrics) {
17887
+ const target = metric.target ? ` (target ${metric.target.op} ${formatValue(metric.target.value)} ${metric.unit})` : "";
17888
+ lines.push(` ${statusGlyph(metric.status)} ${metric.label}: ${formatValue(metric.value)} ${metric.unit}${target}`);
17889
+ }
17890
+ for (const finding3 of report.findings) {
17891
+ const glyph = finding3.level === "error" ? "\u2717" : finding3.level === "warning" ? "\u26A0" : "\xB7";
17892
+ const suggest = finding3.suggest ? ` \u2192 ${finding3.suggest}` : "";
17893
+ const path5 = finding3.path ? ` (${finding3.path})` : "";
17894
+ lines.push(` ${glyph} [${finding3.code}] ${finding3.message}${path5}${suggest}`);
17895
+ }
17896
+ if (report.trust.assumptions.length > 0) {
17897
+ lines.push(` assumptions: ${report.trust.assumptions.join("; ")}`);
17898
+ }
17899
+ return lines.join("\n");
17900
+ }
17901
+
17902
+ // cli/analysis-shared.ts
17903
+ async function loadAnalysisObjectsWithAssembly(scriptPath, options) {
17904
+ const input = loadCliScriptInput(scriptPath);
17905
+ await initCliBackend(resolveCliBackend(options.backend, input) ?? CLI_DEFAULT_BACKEND);
17906
+ const qualityPreset = options.quality && options.quality !== "default" ? options.quality : void 0;
17907
+ setParamOverrides(options.paramOverrides);
17908
+ const runResult = applyCliJointOverrides(
17909
+ runScript(input.code, input.fileName, input.allFiles, {
17910
+ ...qualityPreset ? { quality: qualityPreset } : {},
17911
+ readBinaryFile: input.readBinaryFile,
17912
+ assemblyState: options.jointOverrides
17913
+ }),
17914
+ options.jointOverrides
17915
+ );
17916
+ if (runResult.error) {
17917
+ return { objects: [], assembly: null, runResult, error: runResult.error };
17918
+ }
17919
+ const objects = runResult.objects.filter((obj) => obj.shape && !obj.mock).map((obj) => ({ name: obj.name, mesh: obj.shape.getMesh() }));
17920
+ if (objects.length === 0) {
17921
+ return { objects: [], assembly: null, runResult, error: "No solid geometry found in the model." };
17922
+ }
17923
+ return { objects, assembly: getRunResultAssemblySource(runResult), runResult, error: null };
17924
+ }
17925
+ async function loadAnalysisObjects(scriptPath, options) {
17926
+ const { objects, error } = await loadAnalysisObjectsWithAssembly(scriptPath, options);
17927
+ return { objects, error };
17928
+ }
17929
+ function printFeedback(report, json, compact) {
17930
+ if (json) {
17931
+ console.log(JSON.stringify(report, null, compact ? 0 : 2));
17932
+ return;
17933
+ }
17934
+ const reports = "runs" in report ? report.runs : [report];
17935
+ for (const entry of reports) {
17936
+ const text = renderFeedbackText(entry);
17937
+ if (entry.ok) console.log(text);
17938
+ else console.error(text);
17939
+ }
17940
+ }
17941
+
17942
+ // cli/check-stability.ts
17943
+ function parseArgs5(argv) {
17944
+ const inputPaths = [];
17945
+ const paramOverrides = {};
17946
+ const jointOverrides = {};
17947
+ let json = false;
17948
+ let compact = false;
17949
+ let minMarginMm = 0;
17950
+ let up = "z";
17951
+ let contactToleranceMm = null;
17952
+ let noFailExit = false;
17953
+ let backend;
17954
+ let quality;
17955
+ for (let i = 0; i < argv.length; i += 1) {
17956
+ const arg = argv[i];
17957
+ if (arg === "--json") {
17958
+ json = true;
17959
+ continue;
17960
+ }
17961
+ if (arg === "--compact") {
17962
+ compact = true;
17963
+ continue;
17964
+ }
17965
+ if (arg === "--no-fail-exit") {
17966
+ noFailExit = true;
17967
+ continue;
17968
+ }
17969
+ if (arg === "--min-margin-mm") {
17970
+ const value = Number(argv[i + 1]);
17971
+ if (!Number.isFinite(value)) throw new Error("--min-margin-mm requires a number");
17972
+ minMarginMm = value;
17973
+ i += 1;
17974
+ continue;
17975
+ }
17976
+ if (arg === "--up") {
17977
+ const val = argv[i + 1];
17978
+ if (val !== "x" && val !== "y" && val !== "z") throw new Error("--up must be x, y, or z");
17979
+ up = val;
17980
+ i += 1;
17981
+ continue;
17982
+ }
17983
+ if (arg === "--contact-tol") {
17984
+ const value = Number(argv[i + 1]);
17985
+ if (!Number.isFinite(value) || value < 0) throw new Error("--contact-tol requires a non-negative number (mm)");
17986
+ contactToleranceMm = value;
17987
+ i += 1;
17988
+ continue;
17989
+ }
17990
+ if (arg === "--quality" || arg === "-q") {
17991
+ const val = argv[i + 1];
17992
+ if (val !== "default" && val !== "live" && val !== "high") throw new Error("--quality must be default, live, or high");
17993
+ quality = val;
17994
+ i += 1;
17995
+ continue;
17996
+ }
17997
+ if (arg === "--backend") {
17998
+ const val = argv[i + 1];
17999
+ if (val !== "manifold" && val !== "occt" && val !== "truck" && val !== "sdf") {
18000
+ throw new Error("--backend must be manifold, occt, truck, or sdf");
18001
+ }
18002
+ backend = val;
18003
+ i += 1;
18004
+ continue;
18005
+ }
18006
+ if (arg === "--joint") {
18007
+ const value = argv[i + 1];
18008
+ if (!value || value.startsWith("--")) throw new Error("--joint requires JointName=Value");
18009
+ addCliJointOverride(value, jointOverrides);
18010
+ i += 1;
18011
+ continue;
18012
+ }
18013
+ if (arg === "--param" || arg === "-p") {
18014
+ const value = argv[i + 1];
18015
+ if (!value || value.startsWith("--")) throw new Error(`${arg} requires Key=Value`);
18016
+ addCliParamOverride(value, paramOverrides);
18017
+ i += 1;
18018
+ continue;
18019
+ }
18020
+ if (arg.startsWith("--")) throw new Error(`Unknown flag: ${arg}`);
18021
+ inputPaths.push(arg);
18022
+ }
18023
+ requireInputPaths(
18024
+ inputPaths,
18025
+ "Usage: forgecad check stability <model.forge.js> [input ...] [--min-margin-mm N] [--up x|y|z] [--json] [--param Key=Value]"
18026
+ );
18027
+ requireScriptInputPaths(inputPaths);
18028
+ return { inputPaths, json, compact, minMarginMm, up, contactToleranceMm, noFailExit, backend, quality, paramOverrides, jointOverrides };
18029
+ }
18030
+ function failedReport2(source, message, code = "STABILITY.ERROR") {
18031
+ return {
18032
+ schema: FEEDBACK_SCHEMA,
18033
+ category: "stability",
18034
+ source,
18035
+ ok: false,
18036
+ summary: "stability analysis failed",
18037
+ metrics: [],
18038
+ findings: [{ level: "error", code, message }],
18039
+ trust: { assumptions: [] },
18040
+ data: {},
18041
+ timeMs: 0
18042
+ };
18043
+ }
18044
+ function round3(value, digits = 4) {
18045
+ const factor = 10 ** digits;
18046
+ return Math.round(value * factor) / factor;
18047
+ }
18048
+ function analyze(source, options, objects) {
18049
+ const start = Date.now();
18050
+ const props = analyzeMassProperties(objects.map((o) => ({ name: o.name, mesh: o.mesh, densityKgM3: 1e3 })));
18051
+ const stab = analyzeStability(objects.map((o) => o.mesh), props.centerOfMassMm, {
18052
+ up: options.up,
18053
+ minMarginMm: options.minMarginMm,
18054
+ contactToleranceMm: options.contactToleranceMm ?? void 0
18055
+ });
18056
+ const findings = [];
18057
+ const assumptions = [
18058
+ "Rigid body at rest under gravity; center of mass is the uniform-density volume centroid.",
18059
+ `Up axis is +${options.up}; the support footprint is the convex hull of the lowest geometry.`
18060
+ ];
18061
+ if (stab.degenerate) {
18062
+ findings.push({
18063
+ level: "error",
18064
+ code: "STABILITY.SUPPORT.DEGENERATE",
18065
+ message: `Support footprint collapses to ${stab.contactVertexCount === 0 ? "no" : "a line/point of"} contact \u2014 no stable base.`,
18066
+ suggest: "Add a flat resting face, widen the base, or check that the model rests on the ground plane."
18067
+ });
18068
+ } else if (stab.tipOverMarginMm < options.minMarginMm) {
18069
+ const dir = stab.comShiftToTargetMm;
18070
+ findings.push({
18071
+ level: "error",
18072
+ code: stab.comInsideSupport ? "STABILITY.MARGIN.LOW" : "STABILITY.TIPS_OVER",
18073
+ message: stab.comInsideSupport ? `Tip-over margin ${round3(stab.tipOverMarginMm, 2)} mm is below the required ${options.minMarginMm} mm.` : `Center of mass falls outside the support polygon by ${round3(-stab.tipOverMarginMm, 2)} mm \u2014 the part tips over.`,
18074
+ suggest: dir ? `Shift the center of mass by (${round3(dir[0], 1)}, ${round3(dir[1], 1)}) mm in the ground plane \u2014 widen the base, move mass inboard, or add ballast on the opposite side.` : "Widen the base or move mass toward the support center."
18075
+ });
18076
+ }
18077
+ const metrics = [
18078
+ makeMetric("tipOverMarginMm", "Tip-over margin", round3(stab.tipOverMarginMm, 3), {
18079
+ unit: "mm",
18080
+ target: { op: ">=", value: options.minMarginMm },
18081
+ status: stab.degenerate ? "fail" : void 0
18082
+ }),
18083
+ makeMetric("tipOverAngleDeg", "Tip-over angle", round3(stab.tipOverAngleDeg, 3), { unit: "deg" }),
18084
+ makeMetric("comHeightMm", "CoM height", round3(stab.comHeightMm, 3), { unit: "mm" }),
18085
+ makeMetric("comInsideSupport", "CoM inside support", stab.comInsideSupport ? 1 : 0, {
18086
+ unit: "bool",
18087
+ target: { op: ">=", value: 1 },
18088
+ status: stab.comInsideSupport ? "pass" : "fail"
18089
+ })
18090
+ ];
18091
+ return {
18092
+ schema: FEEDBACK_SCHEMA,
18093
+ category: "stability",
18094
+ source,
18095
+ ok: reportOk(metrics, findings),
18096
+ summary: stab.degenerate ? "no stable support footprint" : stab.comInsideSupport ? `stable \xB7 margin ${round3(stab.tipOverMarginMm, 2)} mm \xB7 tips at ${round3(stab.tipOverAngleDeg, 1)}\xB0 tilt` : `TIPS OVER \xB7 CoM ${round3(-stab.tipOverMarginMm, 2)} mm past the support edge`,
18097
+ metrics,
18098
+ findings,
18099
+ trust: { assumptions, watertight: true },
18100
+ data: {
18101
+ up: stab.up,
18102
+ groundLevelMm: round3(stab.groundLevelMm, 4),
18103
+ centerOfMassMm: stab.centerOfMassMm.map((v) => round3(v, 4)),
18104
+ comProjectionMm: stab.comProjectionMm.map((v) => round3(v, 4)),
18105
+ comHeightMm: round3(stab.comHeightMm, 4),
18106
+ tipOverMarginMm: round3(stab.tipOverMarginMm, 4),
18107
+ tipOverAngleDeg: round3(stab.tipOverAngleDeg, 4),
18108
+ comInsideSupport: stab.comInsideSupport,
18109
+ contactVertexCount: stab.contactVertexCount,
18110
+ supportPolygonMm: stab.supportPolygonMm.map((p) => [round3(p[0], 3), round3(p[1], 3)]),
18111
+ criticalEdge: stab.criticalEdge ? { a: stab.criticalEdge.a.map((v) => round3(v, 3)), b: stab.criticalEdge.b.map((v) => round3(v, 3)) } : null,
18112
+ tipDirection: stab.tipDirection ? stab.tipDirection.map((v) => round3(v, 5)) : null,
18113
+ comShiftToTargetMm: stab.comShiftToTargetMm ? stab.comShiftToTargetMm.map((v) => round3(v, 3)) : null
18114
+ },
18115
+ timeMs: Date.now() - start
18116
+ };
18117
+ }
18118
+ async function runCheckStabilityCli(argv = process.argv.slice(2)) {
18119
+ const options = parseArgs5(argv);
18120
+ requireExistingInputPaths(options.inputPaths);
18121
+ await init();
18122
+ const reports = [];
18123
+ for (const [index, scriptPath] of options.inputPaths.entries()) {
18124
+ if (!options.json) printBatchHeader(scriptPath, index, options.inputPaths.length);
18125
+ let report;
18126
+ try {
18127
+ const { objects, error } = await loadAnalysisObjects(scriptPath, {
18128
+ backend: options.backend,
18129
+ quality: options.quality,
18130
+ paramOverrides: options.paramOverrides,
18131
+ jointOverrides: options.jointOverrides
18132
+ });
18133
+ report = error ? failedReport2(scriptPath, error, "STABILITY.MODEL.EMPTY") : analyze(scriptPath, options, objects);
18134
+ } catch (error) {
18135
+ report = failedReport2(scriptPath, error instanceof Error ? error.message : String(error));
18136
+ }
18137
+ reports.push(report);
18138
+ }
18139
+ printFeedback(reports.length === 1 ? reports[0] : { runs: reports }, options.json, options.compact);
18140
+ if (reports.some((report) => !report.ok) && !options.noFailExit) process.exitCode = 1;
18141
+ }
18142
+
17507
18143
  // cli/check-text.ts
17508
18144
  import assert6 from "assert/strict";
17509
18145
  var EPS6 = 1e-3;
@@ -18546,11 +19182,12 @@ async function buildStepBlob(objects) {
18546
19182
  }
18547
19183
 
18548
19184
  // cli/forge-brep.ts
18549
- function parseArgs5(argv) {
19185
+ function parseArgs6(argv) {
18550
19186
  let format = "step";
18551
19187
  let backend = "occt";
18552
19188
  let outputPath;
18553
19189
  const inputPaths = [];
19190
+ const paramOverrides = {};
18554
19191
  const jointOverrides = {};
18555
19192
  for (let i = 0; i < argv.length; i += 1) {
18556
19193
  const arg = argv[i];
@@ -18585,6 +19222,13 @@ function parseArgs5(argv) {
18585
19222
  i += 1;
18586
19223
  continue;
18587
19224
  }
19225
+ if (arg === "--param" || arg === "-p") {
19226
+ const value = argv[i + 1];
19227
+ if (!value || value.startsWith("--")) throw new Error(`${arg} requires Key=Value`);
19228
+ addCliParamOverride(value, paramOverrides);
19229
+ i += 1;
19230
+ continue;
19231
+ }
18588
19232
  if (arg === "--python") {
18589
19233
  throw new Error("--python is no longer supported. STEP/BREP export now uses the native OCCT runtime exporter.");
18590
19234
  }
@@ -18603,14 +19247,14 @@ function parseArgs5(argv) {
18603
19247
  }
18604
19248
  requireInputPaths(
18605
19249
  inputPaths,
18606
- "Usage: forgecad export <step|brep> <model.forge.js|asset.step|asset.stp> [input ...] [--output path] [--backend occt|truck] [--joint JointName=Value]"
19250
+ "Usage: forgecad export <step|brep> <model.forge.js|asset.step|asset.stp> [input ...] [--output path] [--backend occt|truck] [--param Key=Value] [--joint JointName=Value]"
18607
19251
  );
18608
19252
  requireExactExportInputPaths(inputPaths);
18609
19253
  requireSingleInputForOutputPath(inputPaths, outputPath);
18610
19254
  if (backend === "truck" && format !== "step") {
18611
19255
  throw new Error("The native Truck BREP exporter currently supports STEP only (use --format step).");
18612
19256
  }
18613
- return { format, backend, outputPath, inputPaths, jointOverrides };
19257
+ return { format, backend, outputPath, inputPaths, paramOverrides, jointOverrides };
18614
19258
  }
18615
19259
  function defaultOutputPath(scriptPath, sourcePath, format) {
18616
19260
  const abs = resolve16(scriptPath);
@@ -18618,8 +19262,9 @@ function defaultOutputPath(scriptPath, sourcePath, format) {
18618
19262
  const target = `${stem}.${format}`;
18619
19263
  return resolve16(target) === resolve16(sourcePath) ? `${stem}.forgecad.${format}` : target;
18620
19264
  }
18621
- async function buildRuntimeExactExportBlob(format, code, fileName, allFiles, readBinaryFile, jointOverrides) {
19265
+ async function buildRuntimeExactExportBlob(format, code, fileName, allFiles, readBinaryFile, paramOverrides, jointOverrides) {
18622
19266
  await activateBackend("occt");
19267
+ setParamOverrides(paramOverrides);
18623
19268
  const result = applyCliJointOverrides(
18624
19269
  runScript(code, fileName, allFiles, { readBinaryFile, assemblyState: jointOverrides }),
18625
19270
  jointOverrides
@@ -18652,8 +19297,9 @@ async function buildRuntimeExactExportBlob(format, code, fileName, allFiles, rea
18652
19297
  objectCount: exactObjects.length
18653
19298
  };
18654
19299
  }
18655
- async function buildTruckStepBlob(code, fileName, allFiles, readBinaryFile, jointOverrides) {
19300
+ async function buildTruckStepBlob(code, fileName, allFiles, readBinaryFile, paramOverrides, jointOverrides) {
18656
19301
  await activateBackend("truck");
19302
+ setParamOverrides(paramOverrides);
18657
19303
  const result = applyCliJointOverrides(
18658
19304
  runScript(code, fileName, allFiles, { readBinaryFile, assemblyState: jointOverrides }),
18659
19305
  jointOverrides
@@ -18669,13 +19315,13 @@ async function buildTruckStepBlob(code, fileName, allFiles, readBinaryFile, join
18669
19315
  return { blob: new Blob([step], { type: "application/step" }), objectCount: handles.length };
18670
19316
  }
18671
19317
  async function runBrepCli(argv = process.argv.slice(2)) {
18672
- const { format, backend, outputPath, inputPaths, jointOverrides } = parseArgs5(argv);
19318
+ const { format, backend, outputPath, inputPaths, paramOverrides, jointOverrides } = parseArgs6(argv);
18673
19319
  requireExistingInputPaths(inputPaths);
18674
19320
  let failures = 0;
18675
19321
  for (const [index, scriptPath] of inputPaths.entries()) {
18676
19322
  printBatchHeader(scriptPath, index, inputPaths.length);
18677
19323
  try {
18678
- await exportBrepInput(format, backend, outputPath, scriptPath, jointOverrides);
19324
+ await exportBrepInput(format, backend, outputPath, scriptPath, paramOverrides, jointOverrides);
18679
19325
  } catch (error) {
18680
19326
  failures += 1;
18681
19327
  console.error(error instanceof Error ? error.message : String(error));
@@ -18685,11 +19331,19 @@ async function runBrepCli(argv = process.argv.slice(2)) {
18685
19331
  process.exit(1);
18686
19332
  }
18687
19333
  }
18688
- async function exportBrepInput(format, backend, outputPath, scriptPath, jointOverrides) {
19334
+ async function exportBrepInput(format, backend, outputPath, scriptPath, paramOverrides, jointOverrides) {
18689
19335
  const input = loadCliScriptInput(scriptPath);
18690
19336
  await init();
18691
19337
  try {
18692
- const exactExport = backend === "truck" ? await buildTruckStepBlob(input.code, input.fileName, input.allFiles, input.readBinaryFile, jointOverrides) : await buildRuntimeExactExportBlob(format, input.code, input.fileName, input.allFiles, input.readBinaryFile, jointOverrides);
19338
+ const exactExport = backend === "truck" ? await buildTruckStepBlob(input.code, input.fileName, input.allFiles, input.readBinaryFile, paramOverrides, jointOverrides) : await buildRuntimeExactExportBlob(
19339
+ format,
19340
+ input.code,
19341
+ input.fileName,
19342
+ input.allFiles,
19343
+ input.readBinaryFile,
19344
+ paramOverrides,
19345
+ jointOverrides
19346
+ );
18693
19347
  if (outputPath && resolve16(outputPath) === input.sourcePath) {
18694
19348
  console.error("Output path would overwrite the input file. Pass a different --output path.");
18695
19349
  process.exit(1);
@@ -18699,6 +19353,7 @@ async function exportBrepInput(format, backend, outputPath, scriptPath, jointOve
18699
19353
  console.log(`\u2713 Exported ${exactExport.objectCount} object(s) to ${finalOutput}`);
18700
19354
  return;
18701
19355
  } catch (runtimeError) {
19356
+ setParamOverrides(paramOverrides);
18702
19357
  const result = applyCliJointOverrides(
18703
19358
  runScript(input.code, input.fileName, input.allFiles, {
18704
19359
  readBinaryFile: input.readBinaryFile,
@@ -19656,7 +20311,7 @@ async function waitForRenderHtml2(port, timeoutMs) {
19656
20311
  const deadline = Date.now() + timeoutMs;
19657
20312
  while (Date.now() < deadline) {
19658
20313
  if (await fetchRenderHtml2(port)) return true;
19659
- await new Promise((resolve44) => setTimeout(resolve44, 250));
20314
+ await new Promise((resolve46) => setTimeout(resolve46, 250));
19660
20315
  }
19661
20316
  return false;
19662
20317
  }
@@ -19710,7 +20365,7 @@ ${detail}` : `Timed out waiting for Vite on port ${port}.`);
19710
20365
  async function stopDevServer2(proc) {
19711
20366
  if (!proc || proc.killed) return;
19712
20367
  proc.kill("SIGTERM");
19713
- await new Promise((resolve44) => setTimeout(resolve44, 150));
20368
+ await new Promise((resolve46) => setTimeout(resolve46, 150));
19714
20369
  if (!proc.killed) {
19715
20370
  proc.kill("SIGKILL");
19716
20371
  }
@@ -20925,9 +21580,9 @@ function toViteArgs(options) {
20925
21580
  return args;
20926
21581
  }
20927
21582
  function waitForExit(child) {
20928
- return new Promise((resolve44, reject) => {
21583
+ return new Promise((resolve46, reject) => {
20929
21584
  child.once("error", reject);
20930
- child.once("exit", (code) => resolve44(code ?? 0));
21585
+ child.once("exit", (code) => resolve46(code ?? 0));
20931
21586
  });
20932
21587
  }
20933
21588
  async function runDevCli(argv = process.argv.slice(2)) {
@@ -20953,9 +21608,10 @@ async function runDevCli(argv = process.argv.slice(2)) {
20953
21608
  // cli/forge-gcode.ts
20954
21609
  import { writeFileSync as writeFileSync6 } from "fs";
20955
21610
  import { extname as extname5, resolve as resolve20 } from "path";
20956
- function parseArgs6(argv) {
21611
+ function parseArgs7(argv) {
20957
21612
  const inputPaths = [];
20958
21613
  let outputPath;
21614
+ const paramOverrides = {};
20959
21615
  const jointOverrides = {};
20960
21616
  for (let i = 0; i < argv.length; i += 1) {
20961
21617
  const arg = argv[i];
@@ -20972,15 +21628,25 @@ function parseArgs6(argv) {
20972
21628
  i += 1;
20973
21629
  continue;
20974
21630
  }
21631
+ if (arg === "--param" || arg === "-p") {
21632
+ const value = argv[i + 1];
21633
+ if (!value || value.startsWith("--")) throw new Error(`${arg} requires Key=Value`);
21634
+ addCliParamOverride(value, paramOverrides);
21635
+ i += 1;
21636
+ continue;
21637
+ }
20975
21638
  if (arg.startsWith("--")) {
20976
21639
  throw new Error(`Unknown flag: ${arg}`);
20977
21640
  }
20978
21641
  inputPaths.push(arg);
20979
21642
  }
20980
- requireInputPaths(inputPaths, "Usage: forgecad export gcode <script.forge.js> [input ...] [--output path] [--joint JointName=Value]");
21643
+ requireInputPaths(
21644
+ inputPaths,
21645
+ "Usage: forgecad export gcode <script.forge.js> [input ...] [--output path] [--param Key=Value] [--joint JointName=Value]"
21646
+ );
20981
21647
  requireScriptInputPaths(inputPaths);
20982
21648
  requireSingleInputForOutputPath(inputPaths, outputPath);
20983
- return { inputPaths, outputPath, jointOverrides };
21649
+ return { inputPaths, outputPath, paramOverrides, jointOverrides };
20984
21650
  }
20985
21651
  function defaultOutputPath3(scriptPath) {
20986
21652
  const abs = resolve20(scriptPath);
@@ -20989,13 +21655,13 @@ function defaultOutputPath3(scriptPath) {
20989
21655
  return `${stem}.gcode`;
20990
21656
  }
20991
21657
  async function runGcodeExportCli(argv) {
20992
- const { inputPaths, outputPath, jointOverrides } = parseArgs6(argv);
21658
+ const { inputPaths, outputPath, paramOverrides, jointOverrides } = parseArgs7(argv);
20993
21659
  requireExistingInputPaths(inputPaths);
20994
21660
  let failures = 0;
20995
21661
  for (const [index, scriptPath] of inputPaths.entries()) {
20996
21662
  printBatchHeader(scriptPath, index, inputPaths.length);
20997
21663
  try {
20998
- await exportGcodeInput(scriptPath, outputPath, jointOverrides);
21664
+ await exportGcodeInput(scriptPath, outputPath, paramOverrides, jointOverrides);
20999
21665
  } catch (error) {
21000
21666
  failures += 1;
21001
21667
  console.error(error instanceof Error ? error.message : String(error));
@@ -21005,10 +21671,11 @@ async function runGcodeExportCli(argv) {
21005
21671
  process.exit(1);
21006
21672
  }
21007
21673
  }
21008
- async function exportGcodeInput(scriptPath, explicitOutputPath, jointOverrides) {
21674
+ async function exportGcodeInput(scriptPath, explicitOutputPath, paramOverrides, jointOverrides) {
21009
21675
  const code = (await import("fs")).readFileSync(resolve20(scriptPath), "utf-8");
21010
21676
  const { allFiles, fileName, readBinaryFile } = collectProjectFiles(scriptPath);
21011
21677
  await initKernel();
21678
+ setParamOverrides(paramOverrides);
21012
21679
  const result = applyCliJointOverrides(
21013
21680
  runScript(code, fileName, allFiles, { readBinaryFile, allowEmptyResult: true, assemblyState: jointOverrides }),
21014
21681
  jointOverrides
@@ -21156,7 +21823,7 @@ function parseImplicitExportArgs(argv) {
21156
21823
  }
21157
21824
  if (!scriptPath) {
21158
21825
  throw new Error(
21159
- "Usage: forgecad export implicit <model.forge.js|asset.step|asset.stp> [--output path] [--format json|webgpu-brick] [--quality live|default|high] [--backend manifold|occt|truck|sdf]"
21826
+ "Usage: forgecad export implicit <model.forge.js|asset.step|asset.stp> [--output path] [--format json|webgpu-brick] [--quality live|default|high] [--backend manifold|occt|truck|sdf] [--param Key=Value] [--joint JointName=Value]"
21160
21827
  );
21161
21828
  }
21162
21829
  return { scriptPath, outputPath, format, backend, quality, workgroupSize, paramOverrides, jointOverrides };
@@ -21248,7 +21915,7 @@ async function runImplicitExportCli(argv = process.argv.slice(2)) {
21248
21915
  const args = parseImplicitExportArgs(argv);
21249
21916
  const input = loadCliScriptInput(args.scriptPath);
21250
21917
  await initCliBackend(args.backend);
21251
- if (Object.keys(args.paramOverrides).length > 0) setParamOverrides(args.paramOverrides);
21918
+ setParamOverrides(args.paramOverrides);
21252
21919
  const runResult = runScript(input.code, input.fileName, input.allFiles, {
21253
21920
  ...args.quality ? { quality: args.quality } : {},
21254
21921
  readBinaryFile: input.readBinaryFile,
@@ -21483,11 +22150,12 @@ async function runLsInput(options, scriptPath, overrides, jointOverrides) {
21483
22150
  // cli/forge-mesh.ts
21484
22151
  import { writeFileSync as writeFileSync8 } from "fs";
21485
22152
  import { extname as extname6, resolve as resolve22 } from "path";
21486
- function parseArgs7(argv) {
22153
+ function parseArgs8(argv) {
21487
22154
  const inputPaths = [];
21488
22155
  let outputPath;
21489
22156
  let quality;
21490
22157
  let backend;
22158
+ const paramOverrides = {};
21491
22159
  const jointOverrides = {};
21492
22160
  for (let i = 0; i < argv.length; i += 1) {
21493
22161
  const arg = argv[i];
@@ -21522,6 +22190,13 @@ function parseArgs7(argv) {
21522
22190
  i += 1;
21523
22191
  continue;
21524
22192
  }
22193
+ if (arg === "--param" || arg === "-p") {
22194
+ const value = argv[i + 1];
22195
+ if (!value || value.startsWith("--")) throw new Error(`${arg} requires Key=Value`);
22196
+ addCliParamOverride(value, paramOverrides);
22197
+ i += 1;
22198
+ continue;
22199
+ }
21525
22200
  if (arg.startsWith("--")) {
21526
22201
  throw new Error(`Unknown flag: ${arg}`);
21527
22202
  }
@@ -21529,11 +22204,11 @@ function parseArgs7(argv) {
21529
22204
  }
21530
22205
  requireInputPaths(
21531
22206
  inputPaths,
21532
- "Usage: forgecad export <3mf|stl> <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [input ...] [--output path] [--quality default|live|high] [--backend manifold|occt|truck|sdf] [--joint JointName=Value]"
22207
+ "Usage: forgecad export <3mf|stl> <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [input ...] [--output path] [--quality default|live|high] [--backend manifold|occt|truck|sdf] [--param Key=Value] [--joint JointName=Value]"
21533
22208
  );
21534
22209
  requireRenderableInputPaths(inputPaths);
21535
22210
  requireSingleInputForOutputPath(inputPaths, outputPath);
21536
- return { inputPaths, outputPath, quality, backend, jointOverrides };
22211
+ return { inputPaths, outputPath, quality, backend, paramOverrides, jointOverrides };
21537
22212
  }
21538
22213
  function defaultOutputPath5(scriptPath, sourcePath, format) {
21539
22214
  const abs = resolve22(scriptPath);
@@ -21550,13 +22225,13 @@ function extractMeshObjects(result) {
21550
22225
  }));
21551
22226
  }
21552
22227
  async function runMeshExportCli(format, argv) {
21553
- const { inputPaths, outputPath, quality, backend, jointOverrides } = parseArgs7(argv);
22228
+ const { inputPaths, outputPath, quality, backend, paramOverrides, jointOverrides } = parseArgs8(argv);
21554
22229
  requireExistingInputPaths(inputPaths);
21555
22230
  let failures = 0;
21556
22231
  for (const [index, scriptPath] of inputPaths.entries()) {
21557
22232
  printBatchHeader(scriptPath, index, inputPaths.length);
21558
22233
  try {
21559
- await exportMeshInput(format, scriptPath, outputPath, quality, backend, jointOverrides);
22234
+ await exportMeshInput(format, scriptPath, outputPath, quality, backend, paramOverrides, jointOverrides);
21560
22235
  } catch (error) {
21561
22236
  failures += 1;
21562
22237
  console.error(error instanceof Error ? error.message : String(error));
@@ -21566,10 +22241,11 @@ async function runMeshExportCli(format, argv) {
21566
22241
  process.exit(1);
21567
22242
  }
21568
22243
  }
21569
- async function exportMeshInput(format, scriptPath, outputPath, quality, backend, jointOverrides) {
22244
+ async function exportMeshInput(format, scriptPath, outputPath, quality, backend, paramOverrides, jointOverrides) {
21570
22245
  const input = loadCliScriptInput(scriptPath);
21571
22246
  await initCliBackend(resolveCliBackend(backend, input) ?? CLI_DEFAULT_BACKEND);
21572
22247
  const qualityPreset = quality && quality !== "default" ? quality : void 0;
22248
+ setParamOverrides(paramOverrides);
21573
22249
  const runResult = runScript(input.code, input.fileName, input.allFiles, {
21574
22250
  ...qualityPreset ? { quality: qualityPreset } : {},
21575
22251
  readBinaryFile: input.readBinaryFile,
@@ -23604,9 +24280,10 @@ function buildMjcfRobotPackage(spec) {
23604
24280
  }
23605
24281
 
23606
24282
  // cli/forge-mjcf.ts
23607
- function parseArgs8(argv) {
24283
+ function parseArgs9(argv) {
23608
24284
  const inputPaths = [];
23609
24285
  let outputPath;
24286
+ const paramOverrides = {};
23610
24287
  const jointOverrides = {};
23611
24288
  for (let i = 0; i < argv.length; i += 1) {
23612
24289
  const arg = argv[i];
@@ -23623,25 +24300,31 @@ function parseArgs8(argv) {
23623
24300
  i += 1;
23624
24301
  continue;
23625
24302
  }
24303
+ if (arg === "--param" || arg === "-p") {
24304
+ const value = argv[i + 1];
24305
+ if (!value || value.startsWith("--")) throw new Error(`${arg} requires Key=Value`);
24306
+ addCliParamOverride(value, paramOverrides);
24307
+ i += 1;
24308
+ continue;
24309
+ }
23626
24310
  if (arg.startsWith("--")) {
23627
24311
  throw new Error(`Unknown flag: ${arg}`);
23628
24312
  }
23629
24313
  inputPaths.push(arg);
23630
24314
  }
23631
- requireInputPaths(inputPaths, "Usage: forgecad export mjcf <script.forge.js> [input ...] [--output dir] [--joint JointName=Value]");
24315
+ requireInputPaths(
24316
+ inputPaths,
24317
+ "Usage: forgecad export mjcf <script.forge.js> [input ...] [--output dir] [--param Key=Value] [--joint JointName=Value]"
24318
+ );
23632
24319
  requireScriptInputPaths(inputPaths);
23633
24320
  requireSingleInputForOutputPath(inputPaths, outputPath, "--output", "dir");
23634
- return { inputPaths, outputPath, jointOverrides };
24321
+ return { inputPaths, outputPath, paramOverrides, jointOverrides };
23635
24322
  }
23636
24323
  function defaultOutputPath6(scriptPath) {
23637
24324
  const abs = resolve23(scriptPath);
23638
24325
  return abs.slice(0, abs.length - extname7(abs).length) + ".mjcfpkg";
23639
24326
  }
23640
24327
  function resolveSimulationModel2(result, jointOverrides) {
23641
- const legacy = getCollectedRobotExport();
23642
- if (legacy) {
23643
- return { ...legacy, state: { ...legacy.state, ...jointOverrides } };
23644
- }
23645
24328
  const assembly2 = getRunResultAssemblySource(result);
23646
24329
  if (!assembly2) return null;
23647
24330
  const def = assembly2.describe();
@@ -23649,13 +24332,13 @@ function resolveSimulationModel2(result, jointOverrides) {
23649
24332
  return collectSimulationModel(def, { state: jointOverrides });
23650
24333
  }
23651
24334
  async function runMjcfCli(argv = process.argv.slice(2)) {
23652
- const { inputPaths, outputPath, jointOverrides } = parseArgs8(argv);
24335
+ const { inputPaths, outputPath, paramOverrides, jointOverrides } = parseArgs9(argv);
23653
24336
  requireExistingInputPaths(inputPaths);
23654
24337
  let failures = 0;
23655
24338
  for (const [index, scriptPath] of inputPaths.entries()) {
23656
24339
  printBatchHeader(scriptPath, index, inputPaths.length);
23657
24340
  try {
23658
- await exportMjcfInput(scriptPath, outputPath, jointOverrides);
24341
+ await exportMjcfInput(scriptPath, outputPath, paramOverrides, jointOverrides);
23659
24342
  } catch (error) {
23660
24343
  failures += 1;
23661
24344
  console.error(error instanceof Error ? error.message : String(error));
@@ -23665,17 +24348,18 @@ async function runMjcfCli(argv = process.argv.slice(2)) {
23665
24348
  process.exit(1);
23666
24349
  }
23667
24350
  }
23668
- async function exportMjcfInput(scriptPath, explicitOutputPath, jointOverrides) {
24351
+ async function exportMjcfInput(scriptPath, explicitOutputPath, paramOverrides, jointOverrides) {
23669
24352
  const code = readFileSync15(resolve23(scriptPath), "utf-8");
23670
24353
  const { allFiles, fileName } = collectProjectFiles(scriptPath);
23671
24354
  await init();
24355
+ setParamOverrides(paramOverrides);
23672
24356
  const result = applyCliJointOverrides(runScript(code, fileName, allFiles, { assemblyState: jointOverrides }), jointOverrides);
23673
24357
  if (result.error) {
23674
24358
  throw new Error(`ERROR: ${result.error}`);
23675
24359
  }
23676
24360
  const robot = resolveSimulationModel2(result, jointOverrides);
23677
24361
  if (!robot) {
23678
- throw new Error("MJCF export requires the script to return assembly(...).withSimulation(...) or call legacy robotExport({...}).");
24362
+ throw new Error("MJCF export requires the script to return assembly(...).withSimulation(...).");
23679
24363
  }
23680
24364
  const packageOut = buildMjcfRobotPackage(robot);
23681
24365
  const targetDir = resolve23(batchOutputPath(scriptPath, explicitOutputPath, defaultOutputPath6));
@@ -24072,7 +24756,7 @@ function readArgValue(argv, idx, flag) {
24072
24756
  }
24073
24757
  return next;
24074
24758
  }
24075
- function parseArgs9(argv) {
24759
+ function parseArgs10(argv) {
24076
24760
  const inputPaths = [];
24077
24761
  let outputPath;
24078
24762
  let width = 1920;
@@ -24346,7 +25030,7 @@ function viewportCameraToBlenderConfig(camera) {
24346
25030
  };
24347
25031
  }
24348
25032
  async function runRenderHqCli(argv) {
24349
- const args = parseArgs9(argv);
25033
+ const args = parseArgs10(argv);
24350
25034
  requireExistingInputPaths(args.inputPaths);
24351
25035
  let failures = 0;
24352
25036
  for (const [index, scriptPath] of args.inputPaths.entries()) {
@@ -24492,7 +25176,9 @@ function defaultPngOutput(scriptPath) {
24492
25176
  return scriptPath.replace(/\.(forge|sketch)\.js$/, ".png").replace(/\.js$/, ".png");
24493
25177
  }
24494
25178
  function usage14() {
24495
- console.error("Usage: forgecad render sketch <script.forge.js> [input ...] [--output path] [--size <px>] [--chrome-path <path>]");
25179
+ console.error(
25180
+ "Usage: forgecad render sketch <script.forge.js> [input ...] [--output path] [--size <px>] [--chrome-path <path>] [--param Key=Value]"
25181
+ );
24496
25182
  process.exit(1);
24497
25183
  }
24498
25184
  function parseCli2(argv) {
@@ -24502,6 +25188,7 @@ function parseCli2(argv) {
24502
25188
  let size = 1024;
24503
25189
  let chromePath = process.env.CHROME_PATH;
24504
25190
  let background = "#2a2a2a";
25191
+ const paramOverrides = {};
24505
25192
  for (let i = 0; i < argv.length; i++) {
24506
25193
  const arg = argv[i];
24507
25194
  if (arg === "--size") {
@@ -24516,6 +25203,13 @@ function parseCli2(argv) {
24516
25203
  background = argv[++i];
24517
25204
  continue;
24518
25205
  }
25206
+ if (arg === "--param" || arg === "-p") {
25207
+ const value = argv[i + 1];
25208
+ if (!value || value.startsWith("--")) throw new Error(`${arg} requires Key=Value.`);
25209
+ addCliParamOverride(value, paramOverrides);
25210
+ i += 1;
25211
+ continue;
25212
+ }
24519
25213
  if (arg === "--output" || arg === "-o") {
24520
25214
  outputPath = argv[++i];
24521
25215
  if (!outputPath || outputPath.startsWith("--")) throw new Error(`${arg} requires a file path.`);
@@ -24533,7 +25227,8 @@ function parseCli2(argv) {
24533
25227
  outputPath,
24534
25228
  size,
24535
25229
  chromePath,
24536
- background
25230
+ background,
25231
+ paramOverrides
24537
25232
  };
24538
25233
  }
24539
25234
  async function runRender2dCli(argv = process.argv.slice(2)) {
@@ -24570,6 +25265,7 @@ async function renderSketchInput(options, scriptPath, chromePath) {
24570
25265
  const code = await readFile(resolve26(scriptPath), "utf-8");
24571
25266
  const { allFiles, fileName } = collectProjectFiles(scriptPath);
24572
25267
  await init();
25268
+ setParamOverrides(options.paramOverrides);
24573
25269
  const result = runScript(code, fileName, allFiles);
24574
25270
  if (result.error) {
24575
25271
  throw new Error(`Script error: ${result.error}`);
@@ -24677,14 +25373,22 @@ function formatSheetCutList(input) {
24677
25373
 
24678
25374
  // cli/forge-cut-list.ts
24679
25375
  function usage15() {
24680
- console.error("Usage: forgecad cut-list <script.forge.js> [input ...] [--joint JointName=Value]");
25376
+ console.error("Usage: forgecad cut-list <script.forge.js> [input ...] [--param Key=Value] [--joint JointName=Value]");
24681
25377
  process.exit(1);
24682
25378
  }
24683
- function parseArgs10(argv) {
25379
+ function parseArgs11(argv) {
24684
25380
  const positionals = [];
25381
+ const paramOverrides = {};
24685
25382
  const jointOverrides = {};
24686
25383
  for (let index = 0; index < argv.length; index += 1) {
24687
25384
  const arg = argv[index];
25385
+ if (arg === "--param" || arg === "-p") {
25386
+ const value = argv[index + 1];
25387
+ if (!value || value.startsWith("--")) throw new Error(`${arg} requires Key=Value`);
25388
+ addCliParamOverride(value, paramOverrides);
25389
+ index += 1;
25390
+ continue;
25391
+ }
24688
25392
  if (arg === "--joint") {
24689
25393
  const value = argv[index + 1];
24690
25394
  if (!value || value.startsWith("--")) throw new Error("--joint requires JointName=Value");
@@ -24698,16 +25402,16 @@ function parseArgs10(argv) {
24698
25402
  if (positionals.length === 0) usage15();
24699
25403
  const inputPaths = requireInputPaths(positionals, "Missing input path.");
24700
25404
  requireScriptInputPaths(inputPaths);
24701
- return { inputPaths, jointOverrides };
25405
+ return { inputPaths, paramOverrides, jointOverrides };
24702
25406
  }
24703
25407
  async function runCutListCli(argv = process.argv.slice(2)) {
24704
- const { inputPaths, jointOverrides } = parseArgs10(argv);
25408
+ const { inputPaths, paramOverrides, jointOverrides } = parseArgs11(argv);
24705
25409
  requireExistingInputPaths(inputPaths);
24706
25410
  let failures = 0;
24707
25411
  for (const [index, scriptPath] of inputPaths.entries()) {
24708
25412
  printBatchHeader(scriptPath, index, inputPaths.length);
24709
25413
  try {
24710
- await printCutListInput(scriptPath, jointOverrides);
25414
+ await printCutListInput(scriptPath, paramOverrides, jointOverrides);
24711
25415
  } catch (error) {
24712
25416
  failures += 1;
24713
25417
  console.error(error instanceof Error ? error.message : String(error));
@@ -24717,10 +25421,11 @@ async function runCutListCli(argv = process.argv.slice(2)) {
24717
25421
  process.exit(1);
24718
25422
  }
24719
25423
  }
24720
- async function printCutListInput(scriptPath, jointOverrides) {
25424
+ async function printCutListInput(scriptPath, paramOverrides, jointOverrides) {
24721
25425
  const source = await readFile2(resolve27(scriptPath), "utf-8");
24722
25426
  const { allFiles, fileName } = collectProjectFiles(scriptPath);
24723
25427
  await init();
25428
+ setParamOverrides(paramOverrides);
24724
25429
  const result = applyCliJointOverrides(runScript(source, fileName, allFiles, { assemblyState: jointOverrides }), jointOverrides);
24725
25430
  if (result.error) {
24726
25431
  throw new Error(`Script error: ${result.error}`);
@@ -24735,7 +25440,7 @@ async function printCutListInput(scriptPath, jointOverrides) {
24735
25440
  import { readFile as readFile3, writeFile as writeFile5 } from "fs/promises";
24736
25441
  import { basename as basename11, resolve as resolve28 } from "path";
24737
25442
  var FORMAT_VALUES = ["pdf", "dxf"];
24738
- var VALUE_FLAGS = /* @__PURE__ */ new Set(["--format", "--sheet-width", "--sheet-height", "--kerf", "--joint", "--output", "--out"]);
25443
+ var VALUE_FLAGS = /* @__PURE__ */ new Set(["--format", "--sheet-width", "--sheet-height", "--kerf", "--param", "-p", "--joint", "--output", "--out"]);
24739
25444
  function argValue(argv, name) {
24740
25445
  const prefix = `${name}=`;
24741
25446
  const equalsArg = argv.find((arg) => arg.startsWith(prefix));
@@ -24749,7 +25454,7 @@ function hasFlag(argv, name) {
24749
25454
  return argv.some((arg) => arg === name || arg.startsWith(prefix));
24750
25455
  }
24751
25456
  function usageText() {
24752
- return "Usage: forgecad export cutting-layout <script.forge.js> [input ...] [--output output.pdf|output.dxf]\n [--format pdf|dxf] [--sheet-width <mm>] [--sheet-height <mm>] [--kerf <mm>] [--joint JointName=Value]";
25457
+ return "Usage: forgecad export cutting-layout <script.forge.js> [input ...] [--output output.pdf|output.dxf]\n [--format pdf|dxf] [--sheet-width <mm>] [--sheet-height <mm>] [--kerf <mm>] [--param Key=Value] [--joint JointName=Value]";
24753
25458
  }
24754
25459
  function usage16() {
24755
25460
  console.error(usageText());
@@ -24779,6 +25484,18 @@ function parseJointOverrides(argv) {
24779
25484
  }
24780
25485
  return jointOverrides;
24781
25486
  }
25487
+ function parseParamOverrides(argv) {
25488
+ const paramOverrides = {};
25489
+ for (let i = 0; i < argv.length; i += 1) {
25490
+ const arg = argv[i];
25491
+ if (arg !== "--param" && arg !== "-p") continue;
25492
+ const value = argv[i + 1];
25493
+ if (!value || value.startsWith("--")) throw new Error(`${arg} requires Key=Value`);
25494
+ addCliParamOverride(value, paramOverrides);
25495
+ i += 1;
25496
+ }
25497
+ return paramOverrides;
25498
+ }
24782
25499
  function normalizeFormat(value) {
24783
25500
  if (value == null) return void 0;
24784
25501
  const normalized = value.toLowerCase();
@@ -24850,6 +25567,7 @@ async function runCuttingLayoutCli(argv = process.argv.slice(2)) {
24850
25567
  if (failures > 0) process.exit(1);
24851
25568
  }
24852
25569
  async function exportCuttingLayoutInput(argv, scriptPath, explicitOutputPath) {
25570
+ const paramOverrides = parseParamOverrides(argv);
24853
25571
  const jointOverrides = parseJointOverrides(argv);
24854
25572
  const formatArg = argValue(argv, "--format");
24855
25573
  if (hasFlag(argv, "--format") && formatArg == null) {
@@ -24870,6 +25588,7 @@ async function exportCuttingLayoutInput(argv, scriptPath, explicitOutputPath) {
24870
25588
  const source = await readFile3(resolve28(scriptPath), "utf-8");
24871
25589
  const { allFiles, fileName } = collectProjectFiles(scriptPath);
24872
25590
  await init();
25591
+ setParamOverrides(paramOverrides);
24873
25592
  const result = applyCliJointOverrides(runScript(source, fileName, allFiles, { assemblyState: jointOverrides }), jointOverrides);
24874
25593
  if (result.error) {
24875
25594
  throw new Error(`Script error: ${result.error}`);
@@ -24907,7 +25626,7 @@ import { writeFile as writeFile6 } from "fs/promises";
24907
25626
  import { basename as basename12, resolve as resolve29 } from "path";
24908
25627
  function usage17() {
24909
25628
  console.error(
24910
- "Usage: forgecad export report <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [input ...] [--output path] [--dim-angle-tol <deg>] [--joint JointName=Value]"
25629
+ "Usage: forgecad export report <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [input ...] [--output path] [--dim-angle-tol <deg>] [--param Key=Value] [--joint JointName=Value]"
24911
25630
  );
24912
25631
  process.exit(1);
24913
25632
  }
@@ -24916,8 +25635,9 @@ function readValue7(argv, index, flag) {
24916
25635
  if (!value || value.startsWith("--")) throw new Error(`${flag} requires a value.`);
24917
25636
  return value;
24918
25637
  }
24919
- function parseArgs11(argv) {
25638
+ function parseArgs12(argv) {
24920
25639
  const positionals = [];
25640
+ const paramOverrides = {};
24921
25641
  const jointOverrides = {};
24922
25642
  let dimAngleToleranceDeg;
24923
25643
  let outputPath;
@@ -24939,6 +25659,11 @@ function parseArgs11(argv) {
24939
25659
  index += 1;
24940
25660
  continue;
24941
25661
  }
25662
+ if (arg === "--param" || arg === "-p") {
25663
+ addCliParamOverride(readValue7(argv, index, arg), paramOverrides);
25664
+ index += 1;
25665
+ continue;
25666
+ }
24942
25667
  if (arg.startsWith("--")) throw new Error(`Unknown option: ${arg}`);
24943
25668
  positionals.push(arg);
24944
25669
  }
@@ -24946,16 +25671,16 @@ function parseArgs11(argv) {
24946
25671
  const inputPaths = requireInputPaths(positionals, "Missing input path.");
24947
25672
  requireRenderableInputPaths(inputPaths);
24948
25673
  requireSingleInputForOutputPath(inputPaths, outputPath);
24949
- return { inputPaths, outputPath, dimAngleToleranceDeg, jointOverrides };
25674
+ return { inputPaths, outputPath, dimAngleToleranceDeg, paramOverrides, jointOverrides };
24950
25675
  }
24951
25676
  async function runReportCli(argv = process.argv.slice(2)) {
24952
- const { inputPaths, outputPath: parsedOutputPath, jointOverrides, dimAngleToleranceDeg } = parseArgs11(argv);
25677
+ const { inputPaths, outputPath: parsedOutputPath, paramOverrides, jointOverrides, dimAngleToleranceDeg } = parseArgs12(argv);
24953
25678
  requireExistingInputPaths(inputPaths);
24954
25679
  let failures = 0;
24955
25680
  for (const [index, scriptPath] of inputPaths.entries()) {
24956
25681
  printBatchHeader(scriptPath, index, inputPaths.length);
24957
25682
  try {
24958
- await exportReportInput(scriptPath, parsedOutputPath, jointOverrides, dimAngleToleranceDeg);
25683
+ await exportReportInput(scriptPath, parsedOutputPath, paramOverrides, jointOverrides, dimAngleToleranceDeg);
24959
25684
  } catch (error) {
24960
25685
  failures += 1;
24961
25686
  console.error(error instanceof Error ? error.message : String(error));
@@ -24965,10 +25690,11 @@ async function runReportCli(argv = process.argv.slice(2)) {
24965
25690
  process.exit(1);
24966
25691
  }
24967
25692
  }
24968
- async function exportReportInput(scriptPath, explicitOutputPath, jointOverrides, dimAngleToleranceDeg) {
25693
+ async function exportReportInput(scriptPath, explicitOutputPath, paramOverrides, jointOverrides, dimAngleToleranceDeg) {
24969
25694
  const outputPath = batchOutputPath(scriptPath, explicitOutputPath, (inputPath) => replaceCliInputExtension(inputPath, ".report.pdf"));
24970
25695
  const input = loadCliScriptInput(scriptPath);
24971
25696
  await initCliBackend(resolveCliBackend(void 0, input) ?? CLI_DEFAULT_BACKEND);
25697
+ setParamOverrides(paramOverrides);
24972
25698
  const result = applyCliJointOverrides(
24973
25699
  runScript(input.code, input.fileName, input.allFiles, {
24974
25700
  readBinaryFile: input.readBinaryFile,
@@ -25828,9 +26554,10 @@ function buildSdfRobotPackage(spec) {
25828
26554
  }
25829
26555
 
25830
26556
  // cli/forge-sdf.ts
25831
- function parseArgs12(argv) {
26557
+ function parseArgs13(argv) {
25832
26558
  const inputPaths = [];
25833
26559
  let outputPath;
26560
+ const paramOverrides = {};
25834
26561
  const jointOverrides = {};
25835
26562
  for (let i = 0; i < argv.length; i += 1) {
25836
26563
  const arg = argv[i];
@@ -25847,25 +26574,31 @@ function parseArgs12(argv) {
25847
26574
  i += 1;
25848
26575
  continue;
25849
26576
  }
26577
+ if (arg === "--param" || arg === "-p") {
26578
+ const value = argv[i + 1];
26579
+ if (!value || value.startsWith("--")) throw new Error(`${arg} requires Key=Value`);
26580
+ addCliParamOverride(value, paramOverrides);
26581
+ i += 1;
26582
+ continue;
26583
+ }
25850
26584
  if (arg.startsWith("--")) {
25851
26585
  throw new Error(`Unknown flag: ${arg}`);
25852
26586
  }
25853
26587
  inputPaths.push(arg);
25854
26588
  }
25855
- requireInputPaths(inputPaths, "Usage: forgecad export sdf <script.forge.js> [input ...] [--output dir] [--joint JointName=Value]");
26589
+ requireInputPaths(
26590
+ inputPaths,
26591
+ "Usage: forgecad export sdf <script.forge.js> [input ...] [--output dir] [--param Key=Value] [--joint JointName=Value]"
26592
+ );
25856
26593
  requireScriptInputPaths(inputPaths);
25857
26594
  requireSingleInputForOutputPath(inputPaths, outputPath, "--output", "dir");
25858
- return { inputPaths, outputPath, jointOverrides };
26595
+ return { inputPaths, outputPath, paramOverrides, jointOverrides };
25859
26596
  }
25860
26597
  function defaultOutputPath9(scriptPath) {
25861
26598
  const abs = resolve30(scriptPath);
25862
26599
  return abs.slice(0, abs.length - extname9(abs).length) + ".sdfpkg";
25863
26600
  }
25864
26601
  function resolveSimulationModel3(result, jointOverrides) {
25865
- const legacy = getCollectedRobotExport();
25866
- if (legacy) {
25867
- return { ...legacy, state: { ...legacy.state, ...jointOverrides } };
25868
- }
25869
26602
  const assembly2 = getRunResultAssemblySource(result);
25870
26603
  if (!assembly2) return null;
25871
26604
  const def = assembly2.describe();
@@ -25873,13 +26606,13 @@ function resolveSimulationModel3(result, jointOverrides) {
25873
26606
  return collectSimulationModel(def, { state: jointOverrides });
25874
26607
  }
25875
26608
  async function runSdfCli(argv = process.argv.slice(2)) {
25876
- const { inputPaths, outputPath, jointOverrides } = parseArgs12(argv);
26609
+ const { inputPaths, outputPath, paramOverrides, jointOverrides } = parseArgs13(argv);
25877
26610
  requireExistingInputPaths(inputPaths);
25878
26611
  let failures = 0;
25879
26612
  for (const [index, scriptPath] of inputPaths.entries()) {
25880
26613
  printBatchHeader(scriptPath, index, inputPaths.length);
25881
26614
  try {
25882
- await exportSdfInput(scriptPath, outputPath, jointOverrides);
26615
+ await exportSdfInput(scriptPath, outputPath, paramOverrides, jointOverrides);
25883
26616
  } catch (error) {
25884
26617
  failures += 1;
25885
26618
  console.error(error instanceof Error ? error.message : String(error));
@@ -25889,17 +26622,18 @@ async function runSdfCli(argv = process.argv.slice(2)) {
25889
26622
  process.exit(1);
25890
26623
  }
25891
26624
  }
25892
- async function exportSdfInput(scriptPath, explicitOutputPath, jointOverrides) {
26625
+ async function exportSdfInput(scriptPath, explicitOutputPath, paramOverrides, jointOverrides) {
25893
26626
  const code = readFileSync16(resolve30(scriptPath), "utf-8");
25894
26627
  const { allFiles, fileName } = collectProjectFiles(scriptPath);
25895
26628
  await init();
26629
+ setParamOverrides(paramOverrides);
25896
26630
  const result = applyCliJointOverrides(runScript(code, fileName, allFiles, { assemblyState: jointOverrides }), jointOverrides);
25897
26631
  if (result.error) {
25898
26632
  throw new Error(`ERROR: ${result.error}`);
25899
26633
  }
25900
26634
  const robot = resolveSimulationModel3(result, jointOverrides);
25901
26635
  if (!robot) {
25902
- throw new Error("SDF export requires the script to return assembly(...).withSimulation(...) or call legacy robotExport({...}).");
26636
+ throw new Error("SDF export requires the script to return assembly(...).withSimulation(...).");
25903
26637
  }
25904
26638
  const packageOut = buildSdfRobotPackage(robot);
25905
26639
  const targetDir = resolve30(batchOutputPath(scriptPath, explicitOutputPath, defaultOutputPath9));
@@ -26625,15 +27359,16 @@ function generateSketchPdf(meta, options) {
26625
27359
 
26626
27360
  // cli/forge-sketch-pdf.ts
26627
27361
  function usage19() {
26628
- console.error("Usage: forgecad export sketch-pdf <script.forge.js> [input ...] [--output path]");
27362
+ console.error("Usage: forgecad export sketch-pdf <script.forge.js> [input ...] [--output path] [--param Key=Value]");
26629
27363
  process.exit(1);
26630
27364
  }
26631
27365
  function defaultPdfOutput(scriptPath) {
26632
27366
  return scriptPath.replace(/\.(forge|sketch)\.js$/, ".sketch.pdf").replace(/\.js$/, ".sketch.pdf");
26633
27367
  }
26634
- function parseArgs13(argv) {
27368
+ function parseArgs14(argv) {
26635
27369
  const inputPaths = [];
26636
27370
  let outputPath;
27371
+ const paramOverrides = {};
26637
27372
  for (let index = 0; index < argv.length; index += 1) {
26638
27373
  const arg = argv[index];
26639
27374
  if (arg === "--output" || arg === "-o") {
@@ -26642,18 +27377,25 @@ function parseArgs13(argv) {
26642
27377
  index += 1;
26643
27378
  continue;
26644
27379
  }
27380
+ if (arg === "--param" || arg === "-p") {
27381
+ const value = argv[index + 1];
27382
+ if (!value || value.startsWith("--")) throw new Error(`${arg} requires Key=Value.`);
27383
+ addCliParamOverride(value, paramOverrides);
27384
+ index += 1;
27385
+ continue;
27386
+ }
26645
27387
  if (arg.startsWith("--")) throw new Error(`Unknown option: ${arg}`);
26646
27388
  inputPaths.push(arg);
26647
27389
  }
26648
27390
  requireInputPaths(inputPaths, "Missing input path.");
26649
27391
  requireScriptInputPaths(inputPaths);
26650
27392
  requireSingleInputForOutputPath(inputPaths, outputPath);
26651
- return { inputPaths, outputPath };
27393
+ return { inputPaths, outputPath, paramOverrides };
26652
27394
  }
26653
27395
  async function runSketchPdfCli(argv = process.argv.slice(2)) {
26654
27396
  let parsed;
26655
27397
  try {
26656
- parsed = parseArgs13(argv);
27398
+ parsed = parseArgs14(argv);
26657
27399
  requireExistingInputPaths(parsed.inputPaths);
26658
27400
  } catch (error) {
26659
27401
  console.error(error instanceof Error ? error.message : String(error));
@@ -26663,7 +27405,7 @@ async function runSketchPdfCli(argv = process.argv.slice(2)) {
26663
27405
  for (const [index, scriptPath] of parsed.inputPaths.entries()) {
26664
27406
  printBatchHeader(scriptPath, index, parsed.inputPaths.length);
26665
27407
  try {
26666
- await exportSketchPdfInput(scriptPath, parsed.outputPath);
27408
+ await exportSketchPdfInput(scriptPath, parsed.outputPath, parsed.paramOverrides);
26667
27409
  } catch (error) {
26668
27410
  failures += 1;
26669
27411
  console.error(error instanceof Error ? error.message : String(error));
@@ -26673,7 +27415,7 @@ async function runSketchPdfCli(argv = process.argv.slice(2)) {
26673
27415
  process.exit(1);
26674
27416
  }
26675
27417
  }
26676
- async function exportSketchPdfInput(scriptPath, explicitOutputPath) {
27418
+ async function exportSketchPdfInput(scriptPath, explicitOutputPath, paramOverrides) {
26677
27419
  const outputPath = batchOutputPath(scriptPath, explicitOutputPath, defaultPdfOutput);
26678
27420
  if (resolve31(outputPath) === resolve31(scriptPath)) {
26679
27421
  throw new Error(`ERROR: output path would overwrite the input script. Specify an explicit output path.`);
@@ -26681,6 +27423,7 @@ async function exportSketchPdfInput(scriptPath, explicitOutputPath) {
26681
27423
  const code = await readFile4(resolve31(scriptPath), "utf-8");
26682
27424
  const { allFiles, fileName } = collectProjectFiles(scriptPath);
26683
27425
  await init();
27426
+ setParamOverrides(paramOverrides);
26684
27427
  const result = runScript(code, fileName, allFiles);
26685
27428
  if (result.error) {
26686
27429
  throw new Error(`Script error: ${result.error}`);
@@ -27221,15 +27964,15 @@ function getEnvToken() {
27221
27964
  }
27222
27965
  function prompt(question) {
27223
27966
  const rl = createInterface({ input: process.stdin, output: process.stderr });
27224
- return new Promise((resolve44) => {
27967
+ return new Promise((resolve46) => {
27225
27968
  rl.question(question, (answer) => {
27226
27969
  rl.close();
27227
- resolve44(answer.trim());
27970
+ resolve46(answer.trim());
27228
27971
  });
27229
27972
  });
27230
27973
  }
27231
27974
  function promptPassword(question) {
27232
- return new Promise((resolve44) => {
27975
+ return new Promise((resolve46) => {
27233
27976
  process.stderr.write(question);
27234
27977
  const stdin = process.stdin;
27235
27978
  const wasRaw = stdin.isRaw;
@@ -27244,7 +27987,7 @@ function promptPassword(question) {
27244
27987
  stdin.removeListener("end", finish);
27245
27988
  stdin.pause();
27246
27989
  process.stderr.write("\n");
27247
- resolve44(password);
27990
+ resolve46(password);
27248
27991
  };
27249
27992
  const onData = (ch) => {
27250
27993
  for (const c of ch.toString("utf-8")) {
@@ -27801,7 +28544,7 @@ import { existsSync as existsSync14, readdirSync as readdirSync8, statSync as st
27801
28544
  import { createServer as createServer2 } from "net";
27802
28545
  import { join as join12 } from "path";
27803
28546
  var startedComputeServer = null;
27804
- var REQUIRED_ENGINE_VERSION = "native-occt-node-api-0.3.0";
28547
+ var REQUIRED_ENGINE_VERSION = "native-occt-node-api-0.3.1";
27805
28548
  var REQUIRED_PLAN_KINDS = [
27806
28549
  "box",
27807
28550
  "cylinder",
@@ -27809,6 +28552,7 @@ var REQUIRED_PLAN_KINDS = [
27809
28552
  "torus",
27810
28553
  "extrude",
27811
28554
  "boolean",
28555
+ "trimByPlane",
27812
28556
  "transform",
27813
28557
  "queryOwner",
27814
28558
  "revolve",
@@ -27847,17 +28591,17 @@ async function waitForHealthy(computeUrl, child) {
27847
28591
  for (let attempt = 0; attempt < 120; attempt += 1) {
27848
28592
  if (await isHealthy(computeUrl)) return;
27849
28593
  if (child.exitCode != null) break;
27850
- await new Promise((resolve44) => setTimeout(resolve44, 50));
28594
+ await new Promise((resolve46) => setTimeout(resolve46, 50));
27851
28595
  }
27852
28596
  throw new Error(`Native OCCT compute backend did not become healthy at ${computeUrl}.`);
27853
28597
  }
27854
28598
  function closeChild(child) {
27855
- return new Promise((resolve44) => {
28599
+ return new Promise((resolve46) => {
27856
28600
  if (child.exitCode != null || child.killed) {
27857
- resolve44();
28601
+ resolve46();
27858
28602
  return;
27859
28603
  }
27860
- child.once("exit", () => resolve44());
28604
+ child.once("exit", () => resolve46());
27861
28605
  child.kill();
27862
28606
  });
27863
28607
  }
@@ -27896,12 +28640,12 @@ async function pickComputeUrl(preferred) {
27896
28640
  const candidate = computeUrlWithPort(preferred, port);
27897
28641
  if (await isHealthy(candidate)) return candidate;
27898
28642
  try {
27899
- const server = await new Promise((resolve44, reject) => {
28643
+ const server = await new Promise((resolve46, reject) => {
27900
28644
  const probe = createServer2();
27901
28645
  probe.once("error", reject);
27902
- probe.listen(port, listenHost, () => resolve44(probe));
28646
+ probe.listen(port, listenHost, () => resolve46(probe));
27903
28647
  });
27904
- await new Promise((resolve44) => server.close(() => resolve44()));
28648
+ await new Promise((resolve46) => server.close(() => resolve46()));
27905
28649
  return candidate;
27906
28650
  } catch {
27907
28651
  }
@@ -28328,14 +29072,14 @@ function sendJson(res, status, payload) {
28328
29072
  res.end(JSON.stringify(payload));
28329
29073
  }
28330
29074
  function readJsonBody(req) {
28331
- return new Promise((resolve44, reject) => {
29075
+ return new Promise((resolve46, reject) => {
28332
29076
  let body = "";
28333
29077
  req.on("data", (chunk) => {
28334
29078
  body += chunk;
28335
29079
  });
28336
29080
  req.on("end", () => {
28337
29081
  try {
28338
- resolve44(body ? JSON.parse(body) : {});
29082
+ resolve46(body ? JSON.parse(body) : {});
28339
29083
  } catch (e) {
28340
29084
  reject(e);
28341
29085
  }
@@ -28344,12 +29088,12 @@ function readJsonBody(req) {
28344
29088
  });
28345
29089
  }
28346
29090
  function readBinaryBody(req) {
28347
- return new Promise((resolve44, reject) => {
29091
+ return new Promise((resolve46, reject) => {
28348
29092
  const chunks = [];
28349
29093
  req.on("data", (chunk) => {
28350
29094
  chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
28351
29095
  });
28352
- req.on("end", () => resolve44(Buffer.concat(chunks)));
29096
+ req.on("end", () => resolve46(Buffer.concat(chunks)));
28353
29097
  req.on("error", reject);
28354
29098
  });
28355
29099
  }
@@ -28449,10 +29193,10 @@ function serveStatic(distDir, req, res) {
28449
29193
  return true;
28450
29194
  }
28451
29195
  function isPortAvailable(port, host) {
28452
- return new Promise((resolve44) => {
29196
+ return new Promise((resolve46) => {
28453
29197
  const s = http.createServer();
28454
- s.listen(port, host, () => s.close(() => resolve44(true)));
28455
- s.on("error", () => resolve44(false));
29198
+ s.listen(port, host, () => s.close(() => resolve46(true)));
29199
+ s.on("error", () => resolve46(false));
28456
29200
  });
28457
29201
  }
28458
29202
  async function pickPort(preferred, host, strict) {
@@ -28545,10 +29289,10 @@ data: ${JSON.stringify(data)}
28545
29289
  );
28546
29290
  }
28547
29291
  function waitForWatcherReady(watcher) {
28548
- return new Promise((resolve44, reject) => {
29292
+ return new Promise((resolve46, reject) => {
28549
29293
  const handleReady = () => {
28550
29294
  watcher.off("error", handleError);
28551
- resolve44();
29295
+ resolve46();
28552
29296
  };
28553
29297
  const handleError = (error) => {
28554
29298
  watcher.off("ready", handleReady);
@@ -29163,12 +29907,12 @@ data: ${JSON.stringify(data)}
29163
29907
  res.end("Not found");
29164
29908
  }
29165
29909
  });
29166
- await new Promise((resolve44) => server.listen(port, host, resolve44));
29910
+ await new Promise((resolve46) => server.listen(port, host, resolve46));
29167
29911
  const displayHost = host === "0.0.0.0" ? "localhost" : host;
29168
29912
  const url = `http://${displayHost}:${port}`;
29169
29913
  if (open) openBrowser(url);
29170
29914
  const close4 = async () => {
29171
- await new Promise((resolve44) => {
29915
+ await new Promise((resolve46) => {
29172
29916
  for (const timer of snapshotTimers.values()) clearTimeout(timer);
29173
29917
  snapshotTimers.clear();
29174
29918
  for (const w of watchers) w.close();
@@ -29181,7 +29925,7 @@ data: ${JSON.stringify(data)}
29181
29925
  }
29182
29926
  }
29183
29927
  sseClients.clear();
29184
- server.close(() => resolve44());
29928
+ server.close(() => resolve46());
29185
29929
  });
29186
29930
  await computeServer?.close();
29187
29931
  };
@@ -29299,7 +30043,7 @@ function usage21(command) {
29299
30043
  if (command === "render") return "Usage: forgecad context render <model.forge.js> [--out <dir>] [--force] [--size <px>]";
29300
30044
  return `Usage: forgecad context ${command} <model.forge.js> [--format text|json]`;
29301
30045
  }
29302
- function parseArgs14(command, args) {
30046
+ function parseArgs15(command, args) {
29303
30047
  let filePath;
29304
30048
  let format = "text";
29305
30049
  for (let i = 0; i < args.length; i++) {
@@ -29584,7 +30328,7 @@ async function writeContextBundle(projectRoot, bundleDir, context, views) {
29584
30328
  }
29585
30329
  async function runContextGetCli(args = process.argv.slice(2)) {
29586
30330
  try {
29587
- const parsed = parseArgs14("get", args);
30331
+ const parsed = parseArgs15("get", args);
29588
30332
  const loaded = await loadAgentContext(parsed.filePath);
29589
30333
  if (!loaded.context) {
29590
30334
  console.error(`No submitted agent context for ${loaded.filePath}.`);
@@ -29603,7 +30347,7 @@ async function runContextGetCli(args = process.argv.slice(2)) {
29603
30347
  }
29604
30348
  async function runContextStatusCli(args = process.argv.slice(2)) {
29605
30349
  try {
29606
- const parsed = parseArgs14("status", args);
30350
+ const parsed = parseArgs15("status", args);
29607
30351
  const loaded = await loadAgentContext(parsed.filePath);
29608
30352
  const status = statusForLoaded(loaded);
29609
30353
  if (parsed.format === "json") {
@@ -29663,15 +30407,16 @@ async function runContextRenderCli(args = process.argv.slice(2)) {
29663
30407
  import { readFile as readFile5, writeFile as writeFile9 } from "fs/promises";
29664
30408
  import { basename as basename14, resolve as resolve34 } from "path";
29665
30409
  function usage22() {
29666
- console.error("Usage: forgecad export svg <script.forge.js> [input ...] [--output path]");
30410
+ console.error("Usage: forgecad export svg <script.forge.js> [input ...] [--output path] [--param Key=Value]");
29667
30411
  process.exit(1);
29668
30412
  }
29669
30413
  function defaultSvgOutput(scriptPath) {
29670
30414
  return scriptPath.replace(/\.(forge|sketch)\.js$/, ".svg").replace(/\.js$/, ".svg");
29671
30415
  }
29672
- function parseArgs15(argv) {
30416
+ function parseArgs16(argv) {
29673
30417
  const inputPaths = [];
29674
30418
  let outputPath;
30419
+ const paramOverrides = {};
29675
30420
  for (let index = 0; index < argv.length; index += 1) {
29676
30421
  const arg = argv[index];
29677
30422
  if (arg === "--output" || arg === "-o") {
@@ -29680,18 +30425,25 @@ function parseArgs15(argv) {
29680
30425
  index += 1;
29681
30426
  continue;
29682
30427
  }
30428
+ if (arg === "--param" || arg === "-p") {
30429
+ const value = argv[index + 1];
30430
+ if (!value || value.startsWith("--")) throw new Error(`${arg} requires Key=Value.`);
30431
+ addCliParamOverride(value, paramOverrides);
30432
+ index += 1;
30433
+ continue;
30434
+ }
29683
30435
  if (arg.startsWith("--")) throw new Error(`Unknown option: ${arg}`);
29684
30436
  inputPaths.push(arg);
29685
30437
  }
29686
30438
  requireInputPaths(inputPaths, "Missing input path.");
29687
30439
  requireScriptInputPaths(inputPaths);
29688
30440
  requireSingleInputForOutputPath(inputPaths, outputPath);
29689
- return { inputPaths, outputPath };
30441
+ return { inputPaths, outputPath, paramOverrides };
29690
30442
  }
29691
30443
  async function runSvgCli(argv = process.argv.slice(2)) {
29692
30444
  let parsed;
29693
30445
  try {
29694
- parsed = parseArgs15(argv);
30446
+ parsed = parseArgs16(argv);
29695
30447
  requireExistingInputPaths(parsed.inputPaths);
29696
30448
  } catch (error) {
29697
30449
  console.error(error instanceof Error ? error.message : String(error));
@@ -29701,7 +30453,7 @@ async function runSvgCli(argv = process.argv.slice(2)) {
29701
30453
  for (const [index, scriptPath] of parsed.inputPaths.entries()) {
29702
30454
  printBatchHeader(scriptPath, index, parsed.inputPaths.length);
29703
30455
  try {
29704
- await exportSvgInput(scriptPath, parsed.outputPath);
30456
+ await exportSvgInput(scriptPath, parsed.outputPath, parsed.paramOverrides);
29705
30457
  } catch (error) {
29706
30458
  failures += 1;
29707
30459
  console.error(error instanceof Error ? error.message : String(error));
@@ -29711,7 +30463,7 @@ async function runSvgCli(argv = process.argv.slice(2)) {
29711
30463
  process.exit(1);
29712
30464
  }
29713
30465
  }
29714
- async function exportSvgInput(scriptPath, explicitOutputPath) {
30466
+ async function exportSvgInput(scriptPath, explicitOutputPath, paramOverrides) {
29715
30467
  const outputPath = batchOutputPath(scriptPath, explicitOutputPath, defaultSvgOutput);
29716
30468
  if (resolve34(outputPath) === resolve34(scriptPath)) {
29717
30469
  throw new Error(`ERROR: output path would overwrite the input script. Specify an explicit output path.`);
@@ -29719,6 +30471,7 @@ async function exportSvgInput(scriptPath, explicitOutputPath) {
29719
30471
  const code = await readFile5(resolve34(scriptPath), "utf-8");
29720
30472
  const { allFiles, fileName } = collectProjectFiles(scriptPath);
29721
30473
  await init();
30474
+ setParamOverrides(paramOverrides);
29722
30475
  const result = runScript(code, fileName, allFiles);
29723
30476
  if (result.error) {
29724
30477
  throw new Error(`Script error: ${result.error}`);
@@ -30145,9 +30898,10 @@ function buildUrdfRobotPackage(spec) {
30145
30898
  }
30146
30899
 
30147
30900
  // cli/forge-urdf.ts
30148
- function parseArgs16(argv) {
30901
+ function parseArgs17(argv) {
30149
30902
  const inputPaths = [];
30150
30903
  let outputPath;
30904
+ const paramOverrides = {};
30151
30905
  const jointOverrides = {};
30152
30906
  for (let i = 0; i < argv.length; i += 1) {
30153
30907
  const arg = argv[i];
@@ -30164,25 +30918,31 @@ function parseArgs16(argv) {
30164
30918
  i += 1;
30165
30919
  continue;
30166
30920
  }
30921
+ if (arg === "--param" || arg === "-p") {
30922
+ const value = argv[i + 1];
30923
+ if (!value || value.startsWith("--")) throw new Error(`${arg} requires Key=Value`);
30924
+ addCliParamOverride(value, paramOverrides);
30925
+ i += 1;
30926
+ continue;
30927
+ }
30167
30928
  if (arg.startsWith("--")) {
30168
30929
  throw new Error(`Unknown flag: ${arg}`);
30169
30930
  }
30170
30931
  inputPaths.push(arg);
30171
30932
  }
30172
- requireInputPaths(inputPaths, "Usage: forgecad export urdf <script.forge.js> [input ...] [--output dir] [--joint JointName=Value]");
30933
+ requireInputPaths(
30934
+ inputPaths,
30935
+ "Usage: forgecad export urdf <script.forge.js> [input ...] [--output dir] [--param Key=Value] [--joint JointName=Value]"
30936
+ );
30173
30937
  requireScriptInputPaths(inputPaths);
30174
30938
  requireSingleInputForOutputPath(inputPaths, outputPath, "--output", "dir");
30175
- return { inputPaths, outputPath, jointOverrides };
30939
+ return { inputPaths, outputPath, paramOverrides, jointOverrides };
30176
30940
  }
30177
30941
  function defaultOutputPath10(scriptPath) {
30178
30942
  const abs = resolve35(scriptPath);
30179
30943
  return abs.slice(0, abs.length - extname11(abs).length) + ".urdfpkg";
30180
30944
  }
30181
30945
  function resolveSimulationModel4(result, jointOverrides) {
30182
- const legacy = getCollectedRobotExport();
30183
- if (legacy) {
30184
- return { ...legacy, state: { ...legacy.state, ...jointOverrides } };
30185
- }
30186
30946
  const assembly2 = getRunResultAssemblySource(result);
30187
30947
  if (!assembly2) return null;
30188
30948
  const def = assembly2.describe();
@@ -30190,13 +30950,13 @@ function resolveSimulationModel4(result, jointOverrides) {
30190
30950
  return collectSimulationModel(def, { state: jointOverrides });
30191
30951
  }
30192
30952
  async function runUrdfCli(argv = process.argv.slice(2)) {
30193
- const { inputPaths, outputPath, jointOverrides } = parseArgs16(argv);
30953
+ const { inputPaths, outputPath, paramOverrides, jointOverrides } = parseArgs17(argv);
30194
30954
  requireExistingInputPaths(inputPaths);
30195
30955
  let failures = 0;
30196
30956
  for (const [index, scriptPath] of inputPaths.entries()) {
30197
30957
  printBatchHeader(scriptPath, index, inputPaths.length);
30198
30958
  try {
30199
- await exportUrdfInput(scriptPath, outputPath, jointOverrides);
30959
+ await exportUrdfInput(scriptPath, outputPath, paramOverrides, jointOverrides);
30200
30960
  } catch (error) {
30201
30961
  failures += 1;
30202
30962
  console.error(error instanceof Error ? error.message : String(error));
@@ -30206,17 +30966,18 @@ async function runUrdfCli(argv = process.argv.slice(2)) {
30206
30966
  process.exit(1);
30207
30967
  }
30208
30968
  }
30209
- async function exportUrdfInput(scriptPath, explicitOutputPath, jointOverrides) {
30969
+ async function exportUrdfInput(scriptPath, explicitOutputPath, paramOverrides, jointOverrides) {
30210
30970
  const code = readFileSync20(resolve35(scriptPath), "utf-8");
30211
30971
  const { allFiles, fileName } = collectProjectFiles(scriptPath);
30212
30972
  await init();
30973
+ setParamOverrides(paramOverrides);
30213
30974
  const result = applyCliJointOverrides(runScript(code, fileName, allFiles, { assemblyState: jointOverrides }), jointOverrides);
30214
30975
  if (result.error) {
30215
30976
  throw new Error(`ERROR: ${result.error}`);
30216
30977
  }
30217
30978
  const robot = resolveSimulationModel4(result, jointOverrides);
30218
30979
  if (!robot) {
30219
- throw new Error("URDF export requires the script to return assembly(...).withSimulation(...) or call legacy robotExport({...}).");
30980
+ throw new Error("URDF export requires the script to return assembly(...).withSimulation(...).");
30220
30981
  }
30221
30982
  const packageOut = buildUrdfRobotPackage(robot);
30222
30983
  const targetDir = resolve35(batchOutputPath(scriptPath, explicitOutputPath, defaultOutputPath10));
@@ -30777,9 +31538,10 @@ ${jointResults.map((result) => result.text).join("\n\n")}
30777
31538
  }
30778
31539
 
30779
31540
  // cli/forge-usd.ts
30780
- function parseArgs17(argv) {
31541
+ function parseArgs18(argv) {
30781
31542
  const inputPaths = [];
30782
31543
  let outputPath;
31544
+ const paramOverrides = {};
30783
31545
  const jointOverrides = {};
30784
31546
  for (let i = 0; i < argv.length; i += 1) {
30785
31547
  const arg = argv[i];
@@ -30796,21 +31558,29 @@ function parseArgs17(argv) {
30796
31558
  i += 1;
30797
31559
  continue;
30798
31560
  }
31561
+ if (arg === "--param" || arg === "-p") {
31562
+ const value = argv[i + 1];
31563
+ if (!value || value.startsWith("--")) throw new Error(`${arg} requires Key=Value`);
31564
+ addCliParamOverride(value, paramOverrides);
31565
+ i += 1;
31566
+ continue;
31567
+ }
30799
31568
  if (arg.startsWith("--")) throw new Error(`Unknown flag: ${arg}`);
30800
31569
  inputPaths.push(arg);
30801
31570
  }
30802
- requireInputPaths(inputPaths, "Usage: forgecad export usd <script.forge.js> [input ...] [--output dir] [--joint JointName=Value]");
31571
+ requireInputPaths(
31572
+ inputPaths,
31573
+ "Usage: forgecad export usd <script.forge.js> [input ...] [--output dir] [--param Key=Value] [--joint JointName=Value]"
31574
+ );
30803
31575
  requireScriptInputPaths(inputPaths);
30804
31576
  requireSingleInputForOutputPath(inputPaths, outputPath, "--output", "dir");
30805
- return { inputPaths, outputPath, jointOverrides };
31577
+ return { inputPaths, outputPath, paramOverrides, jointOverrides };
30806
31578
  }
30807
31579
  function defaultOutputPath11(scriptPath) {
30808
31580
  const abs = resolve36(scriptPath);
30809
31581
  return abs.slice(0, abs.length - extname12(abs).length) + ".usdpkg";
30810
31582
  }
30811
31583
  function resolveSimulationModel5(result, jointOverrides) {
30812
- const legacy = getCollectedRobotExport();
30813
- if (legacy) return { ...legacy, state: { ...legacy.state, ...jointOverrides } };
30814
31584
  const assembly2 = getRunResultAssemblySource(result);
30815
31585
  if (!assembly2) return null;
30816
31586
  const def = assembly2.describe();
@@ -30818,13 +31588,13 @@ function resolveSimulationModel5(result, jointOverrides) {
30818
31588
  return collectSimulationModel(def, { state: jointOverrides });
30819
31589
  }
30820
31590
  async function runUsdCli(argv = process.argv.slice(2)) {
30821
- const { inputPaths, outputPath, jointOverrides } = parseArgs17(argv);
31591
+ const { inputPaths, outputPath, paramOverrides, jointOverrides } = parseArgs18(argv);
30822
31592
  requireExistingInputPaths(inputPaths);
30823
31593
  let failures = 0;
30824
31594
  for (const [index, scriptPath] of inputPaths.entries()) {
30825
31595
  printBatchHeader(scriptPath, index, inputPaths.length);
30826
31596
  try {
30827
- await exportUsdInput(scriptPath, outputPath, jointOverrides);
31597
+ await exportUsdInput(scriptPath, outputPath, paramOverrides, jointOverrides);
30828
31598
  } catch (error) {
30829
31599
  failures += 1;
30830
31600
  console.error(error instanceof Error ? error.message : String(error));
@@ -30834,17 +31604,18 @@ async function runUsdCli(argv = process.argv.slice(2)) {
30834
31604
  process.exit(1);
30835
31605
  }
30836
31606
  }
30837
- async function exportUsdInput(scriptPath, explicitOutputPath, jointOverrides) {
31607
+ async function exportUsdInput(scriptPath, explicitOutputPath, paramOverrides, jointOverrides) {
30838
31608
  const code = readFileSync21(resolve36(scriptPath), "utf-8");
30839
31609
  const { allFiles, fileName } = collectProjectFiles(scriptPath);
30840
31610
  await init();
31611
+ setParamOverrides(paramOverrides);
30841
31612
  const result = applyCliJointOverrides(runScript(code, fileName, allFiles, { assemblyState: jointOverrides }), jointOverrides);
30842
31613
  if (result.error) {
30843
31614
  throw new Error(`ERROR: ${result.error}`);
30844
31615
  }
30845
31616
  const model = resolveSimulationModel5(result, jointOverrides);
30846
31617
  if (!model) {
30847
- throw new Error("USD export requires the script to return assembly(...).withSimulation(...) or call legacy robotExport({...}).");
31618
+ throw new Error("USD export requires the script to return assembly(...).withSimulation(...).");
30848
31619
  }
30849
31620
  const packageOut = buildUsdRobotPackage(model);
30850
31621
  const targetDir = resolve36(batchOutputPath(scriptPath, explicitOutputPath, defaultOutputPath11));
@@ -30886,9 +31657,9 @@ function parseWebArgs(argv) {
30886
31657
  return options;
30887
31658
  }
30888
31659
  function waitForExit2(child) {
30889
- return new Promise((resolve44, reject) => {
31660
+ return new Promise((resolve46, reject) => {
30890
31661
  child.once("error", reject);
30891
- child.once("exit", (code) => resolve44(code ?? 0));
31662
+ child.once("exit", (code) => resolve46(code ?? 0));
30892
31663
  });
30893
31664
  }
30894
31665
  async function runWebCli(argv = process.argv.slice(2)) {
@@ -31502,10 +32273,10 @@ function padRight(s, w) {
31502
32273
  }
31503
32274
  function confirm(question) {
31504
32275
  const rl = createInterface2({ input: process.stdin, output: process.stderr });
31505
- return new Promise((resolve44) => {
32276
+ return new Promise((resolve46) => {
31506
32277
  rl.question(`${question} [y/N] `, (answer) => {
31507
32278
  rl.close();
31508
- resolve44(answer.trim().toLowerCase() === "y");
32279
+ resolve46(answer.trim().toLowerCase() === "y");
31509
32280
  });
31510
32281
  });
31511
32282
  }
@@ -31515,10 +32286,10 @@ function moveCandidateReasonLabel(candidate) {
31515
32286
  }
31516
32287
  function promptChoice(question) {
31517
32288
  const rl = createInterface2({ input: process.stdin, output: process.stderr });
31518
- return new Promise((resolve44) => {
32289
+ return new Promise((resolve46) => {
31519
32290
  rl.question(question, (answer) => {
31520
32291
  rl.close();
31521
- resolve44(answer.trim());
32292
+ resolve46(answer.trim());
31522
32293
  });
31523
32294
  });
31524
32295
  }
@@ -33440,7 +34211,7 @@ function takeValue(argv, index, name) {
33440
34211
  }
33441
34212
  return value;
33442
34213
  }
33443
- function parseArgs18(argv) {
34214
+ function parseArgs19(argv) {
33444
34215
  const { consumed: focusConsumed } = parseFocusFlags(argv);
33445
34216
  const consumed = new Set(focusConsumed);
33446
34217
  const profileOverrides = {};
@@ -33619,7 +34390,7 @@ function scriptErrorReport(scriptPath, message, profile, elapsedMs) {
33619
34390
  };
33620
34391
  }
33621
34392
  async function runPrintCheckCli(argv = process.argv.slice(2)) {
33622
- const args = parseArgs18(argv);
34393
+ const args = parseArgs19(argv);
33623
34394
  requireExistingInputPaths(args.inputPaths);
33624
34395
  const profile = resolvePrintProfile(args.profileId, args.profileOverrides);
33625
34396
  const reports = [];
@@ -33687,12 +34458,1481 @@ async function runPrintCheckInput(args, scriptPath, profile, argv) {
33687
34458
  return report;
33688
34459
  }
33689
34460
 
34461
+ // cli/forge-pinocchio.ts
34462
+ import { mkdirSync as mkdirSync13, readFileSync as readFileSync25, writeFileSync as writeFileSync20 } from "fs";
34463
+ import { dirname as dirname13, extname as extname13, resolve as resolve41 } from "path";
34464
+
34465
+ // src/forge/export/pinocchioExport.ts
34466
+ var DEG2RAD = Math.PI / 180;
34467
+ function defaultRange(type) {
34468
+ return type === "prismatic" ? { min: 0, max: 0.1 } : { min: -Math.PI / 2, max: Math.PI / 2 };
34469
+ }
34470
+ function toModelUnits(type, value) {
34471
+ return type === "prismatic" ? value * 1e-3 : value * DEG2RAD;
34472
+ }
34473
+ function buildPinocchioPackage(spec) {
34474
+ const urdf = buildUrdfRobotPackage(spec);
34475
+ const assemblyJoints = new Map(spec.assembly.joints.map((j) => [j.name, j]));
34476
+ const actuated = urdf.manifest.joints.filter((j) => j.type === "revolute" || j.type === "prismatic");
34477
+ if (actuated.length === 0) {
34478
+ throw new Error("PINOCCHIO.MOTION.EMPTY: the assembly has no revolute or prismatic joints to drive.");
34479
+ }
34480
+ const joints = actuated.map((j) => {
34481
+ const source = assemblyJoints.get(j.sourceName);
34482
+ const range = source && source.min !== void 0 && source.max !== void 0 ? { min: toModelUnits(j.type, source.min), max: toModelUnits(j.type, source.max) } : defaultRange(j.type);
34483
+ const effort = spec.joints[j.sourceName]?.effort;
34484
+ return {
34485
+ sourceName: j.sourceName,
34486
+ urdfName: j.urdfName,
34487
+ type: j.type,
34488
+ min: range.min,
34489
+ max: range.max,
34490
+ samples: 17,
34491
+ budget: typeof effort === "number" && effort > 0 ? effort : null
34492
+ };
34493
+ });
34494
+ const manifest = {
34495
+ format: "forgecad-pinocchio-package",
34496
+ modelName: urdf.modelName,
34497
+ sourceModelName: spec.modelName,
34498
+ urdfPath: urdf.manifest.urdfPath,
34499
+ feedbackPath: "feedback.json",
34500
+ expectedDof: actuated.length,
34501
+ gravity: [0, 0, -9.80665],
34502
+ joints,
34503
+ install: {
34504
+ linuxMac: "uv run --python 3.11 --with pin --with numpy python scripts/run_pinocchio.py",
34505
+ windows: "conda install pinocchio coal -c conda-forge # then: python scripts/run_pinocchio.py"
34506
+ },
34507
+ runCommand: "uv run --python 3.11 --with pin --with numpy python scripts/run_pinocchio.py"
34508
+ };
34509
+ const files = [
34510
+ ...urdf.files,
34511
+ { path: "pinocchio-manifest.json", text: JSON.stringify(manifest, null, 2) },
34512
+ { path: "scripts/run_pinocchio.py", text: runPinocchioScript() },
34513
+ { path: "README.md", text: readme(manifest) }
34514
+ ];
34515
+ return { modelName: urdf.modelName, manifest, files };
34516
+ }
34517
+ function readme(manifest) {
34518
+ return [
34519
+ `# ${manifest.sourceModelName} \u2014 Pinocchio package`,
34520
+ "",
34521
+ "Generated by ForgeCAD. The model is a standard URDF with per-link inertials computed from the",
34522
+ "solid geometry; Pinocchio (BSD-2-Clause) builds its dynamics model from it.",
34523
+ "",
34524
+ "## Run",
34525
+ "",
34526
+ "Linux / macOS:",
34527
+ "",
34528
+ "```bash",
34529
+ manifest.install.linuxMac,
34530
+ "```",
34531
+ "",
34532
+ "Windows (no PyPI wheel \u2014 use conda-forge):",
34533
+ "",
34534
+ "```bash",
34535
+ manifest.install.windows,
34536
+ "```",
34537
+ "",
34538
+ "`run_pinocchio.py` sweeps each joint across its range, computes the gravity-hold torque with",
34539
+ "`pin.computeGeneralizedGravity`, and writes `feedback.json` (the `forgecad.feedback/v1` contract).",
34540
+ "Re-surface it in ForgeCAD with `forgecad sim dynamics <this-dir>`.",
34541
+ "",
34542
+ "Coal (BSD-3-Clause) provides collision via convex hulls / BVH over `.obj`/`.stl`/`.dae` meshes;",
34543
+ "it performs no convex decomposition. This package ships visual + collision STLs from the URDF export.",
34544
+ "",
34545
+ "Cross-check: the gravity torque at pose 0 should match `forgecad sim mechanism` static-hold torque."
34546
+ ].join("\n");
34547
+ }
34548
+ function runPinocchioScript() {
34549
+ return `#!/usr/bin/env python3
34550
+ """Compute per-joint gravity-hold torque budgets with Pinocchio and emit feedback.json."""
34551
+ import json
34552
+ import os
34553
+ import sys
34554
+
34555
+ import numpy as np
34556
+
34557
+ try:
34558
+ import pinocchio as pin
34559
+ except ImportError:
34560
+ raise SystemExit(
34561
+ "Pinocchio not found. Linux/macOS: pip install pin coal. "
34562
+ "Windows: conda install pinocchio coal -c conda-forge."
34563
+ )
34564
+
34565
+ HERE = os.path.dirname(os.path.abspath(__file__))
34566
+ ROOT = os.path.dirname(HERE)
34567
+ manifest = json.load(open(os.path.join(ROOT, "pinocchio-manifest.json")))
34568
+
34569
+ model = pin.buildModelFromUrdf(os.path.join(ROOT, manifest["urdfPath"]))
34570
+ data = model.createData()
34571
+ model.gravity.linear = np.array(manifest["gravity"], dtype=float)
34572
+
34573
+ metrics = [{"key": "dofCount", "label": "DOF (model.nv)", "value": int(model.nv), "unit": "", "status": "info"}]
34574
+ findings = []
34575
+ joint_data = []
34576
+
34577
+ for spec in manifest["joints"]:
34578
+ if not model.existJointName(spec["urdfName"]):
34579
+ findings.append({"level": "warning", "code": "PINOCCHIO.JOINT.MISSING",
34580
+ "message": "URDF joint %s not found in the built model" % spec["urdfName"],
34581
+ "path": spec["sourceName"]})
34582
+ continue
34583
+ jid = model.getJointId(spec["urdfName"])
34584
+ iq = model.joints[jid].idx_q
34585
+ iv = model.joints[jid].idx_v
34586
+ peak = 0.0
34587
+ for value in np.linspace(spec["min"], spec["max"], int(spec["samples"])):
34588
+ q = pin.neutral(model)
34589
+ q[iq] = value
34590
+ g = pin.computeGeneralizedGravity(model, data, q)
34591
+ peak = max(peak, abs(float(g[iv])))
34592
+ is_revolute = spec["type"] == "revolute"
34593
+ unit = "N\xB7m" if is_revolute else "N"
34594
+ key = ("peakGravityTorqueNm." if is_revolute else "peakGravityForceN.") + spec["sourceName"]
34595
+ metrics.append({"key": key, "label": "Peak gravity load (%s)" % spec["sourceName"],
34596
+ "value": round(peak, 4), "unit": unit, "status": "info"})
34597
+ entry = {"joint": spec["sourceName"], "type": spec["type"], "peak": round(peak, 4), "budget": spec["budget"]}
34598
+ if spec["budget"]:
34599
+ margin = (spec["budget"] - peak) / spec["budget"] * 100.0
34600
+ entry["marginPct"] = round(margin, 2)
34601
+ metrics.append({"key": "torqueMarginPct." + spec["sourceName"], "label": "Budget margin (%s)" % spec["sourceName"],
34602
+ "value": round(margin, 2), "unit": "%", "status": "pass" if margin >= 0 else "fail",
34603
+ "target": {"op": ">=", "value": 0}})
34604
+ if margin < 0:
34605
+ findings.append({"level": "error", "code": "PINOCCHIO.HOLD.OVER_BUDGET", "path": spec["sourceName"],
34606
+ "message": "Joint %s peaks at %.3f %s over its %.3f %s budget" % (
34607
+ spec["sourceName"], peak, unit, spec["budget"], unit),
34608
+ "suggest": "Increase the Sim.drive effort or reduce the distal moment."})
34609
+ joint_data.append(entry)
34610
+
34611
+ ok = all(m.get("status") != "fail" for m in metrics) and not any(f["level"] == "error" for f in findings)
34612
+ report = {
34613
+ "schema": "forgecad.feedback/v1", "category": "dynamics", "source": manifest["sourceModelName"], "ok": ok,
34614
+ "summary": "%d DOF, %d joint(s) swept" % (model.nv, len(joint_data)),
34615
+ "metrics": metrics, "findings": findings,
34616
+ "trust": {"assumptions": ["Pinocchio gravity-hold torque swept over joint ranges; no contact or friction."],
34617
+ "converged": True},
34618
+ "data": {"dof": int(model.nv), "joints": joint_data}, "timeMs": 0,
34619
+ }
34620
+ out = os.path.join(ROOT, manifest["feedbackPath"])
34621
+ json.dump(report, open(out, "w"), indent=2)
34622
+ print("Wrote %s (ok=%s)" % (out, ok))
34623
+ sys.exit(0 if ok else 1)
34624
+ `;
34625
+ }
34626
+
34627
+ // cli/forge-pinocchio.ts
34628
+ function parseArgs20(argv) {
34629
+ const inputPaths = [];
34630
+ let outputPath;
34631
+ const paramOverrides = {};
34632
+ const jointOverrides = {};
34633
+ for (let i = 0; i < argv.length; i += 1) {
34634
+ const arg = argv[i];
34635
+ if (arg === "--output") {
34636
+ outputPath = argv[i + 1];
34637
+ if (!outputPath) throw new Error("--output requires a directory path");
34638
+ i += 1;
34639
+ continue;
34640
+ }
34641
+ if (arg === "--joint") {
34642
+ const value = argv[i + 1];
34643
+ if (!value || value.startsWith("--")) throw new Error("--joint requires JointName=Value");
34644
+ addCliJointOverride(value, jointOverrides);
34645
+ i += 1;
34646
+ continue;
34647
+ }
34648
+ if (arg === "--param" || arg === "-p") {
34649
+ const value = argv[i + 1];
34650
+ if (!value || value.startsWith("--")) throw new Error(`${arg} requires Key=Value`);
34651
+ addCliParamOverride(value, paramOverrides);
34652
+ i += 1;
34653
+ continue;
34654
+ }
34655
+ if (arg.startsWith("--")) throw new Error(`Unknown flag: ${arg}`);
34656
+ inputPaths.push(arg);
34657
+ }
34658
+ requireInputPaths(inputPaths, "Usage: forgecad export pinocchio <script.forge.js> [input ...] [--output dir] [--param Key=Value] [--joint JointName=Value]");
34659
+ requireScriptInputPaths(inputPaths);
34660
+ requireSingleInputForOutputPath(inputPaths, outputPath, "--output", "dir");
34661
+ return { inputPaths, outputPath, paramOverrides, jointOverrides };
34662
+ }
34663
+ function defaultOutputPath12(scriptPath) {
34664
+ const abs = resolve41(scriptPath);
34665
+ return abs.slice(0, abs.length - extname13(abs).length) + ".pinocchiopkg";
34666
+ }
34667
+ function resolveSimulationModel6(result, jointOverrides) {
34668
+ const assembly2 = getRunResultAssemblySource(result);
34669
+ if (!assembly2) return null;
34670
+ const def = assembly2.describe();
34671
+ if (!def.sim) return null;
34672
+ return collectSimulationModel(def, { state: jointOverrides });
34673
+ }
34674
+ async function runPinocchioCli(argv = process.argv.slice(2)) {
34675
+ const { inputPaths, outputPath, paramOverrides, jointOverrides } = parseArgs20(argv);
34676
+ requireExistingInputPaths(inputPaths);
34677
+ let failures = 0;
34678
+ for (const [index, scriptPath] of inputPaths.entries()) {
34679
+ printBatchHeader(scriptPath, index, inputPaths.length);
34680
+ try {
34681
+ await exportPinocchioInput(scriptPath, outputPath, paramOverrides, jointOverrides);
34682
+ } catch (error) {
34683
+ failures += 1;
34684
+ console.error(error instanceof Error ? error.message : String(error));
34685
+ }
34686
+ }
34687
+ if (failures > 0) process.exit(1);
34688
+ }
34689
+ async function exportPinocchioInput(scriptPath, explicitOutputPath, paramOverrides, jointOverrides) {
34690
+ const code = readFileSync25(resolve41(scriptPath), "utf-8");
34691
+ const { allFiles, fileName } = collectProjectFiles(scriptPath);
34692
+ await init();
34693
+ setParamOverrides(paramOverrides);
34694
+ const result = applyCliJointOverrides(runScript(code, fileName, allFiles, { assemblyState: jointOverrides }), jointOverrides);
34695
+ if (result.error) throw new Error(`ERROR: ${result.error}`);
34696
+ const robot = resolveSimulationModel6(result, jointOverrides);
34697
+ if (!robot) throw new Error("Pinocchio export requires the script to return assembly(...).withSimulation(...).");
34698
+ const packageOut = buildPinocchioPackage(robot);
34699
+ const targetDir = resolve41(batchOutputPath(scriptPath, explicitOutputPath, defaultOutputPath12));
34700
+ packageOut.files.forEach((file) => {
34701
+ const absPath = resolve41(targetDir, file.path);
34702
+ mkdirSync13(dirname13(absPath), { recursive: true });
34703
+ if (file.text !== void 0) writeFileSync20(absPath, file.text, "utf-8");
34704
+ else if (file.bytes) writeFileSync20(absPath, Buffer.from(file.bytes));
34705
+ });
34706
+ console.log(`\u2713 Exported Pinocchio package to ${targetDir}`);
34707
+ console.log(` urdf: ${packageOut.manifest.urdfPath}`);
34708
+ console.log(` run: ${packageOut.manifest.runCommand}`);
34709
+ console.log(` then: forgecad sim dynamics ${targetDir}`);
34710
+ }
34711
+
34712
+ // cli/sim-dynamics.ts
34713
+ import { existsSync as existsSync21, readFileSync as readFileSync26, statSync as statSync9 } from "fs";
34714
+ import { join as join15, resolve as resolve42 } from "path";
34715
+ function parseArgs21(argv) {
34716
+ const inputPaths = [];
34717
+ let json = false;
34718
+ let compact = false;
34719
+ let noFailExit = false;
34720
+ for (let i = 0; i < argv.length; i += 1) {
34721
+ const arg = argv[i];
34722
+ if (arg === "--json") {
34723
+ json = true;
34724
+ continue;
34725
+ }
34726
+ if (arg === "--compact") {
34727
+ compact = true;
34728
+ continue;
34729
+ }
34730
+ if (arg === "--no-fail-exit") {
34731
+ noFailExit = true;
34732
+ continue;
34733
+ }
34734
+ if (arg.startsWith("--")) throw new Error(`Unknown flag: ${arg}`);
34735
+ inputPaths.push(arg);
34736
+ }
34737
+ requireInputPaths(inputPaths, "Usage: forgecad sim dynamics <pinocchio-package-dir|feedback.json> [--json]");
34738
+ return { inputPaths, json, compact, noFailExit };
34739
+ }
34740
+ function failedReport3(source, message, code, suggest) {
34741
+ return {
34742
+ schema: FEEDBACK_SCHEMA,
34743
+ category: "dynamics",
34744
+ source,
34745
+ ok: false,
34746
+ summary: "dynamics results unavailable",
34747
+ metrics: [],
34748
+ findings: [{ level: "error", code, message, ...suggest ? { suggest } : {} }],
34749
+ trust: { assumptions: [] },
34750
+ data: {},
34751
+ timeMs: 0
34752
+ };
34753
+ }
34754
+ function resolveFeedbackPath(inputPath) {
34755
+ const abs = resolve42(inputPath);
34756
+ if (existsSync21(abs) && statSync9(abs).isDirectory()) return join15(abs, "feedback.json");
34757
+ return abs;
34758
+ }
34759
+ function ingest(inputPath) {
34760
+ const feedbackPath = resolveFeedbackPath(inputPath);
34761
+ if (!existsSync21(feedbackPath)) {
34762
+ return failedReport3(
34763
+ inputPath,
34764
+ `No results at ${feedbackPath}.`,
34765
+ "DYNAMICS.NOT_RUN",
34766
+ "Run the exported scripts/run_pinocchio.py first (see the package README), then re-run sim dynamics."
34767
+ );
34768
+ }
34769
+ let parsed;
34770
+ try {
34771
+ parsed = JSON.parse(readFileSync26(feedbackPath, "utf-8"));
34772
+ } catch (error) {
34773
+ return failedReport3(inputPath, `Could not parse ${feedbackPath}: ${error instanceof Error ? error.message : String(error)}`, "DYNAMICS.PARSE_ERROR");
34774
+ }
34775
+ const report = parsed;
34776
+ if (!report || report.schema !== FEEDBACK_SCHEMA || !Array.isArray(report.metrics)) {
34777
+ return failedReport3(
34778
+ inputPath,
34779
+ `${feedbackPath} is not a ${FEEDBACK_SCHEMA} report.`,
34780
+ "DYNAMICS.BAD_SCHEMA",
34781
+ "Regenerate it with the exported run_pinocchio.py."
34782
+ );
34783
+ }
34784
+ return { ...report, source: inputPath };
34785
+ }
34786
+ async function runSimDynamicsCli(argv = process.argv.slice(2)) {
34787
+ const options = parseArgs21(argv);
34788
+ const reports = options.inputPaths.map((inputPath) => ingest(inputPath));
34789
+ printFeedback(reports.length === 1 ? reports[0] : { runs: reports }, options.json, options.compact);
34790
+ if (reports.some((report) => !report.ok) && !options.noFailExit) process.exitCode = 1;
34791
+ }
34792
+
34793
+ // cli/sim-mass.ts
34794
+ var DEFAULT_DENSITY_KG_M35 = 1e3;
34795
+ function parseArgs22(argv) {
34796
+ const inputPaths = [];
34797
+ const paramOverrides = {};
34798
+ const jointOverrides = {};
34799
+ let json = false;
34800
+ let compact = false;
34801
+ let densityKgM3 = null;
34802
+ let backend;
34803
+ let quality;
34804
+ for (let i = 0; i < argv.length; i += 1) {
34805
+ const arg = argv[i];
34806
+ if (arg === "--json") {
34807
+ json = true;
34808
+ continue;
34809
+ }
34810
+ if (arg === "--compact") {
34811
+ compact = true;
34812
+ continue;
34813
+ }
34814
+ if (arg === "--density") {
34815
+ const value = Number(argv[i + 1]);
34816
+ if (!Number.isFinite(value) || value <= 0) throw new Error("--density requires a positive number (kg/m\xB3)");
34817
+ densityKgM3 = value;
34818
+ i += 1;
34819
+ continue;
34820
+ }
34821
+ if (arg === "--quality" || arg === "-q") {
34822
+ const val = argv[i + 1];
34823
+ if (val !== "default" && val !== "live" && val !== "high") throw new Error("--quality must be default, live, or high");
34824
+ quality = val;
34825
+ i += 1;
34826
+ continue;
34827
+ }
34828
+ if (arg === "--backend") {
34829
+ const val = argv[i + 1];
34830
+ if (val !== "manifold" && val !== "occt" && val !== "truck" && val !== "sdf") {
34831
+ throw new Error("--backend must be manifold, occt, truck, or sdf");
34832
+ }
34833
+ backend = val;
34834
+ i += 1;
34835
+ continue;
34836
+ }
34837
+ if (arg === "--joint") {
34838
+ const value = argv[i + 1];
34839
+ if (!value || value.startsWith("--")) throw new Error("--joint requires JointName=Value");
34840
+ addCliJointOverride(value, jointOverrides);
34841
+ i += 1;
34842
+ continue;
34843
+ }
34844
+ if (arg === "--param" || arg === "-p") {
34845
+ const value = argv[i + 1];
34846
+ if (!value || value.startsWith("--")) throw new Error(`${arg} requires Key=Value`);
34847
+ addCliParamOverride(value, paramOverrides);
34848
+ i += 1;
34849
+ continue;
34850
+ }
34851
+ if (arg.startsWith("--")) throw new Error(`Unknown flag: ${arg}`);
34852
+ inputPaths.push(arg);
34853
+ }
34854
+ requireInputPaths(
34855
+ inputPaths,
34856
+ "Usage: forgecad sim mass <model.forge.js> [input ...] [--density kg/m3] [--json] [--param Key=Value] [--joint JointName=Value]"
34857
+ );
34858
+ requireScriptInputPaths(inputPaths);
34859
+ return { inputPaths, json, compact, densityKgM3, backend, quality, paramOverrides, jointOverrides };
34860
+ }
34861
+ function failedReport4(source, message, code = "MASS.ERROR") {
34862
+ return {
34863
+ schema: FEEDBACK_SCHEMA,
34864
+ category: "mass-properties",
34865
+ source,
34866
+ ok: false,
34867
+ summary: "mass-properties analysis failed",
34868
+ metrics: [],
34869
+ findings: [{ level: "error", code, message }],
34870
+ trust: { assumptions: [] },
34871
+ data: {},
34872
+ timeMs: 0
34873
+ };
34874
+ }
34875
+ function round4(value, digits = 6) {
34876
+ const factor = 10 ** digits;
34877
+ return Math.round(value * factor) / factor;
34878
+ }
34879
+ function analyze2(source, options, objects) {
34880
+ const start = Date.now();
34881
+ const density = options.densityKgM3 ?? DEFAULT_DENSITY_KG_M35;
34882
+ const props = analyzeMassProperties(objects.map((o) => ({ name: o.name, mesh: o.mesh, densityKgM3: density })));
34883
+ const findings = [];
34884
+ const assumptions = [
34885
+ "Mass properties integrated from the triangulated mesh (exact for the mesh; curved features carry tessellation error).",
34886
+ "Manifold kernel meshes are watertight by construction."
34887
+ ];
34888
+ if (options.densityKgM3 === null) {
34889
+ findings.push({
34890
+ level: "warning",
34891
+ code: "MASS.DENSITY.DEFAULTED",
34892
+ message: `No density given; using default ${DEFAULT_DENSITY_KG_M35} kg/m\xB3 (water). Volume, center of mass, and principal axes are exact regardless.`,
34893
+ suggest: "Pass --density <kg/m3> (e.g. 2700 aluminium, 7850 steel, 1240 ABS) for accurate mass and inertia magnitude."
34894
+ });
34895
+ assumptions.push(`uniform density ${DEFAULT_DENSITY_KG_M35} kg/m\xB3 (default)`);
34896
+ } else {
34897
+ assumptions.push(`uniform density ${density} kg/m\xB3`);
34898
+ }
34899
+ const metrics = [
34900
+ makeMetric("totalMassKg", "Total mass", round4(props.totalMassKg, 6), { unit: "kg" }),
34901
+ makeMetric("totalVolumeCm3", "Volume", round4(props.totalVolumeMm3 / 1e3, 4), { unit: "cm\xB3" }),
34902
+ makeMetric("surfaceAreaCm2", "Surface area", round4(props.surfaceAreaMm2 / 100, 4), { unit: "cm\xB2" }),
34903
+ makeMetric("comXmm", "Center of mass X", round4(props.centerOfMassMm[0], 4), { unit: "mm" }),
34904
+ makeMetric("comYmm", "Center of mass Y", round4(props.centerOfMassMm[1], 4), { unit: "mm" }),
34905
+ makeMetric("comZmm", "Center of mass Z", round4(props.centerOfMassMm[2], 4), { unit: "mm" }),
34906
+ makeMetric("principalI1", "Principal moment I1", round4(props.principalMomentsKgM2[0], 9), { unit: "kg\xB7m\xB2" }),
34907
+ makeMetric("principalI2", "Principal moment I2", round4(props.principalMomentsKgM2[1], 9), { unit: "kg\xB7m\xB2" }),
34908
+ makeMetric("principalI3", "Principal moment I3", round4(props.principalMomentsKgM2[2], 9), { unit: "kg\xB7m\xB2" })
34909
+ ];
34910
+ return {
34911
+ schema: FEEDBACK_SCHEMA,
34912
+ category: "mass-properties",
34913
+ source,
34914
+ ok: reportOk(metrics, findings),
34915
+ summary: `mass ${round4(props.totalMassKg, 4)} kg \xB7 volume ${round4(props.totalVolumeMm3 / 1e3, 2)} cm\xB3 \xB7 CoM (${round4(
34916
+ props.centerOfMassMm[0],
34917
+ 2
34918
+ )}, ${round4(props.centerOfMassMm[1], 2)}, ${round4(props.centerOfMassMm[2], 2)}) mm`,
34919
+ metrics,
34920
+ findings,
34921
+ trust: { assumptions, watertight: true },
34922
+ data: {
34923
+ densityKgM3: density,
34924
+ totalVolumeMm3: round4(props.totalVolumeMm3, 4),
34925
+ totalMassKg: round4(props.totalMassKg, 6),
34926
+ centerOfMassMm: props.centerOfMassMm.map((v) => round4(v, 4)),
34927
+ inertiaTensorKgM2: {
34928
+ ixx: round4(props.inertiaTensorKgM2.ixx, 9),
34929
+ iyy: round4(props.inertiaTensorKgM2.iyy, 9),
34930
+ izz: round4(props.inertiaTensorKgM2.izz, 9),
34931
+ ixy: round4(props.inertiaTensorKgM2.ixy, 9),
34932
+ ixz: round4(props.inertiaTensorKgM2.ixz, 9),
34933
+ iyz: round4(props.inertiaTensorKgM2.iyz, 9)
34934
+ },
34935
+ principalMomentsKgM2: props.principalMomentsKgM2.map((v) => round4(v, 9)),
34936
+ principalAxes: props.principalAxes.map((axis) => axis.map((v) => round4(v, 6))),
34937
+ radiusOfGyrationM: props.radiusOfGyrationM.map((v) => round4(v, 6)),
34938
+ boundingBoxMm: {
34939
+ minMm: props.boundingBoxMm.minMm.map((v) => round4(v, 4)),
34940
+ maxMm: props.boundingBoxMm.maxMm.map((v) => round4(v, 4)),
34941
+ sizeMm: props.boundingBoxMm.sizeMm.map((v) => round4(v, 4))
34942
+ },
34943
+ perObject: props.perObject.map((o) => ({
34944
+ name: o.name,
34945
+ volumeMm3: round4(o.volumeMm3, 4),
34946
+ massKg: round4(o.massKg, 6),
34947
+ centerOfMassMm: o.centerOfMassMm.map((v) => round4(v, 4))
34948
+ }))
34949
+ },
34950
+ timeMs: Date.now() - start
34951
+ };
34952
+ }
34953
+ async function runSimMassCli(argv = process.argv.slice(2)) {
34954
+ const options = parseArgs22(argv);
34955
+ requireExistingInputPaths(options.inputPaths);
34956
+ await init();
34957
+ const reports = [];
34958
+ for (const [index, scriptPath] of options.inputPaths.entries()) {
34959
+ if (!options.json) printBatchHeader(scriptPath, index, options.inputPaths.length);
34960
+ let report;
34961
+ try {
34962
+ const { objects, error } = await loadAnalysisObjects(scriptPath, {
34963
+ backend: options.backend,
34964
+ quality: options.quality,
34965
+ paramOverrides: options.paramOverrides,
34966
+ jointOverrides: options.jointOverrides
34967
+ });
34968
+ report = error ? failedReport4(scriptPath, error, "MASS.MODEL.EMPTY") : analyze2(scriptPath, options, objects);
34969
+ } catch (error) {
34970
+ report = failedReport4(scriptPath, error instanceof Error ? error.message : String(error));
34971
+ }
34972
+ reports.push(report);
34973
+ }
34974
+ printFeedback(reports.length === 1 ? reports[0] : { runs: reports }, options.json, options.compact);
34975
+ if (reports.some((report) => !report.ok)) process.exitCode = 1;
34976
+ }
34977
+
34978
+ // src/forge/analysis/mechanism.ts
34979
+ var GRAVITY_MS2 = 9.80665;
34980
+ var DEFAULT_GRAVITY = [0, 0, -GRAVITY_MS2];
34981
+ function cross7(a, b) {
34982
+ return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
34983
+ }
34984
+ function dot5(a, b) {
34985
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
34986
+ }
34987
+ function normalize4(v) {
34988
+ const len = Math.hypot(v[0], v[1], v[2]);
34989
+ if (len < 1e-12) throw new Error("mechanism joint axis has zero length");
34990
+ return [v[0] / len, v[1] / len, v[2] / len];
34991
+ }
34992
+ function freedoms(type) {
34993
+ return type === "fixed" ? 0 : 1;
34994
+ }
34995
+ function distalLinks(jointChild, childrenOf) {
34996
+ const names = /* @__PURE__ */ new Set();
34997
+ let cycle = false;
34998
+ const stack = [jointChild];
34999
+ while (stack.length > 0) {
35000
+ const link = stack.pop();
35001
+ if (names.has(link)) {
35002
+ cycle = true;
35003
+ continue;
35004
+ }
35005
+ names.add(link);
35006
+ for (const j of childrenOf.get(link) ?? []) stack.push(j.child);
35007
+ }
35008
+ return { names, cycle };
35009
+ }
35010
+ function analyzeMechanism(input) {
35011
+ const { links, joints } = input;
35012
+ if (links.length === 0) throw new Error("mechanism analysis needs at least one link");
35013
+ const gravity = input.gravity ?? DEFAULT_GRAVITY;
35014
+ const couplings = input.couplings ?? 0;
35015
+ const linkByName = new Map(links.map((l) => [l.name, l]));
35016
+ for (const j of joints) {
35017
+ if (!linkByName.has(j.parent)) throw new Error(`joint "${j.name}" references unknown parent link "${j.parent}"`);
35018
+ if (!linkByName.has(j.child)) throw new Error(`joint "${j.name}" references unknown child link "${j.child}"`);
35019
+ }
35020
+ const childrenOf = /* @__PURE__ */ new Map();
35021
+ for (const j of joints) {
35022
+ if (!childrenOf.has(j.parent)) childrenOf.set(j.parent, []);
35023
+ childrenOf.get(j.parent).push(j);
35024
+ }
35025
+ const freedomSum = joints.reduce((sum2, j) => sum2 + freedoms(j.type), 0);
35026
+ const actuatedJointCount = joints.filter((j) => j.type !== "fixed").length;
35027
+ const fixedJointCount = joints.length - actuatedJointCount;
35028
+ const hasClosedLoops = joints.length > links.length - 1;
35029
+ const actuatedDof = freedomSum - couplings;
35030
+ const grueblerSpatial = 6 * (links.length - 1 - joints.length) + freedomSum - couplings;
35031
+ const totalMassKg = links.reduce((sum2, l) => sum2 + l.massKg, 0);
35032
+ const jointLoads = [];
35033
+ for (const joint2 of joints) {
35034
+ if (joint2.type === "fixed") continue;
35035
+ const { names } = distalLinks(joint2.child, childrenOf);
35036
+ let mass = 0;
35037
+ const weightedCom = [0, 0, 0];
35038
+ for (const name of names) {
35039
+ const link = linkByName.get(name);
35040
+ mass += link.massKg;
35041
+ weightedCom[0] += link.massKg * link.comWorldMm[0];
35042
+ weightedCom[1] += link.massKg * link.comWorldMm[1];
35043
+ weightedCom[2] += link.massKg * link.comWorldMm[2];
35044
+ }
35045
+ const com = mass > 0 ? [weightedCom[0] / mass, weightedCom[1] / mass, weightedCom[2] / mass] : [...joint2.pointWorldMm];
35046
+ const axis = normalize4(joint2.axisWorld);
35047
+ const force = [mass * gravity[0], mass * gravity[1], mass * gravity[2]];
35048
+ const load = { joint: joint2.name, type: joint2.type, distalMassKg: mass, distalComWorldMm: com, marginPct: null };
35049
+ if (joint2.type === "revolute") {
35050
+ const r = [(com[0] - joint2.pointWorldMm[0]) / 1e3, (com[1] - joint2.pointWorldMm[1]) / 1e3, (com[2] - joint2.pointWorldMm[2]) / 1e3];
35051
+ load.holdTorqueNm = Math.abs(dot5(axis, cross7(r, force)));
35052
+ if (joint2.effort !== void 0 && joint2.effort > 0) {
35053
+ load.budget = joint2.effort;
35054
+ load.marginPct = (joint2.effort - load.holdTorqueNm) / joint2.effort * 100;
35055
+ }
35056
+ } else {
35057
+ load.holdForceN = Math.abs(dot5(force, axis));
35058
+ if (joint2.effort !== void 0 && joint2.effort > 0) {
35059
+ load.budget = joint2.effort;
35060
+ load.marginPct = (joint2.effort - load.holdForceN) / joint2.effort * 100;
35061
+ }
35062
+ }
35063
+ jointLoads.push(load);
35064
+ }
35065
+ return {
35066
+ linkCount: links.length,
35067
+ jointCount: joints.length,
35068
+ actuatedJointCount,
35069
+ fixedJointCount,
35070
+ couplings,
35071
+ hasClosedLoops,
35072
+ actuatedDof,
35073
+ grueblerSpatial,
35074
+ gravity,
35075
+ totalMassKg,
35076
+ jointLoads
35077
+ };
35078
+ }
35079
+
35080
+ // cli/sim-mechanism.ts
35081
+ var DEFAULT_DENSITY_KG_M36 = 1e3;
35082
+ function parseArgs23(argv) {
35083
+ const inputPaths = [];
35084
+ const paramOverrides = {};
35085
+ const jointOverrides = {};
35086
+ let json = false;
35087
+ let compact = false;
35088
+ let minTorqueMarginPct = 0;
35089
+ let densityKgM3 = DEFAULT_DENSITY_KG_M36;
35090
+ let noFailExit = false;
35091
+ let backend;
35092
+ let quality;
35093
+ for (let i = 0; i < argv.length; i += 1) {
35094
+ const arg = argv[i];
35095
+ if (arg === "--json") {
35096
+ json = true;
35097
+ continue;
35098
+ }
35099
+ if (arg === "--compact") {
35100
+ compact = true;
35101
+ continue;
35102
+ }
35103
+ if (arg === "--no-fail-exit") {
35104
+ noFailExit = true;
35105
+ continue;
35106
+ }
35107
+ if (arg === "--min-torque-margin-pct") {
35108
+ const n = Number(argv[++i]);
35109
+ if (!Number.isFinite(n)) throw new Error("--min-torque-margin-pct requires a number");
35110
+ minTorqueMarginPct = n;
35111
+ continue;
35112
+ }
35113
+ if (arg === "--density") {
35114
+ const n = Number(argv[++i]);
35115
+ if (!Number.isFinite(n) || n <= 0) throw new Error("--density requires a positive number (kg/m\xB3)");
35116
+ densityKgM3 = n;
35117
+ continue;
35118
+ }
35119
+ if (arg === "--quality" || arg === "-q") {
35120
+ const v = argv[i + 1];
35121
+ if (v !== "default" && v !== "live" && v !== "high") throw new Error("--quality must be default, live, or high");
35122
+ quality = v;
35123
+ i += 1;
35124
+ continue;
35125
+ }
35126
+ if (arg === "--backend") {
35127
+ const v = argv[i + 1];
35128
+ if (v !== "manifold" && v !== "occt" && v !== "truck" && v !== "sdf") throw new Error("--backend must be manifold, occt, truck, or sdf");
35129
+ backend = v;
35130
+ i += 1;
35131
+ continue;
35132
+ }
35133
+ if (arg === "--joint") {
35134
+ const value = argv[i + 1];
35135
+ if (!value || value.startsWith("--")) throw new Error("--joint requires JointName=Value");
35136
+ addCliJointOverride(value, jointOverrides);
35137
+ i += 1;
35138
+ continue;
35139
+ }
35140
+ if (arg === "--param" || arg === "-p") {
35141
+ const value = argv[i + 1];
35142
+ if (!value || value.startsWith("--")) throw new Error(`${arg} requires Key=Value`);
35143
+ addCliParamOverride(value, paramOverrides);
35144
+ i += 1;
35145
+ continue;
35146
+ }
35147
+ if (arg.startsWith("--")) throw new Error(`Unknown flag: ${arg}`);
35148
+ inputPaths.push(arg);
35149
+ }
35150
+ requireInputPaths(inputPaths, "Usage: forgecad sim mechanism <model.forge.js> [--min-torque-margin-pct N] [--json]");
35151
+ requireScriptInputPaths(inputPaths);
35152
+ return { inputPaths, json, compact, minTorqueMarginPct, densityKgM3, noFailExit, backend, quality, paramOverrides, jointOverrides };
35153
+ }
35154
+ function failedReport5(source, message, code = "MECHANISM.ERROR", suggest) {
35155
+ return {
35156
+ schema: FEEDBACK_SCHEMA,
35157
+ category: "mechanism",
35158
+ source,
35159
+ ok: false,
35160
+ summary: "mechanism analysis failed",
35161
+ metrics: [],
35162
+ findings: [{ level: "error", code, message, ...suggest ? { suggest } : {} }],
35163
+ trust: { assumptions: [] },
35164
+ data: {},
35165
+ timeMs: 0
35166
+ };
35167
+ }
35168
+ function round5(value, digits = 4) {
35169
+ const factor = 10 ** digits;
35170
+ return Math.round(value * factor) / factor;
35171
+ }
35172
+ function flattenShapes(part, out) {
35173
+ if (part instanceof Shape) out.push(part);
35174
+ else if (part instanceof ShapeGroup) part.children.forEach((child) => flattenShapes(child, out));
35175
+ }
35176
+ async function runSimMechanismCli(argv = process.argv.slice(2)) {
35177
+ const options = parseArgs23(argv);
35178
+ requireExistingInputPaths(options.inputPaths);
35179
+ await init();
35180
+ const reports = [];
35181
+ for (const [index, scriptPath] of options.inputPaths.entries()) {
35182
+ if (!options.json) printBatchHeader(scriptPath, index, options.inputPaths.length);
35183
+ let report;
35184
+ try {
35185
+ report = analyze3(scriptPath, options, await loadAnalysisObjectsWithAssembly(scriptPath, options));
35186
+ } catch (error) {
35187
+ report = failedReport5(scriptPath, error instanceof Error ? error.message : String(error));
35188
+ }
35189
+ reports.push(report);
35190
+ }
35191
+ printFeedback(reports.length === 1 ? reports[0] : { runs: reports }, options.json, options.compact);
35192
+ if (reports.some((report) => !report.ok) && !options.noFailExit) process.exitCode = 1;
35193
+ }
35194
+ function analyze3(source, options, loaded) {
35195
+ const start = Date.now();
35196
+ if (loaded.error) return failedReport5(source, loaded.error, "MECHANISM.MODEL.EMPTY");
35197
+ if (!loaded.assembly || !loaded.runResult) {
35198
+ return failedReport5(
35199
+ source,
35200
+ "Model must return an assembly(...) with joints.",
35201
+ "MECHANISM.NO_ASSEMBLY",
35202
+ 'Build the mechanism with assembly(...).addPart(...).connect(..., { type: "revolute" }).'
35203
+ );
35204
+ }
35205
+ const assembly2 = loaded.assembly;
35206
+ const def = assembly2.describe();
35207
+ if (def.joints.length === 0) {
35208
+ return failedReport5(source, "Assembly has no joints to analyze.", "MECHANISM.NO_JOINTS");
35209
+ }
35210
+ const simModel = collectSimulationModel(def, { state: options.jointOverrides });
35211
+ const solved = assembly2.solve(options.jointOverrides);
35212
+ const warnings = [];
35213
+ const worldTransform = (partName) => {
35214
+ try {
35215
+ return solved.getTransform(partName);
35216
+ } catch {
35217
+ return null;
35218
+ }
35219
+ };
35220
+ const links = [];
35221
+ for (const part of def.parts) {
35222
+ const shapes = [];
35223
+ flattenShapes(part.part, shapes);
35224
+ let volumeMm3 = 0;
35225
+ let localCom = [0, 0, 0];
35226
+ if (shapes.length > 0) {
35227
+ const mp = analyzeMassProperties(shapes.map((shape, i) => ({ name: `${part.name}#${i}`, mesh: shape.getMesh(), densityKgM3: 1 })));
35228
+ volumeMm3 = mp.totalVolumeMm3;
35229
+ localCom = mp.centerOfMassMm;
35230
+ }
35231
+ const simLink = simModel.links[part.name];
35232
+ const massKg = simLink?.massKg ?? (simLink?.densityKgM3 ?? options.densityKgM3) * volumeMm3 * 1e-9;
35233
+ const transform = worldTransform(part.name);
35234
+ const comWorldMm = transform ? transform.point(localCom) : localCom;
35235
+ links.push({ name: part.name, massKg, comWorldMm });
35236
+ }
35237
+ const jointView = new Map((loaded.runResult.jointsView?.joints ?? []).map((j) => [j.name, j]));
35238
+ const joints = [];
35239
+ for (const dj of def.joints) {
35240
+ if (dj.type === "fixed") {
35241
+ joints.push({ name: dj.name, type: "fixed", parent: dj.parent, child: dj.child, axisWorld: [0, 0, 1], pointWorldMm: [0, 0, 0] });
35242
+ continue;
35243
+ }
35244
+ const jv = jointView.get(dj.name);
35245
+ const parentTransform = worldTransform(dj.parent);
35246
+ if (!jv || !parentTransform) {
35247
+ warnings.push({
35248
+ level: "warning",
35249
+ code: "MECHANISM.JOINT.UNRESOLVED",
35250
+ path: dj.name,
35251
+ message: `Could not resolve a world frame for joint "${dj.name}"; excluded from torque analysis.`
35252
+ });
35253
+ continue;
35254
+ }
35255
+ joints.push({
35256
+ name: dj.name,
35257
+ type: dj.type,
35258
+ parent: dj.parent,
35259
+ child: dj.child,
35260
+ axisWorld: parentTransform.vector(jv.axis),
35261
+ pointWorldMm: parentTransform.point(jv.pivot),
35262
+ effort: simModel.joints[dj.name]?.effort ?? dj.effort
35263
+ });
35264
+ }
35265
+ const result = analyzeMechanism({ links, joints, couplings: def.jointCouplings?.length ?? 0 });
35266
+ const findings = [...warnings];
35267
+ const metrics = [
35268
+ makeMetric("actuatedDof", "Actuated DOF", result.actuatedDof, { unit: "" }),
35269
+ makeMetric("totalMassKg", "Total mass", round5(result.totalMassKg, 6), { unit: "kg" })
35270
+ ];
35271
+ if (result.hasClosedLoops) {
35272
+ findings.push({
35273
+ level: "info",
35274
+ code: "MECHANISM.CLOSED_LOOP",
35275
+ message: `Graph has closed loops; actuatedDof counts joint freedoms and the spatial Gr\xFCbler mobility is ${result.grueblerSpatial}.`
35276
+ });
35277
+ }
35278
+ for (const load of result.jointLoads) {
35279
+ if (load.type === "revolute") {
35280
+ metrics.push(makeMetric(`holdTorqueNm.${load.joint}`, `Hold torque (${load.joint})`, round5(load.holdTorqueNm ?? 0, 4), { unit: "N\xB7m" }));
35281
+ } else {
35282
+ metrics.push(makeMetric(`holdForceN.${load.joint}`, `Hold force (${load.joint})`, round5(load.holdForceN ?? 0, 4), { unit: "N" }));
35283
+ }
35284
+ if (load.marginPct !== null) {
35285
+ const metric = makeMetric(`torqueMarginPct.${load.joint}`, `Budget margin (${load.joint})`, round5(load.marginPct, 2), {
35286
+ unit: "%",
35287
+ target: { op: ">=", value: options.minTorqueMarginPct }
35288
+ });
35289
+ metrics.push(metric);
35290
+ if (load.marginPct < options.minTorqueMarginPct) {
35291
+ const required = load.type === "revolute" ? `${round5(load.holdTorqueNm ?? 0, 3)} N\xB7m` : `${round5(load.holdForceN ?? 0, 3)} N`;
35292
+ findings.push({
35293
+ level: "error",
35294
+ code: load.marginPct < 0 ? "MECHANISM.HOLD.OVER_BUDGET" : "MECHANISM.HOLD.LOW_MARGIN",
35295
+ path: load.joint,
35296
+ message: `Joint "${load.joint}" needs ${required} to hold its pose; budget margin ${round5(load.marginPct, 1)}% is below ${options.minTorqueMarginPct}%.`,
35297
+ suggest: "Increase the Sim.drive effort, shorten the moment arm, or lighten the distal mass."
35298
+ });
35299
+ }
35300
+ }
35301
+ }
35302
+ const assumptions = [
35303
+ "Static gravity hold at the current pose; no dynamics, friction, or contact.",
35304
+ "Per-link mass from Sim.body (massKg/density) when set, else uniform default density.",
35305
+ "Joint world axes/pivots taken from the solved assembly pose."
35306
+ ];
35307
+ return {
35308
+ schema: FEEDBACK_SCHEMA,
35309
+ category: "mechanism",
35310
+ source,
35311
+ ok: reportOk(metrics, findings),
35312
+ summary: `${result.actuatedDof} DOF \xB7 ${result.jointLoads.length} actuated joint(s) \xB7 total mass ${round5(result.totalMassKg, 3)} kg`,
35313
+ metrics,
35314
+ findings,
35315
+ trust: { assumptions, watertight: true },
35316
+ data: {
35317
+ linkCount: result.linkCount,
35318
+ jointCount: result.jointCount,
35319
+ actuatedJointCount: result.actuatedJointCount,
35320
+ fixedJointCount: result.fixedJointCount,
35321
+ couplings: result.couplings,
35322
+ hasClosedLoops: result.hasClosedLoops,
35323
+ actuatedDof: result.actuatedDof,
35324
+ grueblerSpatial: result.grueblerSpatial,
35325
+ gravity: result.gravity,
35326
+ totalMassKg: round5(result.totalMassKg, 6),
35327
+ jointLoads: result.jointLoads.map((l) => ({
35328
+ joint: l.joint,
35329
+ type: l.type,
35330
+ distalMassKg: round5(l.distalMassKg, 6),
35331
+ distalComWorldMm: l.distalComWorldMm.map((v) => round5(v, 4)),
35332
+ holdTorqueNm: l.holdTorqueNm === void 0 ? void 0 : round5(l.holdTorqueNm, 4),
35333
+ holdForceN: l.holdForceN === void 0 ? void 0 : round5(l.holdForceN, 4),
35334
+ budget: l.budget === void 0 ? void 0 : round5(l.budget, 4),
35335
+ marginPct: l.marginPct === null ? null : round5(l.marginPct, 2)
35336
+ }))
35337
+ },
35338
+ timeMs: Date.now() - start
35339
+ };
35340
+ }
35341
+
35342
+ // src/forge/analysis/statistics.ts
35343
+ function createRng2(seed) {
35344
+ let state = seed >>> 0;
35345
+ return () => {
35346
+ state = state + 1831565813 >>> 0;
35347
+ let t = state;
35348
+ t = Math.imul(t ^ t >>> 15, t | 1);
35349
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
35350
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
35351
+ };
35352
+ }
35353
+ function sampleNormal(rng, mean2, sigma) {
35354
+ const u1 = rng() || Number.MIN_VALUE;
35355
+ const u2 = rng();
35356
+ const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
35357
+ return mean2 + sigma * z;
35358
+ }
35359
+ function sampleUniform(rng, lo, hi) {
35360
+ return lo + (hi - lo) * rng();
35361
+ }
35362
+ function mean(values) {
35363
+ if (values.length === 0) return 0;
35364
+ return values.reduce((sum2, v) => sum2 + v, 0) / values.length;
35365
+ }
35366
+ function stdDev(values) {
35367
+ if (values.length < 2) return 0;
35368
+ const m = mean(values);
35369
+ const ss = values.reduce((sum2, v) => sum2 + (v - m) * (v - m), 0);
35370
+ return Math.sqrt(ss / (values.length - 1));
35371
+ }
35372
+ function capabilityIndices(meanValue, sigma, lsl, usl) {
35373
+ if (!(sigma > 0)) return { cp: null, cpk: null };
35374
+ const cp = lsl !== null && usl !== null ? (usl - lsl) / (6 * sigma) : null;
35375
+ const upper = usl !== null ? (usl - meanValue) / (3 * sigma) : Infinity;
35376
+ const lower2 = lsl !== null ? (meanValue - lsl) / (3 * sigma) : Infinity;
35377
+ const cpk = Math.min(upper, lower2);
35378
+ return { cp, cpk: Number.isFinite(cpk) ? cpk : null };
35379
+ }
35380
+ function dpmoFromYield(yieldFraction) {
35381
+ return Math.max(0, (1 - yieldFraction) * 1e6);
35382
+ }
35383
+
35384
+ // src/forge/analysis/tolerance.ts
35385
+ var ToleranceNonlinearError = class extends Error {
35386
+ constructor(response, input, ratio, tolerance) {
35387
+ super(
35388
+ `Response "${response}" is nonlinear in input "${input}" across its tolerance band (curvature ratio ${ratio.toFixed(3)} > ${tolerance}). The linearized method would report a wrong yield. Re-run with --method full-rebuild.`
35389
+ );
35390
+ this.response = response;
35391
+ this.input = input;
35392
+ this.ratio = ratio;
35393
+ this.tolerance = tolerance;
35394
+ this.name = "ToleranceNonlinearError";
35395
+ }
35396
+ };
35397
+ function readResponse(map, name) {
35398
+ const value = map[name];
35399
+ if (typeof value !== "number" || !Number.isFinite(value)) {
35400
+ throw new Error(`tolerance sample did not return a finite value for response "${name}"`);
35401
+ }
35402
+ return value;
35403
+ }
35404
+ function analyzeTolerance(sample, inputs, responses, options) {
35405
+ if (inputs.length === 0) throw new Error("tolerance analysis needs at least one varying input");
35406
+ if (responses.length === 0) throw new Error("tolerance analysis needs at least one measured response");
35407
+ const responseNames = responses.map((r) => r.name);
35408
+ const baseline = sample({});
35409
+ const nominalResp = {};
35410
+ for (const name of responseNames) nominalResp[name] = readResponse(baseline, name);
35411
+ const jac = {};
35412
+ for (const name of responseNames) jac[name] = {};
35413
+ for (const input of inputs) {
35414
+ const delta = input.halfWidth > 0 ? input.halfWidth : Math.max(1e-6, Math.abs(input.nominal) * 1e-3);
35415
+ const plus = sample({ [input.name]: input.nominal + delta });
35416
+ const minus = sample({ [input.name]: input.nominal - delta });
35417
+ for (const name of responseNames) {
35418
+ const fp = readResponse(plus, name);
35419
+ const fm = readResponse(minus, name);
35420
+ const f0 = nominalResp[name];
35421
+ jac[name][input.name] = (fp - fm) / (2 * delta);
35422
+ if (options.method === "linearized") {
35423
+ const curvature = Math.abs(fp - 2 * f0 + fm);
35424
+ const slopeSpan = Math.abs(fp - f0) + Math.abs(fm - f0);
35425
+ const ratio = slopeSpan > 1e-12 ? curvature / slopeSpan : curvature > 1e-9 ? Infinity : 0;
35426
+ if (ratio > options.nonlinearityTol) {
35427
+ throw new ToleranceNonlinearError(name, input.name, ratio, options.nonlinearityTol);
35428
+ }
35429
+ }
35430
+ }
35431
+ }
35432
+ const rng = createRng2(options.seed);
35433
+ const draws = {};
35434
+ for (const name of responseNames) draws[name] = [];
35435
+ let inSpecAll = 0;
35436
+ for (let s = 0; s < options.samples; s += 1) {
35437
+ const overrides = {};
35438
+ for (const input of inputs) {
35439
+ overrides[input.name] = input.dist === "uniform" ? sampleUniform(rng, input.nominal - input.halfWidth, input.nominal + input.halfWidth) : sampleNormal(rng, input.nominal, input.sigma);
35440
+ }
35441
+ let allIn = true;
35442
+ for (const response of responses) {
35443
+ let value;
35444
+ if (options.method === "full-rebuild") {
35445
+ value = readResponse(sample(overrides), response.name);
35446
+ } else {
35447
+ value = nominalResp[response.name];
35448
+ for (const input of inputs) value += jac[response.name][input.name] * (overrides[input.name] - input.nominal);
35449
+ }
35450
+ draws[response.name].push(value);
35451
+ if (response.lsl !== null && value < response.lsl) allIn = false;
35452
+ if (response.usl !== null && value > response.usl) allIn = false;
35453
+ }
35454
+ if (allIn) inSpecAll += 1;
35455
+ }
35456
+ const responseStats = responses.map((response) => reduceResponse(response, draws[response.name], nominalResp[response.name], inputs, jac[response.name], options));
35457
+ return {
35458
+ method: options.method,
35459
+ samples: options.samples,
35460
+ seed: options.seed,
35461
+ targetCpk: options.targetCpk,
35462
+ inputs,
35463
+ responses: responseStats,
35464
+ overallYieldPct: inSpecAll / options.samples * 100
35465
+ };
35466
+ }
35467
+ function reduceResponse(response, values, nominal, inputs, sensitivities, options) {
35468
+ const m = mean(values);
35469
+ const sigma = stdDev(values);
35470
+ const { cp, cpk } = capabilityIndices(m, sigma, response.lsl, response.usl);
35471
+ let inSpec = 0;
35472
+ for (const v of values) {
35473
+ if ((response.lsl === null || v >= response.lsl) && (response.usl === null || v <= response.usl)) inSpec += 1;
35474
+ }
35475
+ const yieldFraction = values.length > 0 ? inSpec / values.length : 1;
35476
+ let worstHalf = 0;
35477
+ let rssVariance = 0;
35478
+ const variancePerInput = inputs.map((input) => {
35479
+ const s = sensitivities[input.name];
35480
+ worstHalf += Math.abs(s) * input.halfWidth;
35481
+ rssVariance += s * input.halfWidth * (s * input.halfWidth);
35482
+ return { input: input.name, sensitivity: s, variance: s * input.sigma * (s * input.sigma) };
35483
+ });
35484
+ const totalVariance = variancePerInput.reduce((sum2, v) => sum2 + v.variance, 0);
35485
+ const contributions = variancePerInput.map((v) => ({ input: v.input, sensitivity: v.sensitivity, fraction: totalVariance > 0 ? v.variance / totalVariance : 0 })).sort((a, b) => b.fraction - a.fraction);
35486
+ const rssHalf = Math.sqrt(rssVariance);
35487
+ let allocation = null;
35488
+ if (cpk !== null && cpk < options.targetCpk && cpk > 0) {
35489
+ const scale2 = cpk / options.targetCpk;
35490
+ allocation = inputs.map((input) => ({
35491
+ input: input.name,
35492
+ currentHalfWidth: input.halfWidth,
35493
+ recommendedHalfWidth: input.halfWidth * scale2
35494
+ }));
35495
+ }
35496
+ return {
35497
+ name: response.name,
35498
+ nominal,
35499
+ mean: m,
35500
+ sigma,
35501
+ lsl: response.lsl,
35502
+ usl: response.usl,
35503
+ cp,
35504
+ cpk,
35505
+ pp: cp,
35506
+ ppk: cpk,
35507
+ yieldPct: yieldFraction * 100,
35508
+ dpmo: dpmoFromYield(yieldFraction),
35509
+ worstCaseMin: nominal - worstHalf,
35510
+ worstCaseMax: nominal + worstHalf,
35511
+ rssMin: nominal - rssHalf,
35512
+ rssMax: nominal + rssHalf,
35513
+ contributions,
35514
+ allocation
35515
+ };
35516
+ }
35517
+
35518
+ // cli/tolerance-inputs.ts
35519
+ function extractNumericActual(value) {
35520
+ if (value === void 0) return null;
35521
+ const match = value.match(/-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/);
35522
+ if (!match) return null;
35523
+ const n = Number(match[0]);
35524
+ return Number.isFinite(n) ? n : null;
35525
+ }
35526
+ function parseTolFlag(raw) {
35527
+ const eq = raw.indexOf("=");
35528
+ if (eq <= 0) throw new Error(`--tol expects Name=\xB1X or Name=lo..hi, got "${raw}"`);
35529
+ const name = raw.slice(0, eq).trim();
35530
+ const spec = raw.slice(eq + 1).trim();
35531
+ const range = spec.match(/^(-?[\d.eE+-]+)\.\.(-?[\d.eE+-]+)$/);
35532
+ if (range) {
35533
+ const lo = Number(range[1]);
35534
+ const hi = Number(range[2]);
35535
+ if (!Number.isFinite(lo) || !Number.isFinite(hi) || hi <= lo) throw new Error(`--tol ${name}: invalid range "${spec}"`);
35536
+ return { name, band: { lo, hi } };
35537
+ }
35538
+ const half = Number(spec.replace(/^[±+]/, ""));
35539
+ if (!Number.isFinite(half) || half <= 0) throw new Error(`--tol ${name}: expected \xB1X or lo..hi, got "${spec}"`);
35540
+ return { name, band: { halfWidth: half } };
35541
+ }
35542
+ function parseSpecFlag(raw) {
35543
+ const eq = raw.indexOf("=");
35544
+ if (eq <= 0) throw new Error(`--spec expects Name=lsl..usl, got "${raw}"`);
35545
+ const name = raw.slice(0, eq).trim();
35546
+ const m = raw.slice(eq + 1).trim().match(/^(\*|-?[\d.eE+-]+)\.\.(\*|-?[\d.eE+-]+)$/);
35547
+ if (!m) throw new Error(`--spec ${name}: expected lsl..usl (use * for an open bound), got "${raw.slice(eq + 1)}"`);
35548
+ const lsl = m[1] === "*" ? null : Number(m[1]);
35549
+ const usl = m[2] === "*" ? null : Number(m[2]);
35550
+ if (lsl !== null && !Number.isFinite(lsl) || usl !== null && !Number.isFinite(usl)) throw new Error(`--spec ${name}: non-finite bound`);
35551
+ if (lsl === null && usl === null) throw new Error(`--spec ${name}: at least one bound must be a number`);
35552
+ return { name, lsl, usl };
35553
+ }
35554
+ function parseDistFlag(raw) {
35555
+ const eq = raw.indexOf("=");
35556
+ if (eq <= 0) throw new Error(`--dist expects Name=normal|uniform, got "${raw}"`);
35557
+ const name = raw.slice(0, eq).trim();
35558
+ const dist3 = raw.slice(eq + 1).trim();
35559
+ if (dist3 !== "normal" && dist3 !== "uniform") throw new Error(`--dist ${name}: expected normal or uniform`);
35560
+ return { name, dist: dist3 };
35561
+ }
35562
+ function resolveToleranceInputs(params, options) {
35563
+ const inputs = [];
35564
+ for (const param of params) {
35565
+ if (options.pinned.has(param.name)) continue;
35566
+ if (param.integer || param.boolean || param.choices) continue;
35567
+ const override = options.tolOverrides.get(param.name);
35568
+ let nominal = param.defaultValue;
35569
+ let halfWidth;
35570
+ if (override?.lo !== void 0 && override?.hi !== void 0) {
35571
+ nominal = (override.lo + override.hi) / 2;
35572
+ halfWidth = (override.hi - override.lo) / 2;
35573
+ } else if (override?.halfWidth !== void 0) {
35574
+ halfWidth = override.halfWidth;
35575
+ } else {
35576
+ halfWidth = (param.max - param.min) / 2;
35577
+ }
35578
+ if (!(halfWidth > 0)) {
35579
+ if (override) throw new Error(`tolerance input "${param.name}" has a non-positive band`);
35580
+ continue;
35581
+ }
35582
+ const dist3 = options.distOverrides.get(param.name) ?? "normal";
35583
+ const sigma = dist3 === "uniform" ? halfWidth / Math.sqrt(3) : halfWidth / options.sigmaLevel;
35584
+ inputs.push({ name: param.name, nominal, halfWidth, sigma, dist: dist3, unit: param.unit });
35585
+ }
35586
+ return inputs;
35587
+ }
35588
+ var RANGE_RE = /^\[\s*(-?[\d.eE+-]+)\s*,\s*(-?[\d.eE+-]+)\s*\]$/;
35589
+ var GT_RE = /^>\s*(-?[\d.eE+-]+)$/;
35590
+ var LT_RE = /^<\s*(-?[\d.eE+-]+)$/;
35591
+ function limitsFromExpected(expected) {
35592
+ if (!expected) return { lsl: null, usl: null };
35593
+ const range = expected.match(RANGE_RE);
35594
+ if (range) return { lsl: Number(range[1]), usl: Number(range[2]) };
35595
+ const gt = expected.match(GT_RE);
35596
+ if (gt) return { lsl: Number(gt[1]), usl: null };
35597
+ const lt = expected.match(LT_RE);
35598
+ if (lt) return { lsl: null, usl: Number(lt[1]) };
35599
+ return { lsl: null, usl: null };
35600
+ }
35601
+ function resolveToleranceResponses(verifications, specOverrides) {
35602
+ const byLabel = /* @__PURE__ */ new Map();
35603
+ for (const v of verifications) {
35604
+ if (extractNumericActual(v.actual) !== null) byLabel.set(v.label, v);
35605
+ }
35606
+ for (const name of specOverrides.keys()) {
35607
+ if (!byLabel.has(name)) {
35608
+ throw new Error(`--spec names response "${name}", but the model has no numeric verify.* result with that label`);
35609
+ }
35610
+ }
35611
+ const responses = [];
35612
+ const unbounded = [];
35613
+ for (const [label, v] of byLabel) {
35614
+ const override = specOverrides.get(label);
35615
+ const limits = override ?? limitsFromExpected(v.expected);
35616
+ if (limits.lsl === null && limits.usl === null) unbounded.push(label);
35617
+ responses.push({ name: label, lsl: limits.lsl, usl: limits.usl });
35618
+ }
35619
+ return { responses, unbounded };
35620
+ }
35621
+
35622
+ // cli/sim-tolerance.ts
35623
+ function parseArgs24(argv) {
35624
+ const inputPaths = [];
35625
+ const paramOverrides = {};
35626
+ const tolOverrides = /* @__PURE__ */ new Map();
35627
+ const distOverrides = /* @__PURE__ */ new Map();
35628
+ const specOverrides = /* @__PURE__ */ new Map();
35629
+ let json = false;
35630
+ let compact = false;
35631
+ let method = "linearized";
35632
+ let samples = null;
35633
+ let seed = 12345;
35634
+ let sigmaLevel = 3;
35635
+ let targetCpk = 1.33;
35636
+ let nonlinearityTol = 0.05;
35637
+ let noFailExit = false;
35638
+ let backend;
35639
+ let quality;
35640
+ const numberFlag = (value, label, predicate) => {
35641
+ const n = Number(value);
35642
+ if (!Number.isFinite(n) || !predicate(n)) throw new Error(`${label} requires a valid number`);
35643
+ return n;
35644
+ };
35645
+ for (let i = 0; i < argv.length; i += 1) {
35646
+ const arg = argv[i];
35647
+ if (arg === "--json") {
35648
+ json = true;
35649
+ continue;
35650
+ }
35651
+ if (arg === "--compact") {
35652
+ compact = true;
35653
+ continue;
35654
+ }
35655
+ if (arg === "--no-fail-exit") {
35656
+ noFailExit = true;
35657
+ continue;
35658
+ }
35659
+ if (arg === "--method") {
35660
+ const v = argv[i + 1];
35661
+ if (v !== "linearized" && v !== "full-rebuild") throw new Error("--method must be linearized or full-rebuild");
35662
+ method = v;
35663
+ i += 1;
35664
+ continue;
35665
+ }
35666
+ if (arg === "--samples") {
35667
+ samples = numberFlag(argv[++i], "--samples", (n) => n >= 1);
35668
+ continue;
35669
+ }
35670
+ if (arg === "--seed") {
35671
+ seed = numberFlag(argv[++i], "--seed", () => true) >>> 0;
35672
+ continue;
35673
+ }
35674
+ if (arg === "--sigma") {
35675
+ sigmaLevel = numberFlag(argv[++i], "--sigma", (n) => n > 0);
35676
+ continue;
35677
+ }
35678
+ if (arg === "--target-cpk") {
35679
+ targetCpk = numberFlag(argv[++i], "--target-cpk", (n) => n > 0);
35680
+ continue;
35681
+ }
35682
+ if (arg === "--nonlinearity-tol") {
35683
+ nonlinearityTol = numberFlag(argv[++i], "--nonlinearity-tol", (n) => n > 0);
35684
+ continue;
35685
+ }
35686
+ if (arg === "--tol") {
35687
+ const { name, band } = parseTolFlag(argv[++i] ?? "");
35688
+ tolOverrides.set(name, band);
35689
+ continue;
35690
+ }
35691
+ if (arg === "--dist") {
35692
+ const { name, dist: dist3 } = parseDistFlag(argv[++i] ?? "");
35693
+ distOverrides.set(name, dist3);
35694
+ continue;
35695
+ }
35696
+ if (arg === "--spec") {
35697
+ const { name, lsl, usl } = parseSpecFlag(argv[++i] ?? "");
35698
+ specOverrides.set(name, { lsl, usl });
35699
+ continue;
35700
+ }
35701
+ if (arg === "--quality" || arg === "-q") {
35702
+ const v = argv[i + 1];
35703
+ if (v !== "default" && v !== "live" && v !== "high") throw new Error("--quality must be default, live, or high");
35704
+ quality = v;
35705
+ i += 1;
35706
+ continue;
35707
+ }
35708
+ if (arg === "--backend") {
35709
+ const v = argv[i + 1];
35710
+ if (v !== "manifold" && v !== "occt" && v !== "truck" && v !== "sdf") throw new Error("--backend must be manifold, occt, truck, or sdf");
35711
+ backend = v;
35712
+ i += 1;
35713
+ continue;
35714
+ }
35715
+ if (arg === "--param" || arg === "-p") {
35716
+ const value = argv[i + 1];
35717
+ if (!value || value.startsWith("--")) throw new Error(`${arg} requires Key=Value`);
35718
+ addCliParamOverride(value, paramOverrides);
35719
+ i += 1;
35720
+ continue;
35721
+ }
35722
+ if (arg.startsWith("--")) throw new Error(`Unknown flag: ${arg}`);
35723
+ inputPaths.push(arg);
35724
+ }
35725
+ requireInputPaths(inputPaths, "Usage: forgecad sim tolerance <model.forge.js> [--tol Param=\xB1X] [--spec Resp=lo..hi] [--json]");
35726
+ requireScriptInputPaths(inputPaths);
35727
+ return {
35728
+ inputPaths,
35729
+ json,
35730
+ compact,
35731
+ method,
35732
+ samples,
35733
+ seed,
35734
+ sigmaLevel,
35735
+ targetCpk,
35736
+ nonlinearityTol,
35737
+ noFailExit,
35738
+ tolOverrides,
35739
+ distOverrides,
35740
+ specOverrides,
35741
+ backend,
35742
+ quality,
35743
+ paramOverrides
35744
+ };
35745
+ }
35746
+ function failedReport6(source, message, code = "TOLERANCE.ERROR", suggest) {
35747
+ return {
35748
+ schema: FEEDBACK_SCHEMA,
35749
+ category: "tolerance",
35750
+ source,
35751
+ ok: false,
35752
+ summary: "tolerance analysis failed",
35753
+ metrics: [],
35754
+ findings: [{ level: "error", code, message, ...suggest ? { suggest } : {} }],
35755
+ trust: { assumptions: [] },
35756
+ data: {},
35757
+ timeMs: 0
35758
+ };
35759
+ }
35760
+ function round6(value, digits = 6) {
35761
+ const factor = 10 ** digits;
35762
+ return Math.round(value * factor) / factor;
35763
+ }
35764
+ async function analyze4(scriptPath, options) {
35765
+ const start = Date.now();
35766
+ const input = loadCliScriptInput(scriptPath);
35767
+ await initCliBackend(resolveCliBackend(options.backend, input) ?? CLI_DEFAULT_BACKEND);
35768
+ const qualityPreset = options.quality && options.quality !== "default" ? options.quality : void 0;
35769
+ const buildResponses = (overrides) => {
35770
+ setParamOverrides({ ...options.paramOverrides, ...overrides });
35771
+ const rr = runScript(input.code, input.fileName, input.allFiles, {
35772
+ ...qualityPreset ? { quality: qualityPreset } : {},
35773
+ readBinaryFile: input.readBinaryFile
35774
+ });
35775
+ if (rr.error) throw new Error(`model run failed: ${rr.error}`);
35776
+ const map = {};
35777
+ for (const v of rr.verifications) {
35778
+ const n = extractNumericActual(v.actual);
35779
+ if (n !== null) map[v.label] = n;
35780
+ }
35781
+ return map;
35782
+ };
35783
+ setParamOverrides({ ...options.paramOverrides });
35784
+ const nominal = runScript(input.code, input.fileName, input.allFiles, {
35785
+ ...qualityPreset ? { quality: qualityPreset } : {},
35786
+ readBinaryFile: input.readBinaryFile
35787
+ });
35788
+ if (nominal.error) return failedReport6(scriptPath, nominal.error, "TOLERANCE.MODEL.ERROR");
35789
+ const inputs = resolveToleranceInputs(nominal.params, {
35790
+ pinned: new Set(Object.keys(options.paramOverrides)),
35791
+ sigmaLevel: options.sigmaLevel,
35792
+ tolOverrides: options.tolOverrides,
35793
+ distOverrides: options.distOverrides
35794
+ });
35795
+ if (inputs.length === 0) {
35796
+ return failedReport6(
35797
+ scriptPath,
35798
+ "No continuous parameters to vary.",
35799
+ "TOLERANCE.NO_INPUTS",
35800
+ "Add a number param with a min/max range, or pass --tol Param=\xB1X."
35801
+ );
35802
+ }
35803
+ const { responses, unbounded } = resolveToleranceResponses(nominal.verifications, options.specOverrides);
35804
+ if (responses.length === 0) {
35805
+ return failedReport6(
35806
+ scriptPath,
35807
+ "No numeric verify.* responses to measure.",
35808
+ "TOLERANCE.NO_RESPONSES",
35809
+ 'Add a verify.inRange("Name", measured, lo, hi) call (or similar) to the model.'
35810
+ );
35811
+ }
35812
+ const a = buildResponses({});
35813
+ const b = buildResponses({});
35814
+ for (const r of responses) {
35815
+ if (Math.abs((a[r.name] ?? NaN) - (b[r.name] ?? NaN)) > 1e-9) {
35816
+ return failedReport6(
35817
+ scriptPath,
35818
+ `Response "${r.name}" changed between identical builds; the model is non-deterministic.`,
35819
+ "TOLERANCE.MODEL.NONDETERMINISTIC",
35820
+ "Remove randomness (e.g. Math.random) from the model before tolerance analysis."
35821
+ );
35822
+ }
35823
+ }
35824
+ const samples = options.samples ?? (options.method === "full-rebuild" ? 2e3 : 1e5);
35825
+ let result;
35826
+ try {
35827
+ result = analyzeTolerance(buildResponses, inputs, responses, {
35828
+ method: options.method,
35829
+ samples,
35830
+ seed: options.seed,
35831
+ nonlinearityTol: options.nonlinearityTol,
35832
+ targetCpk: options.targetCpk
35833
+ });
35834
+ } catch (error) {
35835
+ if (error instanceof ToleranceNonlinearError) {
35836
+ return failedReport6(scriptPath, error.message, "TOLERANCE.NONLINEAR", "Re-run with --method full-rebuild.");
35837
+ }
35838
+ throw error;
35839
+ }
35840
+ const findings = [];
35841
+ for (const name of unbounded) {
35842
+ findings.push({
35843
+ level: "warning",
35844
+ code: "TOLERANCE.SPEC.UNBOUNDED",
35845
+ path: name,
35846
+ message: `Response "${name}" has no spec limits; reporting variation only.`,
35847
+ suggest: `Bound it with verify.inRange in the model, or pass --spec ${name}=lo..hi`
35848
+ });
35849
+ }
35850
+ for (const r of result.responses) {
35851
+ if (r.cpk !== null && r.cpk < options.targetCpk) {
35852
+ const top = r.contributions[0];
35853
+ const rec = r.allocation?.find((alloc) => alloc.input === top?.input);
35854
+ findings.push({
35855
+ level: "error",
35856
+ code: "TOLERANCE.CPK.LOW",
35857
+ path: r.name,
35858
+ message: `Response "${r.name}" Cpk ${round6(r.cpk, 3)} is below target ${options.targetCpk} (yield ${round6(r.yieldPct, 2)}%).`,
35859
+ suggest: top ? `Tighten "${top.input}" (${round6(top.fraction * 100, 0)}% of the variation)${rec ? `, e.g. \xB1${round6(rec.recommendedHalfWidth, 4)}` : ""}.` : "Tighten the dominant input tolerance."
35860
+ });
35861
+ }
35862
+ }
35863
+ const metrics = [makeMetric("overallYieldPct", "Overall yield", round6(result.overallYieldPct, 4), { unit: "%" })];
35864
+ for (const r of result.responses) {
35865
+ if (r.cpk !== null) {
35866
+ metrics.push(makeMetric(`cpk.${r.name}`, `Cpk (${r.name})`, round6(r.cpk, 4), { unit: "", target: { op: ">=", value: options.targetCpk } }));
35867
+ }
35868
+ metrics.push(makeMetric(`yieldPct.${r.name}`, `Yield (${r.name})`, round6(r.yieldPct, 4), { unit: "%" }));
35869
+ }
35870
+ const assumptions = [
35871
+ `${result.method === "linearized" ? "Linearized surrogate" : "Full model rebuild"} Monte Carlo, ${samples} samples, seed ${result.seed}.`,
35872
+ "Normal input distributions unless --dist sets uniform; default band = (max-min)/2 of each param, read as \xB1" + options.sigmaLevel + "\u03C3.",
35873
+ "Single population: Cp/Cpk equal Pp/Ppk (no subgroup short-vs-long-term split)."
35874
+ ];
35875
+ return {
35876
+ schema: FEEDBACK_SCHEMA,
35877
+ category: "tolerance",
35878
+ source: scriptPath,
35879
+ ok: reportOk(metrics, findings),
35880
+ summary: `${result.responses.length} response(s) \xB7 overall yield ${round6(result.overallYieldPct, 2)}% \xB7 ${result.responses.filter((r) => r.cpk !== null && r.cpk >= options.targetCpk).length}/${result.responses.filter((r) => r.cpk !== null).length} meet Cpk ${options.targetCpk}`,
35881
+ metrics,
35882
+ findings,
35883
+ trust: { assumptions, watertight: true },
35884
+ data: {
35885
+ method: result.method,
35886
+ samples: result.samples,
35887
+ seed: result.seed,
35888
+ targetCpk: result.targetCpk,
35889
+ overallYieldPct: round6(result.overallYieldPct, 4),
35890
+ inputs: result.inputs.map((i) => ({ name: i.name, nominal: round6(i.nominal, 6), halfWidth: round6(i.halfWidth, 6), sigma: round6(i.sigma, 6), dist: i.dist, unit: i.unit })),
35891
+ responses: result.responses.map((r) => ({
35892
+ name: r.name,
35893
+ nominal: round6(r.nominal, 6),
35894
+ mean: round6(r.mean, 6),
35895
+ sigma: round6(r.sigma, 6),
35896
+ lsl: r.lsl,
35897
+ usl: r.usl,
35898
+ cp: r.cp === null ? null : round6(r.cp, 4),
35899
+ cpk: r.cpk === null ? null : round6(r.cpk, 4),
35900
+ yieldPct: round6(r.yieldPct, 4),
35901
+ dpmo: round6(r.dpmo, 1),
35902
+ worstCase: [round6(r.worstCaseMin, 6), round6(r.worstCaseMax, 6)],
35903
+ rss: [round6(r.rssMin, 6), round6(r.rssMax, 6)],
35904
+ contributions: r.contributions.map((c) => ({ input: c.input, percent: round6(c.fraction * 100, 2), sensitivity: round6(c.sensitivity, 6) })),
35905
+ allocation: r.allocation?.map((alloc) => ({ input: alloc.input, currentHalfWidth: round6(alloc.currentHalfWidth, 6), recommendedHalfWidth: round6(alloc.recommendedHalfWidth, 6) })) ?? null
35906
+ }))
35907
+ },
35908
+ timeMs: Date.now() - start
35909
+ };
35910
+ }
35911
+ async function runSimToleranceCli(argv = process.argv.slice(2)) {
35912
+ const options = parseArgs24(argv);
35913
+ requireExistingInputPaths(options.inputPaths);
35914
+ await init();
35915
+ const reports = [];
35916
+ for (const [index, scriptPath] of options.inputPaths.entries()) {
35917
+ if (!options.json) printBatchHeader(scriptPath, index, options.inputPaths.length);
35918
+ let report;
35919
+ try {
35920
+ report = await analyze4(scriptPath, options);
35921
+ } catch (error) {
35922
+ report = failedReport6(scriptPath, error instanceof Error ? error.message : String(error));
35923
+ }
35924
+ reports.push(report);
35925
+ }
35926
+ printFeedback(reports.length === 1 ? reports[0] : { runs: reports }, options.json, options.compact);
35927
+ if (reports.some((report) => !report.ok) && !options.noFailExit) process.exitCode = 1;
35928
+ }
35929
+
33690
35930
  // cli/test-run.ts
33691
- import { resolve as resolve42 } from "path";
35931
+ import { resolve as resolve44 } from "path";
33692
35932
 
33693
35933
  // cli/solver-debug-artifacts.ts
33694
35934
  import { mkdir as mkdir5, writeFile as writeFile10 } from "fs/promises";
33695
- import { basename as basename16, join as join15, resolve as resolve41 } from "path";
35935
+ import { basename as basename16, join as join16, resolve as resolve43 } from "path";
33696
35936
 
33697
35937
  // cli/solver-debug-html.ts
33698
35938
  function escapeHtml(s) {
@@ -34393,7 +36633,7 @@ function appendResiduals(lines, step) {
34393
36633
  }
34394
36634
  }
34395
36635
  async function writeSolverDebugArtifacts(outputRoot, scriptPath, objects) {
34396
- const root = resolve41(outputRoot);
36636
+ const root = resolve43(outputRoot);
34397
36637
  await mkdir5(root, { recursive: true });
34398
36638
  const bundles = [];
34399
36639
  const usedNames = /* @__PURE__ */ new Map();
@@ -34407,14 +36647,14 @@ async function writeSolverDebugArtifacts(outputRoot, scriptPath, objects) {
34407
36647
  const seen = usedNames.get(baseName) ?? 0;
34408
36648
  usedNames.set(baseName, seen + 1);
34409
36649
  const dirName = seen === 0 ? baseName : `${baseName}-${seen + 1}`;
34410
- const dir = join15(root, dirName);
34411
- const svgDir = join15(dir, "svg");
36650
+ const dir = join16(root, dirName);
36651
+ const svgDir = join16(dir, "svg");
34412
36652
  await mkdir5(svgDir, { recursive: true });
34413
- const transcriptPath = join15(dir, "constructive-transcript.md");
36653
+ const transcriptPath = join16(dir, "constructive-transcript.md");
34414
36654
  await writeFile10(transcriptPath, buildConstructiveTranscriptMarkdown(object.name || dirName, meta), "utf8");
34415
36655
  const snapshotPaths = [];
34416
36656
  for (const snapshot of debug.svgSnapshots) {
34417
- const snapshotPath = join15(svgDir, `${snapshot.label}.svg`);
36657
+ const snapshotPath = join16(svgDir, `${snapshot.label}.svg`);
34418
36658
  await writeFile10(snapshotPath, snapshot.svg, "utf8");
34419
36659
  snapshotPaths.push(snapshotPath);
34420
36660
  }
@@ -34439,7 +36679,7 @@ async function writeSolverDebugArtifacts(outputRoot, scriptPath, objects) {
34439
36679
  for (const [index, html] of htmlMap) {
34440
36680
  const bundle = bundles[index] ?? bundles[bundles.length - 1];
34441
36681
  if (bundle) {
34442
- const htmlPath = join15(bundle.dir, "dashboard.html");
36682
+ const htmlPath = join16(bundle.dir, "dashboard.html");
34443
36683
  await writeFile10(htmlPath, html, "utf8");
34444
36684
  }
34445
36685
  }
@@ -34447,7 +36687,7 @@ async function writeSolverDebugArtifacts(outputRoot, scriptPath, objects) {
34447
36687
  return bundles;
34448
36688
  }
34449
36689
  async function writeSolverDebugIndex(root, scriptPath, bundles) {
34450
- const lines = ["# Solver debug artifacts", "", `script: ${resolve41(scriptPath)}`, `artifacts: ${bundles.length}`, ""];
36690
+ const lines = ["# Solver debug artifacts", "", `script: ${resolve43(scriptPath)}`, `artifacts: ${bundles.length}`, ""];
34451
36691
  if (bundles.length === 0) {
34452
36692
  lines.push("No solver debug artifacts were captured.");
34453
36693
  } else {
@@ -34458,7 +36698,7 @@ async function writeSolverDebugIndex(root, scriptPath, bundles) {
34458
36698
  lines.push(` svg_snapshots: ${bundle.snapshotPaths.length}`);
34459
36699
  }
34460
36700
  }
34461
- await writeFile10(join15(root, "README.md"), lines.join("\n") + "\n", "utf8");
36701
+ await writeFile10(join16(root, "README.md"), lines.join("\n") + "\n", "utf8");
34462
36702
  }
34463
36703
 
34464
36704
  // cli/test-run.ts
@@ -35062,7 +37302,7 @@ async function runScriptCli(argv = process.argv.slice(2)) {
35062
37302
  if (!printSolverProfile && solverDebugOut) {
35063
37303
  const bundles = await writeSolverDebugArtifacts(solverDebugOut, scriptPath, result.objects);
35064
37304
  console.log(`
35065
- \u2713 Solver debug artifacts: ${bundles.length} sketch(es) \u2192 ${resolve42(solverDebugOut)}`);
37305
+ \u2713 Solver debug artifacts: ${bundles.length} sketch(es) \u2192 ${resolve44(solverDebugOut)}`);
35066
37306
  for (const bundle of bundles) {
35067
37307
  console.log(` ${bundle.name}: transcript=${bundle.transcriptPath} svg=${bundle.snapshotPaths.length}`);
35068
37308
  }
@@ -35191,7 +37431,7 @@ async function runScriptCli(argv = process.argv.slice(2)) {
35191
37431
  if (solverDebugOut) {
35192
37432
  const bundles = await writeSolverDebugArtifacts(solverDebugOut, scriptPath, result.objects);
35193
37433
  console.log(`
35194
- \u2713 Solver debug artifacts: ${bundles.length} sketch(es) \u2192 ${resolve42(solverDebugOut)}`);
37434
+ \u2713 Solver debug artifacts: ${bundles.length} sketch(es) \u2192 ${resolve44(solverDebugOut)}`);
35195
37435
  for (const bundle of bundles) {
35196
37436
  console.log(` ${bundle.name}: transcript=${bundle.transcriptPath} svg=${bundle.snapshotPaths.length}`);
35197
37437
  }
@@ -35217,7 +37457,7 @@ async function runScriptCli(argv = process.argv.slice(2)) {
35217
37457
 
35218
37458
  // cli/forge-render-section.ts
35219
37459
  import { writeFile as writeFile11 } from "fs/promises";
35220
- import { basename as basename17, extname as extname13, resolve as resolve43 } from "path";
37460
+ import { basename as basename17, extname as extname14, resolve as resolve45 } from "path";
35221
37461
  function usage26() {
35222
37462
  console.error(
35223
37463
  "Usage: forgecad render section <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [input ...] [--output path] [--format svg|png] [--param Key=Value] [--joint JointName=Value] [--plane XY|XZ|YZ] [--offset <number>] [--size <px>] [--edges <off|thin|bold>] [--chrome-path <path>] [--background <color>]"
@@ -35309,7 +37549,7 @@ function parseRenderSectionArgs(argv) {
35309
37549
  async function runRenderSectionCli(argv = process.argv.slice(2)) {
35310
37550
  const options = parseRenderSectionArgs(argv);
35311
37551
  requireExistingInputPaths(options.inputPaths);
35312
- const needsPng = options.format === "png" || (options.outputPath ? extname13(options.outputPath).toLowerCase() === ".png" : false);
37552
+ const needsPng = options.format === "png" || (options.outputPath ? extname14(options.outputPath).toLowerCase() === ".png" : false);
35313
37553
  let resolvedChromePath;
35314
37554
  if (needsPng) {
35315
37555
  resolvedChromePath = findChromePath(options.chromePath) ?? void 0;
@@ -35333,9 +37573,9 @@ async function runRenderSectionCli(argv = process.argv.slice(2)) {
35333
37573
  }
35334
37574
  }
35335
37575
  async function renderSectionInput(options, scriptPath, resolvedChromePath) {
35336
- const outputFormat = options.format ?? (options.outputPath && extname13(options.outputPath).toLowerCase() === ".png" ? "png" : "svg");
37576
+ const outputFormat = options.format ?? (options.outputPath && extname14(options.outputPath).toLowerCase() === ".png" ? "png" : "svg");
35337
37577
  const outputPath = batchOutputPath(scriptPath, options.outputPath, (inputPath) => defaultOutput(inputPath, outputFormat));
35338
- if (resolve43(outputPath) === resolve43(scriptPath)) {
37578
+ if (resolve45(outputPath) === resolve45(scriptPath)) {
35339
37579
  throw new Error(`ERROR: output path would overwrite the input script. Specify an explicit output path.`);
35340
37580
  }
35341
37581
  const input = loadCliScriptInput(scriptPath);
@@ -35374,9 +37614,9 @@ async function renderSectionInput(options, scriptPath, resolvedChromePath) {
35374
37614
  background: options.background,
35375
37615
  chromePath: resolvedChromePath
35376
37616
  });
35377
- await writeFile11(resolve43(outputPath), png);
37617
+ await writeFile11(resolve45(outputPath), png);
35378
37618
  } else {
35379
- await writeFile11(resolve43(outputPath), svgDocument.svg);
37619
+ await writeFile11(resolve45(outputPath), svgDocument.svg);
35380
37620
  }
35381
37621
  const sz = [svgDocument.width.toFixed(1), svgDocument.height.toFixed(1)];
35382
37622
  const totalArea = sectionSketches.reduce((sum2, s) => sum2 + s.sketch.area(), 0);
@@ -35387,20 +37627,20 @@ async function renderSectionInput(options, scriptPath, resolvedChromePath) {
35387
37627
 
35388
37628
  // cli/update-check.ts
35389
37629
  import { spawn as spawn4 } from "child_process";
35390
- import { existsSync as existsSync21, mkdirSync as mkdirSync13, readFileSync as readFileSync25, writeFileSync as writeFileSync20 } from "fs";
37630
+ import { existsSync as existsSync22, mkdirSync as mkdirSync14, readFileSync as readFileSync27, writeFileSync as writeFileSync21 } from "fs";
35391
37631
  import { homedir as homedir9 } from "os";
35392
- import { dirname as dirname13, join as join16 } from "path";
37632
+ import { dirname as dirname14, join as join17 } from "path";
35393
37633
  var PACKAGE_NAME = "forgecad";
35394
37634
  var REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
35395
37635
  var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
35396
37636
  var FETCH_TIMEOUT_MS = 3e3;
35397
37637
  var WORKER_FLAG = "--__update-check-worker";
35398
37638
  function cachePath() {
35399
- return join16(homedir9(), ".forgecad", "update-check.json");
37639
+ return join17(homedir9(), ".forgecad", "update-check.json");
35400
37640
  }
35401
37641
  function readCache() {
35402
37642
  try {
35403
- const text = readFileSync25(cachePath(), "utf-8");
37643
+ const text = readFileSync27(cachePath(), "utf-8");
35404
37644
  const parsed = JSON.parse(text);
35405
37645
  if (typeof parsed.checkedAt !== "number" || typeof parsed.latest !== "string") return null;
35406
37646
  return parsed;
@@ -35410,9 +37650,9 @@ function readCache() {
35410
37650
  }
35411
37651
  function writeCache(cache) {
35412
37652
  const p = cachePath();
35413
- const dir = dirname13(p);
35414
- if (!existsSync21(dir)) mkdirSync13(dir, { recursive: true });
35415
- writeFileSync20(p, JSON.stringify(cache), "utf-8");
37653
+ const dir = dirname14(p);
37654
+ if (!existsSync22(dir)) mkdirSync14(dir, { recursive: true });
37655
+ writeFileSync21(p, JSON.stringify(cache), "utf-8");
35416
37656
  }
35417
37657
  function shouldSkip() {
35418
37658
  if (process.env.FORGE_NO_UPDATE_CHECK) return true;
@@ -35633,7 +37873,9 @@ var PARAM_OPTIONS = [
35633
37873
  repeatable: true
35634
37874
  }
35635
37875
  ];
35636
- var JOINT_OPTIONS = PARAM_OPTIONS.filter((option) => option.name === "--joint");
37876
+ var PARAM_OVERRIDE_OPTIONS = PARAM_OPTIONS.filter(
37877
+ (option) => option.name === "--param" || option.name === "-p"
37878
+ );
35637
37879
  var RENDER_OPTIONS = [
35638
37880
  ...PARAM_OPTIONS,
35639
37881
  {
@@ -36510,7 +38752,7 @@ var HISTORY_INSPECT_COMMANDS = [
36510
38752
  { name: "--params", description: "Print resolved parameter values after the history" },
36511
38753
  { name: "--source", description: "Show source file and line for each printed feature step" },
36512
38754
  { name: "--trace-ids", description: "Show trace ids for feature steps and final objects" },
36513
- ...PARAM_OPTIONS,
38755
+ ...PARAM_OVERRIDE_OPTIONS,
36514
38756
  {
36515
38757
  name: "--backend",
36516
38758
  description: "Geometry backend (default: manifold; STEP inputs default to OCCT)",
@@ -36555,7 +38797,7 @@ var HISTORY_INSPECT_COMMANDS = [
36555
38797
  },
36556
38798
  { name: "--anchor", description: "Print one agent-ready feedback anchor when the query matches exactly one trace node" },
36557
38799
  { name: "--full", description: "Print all matching trace nodes" },
36558
- ...PARAM_OPTIONS,
38800
+ ...PARAM_OVERRIDE_OPTIONS,
36559
38801
  {
36560
38802
  name: "--backend",
36561
38803
  description: "Geometry backend (default: manifold; STEP inputs default to OCCT)",
@@ -36916,7 +39158,7 @@ var commands = [
36916
39158
  valueLabel: "<live|default|high>",
36917
39159
  values: QUALITY_VALUES
36918
39160
  },
36919
- ...PARAM_OPTIONS
39161
+ ...PARAM_OVERRIDE_OPTIONS
36920
39162
  ],
36921
39163
  positionals: [{ description: "Forge script or CAD asset", valueKind: "renderable", repeatable: true }]
36922
39164
  },
@@ -37006,7 +39248,7 @@ var commands = [
37006
39248
  completion: {
37007
39249
  options: [
37008
39250
  { name: "--json", description: "Print machine-readable JSON" },
37009
- ...PARAM_OPTIONS,
39251
+ ...PARAM_OVERRIDE_OPTIONS,
37010
39252
  {
37011
39253
  name: "--quality",
37012
39254
  description: "Mesh quality preset",
@@ -37052,7 +39294,7 @@ var commands = [
37052
39294
  valueLabel: "<name>",
37053
39295
  values: HQ_PRESET_VALUES
37054
39296
  },
37055
- ...PARAM_OPTIONS,
39297
+ ...PARAM_OVERRIDE_OPTIONS,
37056
39298
  {
37057
39299
  name: "--edges",
37058
39300
  description: "Edge overlay preset (default: off)",
@@ -37124,7 +39366,7 @@ var commands = [
37124
39366
  path: ["render", "sketch"],
37125
39367
  summary: "Render a 2D sketch .forge.js to PNG.",
37126
39368
  usage: [
37127
- "forgecad render sketch <script.forge.js> [input ...] [--output path] [--size <px>] [--background <color>] [--chrome-path <path>]"
39369
+ "forgecad render sketch <script.forge.js> [input ...] [--output path] [--size <px>] [--background <color>] [--chrome-path <path>] [--param Key=Value]"
37128
39370
  ],
37129
39371
  examples: [
37130
39372
  "forgecad render sketch examples/01-fully-constrained-rect.forge.js",
@@ -37133,6 +39375,7 @@ var commands = [
37133
39375
  ],
37134
39376
  completion: {
37135
39377
  options: [
39378
+ ...PARAM_OVERRIDE_OPTIONS,
37136
39379
  { name: "--size", description: "Output image size in pixels (square)", argument: "required", valueLabel: "<px>" },
37137
39380
  { name: "--background", description: "Background color (CSS color)", argument: "required", valueLabel: "<color>" },
37138
39381
  { name: "--chrome-path", description: "Chrome executable path", argument: "required", valueLabel: "<path>" },
@@ -37211,7 +39454,7 @@ var commands = [
37211
39454
  ],
37212
39455
  completion: {
37213
39456
  options: [
37214
- ...PARAM_OPTIONS,
39457
+ ...PARAM_OVERRIDE_OPTIONS,
37215
39458
  { name: "--plane", description: "Cut plane", argument: "required", valueLabel: "<XY|XZ|YZ>", values: SECTION_PLANE_VALUES },
37216
39459
  { name: "--offset", description: "Shift the cut position along the plane normal", argument: "required", valueLabel: "<mm>" },
37217
39460
  { name: "--size", description: "Output PNG size in pixels (square)", argument: "required", valueLabel: "<px>" },
@@ -37274,13 +39517,13 @@ var commands = [
37274
39517
  path: ["cut-list"],
37275
39518
  summary: "Print a grouped sheet-material cut list from `sheetStock()` declarations.",
37276
39519
  description: 'Runs the script, collects every `sheetStock(...)` declaration, and prints a grouped terminal cut list with quantity, part name, size, and material.\n\nUse this when you want to answer "what rectangles do I actually need to cut?" without generating a PDF layout. For sheet packing, kerf-aware nesting, and cut sequencing, use `forgecad export cutting-layout` instead.',
37277
- usage: ["forgecad cut-list <script.forge.js> [input ...] [--joint JointName=Value]"],
39520
+ usage: ["forgecad cut-list <script.forge.js> [input ...] [--param Key=Value] [--joint JointName=Value]"],
37278
39521
  examples: [
37279
39522
  "forgecad cut-list examples/api/sheet-stock-cut-list.forge.js",
37280
39523
  "forgecad cut-list examples/api/sheet-stock-cut-list.forge.js path/to/my-cabinet.forge.js"
37281
39524
  ],
37282
39525
  completion: {
37283
- options: JOINT_OPTIONS,
39526
+ options: PARAM_OPTIONS,
37284
39527
  positionals: [{ description: "Forge script", valueKind: "forge-script", repeatable: true }]
37285
39528
  },
37286
39529
  run: runCutListCli
@@ -37289,7 +39532,7 @@ var commands = [
37289
39532
  group: "Export",
37290
39533
  path: ["export", "svg"],
37291
39534
  summary: "Export a sketch `.forge.js` file to SVG.",
37292
- usage: ["forgecad export svg <script.forge.js> [input ...] [--output path]"],
39535
+ usage: ["forgecad export svg <script.forge.js> [input ...] [--output path] [--param Key=Value]"],
37293
39536
  examples: [
37294
39537
  "forgecad export svg examples/01-fully-constrained-rect.forge.js",
37295
39538
  "forgecad export svg examples/01-fully-constrained-rect.forge.js --output out/rect.svg",
@@ -37297,6 +39540,7 @@ var commands = [
37297
39540
  ],
37298
39541
  completion: {
37299
39542
  options: [
39543
+ ...PARAM_OVERRIDE_OPTIONS,
37300
39544
  {
37301
39545
  name: "--output",
37302
39546
  description: "Output SVG path for a single input",
@@ -37313,7 +39557,7 @@ var commands = [
37313
39557
  group: "Export",
37314
39558
  path: ["export", "sketch-pdf"],
37315
39559
  summary: "Export a constrained sketch `.forge.js` to a single-page PDF with dimensions, constraints, and surfaces.",
37316
- usage: ["forgecad export sketch-pdf <script.forge.js> [input ...] [--output path]"],
39560
+ usage: ["forgecad export sketch-pdf <script.forge.js> [input ...] [--output path] [--param Key=Value]"],
37317
39561
  examples: [
37318
39562
  "forgecad export sketch-pdf examples/constraints/01-fully-constrained-rect.forge.js",
37319
39563
  "forgecad export sketch-pdf examples/constraints/01-fully-constrained-rect.forge.js --output out/rect.pdf",
@@ -37321,6 +39565,7 @@ var commands = [
37321
39565
  ],
37322
39566
  completion: {
37323
39567
  options: [
39568
+ ...PARAM_OVERRIDE_OPTIONS,
37324
39569
  {
37325
39570
  name: "--output",
37326
39571
  description: "Output PDF path for a single input",
@@ -37338,7 +39583,7 @@ var commands = [
37338
39583
  path: ["export", "step"],
37339
39584
  summary: "Export exact STEP through the native OCCT or Truck runtime exporter.",
37340
39585
  usage: [
37341
- "forgecad export step <model.forge.js|asset.step|asset.stp> [input ...] [--output path] [--backend occt|truck] [--joint JointName=Value]"
39586
+ "forgecad export step <model.forge.js|asset.step|asset.stp> [input ...] [--output path] [--backend occt|truck] [--param Key=Value] [--joint JointName=Value]"
37342
39587
  ],
37343
39588
  examples: [
37344
39589
  "forgecad export step examples/api/boolean-operations.forge.js",
@@ -37348,7 +39593,7 @@ var commands = [
37348
39593
  ],
37349
39594
  completion: {
37350
39595
  options: [
37351
- ...JOINT_OPTIONS,
39596
+ ...PARAM_OPTIONS,
37352
39597
  { name: "--output", description: "Output STEP path", argument: "required", valueLabel: "<path>", valueKind: "path" },
37353
39598
  {
37354
39599
  name: "--backend",
@@ -37365,14 +39610,16 @@ var commands = [
37365
39610
  group: "Export",
37366
39611
  path: ["export", "brep"],
37367
39612
  summary: "Export exact BREP through the native OCCT runtime exporter.",
37368
- usage: ["forgecad export brep <model.forge.js|asset.step|asset.stp> [input ...] [--output path] [--joint JointName=Value]"],
39613
+ usage: [
39614
+ "forgecad export brep <model.forge.js|asset.step|asset.stp> [input ...] [--output path] [--param Key=Value] [--joint JointName=Value]"
39615
+ ],
37369
39616
  examples: [
37370
39617
  "forgecad export brep examples/api/boolean-operations.forge.js",
37371
39618
  "forgecad export brep examples/api/boolean-operations.forge.js examples/api/mixed-edge-finishes-proof.forge.js"
37372
39619
  ],
37373
39620
  completion: {
37374
39621
  options: [
37375
- ...JOINT_OPTIONS,
39622
+ ...PARAM_OPTIONS,
37376
39623
  { name: "--output", description: "Output BREP path", argument: "required", valueLabel: "<path>", valueKind: "path" }
37377
39624
  ],
37378
39625
  positionals: [{ description: "Forge script or STEP file", valueKind: "exact-cad-input", repeatable: true }]
@@ -37384,7 +39631,7 @@ var commands = [
37384
39631
  path: ["export", "3mf"],
37385
39632
  summary: "Export a Forge script to 3MF (3D Manufacturing Format) for 3D printing.",
37386
39633
  usage: [
37387
- "forgecad export 3mf <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [input ...] [--output path] [--quality default|live|high] [--backend manifold|occt|truck|sdf] [--joint JointName=Value]"
39634
+ "forgecad export 3mf <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [input ...] [--output path] [--quality default|live|high] [--backend manifold|occt|truck|sdf] [--param Key=Value] [--joint JointName=Value]"
37388
39635
  ],
37389
39636
  examples: [
37390
39637
  "forgecad export 3mf examples/products/cup.forge.js",
@@ -37395,7 +39642,7 @@ var commands = [
37395
39642
  ],
37396
39643
  completion: {
37397
39644
  options: [
37398
- ...JOINT_OPTIONS,
39645
+ ...PARAM_OPTIONS,
37399
39646
  { name: "--output", description: "Output 3MF path", argument: "required", valueLabel: "<path>", valueKind: "path" },
37400
39647
  {
37401
39648
  name: "--quality",
@@ -37421,18 +39668,19 @@ var commands = [
37421
39668
  path: ["export", "stl"],
37422
39669
  summary: "Export a Forge script to binary STL.",
37423
39670
  usage: [
37424
- "forgecad export stl <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [input ...] [--output path] [--quality default|live|high] [--backend manifold|occt|truck|sdf] [--joint JointName=Value]"
39671
+ "forgecad export stl <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [input ...] [--output path] [--quality default|live|high] [--backend manifold|occt|truck|sdf] [--param Key=Value] [--joint JointName=Value]"
37425
39672
  ],
37426
39673
  examples: [
37427
39674
  "forgecad export stl examples/products/cup.forge.js",
37428
39675
  "forgecad export stl examples/products/cup.forge.js --output out/cup.stl",
39676
+ "forgecad export stl examples/products/cup.forge.js --param Diameter=80 --output out/cup.stl",
37429
39677
  "forgecad export stl examples/products/cup.forge.js examples/api/boolean-operations.forge.js",
37430
39678
  "forgecad export stl examples/products/cup.forge.js --quality high",
37431
39679
  "forgecad export stl examples/products/cup.forge.js --backend occt"
37432
39680
  ],
37433
39681
  completion: {
37434
39682
  options: [
37435
- ...JOINT_OPTIONS,
39683
+ ...PARAM_OPTIONS,
37436
39684
  { name: "--output", description: "Output STL path", argument: "required", valueLabel: "<path>", valueKind: "path" },
37437
39685
  {
37438
39686
  name: "--quality",
@@ -37457,7 +39705,7 @@ var commands = [
37457
39705
  group: "Export",
37458
39706
  path: ["export", "gcode"],
37459
39707
  summary: "Export a G-code toolpath script to .gcode for direct 3D printing.",
37460
- usage: ["forgecad export gcode <script.forge.js> [input ...] [--output path] [--joint JointName=Value]"],
39708
+ usage: ["forgecad export gcode <script.forge.js> [input ...] [--output path] [--param Key=Value] [--joint JointName=Value]"],
37461
39709
  examples: [
37462
39710
  "forgecad export gcode examples/gcode/parametric-vase.forge.js",
37463
39711
  "forgecad export gcode examples/gcode/parametric-vase.forge.js --output out/vase.gcode",
@@ -37465,7 +39713,7 @@ var commands = [
37465
39713
  ],
37466
39714
  completion: {
37467
39715
  options: [
37468
- ...JOINT_OPTIONS,
39716
+ ...PARAM_OPTIONS,
37469
39717
  { name: "--output", description: "Output file path for a single input", argument: "required", valueLabel: "<path>" },
37470
39718
  { name: "-o", description: "Shorthand for --output", argument: "required", valueLabel: "<path>" }
37471
39719
  ],
@@ -37479,7 +39727,7 @@ var commands = [
37479
39727
  summary: "Export direct SDF scene data for GPU or server-side implicit rendering.",
37480
39728
  description: "Writes JSON SDF scene programs or a WebGPU distance-brick compute package without converting the model to STL/3MF meshes. Defaults to the SDF backend.",
37481
39729
  usage: [
37482
- "forgecad export implicit <model.forge.js|asset.step|asset.stp> [--output path] [--format json|webgpu-brick] [--quality live|default|high] [--backend manifold|occt|truck|sdf] [--workgroup-size 4x4x4]"
39730
+ "forgecad export implicit <model.forge.js|asset.step|asset.stp> [--output path] [--format json|webgpu-brick] [--quality live|default|high] [--backend manifold|occt|truck|sdf] [--workgroup-size 4x4x4] [--param Key=Value] [--joint JointName=Value]"
37483
39731
  ],
37484
39732
  examples: [
37485
39733
  "forgecad export implicit examples/products/bottle.forge.js --quality live",
@@ -37488,7 +39736,6 @@ var commands = [
37488
39736
  completion: {
37489
39737
  options: [
37490
39738
  ...PARAM_OPTIONS,
37491
- ...JOINT_OPTIONS,
37492
39739
  { name: "--output", description: "Output file or package directory", argument: "required", valueLabel: "<path>" },
37493
39740
  { name: "-o", description: "Shorthand for --output", argument: "required", valueLabel: "<path>" },
37494
39741
  {
@@ -37529,7 +39776,7 @@ var commands = [
37529
39776
  group: "Export",
37530
39777
  path: ["export", "sdf"],
37531
39778
  summary: "Export a robot assembly as a Gazebo-ready SDF package.",
37532
- usage: ["forgecad export sdf <script.forge.js> [input ...] [--output dir] [--joint JointName=Value]"],
39779
+ usage: ["forgecad export sdf <script.forge.js> [input ...] [--output dir] [--param Key=Value] [--joint JointName=Value]"],
37533
39780
  examples: [
37534
39781
  "forgecad export sdf path/to/robot.forge.js",
37535
39782
  "forgecad export sdf path/to/robot.forge.js --output out/robot_sdf",
@@ -37537,7 +39784,7 @@ var commands = [
37537
39784
  ],
37538
39785
  completion: {
37539
39786
  options: [
37540
- ...JOINT_OPTIONS,
39787
+ ...PARAM_OPTIONS,
37541
39788
  {
37542
39789
  name: "--output",
37543
39790
  description: "Output package directory for a single input",
@@ -37554,15 +39801,16 @@ var commands = [
37554
39801
  group: "Export",
37555
39802
  path: ["export", "mjcf"],
37556
39803
  summary: "Export a robot assembly as a native MuJoCo MJCF package.",
37557
- usage: ["forgecad export mjcf <script.forge.js> [input ...] [--output dir] [--joint JointName=Value]"],
39804
+ usage: ["forgecad export mjcf <script.forge.js> [input ...] [--output dir] [--param Key=Value] [--joint JointName=Value]"],
37558
39805
  examples: [
37559
39806
  "forgecad export mjcf path/to/robot.forge.js",
37560
39807
  "forgecad export mjcf path/to/robot.forge.js --output out/robot_mjcf",
39808
+ "forgecad export mjcf path/to/robot.forge.js --param Wheelbase=180 --output out/robot_mjcf",
37561
39809
  "forgecad export mjcf path/to/robot.forge.js path/to/second-robot.forge.js"
37562
39810
  ],
37563
39811
  completion: {
37564
39812
  options: [
37565
- ...JOINT_OPTIONS,
39813
+ ...PARAM_OPTIONS,
37566
39814
  {
37567
39815
  name: "--output",
37568
39816
  description: "Output package directory for a single input",
@@ -37579,7 +39827,7 @@ var commands = [
37579
39827
  group: "Export",
37580
39828
  path: ["export", "urdf"],
37581
39829
  summary: "Export a robot assembly as a URDF package (ROS/PyBullet/MuJoCo).",
37582
- usage: ["forgecad export urdf <script.forge.js> [input ...] [--output dir] [--joint JointName=Value]"],
39830
+ usage: ["forgecad export urdf <script.forge.js> [input ...] [--output dir] [--param Key=Value] [--joint JointName=Value]"],
37583
39831
  examples: [
37584
39832
  "forgecad export urdf path/to/robot.forge.js",
37585
39833
  "forgecad export urdf path/to/robot.forge.js --output out/robot_urdf",
@@ -37587,7 +39835,7 @@ var commands = [
37587
39835
  ],
37588
39836
  completion: {
37589
39837
  options: [
37590
- ...JOINT_OPTIONS,
39838
+ ...PARAM_OPTIONS,
37591
39839
  {
37592
39840
  name: "--output",
37593
39841
  description: "Output package directory for a single input",
@@ -37600,11 +39848,30 @@ var commands = [
37600
39848
  },
37601
39849
  run: runUrdfCli
37602
39850
  },
39851
+ {
39852
+ group: "Export",
39853
+ path: ["export", "pinocchio"],
39854
+ summary: "Export a robot assembly as a Pinocchio dynamics package (URDF + gravity-torque harness).",
39855
+ description: "Reuses the URDF package (with per-link inertials computed from the solid) and adds a runnable Pinocchio harness. `scripts/run_pinocchio.py` sweeps each joint, computes the gravity-hold torque via `pin.computeGeneralizedGravity`, and writes `feedback.json` (the `forgecad.feedback/v1` contract); re-surface it with `forgecad sim dynamics <dir>`. Pinocchio (BSD-2) and Coal (BSD-3) are user-installed \u2014 Linux/macOS `pip install pin coal`, Windows conda-forge \u2014 never bundled.",
39856
+ usage: ["forgecad export pinocchio <script.forge.js> [input ...] [--output dir] [--param Key=Value] [--joint JointName=Value]"],
39857
+ examples: [
39858
+ "forgecad export pinocchio examples/analysis/lever-arm-actuator.forge.js",
39859
+ "forgecad export pinocchio path/to/robot.forge.js --output out/robot_pinocchio"
39860
+ ],
39861
+ completion: {
39862
+ options: [
39863
+ ...PARAM_OPTIONS,
39864
+ { name: "--output", description: "Output package directory for a single input", argument: "required", valueLabel: "<dir>", valueKind: "directory" }
39865
+ ],
39866
+ positionals: [{ description: "Forge script", valueKind: "forge-script", repeatable: true }]
39867
+ },
39868
+ run: runPinocchioCli
39869
+ },
37603
39870
  {
37604
39871
  group: "Export",
37605
39872
  path: ["export", "usd"],
37606
39873
  summary: "Export a SimReady assembly as an Isaac Sim-ready USD package.",
37607
- usage: ["forgecad export usd <script.forge.js> [input ...] [--output dir] [--joint JointName=Value]"],
39874
+ usage: ["forgecad export usd <script.forge.js> [input ...] [--output dir] [--param Key=Value] [--joint JointName=Value]"],
37608
39875
  examples: [
37609
39876
  "forgecad export usd path/to/robot.forge.js",
37610
39877
  "forgecad export usd path/to/robot.forge.js path/to/arm.forge.js",
@@ -37612,7 +39879,7 @@ var commands = [
37612
39879
  ],
37613
39880
  completion: {
37614
39881
  options: [
37615
- ...JOINT_OPTIONS,
39882
+ ...PARAM_OPTIONS,
37616
39883
  {
37617
39884
  name: "--output",
37618
39885
  description: "Output package directory for a single input",
@@ -37630,7 +39897,7 @@ var commands = [
37630
39897
  path: ["export", "report"],
37631
39898
  summary: "Generate a multi-view PDF report with BOM and dimensions.",
37632
39899
  usage: [
37633
- "forgecad export report <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [input ...] [--output path] [--dim-angle-tol <deg>] [--joint JointName=Value]"
39900
+ "forgecad export report <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [input ...] [--output path] [--dim-angle-tol <deg>] [--param Key=Value] [--joint JointName=Value]"
37634
39901
  ],
37635
39902
  examples: [
37636
39903
  "forgecad export report examples/products/cup.forge.js",
@@ -37639,7 +39906,7 @@ var commands = [
37639
39906
  ],
37640
39907
  completion: {
37641
39908
  options: [
37642
- ...JOINT_OPTIONS,
39909
+ ...PARAM_OPTIONS,
37643
39910
  {
37644
39911
  name: "--output",
37645
39912
  description: "Output PDF path for a single input",
@@ -37660,7 +39927,7 @@ var commands = [
37660
39927
  description: "Packs `sheetStock(...)` pieces onto stock sheets, writes a cutting-layout PDF or DXF, and prints the guillotine cut sequence in the terminal.\n\nPDF is the default review format. Use `--format dxf` or an `.dxf` output path when sending the packed layout to CAD/CAM tools; DXF output contains only the placed cut outlines by default. For the simpler grouped parts list without packing or layout export, use `forgecad cut-list`.",
37661
39928
  usage: [
37662
39929
  "forgecad export cutting-layout <script.forge.js> [input ...] [--output output.pdf|output.dxf]",
37663
- " [--format pdf|dxf] [--sheet-width <mm>] [--sheet-height <mm>] [--kerf <mm>] [--joint JointName=Value]"
39930
+ " [--format pdf|dxf] [--sheet-width <mm>] [--sheet-height <mm>] [--kerf <mm>] [--param Key=Value] [--joint JointName=Value]"
37664
39931
  ],
37665
39932
  examples: [
37666
39933
  "forgecad export cutting-layout examples/api/sheet-stock-cut-list.forge.js",
@@ -37670,7 +39937,7 @@ var commands = [
37670
39937
  ],
37671
39938
  completion: {
37672
39939
  options: [
37673
- ...JOINT_OPTIONS,
39940
+ ...PARAM_OPTIONS,
37674
39941
  { name: "--output", description: "Output PDF or DXF path (single input only)", argument: "required", valueLabel: "<path>" },
37675
39942
  { name: "--out", description: "Alias for --output", argument: "required", valueLabel: "<path>" },
37676
39943
  {
@@ -38423,23 +40690,145 @@ var commands = [
38423
40690
  path: ["check", "simready"],
38424
40691
  summary: "Validate source-authored robot and physics metadata before simulation export.",
38425
40692
  description: "Runs a Forge script and checks the returned `assembly(...).withSimulation(...)` contract without Isaac Sim, OpenUSD, or NVIDIA validators. The gate validates Sim.body metadata, explicit colliders, contact connectors, controller joints, numeric physics values, and the robot joint graph.",
38426
- usage: ["forgecad check simready <script.forge.js> [input ...] [--json] [--joint JointName=Value]"],
40693
+ usage: ["forgecad check simready <script.forge.js> [input ...] [--json] [--param Key=Value] [--joint JointName=Value]"],
38427
40694
  examples: [
38428
40695
  "forgecad check simready examples/robotics/simready-diff-drive-rover.forge.js",
38429
40696
  "forgecad check simready examples/robotics/simready-diff-drive-rover.forge.js path/to/second-robot.forge.js",
38430
- "forgecad check simready robot.forge.js --json"
40697
+ "forgecad check simready robot.forge.js --param Wheelbase=180 --json"
38431
40698
  ],
38432
40699
  completion: {
38433
40700
  options: [
40701
+ ...PARAM_OPTIONS,
38434
40702
  { name: "--json", description: "Emit the machine-readable report to stdout" },
38435
40703
  { name: "--compact", description: "Minify JSON output" },
38436
- { name: "--no-fail-exit", description: "Always exit 0 after writing the report" },
38437
- ...JOINT_OPTIONS
40704
+ { name: "--no-fail-exit", description: "Always exit 0 after writing the report" }
38438
40705
  ],
38439
40706
  positionals: [{ description: "Forge script", valueKind: "forge-script", repeatable: true }]
38440
40707
  },
38441
40708
  run: runCheckSimReadyCli
38442
40709
  },
40710
+ {
40711
+ group: "Inspect",
40712
+ path: ["sim", "mass"],
40713
+ summary: "Compute mass properties: volume, mass, center of mass, inertia tensor, principal moments.",
40714
+ description: "Native-analytic mass-properties analysis computed directly from the model geometry \u2014 no external solver. Integrates the triangulated mesh (Mirtich/Eberly divergence theorem) to report volume, surface area, bounding box, mass-weighted center of mass, the full inertia tensor about the center of mass, principal moments and axes, and a per-object breakdown.\n\nVolume, center of mass, and principal axes are exact regardless of material. Pass `--density <kg/m3>` for accurate mass and inertia magnitude (default 1000 kg/m\xB3 water, reported as a warning). Emits the `forgecad.feedback/v1` JSON contract with `--json`.",
40715
+ usage: ["forgecad sim mass <model.forge.js> [input ...] [--density kg/m3] [--json] [--param Key=Value] [--joint JointName=Value]"],
40716
+ examples: [
40717
+ "forgecad sim mass examples/analysis/tipping-tripod.forge.js",
40718
+ "forgecad sim mass bracket.forge.js --density 2700 --json",
40719
+ "forgecad sim mass rover.forge.js --param Wheelbase=180 --json"
40720
+ ],
40721
+ completion: {
40722
+ options: [
40723
+ ...PARAM_OPTIONS,
40724
+ { name: "--density", description: "Uniform density in kg/m\xB3 for mass and inertia", argument: "required", valueLabel: "<kg/m3>" },
40725
+ { name: "--json", description: "Emit the forgecad.feedback/v1 report to stdout" },
40726
+ { name: "--compact", description: "Minify JSON output" }
40727
+ ],
40728
+ positionals: [{ description: "Forge script", valueKind: "forge-script", repeatable: true }]
40729
+ },
40730
+ run: runSimMassCli
40731
+ },
40732
+ {
40733
+ group: "Inspect",
40734
+ path: ["sim", "tolerance"],
40735
+ summary: "Tolerance / GD&T stack-up: yield, Cp/Cpk, and which dimension to tighten.",
40736
+ description: "Native-analytic tolerance stack-up \u2014 no external solver. Treats the model's continuous parameters as the varying manufacturing inputs and its numeric `verify.*` results as the measured responses (with spec limits from the verify call or `--spec`). Builds a finite-difference Jacobian, runs a deterministic Monte Carlo over the linearized surrogate, and reports per-response mean/sigma, Cp/Cpk, yield % and DPMO, worst-case and RSS envelopes, a ranked variance-contribution list, and a tolerance-allocation recommendation to reach the target Cpk.\n\nA nonlinearity guard errors (telling you to pass `--method full-rebuild`) rather than silently reporting a wrong yield. Exits non-zero when any response's Cpk is below `--target-cpk` (default 1.33). Emits the `forgecad.feedback/v1` JSON contract with `--json`.",
40737
+ usage: ["forgecad sim tolerance <model.forge.js> [--tol Param=\xB1X] [--spec Resp=lo..hi] [--method linearized|full-rebuild] [--json]"],
40738
+ examples: [
40739
+ "forgecad sim tolerance examples/analysis/clearance-fit.forge.js --json",
40740
+ "forgecad sim tolerance fit.forge.js --tol BoreDia=\xB10.02 --target-cpk 1.33 --json",
40741
+ "forgecad sim tolerance fit.forge.js --method full-rebuild --samples 5000 --json"
40742
+ ],
40743
+ completion: {
40744
+ options: [
40745
+ ...PARAM_OPTIONS,
40746
+ { name: "--method", description: "linearized (default) or full-rebuild", argument: "required", valueLabel: "<linearized|full-rebuild>" },
40747
+ { name: "--samples", description: "Monte Carlo sample count", argument: "required", valueLabel: "<n>" },
40748
+ { name: "--seed", description: "Deterministic RNG seed", argument: "required", valueLabel: "<n>" },
40749
+ { name: "--sigma", description: "Tolerance band = N sigma (default 3)", argument: "required", valueLabel: "<n>" },
40750
+ { name: "--target-cpk", description: "Target Cpk for pass/allocation (default 1.33)", argument: "required", valueLabel: "<n>" },
40751
+ { name: "--tol", description: "Override an input band: Param=\xB1X or Param=lo..hi", argument: "required", valueLabel: "<spec>" },
40752
+ { name: "--dist", description: "Input distribution: Param=normal|uniform", argument: "required", valueLabel: "<spec>" },
40753
+ { name: "--spec", description: "Declare response spec limits: Resp=lsl..usl (* for open)", argument: "required", valueLabel: "<spec>" },
40754
+ { name: "--nonlinearity-tol", description: "Curvature threshold tripping the guard (default 0.05)", argument: "required", valueLabel: "<n>" },
40755
+ { name: "--json", description: "Emit the forgecad.feedback/v1 report to stdout" },
40756
+ { name: "--compact", description: "Minify JSON output" },
40757
+ { name: "--no-fail-exit", description: "Always exit 0 after writing the report" }
40758
+ ],
40759
+ positionals: [{ description: "Forge script", valueKind: "forge-script", repeatable: true }]
40760
+ },
40761
+ run: runSimToleranceCli
40762
+ },
40763
+ {
40764
+ group: "Inspect",
40765
+ path: ["sim", "mechanism"],
40766
+ summary: "Mechanism budget: DOF, and the gravity-hold torque each joint must supply.",
40767
+ description: "Native-analytic mechanism analysis from the assembly joint graph \u2014 no external solver, no time-stepping. Reports the actuated degrees of freedom (and a spatial Gr\xFCbler mobility diagnostic for closed loops), and for each actuated joint the gravity-hold torque (revolute) or force (prismatic) it must supply at the current pose, computed from the mass of the distal subtree. When a joint carries a `Sim.drive` effort budget, the report includes the margin and flags over-budget joints.\n\nThis is the deterministic budget tier; the dynamic/contact tier is `export pinocchio`. Exits non-zero when any joint exceeds its budget (or falls below `--min-torque-margin-pct`). Use `--joint Name=Value` to evaluate a specific pose. Emits the `forgecad.feedback/v1` JSON contract with `--json`.",
40768
+ usage: ["forgecad sim mechanism <model.forge.js> [--min-torque-margin-pct N] [--joint Name=Value] [--json]"],
40769
+ examples: [
40770
+ "forgecad sim mechanism examples/analysis/lever-arm-actuator.forge.js --json",
40771
+ "forgecad sim mechanism arm.forge.js --joint shoulder=45 --json",
40772
+ "forgecad sim mechanism arm.forge.js --min-torque-margin-pct 20 --json"
40773
+ ],
40774
+ completion: {
40775
+ options: [
40776
+ ...PARAM_OPTIONS,
40777
+ { name: "--min-torque-margin-pct", description: "Required actuator budget margin %", argument: "required", valueLabel: "<pct>" },
40778
+ { name: "--density", description: "Fallback density kg/m\xB3 for parts without Sim.body mass", argument: "required", valueLabel: "<kg/m3>" },
40779
+ { name: "--json", description: "Emit the forgecad.feedback/v1 report to stdout" },
40780
+ { name: "--compact", description: "Minify JSON output" },
40781
+ { name: "--no-fail-exit", description: "Always exit 0 after writing the report" }
40782
+ ],
40783
+ positionals: [{ description: "Forge script", valueKind: "forge-script", repeatable: true }]
40784
+ },
40785
+ run: runSimMechanismCli
40786
+ },
40787
+ {
40788
+ group: "Inspect",
40789
+ path: ["sim", "dynamics"],
40790
+ summary: "Read back a Pinocchio dynamics run (feedback.json) as the forgecad.feedback/v1 contract.",
40791
+ description: "Ingests the `feedback.json` written by an exported Pinocchio package's `run_pinocchio.py` and re-surfaces it through ForgeCAD, closing the read-back side of the dynamics loop. Pass the package directory (or the feedback.json directly). Reports `DYNAMICS.NOT_RUN` when the results are missing, and exits non-zero when the run reported a failing metric.",
40792
+ usage: ["forgecad sim dynamics <pinocchio-package-dir|feedback.json> [--json] [--compact]"],
40793
+ examples: [
40794
+ "forgecad sim dynamics examples/analysis/lever-arm-actuator.pinocchiopkg",
40795
+ "forgecad sim dynamics out/robot_pinocchio/feedback.json --json"
40796
+ ],
40797
+ completion: {
40798
+ options: [
40799
+ { name: "--json", description: "Emit the forgecad.feedback/v1 report to stdout" },
40800
+ { name: "--compact", description: "Minify JSON output" },
40801
+ { name: "--no-fail-exit", description: "Always exit 0 after writing the report" }
40802
+ ],
40803
+ positionals: [{ description: "Pinocchio package directory or feedback.json", valueKind: "path", repeatable: true }]
40804
+ },
40805
+ run: runSimDynamicsCli
40806
+ },
40807
+ {
40808
+ group: "Checks",
40809
+ path: ["check", "stability"],
40810
+ summary: "Check static tip-over stability: center of mass vs the support footprint.",
40811
+ description: "Native-analytic static stability check \u2014 no external solver. Computes the uniform-density center of mass, derives the support footprint as the convex hull of the lowest geometry, and reports the signed tip-over margin (distance from the CoM projection to the footprint edge; negative means it tips), the tilt angle at which it tips, the critical edge, and the center-of-mass shift needed to reach the required margin.\n\nExits non-zero when the margin is below `--min-margin-mm` (default 0). Use `--up x|y|z` to set the up axis (default z). Emits the `forgecad.feedback/v1` JSON contract with `--json`.",
40812
+ usage: ["forgecad check stability <model.forge.js> [input ...] [--min-margin-mm N] [--up x|y|z] [--json] [--param Key=Value]"],
40813
+ examples: [
40814
+ "forgecad check stability examples/analysis/tipping-tripod.forge.js",
40815
+ "forgecad check stability tower.forge.js --min-margin-mm 5 --json",
40816
+ "forgecad check stability tower.forge.js --param BaseSpreadMm=120 --json"
40817
+ ],
40818
+ completion: {
40819
+ options: [
40820
+ ...PARAM_OPTIONS,
40821
+ { name: "--min-margin-mm", description: "Required signed tip-over margin in mm", argument: "required", valueLabel: "<mm>" },
40822
+ { name: "--up", description: "Up axis (gravity is its negative)", argument: "required", valueLabel: "<x|y|z>" },
40823
+ { name: "--contact-tol", description: "Footprint contact tolerance in mm", argument: "required", valueLabel: "<mm>" },
40824
+ { name: "--json", description: "Emit the forgecad.feedback/v1 report to stdout" },
40825
+ { name: "--compact", description: "Minify JSON output" },
40826
+ { name: "--no-fail-exit", description: "Always exit 0 after writing the report" }
40827
+ ],
40828
+ positionals: [{ description: "Forge script", valueKind: "forge-script", repeatable: true }]
40829
+ },
40830
+ run: runCheckStabilityCli
40831
+ },
38443
40832
  {
38444
40833
  group: "Checks",
38445
40834
  path: ["check", "params"],
@@ -38805,7 +41194,7 @@ var commands = [
38805
41194
  { name: "--update", description: "Regenerate compiler snapshots" }
38806
41195
  ]
38807
41196
  },
38808
- run: async (args) => (await import("./check-compiler-SP7FAL7R.js")).runCheckCompilerCli(args)
41197
+ run: async (args) => (await import("./check-compiler-HPF2T2FS.js")).runCheckCompilerCli(args)
38809
41198
  },
38810
41199
  {
38811
41200
  group: "Checks",
@@ -38828,7 +41217,7 @@ var commands = [
38828
41217
  { name: "--update", description: "Regenerate query-propagation snapshots" }
38829
41218
  ]
38830
41219
  },
38831
- run: async (args) => (await import("./check-query-propagation-BRLSHP22.js")).runCheckQueryPropagationCli(args)
41220
+ run: async (args) => (await import("./check-query-propagation-HYSLTXAB.js")).runCheckQueryPropagationCli(args)
38832
41221
  },
38833
41222
  {
38834
41223
  group: "Checks",
@@ -39057,7 +41446,7 @@ var commands = [
39057
41446
  ];
39058
41447
  function readVersion() {
39059
41448
  try {
39060
- const pkg = JSON.parse(readFileSync26(resolvePackagePath(import.meta.url, "package.json"), "utf-8"));
41449
+ const pkg = JSON.parse(readFileSync28(resolvePackagePath(import.meta.url, "package.json"), "utf-8"));
39061
41450
  return pkg.version || "0.0.0";
39062
41451
  } catch {
39063
41452
  return "0.0.0";
@@ -39555,7 +41944,7 @@ function oclifCommandTarget() {
39555
41944
  return import.meta.url.includes("/dist-cli/") ? "./dist-cli/forgecad.js" : "./cli/forgecad.ts";
39556
41945
  }
39557
41946
  function forgeCadOclifPjson() {
39558
- const pkg = JSON.parse(readFileSync26(resolvePackagePath(import.meta.url, "package.json"), "utf-8"));
41947
+ const pkg = JSON.parse(readFileSync28(resolvePackagePath(import.meta.url, "package.json"), "utf-8"));
39559
41948
  return {
39560
41949
  ...pkg,
39561
41950
  oclif: {