forgecad 0.9.14 → 0.9.15

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 (219) hide show
  1. package/LICENSE +6 -4
  2. package/README.md +8 -4
  3. package/dist/assets/{AdminPage-eWGs2K6H.js → AdminPage-CDyGUinA.js} +2 -2
  4. package/dist/assets/{BenchmarkPage-CTrLKfpo.js → BenchmarkPage-DfPMY_-d.js} +4 -15
  5. package/dist/assets/{BlogPage-5nPesyds.js → BlogPage-kF0fkdJT.js} +2 -2
  6. package/dist/assets/{DocsPage-C4Y3nbYc.js → DocsPage-B954L3YN.js} +9 -3
  7. package/dist/assets/EditorApp-Beb-IZ0y.js +14014 -0
  8. package/dist/assets/{EditorApp-BAnckbsk.css → EditorApp-CuDLxKqL.css} +698 -0
  9. package/dist/assets/{EmbedViewer-C8fB4n5U.js → EmbedViewer-C77B-TrF.js} +3 -3
  10. package/dist/assets/{LandingPageProofDriven-jSz0LaMM.js → LandingPageProofDriven-Cr6fXMDj.js} +35 -37
  11. package/dist/assets/LegalPage-BRlScr9A.css +91 -0
  12. package/dist/assets/LegalPage-Dzklqmmg.js +39 -0
  13. package/dist/assets/{PricingPage-BMedqFef.css → PricingPage-BPF6HKyO.css} +25 -0
  14. package/dist/assets/{PricingPage-B83B90zh.js → PricingPage-zWXkvlwl.js} +19 -19
  15. package/dist/assets/{SettingsPage-DY889pcu.js → SettingsPage-Bz0of4KQ.js} +2 -2
  16. package/dist/assets/app-CE3sYcV7.css +3890 -0
  17. package/dist/assets/{app-bEww1ic4.js → app-D3kDkggg.js} +2293 -946
  18. package/dist/assets/cli/{render-Cho2uKG_.js → render-DSY3mMQa.js} +337 -7
  19. package/dist/assets/{constructionHistoryWorker-HYwzJY4m.js → constructionHistoryWorker-gpDo-uH2.js} +927 -243
  20. package/dist/assets/{evalWorker-CjQwJSE-.js → evalWorker-CU0Ke6DP.js} +7800 -4164
  21. package/dist/assets/{forgecad_geometry-CH2nvuLA.js → forgecad_geometry-Dgceylq9.js} +43 -1
  22. package/dist/assets/forgecad_geometry_bg-dD4RNQF1.wasm +0 -0
  23. package/dist/assets/{inspectWorker-DeRnMVv1.js → inspectWorker-COyp8XXA.js} +927 -243
  24. package/dist/assets/{javascript-70-4uGcz.js → javascript-1kQXfVaz.js} +1 -1
  25. package/dist/assets/landing-proof-driven-DiGqdtWa.js +18 -0
  26. package/dist/assets/{landing-proof-driven-oFYW6mjz.css → landing-proof-driven-ORyigZ6p.css} +13 -7
  27. package/dist/assets/legalContent-ZfFGMmi4.js +251 -0
  28. package/dist/assets/{manifold-CG9Fokx-.js → manifold-BRI5prcH.js} +1 -1
  29. package/dist/assets/{manifold-uRzgk5O8.js → manifold-C-3h2M7p.js} +2 -2
  30. package/dist/assets/{manifold-rmfAcdwF.js → manifold-DNkrUWpA.js} +1 -1
  31. package/dist/assets/{reportWorker-4cW_ZpoS.js → reportWorker-CdBz5bNg.js} +7538 -10857
  32. package/dist/assets/{scalar-sampling-budget-CfDiFvh7.js → scalar-sampling-budget-wJF98aY9.js} +6935 -4331
  33. package/dist/assets/{scanProxyWorker-Bs2TDgLw.js → scanProxyWorker-B-9VbLIs.js} +32 -1
  34. package/dist/assets/{solver-DuJAO8S6.js → solver-BZ9LPTHs.js} +1 -1
  35. package/dist/assets/solver_bg-DAHZJ_rw.wasm +0 -0
  36. package/dist/assets/{targets-D6PWsv6X.js → targets-B9sGB5nB.js} +1 -1
  37. package/dist/assets/{vendor-react-Da3A2QmU.js → vendor-react-6j1Kke-Y.js} +6 -5
  38. package/dist/cli/render.html +1 -1
  39. package/dist/docs/index.html +2 -2
  40. package/dist/docs-raw/AI/ai-native-cad.md +50 -0
  41. package/dist/docs-raw/AI/usage.md +3 -12
  42. package/dist/docs-raw/CLI.md +30 -10
  43. package/dist/docs-raw/component-model.md +27 -11
  44. package/dist/docs-raw/generated/assembly.md +301 -212
  45. package/dist/docs-raw/generated/concepts.md +235 -237
  46. package/dist/docs-raw/generated/core.md +283 -6
  47. package/dist/docs-raw/generated/curves.md +274 -361
  48. package/dist/docs-raw/generated/lib.md +7 -1
  49. package/dist/docs-raw/generated/output.md +19 -4
  50. package/dist/docs-raw/generated/runtime-names.md +41 -0
  51. package/dist/docs-raw/generated/sdf.md +31 -0
  52. package/dist/docs-raw/generated/sheet-metal.md +9 -0
  53. package/dist/docs-raw/generated/sketch.md +44 -1
  54. package/dist/docs-raw/generated/viewport.md +11 -3
  55. package/dist/docs-raw/guides/coordinate-system.md +20 -16
  56. package/dist/docs-raw/guides/geometry-conventions.md +2 -2
  57. package/dist/docs-raw/guides/inspection-bundles.md +2 -1
  58. package/dist/docs-raw/guides/joint-design.md +24 -0
  59. package/dist/docs-raw/guides/positioning.md +13 -3
  60. package/dist/docs-raw/legal/privacy.md +63 -0
  61. package/dist/docs-raw/legal/software-license.md +55 -0
  62. package/dist/docs-raw/legal/terms.md +87 -0
  63. package/dist/docs-raw/skills/forgecad-3d-reconstruction.md +1 -1
  64. package/dist/docs-raw/skills/forgecad-blockout-model.md +1 -1
  65. package/dist/docs-raw/skills/forgecad-component-model.md +11 -2
  66. package/dist/docs-raw/skills/forgecad-high-level-spec.md +1 -1
  67. package/dist/docs-raw/skills/forgecad-image-replicator.md +8 -8
  68. package/dist/docs-raw/skills/forgecad-lld.md +1 -1
  69. package/dist/docs-raw/skills/forgecad-make-a-model.md +1 -1
  70. package/dist/docs-raw/skills/forgecad-model-grader.md +2 -2
  71. package/dist/docs-raw/skills/forgecad-prepare-prompt.md +2 -2
  72. package/dist/docs-raw/skills/forgecad-project.md +1 -1
  73. package/dist/docs-raw/skills/forgecad-reconstruction-benchmark.md +1 -1
  74. package/dist/docs-raw/skills/forgecad-render-inspect.md +4 -2
  75. package/dist/docs-raw/skills/forgecad-visual-spec.md +1 -1
  76. package/dist/docs-raw/skills/forgecad.md +4 -3
  77. package/dist/index.html +40 -12
  78. package/dist/llms.txt +8 -0
  79. package/dist/site.webmanifest +1 -1
  80. package/dist/sitemap.xml +49 -13
  81. package/dist-cli/{check-compiler-U5SOPN7X.js → check-compiler-SDX5QIXI.js} +1 -2
  82. package/dist-cli/{check-query-propagation-XOKNSSYU.js → check-query-propagation-EAYEFT77.js} +1 -2
  83. package/dist-cli/{chunk-EXWGNL6K.js → chunk-N4O47JLF.js} +12540 -9046
  84. package/dist-cli/forgecad.js +1786 -679
  85. package/dist-cli/{forgecad_geometry-GYVNKPIE.js → forgecad_geometry-QOQIIP53.js} +42 -1
  86. package/dist-cli/forgecad_geometry_bg.wasm +0 -0
  87. package/dist-cli/{solver-46FFSK2U.js → solver-OK4HECRH.js} +0 -1
  88. package/dist-cli/solver_bg.wasm +0 -0
  89. package/dist-skill/CONTEXT.md +1117 -721
  90. package/dist-skill/SKILL.md +3 -2
  91. package/dist-skill/docs/API/core/concepts.md +64 -1
  92. package/dist-skill/docs/CLI.md +30 -10
  93. package/dist-skill/docs/generated/assembly.md +277 -229
  94. package/dist-skill/docs/generated/core.md +283 -6
  95. package/dist-skill/docs/generated/curves.md +272 -362
  96. package/dist-skill/docs/generated/lib.md +7 -1
  97. package/dist-skill/docs/generated/output.md +19 -4
  98. package/dist-skill/docs/generated/runtime-names.md +41 -0
  99. package/dist-skill/docs/generated/sdf.md +31 -0
  100. package/dist-skill/docs/generated/sheet-metal.md +9 -0
  101. package/dist-skill/docs/generated/sketch.md +44 -2
  102. package/dist-skill/docs/generated/viewport.md +2 -87
  103. package/dist-skill/docs/guides/coordinate-system.md +20 -16
  104. package/dist-skill/docs/guides/geometry-conventions.md +2 -2
  105. package/dist-skill/docs/guides/inspection-bundles.md +2 -1
  106. package/dist-skill/docs/guides/joint-design.md +24 -0
  107. package/dist-skill/docs/guides/positioning.md +13 -3
  108. package/dist-skill/library/forgecad-component-model/SKILL.md +10 -1
  109. package/dist-skill/library/forgecad-image-replicator/SKILL.md +6 -6
  110. package/dist-skill/library/forgecad-image-replicator/scripts/compare_images.py +166 -0
  111. package/dist-skill/library/forgecad-model-grader/SKILL.md +1 -1
  112. package/dist-skill/library/forgecad-prepare-prompt/SKILL.md +1 -1
  113. package/dist-skill/library/forgecad-render-inspect/SKILL.md +3 -1
  114. package/examples/api/assembly-kinematics-foundation.forge.js +65 -0
  115. package/examples/api/assembly-kinematics-four-bar.forge.js +115 -0
  116. package/examples/api/assembly-kinematics-limb.forge.js +116 -0
  117. package/examples/api/connector-frame-rig-chain.forge.js +102 -0
  118. package/examples/api/exact-sheet-shell-assembly.forge.js +0 -2
  119. package/examples/api/exact-surface-studio.forge.js +6 -8
  120. package/examples/api/helix-basics.forge.js +6 -6
  121. package/examples/api/lean-foundations/README.md +12 -0
  122. package/examples/api/lean-foundations/curve-blend-exact.forge.js +22 -0
  123. package/examples/api/lean-foundations/curve-fit-interpolation.forge.js +18 -0
  124. package/examples/api/lean-foundations/curve-helix-canonicalization.forge.js +27 -0
  125. package/examples/api/lean-foundations/curve-route-canonicalization.forge.js +16 -0
  126. package/examples/api/lean-foundations/curve-trim-reverse.forge.js +24 -0
  127. package/examples/api/lean-foundations/exact-curve-arc.forge.js +36 -0
  128. package/examples/api/mixed-edge-finishes-proof.forge.js +8 -11
  129. package/examples/api/route3d-elbow.forge.js +68 -0
  130. package/examples/api/transition-curves.forge.js +44 -15
  131. package/examples/api/y-blend-corner-showcase.forge.js +0 -2
  132. package/examples/generative/coral-vase.forge.js +1 -1
  133. package/examples/nurbs-tube.forge.js +1 -1
  134. package/package.json +14 -13
  135. package/dist/assets/EditorApp-lXv53A1m.js +0 -13610
  136. package/dist/assets/app-CsHnaBWt.css +0 -1789
  137. package/dist/assets/forgecad_geometry_bg-C5_E9Oa9.wasm +0 -0
  138. package/dist/assets/solver_bg-CWvv4lnN.wasm +0 -0
  139. package/dist/docs-raw/API/README.md +0 -16
  140. package/dist/docs-raw/API/core/concepts.md +0 -118
  141. package/dist/docs-raw/INDEX.md +0 -138
  142. package/dist/docs-raw/RELEASING.md +0 -87
  143. package/dist/docs-raw/agent-native-api.md +0 -27
  144. package/dist/docs-raw/beta-deployment.md +0 -304
  145. package/dist/docs-raw/beta-operations.md +0 -325
  146. package/dist/docs-raw/blueprint-first.md +0 -145
  147. package/dist/docs-raw/cli-monetization.md +0 -112
  148. package/dist/docs-raw/coding-best-practices.md +0 -120
  149. package/dist/docs-raw/coding.md +0 -340
  150. package/dist/docs-raw/deployment.md +0 -374
  151. package/dist/docs-raw/guides/skill-maintenance.md +0 -161
  152. package/dist/docs-raw/guides/surface-members.md +0 -82
  153. package/dist/docs-raw/harbor-cli.md +0 -854
  154. package/dist/docs-raw/internals/backend-vocabulary.md +0 -35
  155. package/dist/docs-raw/internals/compiler.md +0 -307
  156. package/dist/docs-raw/internals/constraint-solver-quality.md +0 -161
  157. package/dist/docs-raw/internals/constraint-solver.md +0 -176
  158. package/dist/docs-raw/internals/shape-from-slices.md +0 -152
  159. package/dist/docs-raw/internals/sketch-2d-pipeline.md +0 -108
  160. package/dist/docs-raw/platform/admin.md +0 -45
  161. package/dist/docs-raw/platform/architecture.md +0 -82
  162. package/dist/docs-raw/platform/auth.md +0 -139
  163. package/dist/docs-raw/platform/email.md +0 -67
  164. package/dist/docs-raw/platform/google-oauth-setup.md +0 -88
  165. package/dist/docs-raw/platform/observability.md +0 -197
  166. package/dist/docs-raw/platform/projects.md +0 -111
  167. package/dist/docs-raw/platform/sharing.md +0 -90
  168. package/dist/docs-raw/product/README.md +0 -39
  169. package/dist/docs-raw/product/api-as-product-language.md +0 -13
  170. package/dist/docs-raw/product/business-model.md +0 -15
  171. package/dist/docs-raw/product/competitive-positioning.md +0 -17
  172. package/dist/docs-raw/product/creative-manufacturing.md +0 -15
  173. package/dist/docs-raw/product/founder-story.md +0 -11
  174. package/dist/docs-raw/product/manufacturing-workflows.md +0 -15
  175. package/dist/docs-raw/product/onboarding-first-experience.md +0 -256
  176. package/dist/docs-raw/product/product-loop.md +0 -17
  177. package/dist/docs-raw/product/strategic-decisions.md +0 -22
  178. package/dist/docs-raw/product/user-outreach-email-templates.md +0 -161
  179. package/dist/docs-raw/product/user-segments.md +0 -15
  180. package/dist/docs-raw/product/vision.md +0 -26
  181. package/dist/docs-raw/rl-environments.md +0 -350
  182. package/dist/docs-raw/runbook.md +0 -611
  183. package/dist-cli/check-compiler-U5SOPN7X.js.map +0 -1
  184. package/dist-cli/check-query-propagation-XOKNSSYU.js.map +0 -1
  185. package/dist-cli/chunk-EXWGNL6K.js.map +0 -1
  186. package/dist-cli/forgecad.js.map +0 -1
  187. package/dist-cli/forgecad_geometry-GYVNKPIE.js.map +0 -1
  188. package/dist-cli/solver-46FFSK2U.js.map +0 -1
  189. package/dist-skill/SKILL-dev.md +0 -145
  190. package/dist-skill/docs-dev/API/core/concepts.md +0 -118
  191. package/dist-skill/docs-dev/CLI.md +0 -677
  192. package/dist-skill/docs-dev/agent-native-api.md +0 -27
  193. package/dist-skill/docs-dev/blueprint-first.md +0 -145
  194. package/dist-skill/docs-dev/coding-best-practices.md +0 -120
  195. package/dist-skill/docs-dev/coding.md +0 -340
  196. package/dist-skill/docs-dev/component-model.md +0 -164
  197. package/dist-skill/docs-dev/generated/assembly.md +0 -794
  198. package/dist-skill/docs-dev/generated/core.md +0 -2117
  199. package/dist-skill/docs-dev/generated/curves.md +0 -2583
  200. package/dist-skill/docs-dev/generated/lib.md +0 -169
  201. package/dist-skill/docs-dev/generated/output.md +0 -247
  202. package/dist-skill/docs-dev/generated/sdf.md +0 -446
  203. package/dist-skill/docs-dev/generated/sheet-metal.md +0 -504
  204. package/dist-skill/docs-dev/generated/sketch.md +0 -1811
  205. package/dist-skill/docs-dev/generated/viewport.md +0 -585
  206. package/dist-skill/docs-dev/generated/wood.md +0 -108
  207. package/dist-skill/docs-dev/guides/coordinate-system.md +0 -46
  208. package/dist-skill/docs-dev/guides/geometry-conventions.md +0 -52
  209. package/dist-skill/docs-dev/guides/inspection-bundles.md +0 -485
  210. package/dist-skill/docs-dev/guides/joint-design.md +0 -78
  211. package/dist-skill/docs-dev/guides/modeling-recipes.md +0 -78
  212. package/dist-skill/docs-dev/guides/positioning.md +0 -161
  213. package/dist-skill/docs-dev/guides/skill-maintenance.md +0 -161
  214. package/dist-skill/docs-dev/internals/backend-vocabulary.md +0 -35
  215. package/dist-skill/docs-dev/internals/compiler.md +0 -307
  216. package/dist-skill/docs-dev/internals/constraint-solver-quality.md +0 -161
  217. package/dist-skill/docs-dev/internals/constraint-solver.md +0 -176
  218. package/dist-skill/docs-dev/internals/sketch-2d-pipeline.md +0 -108
  219. package/dist-skill/library/forgecad-image-replicator/scripts/compare_images.mjs +0 -289
@@ -29,6 +29,7 @@ import {
29
29
  buildDesignTraceNeighborhood,
30
30
  buildEdgeSvg,
31
31
  buildObjString,
32
+ buildSketchFromCompileProfilePlan,
32
33
  chamferTrackedEdge,
33
34
  circle2d,
34
35
  collectProjectFiles,
@@ -72,6 +73,8 @@ import {
72
73
  getSketchPlacementModel,
73
74
  getSketchWorkplane,
74
75
  getSolverStats,
76
+ getTruckGeometryWasm,
77
+ getTruckShapeBackendHandle,
75
78
  group,
76
79
  init,
77
80
  initKernel,
@@ -82,6 +85,7 @@ import {
82
85
  intersection,
83
86
  intersection2d,
84
87
  isCompiledBinary,
88
+ isConstraintSketch,
85
89
  isDirectCliRun,
86
90
  isOCCTShapeBackend,
87
91
  jointsView,
@@ -121,7 +125,7 @@ import {
121
125
  union2d,
122
126
  updateConstraintValue,
123
127
  wrapOCCTShapeBackend
124
- } from "./chunk-EXWGNL6K.js";
128
+ } from "./chunk-N4O47JLF.js";
125
129
 
126
130
  // cli/forgecad.ts
127
131
  import { Command, Flags, Help, flush as flushOclif, handle as handleOclif, run as runOclif } from "@oclif/core";
@@ -3469,7 +3473,7 @@ function testCaseLayoutWindingOrder() {
3469
3473
  assertConverged(result, "caseLayoutWinding");
3470
3474
  const def = result.definition;
3471
3475
  const ptMap = new Map(def.points.map((p) => [p.id, p]));
3472
- function signedArea(vertices) {
3476
+ function signedArea2(vertices) {
3473
3477
  const pts = vertices.map((id) => ptMap.get(id)).filter(Boolean);
3474
3478
  let area = 0;
3475
3479
  for (let i = 0; i < pts.length; i++) {
@@ -3492,7 +3496,7 @@ function testCaseLayoutWindingOrder() {
3492
3496
  { name: "wrapper", v: wrapper.vertices }
3493
3497
  ];
3494
3498
  for (const { name, v } of rects) {
3495
- const area = signedArea(v);
3499
+ const area = signedArea2(v);
3496
3500
  assert3(area > 0, `caseLayoutWinding: ${name} has CW winding (signed area=${area.toFixed(2)})`);
3497
3501
  }
3498
3502
  }
@@ -4112,6 +4116,11 @@ var API_EXACT_PART_PATHS = [
4112
4116
  "examples/api/drive-wheel-regions.forge.js"
4113
4117
  ];
4114
4118
  var API_FACETED_PARTS = [
4119
+ {
4120
+ path: "examples/api/route3d-elbow.forge.js",
4121
+ blocker: "The Route3D elbow still uses segmented circle profiles and segmented cylinders in its swept tube and flange cuts, so exact CadQuery/OCCT export remains blocked while runtime backends validate the route primitive.",
4122
+ note: "Route3D itself preserves line/arc route intent; this contract fences the surrounding faceted profile and flange geometry."
4123
+ },
4115
4124
  {
4116
4125
  path: "examples/api/profile-2020-b-slot6.forge.js",
4117
4126
  blocker: "The direct 3D profile helper still lowers through segmented profile geometry, so the extrusion must stay on the faceted route for now.",
@@ -5029,8 +5038,8 @@ function applyNonPartExpectations(entryPath, result, expectations) {
5029
5038
  assertMinimum(entryPath, uniqueGroups, expectations.minUniqueGroups, "named group(s)");
5030
5039
  assertMinimum(entryPath, result.bom.length, expectations.minBomEntries, "BOM entrie(s)");
5031
5040
  assertMinimum(entryPath, result.cutPlanes.length, expectations.minCutPlanes, "cut plane(s)");
5032
- assertMinimum(entryPath, jointCount, expectations.minJoints, "jointsView joint(s)");
5033
- assertMinimum(entryPath, animationCount, expectations.minAnimations, "jointsView animation(s)");
5041
+ assertMinimum(entryPath, jointCount, expectations.minJoints, "joint control(s)");
5042
+ assertMinimum(entryPath, animationCount, expectations.minAnimations, "joint animation(s)");
5034
5043
  if (expectations.requireRobotExport || expectations.minRobotParts != null || expectations.minRobotJoints != null) {
5035
5044
  assert4(result.robotExport, `${entryPath}: expected robotExport(...) data to stay available to the example gate`);
5036
5045
  if (!result.robotExport) return;
@@ -5510,9 +5519,10 @@ function replaceCliInputExtension(path3, newExt) {
5510
5519
  }
5511
5520
  function buildDirectCadImportScript(fileName, kind) {
5512
5521
  const importFn = kind === "step" ? "importStep" : "importMesh";
5522
+ const importOptions = kind === "mesh" && extname(fileName).toLowerCase() === ".3mf" ? ", { separateObjects: true }" : "";
5513
5523
  const objectName = basename3(fileName);
5514
5524
  return [
5515
- `const imported = ${importFn}(${JSON.stringify(fileName)});`,
5525
+ `const imported = ${importFn}(${JSON.stringify(fileName)}${importOptions});`,
5516
5526
  `return [{ name: ${JSON.stringify(objectName)}, shape: imported }];`
5517
5527
  ].join("\n");
5518
5528
  }
@@ -7042,8 +7052,9 @@ var CHROME_PATHS = [
7042
7052
  "/Applications/Chromium.app/Contents/MacOS/Chromium",
7043
7053
  "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
7044
7054
  ];
7045
- var DEFAULT_PORT = parseInt(process.env.FORGE_PORT || "5173", 10);
7055
+ var DEFAULT_PORT = process.env.FORGE_PORT ? parseInt(process.env.FORGE_PORT, 10) : null;
7046
7056
  var DEFAULT_SIZE = parseInt(process.env.FORGE_SIZE || "1024", 10);
7057
+ var RENDERER_PORT_HELP = "Renderer port (default: private ephemeral; FORGE_PORT sets a reusable port)";
7047
7058
  var INSPECT_VIEWS = ["front", "right", "top", "iso"];
7048
7059
  var INSPECT_EVIDENCE_DEFINITIONS = [
7049
7060
  {
@@ -7070,6 +7081,12 @@ var INSPECT_EVIDENCE_DEFINITIONS = [
7070
7081
  summary: "Capture surface normal evidence.",
7071
7082
  description: "Writes view-space normal maps for spotting orientation changes, faceting, and surface direction problems."
7072
7083
  },
7084
+ {
7085
+ name: "rig",
7086
+ channel: "rig",
7087
+ summary: "Capture kinematic rig skeleton evidence.",
7088
+ description: "Writes bright joint, axis, ring, and link overlays with source geometry reduced to a translucent shadow."
7089
+ },
7073
7090
  {
7074
7091
  name: "zebra",
7075
7092
  channel: "zebra",
@@ -7138,7 +7155,8 @@ var INSPECT_EVIDENCE_BY_NAME = new Map(
7138
7155
  INSPECT_EVIDENCE_DEFINITIONS.flatMap((entry) => [[entry.name, entry], ...(entry.aliases ?? []).map((alias) => [alias, entry])])
7139
7156
  );
7140
7157
  var INSPECT_EVIDENCE_BY_CHANNEL = new Map(INSPECT_EVIDENCE_DEFINITIONS.map((entry) => [entry.channel, entry]));
7141
- var RENDER_STYLE_NAMES = ["classic", "studio", "fast", "glass", "inspection", "precision", "hybrid", "scan"];
7158
+ var RENDER_STYLE_NAMES = ["classic", "studio", "fast", "glass", "inspection", "contour", "scan"];
7159
+ var LEGACY_RENDER_STYLE_ALIASES = /* @__PURE__ */ new Map([["precision", "contour"]]);
7142
7160
  var RENDER_STYLES = new Set(RENDER_STYLE_NAMES);
7143
7161
  var RENDER_STYLE_LABEL = RENDER_STYLE_NAMES.join("|");
7144
7162
  var SCAN_GRANULARITY_MIN = 12;
@@ -7180,14 +7198,14 @@ Options:
7180
7198
  --render-mode <solid|wireframe> Render as shaded solid (default) or wireframe only
7181
7199
  --edges <off|thin|bold> Edge overlay preset for solid mode (default: off)
7182
7200
  --backend <manifold|occt|truck> Geometry backend (auto-selects OCCT for direct STEP inputs)
7183
- --port <n> Vite dev server port (default: ${DEFAULT_PORT})
7184
- --fresh-server Start a fresh renderer instead of reusing an existing one
7201
+ --port <n> ${RENDERER_PORT_HELP}
7202
+ --fresh-server Start an isolated renderer instead of reusing the requested port
7185
7203
  --chrome-path <path> Chrome executable path
7186
7204
  -h, --help Show this help
7187
7205
 
7188
7206
  Environment variables:
7189
7207
  FORGE_SIZE=1024
7190
- FORGE_PORT=5173
7208
+ FORGE_PORT=<n>
7191
7209
  CHROME_PATH=/path/to/chrome
7192
7210
 
7193
7211
  Examples:
@@ -7213,6 +7231,7 @@ function splitCsv(value) {
7213
7231
  }
7214
7232
  function parseRenderStyle(value) {
7215
7233
  if (RENDER_STYLES.has(value)) return value;
7234
+ if (LEGACY_RENDER_STYLE_ALIASES.has(value)) return LEGACY_RENDER_STYLE_ALIASES.get(value);
7216
7235
  throw new Error(`--render-style must be one of ${RENDER_STYLE_NAMES.map((entry) => `'${entry}'`).join(", ")} (got '${value}')`);
7217
7236
  }
7218
7237
  function parseEdgesPreset(value) {
@@ -7332,8 +7351,8 @@ function inspectSectionModeUsage(mode) {
7332
7351
  --quality <default|live|high> Mesh/render quality (default: default)
7333
7352
  --backend <manifold|occt|truck> Geometry backend (auto-selects OCCT for direct STEP inputs)
7334
7353
  --force Replace the requested output directory instead of allocating a sibling
7335
- --port <n> Vite dev server port (default: ${DEFAULT_PORT})
7336
- --fresh-server Start a fresh renderer instead of reusing an existing one
7354
+ --port <n> ${RENDERER_PORT_HELP}
7355
+ --fresh-server Start an isolated renderer instead of reusing the requested port
7337
7356
  --chrome-path <path> Chrome executable path
7338
7357
  -h, --help Show this help`;
7339
7358
  const output = `Output:
@@ -7452,6 +7471,7 @@ function parseRenderCliOptions(argv) {
7452
7471
  const cameras = [];
7453
7472
  let size = DEFAULT_SIZE;
7454
7473
  let port = DEFAULT_PORT;
7474
+ let portRequested = DEFAULT_PORT != null;
7455
7475
  let chromePath = process.env.CHROME_PATH;
7456
7476
  let cameraSpec;
7457
7477
  let viewName;
@@ -7525,6 +7545,7 @@ function parseRenderCliOptions(argv) {
7525
7545
  }
7526
7546
  if (arg === "--port") {
7527
7547
  port = parseInt(readValue2(argv, i, arg), 10);
7548
+ portRequested = true;
7528
7549
  i += 1;
7529
7550
  continue;
7530
7551
  }
@@ -7598,7 +7619,7 @@ function parseRenderCliOptions(argv) {
7598
7619
  if (!Number.isFinite(size) || size < 128 || size > 4096) {
7599
7620
  throw new Error(`--size must be between 128 and 4096 (got ${size})`);
7600
7621
  }
7601
- if (!Number.isFinite(port) || port < 1 || port > 65535) {
7622
+ if (port != null && (!Number.isFinite(port) || port < 1 || port > 65535)) {
7602
7623
  throw new Error(`--port must be between 1 and 65535 (got ${port})`);
7603
7624
  }
7604
7625
  if (cameraSpec && cameras.length > 0) {
@@ -7628,6 +7649,7 @@ function parseRenderCliOptions(argv) {
7628
7649
  cameras,
7629
7650
  size,
7630
7651
  port,
7652
+ portRequested,
7631
7653
  chromePath: resolveChromePath(chromePath),
7632
7654
  cameraSpec,
7633
7655
  viewName,
@@ -7677,8 +7699,8 @@ Common options:
7677
7699
  --edges <off|thin|bold> Edge overlay preset for solid visual evidence (default: thin)
7678
7700
  --backend <manifold|occt|truck> Geometry backend (auto-selects OCCT for direct STEP inputs)
7679
7701
  --force Replace the requested output directory instead of allocating a sibling
7680
- --port <n> Vite dev server port (default: ${DEFAULT_PORT})
7681
- --fresh-server Start a fresh renderer instead of reusing an existing one
7702
+ --port <n> ${RENDERER_PORT_HELP}
7703
+ --fresh-server Start an isolated renderer instead of reusing the requested port
7682
7704
  --chrome-path <path> Chrome executable path
7683
7705
  -h, --help Show this help
7684
7706
 
@@ -7692,6 +7714,7 @@ Examples:
7692
7714
  cutaway: "visual cutaway",
7693
7715
  depth: "visual depth",
7694
7716
  normals: "visual normals",
7717
+ rig: "visual rig",
7695
7718
  zebra: "surface zebra",
7696
7719
  roughness: "surface roughness",
7697
7720
  objects: "visual objects",
@@ -7734,8 +7757,8 @@ Options:
7734
7757
  --edges <off|thin|bold> Edge overlay preset for solid visual evidence (default: thin)
7735
7758
  --backend <manifold|occt|truck> Geometry backend (auto-selects OCCT for direct STEP inputs)
7736
7759
  --force Replace the requested output directory instead of allocating a sibling
7737
- --port <n> Vite dev server port (default: ${DEFAULT_PORT})
7738
- --fresh-server Start a fresh renderer instead of reusing an existing one
7760
+ --port <n> ${RENDERER_PORT_HELP}
7761
+ --fresh-server Start an isolated renderer instead of reusing the requested port
7739
7762
  --chrome-path <path> Chrome executable path
7740
7763
  -h, --help Show this help
7741
7764
 
@@ -7776,6 +7799,7 @@ function parseInspectCli(argv, config = {}) {
7776
7799
  const cameras = [];
7777
7800
  let size = DEFAULT_SIZE;
7778
7801
  let port = DEFAULT_PORT;
7802
+ let portRequested = DEFAULT_PORT != null;
7779
7803
  let chromePath = process.env.CHROME_PATH;
7780
7804
  let quality = "default";
7781
7805
  let backend;
@@ -8042,6 +8066,7 @@ function parseInspectCli(argv, config = {}) {
8042
8066
  }
8043
8067
  if (arg === "--port") {
8044
8068
  port = parseInt(readValue2(argv, i, arg), 10);
8069
+ portRequested = true;
8045
8070
  i += 1;
8046
8071
  continue;
8047
8072
  }
@@ -8077,7 +8102,7 @@ function parseInspectCli(argv, config = {}) {
8077
8102
  if (!Number.isFinite(size) || size < 128 || size > 4096) {
8078
8103
  throw new Error(`--size must be between 128 and 4096 (got ${size})`);
8079
8104
  }
8080
- if (!Number.isFinite(port) || port < 1 || port > 65535) {
8105
+ if (port != null && (!Number.isFinite(port) || port < 1 || port > 65535)) {
8081
8106
  throw new Error(`--port must be between 1 and 65535 (got ${port})`);
8082
8107
  }
8083
8108
  if (cameraSpec && cameras.length > 0) {
@@ -8159,6 +8184,7 @@ function parseInspectCli(argv, config = {}) {
8159
8184
  edges,
8160
8185
  size,
8161
8186
  port,
8187
+ portRequested,
8162
8188
  chromePath: resolveChromePath(chromePath),
8163
8189
  quality,
8164
8190
  backend,
@@ -8436,6 +8462,7 @@ function collectInspectViewNames(emittedPaths) {
8436
8462
  add2(emittedPaths.cutaway);
8437
8463
  add2(emittedPaths.depth);
8438
8464
  add2(emittedPaths.normals);
8465
+ add2(emittedPaths.rig);
8439
8466
  add2(emittedPaths.zebra);
8440
8467
  add2(emittedPaths.roughness);
8441
8468
  add2(emittedPaths.mask);
@@ -8576,6 +8603,23 @@ function buildInspectManifest({ options, result, scriptPath, projectRoot, emitte
8576
8603
  views: buildViewPathEntries(emittedPaths.zebra)
8577
8604
  };
8578
8605
  }
8606
+ if (emittedPaths.rig) {
8607
+ evidence.rig = {
8608
+ format: "png",
8609
+ encoding: "rgba-over-dark-shadow-rig-skeleton",
8610
+ coordinateSpace: "world",
8611
+ decode: "source geometry is rendered as a translucent shadow; bright cylinders show hierarchy and part links; oriented rings and axes show joint pivots and motion directions",
8612
+ method: result.rig?.method ?? "rig-skeleton-shadow-v1",
8613
+ objectCount: result.rig?.objectCount ?? 0,
8614
+ jointCount: result.rig?.jointCount ?? 0,
8615
+ linkCount: result.rig?.linkCount ?? 0,
8616
+ hiddenJointCount: result.rig?.hiddenJointCount ?? 0,
8617
+ palette: result.rig?.palette,
8618
+ joints: result.rig?.joints ?? [],
8619
+ links: result.rig?.links ?? [],
8620
+ views: buildViewPathEntries(emittedPaths.rig)
8621
+ };
8622
+ }
8579
8623
  if (emittedPaths.roughness) {
8580
8624
  evidence.roughness = {
8581
8625
  format: "png",
@@ -8854,23 +8898,6 @@ function buildInspectManifest({ options, result, scriptPath, projectRoot, emitte
8854
8898
  evidence
8855
8899
  };
8856
8900
  }
8857
- async function isPortFree(port) {
8858
- return new Promise((resolve40) => {
8859
- const server = createNetServer();
8860
- server.once("error", () => resolve40(false));
8861
- server.once("listening", () => {
8862
- server.close();
8863
- resolve40(true);
8864
- });
8865
- server.listen(port, "127.0.0.1");
8866
- });
8867
- }
8868
- async function findFreePort(startPort) {
8869
- for (let port = startPort; port <= 65535; port += 1) {
8870
- if (await isPortFree(port)) return port;
8871
- }
8872
- return null;
8873
- }
8874
8901
  var viteProcess = null;
8875
8902
  var staticServer = null;
8876
8903
  var STATIC_MIME = {
@@ -8887,6 +8914,29 @@ var STATIC_MIME = {
8887
8914
  };
8888
8915
  var PUPPETEER_PROTOCOL_TIMEOUT_MS = 10 * 60 * 1e3;
8889
8916
  var RENDER_READY_TIMEOUT_MS = 30 * 1e3;
8917
+ var VITE_STARTUP_TIMEOUT_MS = 30 * 1e3;
8918
+ var PRIVATE_RENDER_SERVER_ATTEMPTS = 5;
8919
+ var STARTUP_OUTPUT_LIMIT = 4e3;
8920
+ function sleep(ms) {
8921
+ return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
8922
+ }
8923
+ async function reserveEphemeralPort() {
8924
+ return new Promise((resolvePort, reject) => {
8925
+ const server = createNetServer();
8926
+ server.once("error", reject);
8927
+ server.once("listening", () => {
8928
+ const address = server.address();
8929
+ server.close(() => {
8930
+ if (address && typeof address === "object") {
8931
+ resolvePort(address.port);
8932
+ } else {
8933
+ reject(new Error("Failed to reserve a private renderer port."));
8934
+ }
8935
+ });
8936
+ });
8937
+ server.listen(0, "127.0.0.1");
8938
+ });
8939
+ }
8890
8940
  async function fetchRenderHtml(port) {
8891
8941
  const ctrl = new AbortController();
8892
8942
  const timer = setTimeout(() => ctrl.abort(), 1200);
@@ -8901,6 +8951,18 @@ async function fetchRenderHtml(port) {
8901
8951
  clearTimeout(timer);
8902
8952
  }
8903
8953
  }
8954
+ async function waitForRenderHtml(port, timeoutMs) {
8955
+ const deadline = Date.now() + timeoutMs;
8956
+ while (Date.now() < deadline) {
8957
+ if (await fetchRenderHtml(port)) return true;
8958
+ await sleep(250);
8959
+ }
8960
+ return false;
8961
+ }
8962
+ function isAddressInUse(value) {
8963
+ const message = value instanceof Error ? `${value.message} ${value.code ?? ""}` : String(value ?? "");
8964
+ return /EADDRINUSE|address already in use|port \d+ is already in use/i.test(message);
8965
+ }
8904
8966
  function serveStaticFile(distDir, req, res) {
8905
8967
  const root = resolve8(distDir);
8906
8968
  const requestUrl = new URL(req.url || "/", "http://localhost");
@@ -8930,58 +8992,147 @@ async function startStaticRenderServer(port) {
8930
8992
  const server = createHttpServer((req, res) => serveStaticFile(distDir, req, res));
8931
8993
  await new Promise((resolveListen, reject) => {
8932
8994
  server.once("error", reject);
8933
- server.listen(port, "127.0.0.1", resolveListen);
8995
+ server.listen(port ?? 0, "127.0.0.1", resolveListen);
8934
8996
  });
8997
+ const address = server.address();
8998
+ if (!address || typeof address === "string") {
8999
+ server.close();
9000
+ throw new Error("Packaged render server started without a TCP port.");
9001
+ }
8935
9002
  staticServer = server;
9003
+ return address.port;
8936
9004
  }
8937
- async function ensureDevServer(port, { fresh = false } = {}) {
8938
- let activePort = port;
8939
- if (!fresh && await fetchRenderHtml(activePort)) {
8940
- console.log(`Reusing existing ForgeCAD render server on :${activePort}.`);
8941
- return activePort;
9005
+ async function startViteRenderServer(port, { allowReuseAfterConflict = false } = {}) {
9006
+ const packageRoot = packageRootFrom(import.meta.url);
9007
+ const proc = spawnPackageVite(import.meta.url, ["--host", "127.0.0.1", "--port", String(port), "--strictPort"], {
9008
+ cwd: packageRoot,
9009
+ stdio: "pipe",
9010
+ detached: false
9011
+ });
9012
+ let startupOutput = "";
9013
+ let exitedEarly = false;
9014
+ let exitCode = null;
9015
+ let exitSignal = null;
9016
+ let spawnError = null;
9017
+ const captureOutput = (chunk) => {
9018
+ startupOutput += String(chunk);
9019
+ if (startupOutput.length > STARTUP_OUTPUT_LIMIT) {
9020
+ startupOutput = startupOutput.slice(-STARTUP_OUTPUT_LIMIT);
9021
+ }
9022
+ };
9023
+ proc.stdout?.on("data", captureOutput);
9024
+ proc.stderr?.on("data", captureOutput);
9025
+ proc.once("error", (error) => {
9026
+ spawnError = error;
9027
+ captureOutput(`
9028
+ ${error.message}`);
9029
+ });
9030
+ proc.once("exit", (code, signal) => {
9031
+ exitedEarly = true;
9032
+ exitCode = code;
9033
+ exitSignal = signal;
9034
+ });
9035
+ const deadline = Date.now() + VITE_STARTUP_TIMEOUT_MS;
9036
+ while (Date.now() < deadline) {
9037
+ if (await fetchRenderHtml(port)) {
9038
+ return { ok: true, proc, startupOutput };
9039
+ }
9040
+ if (spawnError || exitedEarly) {
9041
+ if (allowReuseAfterConflict && isAddressInUse(startupOutput) && await waitForRenderHtml(port, 5e3)) {
9042
+ return { ok: true, proc: null, startupOutput, reusedAfterConflict: true };
9043
+ }
9044
+ return { ok: false, proc: null, startupOutput, spawnError, exitedEarly: true, exitCode, exitSignal };
9045
+ }
9046
+ await sleep(250);
9047
+ }
9048
+ proc.kill();
9049
+ return { ok: false, proc: null, startupOutput, timedOut: true };
9050
+ }
9051
+ function viteStartupError(port, result) {
9052
+ const detail = result.startupOutput.trim();
9053
+ const suffix = detail ? `
9054
+ ${detail}` : "";
9055
+ if (result.spawnError) {
9056
+ return new Error(`Failed to start Vite render server on :${port}: ${result.spawnError.message}${suffix}`);
9057
+ }
9058
+ if (result.timedOut) {
9059
+ return new Error(`Timed out waiting for ForgeCAD render server on :${port}.${suffix}`);
9060
+ }
9061
+ if (result.exitedEarly) {
9062
+ const code = result.exitCode != null ? `code ${result.exitCode}` : result.exitSignal ? `signal ${result.exitSignal}` : "unknown exit";
9063
+ return new Error(`Vite render server on :${port} exited before becoming ready (${code}).${suffix}`);
9064
+ }
9065
+ return new Error(`Failed to start ForgeCAD render server on :${port}.${suffix}`);
9066
+ }
9067
+ async function startPrivateViteRenderServer() {
9068
+ let lastError = null;
9069
+ for (let attempt = 1; attempt <= PRIVATE_RENDER_SERVER_ATTEMPTS; attempt += 1) {
9070
+ const port = await reserveEphemeralPort();
9071
+ console.log(`Starting private ForgeCAD render server on :${port} ...`);
9072
+ const result = await startViteRenderServer(port);
9073
+ if (result.ok) {
9074
+ viteProcess = result.proc;
9075
+ return port;
9076
+ }
9077
+ const error = viteStartupError(port, result);
9078
+ if (isAddressInUse(error)) {
9079
+ lastError = error;
9080
+ continue;
9081
+ }
9082
+ throw error;
9083
+ }
9084
+ throw lastError ?? new Error("Failed to start a private ForgeCAD render server.");
9085
+ }
9086
+ async function startConfiguredViteRenderServer(port, { fresh = false, allowReuse = false } = {}) {
9087
+ if (allowReuse && !fresh && await fetchRenderHtml(port)) {
9088
+ console.log(`Reusing existing ForgeCAD render server on :${port}.`);
9089
+ return port;
8942
9090
  }
8943
- const portFree = await isPortFree(activePort);
8944
- if (!portFree) {
8945
- if (!fresh) {
8946
- throw new Error(`Port ${activePort} is already in use, but it is not serving the ForgeCAD renderer.`);
9091
+ console.log(`${fresh ? "Starting fresh" : "Starting"} ForgeCAD render server on :${port} ...`);
9092
+ const result = await startViteRenderServer(port, { allowReuseAfterConflict: allowReuse && !fresh });
9093
+ if (result.ok) {
9094
+ if (result.reusedAfterConflict) {
9095
+ console.log(`Reusing ForgeCAD render server that became ready on :${port}.`);
8947
9096
  }
8948
- const fallbackPort = await findFreePort(activePort + 1);
8949
- if (fallbackPort == null) {
8950
- throw new Error(`Port ${activePort} is occupied and no free fallback port was found.`);
9097
+ viteProcess = result.proc;
9098
+ return port;
9099
+ }
9100
+ if (fresh && isAddressInUse(result.startupOutput)) {
9101
+ console.log(`Port ${port} is occupied; starting a private ForgeCAD render server instead.`);
9102
+ return startPrivateViteRenderServer();
9103
+ }
9104
+ throw viteStartupError(port, result);
9105
+ }
9106
+ async function startPackagedRenderServer(port, { fresh = false, allowReuse = false } = {}) {
9107
+ if (port != null && allowReuse && !fresh && await fetchRenderHtml(port)) {
9108
+ console.log(`Reusing existing ForgeCAD render server on :${port}.`);
9109
+ return port;
9110
+ }
9111
+ try {
9112
+ const activePort = await startStaticRenderServer(port);
9113
+ console.log(`Starting packaged ForgeCAD render server on :${activePort}.`);
9114
+ return activePort;
9115
+ } catch (error) {
9116
+ if (port != null && fresh && isAddressInUse(error)) {
9117
+ console.log(`Port ${port} is occupied; starting a private packaged ForgeCAD render server instead.`);
9118
+ const activePort = await startStaticRenderServer(null);
9119
+ console.log(`Starting packaged ForgeCAD render server on :${activePort}.`);
9120
+ return activePort;
8951
9121
  }
8952
- console.log(`Port ${activePort} is occupied; starting fresh ForgeCAD render server on :${fallbackPort}.`);
8953
- activePort = fallbackPort;
8954
- } else if (fresh) {
8955
- console.log(`Starting fresh ForgeCAD render server on :${activePort}.`);
9122
+ throw error;
8956
9123
  }
9124
+ }
9125
+ async function ensureDevServer(port, { fresh = false, allowReuse = false } = {}) {
8957
9126
  const packageRoot = packageRootFrom(import.meta.url);
8958
9127
  const sourceRenderHtml = join4(packageRoot, "cli", "render.html");
8959
9128
  const packagedRenderHtml = join4(packageRoot, "dist", "cli", "render.html");
8960
9129
  if (!existsSync3(sourceRenderHtml) && existsSync3(packagedRenderHtml)) {
8961
- console.log("Starting packaged render server...");
8962
- await startStaticRenderServer(activePort);
8963
- return activePort;
9130
+ return startPackagedRenderServer(port, { fresh, allowReuse });
8964
9131
  }
8965
- console.log("Starting Vite dev server...");
8966
- viteProcess = spawnPackageVite(import.meta.url, ["--host", "127.0.0.1", "--port", String(activePort), "--strictPort"], {
8967
- cwd: packageRoot,
8968
- stdio: "pipe",
8969
- detached: false
8970
- });
8971
- await new Promise((resolve40, reject) => {
8972
- const timeout = setTimeout(() => reject(new Error("Vite startup timeout")), 15e3);
8973
- viteProcess.stdout.on("data", (data) => {
8974
- if (data.toString().includes("ready")) {
8975
- clearTimeout(timeout);
8976
- resolve40();
8977
- }
8978
- });
8979
- viteProcess.on("error", (e) => {
8980
- clearTimeout(timeout);
8981
- reject(e);
8982
- });
8983
- });
8984
- return activePort;
9132
+ if (port == null) {
9133
+ return startPrivateViteRenderServer();
9134
+ }
9135
+ return startConfiguredViteRenderServer(port, { fresh, allowReuse });
8985
9136
  }
8986
9137
  function stopDevServer() {
8987
9138
  if (viteProcess) {
@@ -9014,7 +9165,10 @@ async function runRenderCli(argv = process.argv.slice(2)) {
9014
9165
  const binaryFiles = collectBrowserBinaryFiles(input);
9015
9166
  const needsBinaryFileReader = Object.keys(binaryFiles).length > 0;
9016
9167
  const activeBackend = resolveCliBackend(options.backend, input) ?? CLI_DEFAULT_BACKEND;
9017
- const activePort = await ensureDevServer(options.port, { fresh: options.freshServer || needsBinaryFileReader });
9168
+ const activePort = await ensureDevServer(options.port, {
9169
+ fresh: options.freshServer || needsBinaryFileReader,
9170
+ allowReuse: options.portRequested && !needsBinaryFileReader
9171
+ });
9018
9172
  const browser = await puppeteer.launch({
9019
9173
  headless: true,
9020
9174
  protocolTimeout: PUPPETEER_PROTOCOL_TIMEOUT_MS,
@@ -9149,7 +9303,10 @@ async function runInspectBundleCli(argv, config) {
9149
9303
  console.log(`[inspect] Preparing bundle directory: ${resolve8(options.outputDir)}`);
9150
9304
  const bundleDirs = await prepareInspectBundleDir(options.outputDir, options.force, options.channels);
9151
9305
  tempDir = bundleDirs.tempDir;
9152
- const activePort = await ensureDevServer(options.port, { fresh: options.freshServer || needsBinaryFileReader });
9306
+ const activePort = await ensureDevServer(options.port, {
9307
+ fresh: options.freshServer || needsBinaryFileReader,
9308
+ allowReuse: options.portRequested && !needsBinaryFileReader
9309
+ });
9153
9310
  const browser = await puppeteer.launch({
9154
9311
  headless: true,
9155
9312
  protocolTimeout: PUPPETEER_PROTOCOL_TIMEOUT_MS,
@@ -9316,6 +9473,9 @@ async function runInspectBundleCli(argv, config) {
9316
9473
  if (requested.has("zebra")) {
9317
9474
  await writeViewChannel("zebra", result.zebra);
9318
9475
  }
9476
+ if (requested.has("rig")) {
9477
+ await writeViewChannel("rig", result.rig?.views);
9478
+ }
9319
9479
  if (requested.has("roughness")) {
9320
9480
  await writeViewChannel("roughness", result.roughness?.views);
9321
9481
  emittedPaths.roughnessPointCloud = await writeChannelJson("roughness", "point-cloud.json", result.roughness?.pointCloud, {
@@ -9898,11 +10058,15 @@ function parseTraceQueryFlags(argv) {
9898
10058
  consumed.add(i);
9899
10059
  const raw = argv[i + 1];
9900
10060
  if (!raw || raw.startsWith("-")) {
9901
- console.error("Missing value for --query. Expected a trace lens such as feature:hole, source:main.forge.js:42, trace:<id>, or cache:hits.");
10061
+ console.error(
10062
+ "Missing value for --query. Expected a trace lens such as feature:hole, source:main.forge.js:42, trace:<id>, or cache:hits."
10063
+ );
9902
10064
  process.exit(1);
9903
10065
  }
9904
10066
  consumed.add(i + 1);
9905
- queries.push(...raw.split(",").map((query) => query.trim()).filter(Boolean));
10067
+ queries.push(
10068
+ ...raw.split(",").map((query) => query.trim()).filter(Boolean)
10069
+ );
9906
10070
  i += 1;
9907
10071
  }
9908
10072
  return { queries, consumed };
@@ -9918,7 +10082,7 @@ async function runInspectDesignTraceCli(argv = process.argv.slice(2)) {
9918
10082
  argv.forEach((arg, index) => {
9919
10083
  if (arg === "--anchor" || arg === "--full") consumed.add(index);
9920
10084
  });
9921
- const positional = argv.filter((arg, index) => !consumed.has(index));
10085
+ const positional = argv.filter((_arg, index) => !consumed.has(index));
9922
10086
  const scriptPath = positional[0];
9923
10087
  if (!scriptPath) usage3();
9924
10088
  if (positional.length > 1) {
@@ -9965,7 +10129,8 @@ function cacheNote(node) {
9965
10129
  if (node.liveCacheStatus === "rebuilt") return `cache rebuilt in ${node.liveCacheBuildMs ?? 0}ms`;
9966
10130
  if (node.liveCacheStatus === "miss") return "cache miss";
9967
10131
  if (node.cacheRole === "reuseCandidate") return `structural reuse candidate, saved estimate ${node.cacheOpportunitySavedMs}ms`;
9968
- if (node.cacheRole === "seed" && node.duplicateGeometryCount > 1) return `repeated geometry seed, ${node.duplicateGeometryCount} equivalent nodes`;
10132
+ if (node.cacheRole === "seed" && node.duplicateGeometryCount > 1)
10133
+ return `repeated geometry seed, ${node.duplicateGeometryCount} equivalent nodes`;
9969
10134
  return void 0;
9970
10135
  }
9971
10136
  function toHistoryStep(trace, node) {
@@ -10002,7 +10167,9 @@ function repeatedGeometryGroups(nodes) {
10002
10167
  function projectDesignHistory(trace, options = {}) {
10003
10168
  const objectQueries = (options.objectQueries ?? []).map(normalize).filter(Boolean);
10004
10169
  const nodeQueries = (options.nodeQueries ?? []).map(normalize).filter(Boolean);
10005
- const matchingNodeIds = new Set((nodeQueries.length > 0 ? filterDesignTraceNodes(trace, nodeQueries) : trace.nodes).map((node) => node.id));
10170
+ const matchingNodeIds = new Set(
10171
+ (nodeQueries.length > 0 ? filterDesignTraceNodes(trace, nodeQueries) : trace.nodes).map((node) => node.id)
10172
+ );
10006
10173
  const nodesByObjectId = /* @__PURE__ */ new Map();
10007
10174
  for (const node of trace.nodes) {
10008
10175
  const objectNodes = nodesByObjectId.get(node.objectId) ?? [];
@@ -10075,7 +10242,9 @@ function printStep(step, detailed, options) {
10075
10242
  if (summary) console.log(` uses: ${summary}`);
10076
10243
  return;
10077
10244
  }
10078
- console.log(` inputs: ${step.inputRefs.length > 0 ? step.inputRefs.map((ref) => formatStepRef(ref, Boolean(options.printTraceIds))).join("; ") : "none"}`);
10245
+ console.log(
10246
+ ` inputs: ${step.inputRefs.length > 0 ? step.inputRefs.map((ref) => formatStepRef(ref, Boolean(options.printTraceIds))).join("; ") : "none"}`
10247
+ );
10079
10248
  console.log(
10080
10249
  ` used by: ${step.usedByRefs.length > 0 ? step.usedByRefs.map((ref) => formatStepRef(ref, Boolean(options.printTraceIds))).join("; ") : "none"}`
10081
10250
  );
@@ -10131,8 +10300,10 @@ Object recipe: ${pathLabel(object.path, object.name)}`);
10131
10300
  } else if (selectedTraceIds.length === 0) {
10132
10301
  console.log("\nHistory feedback anchor: no matching feature step.");
10133
10302
  } else {
10134
- console.log(`
10135
- History feedback anchor: ${selectedTraceIds.length} feature steps matched; narrow --query to one feature/source/trace node.`);
10303
+ console.log(
10304
+ `
10305
+ History feedback anchor: ${selectedTraceIds.length} feature steps matched; narrow --query to one feature/source/trace node.`
10306
+ );
10136
10307
  }
10137
10308
  }
10138
10309
 
@@ -10155,7 +10326,9 @@ function parseListFlag(argv, flag, missingHelp) {
10155
10326
  process.exit(1);
10156
10327
  }
10157
10328
  consumed.add(i + 1);
10158
- values.push(...raw.split(",").map((value) => value.trim()).filter(Boolean));
10329
+ values.push(
10330
+ ...raw.split(",").map((value) => value.trim()).filter(Boolean)
10331
+ );
10159
10332
  i += 1;
10160
10333
  }
10161
10334
  return { values, consumed };
@@ -10184,7 +10357,14 @@ async function runInspectHistoryCli(argv = process.argv.slice(2)) {
10184
10357
  "--query",
10185
10358
  "Missing value for --query. Expected feature:hole, source:main.forge.js:42, trace:<id>, cache:hits, or text."
10186
10359
  );
10187
- const consumed = /* @__PURE__ */ new Set([...paramConsumed, ...backendConsumed, ...qualityConsumed, ...levelConsumed, ...objectConsumed, ...queryConsumed]);
10360
+ const consumed = /* @__PURE__ */ new Set([
10361
+ ...paramConsumed,
10362
+ ...backendConsumed,
10363
+ ...qualityConsumed,
10364
+ ...levelConsumed,
10365
+ ...objectConsumed,
10366
+ ...queryConsumed
10367
+ ]);
10188
10368
  const printAnchor = argv.includes("--anchor");
10189
10369
  const printSource = argv.includes("--source");
10190
10370
  const printTraceIds = argv.includes("--trace-ids");
@@ -10192,7 +10372,7 @@ async function runInspectHistoryCli(argv = process.argv.slice(2)) {
10192
10372
  argv.forEach((arg, index) => {
10193
10373
  if (arg === "--anchor" || arg === "--source" || arg === "--trace-ids" || arg === "--params") consumed.add(index);
10194
10374
  });
10195
- const positional = argv.filter((arg, index) => !consumed.has(index));
10375
+ const positional = argv.filter((_arg, index) => !consumed.has(index));
10196
10376
  const scriptPath = positional[0];
10197
10377
  if (!scriptPath) usage4();
10198
10378
  if (positional.length > 1) {
@@ -10772,7 +10952,7 @@ function auditAssembly(assembly2, rawOptions = {}) {
10772
10952
  continue;
10773
10953
  }
10774
10954
  try {
10775
- const frames = assembly2.sweepJoint(joint2.name, joint2.min, joint2.max, options.sweepSteps, options.state, {
10955
+ const frames = assembly2._sweepJointForDiagnostics(joint2.name, joint2.min, joint2.max, options.sweepSteps, options.state, {
10776
10956
  minOverlapVolume: options.minOverlapVolume
10777
10957
  });
10778
10958
  for (const frame of frames) {
@@ -13452,6 +13632,733 @@ async function runInspectReplayCli(argv = process.argv.slice(2)) {
13452
13632
  printSectionSummary(result);
13453
13633
  }
13454
13634
 
13635
+ // src/forge/inspection/sketch.ts
13636
+ var EPS5 = 1e-9;
13637
+ var BOUNDARY_EPS = 1e-7;
13638
+ function inspectSketches(objects, options = {}) {
13639
+ const targets = [];
13640
+ const diagnostics = [];
13641
+ for (const object of objects) {
13642
+ if (options.objectIds && !options.objectIds.has(object.id)) continue;
13643
+ const objectPath = object.treePath && object.treePath.length > 0 ? object.treePath.join("/") : object.name;
13644
+ if (object.sketch) {
13645
+ targets.push(
13646
+ inspectSketchTarget(object.sketch, {
13647
+ id: `${object.id}:sketch`,
13648
+ object,
13649
+ objectPath,
13650
+ sourceKind: "returned-sketch",
13651
+ options
13652
+ })
13653
+ );
13654
+ }
13655
+ if (object.shape) {
13656
+ const shapePlan = getShapeCompilePlan(object.shape);
13657
+ const profileUses = collectProfileUses(shapePlan);
13658
+ profileUses.forEach((use, index) => {
13659
+ targets.push(
13660
+ inspectProfileTarget(use.profile, {
13661
+ id: `${object.id}:profile:${index}`,
13662
+ object,
13663
+ objectPath,
13664
+ operation: use.operation,
13665
+ profilePath: use.path,
13666
+ options
13667
+ })
13668
+ );
13669
+ });
13670
+ }
13671
+ }
13672
+ if (targets.length === 0) {
13673
+ diagnostics.push({
13674
+ level: "info",
13675
+ code: "no-sketch-targets",
13676
+ message: "No returned sketches or supported profile-bearing shape operations were found."
13677
+ });
13678
+ }
13679
+ return { targets, diagnostics };
13680
+ }
13681
+ function inspectSketchTarget(sketch, args) {
13682
+ const diagnostics = [];
13683
+ const regions = extractRegionsForSketch(sketch, diagnostics);
13684
+ const bounds2 = readSketchBounds(sketch, diagnostics);
13685
+ const area = readSketchArea(sketch, diagnostics);
13686
+ addSketchHealthDiagnostics(sketch, regions, diagnostics);
13687
+ const target = {
13688
+ id: args.id,
13689
+ objectId: args.object.id,
13690
+ objectName: args.object.name,
13691
+ objectPath: args.objectPath,
13692
+ sourceKind: args.sourceKind,
13693
+ bounds: bounds2,
13694
+ area,
13695
+ isEmpty: area === null ? regions.length === 0 : Math.abs(area) <= EPS5,
13696
+ regions,
13697
+ diagnostics
13698
+ };
13699
+ if (isConstraintSketch(sketch)) {
13700
+ target.constraintStatus = sketch.constraintMeta.status;
13701
+ target.constraintDof = sketch.constraintMeta.dof;
13702
+ target.constraintMaxError = sketch.constraintMeta.maxError;
13703
+ }
13704
+ if (args.options.seed) {
13705
+ target.selection = selectRegion(regions, args.options.seed, args.options.operation);
13706
+ }
13707
+ return target;
13708
+ }
13709
+ function inspectProfileTarget(plan, args) {
13710
+ const diagnostics = [];
13711
+ const targetBase = {
13712
+ id: args.id,
13713
+ objectId: args.object.id,
13714
+ objectName: args.object.name,
13715
+ objectPath: args.objectPath,
13716
+ sourceKind: "shape-profile",
13717
+ operation: args.operation,
13718
+ profilePath: args.profilePath,
13719
+ profileTree: summarizeProfilePlan(plan, "$")
13720
+ };
13721
+ try {
13722
+ const sketch = buildSketchFromCompileProfilePlan(plan);
13723
+ const target = inspectSketchTarget(sketch, {
13724
+ id: args.id,
13725
+ object: args.object,
13726
+ objectPath: args.objectPath,
13727
+ sourceKind: "shape-profile",
13728
+ options: args.options
13729
+ });
13730
+ return {
13731
+ ...target,
13732
+ ...targetBase,
13733
+ diagnostics: [...target.diagnostics, ...diagnostics]
13734
+ };
13735
+ } catch (error) {
13736
+ diagnostics.push({
13737
+ level: "error",
13738
+ code: "profile-lowering-failed",
13739
+ message: error instanceof Error ? error.message : String(error)
13740
+ });
13741
+ return {
13742
+ ...targetBase,
13743
+ bounds: null,
13744
+ area: null,
13745
+ isEmpty: true,
13746
+ regions: [],
13747
+ diagnostics,
13748
+ ...args.options.seed ? { selection: selectRegion([], args.options.seed, args.options.operation) } : {}
13749
+ };
13750
+ }
13751
+ }
13752
+ function addSketchHealthDiagnostics(sketch, regions, diagnostics) {
13753
+ if (regions.length === 0) {
13754
+ diagnostics.push({
13755
+ level: "warning",
13756
+ code: "no-regions",
13757
+ message: "No filled sketch regions were detected."
13758
+ });
13759
+ }
13760
+ if (!isConstraintSketch(sketch)) return;
13761
+ const meta = sketch.constraintMeta;
13762
+ if (meta.status === "under") {
13763
+ diagnostics.push({
13764
+ level: "warning",
13765
+ code: "under-constrained",
13766
+ message: `Constraint sketch is under-constrained with ${meta.dof} degree(s) of freedom.`
13767
+ });
13768
+ } else if (meta.status === "over" || meta.status === "over-redundant") {
13769
+ diagnostics.push({
13770
+ level: "warning",
13771
+ code: meta.status === "over" ? "over-constrained" : "over-redundant",
13772
+ message: `Constraint sketch status is ${meta.status}.`
13773
+ });
13774
+ }
13775
+ if (meta.maxError > 1e-6) {
13776
+ diagnostics.push({
13777
+ level: "warning",
13778
+ code: "constraint-residual",
13779
+ message: `Constraint max error is ${meta.maxError}.`
13780
+ });
13781
+ }
13782
+ }
13783
+ function extractRegionsForSketch(sketch, diagnostics) {
13784
+ if (isConstraintSketch(sketch) && sketch.constraintMeta.surfaces.length > 0) {
13785
+ return sketch.constraintMeta.surfaces.map((surface, index) => ({
13786
+ id: `R${index}`,
13787
+ area: surface.area,
13788
+ bounds: { min: [...surface.bounds.min], max: [...surface.bounds.max] },
13789
+ centroid: [...surface.centroid],
13790
+ seed: [...surface.seed],
13791
+ outer: surface.polygon.map(cloneVec2),
13792
+ holes: [],
13793
+ compatibleOperations: { extrude: surface.area > EPS5 }
13794
+ }));
13795
+ }
13796
+ try {
13797
+ return extractFilledRegions(sketch.toPolygons()).map((region, index) => ({
13798
+ id: `R${index}`,
13799
+ ...region,
13800
+ compatibleOperations: { extrude: region.area > EPS5 }
13801
+ }));
13802
+ } catch (error) {
13803
+ diagnostics.push({
13804
+ level: "error",
13805
+ code: "polygon-extraction-failed",
13806
+ message: error instanceof Error ? error.message : String(error)
13807
+ });
13808
+ return [];
13809
+ }
13810
+ }
13811
+ function extractFilledRegions(rawLoops) {
13812
+ const loops = rawLoops.map((loop) => loop.map(([x, y]) => [x, y])).map(removeDuplicateClose).filter((points) => points.length >= 3).map((points) => {
13813
+ const area = signedArea(points);
13814
+ return { points, area, absArea: Math.abs(area), sample: polygonCentroid(points) };
13815
+ }).filter((loop) => loop.absArea > EPS5);
13816
+ if (loops.length === 0) return [];
13817
+ const outers = loops.filter((loop) => loop.area > 0);
13818
+ const holes = loops.filter((loop) => loop.area < 0);
13819
+ const effectiveOuters = outers.length > 0 ? outers : [loops.slice().sort((a, b) => b.absArea - a.absArea)[0]];
13820
+ const regions = effectiveOuters.map((outer) => ({ outer, holes: [] }));
13821
+ for (const hole of holes) {
13822
+ const containers = regions.filter((region) => pointInPolygon(hole.sample, region.outer.points)).sort((a, b) => a.outer.absArea - b.outer.absArea);
13823
+ if (containers[0]) containers[0].holes.push(hole);
13824
+ }
13825
+ return regions.map((region) => {
13826
+ const holePoints = region.holes.map((hole) => hole.points);
13827
+ const area = Math.max(0, region.outer.absArea - region.holes.reduce((sum2, hole) => sum2 + hole.absArea, 0));
13828
+ return {
13829
+ area,
13830
+ bounds: boundsForLoops([region.outer.points, ...holePoints]),
13831
+ centroid: polygonCentroid(region.outer.points),
13832
+ seed: findRegionSeed(region.outer.points, holePoints),
13833
+ outer: region.outer.points.map(cloneVec2),
13834
+ holes: holePoints.map((hole) => hole.map(cloneVec2))
13835
+ };
13836
+ }).filter((region) => region.area > EPS5).sort((a, b) => b.area - a.area);
13837
+ }
13838
+ function selectRegion(regions, seed, operation) {
13839
+ for (const region of regions) {
13840
+ if (pointOnLoopBoundary(seed, region.outer) || region.holes.some((hole) => pointOnLoopBoundary(seed, hole))) {
13841
+ return {
13842
+ seed,
13843
+ ok: false,
13844
+ regionId: null,
13845
+ code: "seed-on-boundary",
13846
+ message: `Seed [${seed[0]}, ${seed[1]}] lies on a region boundary.`,
13847
+ operation,
13848
+ operationCompatible: false
13849
+ };
13850
+ }
13851
+ }
13852
+ const outerMatches = regions.filter((region) => pointInPolygon(seed, region.outer));
13853
+ const holeMatches = outerMatches.filter((region) => region.holes.some((hole) => pointInPolygon(seed, hole)));
13854
+ const matches = outerMatches.filter((region) => !region.holes.some((hole) => pointInPolygon(seed, hole)));
13855
+ if (matches.length === 1) {
13856
+ const compatible = operation ? matches[0].compatibleOperations[operation] === true : void 0;
13857
+ return {
13858
+ seed,
13859
+ ok: operation ? compatible === true : true,
13860
+ regionId: matches[0].id,
13861
+ code: operation && !compatible ? "operation-incompatible" : "matched",
13862
+ message: operation && !compatible ? `Region ${matches[0].id} is not compatible with ${operation}.` : `Seed matched region ${matches[0].id}.`,
13863
+ operation,
13864
+ operationCompatible: compatible
13865
+ };
13866
+ }
13867
+ if (matches.length > 1) {
13868
+ return {
13869
+ seed,
13870
+ ok: false,
13871
+ regionId: null,
13872
+ code: "seed-ambiguous",
13873
+ message: `Seed [${seed[0]}, ${seed[1]}] matched ${matches.length} regions.`,
13874
+ operation,
13875
+ operationCompatible: false
13876
+ };
13877
+ }
13878
+ if (holeMatches.length > 0) {
13879
+ return {
13880
+ seed,
13881
+ ok: false,
13882
+ regionId: null,
13883
+ code: "seed-inside-hole",
13884
+ message: `Seed [${seed[0]}, ${seed[1]}] is inside a region hole.`,
13885
+ operation,
13886
+ operationCompatible: false
13887
+ };
13888
+ }
13889
+ return {
13890
+ seed,
13891
+ ok: false,
13892
+ regionId: null,
13893
+ code: regions.length === 0 ? "no-regions" : "seed-outside-regions",
13894
+ message: regions.length === 0 ? "No regions are available for seed selection." : `Seed [${seed[0]}, ${seed[1]}] is outside all detected regions.`,
13895
+ operation,
13896
+ operationCompatible: false
13897
+ };
13898
+ }
13899
+ function collectProfileUses(plan, path3 = "$") {
13900
+ switch (plan.kind) {
13901
+ case "extrude":
13902
+ return [{ operation: "extrude", profile: plan.profile, path: `${path3}.profile` }];
13903
+ case "cut":
13904
+ return [...collectProfileUses(plan.base, `${path3}.base`), { operation: "cut", profile: plan.profile, path: `${path3}.profile` }];
13905
+ case "revolve":
13906
+ return [{ operation: "revolve", profile: plan.profile, path: `${path3}.profile` }];
13907
+ case "queryOwner":
13908
+ return collectProfileUses(plan.base, `${path3}.base`);
13909
+ case "transform":
13910
+ return collectProfileUses(plan.base, `${path3}.base`);
13911
+ case "boolean":
13912
+ return plan.shapes.flatMap((shape, index) => collectProfileUses(shape, `${path3}.shapes[${index}]`));
13913
+ case "shell":
13914
+ case "hole":
13915
+ case "trimByPlane":
13916
+ case "fillet":
13917
+ case "chamfer":
13918
+ case "filletEdges":
13919
+ case "cornerYBlend":
13920
+ case "surfaceExtend":
13921
+ case "surfaceThicken":
13922
+ case "surfaceSolid":
13923
+ return collectProfileUses(plan.base, `${path3}.base`);
13924
+ case "surfaceSew":
13925
+ return plan.shapes.flatMap((shape, index) => collectProfileUses(shape, `${path3}.shapes[${index}]`));
13926
+ default:
13927
+ return [];
13928
+ }
13929
+ }
13930
+ function summarizeProfilePlan(plan, path3) {
13931
+ switch (plan.kind) {
13932
+ case "boolean":
13933
+ return {
13934
+ path: path3,
13935
+ kind: plan.kind,
13936
+ op: plan.op,
13937
+ children: plan.profiles.map((profile, index) => summarizeProfilePlan(profile, `${path3}.profiles[${index}]`))
13938
+ };
13939
+ case "offset":
13940
+ return { path: path3, kind: plan.kind, children: [summarizeProfilePlan(plan.base, `${path3}.base`)] };
13941
+ case "project":
13942
+ return {
13943
+ path: path3,
13944
+ kind: plan.kind,
13945
+ children: plan.replayProfile ? [summarizeProfilePlan(plan.replayProfile, `${path3}.replayProfile`)] : []
13946
+ };
13947
+ default:
13948
+ return { path: path3, kind: plan.kind, children: [] };
13949
+ }
13950
+ }
13951
+ function readSketchBounds(sketch, diagnostics) {
13952
+ try {
13953
+ const bounds2 = sketch.bounds();
13954
+ return { min: [bounds2.min[0], bounds2.min[1]], max: [bounds2.max[0], bounds2.max[1]] };
13955
+ } catch (error) {
13956
+ diagnostics.push({
13957
+ level: "error",
13958
+ code: "bounds-failed",
13959
+ message: error instanceof Error ? error.message : String(error)
13960
+ });
13961
+ return null;
13962
+ }
13963
+ }
13964
+ function readSketchArea(sketch, diagnostics) {
13965
+ try {
13966
+ return sketch.area();
13967
+ } catch (error) {
13968
+ diagnostics.push({
13969
+ level: "error",
13970
+ code: "area-failed",
13971
+ message: error instanceof Error ? error.message : String(error)
13972
+ });
13973
+ return null;
13974
+ }
13975
+ }
13976
+ function findRegionSeed(outer, holes) {
13977
+ const candidates = [polygonCentroid(outer)];
13978
+ for (let i = 0; i < outer.length; i += 1) {
13979
+ const a = outer[i];
13980
+ const b = outer[(i + 1) % outer.length];
13981
+ const dx = b[0] - a[0];
13982
+ const dy = b[1] - a[1];
13983
+ const length3 = Math.hypot(dx, dy);
13984
+ if (length3 <= EPS5) continue;
13985
+ const nudge = Math.min(1, length3 * 0.01);
13986
+ candidates.push([(a[0] + b[0]) / 2 - dy / length3 * nudge, (a[1] + b[1]) / 2 + dx / length3 * nudge]);
13987
+ }
13988
+ return candidates.find((candidate) => pointInFilledRegion(candidate, outer, holes)) ?? candidates[0] ?? [0, 0];
13989
+ }
13990
+ function pointInFilledRegion(point, outer, holes) {
13991
+ return pointInPolygon(point, outer) && !holes.some((hole) => pointInPolygon(point, hole));
13992
+ }
13993
+ function boundsForLoops(loops) {
13994
+ let minX = Infinity;
13995
+ let minY = Infinity;
13996
+ let maxX = -Infinity;
13997
+ let maxY = -Infinity;
13998
+ for (const loop of loops) {
13999
+ for (const [x, y] of loop) {
14000
+ if (x < minX) minX = x;
14001
+ if (y < minY) minY = y;
14002
+ if (x > maxX) maxX = x;
14003
+ if (y > maxY) maxY = y;
14004
+ }
14005
+ }
14006
+ return { min: [minX, minY], max: [maxX, maxY] };
14007
+ }
14008
+ function removeDuplicateClose(points) {
14009
+ if (points.length <= 1) return points;
14010
+ return Math.hypot(points[0][0] - points[points.length - 1][0], points[0][1] - points[points.length - 1][1]) <= BOUNDARY_EPS ? points.slice(0, -1) : points;
14011
+ }
14012
+ function signedArea(points) {
14013
+ let area = 0;
14014
+ for (let i = 0; i < points.length; i += 1) {
14015
+ const [x1, y1] = points[i];
14016
+ const [x2, y2] = points[(i + 1) % points.length];
14017
+ area += x1 * y2 - x2 * y1;
14018
+ }
14019
+ return area * 0.5;
14020
+ }
14021
+ function polygonCentroid(points) {
14022
+ let cx = 0;
14023
+ let cy = 0;
14024
+ let a2 = 0;
14025
+ for (let i = 0; i < points.length; i += 1) {
14026
+ const [x1, y1] = points[i];
14027
+ const [x2, y2] = points[(i + 1) % points.length];
14028
+ const cross6 = x1 * y2 - x2 * y1;
14029
+ a2 += cross6;
14030
+ cx += (x1 + x2) * cross6;
14031
+ cy += (y1 + y2) * cross6;
14032
+ }
14033
+ if (Math.abs(a2) < EPS5) {
14034
+ 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];
14035
+ }
14036
+ const f2 = 1 / (3 * a2);
14037
+ return [cx * f2, cy * f2];
14038
+ }
14039
+ function pointInPolygon(point, polygon) {
14040
+ const [px, py] = point;
14041
+ let inside = false;
14042
+ for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
14043
+ const [xi, yi] = polygon[i];
14044
+ const [xj, yj] = polygon[j];
14045
+ if (yi > py !== yj > py && px < (xj - xi) * (py - yi) / (yj - yi || 1e-20) + xi) inside = !inside;
14046
+ }
14047
+ return inside;
14048
+ }
14049
+ function pointOnLoopBoundary(point, loop) {
14050
+ for (let i = 0; i < loop.length; i += 1) {
14051
+ if (distToSegment(point, loop[i], loop[(i + 1) % loop.length]) <= BOUNDARY_EPS) return true;
14052
+ }
14053
+ return false;
14054
+ }
14055
+ function distToSegment(point, a, b) {
14056
+ const dx = b[0] - a[0];
14057
+ const dy = b[1] - a[1];
14058
+ const len2 = dx * dx + dy * dy;
14059
+ if (len2 <= EPS5) return Math.hypot(point[0] - a[0], point[1] - a[1]);
14060
+ const t = Math.max(0, Math.min(1, ((point[0] - a[0]) * dx + (point[1] - a[1]) * dy) / len2));
14061
+ return Math.hypot(point[0] - (a[0] + t * dx), point[1] - (a[1] + t * dy));
14062
+ }
14063
+ function cloneVec2(point) {
14064
+ return [point[0], point[1]];
14065
+ }
14066
+
14067
+ // src/forge/targets.ts
14068
+ var cleanSceneTargetPathSegments = (segments) => (segments ?? []).map((segment) => segment.trim()).filter((segment) => segment.length > 0);
14069
+ function getSceneObjectTreePath(object) {
14070
+ const explicitTreePath = cleanSceneTargetPathSegments(object.treePath);
14071
+ if (explicitTreePath.length > 0) return explicitTreePath;
14072
+ const name = object.name.trim() || object.id;
14073
+ const groupName = object.groupName?.trim();
14074
+ if (!groupName) return [name];
14075
+ const groupPath = groupName.split(".").map((segment) => segment.trim()).filter((segment) => segment.length > 0);
14076
+ const prefixedLeaf = `${groupName}.`;
14077
+ if (name.startsWith(prefixedLeaf)) {
14078
+ const leafName = name.slice(prefixedLeaf.length).trim();
14079
+ return [...groupPath, leafName || name];
14080
+ }
14081
+ return [...groupPath, name];
14082
+ }
14083
+ function getSceneObjectKind(object) {
14084
+ if (object.mock) return "mock";
14085
+ if (object.sketch) return "sketch";
14086
+ if (object.toolpath) return "toolpath";
14087
+ if (object.sdf) return "sdf";
14088
+ if (object.shape) return "shape";
14089
+ return "object";
14090
+ }
14091
+ function buildSceneTargetEntries(objects) {
14092
+ return objects.map((object) => {
14093
+ const pathSegments = getSceneObjectTreePath(object);
14094
+ return {
14095
+ id: object.id,
14096
+ name: object.name,
14097
+ kind: getSceneObjectKind(object),
14098
+ pathSegments,
14099
+ path: pathSegments.join("/"),
14100
+ dottedPath: pathSegments.join("."),
14101
+ group: object.groupName?.trim() || void 0,
14102
+ tags: object.tags ?? [],
14103
+ mock: object.mock === true,
14104
+ object
14105
+ };
14106
+ });
14107
+ }
14108
+ function formatSceneTargetPath(entry) {
14109
+ return entry.path || entry.name;
14110
+ }
14111
+ function resolveSceneTargets(entries, selector) {
14112
+ const needle = normalizeTargetText(selector);
14113
+ if (!needle) throw new Error("Target selector must not be empty.");
14114
+ const hasGlob = /[*?]/.test(selector);
14115
+ if (hasGlob) {
14116
+ const pattern = globToRegExp(needle);
14117
+ const matches = entries.filter((entry) => targetCandidateTexts(entry).some((candidate) => pattern.test(candidate)));
14118
+ if (matches.length > 0) return matches;
14119
+ throw targetNotFound(selector, entries);
14120
+ }
14121
+ const idMatches = entries.filter((entry) => normalizeTargetText(entry.id) === needle);
14122
+ if (idMatches.length > 0) return idMatches;
14123
+ const exactPathMatches = entries.filter(
14124
+ (entry) => normalizeTargetText(entry.path) === needle || normalizeTargetText(entry.dottedPath) === needle
14125
+ );
14126
+ if (exactPathMatches.length > 0) return exactPathMatches;
14127
+ const groupMatches = entries.filter((entry) => {
14128
+ const groupPath = entry.pathSegments.slice(0, -1);
14129
+ return normalizeTargetText(groupPath.join("/")) === needle || normalizeTargetText(groupPath.join(".")) === needle;
14130
+ });
14131
+ if (groupMatches.length > 0) return groupMatches;
14132
+ const nameMatches2 = entries.filter(
14133
+ (entry) => normalizeTargetText(entry.name) === needle || normalizeTargetText(entry.pathSegments[entry.pathSegments.length - 1] ?? "") === needle
14134
+ );
14135
+ if (nameMatches2.length === 1) return nameMatches2;
14136
+ if (nameMatches2.length > 1) throw ambiguousTarget(selector, nameMatches2);
14137
+ throw targetNotFound(selector, entries);
14138
+ }
14139
+ function suggestSceneTargets(selector, entries, limit = 8) {
14140
+ const needle = normalizeTargetText(selector);
14141
+ if (!needle) return entries.slice(0, limit);
14142
+ return entries.map((entry) => ({ entry, score: scoreTarget(entry, needle) })).filter((ranked) => ranked.score > 0).sort((a, b) => b.score - a.score || a.entry.path.localeCompare(b.entry.path)).slice(0, limit).map((ranked) => ranked.entry);
14143
+ }
14144
+ function targetCandidateTexts(entry) {
14145
+ return [
14146
+ entry.id,
14147
+ entry.name,
14148
+ entry.path,
14149
+ entry.dottedPath,
14150
+ entry.group ?? "",
14151
+ entry.pathSegments[entry.pathSegments.length - 1] ?? ""
14152
+ ].map(normalizeTargetText);
14153
+ }
14154
+ function normalizeTargetText(value) {
14155
+ return value.trim().replace(/\\/g, "/").replace(/\s*\/\s*/g, "/").replace(/\s*\.\s*/g, ".").toLowerCase();
14156
+ }
14157
+ function globToRegExp(value) {
14158
+ const escaped = value.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
14159
+ return new RegExp(`^${escaped}$`, "i");
14160
+ }
14161
+ function scoreTarget(entry, needle) {
14162
+ const candidates = targetCandidateTexts(entry);
14163
+ if (candidates.some((candidate) => candidate === needle)) return 100;
14164
+ if (candidates.some((candidate) => candidate.startsWith(needle))) return 60;
14165
+ if (candidates.some((candidate) => candidate.includes(needle))) return 30;
14166
+ const tokens = needle.split(/[\s/._-]+/).filter(Boolean);
14167
+ if (tokens.length > 0 && tokens.every((token) => candidates.some((candidate) => candidate.includes(token)))) return 15;
14168
+ return 0;
14169
+ }
14170
+ function targetSummary(entry) {
14171
+ return `${formatSceneTargetPath(entry)} (${entry.kind}, ${entry.id})`;
14172
+ }
14173
+ function targetNotFound(selector, entries) {
14174
+ const suggestions = suggestSceneTargets(selector, entries);
14175
+ const detail = suggestions.length > 0 ? `
14176
+ Did you mean:
14177
+ ${suggestions.map((entry) => ` ${targetSummary(entry)}`).join("\n")}` : "";
14178
+ return new Error(`Target "${selector}" matched no scene objects.${detail}`);
14179
+ }
14180
+ function ambiguousTarget(selector, matches) {
14181
+ return new Error(
14182
+ `Target "${selector}" is ambiguous.
14183
+ Use a full path:
14184
+ ${matches.map((entry) => ` ${targetSummary(entry)}`).join("\n")}`
14185
+ );
14186
+ }
14187
+
14188
+ // cli/inspect-sketch.ts
14189
+ function usage7() {
14190
+ return [
14191
+ "Usage: forgecad inspect sketch <model.forge.js> [--json] [--object <name-or-path>] [--seed <x,y>] [--operation extrude] [--param Key=Value] [--backend manifold|occt|truck] [--quality live|default|high]"
14192
+ ].join("\n");
14193
+ }
14194
+ function parseSeed(raw) {
14195
+ const parts = raw.split(",").map((part) => Number(part.trim()));
14196
+ if (parts.length !== 2 || !Number.isFinite(parts[0]) || !Number.isFinite(parts[1])) {
14197
+ throw new Error(`--seed must be two finite numbers as x,y (got "${raw}")`);
14198
+ }
14199
+ return [parts[0], parts[1]];
14200
+ }
14201
+ function parseOperation(raw) {
14202
+ if (raw === "extrude") return raw;
14203
+ throw new Error(`--operation must be extrude (got "${raw}")`);
14204
+ }
14205
+ function readFlagValue(argv, index, flag) {
14206
+ const value = argv[index + 1];
14207
+ if (!value || value.startsWith("--")) throw new Error(`${flag} requires a value.`);
14208
+ return value;
14209
+ }
14210
+ function parseOptions(argv) {
14211
+ if (argv.length === 0 || argv.includes("-h") || argv.includes("--help")) {
14212
+ console.log(usage7());
14213
+ process.exit(0);
14214
+ }
14215
+ const { consumed: paramConsumed } = parseParamFlags(argv);
14216
+ const { backend, consumed: backendConsumed } = parseBackendArg(argv);
14217
+ const { quality, consumed: qualityConsumed } = parseQualityArg(argv);
14218
+ const consumed = /* @__PURE__ */ new Set([...paramConsumed, ...backendConsumed, ...qualityConsumed]);
14219
+ let json = false;
14220
+ let compact = false;
14221
+ let objectSelector;
14222
+ let seed;
14223
+ let operation;
14224
+ for (let i = 0; i < argv.length; i += 1) {
14225
+ if (consumed.has(i)) continue;
14226
+ const arg = argv[i];
14227
+ if (arg === "--json") {
14228
+ json = true;
14229
+ consumed.add(i);
14230
+ } else if (arg === "--compact") {
14231
+ compact = true;
14232
+ consumed.add(i);
14233
+ } else if (arg === "--object") {
14234
+ objectSelector = readFlagValue(argv, i, arg);
14235
+ consumed.add(i);
14236
+ consumed.add(i + 1);
14237
+ i += 1;
14238
+ } else if (arg === "--seed") {
14239
+ seed = parseSeed(readFlagValue(argv, i, arg));
14240
+ consumed.add(i);
14241
+ consumed.add(i + 1);
14242
+ i += 1;
14243
+ } else if (arg === "--operation") {
14244
+ operation = parseOperation(readFlagValue(argv, i, arg));
14245
+ consumed.add(i);
14246
+ consumed.add(i + 1);
14247
+ i += 1;
14248
+ } else if (arg.startsWith("--")) {
14249
+ throw new Error(`Unknown option: ${arg}`);
14250
+ }
14251
+ }
14252
+ const positionals = argv.filter((_, index) => !consumed.has(index));
14253
+ if (!positionals[0]) throw new Error("Missing input path.");
14254
+ if (positionals.length > 1) throw new Error(`Unexpected argument: ${positionals[1]}`);
14255
+ return { scriptPath: positionals[0], json, compact, objectSelector, seed, operation, backend, quality };
14256
+ }
14257
+ function formatNumber(value, digits = 3) {
14258
+ if (value == null || !Number.isFinite(value)) return String(value);
14259
+ return value.toFixed(digits).replace(/\.?0+$/, "");
14260
+ }
14261
+ function formatPoint(point) {
14262
+ return `(${formatNumber(point[0])}, ${formatNumber(point[1])})`;
14263
+ }
14264
+ function formatBounds(target) {
14265
+ if (!target.bounds) return "unknown";
14266
+ return `${formatPoint(target.bounds.min)}..${formatPoint(target.bounds.max)}`;
14267
+ }
14268
+ function printHuman2(result, scriptPath) {
14269
+ console.log(`Sketch inspection for ${scriptPath}`);
14270
+ console.log(`${result.targets.length} target(s)`);
14271
+ for (const diagnostic of result.diagnostics) {
14272
+ console.log(`${diagnostic.level}: ${diagnostic.code}: ${diagnostic.message}`);
14273
+ }
14274
+ for (const target of result.targets) {
14275
+ const source = target.sourceKind === "shape-profile" ? `${target.sourceKind}:${target.operation} ${target.profilePath}` : target.sourceKind;
14276
+ console.log("");
14277
+ console.log(`${target.id} ${target.objectPath} ${source}`);
14278
+ console.log(
14279
+ ` area=${formatNumber(target.area)} bounds=${formatBounds(target)} empty=${target.isEmpty} regions=${target.regions.length}`
14280
+ );
14281
+ if (target.constraintStatus) {
14282
+ console.log(
14283
+ ` constraints=${target.constraintStatus} dof=${target.constraintDof ?? "?"} maxError=${formatNumber(target.constraintMaxError, 6)}`
14284
+ );
14285
+ }
14286
+ for (const region of target.regions) {
14287
+ console.log(
14288
+ ` ${region.id} area=${formatNumber(region.area)} seed=${formatPoint(region.seed)} bounds=${formatPoint(region.bounds.min)}..${formatPoint(region.bounds.max)} holes=${region.holes.length}`
14289
+ );
14290
+ }
14291
+ if (target.selection) {
14292
+ const status = target.selection.ok ? "ok" : "failed";
14293
+ const region = target.selection.regionId ? ` ${target.selection.regionId}` : "";
14294
+ const op = target.selection.operation && target.selection.operationCompatible != null ? ` ${target.selection.operation}=${target.selection.operationCompatible ? "yes" : "no"}` : "";
14295
+ console.log(` selection ${status}${region}${op}: ${target.selection.message}`);
14296
+ }
14297
+ for (const diagnostic of target.diagnostics) {
14298
+ console.log(` ${diagnostic.level}: ${diagnostic.code}: ${diagnostic.message}`);
14299
+ }
14300
+ }
14301
+ }
14302
+ async function captureConsoleWarnings(fn) {
14303
+ const originalWarn = console.warn;
14304
+ const warnings = [];
14305
+ console.warn = (...args) => {
14306
+ warnings.push(args.map((arg) => typeof arg === "string" ? arg : JSON.stringify(arg)).join(" "));
14307
+ };
14308
+ try {
14309
+ return { result: await fn(), warnings };
14310
+ } finally {
14311
+ console.warn = originalWarn;
14312
+ }
14313
+ }
14314
+ async function runInspectSketchCli(argv = process.argv.slice(2)) {
14315
+ let options;
14316
+ try {
14317
+ options = parseOptions(argv);
14318
+ } catch (error) {
14319
+ console.error(error instanceof Error ? error.message : String(error));
14320
+ console.error(usage7());
14321
+ process.exit(1);
14322
+ }
14323
+ const { overrides } = parseParamFlags(argv);
14324
+ const run = () => runModelForInspect(options.scriptPath, options.backend, options.quality, overrides);
14325
+ const { result: runResult, warnings: engineWarnings } = options.json ? await captureConsoleWarnings(run) : { result: await run(), warnings: [] };
14326
+ if (runResult.error) {
14327
+ console.error("ERROR:", runResult.error);
14328
+ process.exit(1);
14329
+ }
14330
+ let objectIds;
14331
+ if (options.objectSelector) {
14332
+ try {
14333
+ const entries = resolveSceneTargets(buildSceneTargetEntries(runResult.objects), options.objectSelector);
14334
+ objectIds = new Set(entries.map((entry) => entry.id));
14335
+ } catch (error) {
14336
+ console.error(error instanceof Error ? error.message : String(error));
14337
+ process.exit(1);
14338
+ }
14339
+ }
14340
+ const inspection = inspectSketches(runResult.objects, {
14341
+ objectIds,
14342
+ seed: options.seed,
14343
+ operation: options.operation
14344
+ });
14345
+ for (const warning of engineWarnings) {
14346
+ inspection.diagnostics.push({
14347
+ level: "warning",
14348
+ code: "engine-warning",
14349
+ message: warning
14350
+ });
14351
+ }
14352
+ if (options.json) {
14353
+ console.log(JSON.stringify({ script: options.scriptPath, ...inspection }, null, options.compact ? 0 : 2));
14354
+ } else {
14355
+ printHuman2(inspection, options.scriptPath);
14356
+ }
14357
+ if (options.seed && inspection.targets.length > 0 && !inspection.targets.some((target) => target.selection?.ok)) {
14358
+ process.exit(1);
14359
+ }
14360
+ }
14361
+
13455
14362
  // cli/check-occt-lower.ts
13456
14363
  import assert5 from "assert/strict";
13457
14364
  function approx3(a, b, eps = 0.01) {
@@ -15485,9 +16392,13 @@ var ESTABLISHED_LOWERCASE_PUBLIC_API_GLOBALS = /* @__PURE__ */ new Set([
15485
16392
  "kerfCompensateTabs",
15486
16393
  "require"
15487
16394
  ]);
16395
+ function readText(relativePath) {
16396
+ const path3 = resolvePackagePath(import.meta.url, relativePath);
16397
+ return readFileSync9(path3, "utf8");
16398
+ }
15488
16399
  function readSource(relativePath) {
15489
16400
  const path3 = resolvePackagePath(import.meta.url, relativePath);
15490
- return ts.createSourceFile(path3, readFileSync9(path3, "utf8"), ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
16401
+ return ts.createSourceFile(path3, readText(relativePath), ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
15491
16402
  }
15492
16403
  function propertyNameText(name) {
15493
16404
  if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) return name.text;
@@ -15570,6 +16481,29 @@ function uniqueSorted2(values) {
15570
16481
  function isLowercaseGlobal(name) {
15571
16482
  return /^[a-z]/u.test(name);
15572
16483
  }
16484
+ function renderRuntimeNameList(names) {
16485
+ const lines = [];
16486
+ for (let i = 0; i < names.length; i += 8) {
16487
+ lines.push(names.slice(i, i + 8).join(", "));
16488
+ }
16489
+ return lines.join("\n");
16490
+ }
16491
+ function checkRuntimeNamesDoc(runtimeNames) {
16492
+ const collisionReservedNames = runtimeNames.filter((name) => name !== "showLabels");
16493
+ const expectedList = renderRuntimeNameList(collisionReservedNames);
16494
+ let doc;
16495
+ try {
16496
+ doc = readText("docs/permanent/generated/runtime-names.md");
16497
+ } catch {
16498
+ return "Generated runtime names doc is missing. Run npm run gen:docs.";
16499
+ }
16500
+ if (!doc.includes(`\`\`\`text
16501
+ ${expectedList}
16502
+ \`\`\``)) {
16503
+ return "Generated runtime names doc is stale. Run npm run gen:docs.";
16504
+ }
16505
+ return null;
16506
+ }
15573
16507
  async function runCheckRuntimeGlobalsCli() {
15574
16508
  const errors = [];
15575
16509
  const runtime = collectRuntimeBindingNames(readSource("src/forge/script-runtime/runScript.ts"));
@@ -15577,6 +16511,8 @@ async function runCheckRuntimeGlobalsCli() {
15577
16511
  if (runtime.unsupported.length > 0) {
15578
16512
  errors.push(`runtimeBindings must use explicit property names; unsupported object entries at lines ${runtime.unsupported.join(", ")}.`);
15579
16513
  }
16514
+ const runtimeNamesDocError = checkRuntimeNamesDoc(runtimeNames);
16515
+ if (runtimeNamesDocError) errors.push(runtimeNamesDocError);
15580
16516
  const newLowercaseGlobals = runtimeNames.filter((name) => isLowercaseGlobal(name) && !ESTABLISHED_LOWERCASE_RUNTIME_GLOBALS.has(name));
15581
16517
  if (newLowercaseGlobals.length > 0) {
15582
16518
  errors.push(
@@ -15631,11 +16567,11 @@ runDirectCliMain(import.meta.url, "cli/check-runtime-globals.ts", () => runCheck
15631
16567
 
15632
16568
  // cli/check-text.ts
15633
16569
  import assert6 from "assert/strict";
15634
- var EPS5 = 1e-3;
15635
- function approx4(a, b, eps = EPS5) {
16570
+ var EPS6 = 1e-3;
16571
+ function approx4(a, b, eps = EPS6) {
15636
16572
  return Math.abs(a - b) <= eps;
15637
16573
  }
15638
- function expectClose3(actual, expected, label, eps = EPS5) {
16574
+ function expectClose3(actual, expected, label, eps = EPS6) {
15639
16575
  assert6(approx4(actual, expected, eps), `${label}: expected ${expected}, got ${actual}`);
15640
16576
  }
15641
16577
  function bounds(sk) {
@@ -15690,7 +16626,7 @@ function checkTextWidth() {
15690
16626
  Math.abs(reported - w) < tolerance,
15691
16627
  `textWidth ${reported.toFixed(3)} should be close to sketch width ${w.toFixed(3)} within ${tolerance.toFixed(3)}`
15692
16628
  );
15693
- assert6(reported >= w - EPS5, `textWidth ${reported.toFixed(3)} should be >= sketch width ${w.toFixed(3)}`);
16629
+ assert6(reported >= w - EPS6, `textWidth ${reported.toFixed(3)} should be >= sketch width ${w.toFixed(3)}`);
15694
16630
  }
15695
16631
  function checkHorizontalAlignment() {
15696
16632
  const left = text2d("CAD", { size: 10, align: "left" });
@@ -15753,8 +16689,8 @@ runDirectCliMain(import.meta.url, "cli/check-text.ts", () => runCheckTextCli());
15753
16689
 
15754
16690
  // cli/check-transforms.ts
15755
16691
  import assert7 from "assert/strict";
15756
- var EPS6 = 1e-6;
15757
- function approx5(a, b, eps = EPS6) {
16692
+ var EPS7 = 1e-6;
16693
+ function approx5(a, b, eps = EPS7) {
15758
16694
  return Math.abs(a - b) <= eps;
15759
16695
  }
15760
16696
  function assertVec(actual, expected, label) {
@@ -15893,53 +16829,16 @@ function testAssemblyNamedGroupLabels() {
15893
16829
  `Expected flattened objects to retain groupName Base Assembly, got ${JSON.stringify(result.objects.map((obj) => obj.groupName))}`
15894
16830
  );
15895
16831
  }
15896
- function testAssemblyJointCouplings() {
15897
- const mech = assembly("CoupledJointsInvariant").addFrame("Base").addFrame("A").addFrame("B").addFrame("C").addRevolute("A", "Base", "A", { axis: [0, 0, 1] }).addRevolute("B", "A", "B", { axis: [0, 0, 1], min: -30, max: 30 }).addRevolute("C", "B", "C", { axis: [0, 0, 1] }).addJointCoupling("B", { terms: [{ joint: "A", ratio: 2 }], offset: 10 }).addJointCoupling("C", {
15898
- terms: [
15899
- { joint: "A", ratio: -1 },
15900
- { joint: "B", ratio: 0.5 }
15901
- ],
15902
- offset: 5
15903
- });
15904
- const solved = mech.solve({ A: 20, B: 999 });
15905
- const state = solved.getJointState();
15906
- assert7(approx5(state.A ?? Number.NaN, 20), `Expected A=20, got ${state.A}`);
15907
- assert7(approx5(state.B ?? Number.NaN, 30), `Expected B=30 after clamp, got ${state.B}`);
15908
- assert7(approx5(state.C ?? Number.NaN, 0), `Expected C=0 from coupled joints, got ${state.C}`);
15909
- const warnings = solved.warnings().join("\n");
15910
- assert7(
15911
- warnings.includes('Joint "B" state override ignored because it is coupled'),
15912
- `Expected ignored-override warning for B, got:
15913
- ${warnings}`
15914
- );
15915
- }
15916
- function testAssemblyGearCouplings() {
15917
- const mech = assembly("GearCouplingsInvariant").addFrame("Base").addFrame("DriverPart").addFrame("DrivenPart").addFrame("FollowerPart").addRevolute("Driver", "Base", "DriverPart", { axis: [0, 0, 1] }).addRevolute("Driven", "Base", "DrivenPart", { axis: [0, 0, 1], min: -20, max: 20 }).addRevolute("Follower", "Base", "FollowerPart", { axis: [0, 0, 1] }).addGearCoupling("Driven", "Driver", { driverTeeth: 14, drivenTeeth: 28 }).addGearCoupling("Follower", "Driven", { pair: { jointRatio: -2 }, offset: 5 });
15918
- const solved = mech.solve({ Driver: 30, Driven: 999, Follower: 999 });
15919
- const state = solved.getJointState();
15920
- assert7(approx5(state.Driver ?? Number.NaN, 30), `Expected Driver=30, got ${state.Driver}`);
15921
- assert7(approx5(state.Driven ?? Number.NaN, -15), `Expected Driven=-15 from teeth ratio, got ${state.Driven}`);
15922
- assert7(approx5(state.Follower ?? Number.NaN, 35), `Expected Follower=35 from pair ratio, got ${state.Follower}`);
15923
- const warnings = solved.warnings().join("\n");
15924
- assert7(
15925
- warnings.includes('Joint "Driven" state override ignored because it is coupled'),
15926
- `Expected ignored-override warning for Driven, got:
15927
- ${warnings}`
16832
+ function testRetiredAssemblyCouplingStubs() {
16833
+ const mech = assembly("RetiredCouplingInvariant").addFrame("Base").addFrame("DriverPart").addFrame("DrivenPart").addRevolute("Driver", "Base", "DriverPart", { axis: [0, 0, 1] }).addRevolute("Driven", "Base", "DrivenPart", { axis: [0, 0, 1] });
16834
+ assert7.throws(
16835
+ () => mech.addJointCoupling("Driven", { terms: [{ joint: "Driver", ratio: 1 }] }),
16836
+ /addJointCoupling\(\) has been removed from the modeling API/
16837
+ );
16838
+ assert7.throws(
16839
+ () => mech.addGearCoupling("Driven", "Driver", { ratio: -1 }),
16840
+ /addGearCoupling\(\) has been removed from the modeling API/
15928
16841
  );
15929
- assert7(
15930
- warnings.includes('Joint "Follower" state override ignored because it is coupled'),
15931
- `Expected ignored-override warning for Follower, got:
15932
- ${warnings}`
15933
- );
15934
- const internal = assembly("InternalMeshSignInvariant").addFrame("Base").addFrame("A").addFrame("B").addRevolute("A", "Base", "A", { axis: [0, 0, 1] }).addRevolute("B", "Base", "B", { axis: [0, 0, 1] }).addGearCoupling("B", "A", { driverTeeth: 20, drivenTeeth: 40, mesh: "internal" });
15935
- const internalState = internal.solve({ A: 12 }).getJointState();
15936
- assert7(approx5(internalState.B ?? Number.NaN, 6), `Expected internal mesh B=6, got ${internalState.B}`);
15937
- const bevel = assembly("BevelMeshSignInvariant").addFrame("Base").addFrame("A").addFrame("B").addRevolute("A", "Base", "A", { axis: [0, 0, 1] }).addRevolute("B", "Base", "B", { axis: [1, 0, 0] }).addGearCoupling("B", "A", { driverTeeth: 24, drivenTeeth: 48, mesh: "bevel" });
15938
- const bevelState = bevel.solve({ A: 12 }).getJointState();
15939
- assert7(approx5(bevelState.B ?? Number.NaN, -6), `Expected bevel mesh B=-6, got ${bevelState.B}`);
15940
- const face = assembly("FaceMeshSignInvariant").addFrame("Base").addFrame("A").addFrame("B").addRevolute("A", "Base", "A", { axis: [0, 0, 1] }).addRevolute("B", "Base", "B", { axis: [1, 0, 0] }).addGearCoupling("B", "A", { driverTeeth: 24, drivenTeeth: 48, mesh: "face" });
15941
- const faceState = face.solve({ A: 12 }).getJointState();
15942
- assert7(approx5(faceState.B ?? Number.NaN, -6), `Expected face mesh B=-6, got ${faceState.B}`);
15943
16842
  }
15944
16843
  function testRuntimeJointCouplingResolution() {
15945
16844
  const joints = [
@@ -16180,7 +17079,7 @@ function testBevelGearTopSectionCircularity() {
16180
17079
  const topSlice = gear.slice(bb.max[2] - 1e-4).bounds();
16181
17080
  const spanX = topSlice.max[0] - topSlice.min[0];
16182
17081
  const spanY = topSlice.max[1] - topSlice.min[1];
16183
- assert7(spanX > EPS6 && spanY > EPS6, `Expected non-degenerate top slice, got spanX=${spanX}, spanY=${spanY}`);
17082
+ assert7(spanX > EPS7 && spanY > EPS7, `Expected non-degenerate top slice, got spanX=${spanX}, spanY=${spanY}`);
16184
17083
  const aspect = spanY / spanX;
16185
17084
  assert7(aspect > 0.85 && aspect < 1.15, `Expected near-circular top slice, got spanX=${spanX}, spanY=${spanY}, aspect=${aspect}`);
16186
17085
  }
@@ -16193,8 +17092,7 @@ async function runCheckTransformsCli() {
16193
17092
  testShapeRotateAroundTo();
16194
17093
  testShapeGroupRotateAroundToSugar();
16195
17094
  testAssemblyNamedGroupLabels();
16196
- testAssemblyJointCouplings();
16197
- testAssemblyGearCouplings();
17095
+ testRetiredAssemblyCouplingStubs();
16198
17096
  testRuntimeJointCouplingResolution();
16199
17097
  testContinuousRuntimeJointAnimation();
16200
17098
  testTickBasedKeyframes();
@@ -16258,13 +17156,13 @@ function enforceCommandConfig(commandPath) {
16258
17156
  }
16259
17157
 
16260
17158
  // cli/debug-compiler.ts
16261
- function usage7() {
17159
+ function usage8() {
16262
17160
  console.error("Usage: forgecad debug compiler <script.forge.js> [--compact]");
16263
17161
  process.exit(1);
16264
17162
  }
16265
17163
  async function runDebugCompilerCli(argv = process.argv.slice(2)) {
16266
17164
  const scriptPath = argv.find((arg) => !arg.startsWith("--"));
16267
- if (!scriptPath) usage7();
17165
+ if (!scriptPath) usage8();
16268
17166
  const compact = argv.includes("--compact");
16269
17167
  await init();
16270
17168
  const inspection = inspectCompilerScene(loadCompilerInspectionInput(scriptPath));
@@ -16274,7 +17172,7 @@ async function runDebugCompilerCli(argv = process.argv.slice(2)) {
16274
17172
  // cli/debug-assembly.ts
16275
17173
  import { readFileSync as readFileSync11 } from "fs";
16276
17174
  import { resolve as resolve12 } from "path";
16277
- function usage8() {
17175
+ function usage9() {
16278
17176
  console.error(
16279
17177
  "Usage: forgecad debug assembly <script.forge.js> [--json] [--compact] [--all] [--exact-geometry] [--fail-on error|warning] [--backend manifold|occt|truck] [--sweep-steps <n>]"
16280
17178
  );
@@ -16334,7 +17232,7 @@ async function runDebugAssemblyCli(argv = process.argv.slice(2)) {
16334
17232
  const consumed = consumedIndexes(argv);
16335
17233
  const positional = argv.filter((arg, index) => !consumed.has(index) && !arg.startsWith("--"));
16336
17234
  const scriptPath = positional[0];
16337
- if (!scriptPath) usage8();
17235
+ if (!scriptPath) usage9();
16338
17236
  const asJson = argv.includes("--json");
16339
17237
  const compact = argv.includes("--compact");
16340
17238
  const all = argv.includes("--all");
@@ -16456,13 +17354,13 @@ function mapDimensionsToOwners(dimensions, objects) {
16456
17354
  });
16457
17355
  return { combinedCount, byDimensionId };
16458
17356
  }
16459
- function usage9() {
17357
+ function usage10() {
16460
17358
  console.error("Usage: forgecad debug dimensions <script.forge.js> [--all] [--dim-angle-tol 12]");
16461
17359
  process.exit(1);
16462
17360
  }
16463
17361
  async function runDebugDimensionsCli(argv = process.argv.slice(2)) {
16464
17362
  const scriptPath = argv[0];
16465
- if (!scriptPath) usage9();
17363
+ if (!scriptPath) usage10();
16466
17364
  const showAll = argv.includes("--all");
16467
17365
  const tolFlagIndex = argv.indexOf("--dim-angle-tol");
16468
17366
  const tolValue = tolFlagIndex >= 0 ? Number(argv[tolFlagIndex + 1]) : NaN;
@@ -16712,6 +17610,7 @@ async function buildStepBlob(objects) {
16712
17610
  // cli/forge-brep.ts
16713
17611
  function parseArgs4(argv) {
16714
17612
  let format = "step";
17613
+ let backend = "occt";
16715
17614
  let outputPath;
16716
17615
  let scriptPath;
16717
17616
  for (let i = 0; i < argv.length; i += 1) {
@@ -16725,6 +17624,15 @@ function parseArgs4(argv) {
16725
17624
  i += 1;
16726
17625
  continue;
16727
17626
  }
17627
+ if (arg === "--backend") {
17628
+ const value = argv[i + 1];
17629
+ if (value !== "occt" && value !== "truck") {
17630
+ throw new Error(`--backend must be "occt" or "truck" (got ${value ?? "missing"})`);
17631
+ }
17632
+ backend = value;
17633
+ i += 1;
17634
+ continue;
17635
+ }
16728
17636
  if (arg === "--output") {
16729
17637
  outputPath = argv[i + 1];
16730
17638
  if (!outputPath) throw new Error("--output requires a path");
@@ -16749,9 +17657,14 @@ function parseArgs4(argv) {
16749
17657
  scriptPath = arg;
16750
17658
  }
16751
17659
  if (!scriptPath) {
16752
- throw new Error("Usage: npx tsx cli/forge-brep.ts [--format step|brep] [--output path] <model.forge.js|asset.step|asset.stp>");
17660
+ throw new Error(
17661
+ "Usage: npx tsx cli/forge-brep.ts [--format step|brep] [--backend occt|truck] [--output path] <model.forge.js|asset.step|asset.stp>"
17662
+ );
17663
+ }
17664
+ if (backend === "truck" && format !== "step") {
17665
+ throw new Error("The native Truck BREP exporter currently supports STEP only (use --format step).");
16753
17666
  }
16754
- return { format, outputPath, scriptPath };
17667
+ return { format, backend, outputPath, scriptPath };
16755
17668
  }
16756
17669
  function defaultOutputPath(scriptPath, sourcePath, format) {
16757
17670
  const abs = resolve15(scriptPath);
@@ -16790,12 +17703,25 @@ async function buildRuntimeExactExportBlob(format, code, fileName, allFiles, rea
16790
17703
  objectCount: exactObjects.length
16791
17704
  };
16792
17705
  }
17706
+ async function buildTruckStepBlob(code, fileName, allFiles, readBinaryFile) {
17707
+ await activateBackend("truck");
17708
+ const result = runScript(code, fileName, allFiles, { readBinaryFile });
17709
+ if (result.error) {
17710
+ throw new Error(result.error);
17711
+ }
17712
+ const handles = result.objects.filter((obj) => obj.shape).map((obj) => getTruckShapeBackendHandle(getShapeRuntimeBackend(obj.shape)));
17713
+ if (handles.length === 0) {
17714
+ throw new Error("No 3D shapes found in the script output.");
17715
+ }
17716
+ const step = getTruckGeometryWasm().geometry_export_step_multi(JSON.stringify(handles));
17717
+ return { blob: new Blob([step], { type: "application/step" }), objectCount: handles.length };
17718
+ }
16793
17719
  async function runBrepCli(argv = process.argv.slice(2)) {
16794
- const { format, outputPath, scriptPath } = parseArgs4(argv);
17720
+ const { format, backend, outputPath, scriptPath } = parseArgs4(argv);
16795
17721
  const input = loadCliScriptInput(scriptPath);
16796
17722
  await init();
16797
17723
  try {
16798
- const exactExport = await buildRuntimeExactExportBlob(format, input.code, input.fileName, input.allFiles, input.readBinaryFile);
17724
+ const exactExport = backend === "truck" ? await buildTruckStepBlob(input.code, input.fileName, input.allFiles, input.readBinaryFile) : await buildRuntimeExactExportBlob(format, input.code, input.fileName, input.allFiles, input.readBinaryFile);
16799
17725
  if (outputPath && resolve15(outputPath) === input.sourcePath) {
16800
17726
  console.error("Output path would overwrite the input file. Pass a different --output path.");
16801
17727
  process.exit(1);
@@ -17123,7 +18049,7 @@ function parseSweepEaseEnv(raw) {
17123
18049
  if (raw === "linear" || raw === "smoothstep") return raw;
17124
18050
  return null;
17125
18051
  }
17126
- function usage10(config) {
18052
+ function usage11(config) {
17127
18053
  const defaultPitch = DEFAULTS.pitchDeg == null ? "copied camera pitch or 18" : String(DEFAULTS.pitchDeg);
17128
18054
  return `ForgeCAD Capture Renderer
17129
18055
 
@@ -17134,7 +18060,7 @@ Options:
17134
18060
  --format <gif|mp4> Output format (default: ${config.defaultFormat})
17135
18061
  --capture <orbit|animation|section-sweep>
17136
18062
  Capture preset (default: orbit)
17137
- --animation <name> Select a jointsView animation clip
18063
+ --animation <name> Select a named joint animation clip
17138
18064
  --animation-loops <n> Repeat the selected clip this many times (default: ${DEFAULTS.animationLoops})
17139
18065
  --cut-plane <name> Enable a named cut plane (repeatable)
17140
18066
  --sweep-plane <XY|XZ|YZ> Plane to move for --capture section-sweep (default: ${DEFAULTS.sweepPlane})
@@ -17179,7 +18105,7 @@ Options:
17179
18105
 
17180
18106
  Examples:
17181
18107
  forgecad capture gif examples/products/cup.forge.js
17182
- forgecad capture mp4 examples/api/runtime-joints-view.forge.js out/step.mp4 --capture animation --animation Step
18108
+ forgecad capture mp4 examples/api/assembly-kinematics-four-bar.forge.js out/four-bar.mp4 --view iso
17183
18109
  forgecad capture mp4 model.forge.js out/raw.mp4 --param "Output=raw-sdf"
17184
18110
  forgecad capture gif examples/3d-printer.forge.js out/section.gif --cut-plane "Front Section"
17185
18111
  forgecad capture mp4 examples/3d-printer.forge.js out/sweep.mp4 --capture section-sweep --sweep-plane YZ --sweep-frames 180
@@ -17255,7 +18181,7 @@ function defaultOutputPath2(scriptPath, format, capture) {
17255
18181
  }
17256
18182
  function parseCli(argv, config) {
17257
18183
  if (argv.length === 0 || argv.includes("-h") || argv.includes("--help")) {
17258
- console.log(usage10(config));
18184
+ console.log(usage11(config));
17259
18185
  process.exit(0);
17260
18186
  }
17261
18187
  let scriptPath;
@@ -17744,7 +18670,7 @@ async function fetchRenderHtml2(port) {
17744
18670
  clearTimeout(timer);
17745
18671
  }
17746
18672
  }
17747
- async function waitForRenderHtml(port, timeoutMs) {
18673
+ async function waitForRenderHtml2(port, timeoutMs) {
17748
18674
  const deadline = Date.now() + timeoutMs;
17749
18675
  while (Date.now() < deadline) {
17750
18676
  if (await fetchRenderHtml2(port)) return true;
@@ -17784,7 +18710,7 @@ async function ensureDevServer2(port) {
17784
18710
  proc.once("exit", () => {
17785
18711
  exitedEarly = true;
17786
18712
  });
17787
- const ready = await waitForRenderHtml(port, 3e4);
18713
+ const ready = await waitForRenderHtml2(port, 3e4);
17788
18714
  if (!ready) {
17789
18715
  proc.kill();
17790
18716
  const detail = startupOutput.trim();
@@ -17807,7 +18733,7 @@ async function stopDevServer2(proc) {
17807
18733
  proc.kill("SIGKILL");
17808
18734
  }
17809
18735
  }
17810
- async function isPortFree2(port) {
18736
+ async function isPortFree(port) {
17811
18737
  const probe = (host) => new Promise((resolvePort) => {
17812
18738
  const server = createServer();
17813
18739
  server.once("error", (err) => {
@@ -17826,10 +18752,10 @@ async function isPortFree2(port) {
17826
18752
  const ipv6Free = await probe("::1");
17827
18753
  return ipv4Free && ipv6Free;
17828
18754
  }
17829
- async function findFreePort2(startPort, maxAttempts = 30) {
18755
+ async function findFreePort(startPort, maxAttempts = 30) {
17830
18756
  let candidate = Math.max(1024, startPort);
17831
18757
  for (let i = 0; i < maxAttempts; i += 1) {
17832
- if (await isPortFree2(candidate)) return candidate;
18758
+ if (await isPortFree(candidate)) return candidate;
17833
18759
  candidate += 1;
17834
18760
  }
17835
18761
  return null;
@@ -18280,7 +19206,7 @@ async function runCaptureCli(config, argv = process.argv.slice(2)) {
18280
19206
  } catch (err) {
18281
19207
  console.error(String(err));
18282
19208
  console.error("");
18283
- console.error(usage10(config));
19209
+ console.error(usage11(config));
18284
19210
  process.exit(1);
18285
19211
  }
18286
19212
  const chromePath = findChromePath2(options.chromePath);
@@ -18301,8 +19227,8 @@ async function runCaptureCli(config, argv = process.argv.slice(2)) {
18301
19227
  if (forgeAlreadyRunning) {
18302
19228
  console.log(`Reusing existing ForgeCAD render server on :${activePort}.`);
18303
19229
  }
18304
- if (!forgeAlreadyRunning && !await isPortFree2(activePort)) {
18305
- const fallbackPort = await findFreePort2(activePort + 1);
19230
+ if (!forgeAlreadyRunning && !await isPortFree(activePort)) {
19231
+ const fallbackPort = await findFreePort(activePort + 1);
18306
19232
  if (fallbackPort == null) {
18307
19233
  throw new Error(`Port ${activePort} is occupied and no free fallback port was found.`);
18308
19234
  }
@@ -18322,7 +19248,7 @@ async function runCaptureCli(config, argv = process.argv.slice(2)) {
18322
19248
  const message = String(err);
18323
19249
  const isPortConflict = message.includes("already in use") || message.includes("EADDRINUSE");
18324
19250
  if (!isPortConflict) throw err;
18325
- const fallbackPort = await findFreePort2(activePort + 1);
19251
+ const fallbackPort = await findFreePort(activePort + 1);
18326
19252
  if (fallbackPort == null) throw err;
18327
19253
  console.log(`Port ${activePort} failed to start due to a port conflict. Retrying on ${fallbackPort} ...`);
18328
19254
  activePort = fallbackPort;
@@ -18357,7 +19283,7 @@ async function runCaptureCli(config, argv = process.argv.slice(2)) {
18357
19283
  debugLog(`loading capture runtime on :${activePort}`);
18358
19284
  let captureRuntimeReady = await loadCaptureRuntime(page, activePort, options.capture);
18359
19285
  if (!captureRuntimeReady && !viteProc) {
18360
- const fallbackPort = await findFreePort2(activePort + 1);
19286
+ const fallbackPort = await findFreePort(activePort + 1);
18361
19287
  if (fallbackPort != null) {
18362
19288
  console.log(`Existing server on :${activePort} is missing required capture APIs. Starting a fresh server on :${fallbackPort} ...`);
18363
19289
  activePort = fallbackPort;
@@ -18906,11 +19832,10 @@ function runHiddenCompletionCli(argv, commands2) {
18906
19832
  }
18907
19833
 
18908
19834
  // cli/forge-dev.ts
18909
- import { fork } from "child_process";
18910
19835
  import path from "path";
18911
19836
  import { resolve as resolve18, dirname as dirname4, basename as basename7 } from "path";
18912
- import { existsSync as existsSync9, statSync as statSync7 } from "fs";
18913
- function usage11() {
19837
+ import { statSync as statSync7 } from "fs";
19838
+ function usage12() {
18914
19839
  console.error(`ForgeCAD Dev Server
18915
19840
 
18916
19841
  Usage:
@@ -18938,7 +19863,7 @@ function parseDevArgs(argv) {
18938
19863
  if (argv.length === 0) {
18939
19864
  throw new Error("Missing project path. Use `forgecad dev <project-path> [project-path ...]`.");
18940
19865
  }
18941
- if (argv.includes("-h") || argv.includes("--help")) usage11();
19866
+ if (argv.includes("-h") || argv.includes("--help")) usage12();
18942
19867
  const options = { open: false, strictPort: false, projectPaths: [] };
18943
19868
  for (let i = 0; i < argv.length; i += 1) {
18944
19869
  const arg = argv[i];
@@ -18998,35 +19923,10 @@ function waitForExit(child) {
18998
19923
  child.once("exit", (code) => resolve40(code ?? 0));
18999
19924
  });
19000
19925
  }
19001
- function spawnBackendServer(packageRoot) {
19002
- const backendEntry = resolve18(packageRoot, "dist-backend", "server.js");
19003
- if (!existsSync9(backendEntry)) return null;
19004
- const child = fork(backendEntry, {
19005
- stdio: ["ignore", "pipe", "pipe", "ipc"],
19006
- env: { ...process.env, FORGE_BACKEND_PORT: "4510" }
19007
- });
19008
- child.stdout?.on("data", (data) => {
19009
- for (const line of data.toString().trim().split("\n")) {
19010
- console.log(` [backend] ${line}`);
19011
- }
19012
- });
19013
- child.stderr?.on("data", (data) => {
19014
- for (const line of data.toString().trim().split("\n")) {
19015
- console.error(` [backend] ${line}`);
19016
- }
19017
- });
19018
- child.on("exit", (code) => {
19019
- if (code !== null && code !== 0) {
19020
- console.error(` [backend] Compute server exited with code ${code}`);
19021
- }
19022
- });
19023
- return child;
19024
- }
19025
19926
  async function runDevCli(argv = process.argv.slice(2)) {
19026
19927
  const options = parseDevArgs(argv);
19027
19928
  const projectDirs = options.projectPaths;
19028
19929
  const packageRoot = packageRootFrom(import.meta.url);
19029
- const backendChild = spawnBackendServer(packageRoot);
19030
19930
  const child = spawnPackageVite(import.meta.url, toViteArgs(options), {
19031
19931
  cwd: packageRoot,
19032
19932
  stdio: "inherit",
@@ -19040,7 +19940,6 @@ async function runDevCli(argv = process.argv.slice(2)) {
19040
19940
  }
19041
19941
  });
19042
19942
  const code = await waitForExit(child);
19043
- if (backendChild && !backendChild.killed) backendChild.kill();
19044
19943
  if (code !== 0) process.exit(code);
19045
19944
  }
19046
19945
 
@@ -19108,130 +20007,9 @@ async function runGcodeExportCli(argv) {
19108
20007
  console.log(` Bounds: [${tp.bounds.min.map((v) => v.toFixed(1)).join(", ")}] \u2192 [${tp.bounds.max.map((v) => v.toFixed(1)).join(", ")}]`);
19109
20008
  }
19110
20009
 
19111
- // src/forge/targets.ts
19112
- var cleanSceneTargetPathSegments = (segments) => (segments ?? []).map((segment) => segment.trim()).filter((segment) => segment.length > 0);
19113
- function getSceneObjectTreePath(object) {
19114
- const explicitTreePath = cleanSceneTargetPathSegments(object.treePath);
19115
- if (explicitTreePath.length > 0) return explicitTreePath;
19116
- const name = object.name.trim() || object.id;
19117
- const groupName = object.groupName?.trim();
19118
- if (!groupName) return [name];
19119
- const groupPath = groupName.split(".").map((segment) => segment.trim()).filter((segment) => segment.length > 0);
19120
- const prefixedLeaf = `${groupName}.`;
19121
- if (name.startsWith(prefixedLeaf)) {
19122
- const leafName = name.slice(prefixedLeaf.length).trim();
19123
- return [...groupPath, leafName || name];
19124
- }
19125
- return [...groupPath, name];
19126
- }
19127
- function getSceneObjectKind(object) {
19128
- if (object.mock) return "mock";
19129
- if (object.sketch) return "sketch";
19130
- if (object.toolpath) return "toolpath";
19131
- if (object.sdf) return "sdf";
19132
- if (object.shape) return "shape";
19133
- return "object";
19134
- }
19135
- function buildSceneTargetEntries(objects) {
19136
- return objects.map((object) => {
19137
- const pathSegments = getSceneObjectTreePath(object);
19138
- return {
19139
- id: object.id,
19140
- name: object.name,
19141
- kind: getSceneObjectKind(object),
19142
- pathSegments,
19143
- path: pathSegments.join("/"),
19144
- dottedPath: pathSegments.join("."),
19145
- group: object.groupName?.trim() || void 0,
19146
- tags: object.tags ?? [],
19147
- mock: object.mock === true,
19148
- object
19149
- };
19150
- });
19151
- }
19152
- function formatSceneTargetPath(entry) {
19153
- return entry.path || entry.name;
19154
- }
19155
- function resolveSceneTargets(entries, selector) {
19156
- const needle = normalizeTargetText(selector);
19157
- if (!needle) throw new Error("Target selector must not be empty.");
19158
- const hasGlob = /[*?]/.test(selector);
19159
- if (hasGlob) {
19160
- const pattern = globToRegExp(needle);
19161
- const matches = entries.filter((entry) => targetCandidateTexts(entry).some((candidate) => pattern.test(candidate)));
19162
- if (matches.length > 0) return matches;
19163
- throw targetNotFound(selector, entries);
19164
- }
19165
- const idMatches = entries.filter((entry) => normalizeTargetText(entry.id) === needle);
19166
- if (idMatches.length > 0) return idMatches;
19167
- const exactPathMatches = entries.filter(
19168
- (entry) => normalizeTargetText(entry.path) === needle || normalizeTargetText(entry.dottedPath) === needle
19169
- );
19170
- if (exactPathMatches.length > 0) return exactPathMatches;
19171
- const groupMatches = entries.filter((entry) => {
19172
- const groupPath = entry.pathSegments.slice(0, -1);
19173
- return normalizeTargetText(groupPath.join("/")) === needle || normalizeTargetText(groupPath.join(".")) === needle;
19174
- });
19175
- if (groupMatches.length > 0) return groupMatches;
19176
- const nameMatches2 = entries.filter(
19177
- (entry) => normalizeTargetText(entry.name) === needle || normalizeTargetText(entry.pathSegments[entry.pathSegments.length - 1] ?? "") === needle
19178
- );
19179
- if (nameMatches2.length === 1) return nameMatches2;
19180
- if (nameMatches2.length > 1) throw ambiguousTarget(selector, nameMatches2);
19181
- throw targetNotFound(selector, entries);
19182
- }
19183
- function suggestSceneTargets(selector, entries, limit = 8) {
19184
- const needle = normalizeTargetText(selector);
19185
- if (!needle) return entries.slice(0, limit);
19186
- return entries.map((entry) => ({ entry, score: scoreTarget(entry, needle) })).filter((ranked) => ranked.score > 0).sort((a, b) => b.score - a.score || a.entry.path.localeCompare(b.entry.path)).slice(0, limit).map((ranked) => ranked.entry);
19187
- }
19188
- function targetCandidateTexts(entry) {
19189
- return [
19190
- entry.id,
19191
- entry.name,
19192
- entry.path,
19193
- entry.dottedPath,
19194
- entry.group ?? "",
19195
- entry.pathSegments[entry.pathSegments.length - 1] ?? ""
19196
- ].map(normalizeTargetText);
19197
- }
19198
- function normalizeTargetText(value) {
19199
- return value.trim().replace(/\\/g, "/").replace(/\s*\/\s*/g, "/").replace(/\s*\.\s*/g, ".").toLowerCase();
19200
- }
19201
- function globToRegExp(value) {
19202
- const escaped = value.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
19203
- return new RegExp(`^${escaped}$`, "i");
19204
- }
19205
- function scoreTarget(entry, needle) {
19206
- const candidates = targetCandidateTexts(entry);
19207
- if (candidates.some((candidate) => candidate === needle)) return 100;
19208
- if (candidates.some((candidate) => candidate.startsWith(needle))) return 60;
19209
- if (candidates.some((candidate) => candidate.includes(needle))) return 30;
19210
- const tokens = needle.split(/[\s/._-]+/).filter(Boolean);
19211
- if (tokens.length > 0 && tokens.every((token) => candidates.some((candidate) => candidate.includes(token)))) return 15;
19212
- return 0;
19213
- }
19214
- function targetSummary(entry) {
19215
- return `${formatSceneTargetPath(entry)} (${entry.kind}, ${entry.id})`;
19216
- }
19217
- function targetNotFound(selector, entries) {
19218
- const suggestions = suggestSceneTargets(selector, entries);
19219
- const detail = suggestions.length > 0 ? `
19220
- Did you mean:
19221
- ${suggestions.map((entry) => ` ${targetSummary(entry)}`).join("\n")}` : "";
19222
- return new Error(`Target "${selector}" matched no scene objects.${detail}`);
19223
- }
19224
- function ambiguousTarget(selector, matches) {
19225
- return new Error(
19226
- `Target "${selector}" is ambiguous.
19227
- Use a full path:
19228
- ${matches.map((entry) => ` ${targetSummary(entry)}`).join("\n")}`
19229
- );
19230
- }
19231
-
19232
20010
  // cli/forge-ls.ts
19233
20011
  var TARGET_KINDS = ["shape", "sketch", "sdf", "toolpath", "mock", "object"];
19234
- function usage12() {
20012
+ function usage13() {
19235
20013
  return [
19236
20014
  "Usage: forgecad ls <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [target] [--tree] [--long] [--json] [--kind shape,sketch,mock] [--param Key=Value] [--backend manifold|occt|truck] [--quality live|default|high]"
19237
20015
  ].join("\n");
@@ -19250,7 +20028,7 @@ function parseKindFilter(raw) {
19250
20028
  }
19251
20029
  function parseLsOptions(argv) {
19252
20030
  if (argv.length === 0 || argv.includes("-h") || argv.includes("--help")) {
19253
- console.log(usage12());
20031
+ console.log(usage13());
19254
20032
  process.exit(0);
19255
20033
  }
19256
20034
  const { overrides: _paramOverrides, consumed: paramConsumed } = parseParamFlags(argv);
@@ -19298,12 +20076,12 @@ function filteredTargets(entries, options) {
19298
20076
  if (options.kinds) targets = targets.filter((entry) => options.kinds?.has(entry.kind));
19299
20077
  return targets;
19300
20078
  }
19301
- function formatNumber(value, digits = 1) {
20079
+ function formatNumber2(value, digits = 1) {
19302
20080
  if (!Number.isFinite(value)) return String(value);
19303
20081
  return value.toFixed(digits).replace(/\.0$/, "");
19304
20082
  }
19305
- function formatBounds(min, max) {
19306
- return `[${min.map((v) => formatNumber(v)).join(",")}]..[${max.map((v) => formatNumber(v)).join(",")}]`;
20083
+ function formatBounds2(min, max) {
20084
+ return `[${min.map((v) => formatNumber2(v)).join(",")}]..[${max.map((v) => formatNumber2(v)).join(",")}]`;
19307
20085
  }
19308
20086
  function targetMetrics(entry) {
19309
20087
  const object = entry.object;
@@ -19340,7 +20118,7 @@ function printLineList(entries, long) {
19340
20118
  continue;
19341
20119
  }
19342
20120
  const metrics = targetMetrics(entry);
19343
- const detail = metrics && "volume" in metrics ? ` id=${entry.id} vol=${formatNumber(metrics.volume)} tris=${metrics.triangles.toLocaleString()} bodies=${metrics.bodies} bbox=${formatBounds(metrics.bounds.min, metrics.bounds.max)}` : metrics && "area" in metrics ? ` id=${entry.id} area=${formatNumber(metrics.area)} regions=${metrics.regions}` : ` id=${entry.id}`;
20121
+ const detail = metrics && "volume" in metrics ? ` id=${entry.id} vol=${formatNumber2(metrics.volume)} tris=${metrics.triangles.toLocaleString()} bodies=${metrics.bodies} bbox=${formatBounds2(metrics.bounds.min, metrics.bounds.max)}` : metrics && "area" in metrics ? ` id=${entry.id} area=${formatNumber2(metrics.area)} regions=${metrics.regions}` : ` id=${entry.id}`;
19344
20122
  console.log(`${base}${detail}`);
19345
20123
  }
19346
20124
  }
@@ -19364,7 +20142,7 @@ async function runLsCli(argv = process.argv.slice(2)) {
19364
20142
  options = parseLsOptions(argv);
19365
20143
  } catch (error) {
19366
20144
  console.error(error instanceof Error ? error.message : String(error));
19367
- console.error(usage12());
20145
+ console.error(usage13());
19368
20146
  process.exit(1);
19369
20147
  }
19370
20148
  const { overrides } = parseParamFlags(argv);
@@ -19635,6 +20413,11 @@ async function runInspectEvidenceListCli(argv) {
19635
20413
  process.exit(1);
19636
20414
  }
19637
20415
  const commands2 = [
20416
+ {
20417
+ command: "inspect sketch",
20418
+ evidence: "sketch",
20419
+ description: "Inspect returned sketch/profile regions and seed selector dry-runs."
20420
+ },
19638
20421
  {
19639
20422
  command: "inspect visual image",
19640
20423
  evidence: "image",
@@ -19643,6 +20426,7 @@ async function runInspectEvidenceListCli(argv) {
19643
20426
  { command: "inspect visual cutaway", evidence: "cutaway", description: "Capture a clipped 3D viewport from the cut side." },
19644
20427
  { command: "inspect visual depth", evidence: "depth", description: "Capture visible surface depth evidence." },
19645
20428
  { command: "inspect visual normals", evidence: "normals", description: "Capture camera-view surface normal evidence." },
20429
+ { command: "inspect visual rig", evidence: "rig", description: "Capture kinematic rig skeleton evidence." },
19646
20430
  { command: "inspect visual objects", evidence: "objects", description: "Capture object identity evidence." },
19647
20431
  { command: "inspect surface zebra", evidence: "zebra", description: "Capture Zebra stripe surface-continuity evidence." },
19648
20432
  { command: "inspect surface roughness", evidence: "roughness", description: "Capture mesh roughness and sharp-feature evidence." },
@@ -19680,7 +20464,7 @@ async function runInspectEvidenceListCli(argv) {
19680
20464
  }
19681
20465
 
19682
20466
  // cli/forge-render-hq.ts
19683
- import { writeFileSync as writeFileSync8, mkdtempSync as mkdtempSync2, rmSync as rmSync2, existsSync as existsSync10 } from "fs";
20467
+ import { writeFileSync as writeFileSync8, mkdtempSync as mkdtempSync2, rmSync as rmSync2, existsSync as existsSync9 } from "fs";
19684
20468
  import { resolve as resolve22, dirname as dirname5, join as join7, extname as extname7 } from "path";
19685
20469
  import { execSync as execSync2, spawnSync } from "child_process";
19686
20470
  import { tmpdir as tmpdir2 } from "os";
@@ -19897,7 +20681,7 @@ function parseArgs7(argv) {
19897
20681
  i += 1;
19898
20682
  } else if (arg === "--hdri") {
19899
20683
  hdriPath = resolve22(next);
19900
- if (!existsSync10(hdriPath)) throw new Error(`HDRI file not found: ${hdriPath}`);
20684
+ if (!existsSync9(hdriPath)) throw new Error(`HDRI file not found: ${hdriPath}`);
19901
20685
  i += 1;
19902
20686
  } else if (arg === "--camera") {
19903
20687
  const value = readArgValue(argv, i, arg);
@@ -20002,11 +20786,11 @@ function findBlender() {
20002
20786
  `${process.env.HOME}/Applications/Blender.app/Contents/MacOS/Blender`
20003
20787
  ];
20004
20788
  for (const p of macPaths) {
20005
- if (existsSync10(p)) return p;
20789
+ if (existsSync9(p)) return p;
20006
20790
  }
20007
20791
  const linuxPaths = ["/usr/bin/blender", "/snap/bin/blender", "/usr/local/bin/blender"];
20008
20792
  for (const p of linuxPaths) {
20009
- if (existsSync10(p)) return p;
20793
+ if (existsSync9(p)) return p;
20010
20794
  }
20011
20795
  throw new Error(
20012
20796
  "Blender not found. Install it:\n macOS: brew install --cask blender\n Linux: sudo apt install blender (or snap install blender)\n All: https://www.blender.org/download/"
@@ -20021,7 +20805,7 @@ function defaultOutputPath5(scriptPath, video) {
20021
20805
  function resolveBlenderRenderScript() {
20022
20806
  const cliDir = dirname5(fileURLToPath2(import.meta.url));
20023
20807
  const renderScript = join7(cliDir, "blender", "render.py");
20024
- if (!existsSync10(renderScript)) {
20808
+ if (!existsSync9(renderScript)) {
20025
20809
  throw new Error(
20026
20810
  `Blender render script missing at ${renderScript}. Rebuild the CLI with \`npm run build:cli\`; packaged installs should include dist-cli/blender/render.py.`
20027
20811
  );
@@ -20199,7 +20983,7 @@ async function runRenderHqCli(argv) {
20199
20983
  rmSync2(tmpDir, { recursive: true, force: true });
20200
20984
  } catch {
20201
20985
  }
20202
- if (existsSync10(outputPath)) {
20986
+ if (existsSync9(outputPath)) {
20203
20987
  const stats = (await import("fs")).statSync(outputPath);
20204
20988
  const sizeMb = (stats.size / 1024 / 1024).toFixed(1);
20205
20989
  console.log(`
@@ -20225,12 +21009,12 @@ import { basename as basename9, resolve as resolve23 } from "path";
20225
21009
  function defaultPngOutput(scriptPath) {
20226
21010
  return scriptPath.replace(/\.(forge|sketch)\.js$/, ".png").replace(/\.js$/, ".png");
20227
21011
  }
20228
- function usage13() {
21012
+ function usage14() {
20229
21013
  console.error("Usage: forgecad render sketch <script.forge.js> [output.png] [--size <px>] [--chrome-path <path>]");
20230
21014
  process.exit(1);
20231
21015
  }
20232
21016
  function parseCli2(argv) {
20233
- if (argv.length === 0 || argv.includes("-h") || argv.includes("--help")) usage13();
21017
+ if (argv.length === 0 || argv.includes("-h") || argv.includes("--help")) usage14();
20234
21018
  let scriptPath;
20235
21019
  let outputPath;
20236
21020
  let size = 1024;
@@ -20255,7 +21039,7 @@ function parseCli2(argv) {
20255
21039
  else if (!outputPath) outputPath = arg;
20256
21040
  else throw new Error(`Unexpected argument: ${arg}`);
20257
21041
  }
20258
- if (!scriptPath) usage13();
21042
+ if (!scriptPath) usage14();
20259
21043
  if (!Number.isFinite(size) || size < 128 || size > 4096) throw new Error(`--size must be between 128 and 4096 (got ${size})`);
20260
21044
  return {
20261
21045
  scriptPath,
@@ -20271,7 +21055,7 @@ async function runRender2dCli(argv = process.argv.slice(2)) {
20271
21055
  options = parseCli2(argv);
20272
21056
  } catch (err) {
20273
21057
  console.error(String(err));
20274
- usage13();
21058
+ usage14();
20275
21059
  }
20276
21060
  const chromePath = findChromePath(options.chromePath);
20277
21061
  if (!chromePath) {
@@ -20390,13 +21174,13 @@ function formatSheetCutList(input) {
20390
21174
  }
20391
21175
 
20392
21176
  // cli/forge-cut-list.ts
20393
- function usage14() {
21177
+ function usage15() {
20394
21178
  console.error("Usage: forgecad cut-list <script.forge.js>");
20395
21179
  process.exit(1);
20396
21180
  }
20397
21181
  async function runCutListCli(argv = process.argv.slice(2)) {
20398
21182
  const scriptPath = argv[0];
20399
- if (!scriptPath) usage14();
21183
+ if (!scriptPath) usage15();
20400
21184
  const source = await readFile2(resolve24(scriptPath), "utf-8");
20401
21185
  const { allFiles, fileName } = collectProjectFiles(scriptPath);
20402
21186
  await init();
@@ -20429,7 +21213,7 @@ function hasFlag(argv, name) {
20429
21213
  const prefix = `${name}=`;
20430
21214
  return argv.some((arg) => arg === name || arg.startsWith(prefix));
20431
21215
  }
20432
- function usage15() {
21216
+ function usage16() {
20433
21217
  console.error(
20434
21218
  "Usage: forgecad export cutting-layout <script.forge.js> [output.pdf|output.dxf]\n [--format pdf|dxf] [--sheet-width <mm>] [--sheet-height <mm>] [--kerf <mm>]"
20435
21219
  );
@@ -20493,7 +21277,7 @@ function smartDefaults(entries) {
20493
21277
  }
20494
21278
  async function runCuttingLayoutCli(argv = process.argv.slice(2)) {
20495
21279
  const scriptPath = argv[0];
20496
- if (!scriptPath) usage15();
21280
+ if (!scriptPath) usage16();
20497
21281
  const explicitOutputPath = outputPathArg(argv);
20498
21282
  const formatArg = argValue(argv, "--format");
20499
21283
  if (hasFlag(argv, "--format") && formatArg == null) {
@@ -20556,7 +21340,7 @@ function argValue2(argv, name) {
20556
21340
  if (idx === -1) return void 0;
20557
21341
  return argv[idx + 1];
20558
21342
  }
20559
- function usage16() {
21343
+ function usage17() {
20560
21344
  console.error(
20561
21345
  "Usage: forgecad export report <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [output.pdf] [--dim-angle-tol <deg>]"
20562
21346
  );
@@ -20564,7 +21348,7 @@ function usage16() {
20564
21348
  }
20565
21349
  async function runReportCli(argv = process.argv.slice(2)) {
20566
21350
  const scriptPath = argv[0];
20567
- if (!scriptPath) usage16();
21351
+ if (!scriptPath) usage17();
20568
21352
  const defaultOut = replaceCliInputExtension(scriptPath, ".report.pdf");
20569
21353
  const outputPath = argv[1] && !argv[1].startsWith("--") ? argv[1] : defaultOut;
20570
21354
  const toleranceArg = argValue2(argv, "--dim-angle-tol");
@@ -20628,16 +21412,16 @@ function mmToM(valueMm) {
20628
21412
  function degToRad(valueDeg) {
20629
21413
  return valueDeg * Math.PI / 180;
20630
21414
  }
20631
- function formatNumber2(value, digits = 6) {
21415
+ function formatNumber3(value, digits = 6) {
20632
21416
  if (!Number.isFinite(value)) return "0";
20633
21417
  const normalized = Math.abs(value) < 1e-12 ? 0 : value;
20634
21418
  return normalized.toFixed(digits).replace(/\.?0+$/, "");
20635
21419
  }
20636
21420
  function formatPose(parts) {
20637
- return [...parts.xyzM.map((value) => formatNumber2(value, 6)), ...parts.rpyRad.map((value) => formatNumber2(value, 6))].join(" ");
21421
+ return [...parts.xyzM.map((value) => formatNumber3(value, 6)), ...parts.rpyRad.map((value) => formatNumber3(value, 6))].join(" ");
20638
21422
  }
20639
21423
  function axisToText(axis) {
20640
- return axis.map((value) => formatNumber2(value, 6)).join(" ");
21424
+ return axis.map((value) => formatNumber3(value, 6)).join(" ");
20641
21425
  }
20642
21426
  function transformToPose(transform) {
20643
21427
  const m = transform.toArray();
@@ -20783,15 +21567,15 @@ function inertiaFromBounds(geometry, massKg) {
20783
21567
  }
20784
21568
  function jointTypeLimitUnits(joint2, value) {
20785
21569
  if (value === void 0) return null;
20786
- if (joint2.type === "revolute") return formatNumber2(degToRad(value), 6);
20787
- if (joint2.type === "prismatic") return formatNumber2(mmToM(value), 6);
21570
+ if (joint2.type === "revolute") return formatNumber3(degToRad(value), 6);
21571
+ if (joint2.type === "prismatic") return formatNumber3(mmToM(value), 6);
20788
21572
  return null;
20789
21573
  }
20790
21574
  function jointVelocityUnits(joint2, value) {
20791
21575
  if (value === void 0) return null;
20792
- if (joint2.type === "revolute") return formatNumber2(degToRad(value), 6);
20793
- if (joint2.type === "prismatic") return formatNumber2(mmToM(value), 6);
20794
- return formatNumber2(value, 6);
21576
+ if (joint2.type === "revolute") return formatNumber3(degToRad(value), 6);
21577
+ if (joint2.type === "prismatic") return formatNumber3(mmToM(value), 6);
21578
+ return formatNumber3(value, 6);
20795
21579
  }
20796
21580
  function sRgbFloat(hex) {
20797
21581
  if (!hex || !/^#([0-9a-f]{6})$/i.test(hex)) return null;
@@ -20820,14 +21604,14 @@ function demoWorldName(world, modelName) {
20820
21604
  }
20821
21605
  function keyboardPluginXml(cmdVelTopic, linearStep, angularStep) {
20822
21606
  const bindings = [
20823
- { key: 87, twist: `linear: {x: ${formatNumber2(linearStep, 3)}}, angular: {z: 0.0}` },
20824
- { key: 88, twist: `linear: {x: ${formatNumber2(-linearStep, 3)}}, angular: {z: 0.0}` },
20825
- { key: 65, twist: `linear: {x: 0.0}, angular: {z: ${formatNumber2(angularStep, 3)}}` },
20826
- { key: 68, twist: `linear: {x: 0.0}, angular: {z: ${formatNumber2(-angularStep, 3)}}` },
20827
- { key: 81, twist: `linear: {x: ${formatNumber2(linearStep, 3)}}, angular: {z: ${formatNumber2(angularStep, 3)}}` },
20828
- { key: 69, twist: `linear: {x: ${formatNumber2(linearStep, 3)}}, angular: {z: ${formatNumber2(-angularStep, 3)}}` },
20829
- { key: 90, twist: `linear: {x: ${formatNumber2(-linearStep, 3)}}, angular: {z: ${formatNumber2(angularStep, 3)}}` },
20830
- { key: 67, twist: `linear: {x: ${formatNumber2(-linearStep, 3)}}, angular: {z: ${formatNumber2(-angularStep, 3)}}` },
21607
+ { key: 87, twist: `linear: {x: ${formatNumber3(linearStep, 3)}}, angular: {z: 0.0}` },
21608
+ { key: 88, twist: `linear: {x: ${formatNumber3(-linearStep, 3)}}, angular: {z: 0.0}` },
21609
+ { key: 65, twist: `linear: {x: 0.0}, angular: {z: ${formatNumber3(angularStep, 3)}}` },
21610
+ { key: 68, twist: `linear: {x: 0.0}, angular: {z: ${formatNumber3(-angularStep, 3)}}` },
21611
+ { key: 81, twist: `linear: {x: ${formatNumber3(linearStep, 3)}}, angular: {z: ${formatNumber3(angularStep, 3)}}` },
21612
+ { key: 69, twist: `linear: {x: ${formatNumber3(linearStep, 3)}}, angular: {z: ${formatNumber3(-angularStep, 3)}}` },
21613
+ { key: 90, twist: `linear: {x: ${formatNumber3(-linearStep, 3)}}, angular: {z: ${formatNumber3(angularStep, 3)}}` },
21614
+ { key: 67, twist: `linear: {x: ${formatNumber3(-linearStep, 3)}}, angular: {z: ${formatNumber3(-angularStep, 3)}}` },
20831
21615
  { key: 83, twist: "linear: {x: 0.0}, angular: {z: 0.0}" },
20832
21616
  { key: 32, twist: "linear: {x: 0.0}, angular: {z: 0.0}" }
20833
21617
  ];
@@ -20987,12 +21771,12 @@ ${keyboardGuiPluginXml()}` : "";
20987
21771
  function demoWorldXml(worldName, modelName, cmdVelTopic, world) {
20988
21772
  const spawnPose = world?.spawnPose ?? [0, 0, 120, 0, 0, 0];
20989
21773
  const spawnPoseText = [
20990
- formatNumber2(mmToM(spawnPose[0]), 6),
20991
- formatNumber2(mmToM(spawnPose[1]), 6),
20992
- formatNumber2(mmToM(spawnPose[2]), 6),
20993
- formatNumber2(degToRad(spawnPose[3]), 6),
20994
- formatNumber2(degToRad(spawnPose[4]), 6),
20995
- formatNumber2(degToRad(spawnPose[5]), 6)
21774
+ formatNumber3(mmToM(spawnPose[0]), 6),
21775
+ formatNumber3(mmToM(spawnPose[1]), 6),
21776
+ formatNumber3(mmToM(spawnPose[2]), 6),
21777
+ formatNumber3(degToRad(spawnPose[3]), 6),
21778
+ formatNumber3(degToRad(spawnPose[4]), 6),
21779
+ formatNumber3(degToRad(spawnPose[5]), 6)
20996
21780
  ].join(" ");
20997
21781
  const keyboardEnabled = world?.keyboardTeleop?.enabled ?? true;
20998
21782
  const linearStep = world?.keyboardTeleop?.linearStep ?? 0.9;
@@ -21162,28 +21946,28 @@ function modelXml(spec, modelName, linkNameMap, jointNameMap, geometries, linkWo
21162
21946
  const color = sRgbFloat(geometry.shapes[0]?.colorHex);
21163
21947
  const materialXml = color ? `
21164
21948
  <material>
21165
- <ambient>${formatNumber2(color[0], 3)} ${formatNumber2(color[1], 3)} ${formatNumber2(color[2], 3)} 1</ambient>
21166
- <diffuse>${formatNumber2(color[0], 3)} ${formatNumber2(color[1], 3)} ${formatNumber2(color[2], 3)} 1</diffuse>
21949
+ <ambient>${formatNumber3(color[0], 3)} ${formatNumber3(color[1], 3)} ${formatNumber3(color[2], 3)} 1</ambient>
21950
+ <diffuse>${formatNumber3(color[0], 3)} ${formatNumber3(color[1], 3)} ${formatNumber3(color[2], 3)} 1</diffuse>
21167
21951
  </material>` : "";
21168
21952
  return ` <link name="${escapeXml(sdfLinkName)}">
21169
21953
  <pose relative_to="__model__">${formatPose(worldPose)}</pose>
21170
21954
  <inertial>
21171
21955
  <pose>${formatPose(inertia.pose)}</pose>
21172
- <mass>${formatNumber2(massKg, 6)}</mass>
21956
+ <mass>${formatNumber3(massKg, 6)}</mass>
21173
21957
  <inertia>
21174
- <ixx>${formatNumber2(inertia.ixx, 8)}</ixx>
21175
- <ixy>${formatNumber2(inertia.ixy, 8)}</ixy>
21176
- <ixz>${formatNumber2(inertia.ixz, 8)}</ixz>
21177
- <iyy>${formatNumber2(inertia.iyy, 8)}</iyy>
21178
- <iyz>${formatNumber2(inertia.iyz, 8)}</iyz>
21179
- <izz>${formatNumber2(inertia.izz, 8)}</izz>
21958
+ <ixx>${formatNumber3(inertia.ixx, 8)}</ixx>
21959
+ <ixy>${formatNumber3(inertia.ixy, 8)}</ixy>
21960
+ <ixz>${formatNumber3(inertia.ixz, 8)}</ixz>
21961
+ <iyy>${formatNumber3(inertia.iyy, 8)}</iyy>
21962
+ <iyz>${formatNumber3(inertia.iyz, 8)}</iyz>
21963
+ <izz>${formatNumber3(inertia.izz, 8)}</izz>
21180
21964
  </inertia>
21181
21965
  </inertial>
21182
21966
  <visual name="${escapeXml(sdfLinkName)}_visual">
21183
21967
  <geometry>
21184
21968
  <mesh>
21185
21969
  <uri>${escapeXml(meshPath)}</uri>
21186
- <scale>${formatNumber2(STL_SCALE_METERS, 6)} ${formatNumber2(STL_SCALE_METERS, 6)} ${formatNumber2(STL_SCALE_METERS, 6)}</scale>
21970
+ <scale>${formatNumber3(STL_SCALE_METERS, 6)} ${formatNumber3(STL_SCALE_METERS, 6)} ${formatNumber3(STL_SCALE_METERS, 6)}</scale>
21187
21971
  </mesh>
21188
21972
  </geometry>${materialXml}
21189
21973
  </visual>${(() => {
@@ -21193,7 +21977,7 @@ function modelXml(spec, modelName, linkNameMap, jointNameMap, geometries, linkWo
21193
21977
  <geometry>
21194
21978
  <mesh>
21195
21979
  <uri>${escapeXml(meshPath)}</uri>
21196
- <scale>${formatNumber2(STL_SCALE_METERS, 6)} ${formatNumber2(STL_SCALE_METERS, 6)} ${formatNumber2(STL_SCALE_METERS, 6)}</scale>
21980
+ <scale>${formatNumber3(STL_SCALE_METERS, 6)} ${formatNumber3(STL_SCALE_METERS, 6)} ${formatNumber3(STL_SCALE_METERS, 6)}</scale>
21197
21981
  </mesh>
21198
21982
  </geometry>
21199
21983
  </collision>`;
@@ -21205,7 +21989,7 @@ function modelXml(spec, modelName, linkNameMap, jointNameMap, geometries, linkWo
21205
21989
  <geometry>
21206
21990
  <mesh>
21207
21991
  <uri>${escapeXml(collisionMeshPath)}</uri>
21208
- <scale>${formatNumber2(STL_SCALE_METERS, 6)} ${formatNumber2(STL_SCALE_METERS, 6)} ${formatNumber2(STL_SCALE_METERS, 6)}</scale>
21992
+ <scale>${formatNumber3(STL_SCALE_METERS, 6)} ${formatNumber3(STL_SCALE_METERS, 6)} ${formatNumber3(STL_SCALE_METERS, 6)}</scale>
21209
21993
  </mesh>
21210
21994
  </geometry>
21211
21995
  </collision>`;
@@ -21219,9 +22003,9 @@ function modelXml(spec, modelName, linkNameMap, jointNameMap, geometries, linkWo
21219
22003
  const bCz = mmToM((geometry.bboxMin[2] + geometry.bboxMax[2]) * 0.5);
21220
22004
  return `
21221
22005
  <collision name="${escapeXml(sdfLinkName)}_collision">
21222
- <pose>${formatNumber2(bCx, 6)} ${formatNumber2(bCy, 6)} ${formatNumber2(bCz, 6)} 0 0 0</pose>
22006
+ <pose>${formatNumber3(bCx, 6)} ${formatNumber3(bCy, 6)} ${formatNumber3(bCz, 6)} 0 0 0</pose>
21223
22007
  <geometry>
21224
- <box><size>${formatNumber2(bDx, 6)} ${formatNumber2(bDy, 6)} ${formatNumber2(bDz, 6)}</size></box>
22008
+ <box><size>${formatNumber3(bDx, 6)} ${formatNumber3(bDy, 6)} ${formatNumber3(bDz, 6)}</size></box>
21225
22009
  </geometry>
21226
22010
  </collision>`;
21227
22011
  }
@@ -21248,8 +22032,8 @@ function modelXml(spec, modelName, linkNameMap, jointNameMap, geometries, linkWo
21248
22032
  const leaderSdfName = jointNameMap.get(`${primary.joint}_joint`);
21249
22033
  mimicXml = `
21250
22034
  <mimic joint="${escapeXml(leaderSdfName)}">
21251
- <multiplier>${formatNumber2(primary.ratio, 6)}</multiplier>
21252
- <offset>${formatNumber2(coupling.offset, 6)}</offset>
22035
+ <multiplier>${formatNumber3(primary.ratio, 6)}</multiplier>
22036
+ <offset>${formatNumber3(coupling.offset, 6)}</offset>
21253
22037
  </mimic>`;
21254
22038
  if (coupling.terms.length > 1) {
21255
22039
  warnings.push(
@@ -21266,12 +22050,12 @@ function modelXml(spec, modelName, linkNameMap, jointNameMap, geometries, linkWo
21266
22050
  <limit>${limitLower !== null ? `
21267
22051
  <lower>${limitLower}</lower>` : ""}${limitUpper !== null ? `
21268
22052
  <upper>${limitUpper}</upper>` : ""}${effort !== void 0 ? `
21269
- <effort>${formatNumber2(effort, 6)}</effort>` : ""}${velocity !== null ? `
22053
+ <effort>${formatNumber3(effort, 6)}</effort>` : ""}${velocity !== null ? `
21270
22054
  <velocity>${velocity}</velocity>` : ""}
21271
22055
  </limit>` : ""}${damping !== void 0 || friction !== void 0 ? `
21272
22056
  <dynamics>${damping !== void 0 ? `
21273
- <damping>${formatNumber2(damping, 6)}</damping>` : ""}${friction !== void 0 ? `
21274
- <friction>${formatNumber2(friction, 6)}</friction>` : ""}
22057
+ <damping>${formatNumber3(damping, 6)}</damping>` : ""}${friction !== void 0 ? `
22058
+ <friction>${formatNumber3(friction, 6)}</friction>` : ""}
21275
22059
  </dynamics>` : ""}
21276
22060
  </axis>` : ""}
21277
22061
  </joint>`;
@@ -21281,17 +22065,17 @@ function modelXml(spec, modelName, linkNameMap, jointNameMap, geometries, linkWo
21281
22065
  plugins.push(` <plugin filename="gz-sim-diff-drive-system" name="gz::sim::systems::DiffDrive">
21282
22066
  ${spec.plugins.diffDrive.leftJoints.map((jointName) => `<left_joint>${escapeXml(jointNameMap.get(`${jointName}_joint`))}</left_joint>`).join("\n ")}
21283
22067
  ${spec.plugins.diffDrive.rightJoints.map((jointName) => `<right_joint>${escapeXml(jointNameMap.get(`${jointName}_joint`))}</right_joint>`).join("\n ")}
21284
- <wheel_separation>${formatNumber2(mmToM(spec.plugins.diffDrive.wheelSeparationMm), 6)}</wheel_separation>
21285
- <wheel_radius>${formatNumber2(mmToM(spec.plugins.diffDrive.wheelRadiusMm), 6)}</wheel_radius>
22068
+ <wheel_separation>${formatNumber3(mmToM(spec.plugins.diffDrive.wheelSeparationMm), 6)}</wheel_separation>
22069
+ <wheel_radius>${formatNumber3(mmToM(spec.plugins.diffDrive.wheelRadiusMm), 6)}</wheel_radius>
21286
22070
  <topic>${escapeXml(cmdVelTopic)}</topic>${spec.plugins.diffDrive.odomTopic ? `
21287
22071
  <odom_topic>${escapeXml(spec.plugins.diffDrive.odomTopic)}</odom_topic>` : ""}${spec.plugins.diffDrive.tfTopic ? `
21288
22072
  <tf_topic>${escapeXml(spec.plugins.diffDrive.tfTopic)}</tf_topic>` : ""}${spec.plugins.diffDrive.frameId ? `
21289
22073
  <frame_id>${escapeXml(spec.plugins.diffDrive.frameId)}</frame_id>` : ""}${spec.plugins.diffDrive.odomFrameId ? `
21290
22074
  <odom_frame>${escapeXml(spec.plugins.diffDrive.odomFrameId)}</odom_frame>` : ""}${spec.plugins.diffDrive.maxLinearVelocity !== void 0 ? `
21291
- <max_linear_velocity>${formatNumber2(spec.plugins.diffDrive.maxLinearVelocity, 6)}</max_linear_velocity>` : ""}${spec.plugins.diffDrive.maxAngularVelocity !== void 0 ? `
21292
- <max_angular_velocity>${formatNumber2(spec.plugins.diffDrive.maxAngularVelocity, 6)}</max_angular_velocity>` : ""}${spec.plugins.diffDrive.linearAcceleration !== void 0 ? `
21293
- <linear_acceleration>${formatNumber2(spec.plugins.diffDrive.linearAcceleration, 6)}</linear_acceleration>` : ""}${spec.plugins.diffDrive.angularAcceleration !== void 0 ? `
21294
- <angular_acceleration>${formatNumber2(spec.plugins.diffDrive.angularAcceleration, 6)}</angular_acceleration>` : ""}
22075
+ <max_linear_velocity>${formatNumber3(spec.plugins.diffDrive.maxLinearVelocity, 6)}</max_linear_velocity>` : ""}${spec.plugins.diffDrive.maxAngularVelocity !== void 0 ? `
22076
+ <max_angular_velocity>${formatNumber3(spec.plugins.diffDrive.maxAngularVelocity, 6)}</max_angular_velocity>` : ""}${spec.plugins.diffDrive.linearAcceleration !== void 0 ? `
22077
+ <linear_acceleration>${formatNumber3(spec.plugins.diffDrive.linearAcceleration, 6)}</linear_acceleration>` : ""}${spec.plugins.diffDrive.angularAcceleration !== void 0 ? `
22078
+ <angular_acceleration>${formatNumber3(spec.plugins.diffDrive.angularAcceleration, 6)}</angular_acceleration>` : ""}
21295
22079
  </plugin>`);
21296
22080
  }
21297
22081
  const jointState = spec.plugins.jointStatePublisher;
@@ -21301,7 +22085,7 @@ function modelXml(spec, modelName, linkNameMap, jointNameMap, geometries, linkWo
21301
22085
  (jointName) => `
21302
22086
  <joint_name>${escapeXml(jointNameMap.get(`${jointName}_joint`))}</joint_name>`
21303
22087
  ).join("")}${jointState?.updateRate !== void 0 ? `
21304
- <update_rate>${formatNumber2(jointState.updateRate, 6)}</update_rate>` : ""}
22088
+ <update_rate>${formatNumber3(jointState.updateRate, 6)}</update_rate>` : ""}
21305
22089
  </plugin>`);
21306
22090
  }
21307
22091
  const rootNames = spec.assembly.parts.filter((part) => !spec.assembly.joints.some((joint2) => joint2.child === part.name)).map((part) => linkNameMap.get(part.name));
@@ -21513,7 +22297,7 @@ var FORWARDED_VALUE_OPTIONS = /* @__PURE__ */ new Set([
21513
22297
  ]);
21514
22298
  var FORWARDED_FLAG_OPTIONS = /* @__PURE__ */ new Set(["--fresh-server"]);
21515
22299
  var CAMERA_SOURCE_OPTIONS = /* @__PURE__ */ new Set(["--camera", "--camera-json", "--view", "--scene"]);
21516
- function usage17() {
22300
+ function usage18() {
21517
22301
  return [
21518
22302
  "Usage: forgecad show <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [target] [output.png] [--from front|back|side|right|top|iso|az:el|az:el:dist] [--out output.png] [--param Key=Value] [--backend manifold|occt|truck] [render options]"
21519
22303
  ].join("\n");
@@ -21528,7 +22312,7 @@ function readValue6(argv, index, flag) {
21528
22312
  }
21529
22313
  function parseShowOptions(argv) {
21530
22314
  if (argv.length === 0 || argv.includes("-h") || argv.includes("--help")) {
21531
- console.log(usage17());
22315
+ console.log(usage18());
21532
22316
  process.exit(0);
21533
22317
  }
21534
22318
  const consumed = /* @__PURE__ */ new Set();
@@ -21594,7 +22378,7 @@ async function runShowCli(argv = process.argv.slice(2)) {
21594
22378
  options = parseShowOptions(argv);
21595
22379
  } catch (error) {
21596
22380
  console.error(error instanceof Error ? error.message : String(error));
21597
- console.error(usage17());
22381
+ console.error(usage18());
21598
22382
  process.exit(1);
21599
22383
  }
21600
22384
  const renderArgs = [options.scriptPath];
@@ -22175,7 +22959,7 @@ function generateSketchPdf(meta, options) {
22175
22959
  }
22176
22960
 
22177
22961
  // cli/forge-sketch-pdf.ts
22178
- function usage18() {
22962
+ function usage19() {
22179
22963
  console.error("Usage: forgecad export sketch-pdf <script.forge.js> [output.pdf]");
22180
22964
  process.exit(1);
22181
22965
  }
@@ -22184,7 +22968,7 @@ function defaultPdfOutput(scriptPath) {
22184
22968
  }
22185
22969
  async function runSketchPdfCli(argv = process.argv.slice(2)) {
22186
22970
  const scriptPath = argv[0];
22187
- if (!scriptPath) usage18();
22971
+ if (!scriptPath) usage19();
22188
22972
  const outputPath = argv[1] || defaultPdfOutput(scriptPath);
22189
22973
  if (resolve28(outputPath) === resolve28(scriptPath)) {
22190
22974
  console.error(`ERROR: output path would overwrite the input script. Specify an explicit output path.`);
@@ -22217,7 +23001,7 @@ async function runSketchPdfCli(argv = process.argv.slice(2)) {
22217
23001
  }
22218
23002
 
22219
23003
  // cli/forge-skill.ts
22220
- import { cpSync, existsSync as existsSync11, mkdirSync as mkdirSync4, readdirSync as readdirSync6, readFileSync as readFileSync15, rmSync as rmSync3, writeFileSync as writeFileSync10 } from "fs";
23004
+ import { cpSync, existsSync as existsSync10, mkdirSync as mkdirSync4, readdirSync as readdirSync6, readFileSync as readFileSync15, rmSync as rmSync3, writeFileSync as writeFileSync10 } from "fs";
22221
23005
  import { homedir as homedir6 } from "os";
22222
23006
  import { extname as extname9, join as join8, relative as relative5, resolve as resolve29 } from "path";
22223
23007
  var INSTALL_TARGETS = {
@@ -22230,7 +23014,7 @@ var ALL_INSTALL_TARGETS = Object.keys(INSTALL_TARGETS);
22230
23014
  var DEFAULT_INSTALL_TARGET = "agents";
22231
23015
  function installUsage() {
22232
23016
  return [
22233
- "Usage: forgecad skill install [--target agents|claude|codex|opencode|all] [--core-only] [--dev]",
23017
+ "Usage: forgecad skill install [--target agents|claude|codex|opencode|all] [--core-only]",
22234
23018
  "Examples:",
22235
23019
  " forgecad skill install",
22236
23020
  " forgecad skill install --target claude",
@@ -22261,15 +23045,12 @@ function uniqueTargets(targets) {
22261
23045
  return [...new Set(targets)];
22262
23046
  }
22263
23047
  function parseInstallArgs(argv) {
22264
- let isDev = false;
22265
23048
  let coreOnly = false;
22266
23049
  let sawLibraryAlias = false;
22267
23050
  let targets = [DEFAULT_INSTALL_TARGET];
22268
23051
  for (let i = 0; i < argv.length; i++) {
22269
23052
  const arg = argv[i];
22270
- if (arg === "--dev") {
22271
- isDev = true;
22272
- } else if (arg === "--library" || arg === "--all") {
23053
+ if (arg === "--library" || arg === "--all") {
22273
23054
  sawLibraryAlias = true;
22274
23055
  } else if (arg === "--core-only") {
22275
23056
  coreOnly = true;
@@ -22297,13 +23078,12 @@ ${installUsage()}`);
22297
23078
  if (coreOnly && sawLibraryAlias) {
22298
23079
  throw new Error("`--core-only` cannot be combined with `--library` or `--all`.");
22299
23080
  }
22300
- return { isDev, includeLibrary: !coreOnly, targets: uniqueTargets(targets) };
23081
+ return { includeLibrary: !coreOnly, targets: uniqueTargets(targets) };
22301
23082
  }
22302
- function installCoreSkill(isDev, targetRoot) {
22303
- const skillFile = isDev ? "SKILL-dev.md" : "SKILL.md";
22304
- const srcSkill = resolvePackagePath(import.meta.url, "dist-skill", skillFile);
22305
- const srcDocs = resolvePackagePath(import.meta.url, "dist-skill", isDev ? "docs-dev" : "docs");
22306
- if (!existsSync11(srcSkill)) {
23083
+ function installCoreSkill(targetRoot) {
23084
+ const srcSkill = resolvePackagePath(import.meta.url, "dist-skill", "SKILL.md");
23085
+ const srcDocs = resolvePackagePath(import.meta.url, "dist-skill", "docs");
23086
+ if (!existsSync10(srcSkill)) {
22307
23087
  throw new Error(
22308
23088
  `Built skill file not found at ${srcSkill}.
22309
23089
  If you are running from a source checkout, run: npm run build:skill:forgecad`
@@ -22314,18 +23094,17 @@ If you are running from a source checkout, run: npm run build:skill:forgecad`
22314
23094
  mkdirSync4(destDir, { recursive: true });
22315
23095
  const skillContent = readFileSync15(srcSkill, "utf-8").replaceAll("{{SKILL_DIR}}", destDir);
22316
23096
  writeFileSync10(dest, skillContent);
22317
- if (existsSync11(srcDocs)) {
23097
+ if (existsSync10(srcDocs)) {
22318
23098
  const destDocs = join8(destDir, "docs");
22319
- if (existsSync11(destDocs)) rmSync3(destDocs, { recursive: true });
23099
+ if (existsSync10(destDocs)) rmSync3(destDocs, { recursive: true });
22320
23100
  cpSync(srcDocs, destDocs, { recursive: true });
22321
23101
  }
22322
- const mode = isDev ? "dev (includes internals, conventions, skill maintenance)" : "standard (model authoring)";
22323
- console.log(`ForgeCAD skill installed to ${dest} [${mode}]`);
23102
+ console.log(`ForgeCAD skill installed to ${dest} [standard model authoring]`);
22324
23103
  return dest;
22325
23104
  }
22326
23105
  function installCompanionLibrary(targetRoot) {
22327
23106
  const srcLibrary = resolvePackagePath(import.meta.url, "dist-skill", "library");
22328
- if (!existsSync11(srcLibrary)) {
23107
+ if (!existsSync10(srcLibrary)) {
22329
23108
  throw new Error(
22330
23109
  `Built companion skill library not found at ${srcLibrary}.
22331
23110
  If you are running from a source checkout, run: npm run build:skill:forgecad`
@@ -22353,7 +23132,7 @@ async function runSkillInstallCli(argv = []) {
22353
23132
  for (const target of options.targets) {
22354
23133
  const config = INSTALL_TARGETS[target];
22355
23134
  console.log(`Installing ForgeCAD skills for ${config.label} at ${config.root}`);
22356
- installCoreSkill(options.isDev, config.root);
23135
+ installCoreSkill(config.root);
22357
23136
  if (options.includeLibrary) installCompanionLibrary(config.root);
22358
23137
  }
22359
23138
  console.log(`Reload your agent (Claude Code, Codex, OpenCode, \u2026) to activate.`);
@@ -22365,7 +23144,7 @@ async function runSkillOneFileCli(argv = []) {
22365
23144
  Example: forgecad skill one-file ~/Desktop/forgecad-context.md`);
22366
23145
  }
22367
23146
  const src = resolvePackagePath(import.meta.url, "dist-skill", "CONTEXT.md");
22368
- if (!existsSync11(src)) {
23147
+ if (!existsSync10(src)) {
22369
23148
  throw new Error(
22370
23149
  `Built context file not found at ${src}.
22371
23150
  If you are running from a source checkout, run: npm run build:skill:forgecad`
@@ -22429,7 +23208,7 @@ function discoverFlattenedSkillSources() {
22429
23208
  const coreSkill = resolvePackagePath(import.meta.url, "dist-skill", "SKILL.md");
22430
23209
  const coreDocs = resolvePackagePath(import.meta.url, "dist-skill", "docs");
22431
23210
  const libraryRoot = resolvePackagePath(import.meta.url, "dist-skill", "library");
22432
- if (!existsSync11(coreSkill) || !existsSync11(coreDocs)) {
23211
+ if (!existsSync10(coreSkill) || !existsSync10(coreDocs)) {
22433
23212
  throw new Error(
22434
23213
  `Built skill artifacts not found in ${resolvePackagePath(import.meta.url, "dist-skill")}.
22435
23214
  If you are running from a source checkout, run: npm run build:skill:forgecad`
@@ -22444,12 +23223,12 @@ If you are running from a source checkout, run: npm run build:skill:forgecad`
22444
23223
  ]
22445
23224
  }
22446
23225
  ];
22447
- if (existsSync11(libraryRoot)) {
23226
+ if (existsSync10(libraryRoot)) {
22448
23227
  for (const entry of readdirSync6(libraryRoot, { withFileTypes: true })) {
22449
23228
  if (!entry.isDirectory()) continue;
22450
23229
  const root = join8(libraryRoot, entry.name);
22451
23230
  const skillFile = join8(root, "SKILL.md");
22452
- if (!existsSync11(skillFile)) continue;
23231
+ if (!existsSync10(skillFile)) continue;
22453
23232
  sources.push({ name: entry.name, files: findSkillFiles(root) });
22454
23233
  }
22455
23234
  }
@@ -22485,19 +23264,113 @@ import { existsSync as existsSync14 } from "fs";
22485
23264
  import { resolve as resolve30 } from "path";
22486
23265
 
22487
23266
  // cli/forge-studio-server.ts
22488
- import { fork as fork2 } from "child_process";
22489
23267
  import chokidar from "chokidar";
22490
23268
  import fs from "fs";
22491
23269
  import http from "http";
22492
23270
  import path2 from "path";
22493
23271
 
23272
+ // server/importAnalysis.ts
23273
+ var FORGE_IMPORT_RE2 = /\b(?:importMesh|importStep|importSvgSketch|Import\.dxfSketch)\s*\(\s*(?:"([^"]+)"|'([^']+)')/g;
23274
+ var REQUIRE_RE2 = /\brequire\s*\(\s*(?:"([^"]+)"|'([^']+)')/g;
23275
+ var ES_IMPORT_RE2 = /\bfrom\s+(?:"([^"]+)"|'([^']+)')/g;
23276
+ var VIRTUAL_MODULES2 = /* @__PURE__ */ new Set(["forgecad", "@forge/runtime", "@forgecad/runtime"]);
23277
+ function extractImports2(code) {
23278
+ const refs = [];
23279
+ const seen = /* @__PURE__ */ new Set();
23280
+ const add2 = (raw, kind) => {
23281
+ if (!seen.has(raw) && !VIRTUAL_MODULES2.has(raw)) {
23282
+ seen.add(raw);
23283
+ refs.push({ raw, kind });
23284
+ }
23285
+ };
23286
+ const kindMap = {
23287
+ importMesh: "forgeMesh",
23288
+ importStep: "forgeMesh",
23289
+ importSvgSketch: "forgeSvg",
23290
+ "Import.dxfSketch": "forgeDxf"
23291
+ };
23292
+ let m;
23293
+ const forgeRe = new RegExp(FORGE_IMPORT_RE2.source, FORGE_IMPORT_RE2.flags);
23294
+ while ((m = forgeRe.exec(code)) !== null) {
23295
+ const path3 = m[1] ?? m[2];
23296
+ const fn = m[0].match(/\b(importMesh|importStep|importSvgSketch|Import\.dxfSketch)/)?.[1];
23297
+ add2(path3, kindMap[fn] ?? "forgeMesh");
23298
+ }
23299
+ const reqRe = new RegExp(REQUIRE_RE2.source, REQUIRE_RE2.flags);
23300
+ while ((m = reqRe.exec(code)) !== null) {
23301
+ const path3 = m[1] ?? m[2];
23302
+ add2(path3, "require");
23303
+ }
23304
+ const esRe = new RegExp(ES_IMPORT_RE2.source, ES_IMPORT_RE2.flags);
23305
+ while ((m = esRe.exec(code)) !== null) {
23306
+ const path3 = m[1] ?? m[2];
23307
+ add2(path3, "esImport");
23308
+ }
23309
+ return refs;
23310
+ }
23311
+ function resolveRelative2(fromFile, importPath) {
23312
+ if (!importPath.startsWith("./") && !importPath.startsWith("../")) {
23313
+ return importPath;
23314
+ }
23315
+ const fromDir = fromFile.includes("/") ? fromFile.slice(0, fromFile.lastIndexOf("/")) : "";
23316
+ const combined = fromDir ? `${fromDir}/${importPath}` : importPath;
23317
+ const parts = combined.split("/");
23318
+ const resolved = [];
23319
+ for (const part of parts) {
23320
+ if (part === "." || part === "") continue;
23321
+ if (part === "..") {
23322
+ if (resolved.length > 0 && resolved[resolved.length - 1] !== "..") {
23323
+ resolved.pop();
23324
+ } else {
23325
+ resolved.push("..");
23326
+ }
23327
+ } else {
23328
+ resolved.push(part);
23329
+ }
23330
+ }
23331
+ return resolved.join("/");
23332
+ }
23333
+ function collectDependencyFiles(entryFile, allFiles) {
23334
+ const result = {};
23335
+ const visited = /* @__PURE__ */ new Set();
23336
+ function visit(fileName) {
23337
+ if (visited.has(fileName)) return;
23338
+ visited.add(fileName);
23339
+ const code = allFiles[fileName];
23340
+ if (code == null) return;
23341
+ result[fileName] = code;
23342
+ const imports = extractImports2(code);
23343
+ for (const imp of imports) {
23344
+ const resolved = resolveRelative2(fileName, imp.raw);
23345
+ if (imp.kind === "forgeMesh") continue;
23346
+ if (imp.kind === "forgeSvg") {
23347
+ if (allFiles[resolved] != null) {
23348
+ result[resolved] = allFiles[resolved];
23349
+ }
23350
+ continue;
23351
+ }
23352
+ if (imp.kind === "forgeDxf") {
23353
+ if (allFiles[resolved] != null) {
23354
+ result[resolved] = allFiles[resolved];
23355
+ }
23356
+ continue;
23357
+ }
23358
+ if (allFiles[resolved] != null) {
23359
+ visit(resolved);
23360
+ }
23361
+ }
23362
+ }
23363
+ visit(entryFile);
23364
+ return result;
23365
+ }
23366
+
22494
23367
  // cli/project.ts
22495
23368
  import { createHash as createHash2 } from "crypto";
22496
- import { existsSync as existsSync13, readFileSync as readFileSync17, readdirSync as readdirSync7, writeFileSync as writeFileSync12 } from "fs";
23369
+ import { existsSync as existsSync12, readFileSync as readFileSync17, readdirSync as readdirSync7, writeFileSync as writeFileSync12 } from "fs";
22497
23370
  import { join as join10 } from "path";
22498
23371
 
22499
23372
  // cli/auth.ts
22500
- import { chmodSync, existsSync as existsSync12, mkdirSync as mkdirSync5, readFileSync as readFileSync16, unlinkSync, writeFileSync as writeFileSync11 } from "fs";
23373
+ import { chmodSync, existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync16, unlinkSync, writeFileSync as writeFileSync11 } from "fs";
22501
23374
  import { homedir as homedir7 } from "os";
22502
23375
  import { join as join9 } from "path";
22503
23376
  import { createInterface } from "readline";
@@ -22584,7 +23457,7 @@ function forgecadDir() {
22584
23457
  }
22585
23458
  function ensureDir() {
22586
23459
  const dir = forgecadDir();
22587
- if (!existsSync12(dir)) mkdirSync5(dir, { recursive: true, mode: 448 });
23460
+ if (!existsSync11(dir)) mkdirSync5(dir, { recursive: true, mode: 448 });
22588
23461
  try {
22589
23462
  chmodSync(dir, 448);
22590
23463
  } catch {
@@ -22595,7 +23468,7 @@ function authFilePath() {
22595
23468
  }
22596
23469
  function readStoredAuth() {
22597
23470
  const p = authFilePath();
22598
- if (!existsSync12(p)) return null;
23471
+ if (!existsSync11(p)) return null;
22599
23472
  try {
22600
23473
  const auth = JSON.parse(readFileSync16(p, "utf-8"));
22601
23474
  return { ...auth, server: normalizeServerUrl(auth.server, "Stored ForgeCAD server URL") };
@@ -22614,7 +23487,7 @@ function writeStoredAuth(auth) {
22614
23487
  }
22615
23488
  function clearStoredAuth() {
22616
23489
  const p = authFilePath();
22617
- if (existsSync12(p)) unlinkSync(p);
23490
+ if (existsSync11(p)) unlinkSync(p);
22618
23491
  }
22619
23492
  function getServerUrl() {
22620
23493
  if (process.env.FORGECAD_TOKEN) {
@@ -22918,7 +23791,7 @@ function manifestPath(projectDir) {
22918
23791
  }
22919
23792
  function readManifest(projectDir) {
22920
23793
  const p = manifestPath(projectDir);
22921
- if (!existsSync13(p)) return null;
23794
+ if (!existsSync12(p)) return null;
22922
23795
  try {
22923
23796
  return JSON.parse(readFileSync17(p, "utf-8"));
22924
23797
  } catch {
@@ -23164,99 +24037,164 @@ async function deleteShare(shareId) {
23164
24037
  }
23165
24038
  }
23166
24039
 
23167
- // server/importAnalysis.ts
23168
- var FORGE_IMPORT_RE2 = /\b(?:importMesh|importStep|importSvgSketch|Import\.dxfSketch)\s*\(\s*(?:"([^"]+)"|'([^']+)')/g;
23169
- var REQUIRE_RE2 = /\brequire\s*\(\s*(?:"([^"]+)"|'([^']+)')/g;
23170
- var ES_IMPORT_RE2 = /\bfrom\s+(?:"([^"]+)"|'([^']+)')/g;
23171
- var VIRTUAL_MODULES2 = /* @__PURE__ */ new Set(["forgecad", "@forge/runtime", "@forgecad/runtime"]);
23172
- function extractImports2(code) {
23173
- const refs = [];
23174
- const seen = /* @__PURE__ */ new Set();
23175
- const add2 = (raw, kind) => {
23176
- if (!seen.has(raw) && !VIRTUAL_MODULES2.has(raw)) {
23177
- seen.add(raw);
23178
- refs.push({ raw, kind });
23179
- }
23180
- };
23181
- const kindMap = {
23182
- importMesh: "forgeMesh",
23183
- importStep: "forgeMesh",
23184
- importSvgSketch: "forgeSvg",
23185
- "Import.dxfSketch": "forgeDxf"
23186
- };
23187
- let m;
23188
- const forgeRe = new RegExp(FORGE_IMPORT_RE2.source, FORGE_IMPORT_RE2.flags);
23189
- while ((m = forgeRe.exec(code)) !== null) {
23190
- const path3 = m[1] ?? m[2];
23191
- const fn = m[0].match(/\b(importMesh|importStep|importSvgSketch|Import\.dxfSketch)/)?.[1];
23192
- add2(path3, kindMap[fn] ?? "forgeMesh");
23193
- }
23194
- const reqRe = new RegExp(REQUIRE_RE2.source, REQUIRE_RE2.flags);
23195
- while ((m = reqRe.exec(code)) !== null) {
23196
- const path3 = m[1] ?? m[2];
23197
- add2(path3, "require");
23198
- }
23199
- const esRe = new RegExp(ES_IMPORT_RE2.source, ES_IMPORT_RE2.flags);
23200
- while ((m = esRe.exec(code)) !== null) {
23201
- const path3 = m[1] ?? m[2];
23202
- add2(path3, "esImport");
24040
+ // cli/local-native-compute.ts
24041
+ import { spawn as spawn3, execFileSync as execFileSync2 } from "child_process";
24042
+ import { existsSync as existsSync13, readdirSync as readdirSync8, statSync as statSync8 } from "fs";
24043
+ import { createServer as createServer2 } from "net";
24044
+ import { join as join11 } from "path";
24045
+ var startedComputeServer = null;
24046
+ var REQUIRED_ENGINE_VERSION = "native-occt-node-api-0.2.0";
24047
+ var REQUIRED_PLAN_KINDS = [
24048
+ "box",
24049
+ "cylinder",
24050
+ "sphere",
24051
+ "torus",
24052
+ "extrude",
24053
+ "boolean",
24054
+ "transform",
24055
+ "queryOwner",
24056
+ "revolve",
24057
+ "loft",
24058
+ "sweep",
24059
+ "nurbsSurface",
24060
+ "filletEdges",
24061
+ "chamferEdges"
24062
+ ];
24063
+ function npmCommand() {
24064
+ return process.platform === "win32" ? "npm.cmd" : "npm";
24065
+ }
24066
+ function isLocalComputeUrl(url) {
24067
+ return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1";
24068
+ }
24069
+ async function isHealthy(computeUrl) {
24070
+ try {
24071
+ const response = await fetch(`${computeUrl}/health`, { signal: AbortSignal.timeout(1e3) });
24072
+ if (!response.ok) return false;
24073
+ const body = await response.json().catch(() => null);
24074
+ if (body?.status !== "ok" || body?.nativeOcctAvailable !== true || body?.engineVersion !== REQUIRED_ENGINE_VERSION) return false;
24075
+ const capabilitiesResponse = await fetch(`${computeUrl}/capabilities`, { signal: AbortSignal.timeout(1e3) });
24076
+ if (!capabilitiesResponse.ok) return false;
24077
+ const capabilities = await capabilitiesResponse.json().catch(() => null);
24078
+ const supportedKinds = capabilities?.nativeOcct?.supportedPlanKinds;
24079
+ return Array.isArray(supportedKinds) && REQUIRED_PLAN_KINDS.every((kind) => supportedKinds.includes(kind));
24080
+ } catch {
24081
+ return false;
23203
24082
  }
23204
- return refs;
23205
24083
  }
23206
- function resolveRelative2(fromFile, importPath) {
23207
- if (!importPath.startsWith("./") && !importPath.startsWith("../")) {
23208
- return importPath;
24084
+ async function waitForHealthy(computeUrl, child) {
24085
+ for (let attempt = 0; attempt < 120; attempt += 1) {
24086
+ if (await isHealthy(computeUrl)) return;
24087
+ if (child.exitCode != null) break;
24088
+ await new Promise((resolve40) => setTimeout(resolve40, 50));
23209
24089
  }
23210
- const fromDir = fromFile.includes("/") ? fromFile.slice(0, fromFile.lastIndexOf("/")) : "";
23211
- const combined = fromDir ? `${fromDir}/${importPath}` : importPath;
23212
- const parts = combined.split("/");
23213
- const resolved = [];
23214
- for (const part of parts) {
23215
- if (part === "." || part === "") continue;
23216
- if (part === "..") {
23217
- if (resolved.length > 0 && resolved[resolved.length - 1] !== "..") {
23218
- resolved.pop();
23219
- } else {
23220
- resolved.push("..");
23221
- }
23222
- } else {
23223
- resolved.push(part);
24090
+ throw new Error(`Native OCCT compute backend did not become healthy at ${computeUrl}.`);
24091
+ }
24092
+ function closeChild(child) {
24093
+ return new Promise((resolve40) => {
24094
+ if (child.exitCode != null || child.killed) {
24095
+ resolve40();
24096
+ return;
23224
24097
  }
23225
- }
23226
- return resolved.join("/");
24098
+ child.once("exit", () => resolve40());
24099
+ child.kill();
24100
+ });
23227
24101
  }
23228
- function collectDependencyFiles(entryFile, allFiles) {
23229
- const result = {};
23230
- const visited = /* @__PURE__ */ new Set();
23231
- function visit(fileName) {
23232
- if (visited.has(fileName)) return;
23233
- visited.add(fileName);
23234
- const code = allFiles[fileName];
23235
- if (code == null) return;
23236
- result[fileName] = code;
23237
- const imports = extractImports2(code);
23238
- for (const imp of imports) {
23239
- const resolved = resolveRelative2(fileName, imp.raw);
23240
- if (imp.kind === "forgeMesh") continue;
23241
- if (imp.kind === "forgeSvg") {
23242
- if (allFiles[resolved] != null) {
23243
- result[resolved] = allFiles[resolved];
23244
- }
23245
- continue;
23246
- }
23247
- if (imp.kind === "forgeDxf") {
23248
- if (allFiles[resolved] != null) {
23249
- result[resolved] = allFiles[resolved];
23250
- }
23251
- continue;
23252
- }
23253
- if (allFiles[resolved] != null) {
23254
- visit(resolved);
23255
- }
24102
+ function hasLocalNativeComputeSource(packageRoot) {
24103
+ return existsSync13(join11(packageRoot, "apps/backend/src/server.ts")) && existsSync13(join11(packageRoot, "scripts/build-native-occt.mjs"));
24104
+ }
24105
+ function newestMtimeMs(path3) {
24106
+ if (!existsSync13(path3)) return 0;
24107
+ const stat = statSync8(path3);
24108
+ if (!stat.isDirectory()) return stat.mtimeMs;
24109
+ let newest = stat.mtimeMs;
24110
+ for (const entry of readdirSync8(path3, { withFileTypes: true })) {
24111
+ newest = Math.max(newest, newestMtimeMs(join11(path3, entry.name)));
24112
+ }
24113
+ return newest;
24114
+ }
24115
+ function isNativeAddonStale(packageRoot, addonPath) {
24116
+ if (!existsSync13(addonPath)) return true;
24117
+ const addonMtime = statSync8(addonPath).mtimeMs;
24118
+ const sourceMtime = Math.max(
24119
+ newestMtimeMs(join11(packageRoot, "native/occt-ir/src")),
24120
+ newestMtimeMs(join11(packageRoot, "native/occt-ir/CMakeLists.txt")),
24121
+ newestMtimeMs(join11(packageRoot, "scripts/build-native-occt.mjs"))
24122
+ );
24123
+ return sourceMtime > addonMtime;
24124
+ }
24125
+ function computeUrlWithPort(url, port) {
24126
+ const next = new URL(url.toString());
24127
+ next.port = String(port);
24128
+ return next.toString().replace(/\/$/, "");
24129
+ }
24130
+ async function pickComputeUrl(preferred) {
24131
+ const preferredPort = Number.parseInt(preferred.port || (preferred.protocol === "https:" ? "443" : "80"), 10);
24132
+ for (let port = preferredPort; port < preferredPort + 10; port += 1) {
24133
+ const candidate = computeUrlWithPort(preferred, port);
24134
+ if (await isHealthy(candidate)) return candidate;
24135
+ try {
24136
+ const server = await new Promise((resolve40, reject) => {
24137
+ const probe = createServer2();
24138
+ probe.once("error", reject);
24139
+ probe.listen(port, preferred.hostname, () => resolve40(probe));
24140
+ });
24141
+ await new Promise((resolve40) => server.close(() => resolve40()));
24142
+ return candidate;
24143
+ } catch {
23256
24144
  }
23257
24145
  }
23258
- visit(entryFile);
23259
- return result;
24146
+ throw new Error(`No local port available for native OCCT compute near ${preferred.href}.`);
24147
+ }
24148
+ function startLocalNativeComputeServer(options) {
24149
+ if (startedComputeServer) return startedComputeServer;
24150
+ const startPromise = (async () => {
24151
+ const requestedComputeUrl = options.computeUrl ?? "http://127.0.0.1:4510";
24152
+ if (options.enabled === false) {
24153
+ return { url: requestedComputeUrl, spawned: false, close: async () => {
24154
+ } };
24155
+ }
24156
+ const parsed = new URL(requestedComputeUrl);
24157
+ if (!isLocalComputeUrl(parsed)) {
24158
+ return { url: requestedComputeUrl, spawned: false, close: async () => {
24159
+ } };
24160
+ }
24161
+ if (!hasLocalNativeComputeSource(options.packageRoot)) {
24162
+ return { url: requestedComputeUrl, spawned: false, close: async () => {
24163
+ } };
24164
+ }
24165
+ if (await isHealthy(requestedComputeUrl)) {
24166
+ return { url: requestedComputeUrl, spawned: false, close: async () => {
24167
+ } };
24168
+ }
24169
+ const addonPath = join11(options.packageRoot, "native/occt-ir/build/forgecad_occt.node");
24170
+ if (isNativeAddonStale(options.packageRoot, addonPath)) {
24171
+ console.log("[compute] Native OCCT addon missing or stale; building...");
24172
+ execFileSync2(npmCommand(), ["run", "build:native-occt"], {
24173
+ cwd: options.packageRoot,
24174
+ stdio: "inherit"
24175
+ });
24176
+ }
24177
+ const computeUrl = await pickComputeUrl(parsed);
24178
+ const selected = new URL(computeUrl);
24179
+ const port = selected.port || (selected.protocol === "https:" ? "443" : "80");
24180
+ console.log(`[compute] Starting native OCCT backend at ${computeUrl}`);
24181
+ const child = spawn3("npx", ["tsx", "apps/backend/src/server.ts"], {
24182
+ cwd: options.packageRoot,
24183
+ env: { ...process.env, FORGE_BACKEND_PORT: port },
24184
+ stdio: ["ignore", "inherit", "inherit"]
24185
+ });
24186
+ await waitForHealthy(computeUrl, child);
24187
+ return {
24188
+ url: computeUrl,
24189
+ spawned: true,
24190
+ close: () => closeChild(child)
24191
+ };
24192
+ })();
24193
+ startedComputeServer = startPromise.catch((error) => {
24194
+ startedComputeServer = null;
24195
+ throw error;
24196
+ });
24197
+ return startedComputeServer;
23260
24198
  }
23261
24199
 
23262
24200
  // cli/forge-studio-server.ts
@@ -23396,6 +24334,28 @@ function resolveProjectFile(projectDir, filename, opts = {}) {
23396
24334
  }
23397
24335
  return { filePath, filename: rel.replace(/\\/g, "/") };
23398
24336
  }
24337
+ function resolveProjectDirectory(projectDir, dirPath) {
24338
+ if (!projectDir) throw new Error("No project directory");
24339
+ if (!dirPath) throw new Error("Invalid directory path");
24340
+ const abs = path2.resolve(projectDir);
24341
+ const resolved = path2.resolve(abs, dirPath);
24342
+ const rel = path2.relative(abs, resolved);
24343
+ if (rel.startsWith("..") || path2.isAbsolute(rel)) {
24344
+ throw new Error(`Path "${dirPath}" is outside the project root`);
24345
+ }
24346
+ return { dirPath: resolved, dirname: rel.replace(/\\/g, "/") };
24347
+ }
24348
+ function directoryTreeContainsFile(dirPath) {
24349
+ for (const item of fs.readdirSync(dirPath, { withFileTypes: true })) {
24350
+ const childPath2 = path2.join(dirPath, item.name);
24351
+ if (item.isDirectory()) {
24352
+ if (directoryTreeContainsFile(childPath2)) return true;
24353
+ } else {
24354
+ return true;
24355
+ }
24356
+ }
24357
+ return false;
24358
+ }
23399
24359
  function serveStatic(distDir, req, res) {
23400
24360
  const absDistDir = path2.resolve(distDir);
23401
24361
  const urlPath = decodeURIComponent((req.url ?? "/").split("?")[0]);
@@ -23419,67 +24379,6 @@ function serveStatic(distDir, req, res) {
23419
24379
  res.end(content);
23420
24380
  return true;
23421
24381
  }
23422
- var BACKEND_PORT = 4510;
23423
- var BACKEND_URL = `http://127.0.0.1:${BACKEND_PORT}`;
23424
- function spawnBackendServer2(packageRoot) {
23425
- const backendEntry = path2.join(packageRoot, "dist-backend", "server.js");
23426
- if (!fs.existsSync(backendEntry)) {
23427
- console.log(" Backend compute server not found (dist-backend/server.js) \u2014 server rendering disabled");
23428
- return null;
23429
- }
23430
- const child = fork2(backendEntry, {
23431
- stdio: ["ignore", "pipe", "pipe", "ipc"],
23432
- env: { ...process.env, FORGE_BACKEND_PORT: String(BACKEND_PORT) }
23433
- });
23434
- child.stdout?.on("data", (data) => {
23435
- for (const line of data.toString().trim().split("\n")) {
23436
- console.log(` [backend] ${line}`);
23437
- }
23438
- });
23439
- child.stderr?.on("data", (data) => {
23440
- for (const line of data.toString().trim().split("\n")) {
23441
- console.error(` [backend] ${line}`);
23442
- }
23443
- });
23444
- child.on("exit", (code) => {
23445
- if (code !== null && code !== 0) {
23446
- console.error(` [backend] Compute server exited with code ${code}`);
23447
- }
23448
- });
23449
- return child;
23450
- }
23451
- async function proxyToBackend(req, res, backendPath, method) {
23452
- const abortController = new AbortController();
23453
- res.on("close", () => abortController.abort());
23454
- try {
23455
- const body = method === "POST" ? await readRawBody(req) : void 0;
23456
- const backendRes = await fetch(`${BACKEND_URL}${backendPath}`, {
23457
- method,
23458
- headers: body ? { "Content-Type": "application/json" } : void 0,
23459
- body,
23460
- signal: abortController.signal
23461
- });
23462
- if (abortController.signal.aborted) return false;
23463
- res.writeHead(backendRes.status, {
23464
- "Content-Type": backendRes.headers.get("Content-Type") ?? "application/octet-stream"
23465
- });
23466
- const buffer = Buffer.from(await backendRes.arrayBuffer());
23467
- res.end(buffer);
23468
- return true;
23469
- } catch {
23470
- if (abortController.signal.aborted) return false;
23471
- sendJson(res, 503, { status: "unavailable" });
23472
- return false;
23473
- }
23474
- }
23475
- function readRawBody(req) {
23476
- return new Promise((resolve40, reject) => {
23477
- const chunks = [];
23478
- req.on("data", (chunk) => chunks.push(chunk));
23479
- req.on("end", () => resolve40(Buffer.concat(chunks).toString("utf-8")));
23480
- req.on("error", reject);
23481
- });
23482
- }
23483
24382
  function isPortAvailable(port, host) {
23484
24383
  return new Promise((resolve40) => {
23485
24384
  const s = http.createServer();
@@ -23528,9 +24427,19 @@ function matchSSERoute(url, suffix) {
23528
24427
  }
23529
24428
  async function startStudioServer(options) {
23530
24429
  const { projectDirs, distDir, packageRoot, open, strictPort } = options;
23531
- const backendProcess = spawnBackendServer2(packageRoot);
23532
24430
  const host = options.host || "127.0.0.1";
23533
24431
  const port = await pickPort(options.port || 5173, host, strictPort);
24432
+ const computeUrl = process.env.FORGE_COMPUTE_URL ?? "http://127.0.0.1:4510";
24433
+ const computeServer = await startLocalNativeComputeServer({
24434
+ packageRoot,
24435
+ computeUrl,
24436
+ enabled: process.env.FORGE_AUTO_START_COMPUTE !== "false"
24437
+ }).catch((error) => {
24438
+ const message = error instanceof Error ? error.message : String(error);
24439
+ console.warn(`[compute] Native OCCT backend unavailable: ${message}`);
24440
+ return null;
24441
+ });
24442
+ const activeComputeUrl = computeServer?.url ?? computeUrl;
23534
24443
  const projects = buildProjectRegistry(projectDirs);
23535
24444
  const projectById = new Map(projects.map((p) => [p.id, p]));
23536
24445
  function lookupProject(projectId) {
@@ -23553,6 +24462,18 @@ data: ${JSON.stringify(data)}
23553
24462
  }
23554
24463
  }
23555
24464
  const watchers = [];
24465
+ const snapshotTimers = /* @__PURE__ */ new Map();
24466
+ function broadcastProjectSnapshot(project) {
24467
+ const existing = snapshotTimers.get(project.id);
24468
+ if (existing) clearTimeout(existing);
24469
+ snapshotTimers.set(
24470
+ project.id,
24471
+ setTimeout(() => {
24472
+ snapshotTimers.delete(project.id);
24473
+ broadcastToProject(project.id, "snapshot", {});
24474
+ }, 50)
24475
+ );
24476
+ }
23556
24477
  for (const proj of projects) {
23557
24478
  const abs = path2.resolve(proj.dir);
23558
24479
  const watcher = chokidar.watch(abs, { ignoreInitial: true, ignored: /(^|[/\\])\../ });
@@ -23581,7 +24502,10 @@ data: ${JSON.stringify(data)}
23581
24502
  watcher.on("unlink", (f2) => {
23582
24503
  if (!isProjectFile(f2) && !isBinaryProjectFile(f2)) return;
23583
24504
  broadcastToProject(proj.id, "delete", { filename: path2.relative(abs, f2).replace(/\\/g, "/") });
24505
+ broadcastProjectSnapshot(proj);
23584
24506
  });
24507
+ watcher.on("addDir", () => broadcastProjectSnapshot(proj));
24508
+ watcher.on("unlinkDir", () => broadcastProjectSnapshot(proj));
23585
24509
  watchers.push(watcher);
23586
24510
  }
23587
24511
  const server = http.createServer(async (req, res) => {
@@ -23591,6 +24515,79 @@ data: ${JSON.stringify(data)}
23591
24515
  sendJson(res, 200, { status: "ok", mode: "local", uptime: process.uptime() });
23592
24516
  return;
23593
24517
  }
24518
+ if (method === "GET" && url2 === "/api/compute/health") {
24519
+ try {
24520
+ const response = await fetch(`${activeComputeUrl}/health`, { signal: AbortSignal.timeout(2e3) });
24521
+ if (!response.ok) {
24522
+ sendJson(res, 200, { status: "unavailable" });
24523
+ return;
24524
+ }
24525
+ const body = await response.json();
24526
+ sendJson(res, 200, { status: body?.status === "ok" ? "ok" : "unavailable" });
24527
+ } catch {
24528
+ sendJson(res, 200, { status: "unavailable" });
24529
+ }
24530
+ return;
24531
+ }
24532
+ if (method === "GET" && url2 === "/api/compute/capabilities") {
24533
+ try {
24534
+ const response = await fetch(`${activeComputeUrl}/capabilities`, { signal: AbortSignal.timeout(5e3) });
24535
+ const body = await response.json().catch(() => ({ error: `Compute server returned ${response.status}` }));
24536
+ sendJson(res, response.status, body);
24537
+ } catch {
24538
+ sendJson(res, 502, { error: "Backend compute server unreachable" });
24539
+ }
24540
+ return;
24541
+ }
24542
+ if (method === "GET" && url2.startsWith("/api/compute/jobs")) {
24543
+ try {
24544
+ const response = await fetch(`${activeComputeUrl}${url2.replace("/api", "")}`, { signal: AbortSignal.timeout(5e3) });
24545
+ const body = await response.json().catch(() => ({ error: `Compute server returned ${response.status}` }));
24546
+ sendJson(res, response.status, body);
24547
+ } catch {
24548
+ sendJson(res, 502, { error: "Backend compute server unreachable" });
24549
+ }
24550
+ return;
24551
+ }
24552
+ if (method === "POST" && /^\/api\/compute\/jobs\/[^/]+\/cancel$/.test(url2)) {
24553
+ try {
24554
+ const response = await fetch(`${activeComputeUrl}${url2.replace("/api", "")}`, {
24555
+ method: "POST",
24556
+ signal: AbortSignal.timeout(5e3)
24557
+ });
24558
+ const body = await response.json().catch(() => ({ error: `Compute server returned ${response.status}` }));
24559
+ sendJson(res, response.status, body);
24560
+ } catch {
24561
+ sendJson(res, 502, { error: "Backend compute server unreachable" });
24562
+ }
24563
+ return;
24564
+ }
24565
+ if (method === "POST" && url2 === "/api/compute/ir") {
24566
+ const abortController = new AbortController();
24567
+ res.on("close", () => abortController.abort());
24568
+ try {
24569
+ const body = await readJsonBody(req);
24570
+ const response = await fetch(`${activeComputeUrl}/compute/ir`, {
24571
+ method: "POST",
24572
+ headers: { "Content-Type": "application/json" },
24573
+ body: JSON.stringify(body),
24574
+ signal: abortController.signal
24575
+ });
24576
+ if (abortController.signal.aborted) return;
24577
+ if (!response.ok) {
24578
+ const errBody = await response.json().catch(() => ({ error: `Compute server returned ${response.status}` }));
24579
+ sendJson(res, response.status, errBody);
24580
+ return;
24581
+ }
24582
+ const buffer = Buffer.from(await response.arrayBuffer());
24583
+ res.writeHead(200, { "Content-Type": "application/octet-stream", "Content-Length": buffer.byteLength });
24584
+ res.end(buffer);
24585
+ } catch {
24586
+ if (abortController.signal.aborted) return;
24587
+ sendJson(res, 502, { error: "Backend compute server unreachable" });
24588
+ }
24589
+ return;
24590
+ }
23594
24591
  if (method === "GET" && url2.split("?")[0] === "/api/projects") {
23595
24592
  sendJson(res, 200, {
23596
24593
  projects: projects.map((p) => ({
@@ -23823,6 +24820,49 @@ data: ${JSON.stringify(data)}
23823
24820
  return;
23824
24821
  }
23825
24822
  }
24823
+ {
24824
+ const m = matchProjectRoute(url2, method, "POST", "/rmdir");
24825
+ if (m) {
24826
+ const proj = lookupProject(m.projectId);
24827
+ if (!proj) {
24828
+ sendJson(res, 404, { error: "Project not found" });
24829
+ return;
24830
+ }
24831
+ readJsonBody(req).then((body) => {
24832
+ const { dirPath } = body;
24833
+ if (!dirPath || typeof dirPath !== "string") {
24834
+ sendJson(res, 400, { error: "Invalid request" });
24835
+ return;
24836
+ }
24837
+ let resolved;
24838
+ try {
24839
+ resolved = resolveProjectDirectory(proj.dir, dirPath);
24840
+ } catch (e) {
24841
+ sendJson(res, 400, { error: e.message });
24842
+ return;
24843
+ }
24844
+ if (!resolved.dirname) {
24845
+ sendJson(res, 400, { error: "Cannot delete project root" });
24846
+ return;
24847
+ }
24848
+ if (!fs.existsSync(resolved.dirPath)) {
24849
+ sendJson(res, 200, { success: true });
24850
+ return;
24851
+ }
24852
+ if (!fs.statSync(resolved.dirPath).isDirectory()) {
24853
+ sendJson(res, 400, { error: "Path is not a directory" });
24854
+ return;
24855
+ }
24856
+ if (directoryTreeContainsFile(resolved.dirPath)) {
24857
+ sendJson(res, 409, { error: "Directory is not empty" });
24858
+ return;
24859
+ }
24860
+ fs.rmSync(resolved.dirPath, { recursive: true, force: true });
24861
+ sendJson(res, 200, { success: true });
24862
+ }).catch((e) => sendJson(res, 500, { error: e.message }));
24863
+ return;
24864
+ }
24865
+ }
23826
24866
  {
23827
24867
  const m = matchProjectRoute(url2, method, "POST", "/move");
23828
24868
  if (m) {
@@ -23975,14 +25015,6 @@ data: ${JSON.stringify(data)}
23975
25015
  }
23976
25016
  return;
23977
25017
  }
23978
- if (method === "GET" && url2 === "/api/compute/health") {
23979
- await proxyToBackend(req, res, "/health", "GET");
23980
- return;
23981
- }
23982
- if (method === "POST" && url2 === "/api/compute") {
23983
- await proxyToBackend(req, res, "/compute", "POST");
23984
- return;
23985
- }
23986
25018
  if (!serveStatic(distDir, req, res)) {
23987
25019
  res.statusCode = 404;
23988
25020
  res.end("Not found");
@@ -23992,27 +25024,29 @@ data: ${JSON.stringify(data)}
23992
25024
  const displayHost = host === "0.0.0.0" ? "localhost" : host;
23993
25025
  const url = `http://${displayHost}:${port}`;
23994
25026
  if (open) openBrowser(url);
23995
- const close4 = () => new Promise((resolve40) => {
23996
- for (const w of watchers) w.close();
23997
- if (backendProcess && !backendProcess.killed) {
23998
- backendProcess.kill();
23999
- }
24000
- for (const clients of sseClients.values()) {
24001
- for (const c of clients) {
24002
- try {
24003
- c.end();
24004
- } catch {
25027
+ const close4 = async () => {
25028
+ await new Promise((resolve40) => {
25029
+ for (const timer of snapshotTimers.values()) clearTimeout(timer);
25030
+ snapshotTimers.clear();
25031
+ for (const w of watchers) w.close();
25032
+ for (const clients of sseClients.values()) {
25033
+ for (const c of clients) {
25034
+ try {
25035
+ c.end();
25036
+ } catch {
25037
+ }
24005
25038
  }
24006
25039
  }
24007
- }
24008
- sseClients.clear();
24009
- server.close(() => resolve40());
24010
- });
25040
+ sseClients.clear();
25041
+ server.close(() => resolve40());
25042
+ });
25043
+ await computeServer?.close();
25044
+ };
24011
25045
  return { url, close: close4 };
24012
25046
  }
24013
25047
 
24014
25048
  // cli/forge-studio.ts
24015
- function usage19() {
25049
+ function usage20() {
24016
25050
  console.error(`ForgeCAD Studio
24017
25051
 
24018
25052
  Usage:
@@ -24042,7 +25076,7 @@ function parseStudioArgs(argv) {
24042
25076
  if (argv.length === 0) {
24043
25077
  throw new Error("Missing project path. Use `forgecad studio <project-path> [project-path ...]`.");
24044
25078
  }
24045
- if (argv.includes("-h") || argv.includes("--help")) usage19();
25079
+ if (argv.includes("-h") || argv.includes("--help")) usage20();
24046
25080
  const options = { open: false, strictPort: false, projectPaths: [] };
24047
25081
  for (let i = 0; i < argv.length; i += 1) {
24048
25082
  const arg = argv[i];
@@ -24117,7 +25151,7 @@ async function runStudioCli(argv = process.argv.slice(2)) {
24117
25151
  // cli/forge-svg.ts
24118
25152
  import { readFile as readFile5, writeFile as writeFile8 } from "fs/promises";
24119
25153
  import { basename as basename13, resolve as resolve31 } from "path";
24120
- function usage20() {
25154
+ function usage21() {
24121
25155
  console.error("Usage: forgecad export svg <script.forge.js> [output.svg]");
24122
25156
  process.exit(1);
24123
25157
  }
@@ -24126,7 +25160,7 @@ function defaultSvgOutput(scriptPath) {
24126
25160
  }
24127
25161
  async function runSvgCli(argv = process.argv.slice(2)) {
24128
25162
  const scriptPath = argv[0];
24129
- if (!scriptPath) usage20();
25163
+ if (!scriptPath) usage21();
24130
25164
  const outputPath = argv[1] || defaultSvgOutput(scriptPath);
24131
25165
  if (resolve31(outputPath) === resolve31(scriptPath)) {
24132
25166
  console.error(`ERROR: output path would overwrite the input script. Specify an explicit output path.`);
@@ -24194,7 +25228,7 @@ function mmToM2(valueMm) {
24194
25228
  function degToRad2(valueDeg) {
24195
25229
  return valueDeg * Math.PI / 180;
24196
25230
  }
24197
- function formatNumber3(value, digits = 6) {
25231
+ function formatNumber4(value, digits = 6) {
24198
25232
  if (!Number.isFinite(value)) return "0";
24199
25233
  const normalized = Math.abs(value) < 1e-12 ? 0 : value;
24200
25234
  return normalized.toFixed(digits).replace(/\.?0+$/, "");
@@ -24317,7 +25351,7 @@ function transformToOrigin(transform) {
24317
25351
  yaw = Math.atan2(r10, r00);
24318
25352
  }
24319
25353
  const x = mmToM2(m[12]), y = mmToM2(m[13]), z = mmToM2(m[14]);
24320
- return `xyz="${formatNumber3(x)} ${formatNumber3(y)} ${formatNumber3(z)}" rpy="${formatNumber3(roll)} ${formatNumber3(pitch)} ${formatNumber3(yaw)}"`;
25354
+ return `xyz="${formatNumber4(x)} ${formatNumber4(y)} ${formatNumber4(z)}" rpy="${formatNumber4(roll)} ${formatNumber4(pitch)} ${formatNumber4(yaw)}"`;
24321
25355
  }
24322
25356
  function sRgbFloat2(hex) {
24323
25357
  if (!hex || !/^#([0-9a-f]{6})$/i.test(hex)) return null;
@@ -24378,7 +25412,7 @@ function urdfXml(spec, modelName, linkNameMap, jointNameMap, geometries, _linkWo
24378
25412
  const color = sRgbFloat2(geometry.shapes[0]?.colorHex);
24379
25413
  const materialXml = color ? `
24380
25414
  <material name="${escapeXml2(urdfLinkName)}_material">
24381
- <color rgba="${formatNumber3(color[0], 3)} ${formatNumber3(color[1], 3)} ${formatNumber3(color[2], 3)} 1"/>
25415
+ <color rgba="${formatNumber4(color[0], 3)} ${formatNumber4(color[1], 3)} ${formatNumber4(color[2], 3)} 1"/>
24382
25416
  </material>` : "";
24383
25417
  let collisionXml = "";
24384
25418
  if (collisionMode === "visual") {
@@ -24407,17 +25441,17 @@ function urdfXml(spec, modelName, linkNameMap, jointNameMap, geometries, _linkWo
24407
25441
  const bCz = mmToM2((geometry.bboxMin[2] + geometry.bboxMax[2]) * 0.5);
24408
25442
  collisionXml = `
24409
25443
  <collision>
24410
- <origin xyz="${formatNumber3(bCx)} ${formatNumber3(bCy)} ${formatNumber3(bCz)}" rpy="0 0 0"/>
25444
+ <origin xyz="${formatNumber4(bCx)} ${formatNumber4(bCy)} ${formatNumber4(bCz)}" rpy="0 0 0"/>
24411
25445
  <geometry>
24412
- <box size="${formatNumber3(bDx)} ${formatNumber3(bDy)} ${formatNumber3(bDz)}"/>
25446
+ <box size="${formatNumber4(bDx)} ${formatNumber4(bDy)} ${formatNumber4(bDz)}"/>
24413
25447
  </geometry>
24414
25448
  </collision>`;
24415
25449
  }
24416
25450
  return ` <link name="${escapeXml2(urdfLinkName)}">
24417
25451
  <inertial>
24418
- <origin xyz="${formatNumber3(comX)} ${formatNumber3(comY)} ${formatNumber3(comZ)}" rpy="0 0 0"/>
24419
- <mass value="${formatNumber3(massKg, 6)}"/>
24420
- <inertia ixx="${formatNumber3(ixx, 8)}" ixy="${formatNumber3(ixy, 8)}" ixz="${formatNumber3(ixz, 8)}" iyy="${formatNumber3(iyy, 8)}" iyz="${formatNumber3(iyz, 8)}" izz="${formatNumber3(izz, 8)}"/>
25452
+ <origin xyz="${formatNumber4(comX)} ${formatNumber4(comY)} ${formatNumber4(comZ)}" rpy="0 0 0"/>
25453
+ <mass value="${formatNumber4(massKg, 6)}"/>
25454
+ <inertia ixx="${formatNumber4(ixx, 8)}" ixy="${formatNumber4(ixy, 8)}" ixz="${formatNumber4(ixz, 8)}" iyy="${formatNumber4(iyy, 8)}" iyz="${formatNumber4(iyz, 8)}" izz="${formatNumber4(izz, 8)}"/>
24421
25455
  </inertial>
24422
25456
  <visual>
24423
25457
  <geometry>
@@ -24434,7 +25468,7 @@ function urdfXml(spec, modelName, linkNameMap, jointNameMap, geometries, _linkWo
24434
25468
  const jType = urdfJointHasContinuous(joint2) ? "continuous" : urdfJointType(joint2.type);
24435
25469
  const originAttr = transformToOrigin(joint2.frame);
24436
25470
  const axisXml = joint2.type !== "fixed" ? `
24437
- <axis xyz="${joint2.axis.map((v) => formatNumber3(v)).join(" ")}"/>` : "";
25471
+ <axis xyz="${joint2.axis.map((v) => formatNumber4(v)).join(" ")}"/>` : "";
24438
25472
  let limitXml = "";
24439
25473
  if (joint2.type !== "fixed" && jType !== "continuous") {
24440
25474
  const lower2 = joint2.min !== void 0 ? joint2.type === "revolute" ? degToRad2(joint2.min) : mmToM2(joint2.min) : void 0;
@@ -24443,20 +25477,20 @@ function urdfXml(spec, modelName, linkNameMap, jointNameMap, geometries, _linkWo
24443
25477
  const velocity = sourceOverrides?.velocity ?? joint2.velocity;
24444
25478
  const vel = velocity !== void 0 ? joint2.type === "revolute" ? degToRad2(velocity) : mmToM2(velocity) : 10;
24445
25479
  limitXml = `
24446
- <limit${lower2 !== void 0 ? ` lower="${formatNumber3(lower2)}"` : ""}${upper !== void 0 ? ` upper="${formatNumber3(upper)}"` : ""} effort="${formatNumber3(effort)}" velocity="${formatNumber3(vel)}"/>`;
25480
+ <limit${lower2 !== void 0 ? ` lower="${formatNumber4(lower2)}"` : ""}${upper !== void 0 ? ` upper="${formatNumber4(upper)}"` : ""} effort="${formatNumber4(effort)}" velocity="${formatNumber4(vel)}"/>`;
24447
25481
  } else if (jType === "continuous") {
24448
25482
  const effort = sourceOverrides?.effort ?? joint2.effort ?? 100;
24449
25483
  const velocity = sourceOverrides?.velocity ?? joint2.velocity;
24450
25484
  const vel = velocity !== void 0 ? degToRad2(velocity) : 10;
24451
25485
  limitXml = `
24452
- <limit effort="${formatNumber3(effort)}" velocity="${formatNumber3(vel)}"/>`;
25486
+ <limit effort="${formatNumber4(effort)}" velocity="${formatNumber4(vel)}"/>`;
24453
25487
  }
24454
25488
  let dynamicsXml = "";
24455
25489
  const damping = sourceOverrides?.damping ?? joint2.damping;
24456
25490
  const friction = sourceOverrides?.friction ?? joint2.friction;
24457
25491
  if (damping !== void 0 || friction !== void 0) {
24458
25492
  dynamicsXml = `
24459
- <dynamics${damping !== void 0 ? ` damping="${formatNumber3(damping)}"` : ""}${friction !== void 0 ? ` friction="${formatNumber3(friction)}"` : ""}/>`;
25493
+ <dynamics${damping !== void 0 ? ` damping="${formatNumber4(damping)}"` : ""}${friction !== void 0 ? ` friction="${formatNumber4(friction)}"` : ""}/>`;
24460
25494
  }
24461
25495
  let mimicXml = "";
24462
25496
  const coupling = couplingByJoint.get(joint2.name);
@@ -24464,7 +25498,7 @@ function urdfXml(spec, modelName, linkNameMap, jointNameMap, geometries, _linkWo
24464
25498
  const primary = coupling.terms.reduce((a, b) => Math.abs(a.ratio) >= Math.abs(b.ratio) ? a : b);
24465
25499
  const leaderUrdfName = jointNameMap.get(`${primary.joint}_joint`);
24466
25500
  mimicXml = `
24467
- <mimic joint="${escapeXml2(leaderUrdfName)}" multiplier="${formatNumber3(primary.ratio)}" offset="${formatNumber3(coupling.offset)}"/>`;
25501
+ <mimic joint="${escapeXml2(leaderUrdfName)}" multiplier="${formatNumber4(primary.ratio)}" offset="${formatNumber4(coupling.offset)}"/>`;
24468
25502
  if (coupling.terms.length > 1) {
24469
25503
  warnings.push(
24470
25504
  `Joint "${joint2.name}" coupling has ${coupling.terms.length} terms but URDF mimic only supports 1. Using primary term (ratio=${primary.ratio} from "${primary.joint}").`
@@ -25017,12 +26051,12 @@ async function recordCliCommandEvent(input) {
25017
26051
  // cli/forge-project.ts
25018
26052
  import { existsSync as existsSync18, mkdirSync as mkdirSync8, readFileSync as readFileSync20, writeFileSync as writeFileSync15 } from "fs";
25019
26053
  import { createInterface as createInterface2 } from "readline";
25020
- import { basename as basename14, join as join12, resolve as resolve34 } from "path";
26054
+ import { basename as basename14, join as join13, resolve as resolve34 } from "path";
25021
26055
 
25022
26056
  // cli/license.ts
25023
26057
  import { existsSync as existsSync17, mkdirSync as mkdirSync7, readFileSync as readFileSync19, unlinkSync as unlinkSync2, writeFileSync as writeFileSync14 } from "fs";
25024
26058
  import { homedir as homedir8 } from "os";
25025
- import { join as join11 } from "path";
26059
+ import { join as join12 } from "path";
25026
26060
  var VALID_TIERS = /* @__PURE__ */ new Set(["free", "pro", "team"]);
25027
26061
  var PRO_COMMANDS = /* @__PURE__ */ new Set([
25028
26062
  // Advanced rendering
@@ -25063,14 +26097,14 @@ function tierSatisfies(userTier, required) {
25063
26097
  return userTier === "pro" || userTier === "team";
25064
26098
  }
25065
26099
  function licenseDir() {
25066
- return join11(homedir8(), ".forgecad");
26100
+ return join12(homedir8(), ".forgecad");
25067
26101
  }
25068
26102
  function ensureLicenseDir() {
25069
26103
  const dir = licenseDir();
25070
26104
  if (!existsSync17(dir)) mkdirSync7(dir, { recursive: true });
25071
26105
  }
25072
26106
  function licenseFilePath() {
25073
- return join11(licenseDir(), "license.json");
26107
+ return join12(licenseDir(), "license.json");
25074
26108
  }
25075
26109
  function readStoredLicense() {
25076
26110
  const path3 = licenseFilePath();
@@ -25430,8 +26464,8 @@ async function runProjectCloneCli(args) {
25430
26464
  const remoteFiles = await fetchRemoteFiles(project.id);
25431
26465
  for (const file of remoteFiles) {
25432
26466
  if (!file.content) continue;
25433
- const filePath = join12(targetDir, file.path);
25434
- mkdirSync8(join12(filePath, ".."), { recursive: true });
26467
+ const filePath = join13(targetDir, file.path);
26468
+ mkdirSync8(join13(filePath, ".."), { recursive: true });
25435
26469
  writeFileSync15(filePath, file.content, "utf-8");
25436
26470
  console.log(` ${file.path}`);
25437
26471
  }
@@ -25499,8 +26533,8 @@ async function runProjectPullCli(args) {
25499
26533
  for (const diff of incoming) {
25500
26534
  const remote = remoteFiles.find((f2) => f2.path === diff.path);
25501
26535
  if (remote?.content) {
25502
- const filePath = join12(cwd, remote.path);
25503
- mkdirSync8(join12(filePath, ".."), { recursive: true });
26536
+ const filePath = join13(cwd, remote.path);
26537
+ mkdirSync8(join13(filePath, ".."), { recursive: true });
25504
26538
  writeFileSync15(filePath, remote.content, "utf-8");
25505
26539
  written++;
25506
26540
  }
@@ -25975,7 +27009,7 @@ async function runFileSaveCli(args) {
25975
27009
  }
25976
27010
  content = Buffer.concat(chunks).toString("utf-8");
25977
27011
  } else if (!content) {
25978
- const localPath = join12(cwd, filePath);
27012
+ const localPath = join13(cwd, filePath);
25979
27013
  if (!existsSync18(localPath)) {
25980
27014
  console.error(`Local file not found: ${filePath}`);
25981
27015
  console.error("Use --content or --stdin to provide content directly.");
@@ -26399,13 +27433,13 @@ function findCollisions(entries) {
26399
27433
  }
26400
27434
  return collisions;
26401
27435
  }
26402
- function usage21() {
27436
+ function usage22() {
26403
27437
  console.error("Usage: forgecad check params <script.forge.js> [--samples N]");
26404
27438
  process.exit(1);
26405
27439
  }
26406
27440
  async function runParamCheckCli(argv = process.argv.slice(2)) {
26407
27441
  const scriptPath = argv[0];
26408
- if (!scriptPath) usage21();
27442
+ if (!scriptPath) usage22();
26409
27443
  const samplesArg = argv.indexOf("--samples");
26410
27444
  const numSamples = samplesArg >= 0 ? parseInt(argv[samplesArg + 1], 10) : 8;
26411
27445
  const code = readFileSync21(resolve35(scriptPath), "utf-8");
@@ -26965,7 +27999,9 @@ function buildPrintCheckReport(objects, verifications, options) {
26965
27999
  if (entries.length === 0) {
26966
28000
  checks.push(check("print.scene.objects", "Printable objects", "fail", "error", "No visible shape objects were found."));
26967
28001
  } else {
26968
- checks.push(check("print.scene.objects", "Printable objects", "pass", "info", `${entries.length} shape object(s) ready for print checks.`));
28002
+ checks.push(
28003
+ check("print.scene.objects", "Printable objects", "pass", "info", `${entries.length} shape object(s) ready for print checks.`)
28004
+ );
26969
28005
  }
26970
28006
  const sceneBox = entries.length > 0 ? { min: sceneBBox.min, max: sceneBBox.max, size: bboxSize(sceneBBox.min, sceneBBox.max) } : null;
26971
28007
  const failedVerifications = verifications.filter((verification) => verification.status === "fail");
@@ -27017,7 +28053,9 @@ function buildPrintCheckReport(objects, verifications, options) {
27017
28053
  )
27018
28054
  );
27019
28055
  const thinObjects = objectReports.filter((object) => (object.thickness?.criticalAreaPercent ?? 0) > options.profile.maxThinAreaPercent);
27020
- const warningThinObjects = objectReports.filter((object) => (object.thickness?.warningAreaPercent ?? 0) > options.profile.maxThinAreaPercent);
28056
+ const warningThinObjects = objectReports.filter(
28057
+ (object) => (object.thickness?.warningAreaPercent ?? 0) > options.profile.maxThinAreaPercent
28058
+ );
27021
28059
  checks.push(
27022
28060
  check(
27023
28061
  "print.fdm.wall-thickness",
@@ -27085,7 +28123,7 @@ function buildPrintCheckReport(objects, verifications, options) {
27085
28123
  }
27086
28124
 
27087
28125
  // cli/print-check.ts
27088
- function usage22() {
28126
+ function usage23() {
27089
28127
  console.error(
27090
28128
  "Usage: forgecad check print <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [--json] [--output report.json] [--profile fdm-pla-0.4mm] [--param Key=Value]"
27091
28129
  );
@@ -27212,7 +28250,7 @@ function parseArgs10(argv) {
27212
28250
  }
27213
28251
  const positional = argv.filter((arg, index) => !consumed.has(index) && !arg.startsWith("-"));
27214
28252
  const scriptPath = positional[0];
27215
- if (!scriptPath) usage22();
28253
+ if (!scriptPath) usage23();
27216
28254
  return {
27217
28255
  scriptPath,
27218
28256
  json,
@@ -27348,7 +28386,7 @@ import { resolve as resolve38 } from "path";
27348
28386
 
27349
28387
  // cli/solver-debug-artifacts.ts
27350
28388
  import { mkdir as mkdir4, writeFile as writeFile9 } from "fs/promises";
27351
- import { basename as basename15, join as join13, resolve as resolve37 } from "path";
28389
+ import { basename as basename15, join as join14, resolve as resolve37 } from "path";
27352
28390
 
27353
28391
  // cli/solver-debug-html.ts
27354
28392
  function escapeHtml(s) {
@@ -28063,14 +29101,14 @@ async function writeSolverDebugArtifacts(outputRoot, scriptPath, objects) {
28063
29101
  const seen = usedNames.get(baseName) ?? 0;
28064
29102
  usedNames.set(baseName, seen + 1);
28065
29103
  const dirName = seen === 0 ? baseName : `${baseName}-${seen + 1}`;
28066
- const dir = join13(root, dirName);
28067
- const svgDir = join13(dir, "svg");
29104
+ const dir = join14(root, dirName);
29105
+ const svgDir = join14(dir, "svg");
28068
29106
  await mkdir4(svgDir, { recursive: true });
28069
- const transcriptPath = join13(dir, "constructive-transcript.md");
29107
+ const transcriptPath = join14(dir, "constructive-transcript.md");
28070
29108
  await writeFile9(transcriptPath, buildConstructiveTranscriptMarkdown(object.name || dirName, meta), "utf8");
28071
29109
  const snapshotPaths = [];
28072
29110
  for (const snapshot of debug.svgSnapshots) {
28073
- const snapshotPath = join13(svgDir, `${snapshot.label}.svg`);
29111
+ const snapshotPath = join14(svgDir, `${snapshot.label}.svg`);
28074
29112
  await writeFile9(snapshotPath, snapshot.svg, "utf8");
28075
29113
  snapshotPaths.push(snapshotPath);
28076
29114
  }
@@ -28095,7 +29133,7 @@ async function writeSolverDebugArtifacts(outputRoot, scriptPath, objects) {
28095
29133
  for (const [index, html] of htmlMap) {
28096
29134
  const bundle = bundles[index] ?? bundles[bundles.length - 1];
28097
29135
  if (bundle) {
28098
- const htmlPath = join13(bundle.dir, "dashboard.html");
29136
+ const htmlPath = join14(bundle.dir, "dashboard.html");
28099
29137
  await writeFile9(htmlPath, html, "utf8");
28100
29138
  }
28101
29139
  }
@@ -28114,7 +29152,7 @@ async function writeSolverDebugIndex(root, scriptPath, bundles) {
28114
29152
  lines.push(` svg_snapshots: ${bundle.snapshotPaths.length}`);
28115
29153
  }
28116
29154
  }
28117
- await writeFile9(join13(root, "README.md"), lines.join("\n") + "\n", "utf8");
29155
+ await writeFile9(join14(root, "README.md"), lines.join("\n") + "\n", "utf8");
28118
29156
  }
28119
29157
 
28120
29158
  // cli/test-run.ts
@@ -28274,7 +29312,7 @@ function parseParamFlags2(argv) {
28274
29312
  }
28275
29313
  return { overrides, consumed };
28276
29314
  }
28277
- function usage23() {
29315
+ function usage24() {
28278
29316
  console.error(
28279
29317
  "Usage: forgecad run <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [--details] [--history] [--features] [--connectivity] [--connectivity-tolerance <mm>] [--param Key=Value] [--debug-imports] [--verbose|-v] [--backend manifold|occt|truck] [--quality live|default|high] [--solver-profile] [--solver-debug-out <dir>]"
28280
29318
  );
@@ -28418,9 +29456,14 @@ function formatThreeMfStructure(analysis) {
28418
29456
  function objectDisplayName(obj) {
28419
29457
  return obj.groupName ? `${obj.name} [${obj.groupName}]` : obj.name;
28420
29458
  }
28421
- function printCompactObjectSummary(objects, focusSummary) {
29459
+ function printCompactObjectSummary(objects, focusSummary, assemblyKinematics) {
28422
29460
  const focusTag = focusSummary ? ` (focused, ${focusSummary})` : "";
28423
29461
  console.log(`\u2713 Objects: ${objects.length}${focusTag}`);
29462
+ if (objects.length === 0 && assemblyKinematics) {
29463
+ console.log(
29464
+ ` Rig only: ${assemblyKinematics.links.length} link(s), ${assemblyKinematics.edges.length} edge(s), ${assemblyKinematics.angles.length} angle constraint(s)`
29465
+ );
29466
+ }
28424
29467
  const names = objects.map(objectDisplayName);
28425
29468
  const shown = names.slice(0, MAX_COMPACT_OBJECT_NAMES);
28426
29469
  for (let i = 0; i < shown.length; i += 4) {
@@ -28439,7 +29482,9 @@ async function runScriptCli(argv = process.argv.slice(2)) {
28439
29482
  const removedSpatialFlag = argv.find((arg) => arg === "--spatial" || arg.startsWith("--spatial="));
28440
29483
  if (removedSpatialFlag) {
28441
29484
  console.error("`forgecad run --spatial` has been removed.");
28442
- console.error("Use `forgecad inspect fit interference <model>` for collision evidence or `forgecad inspect physical gaps <model>` for spatial gap evidence.");
29485
+ console.error(
29486
+ "Use `forgecad inspect fit interference <model>` for collision evidence or `forgecad inspect physical gaps <model>` for spatial gap evidence."
29487
+ );
28443
29488
  process.exit(1);
28444
29489
  }
28445
29490
  const { overrides: paramCliOverrides, consumed: paramConsumed } = parseParamFlags2(argv);
@@ -28470,7 +29515,7 @@ async function runScriptCli(argv = process.argv.slice(2)) {
28470
29515
  (arg, i) => !paramConsumed.has(i) && !focusConsumed.has(i) && !connectivityConsumed.has(i) && arg !== "--details" && arg !== "--history" && arg !== "--features" && arg !== "--solver-profile" && arg !== "--debug-imports" && arg !== "--journeys" && arg !== "--journeys-json" && arg !== "--verbose" && arg !== "-v" && arg !== "--backend" && argv[i - 1] !== "--backend" && arg !== "--quality" && argv[i - 1] !== "--quality" && arg !== "-q" && argv[i - 1] !== "-q" && arg !== "--solver-debug-out" && argv[i - 1] !== "--solver-debug-out"
28471
29516
  );
28472
29517
  const scriptPath = positional[0];
28473
- if (!scriptPath) usage23();
29518
+ if (!scriptPath) usage24();
28474
29519
  let activeBackend = CLI_DEFAULT_BACKEND;
28475
29520
  try {
28476
29521
  if (solverDebugOut) {
@@ -28564,7 +29609,7 @@ async function runScriptCli(argv = process.argv.slice(2)) {
28564
29609
  }
28565
29610
  }
28566
29611
  } else {
28567
- printCompactObjectSummary(visibleObjects, focusSummary);
29612
+ printCompactObjectSummary(visibleObjects, focusSummary, result.assemblyKinematics);
28568
29613
  }
28569
29614
  if (detailedObjects && input.directThreeMfAnalysis) {
28570
29615
  console.log(`
@@ -28822,7 +29867,7 @@ async function runScriptCli(argv = process.argv.slice(2)) {
28822
29867
  // cli/forge-render-section.ts
28823
29868
  import { writeFile as writeFile10 } from "fs/promises";
28824
29869
  import { basename as basename16, extname as extname11, resolve as resolve39 } from "path";
28825
- function usage24() {
29870
+ function usage25() {
28826
29871
  console.error(
28827
29872
  "Usage: forgecad render section <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [output.svg|.png] [--param Key=Value] [--plane XY|XZ|YZ] [--offset <number>] [--size <px>] [--edges <off|thin|bold>] [--chrome-path <path>] [--background <color>]"
28828
29873
  );
@@ -28883,12 +29928,12 @@ async function runRenderSectionCli(argv = process.argv.slice(2)) {
28883
29928
  background = argv[++i];
28884
29929
  } else if (argv[i].startsWith("-")) {
28885
29930
  console.error(`Unknown option: ${argv[i]}`);
28886
- usage24();
29931
+ usage25();
28887
29932
  } else {
28888
29933
  args.push(argv[i]);
28889
29934
  }
28890
29935
  }
28891
- if (args.length === 0) usage24();
29936
+ if (args.length === 0) usage25();
28892
29937
  scriptPath = args[0];
28893
29938
  const wantsPng = args[1] ? extname11(args[1]).toLowerCase() === ".png" : false;
28894
29939
  outputPath = args[1] || defaultOutput(scriptPath, wantsPng ? "png" : "svg");
@@ -28946,17 +29991,17 @@ async function runRenderSectionCli(argv = process.argv.slice(2)) {
28946
29991
  }
28947
29992
 
28948
29993
  // cli/update-check.ts
28949
- import { spawn as spawn3 } from "child_process";
29994
+ import { spawn as spawn4 } from "child_process";
28950
29995
  import { existsSync as existsSync19, mkdirSync as mkdirSync10, readFileSync as readFileSync22, writeFileSync as writeFileSync17 } from "fs";
28951
29996
  import { homedir as homedir9 } from "os";
28952
- import { dirname as dirname10, join as join14 } from "path";
29997
+ import { dirname as dirname10, join as join15 } from "path";
28953
29998
  var PACKAGE_NAME = "forgecad";
28954
29999
  var REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
28955
30000
  var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
28956
30001
  var FETCH_TIMEOUT_MS = 3e3;
28957
30002
  var WORKER_FLAG = "--__update-check-worker";
28958
30003
  function cachePath() {
28959
- return join14(homedir9(), ".forgecad", "update-check.json");
30004
+ return join15(homedir9(), ".forgecad", "update-check.json");
28960
30005
  }
28961
30006
  function readCache() {
28962
30007
  try {
@@ -29025,7 +30070,7 @@ function scheduleUpdateCheck(currentVersion) {
29025
30070
  const scriptPath = process.argv[1];
29026
30071
  if (!scriptPath) return;
29027
30072
  try {
29028
- const child = spawn3(process.execPath, [scriptPath, WORKER_FLAG], {
30073
+ const child = spawn4(process.execPath, [scriptPath, WORKER_FLAG], {
29029
30074
  detached: true,
29030
30075
  stdio: "ignore",
29031
30076
  windowsHide: true,
@@ -29073,7 +30118,7 @@ var RENDER_ANGLE_VALUES = [
29073
30118
  ];
29074
30119
  var CAPTURE_TYPE_VALUES = [
29075
30120
  { value: "orbit", description: "Orbit the camera around the model" },
29076
- { value: "animation", description: "Play a jointsView clip with a fixed camera" },
30121
+ { value: "animation", description: "Play a named joint clip with a fixed camera" },
29077
30122
  { value: "section-sweep", description: "Move a clipping plane through the model" }
29078
30123
  ];
29079
30124
  var OUTPUT_FORMAT_VALUES = [
@@ -29162,8 +30207,7 @@ var RENDER_STYLE_VALUES = [
29162
30207
  { value: "fast", description: "Low-cost attractive live preview shading" },
29163
30208
  { value: "glass", description: "Transparent inspection view with rim shell and readable edges" },
29164
30209
  { value: "inspection", description: "Light technical evidence style with matte shading and dark edges" },
29165
- { value: "precision", description: "Dense technical surface-field contours" },
29166
- { value: "hybrid", description: "Topo-led surface contours with a quiet orthogonal field" },
30210
+ { value: "contour", description: "Dense technical surface-field contours" },
29167
30211
  { value: "scan", description: "Dark spatial-report style with object-colored scene-grid cells" }
29168
30212
  ];
29169
30213
  var PARAM_OPTIONS = [
@@ -29226,11 +30270,11 @@ var RENDER_OPTIONS = [
29226
30270
  name: "--render-style",
29227
30271
  description: "Visual render style (render default: classic; inspect default: inspection)",
29228
30272
  argument: "required",
29229
- valueLabel: "<classic|studio|fast|glass|inspection|precision|hybrid|scan>",
30273
+ valueLabel: "<classic|studio|fast|glass|inspection|contour|scan>",
29230
30274
  values: RENDER_STYLE_VALUES
29231
30275
  },
29232
30276
  { name: "--scan-granularity", description: "Scan cells across the scene longest axis", argument: "required", valueLabel: "<12-144>" },
29233
- { name: "--port", description: "Vite dev server port", argument: "required", valueLabel: "<n>" },
30277
+ { name: "--port", description: "Renderer server port", argument: "required", valueLabel: "<n>" },
29234
30278
  { name: "--fresh-server", description: "Start a fresh renderer instead of reusing an existing one" },
29235
30279
  {
29236
30280
  name: "--chrome-path",
@@ -29323,7 +30367,7 @@ var INSPECT_EVIDENCE_OPTIONS = [
29323
30367
  name: "--render-style",
29324
30368
  description: "Visual render style (default: inspection)",
29325
30369
  argument: "required",
29326
- valueLabel: "<classic|studio|fast|glass|inspection|precision|hybrid|scan>",
30370
+ valueLabel: "<classic|studio|fast|glass|inspection|contour|scan>",
29327
30371
  values: RENDER_STYLE_VALUES
29328
30372
  },
29329
30373
  {
@@ -29437,7 +30481,7 @@ var INSPECT_EVIDENCE_OPTIONS = [
29437
30481
  { value: "clean", description: "Solid cap faces" }
29438
30482
  ]
29439
30483
  },
29440
- { name: "--port", description: "Vite dev server port", argument: "required", valueLabel: "<n>" },
30484
+ { name: "--port", description: "Renderer server port", argument: "required", valueLabel: "<n>" },
29441
30485
  { name: "--fresh-server", description: "Start a fresh renderer instead of reusing an existing one" },
29442
30486
  {
29443
30487
  name: "--chrome-path",
@@ -29523,7 +30567,7 @@ var INSPECT_SECTIONS_COMMON_OPTIONS = [
29523
30567
  values: BACKEND_VALUES
29524
30568
  },
29525
30569
  { name: "--force", description: "Replace the requested output directory instead of allocating a sibling" },
29526
- { name: "--port", description: "Vite dev server port", argument: "required", valueLabel: "<n>" },
30570
+ { name: "--port", description: "Renderer server port", argument: "required", valueLabel: "<n>" },
29527
30571
  { name: "--fresh-server", description: "Start a fresh renderer instead of reusing an existing one" },
29528
30572
  {
29529
30573
  name: "--chrome-path",
@@ -29703,6 +30747,14 @@ var INSPECT_CANONICAL_SPECS = [
29703
30747
  description: "Use this to inspect orientation changes, flipped surfaces, and faceting next to image or Zebra evidence.",
29704
30748
  examples: ["forgecad inspect visual normals examples/api/static-assembly-connectors.forge.js --camera iso"]
29705
30749
  },
30750
+ {
30751
+ path: ["inspect", "visual", "rig"],
30752
+ evidenceName: "rig",
30753
+ label: "visual rig",
30754
+ summary: "Capture kinematic rig skeleton evidence.",
30755
+ description: "Use this to inspect joint pivots, axes, angle arcs, hierarchy links, and child-part links with geometry reduced to a shadow.",
30756
+ examples: ["forgecad inspect visual rig model.forge.js --camera iso"]
30757
+ },
29706
30758
  {
29707
30759
  path: ["inspect", "visual", "objects"],
29708
30760
  evidenceName: "objects",
@@ -29820,7 +30872,7 @@ Cutaway options:
29820
30872
 
29821
30873
  Visual options:
29822
30874
  --background <color> Canvas background override
29823
- --render-style <classic|studio|fast|glass|inspection|precision|hybrid|scan>
30875
+ --render-style <classic|studio|fast|glass|inspection|contour|scan>
29824
30876
  Visual render style (default: inspection)
29825
30877
  --edges <off|thin|bold> Edge overlay preset for solid visual evidence (default: thin)
29826
30878
 
@@ -29873,18 +30925,19 @@ var INSPECT_TOPIC_COMMANDS = [
29873
30925
  {
29874
30926
  group: "Inspect",
29875
30927
  path: ["inspect", "visual"],
29876
- summary: "Inspect visual context, cutaways, depth, normals, or object identity.",
29877
- description: "Choose `image`, `cutaway`, `depth`, `normals`, or `objects` visual evidence.",
30928
+ summary: "Inspect visual context, cutaways, depth, normals, rig skeletons, or object identity.",
30929
+ description: "Choose `image`, `cutaway`, `depth`, `normals`, `rig`, or `objects` visual evidence.",
29878
30930
  usage: [
29879
- "forgecad inspect visual <image|cutaway|depth|normals|objects> <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [options]"
30931
+ "forgecad inspect visual <image|cutaway|depth|normals|rig|objects> <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [options]"
29880
30932
  ],
29881
30933
  examples: [
29882
30934
  "forgecad inspect visual image model.forge.js --camera iso",
29883
30935
  "forgecad inspect visual cutaway model.forge.js --plane yz",
30936
+ "forgecad inspect visual rig model.forge.js --camera iso",
29884
30937
  "forgecad inspect visual objects model.forge.js --camera front --camera right"
29885
30938
  ],
29886
30939
  run: async () => {
29887
- console.error("`forgecad inspect visual` needs a mode: image, cutaway, depth, normals, or objects.");
30940
+ console.error("`forgecad inspect visual` needs a mode: image, cutaway, depth, normals, rig, or objects.");
29888
30941
  process.exit(1);
29889
30942
  }
29890
30943
  },
@@ -29958,11 +31011,12 @@ var INSPECT_TOPIC_COMMANDS = [
29958
31011
  }
29959
31012
  ];
29960
31013
  var INSPECT_COMMAND_OVERVIEW = [
31014
+ "- `inspect sketch` \u2014 returned sketch/profile regions, selector dry-runs, and extrusion compatibility",
29961
31015
  "- `inspect history` \u2014 final-object feature recipes and feedback anchors",
29962
31016
  "- `inspect design-trace` \u2014 raw construction DAG, source spans, and cache diagnostics",
29963
31017
  "- `inspect section` \u2014 agent-native one-off section probe with optional ray rulers and replay JSON",
29964
31018
  "- `inspect replay` \u2014 rerun a saved section probe on the same or another source",
29965
- "- `inspect visual image|cutaway|depth|normals|objects` \u2014 visual context, clipped 3D cutaways, depth, normals, and identity evidence",
31019
+ "- `inspect visual image|cutaway|depth|normals|rig|objects` \u2014 visual context, clipped 3D cutaways, depth, normals, rig skeletons, and identity evidence",
29966
31020
  "- `inspect surface zebra|roughness` \u2014 surface continuity and roughness evidence",
29967
31021
  "- `inspect physical components|floating|gaps` \u2014 physical component graph evidence",
29968
31022
  "- `inspect fit interference` \u2014 positive-volume overlap evidence",
@@ -30102,7 +31156,7 @@ var CAPTURE_COMMON_OPTIONS = [
30102
31156
  valueLabel: "<orbit|animation|section-sweep>",
30103
31157
  values: CAPTURE_TYPE_VALUES
30104
31158
  },
30105
- { name: "--animation", description: "Named jointsView animation clip", argument: "required", valueLabel: "<name>" },
31159
+ { name: "--animation", description: "Named joint animation clip", argument: "required", valueLabel: "<name>" },
30106
31160
  { name: "--animation-loops", description: "Repeat the selected animation clip", argument: "required", valueLabel: "<n>" },
30107
31161
  { name: "--cut-plane", description: "Enable a named cut plane", argument: "required", valueLabel: "<name>", repeatable: true },
30108
31162
  {
@@ -30262,15 +31316,14 @@ var commands = [
30262
31316
  group: "Shell",
30263
31317
  path: ["skill", "install"],
30264
31318
  summary: "Install ForgeCAD agent skills into the default global agent skill directory.",
30265
- usage: ["forgecad skill install [--target agents|claude|codex|opencode|all] [--core-only] [--dev]"],
31319
+ usage: ["forgecad skill install [--target agents|claude|codex|opencode|all] [--core-only]"],
30266
31320
  examples: [
30267
31321
  "forgecad skill install",
30268
31322
  "forgecad skill install --target claude",
30269
31323
  "forgecad skill install --target codex",
30270
31324
  "forgecad skill install --target opencode",
30271
31325
  "forgecad skill install --target all",
30272
- "forgecad skill install --core-only",
30273
- "forgecad skill install --dev"
31326
+ "forgecad skill install --core-only"
30274
31327
  ],
30275
31328
  completion: {
30276
31329
  options: [
@@ -30289,7 +31342,6 @@ var commands = [
30289
31342
  },
30290
31343
  { name: "--all-agents", description: "Alias for `--target all`" },
30291
31344
  { name: "--core-only", description: "Install only the core forgecad skill, without companion workflow skills" },
30292
- { name: "--dev", description: "Install the internal ForgeCAD developer variant of the core skill" },
30293
31345
  { name: "--library", description: "Deprecated no-op; the public skill library is installed by default" },
30294
31346
  { name: "--all", description: "Deprecated no-op; the public skill library is installed by default" }
30295
31347
  ]
@@ -30504,7 +31556,7 @@ var commands = [
30504
31556
  group: "Modeling",
30505
31557
  path: ["render", "3d"],
30506
31558
  summary: "Render a Forge scene to PNG using the real viewport renderer.",
30507
- description: "Launches a headless Chrome instance, renders the scene with the same WebGL viewport as the editor, and saves a PNG. The output path defaults to `<script-name>.png` next to the input file.\n\nThe input can be a `.forge.js` script or a direct `.stl`, `.obj`, `.3mf`, `.step`, or `.stp` asset. Direct STEP/STP rendering auto-selects OCCT unless you pass `--backend`.\n\nUse `--focus` to isolate specific parts (hides everything else) or `--hide` to remove clutter like mock objects. The `--view` flag selects a named camera declared in `scene({ views })`. The `--camera` flag accepts built-in views (`front`, `top`, `iso`), `azimuth:elevation` angles, or an exact `proj/pos/target/up/fov` camera spec \u2014 pass `--camera` multiple times to render several viewpoints in one run. Use `--camera-json <file>` or `--scene <file>` for exact reproducible viewport cameras without shell escaping. Filtered renders report any visible objects whose bounding boxes are partially or fully outside the camera frame; exact cameras fail when they frame none of the visible objects.\n\nUse `--edges=<off|thin|bold>` to control the edge overlay. For a pure wireframe look, use `render wireframe` instead.\n\nThis is the standard way to visually verify geometry from the CLI or in agent workflows. For higher quality (path-traced, materials, HDRI lighting), use `render hq` instead.",
31559
+ description: "Launches a headless Chrome instance, renders the scene with the same WebGL viewport as the editor, and saves a PNG. The output path defaults to `<script-name>.png` next to the input file. Each render uses a private renderer server by default, so parallel renders do not compete for the same Vite port.\n\nThe input can be a `.forge.js` script or a direct `.stl`, `.obj`, `.3mf`, `.step`, or `.stp` asset. Direct STEP/STP rendering auto-selects OCCT unless you pass `--backend`.\n\nUse `--focus` to isolate specific parts (hides everything else) or `--hide` to remove clutter like mock objects. The `--view` flag selects a named camera declared in `scene({ views })`. The `--camera` flag accepts built-in views (`front`, `top`, `iso`), `azimuth:elevation` angles, or an exact `proj/pos/target/up/fov` camera spec \u2014 pass `--camera` multiple times to render several viewpoints in one run. Use `--camera-json <file>` or `--scene <file>` for exact reproducible viewport cameras without shell escaping. Filtered renders report any visible objects whose bounding boxes are partially or fully outside the camera frame; exact cameras fail when they frame none of the visible objects.\n\nUse `--edges=<off|thin|bold>` to control the edge overlay. For a pure wireframe look, use `render wireframe` instead.\n\nThis is the standard way to visually verify geometry from the CLI or in agent workflows. For higher quality (path-traced, materials, HDRI lighting), use `render hq` instead.",
30508
31560
  usage: [
30509
31561
  "forgecad render 3d <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [output.png] [--param Key=Value] [--focus [names]] [--hide names] [--edges off|thin|bold] [options]"
30510
31562
  ],
@@ -30706,8 +31758,8 @@ var commands = [
30706
31758
  {
30707
31759
  group: "Modeling",
30708
31760
  path: ["capture", "gif"],
30709
- summary: "Capture an animated GIF from a script via orbit, jointsView playback, or section sweep.",
30710
- description: "Renders an animated sequence by either orbiting the camera around the model or playing back a `jointsView` animation. Use `--capture orbit` (default) for a turntable rotation, `--capture animation --animation <name>` to play a named joints clip, or `--capture section-sweep` to move a clipping plane through the model. Supports `--cut-plane` to animate with a static cross-section visible. Use `--view`, `--camera`, `--camera-json`, or `--scene <file>` to choose the orbit base camera or the fixed camera for animations and section sweeps.",
31761
+ summary: "Capture an animated GIF from a script via orbit, named joint playback, or section sweep.",
31762
+ description: "Renders an animated sequence by either orbiting the camera around the model or playing back a named joint animation. Use `--capture orbit` (default) for a turntable rotation, `--capture animation --animation <name>` to play a named joint clip, or `--capture section-sweep` to move a clipping plane through the model. Supports `--cut-plane` to animate with a static cross-section visible. Use `--view`, `--camera`, `--camera-json`, or `--scene <file>` to choose the orbit base camera or the fixed camera for animations and section sweeps.",
30711
31763
  usage: [
30712
31764
  "forgecad capture gif <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [output.gif] [options] [--param Key=Value]"
30713
31765
  ],
@@ -30731,14 +31783,14 @@ var commands = [
30731
31783
  {
30732
31784
  group: "Modeling",
30733
31785
  path: ["capture", "mp4"],
30734
- summary: "Capture an MP4 from a script via orbit, jointsView playback, or section sweep.",
31786
+ summary: "Capture an MP4 from a script via orbit, named joint playback, or section sweep.",
30735
31787
  description: "Same as `capture gif` but outputs H.264 MP4. Prefers ffmpeg when available for better compression; falls back to a pure-JS encoder. MP4 is better for longer animations, section sweeps, or higher resolutions where GIF file sizes become unwieldy. The camera flags match `capture gif`.",
30736
31788
  usage: [
30737
31789
  "forgecad capture mp4 <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [output.mp4] [options] [--param Key=Value]"
30738
31790
  ],
30739
31791
  examples: [
30740
31792
  "forgecad capture mp4 examples/products/cup.forge.js",
30741
- "forgecad capture mp4 examples/api/runtime-joints-view.forge.js out/step.mp4 --capture animation --animation Step",
31793
+ "forgecad capture mp4 examples/api/assembly-kinematics-four-bar.forge.js out/four-bar.mp4 --view iso",
30742
31794
  'forgecad capture mp4 model.forge.js out/raw.mp4 --param "Output=raw-sdf"',
30743
31795
  "forgecad capture mp4 model.forge.js out/front.mp4 --camera front",
30744
31796
  "forgecad capture mp4 model.forge.js out/hero.mp4 --view hero",
@@ -30866,14 +31918,23 @@ var commands = [
30866
31918
  {
30867
31919
  group: "Export",
30868
31920
  path: ["export", "step"],
30869
- summary: "Export exact STEP through the native OCCT runtime exporter.",
30870
- usage: ["forgecad export step <model.forge.js|asset.step|asset.stp> [--output path]"],
31921
+ summary: "Export exact STEP through the native OCCT or Truck runtime exporter.",
31922
+ usage: ["forgecad export step <model.forge.js|asset.step|asset.stp> [--output path] [--backend occt|truck]"],
30871
31923
  examples: [
30872
31924
  "forgecad export step examples/api/boolean-operations.forge.js",
30873
- "forgecad export step housing.step --output housing-copy.step"
31925
+ "forgecad export step housing.step --output housing-copy.step",
31926
+ "forgecad export step examples/api/mixed-edge-finishes-proof.forge.js --backend truck"
30874
31927
  ],
30875
31928
  completion: {
30876
- options: [{ name: "--output", description: "Output STEP path", argument: "required", valueLabel: "<path>", valueKind: "path" }],
31929
+ options: [
31930
+ { name: "--output", description: "Output STEP path", argument: "required", valueLabel: "<path>", valueKind: "path" },
31931
+ {
31932
+ name: "--backend",
31933
+ description: "Exact BREP exporter: occt (default) or truck (native analytic kernel)",
31934
+ argument: "required",
31935
+ valueLabel: "<occt|truck>"
31936
+ }
31937
+ ],
30877
31938
  positionals: [{ description: "Forge script or STEP file", valueKind: "exact-cad-input" }]
30878
31939
  },
30879
31940
  run: (args) => runBrepCli(["--format", "step", ...args])
@@ -31805,6 +32866,51 @@ var commands = [
31805
32866
  examples: ["forgecad inspect evidence", "forgecad inspect evidence --json"],
31806
32867
  run: runInspectEvidenceListCli
31807
32868
  },
32869
+ {
32870
+ group: "Inspect",
32871
+ path: ["inspect", "sketch"],
32872
+ summary: "Inspect returned sketches and profile regions used by returned shapes.",
32873
+ description: "Runs a model and reports inspectable 2D sketch/profile regions without requiring model code to register inspection hooks. The command inspects returned Sketch objects and profile-bearing shape compile plans such as extrude, cut, and revolve. Use `--seed x,y` to dry-run which region a point selector would consume, and `--operation extrude` to check extrusion compatibility.",
32874
+ usage: [
32875
+ "forgecad inspect sketch <model.forge.js> [--json] [--object <name-or-path>] [--seed <x,y>] [--operation extrude] [--param Key=Value] [--backend manifold|occt|truck] [--quality live|default|high]"
32876
+ ],
32877
+ examples: [
32878
+ "forgecad inspect sketch examples/api/sketch-regions.forge.js",
32879
+ 'forgecad inspect sketch model.forge.js --object "Profile" --seed 45,15',
32880
+ 'forgecad inspect sketch model.forge.js --json --object "Body" --seed 45,15 --operation extrude'
32881
+ ],
32882
+ completion: {
32883
+ options: [
32884
+ { name: "--json", description: "Emit machine-readable JSON" },
32885
+ { name: "--compact", description: "Minify JSON output" },
32886
+ { name: "--object", description: "Inspect one scene object by name, id, or tree path", argument: "required", valueLabel: "<name>" },
32887
+ {
32888
+ name: "--seed",
32889
+ description: "Dry-run a region selector point in sketch-local coordinates",
32890
+ argument: "required",
32891
+ valueLabel: "<x,y>"
32892
+ },
32893
+ { name: "--operation", description: "Operation compatibility to preview", argument: "required", valueLabel: "<extrude>" },
32894
+ ...PARAM_OPTIONS,
32895
+ {
32896
+ name: "--quality",
32897
+ description: "Geometry quality",
32898
+ argument: "required",
32899
+ valueLabel: "<default|live|high>",
32900
+ values: QUALITY_VALUES
32901
+ },
32902
+ {
32903
+ name: "--backend",
32904
+ description: "Geometry backend",
32905
+ argument: "required",
32906
+ valueLabel: "<manifold|occt|truck>",
32907
+ values: BACKEND_VALUES
32908
+ }
32909
+ ],
32910
+ positionals: [{ description: "Forge script or CAD asset", valueKind: "renderable" }]
32911
+ },
32912
+ run: runInspectSketchCli
32913
+ },
31808
32914
  {
31809
32915
  group: "Inspect",
31810
32916
  path: ["inspect", "section"],
@@ -32068,7 +33174,7 @@ var commands = [
32068
33174
  { name: "--update", description: "Regenerate compiler snapshots" }
32069
33175
  ]
32070
33176
  },
32071
- run: async (args) => (await import("./check-compiler-U5SOPN7X.js")).runCheckCompilerCli(args)
33177
+ run: async (args) => (await import("./check-compiler-SDX5QIXI.js")).runCheckCompilerCli(args)
32072
33178
  },
32073
33179
  {
32074
33180
  group: "Checks",
@@ -32091,7 +33197,7 @@ var commands = [
32091
33197
  { name: "--update", description: "Regenerate query-propagation snapshots" }
32092
33198
  ]
32093
33199
  },
32094
- run: async (args) => (await import("./check-query-propagation-XOKNSSYU.js")).runCheckQueryPropagationCli(args)
33200
+ run: async (args) => (await import("./check-query-propagation-EAYEFT77.js")).runCheckQueryPropagationCli(args)
32095
33201
  },
32096
33202
  {
32097
33203
  group: "Checks",
@@ -32881,7 +33987,9 @@ function printRemovedRunFullFlagMessage() {
32881
33987
  }
32882
33988
  function printRemovedRunSpatialFlagMessage() {
32883
33989
  console.error("`forgecad run --spatial` has been removed.");
32884
- console.error("Use `forgecad inspect fit interference <model>` for collision evidence or `forgecad inspect physical gaps <model>` for spatial gap evidence.");
33990
+ console.error(
33991
+ "Use `forgecad inspect fit interference <model>` for collision evidence or `forgecad inspect physical gaps <model>` for spatial gap evidence."
33992
+ );
32885
33993
  }
32886
33994
  function printInternalCheckCommandMessage(commandName) {
32887
33995
  if (commandName === "params") {
@@ -33034,4 +34142,3 @@ export {
33034
34142
  runForgeCadCli,
33035
34143
  runForgeCommandDefinition
33036
34144
  };
33037
- //# sourceMappingURL=forgecad.js.map