forgecad 0.9.16 → 0.10.1

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 (162) hide show
  1. package/dist/assets/{AdminPage-CXvls4-J.js → AdminPage-DcCnj0qo.js} +1 -1
  2. package/dist/assets/{BenchmarkPage-B27zk8xL.js → BenchmarkPage-BVEpJSVk.js} +1 -1
  3. package/dist/assets/{BlogPage-CMAVvgQL.js → BlogPage-DHaGP50_.js} +1 -1
  4. package/dist/assets/{DocsPage-knf4I4h7.js → DocsPage-CDoxHkz8.js} +40 -859
  5. package/dist/assets/EditorApp-BJ0Dloyh.js +16446 -0
  6. package/dist/assets/{EmbedViewer-D7ZGlFjx.js → EmbedViewer-CRKZbY0y.js} +2 -2
  7. package/dist/assets/{LandingPageProofDriven-CnevhTE8.js → LandingPageProofDriven-BxHkYRE7.js} +1 -1
  8. package/dist/assets/{LegalPage-BPTUmqeg.js → LegalPage-B-u6FrVv.js} +1 -1
  9. package/dist/assets/{PricingPage-B0D4goG_.js → PricingPage-CzpZ6-Ce.js} +1 -1
  10. package/dist/assets/{SettingsPage-CFF-UgjI.js → SettingsPage-CIZSSAd0.js} +1 -1
  11. package/dist/assets/{app-CE3sYcV7.css → app-CjsbDlb7.css} +143 -0
  12. package/dist/assets/{app-T0pDcSX4.js → app-DaTMg3nH.js} +1310 -290
  13. package/dist/assets/cli/{render-C5pcIISc.js → render-DPf4AYJK.js} +55 -60
  14. package/dist/assets/{constructionHistoryWorker-Ba2Hm58b.js → constructionHistoryWorker-AwMMWSxg.js} +1103 -349
  15. package/dist/assets/{evalWorker-vkx310U2.js → evalWorker-CjZZWRWW.js} +5209 -2643
  16. package/dist/assets/{inspectWorker-BuTJDVX6.js → inspectWorker-CZsCFtQT.js} +1163 -409
  17. package/dist/assets/{jointPose-B_Cgedn9.js → jointPose-DzQOViQH.js} +1 -1
  18. package/dist/assets/{manifold-BWgsjmAM.js → manifold-BYlzU521.js} +1 -1
  19. package/dist/assets/{manifold-D6IFSkhH.js → manifold-DgXo0T5P.js} +2 -2
  20. package/dist/assets/{manifold-rZexZI0G.js → manifold-K1SkarlQ.js} +1 -1
  21. package/dist/assets/{reportWorker-0AGij1Ru.js → reportWorker-B9nWwSrB.js} +8501 -3393
  22. package/dist/assets/{scalar-sampling-budget-J5cuzxT1.js → scalar-sampling-budget-prBw_s8t.js} +6067 -3479
  23. package/dist/assets/{scanProxyWorker-Vl4Wxa1y.js → scanProxyWorker-2GtDLk-R.js} +1 -1
  24. package/dist/assets/{javascript-1kQXfVaz.js → typescript-DBQ6RN5l.js} +874 -22
  25. package/dist/cli/render.html +1 -1
  26. package/dist/docs/index.html +3 -3
  27. package/dist/docs-raw/AI/usage.md +1 -1
  28. package/dist/docs-raw/CLI.md +77 -240
  29. package/dist/docs-raw/README.md +6 -0
  30. package/dist/docs-raw/component-model.md +17 -150
  31. package/dist/docs-raw/generated/assembly.md +188 -582
  32. package/dist/docs-raw/generated/concepts.md +259 -3501
  33. package/dist/docs-raw/generated/core.md +283 -1250
  34. package/dist/docs-raw/generated/curves.md +387 -1608
  35. package/dist/docs-raw/generated/legacy.md +162 -0
  36. package/dist/docs-raw/generated/lib.md +227 -85
  37. package/dist/docs-raw/generated/output.md +35 -99
  38. package/dist/docs-raw/generated/runtime-names.md +23 -23
  39. package/dist/docs-raw/generated/sdf.md +68 -284
  40. package/dist/docs-raw/generated/sheet-metal.md +68 -335
  41. package/dist/docs-raw/generated/sketch.md +240 -1161
  42. package/dist/docs-raw/generated/viewport.md +75 -316
  43. package/dist/docs-raw/generated/wood.md +21 -49
  44. package/dist/docs-raw/guides/coordinate-system.md +4 -42
  45. package/dist/docs-raw/guides/inspection-bundles.md +44 -442
  46. package/dist/docs-raw/guides/joint-design.md +18 -79
  47. package/dist/docs-raw/guides/positioning.md +21 -143
  48. package/dist/docs-raw/guides/scene-presentation.md +89 -0
  49. package/dist/docs-raw/guides/simready-quickstart.md +171 -0
  50. package/dist/docs-raw/simulation-workflow.md +273 -0
  51. package/dist/docs-raw/skills/forgecad-3d-reconstruction.md +25 -111
  52. package/dist/docs-raw/skills/forgecad-blockout-model.md +20 -117
  53. package/dist/docs-raw/skills/forgecad-component-model.md +23 -107
  54. package/dist/docs-raw/skills/forgecad-high-level-spec.md +47 -155
  55. package/dist/docs-raw/skills/forgecad-image-replicator.md +26 -143
  56. package/dist/docs-raw/skills/forgecad-lld.md +19 -113
  57. package/dist/docs-raw/skills/forgecad-make-a-model.md +112 -532
  58. package/dist/docs-raw/skills/forgecad-model-grader.md +38 -108
  59. package/dist/docs-raw/skills/forgecad-prepare-prompt.md +24 -211
  60. package/dist/docs-raw/skills/forgecad-project.md +13 -131
  61. package/dist/docs-raw/skills/forgecad-reconstruction-benchmark.md +42 -134
  62. package/dist/docs-raw/skills/forgecad-render-inspect.md +27 -174
  63. package/dist/docs-raw/skills/forgecad-visual-spec.md +32 -112
  64. package/dist/docs-raw/skills/forgecad.md +19 -18
  65. package/dist/docs-raw/skills/index.md +2 -0
  66. package/dist/docs-raw/welcome.md +2 -2
  67. package/dist/index.html +2 -2
  68. package/dist/llms.txt +1 -2
  69. package/dist/sitemap.xml +25 -13
  70. package/dist-cli/{check-compiler-SYQ2PWOB.js → check-compiler-II7NLPAB.js} +1 -1
  71. package/dist-cli/{check-query-propagation-HIAGV62W.js → check-query-propagation-7462TR3R.js} +1 -1
  72. package/dist-cli/{chunk-SPZE3DUY.js → chunk-UWTJCGXF.js} +5848 -2915
  73. package/dist-cli/forgecad.js +3496 -703
  74. package/dist-skill/CONTEXT.md +1797 -7963
  75. package/dist-skill/SKILL.md +15 -15
  76. package/dist-skill/docs/API/core/concepts.md +27 -157
  77. package/dist-skill/docs/CLI.md +77 -240
  78. package/dist-skill/docs/generated/assembly.md +182 -532
  79. package/dist-skill/docs/generated/core.md +283 -1250
  80. package/dist-skill/docs/generated/curves.md +387 -1609
  81. package/dist-skill/docs/generated/lib.md +227 -85
  82. package/dist-skill/docs/generated/output.md +35 -99
  83. package/dist-skill/docs/generated/runtime-names.md +16 -21
  84. package/dist-skill/docs/generated/sdf.md +68 -284
  85. package/dist-skill/docs/generated/sheet-metal.md +68 -335
  86. package/dist-skill/docs/generated/sketch.md +240 -1160
  87. package/dist-skill/docs/generated/viewport.md +75 -223
  88. package/dist-skill/docs/generated/wood.md +21 -49
  89. package/dist-skill/docs/guides/coordinate-system.md +4 -42
  90. package/dist-skill/docs/guides/inspection-bundles.md +44 -442
  91. package/dist-skill/docs/guides/joint-design.md +18 -79
  92. package/dist-skill/docs/guides/positioning.md +21 -143
  93. package/dist-skill/docs/guides/scene-presentation.md +89 -0
  94. package/dist-skill/docs/guides/surface-members.md +26 -0
  95. package/dist-skill/library/forgecad-3d-reconstruction/SKILL.md +23 -111
  96. package/dist-skill/library/forgecad-blockout-model/SKILL.md +18 -117
  97. package/dist-skill/library/forgecad-component-model/SKILL.md +21 -107
  98. package/dist-skill/library/forgecad-high-level-spec/SKILL.md +45 -155
  99. package/dist-skill/library/forgecad-image-replicator/SKILL.md +24 -143
  100. package/dist-skill/library/forgecad-lld/SKILL.md +17 -113
  101. package/dist-skill/library/forgecad-make-a-model/SKILL.md +110 -532
  102. package/dist-skill/library/forgecad-model-grader/SKILL.md +36 -108
  103. package/dist-skill/library/forgecad-prepare-prompt/SKILL.md +35 -224
  104. package/dist-skill/library/forgecad-prepare-prompt/references/default-profiles.md +43 -271
  105. package/dist-skill/library/forgecad-prepare-prompt/references/master-prompt.md +30 -99
  106. package/dist-skill/library/forgecad-project/SKILL.md +13 -133
  107. package/dist-skill/library/forgecad-reconstruction-benchmark/SKILL.md +29 -123
  108. package/dist-skill/library/forgecad-render-inspect/SKILL.md +25 -174
  109. package/dist-skill/library/forgecad-visual-spec/SKILL.md +30 -111
  110. package/dist-skill/website/skills/forgecad-3d-reconstruction.md +58 -0
  111. package/dist-skill/website/skills/forgecad-blockout-model.md +49 -0
  112. package/dist-skill/website/skills/forgecad-component-model.md +53 -0
  113. package/dist-skill/website/skills/forgecad-high-level-spec.md +101 -0
  114. package/dist-skill/website/skills/forgecad-image-replicator.md +63 -0
  115. package/dist-skill/website/skills/forgecad-lld.md +41 -0
  116. package/dist-skill/website/skills/forgecad-make-a-model.md +186 -0
  117. package/dist-skill/website/skills/forgecad-model-grader.md +82 -0
  118. package/dist-skill/website/skills/forgecad-prepare-prompt.md +63 -0
  119. package/dist-skill/website/skills/forgecad-project.md +26 -0
  120. package/dist-skill/website/skills/forgecad-reconstruction-benchmark.md +60 -0
  121. package/dist-skill/website/skills/forgecad-render-inspect.md +80 -0
  122. package/dist-skill/website/skills/forgecad-visual-spec.md +71 -0
  123. package/dist-skill/website/skills/forgecad.md +122 -0
  124. package/dist-skill/website/skills/index.md +26 -0
  125. package/examples/api/comparison-imported-sphere-candidate.forge.js +1 -1
  126. package/examples/api/conformal-product-ribbon.forge.js +1 -1
  127. package/examples/api/exact-sheet-shell-assembly.forge.js +1 -1
  128. package/examples/api/extrude-options.forge.js +4 -2
  129. package/examples/api/field-loft-drive-tip.forge.js +40 -0
  130. package/examples/api/guided-loft-olive-oil-bottle.forge.js +1 -1
  131. package/examples/api/highlight-debug.forge.js +10 -10
  132. package/examples/api/mesh-import-slats.forge.js +1 -1
  133. package/examples/api/real-product-curves.forge.js +1 -1
  134. package/examples/api/sculpt-box-circle-booleans.forge.js +1 -1
  135. package/examples/api/sdf-shapes.forge.js +2 -5
  136. package/examples/api/sketch-rounding-strategies.forge.js +6 -6
  137. package/examples/api/surface-member-bottle-cage.forge.js +3 -3
  138. package/examples/api/surface-member-conformal-product-ribbon.forge.js +3 -3
  139. package/examples/api/surface-member-razor-inlay.forge.js +1 -1
  140. package/examples/api/variable-sweep-test.forge.js +3 -3
  141. package/examples/mechanical/airplane-propeller.forge.js +74 -39
  142. package/examples/nurbs-surface.forge.js +1 -1
  143. package/examples/products/iphone.forge.js +1 -1
  144. package/examples/robotics/README.md +46 -0
  145. package/examples/robotics/scout-cam-rover-simready/README.md +119 -0
  146. package/examples/robotics/scout-cam-rover-simready/lib/dims.js +140 -0
  147. package/examples/robotics/scout-cam-rover-simready/main.forge.js +343 -0
  148. package/examples/robotics/scout-cam-rover-simready/parts/body.forge.js +304 -0
  149. package/examples/robotics/scout-cam-rover-simready/parts/chassis.forge.js +320 -0
  150. package/examples/robotics/scout-cam-rover-simready/parts/hardware.forge.js +21 -0
  151. package/examples/robotics/scout-cam-rover-simready/parts/turret.forge.js +70 -0
  152. package/examples/robotics/scout-cam-rover-simready/parts/wheel.forge.js +116 -0
  153. package/examples/robotics/simready-asset-crate.forge.js +79 -0
  154. package/examples/robotics/simready-diff-drive-rover.forge.js +141 -0
  155. package/examples/robotics/simready-parallel-gripper.forge.js +102 -0
  156. package/package.json +1 -1
  157. package/dist/assets/EditorApp-BHMQlJ-D.js +0 -14686
  158. package/dist/docs-raw/guides/geometry-conventions.md +0 -52
  159. package/dist/docs-raw/guides/modeling-recipes.md +0 -78
  160. package/dist-skill/docs/guides/geometry-conventions.md +0 -52
  161. package/dist-skill/docs/guides/modeling-recipes.md +0 -78
  162. package/dist-skill/library/forgecad-visual-spec/references/prompt-template.md +0 -79
@@ -5,98 +5,37 @@ skill-order: 5
5
5
 
6
6
  # Joint Design Recipes
7
7
 
8
- How to build mechanical joints — clevis-tongue hinges, ball-and-socket, dovetails — that actually rotate without binding and stop where they should.
8
+ Geometry recipes for joints that actually rotate without binding clevis-tongue hinges, hinge chains, hard stops.
9
9
 
10
- ## Frame-Aware Connectors First
11
-
12
- If a part must rotate, slide, or point in a specific physical direction, define connectors and use `assembly().connect()`. A connector is a small coordinate frame on the part:
13
-
14
- - `origin` is the pivot, pin center, socket center, or contact point.
15
- - `axis` is the hinge line or slide direction.
16
- - `up` is the secondary direction that fixes the part's zero-angle twist.
17
-
18
- For a hip -> knee -> wheel chain, the upper leg should be a real part with a hip connector and a knee connector. The hip connector frame defines how the upper leg sits on the hip drum at rest; the knee connector frame defines where the next part attaches. `connect()` aligns those frames and derives the joint axis.
19
-
20
- Do not treat `up` as optional on hinges, wheels, levers, or keyed sliders. If `up` is omitted, ForgeCAD chooses a deterministic perpendicular vector, which keeps the model stable but may not match the intended mechanical rest pose.
21
-
22
- Use `link()`, `edgeBetweenLinks()`, and `addAngleBetweenLinks()` for solved point skeletons and closed-loop relationships. A link is a point, not a bone frame. `addPart(..., { mate: { connector, toLink } })` only moves a connector origin onto a solved link point; it does not rotate the part to aim along an edge.
23
-
24
- ## Mirrored Revolute Axes
25
-
26
- Revolute joint values are physical values signed by the right-hand rule around the joint axis. If a bilateral mechanism mirrors a hinge axis, the same numeric joint value does not mean "same pose" on both sides.
27
-
28
- Example: a right ankle hinge with `axis: [1, 0, 0]` and a left ankle hinge with `axis: [-1, 0, 0]` are exact geometric mirrors at rest. But `+20` degrees around those two axes rotates the feet in opposite fore/aft senses. The mirrored pose uses `RightAnkle: 20` and `LeftAnkle: -20`.
29
-
30
- When you expose physical joint limits directly, mirror revolute limits as `[min, max] -> [-max, -min]`. Prismatic joints do not have this angle-handedness flip because their scalar value translates along the mirrored axis.
31
-
32
- For bilateral robots, legs, grippers, and suspension mechanisms, prefer a side-neutral control layer when possible: solve mirrored link positions with `link()`, `edgeBetweenLinks()`, and `addAngleBetweenLinks()`, then attach real parts to the solved skeleton. If you use connector-frame joints directly, make the sign mapping explicit in defaults, keyframes, and verification.
10
+ Anything that must rotate or slide gets connector frames: `origin` = pivot, `axis` = hinge line, `up` = rest twist. Always set `up` on hinges, wheels, and levers. Connector/link/mate semantics and mirrored-axis sign rules: see the assembly API reference.
33
11
 
34
12
  ## The Cavity Rule
35
13
 
36
- Every mechanical joint has a **cavity** in one part and a **tenon** in the other. The cavity must be a real empty volume — not a gap implied by the absence of two separate solids.
37
-
38
- If two adjacent parts in an assembly show a collision volume larger than the expected clearance volume in `forgecad run`, one part is missing its cavity. Both parts have solid material at the same joint position. This will look fine at rest pose but will block rotation and produce confusing joint behavior.
14
+ Every joint is a **cavity** in one part plus a **tenon** in the other, and the cavity must be a real empty volume — not a gap implied by separate solids. A body that runs solid through the joint zone (e.g. a stadium cap under the clevis slot) blocks rotation even though the rest pose looks fine. End the body FLAT before the joint; extend the tines forward to the pivot; the inter-tine volume must be genuinely empty.
39
15
 
40
- ```ts
41
- // BAD — body has a stadium cap at both ends; the "slot" between two clevis tines
42
- // is just empty space next to a solid body cap. The next phalanx's tongue knuckle
43
- // has nowhere to go (it intersects the previous body's cap).
44
- const body = stadiumBar(L); // cap at X=0 AND X=L
45
- const tine1 = box(...).translate(L, Y_OFF, 0);
46
- const tine2 = box(...).translate(L, -Y_OFF, 0);
47
- let phalanx = union(body, tine1, tine2);
16
+ Diagnostic: adjacent-part collision volume > expected clearance in `forgecad run` = missing cavity (both parts have solid material at the joint position). After fixing, the collision volume should drop to ~0 (or a few mm³ of clearance overlap).
48
17
 
49
- // GOOD — body ends FLAT before the joint. Tines extend forward to the pivot.
50
- // The X = L-KNUCK_R..L+KNUCK_R volume between the tines is genuinely empty.
51
- const body = box(L - KNUCK_R, TONG_T, H).translate((L - KNUCK_R) / 2, 0, -H / 2);
52
- const tongueKnuckle = knuckleDisc(0, 0, TONG_T); // proximal cap only
53
- let phalanx = union(tongueKnuckle, body, tine1, tine2, ...tineCaps);
54
- ```
18
+ ## Structural Sizing
55
19
 
56
- After applying the cavity rule, `forgecad run` collision volume between adjacent parts in a clevis-tongue chain should drop to **zero** (or a few mm³ of clearance overlap). If it doesn't, there's still solid material where there should be a cavity.
20
+ **Yoke (connecting cantilevers).** Clevis tines at Y = ±Y_OFF are physically disconnected from a body of thickness TONG_T when Y_OFF > TONG_T/2 + clearance — the tines float and would snap under load. Always bridge with a yoke slab spanning the full clevis width, `(Y_OFF + TINE_T/2) * 2`, with a few mm of structural overlap along the joint axis, so material runs continuously from body to each tine.
57
21
 
58
- ## Connecting Cantilevers
59
-
60
- A clevis tine arm at Y=±Y_OFF is geometrically separate from a body at Y=±TONG_T/2. With Y_OFF > TONG_T/2 + clearance, there is a **physical gap** between them. The tines float — they would snap off as soon as load is applied.
61
-
62
- Always add a **yoke**: a short slab spanning the full clevis width, sitting between the body's flat distal end and the tines' attachment point. The yoke fills the Y gap so material is continuous from the body through to each tine.
63
-
64
- ```ts
65
- const yokeLen = 3; // a few mm of structural overlap
66
- const yokeStart = L - KNUCK_R - yokeLen;
67
- const totalY = (Y_OFF + TINE_T / 2) * 2; // full clevis width
68
- const yoke = box(yokeLen, totalY, H)
69
- .translate(yokeStart + yokeLen / 2, 0, -H / 2);
70
- phalanx = union(phalanx, yoke);
71
- ```
22
+ **Knuckle radius.** For body height H, require `KNUCK_R >= H/2`. Smaller, and the body corners protrude past the knuckle's cylindrical envelope and sweep into the adjacent part during rotation. `KNUCK_R = H/2` makes the body cross-section a stadium that exactly fits the envelope.
72
23
 
73
24
  ## Hard Stops vs Slider Limits
74
25
 
75
- `addRevolute({ min: 0, max: 90 })` sets **slider limits** the viewport won't let the user drag past them, but the geometry permits any rotation. There is no physical stop.
76
-
77
- For a **geometric** hard stop (parts can't backbend past extension, or can't curl past full closure), add a small protrusion on one part that interferes with the other at the limit angle:
78
-
79
- - **Extension stop at 0°** (typical for fingers, knees, elbows): add a small "lip" on the dorsal side of the proximal end of the child phalanx, sized so it just touches the parent's distal dorsal corner at 0°. Negative rotation (backbending) is then blocked by part-on-part contact.
80
- - **Flexion stop at θmax**: add a similar lip on the palmar side, or rely on the body-to-body collision when bodies meet.
26
+ Declared joint min/max are not geometrythey only constrain the viewport slider; the geometry still permits any rotation. A physical stop requires an interfering protrusion:
81
27
 
82
- Verify with `forgecad run` at the limit poses the contact pair should show ~0 mm³ collision (just touching), and rotation past the limit should report a non-zero collision volume.
28
+ - **Extension stop at 0°**: a small lip on the dorsal side of the child's proximal end, sized to just touch the parent's distal dorsal corner at 0°; backbending is then blocked by contact.
29
+ - **Flexion stop at θmax**: a palmar lip, or body-on-body contact when bodies meet.
83
30
 
84
- ## Knuckle Sizing
85
-
86
- For a clevis-tongue joint with body height H, the tongue knuckle radius and clevis tine knuckle radius must satisfy:
87
-
88
- ```
89
- KNUCK_R >= H / 2
90
- ```
91
-
92
- If the knuckle radius is smaller than the body's half-height, the body's corners protrude beyond the knuckle envelope. When the joint rotates, those corners sweep through space outside the cylindrical envelope and collide with the adjacent part.
93
-
94
- Setting `KNUCK_R = H / 2` exactly makes the body cross-section a stadium that perfectly fits the knuckle envelope.
31
+ Verify: ~0 mm³ collision exactly at the limit pose (just touching), non-zero past it.
95
32
 
96
33
  ## Verification Workflow
97
34
 
98
- 1. Build the joint at rest pose. Run `forgecad run`. Check collision volumes.
99
- 2. If adjacent parts in the joint show > clearance-volume of overlap → missing cavity (apply the cavity rule).
100
- 3. Render with `--focus PartName` to inspect each part in isolation. The clevis end should clearly show a gap between the tines (the cavity).
101
- 4. Render at curl angles (set joint debug params) at 30°, 60°, 90°. No new collisions should appear from rotation.
102
- 5. Render at -10° (backbend test). Either no rotation possible (geometric stop in place) or rotation occurs and you need to add a stop.
35
+ Check the loop, not just the rest pose:
36
+
37
+ 1. Build at rest; `forgecad run`; check collision volumes.
38
+ 2. Overlap > clearance volume between joint neighbors apply the cavity rule.
39
+ 3. Render each part with `--focus PartName`; the clevis end must show a visible gap between tines.
40
+ 4. Re-check at swept angles (30°/60°/90°) — rotation reveals collisions the rest pose hides.
41
+ 5. Backbend test at -10°: blocked = hard stop exists; rotates = add a stop.
@@ -3,169 +3,47 @@ skill-group: geometry
3
3
  skill-order: 3
4
4
  ---
5
5
 
6
- # Positioning Strategy
6
+ # Positioning Decision Ladder
7
7
 
8
- ## Rule 0: if parts should touch, use connectors first
8
+ Most positioning bugs come from manual coordinate arithmetic. Pick the **highest applicable rung**; drop down only when the rung above doesn't fit.
9
9
 
10
- For any fixed assembly where parts are meant to stay in contact in the final model, start with connectors + `matchTo()`. This applies to furniture, fixtures, toys, enclosures, sleds, and any other static multi-part object, not only mechanisms.
10
+ ## 1. Connectors + `matchTo()` every real part-to-part interface
11
11
 
12
- Use raw `translate()` and `rotate()` when parts are intentionally free-floating or when you are doing quick exploratory layout. Use `attachTo()` for rough bounding-box placement. But if the relationship is a real interface, make it explicit with connectors.
13
-
14
- ## Mechanisms: connector frames vs link points
15
-
16
- For serial articulated parts (hinges, hips, knees, levers, wheels), use `assembly().connect()` with connectors. `connect()` aligns the full connector frame: `origin` is the pivot/contact point, `axis` is the hinge line or slide direction, and `up` locks the zero-angle twist of the child part. `up` is a local roll reference, not world up; author it explicitly whenever the rest pose matters.
17
-
18
- Do not use `addPart(..., { mate: { connector, toLink } })` when the part must point along a bone or inherit orientation from a link edge. Link mates are point attachments only: they translate the connector origin onto the solved link position and preserve the part's existing rotation. That is good for markers, sensors, labels, and debug handles. It is not a bone-frame API.
19
-
20
- Use link graphs (`link()`, `edgeBetweenLinks()`, `addAngleBetweenLinks()`) when the hard part is solving point positions, especially closed loops. Use connector-frame joints when the hard part is orienting real physical parts.
21
-
22
- For bilateral mechanisms, remember that connector-frame revolute values are physical axis-local values. Mirrored hinge axes need negated physical revolute values for the same mirrored pose, and physical limits mirror as `[min, max] -> [-max, -min]`. If you want `HipR: 10` and `HipL: 10` to mean the same semantic pose, drive a mirrored link graph or write an explicit state mapping instead of assuming equal connector-joint values are symmetric.
23
-
24
- ## Primitive origin convention
25
-
26
- All 3D primitives are **centered on XY, base at Z=0**:
27
-
28
- | Primitive | X range | Y range | Z range |
29
- |-----------|---------|---------|---------|
30
- | `box(60, 40, 20)` | [-30, 30] | [-20, 20] | [0, 20] |
31
- | `cylinder(50, 10)` | [-10, 10] | [-10, 10] | [0, 50] |
32
- | `sphere(15)` | [-15, 15] | [-15, 15] | [-15, 15] |
33
- | `torus(20, 5)` | [-25, 25] | [-25, 25] | [-5, 5] |
34
-
35
- Sphere and torus are fully centered (symmetric in Z). Box and cylinder sit on the XY ground plane — **Z goes up from zero, never negative**.
36
-
37
- This means `box(w, d, h).translate(0, 0, -h / 2)` is the manual way to "center on Z" — it moves the box from `[0, h]` to `[-h / 2, h / 2]`. Prefer `box(w, d, h).placeReference('center', [0, 0, 0])` when you want full XYZ centering.
38
-
39
- Do not assume `center: true` or a positional `true` gives OpenSCAD-style full XYZ centering. Primitive placement is fixed unless the primitive docs explicitly say otherwise.
40
-
41
- ---
42
-
43
- Most positioning bugs come from manual coordinate arithmetic. Use these methods in priority order.
44
-
45
- ## 1. Connectors + `matchTo()` — default for mating interfaces
46
-
47
- Define connectors on parts; `matchTo()` provides automatic alignment. With one connector pair, the child translates and rotates so its connector aligns with the target's — origins coincide, axes oppose (plug-in model), and `up` pins the roll reference. With multiple connector pairs, the connector origins define the rigid transform; still author meaningful `axis` and `up` values so the same connectors remain useful for `connect()`, audits, and future matching.
12
+ **Rule 0:** if parts are meant to stay in contact, define connectors and `matchTo()` including *static* assemblies (furniture, enclosures, fixtures, toys), not just mechanisms. Connectors win because they are **stable** (don't shift on fillet/chamfer/boolean), **semantic** (type/gender), **oriented** (full frame), **queryable** (`verify.connectorDistance`), and **explode-aware**.
48
13
 
49
14
  ```javascript
50
- const shelf = box(200, 120, 10).translate(0, 0, -5).withConnectors({
51
- left_tab: connector.male("dovetail", { origin: [-100, 0, 0], axis: [-1, 0, 0], up: [0, 0, 1] }),
15
+ const shelf = box(200, 120, 10).withConnectors({
16
+ tab: connector.male("dovetail", { origin: [-100, 0, 5], axis: [-1, 0, 0], up: [0, 0, 1] }),
52
17
  });
53
- const panel = box(12, 120, 200).translate(0, 0, -100).withConnectors({
54
- shelf_0: connector.female("dovetail", { origin: [6, 0, -50], axis: [1, 0, 0], up: [0, 0, 1] }),
55
- });
56
- const placed = shelf.matchTo(panel, "left_tab", "shelf_0");
57
- // Dictionary form for multiple pairs on same target:
58
- const placed2 = shelf.matchTo(panel, { left_tab: "shelf_0" });
59
- // Named group children bubble connectors via dotted paths:
60
- const cabinet = group({ name: "Left", shape: panel });
61
- shelf.matchTo(cabinet, "left_tab", "Left.shelf_0");
18
+ const placed = shelf.matchTo(panel, "tab", "shelf_slot");
62
19
  ```
63
20
 
64
- **Why connectors first:** stable (don't shift on fillet/chamfer/boolean), semantic (carry type/gender), oriented (full frame), queryable (`shape.connectorDistance('a','b')`), explode-aware.
65
-
66
- For a non-mechanism fixed-assembly example, see `examples/api/static-assembly-connectors.forge.js`.
21
+ Alignment semantics, dictionary form, and dotted group paths: see `matchTo()` JSDoc. For cross-file part alignment, prefer connectors over `withReferences()` placement points.
67
22
 
68
23
  ## 2. `group()` — local coordinates for multi-part assemblies
69
24
 
70
- The most common positioning bug: manually adding a parent's global offset to every sub-part. One wrong sign or forgotten variable and parts float into space. **Use `group()` to build parts in local coordinates (at the origin), then position the group once.**
25
+ Build sub-parts at the **local origin**, group them, then translate the group **once** — never add a parent's global offset to every sub-part. Groups nest; each level has its own local origin. Groups cannot be booleaned: do subtract/intersect first in local coordinates, then group the result. Worked example: `group()` JSDoc.
71
26
 
72
- ```javascript
73
- // BAD — every sub-part repeats the parent's global position
74
- const unitY = -18, unitZ = 70;
75
- const body = lib.roundedBox(100, 20, 32, 4).translate(0, unitY, unitZ);
76
- const panel = box(98, 2, 18).translate(0, unitY - 12, unitZ + 4);
77
- const louver = box(88, 2, 6).translate(0, unitY - 14, unitZ - 11);
78
- const led = sphere(1.2).translate(35, unitY - 12, unitZ + 9);
79
-
80
- // GOOD — build at local origin, group, translate once
81
- const body = lib.roundedBox(100, 20, 32, 4);
82
- const panel = box(98, 2, 18).translate(0, -12, 4); // relative to local origin
83
- const louver = box(88, 2, 6).translate(0, -14, -11); // relative to local origin
84
- const led = sphere(1.2).translate(35, -12, 9); // relative to local origin
85
- const indoorUnit = group(
86
- { name: 'Body', shape: body },
87
- { name: 'Panel', shape: panel },
88
- { name: 'Louver', shape: louver },
89
- { name: 'LED', shape: led },
90
- ).translate(0, -18, 70); // ONE translate for the whole assembly
91
- ```
27
+ ## 3. `pointAlong()` — orient before positioning
92
28
 
93
- **Groups nest.** Build sub-assemblies as groups, then group those into larger assemblies each level has its own local origin.
29
+ Always call `pointAlong()` **before** `matchTo()`/`translate()`it reorients around the origin.
94
30
 
95
- ```javascript
96
- const fan = group(hub, ...blades).translate(0, 25, 0); // fan assembly
97
- const outdoorUnit = group(
98
- { name: 'Body', shape: casing },
99
- { name: 'Fan', shape: fan }, // already a group
100
- { name: 'Grille', shape: grille },
101
- ).translate(0, 23, -42); // position the whole outdoor unit
102
- ```
31
+ ## 4. `attachTo()` — rough bounding-box placement only
103
32
 
104
- **When to use something else:** `group()` preserves individual shapes — you can't boolean (subtract/intersect) a group. If a sub-part needs a boolean with the parent body, do that boolean first in local coordinates, then group the result.
33
+ Anchor points shift after fillet/chamfer/boolean: fine for quick prototyping, fragile for assembly interfaces promote real interfaces to connectors.
105
34
 
106
- ## 3. `pointAlong()` — orient cylinders before positioning
35
+ ## 5. `placeReference()` — land a named anchor on a world coordinate
107
36
 
108
- ```javascript
109
- // BAD
110
- const pipe = cylinder(100, 5).rotateX(90).translate(x, y, z);
111
- // GOOD — reads as "pipe pointing along Y"
112
- const pipe = cylinder(100, 5).pointAlong([0, 1, 0]).translate(x, y, z);
113
- ```
37
+ Grounding, centering, edge alignment, custom reference points: see `placeReference()` JSDoc.
114
38
 
115
- **Always call `pointAlong()` BEFORE `matchTo()` or `translate()`** — it reorients around the origin.
39
+ ## 6. Last resort: `rotateAroundTo()`, `moveToLocal()`, `translate()`
116
40
 
117
- ## 4. `attachTo()` quick bounding-box positioning
41
+ For computed offsets and free-floating or exploratory layout. Raw `translate()`/`rotate()` is correct only when parts are intentionally unrelated.
118
42
 
119
- ```javascript
120
- const column = cylinder(50, 8).attachTo(base, 'top', 'bottom');
121
- ```
122
-
123
- `child.attachTo(parent, parentAnchor, selfAnchor, offset)`. Anchor points shift on fillet/chamfer/boolean — fragile for assembly interfaces, fine for quick prototyping.
124
-
125
- ## 5. `rotateAroundTo()` — aim a point around a hinge/axis
126
-
127
- ```javascript
128
- const aimed = arm.rotateAroundTo([0, 0, 1], [0, 0, 0], "tip", [30, 30, 20]);
129
- // Exact line solve:
130
- const lineHit = arm.rotateAroundTo([0, 0, 1], [0, 0, 0], "tip", [30, 30, 0], { mode: 'line' });
131
- ```
132
-
133
- ## 6. `moveToLocal()` — offset from another shape's min corner
134
-
135
- ```javascript
136
- const part = box(20, 20, 30).moveToLocal(base, 10, 10, 10);
137
- ```
138
-
139
- ## 7. `translate()` — for simple offsets or bridging computed locations
140
-
141
- ```javascript
142
- const pipeLen = bb2.min[1] - bb1.max[1];
143
- const pipe = cylinder(pipeLen, 5).pointAlong([0, 1, 0]).translate(40, (bb1.max[1] + bb2.min[1]) / 2, bb1.min[2] + 15);
144
- ```
145
-
146
- ## 8. `placeReference()` — align any anchor to a world coordinate
147
-
148
- Place a shape so a named anchor point lands exactly where you want it. Accepts all built-in anchors (`'bottom'`, `'center'`, `'top-front-left'`, etc.) plus custom references from `withReferences()`.
149
-
150
- ```javascript
151
- // Ground a shape — bottom face center at Z = 0
152
- const grounded = shape.placeReference('bottom', [0, 0, 0])
43
+ ## Mechanisms
153
44
 
154
- // Center at the world origin
155
- const centered = shape.placeReference('center', [0, 0, 0])
45
+ Link graphs (`link()`, `edgeBetweenLinks()`) solve **point positions** (closed loops); connector-frame joints (`assembly().connect()`) **orient physical parts**. Frame semantics and mirrored-revolute rules: assembly docs; joint geometry: `guides/joint-design.md`.
156
46
 
157
- // Align left edge to X = 10
158
- const aligned = shape.placeReference('left', [10, 0, 0])
159
- ```
160
-
161
- Also works with custom placement references for cross-file parts:
162
-
163
- ```javascript
164
- // widget.forge.js — define once
165
- return union(base, post).withReferences({ points: { mount: [0, -16, -4] } });
166
-
167
- // importer — consume
168
- const widget = require("./widget.forge.js").placeReference("mount", [120, 40, 0]);
169
- ```
47
+ ## Primitive placement
170
48
 
171
- For cross-file parts needing proper alignment, prefer connectors over placement references.
49
+ Box and cylinder sit base-at-Z=0, centered on XY; sphere and torus are fully centered. There is no OpenSCAD-style `center: true` — placement is fixed; use `placeReference('center', [0, 0, 0])` to fully center. Exact per-axis extents: primitive JSDoc.
@@ -0,0 +1,89 @@
1
+ ---
2
+ skill-group: recipes
3
+ skill-order: 1
4
+ ---
5
+
6
+ # Scene Presentation Recipes
7
+
8
+ Worked `scene()` setups. The option schema and behavioral cliffs (e.g. setting `lights` replaces all defaults) live in the `scene()` API docs (viewport group) — this file is copyable recipes only.
9
+
10
+ ## Baseline studio setup
11
+
12
+ Every model deserves at least this; default lighting looks flat.
13
+
14
+ ```js
15
+ scene({
16
+ background: { top: '#1a1a2e', bottom: '#0a0a14' },
17
+ camera: { position: [x, y, z], target: [0, 0, 0], fov: 42 },
18
+ environment: { preset: 'studio', intensity: 0.6 },
19
+ lights: [
20
+ { type: 'ambient', color: '#c8cdd4', intensity: 0.15 },
21
+ { type: 'directional', position: [80, -60, 120], target: [0, 0, 0], color: '#fff4e0', intensity: 1.8, castShadow: true },
22
+ { type: 'directional', position: [-60, 40, 80], target: [0, 0, 0], color: '#b0c4de', intensity: 0.7 },
23
+ ],
24
+ ground: { visible: true, color: '#111118', height: -10, receiveShadow: true },
25
+ postProcessing: {
26
+ bloom: { intensity: 0.3, threshold: 0.85, radius: 0.3 },
27
+ vignette: { darkness: 0.5, offset: 0.4 },
28
+ toneMappingExposure: 1.3,
29
+ },
30
+ });
31
+ ```
32
+
33
+ Adapt to the material family: metallic/jewelry → `studio`, exposure 1.2–1.5, subtle bloom; organic/wood/matte → `warehouse`/`apartment`, warmer ambient, lower bloom; dark/dramatic → `night`, bloom + vignette. Ground with `receiveShadow: true` only for objects that stand on something.
34
+
35
+ Camera: 3/4 angle, `fov` 35–50 (lower = flatter/telephoto), `target` at the visual center of mass — not necessarily `[0,0,0]`.
36
+
37
+ ## Matte industrial hero shot
38
+
39
+ For mechanisms, tools, product prototypes, and vehicles, prefer a matte studio look over gloss or atmosphere (ranges = tuning room, not options syntax):
40
+
41
+ ```js
42
+ scene({
43
+ background: { top: '#c3ccd7', bottom: '#566474' },
44
+ camera: { position: [430, -540, 340], target: [0, 30, 125], fov: 38 },
45
+ environment: { preset: 'studio', intensity: 0.2, background: false }, // 0.15–0.25
46
+ lights: [
47
+ { type: 'ambient', color: '#efe7dc', intensity: 0.15 }, // 0.12–0.2
48
+ { type: 'directional', position: [260, -320, 420], color: '#ffe2bf', intensity: 2.8, castShadow: true }, // 2.6–3.2
49
+ { type: 'directional', position: [-260, 210, 220], color: '#d4e6fb', intensity: 0.85 }, // 0.7–1.0
50
+ { type: 'hemisphere', skyColor: '#c7d3df', groundColor: '#495463', intensity: 0.15 }, // 0.1–0.2
51
+ ],
52
+ postProcessing: {
53
+ bloom: { intensity: 0.04, threshold: 0.94, radius: 0.28 },
54
+ vignette: { darkness: 0.4, offset: 0.32 },
55
+ toneMappingExposure: 1.1, // 1.05–1.18
56
+ },
57
+ });
58
+ ```
59
+
60
+ Stage the model on an intentionally matte plinth:
61
+
62
+ ```js
63
+ const stage = cylinder(16, 226).translate(0, 0, -26)
64
+ .color('#8b97a4').material({ metalness: 0.04, roughness: 0.78 });
65
+ mock(stage, 'StudioPlinth');
66
+ ```
67
+
68
+ Iteration rules that held up in practice:
69
+
70
+ - Prefer roughness over fog for softness — fog flattens form; matte materials keep shadow definition.
71
+ - Keep bloom near zero for mechanical scenes; too much reads toy-like.
72
+ - If the render is close but not right, nudge `toneMappingExposure` by ~0.05 before touching the light rig; avoid large ambient jumps — they kill contrast fastest.
73
+ - Add accent point lights near focal features, localized with `distance` and `decay`.
74
+
75
+ ## Named render views
76
+
77
+ For repeatable review or hero renders, declare views in `scene({ views })` — wrap each camera in `{ camera: ... }`:
78
+
79
+ ```js
80
+ scene({
81
+ camera: { position: [430, -540, 340], target: [0, 30, 125], fov: 38 },
82
+ views: {
83
+ hero: { camera: { position: [430, -540, 340], target: [0, 30, 125], up: [0, 0, 1], fov: 38 } },
84
+ side: { camera: { position: [700, 0, 180], target: [0, 30, 100], up: [0, 0, 1], fov: 32 } },
85
+ },
86
+ });
87
+ ```
88
+
89
+ Render one with `forgecad render 3d model.forge.js --view hero`.
@@ -0,0 +1,171 @@
1
+ # SimReady Quickstart
2
+
3
+ ForgeCAD simulation readiness starts in the model file. The `.forge.js` script should return the same assembly you want to inspect, export, and simulate; simulation metadata lives on that assembly instead of in a separate export command.
4
+
5
+ Use this pattern when you want a robot, fixture, gripper, crate, or imported asset to become a physics package later:
6
+
7
+ 1. Build real parts with connectors.
8
+ 2. Add each part to an `assembly(...)` with `sim: Sim.body(...)`.
9
+ 3. Connect moving parts with `connect(...)` and put drive intent on the joint.
10
+ 4. Finish the root assembly with `withSimulation(...)`.
11
+ 5. Run `forgecad check simready` before exporting.
12
+
13
+ ## Robot Pattern
14
+
15
+ ```js
16
+ const aluminum = Sim.material("6061 aluminum", {
17
+ densityKgM3: 2700,
18
+ staticFriction: 0.45,
19
+ dynamicFriction: 0.35,
20
+ restitution: 0.05,
21
+ });
22
+
23
+ const rubber = Sim.material("rubber tire", {
24
+ densityKgM3: 1100,
25
+ staticFriction: 0.9,
26
+ dynamicFriction: 0.75,
27
+ restitution: 0.12,
28
+ });
29
+
30
+ const chassis = box(220, 120, 28).withConnectors({
31
+ left_axle: connector({ origin: [0, 82, 0], axis: [0, 1, 0], up: [0, 0, 1], kind: "revolute" }),
32
+ right_axle: connector({ origin: [0, -82, 0], axis: [0, -1, 0], up: [0, 0, 1], kind: "revolute" }),
33
+ });
34
+
35
+ const wheel = cylinder(34, 28, undefined, 48)
36
+ .rotateX(90)
37
+ .withConnectors({
38
+ bore: connector({ origin: [0, 0, 0], axis: [0, 1, 0], up: [0, 0, 1], kind: "revolute" }),
39
+ tread: connector({ origin: [0, 0, -34], axis: [0, 0, -1], up: [1, 0, 0] }),
40
+ });
41
+
42
+ return assembly("Rover")
43
+ .addPart("Chassis", chassis, {
44
+ sim: Sim.body({
45
+ massKg: 4.2,
46
+ material: aluminum,
47
+ collider: Sim.collider.boundingBox(),
48
+ }),
49
+ })
50
+ .addPart("Left Wheel", wheel, {
51
+ sim: Sim.body({
52
+ massKg: 0.32,
53
+ material: rubber,
54
+ collider: Sim.collider.convexHull(),
55
+ contacts: { tread: Sim.contact.wheelSurface("tread") },
56
+ }),
57
+ })
58
+ .addPart("Right Wheel", wheel, {
59
+ sim: Sim.body({
60
+ massKg: 0.32,
61
+ material: rubber,
62
+ collider: Sim.collider.convexHull(),
63
+ contacts: { tread: Sim.contact.wheelSurface("tread") },
64
+ }),
65
+ })
66
+ .connect("Chassis.left_axle", "Left Wheel.bore", {
67
+ as: "leftWheel",
68
+ type: "revolute",
69
+ drive: Sim.drive.velocity({ maxTorqueNm: 1.8, maxSpeedRpm: 220, damping: 0.02, friction: 0.01 }),
70
+ })
71
+ .connect("Chassis.right_axle", "Right Wheel.bore", {
72
+ as: "rightWheel",
73
+ type: "revolute",
74
+ drive: Sim.drive.velocity({ maxTorqueNm: 1.8, maxSpeedRpm: 220, damping: 0.02, friction: 0.01 }),
75
+ })
76
+ .withSimulation({
77
+ rootPart: "Chassis",
78
+ profile: Sim.profile.robotBodyRunnable(),
79
+ controllers: [
80
+ Sim.controller.diffDrive({
81
+ leftJoints: ["leftWheel"],
82
+ rightJoints: ["rightWheel"],
83
+ wheelRadiusMm: 34,
84
+ wheelSeparationMm: 164,
85
+ }),
86
+ ],
87
+ });
88
+ ```
89
+
90
+ The important names are authored once:
91
+
92
+ - `rootPart` names the body that downstream simulators treat as the robot root.
93
+ - `leftWheel` and `rightWheel` are joint names, so controllers and exporters do not need to guess motor wiring.
94
+ - `tread` is an existing connector on the wheel, so contact metadata is checked against geometry-owned names.
95
+
96
+ ## Asset Pattern
97
+
98
+ Assets use the same assembly contract. In V1, a crate, prop, imported STEP, or static environment object is usually a one-part assembly:
99
+
100
+ ```js
101
+ const pine = Sim.material("pine crate", {
102
+ densityKgM3: 520,
103
+ staticFriction: 0.55,
104
+ dynamicFriction: 0.42,
105
+ restitution: 0.08,
106
+ });
107
+
108
+ const crate = box(140, 110, 80).withConnectors({
109
+ grip: connector({ origin: [0, -56, 8], axis: [0, -1, 0], up: [0, 0, 1] }),
110
+ });
111
+
112
+ return assembly("Crate Asset")
113
+ .addPart("Body", crate, {
114
+ sim: Sim.body({
115
+ massKg: 2.4,
116
+ material: pine,
117
+ collider: Sim.collider.convexHull(),
118
+ contacts: { grip: Sim.contact.gripperSurface("grip") },
119
+ }),
120
+ })
121
+ .withSimulation({
122
+ profile: Sim.profile.roboticsAssetPhysx(),
123
+ });
124
+ ```
125
+
126
+ Asset profiles require physical body metadata and explicit collider intent. They do not require `rootPart`, joints, or controllers.
127
+
128
+ ## Check Then Export
129
+
130
+ Run the offline gate first:
131
+
132
+ ```bash
133
+ node dist-cli/forgecad.js check simready examples/robotics/simready-diff-drive-rover.forge.js
134
+ ```
135
+
136
+ Then export the package that matches your simulator:
137
+
138
+ ```bash
139
+ node dist-cli/forgecad.js export mjcf examples/robotics/simready-diff-drive-rover.forge.js --output /tmp/rover-mjcf
140
+ node dist-cli/forgecad.js export sdf examples/robotics/simready-diff-drive-rover.forge.js --output /tmp/rover-sdf
141
+ node dist-cli/forgecad.js export urdf examples/robotics/simready-diff-drive-rover.forge.js --output /tmp/rover-urdf
142
+ ```
143
+
144
+ Use MJCF/MuJoCo for the fastest local physics loop. Use SDF for Gazebo Sim. Use URDF when the next tool expects a ROS/PyBullet/Isaac import path.
145
+
146
+ ForgeCAD does not currently have a native USD/USDZ export command. For Isaac workflows today, export URDF as the import artifact and carry the richer ForgeCAD simulation intent through `simready-manifest.json`. A future USD exporter should consume the same source-level simulation contract instead of re-inferring physics metadata from mesh geometry.
147
+
148
+ Every package includes `simready-manifest.json`. That manifest is the neutral handoff: model name, units, profile, bodies, joints, drives, controllers, contacts, and validation results.
149
+
150
+ ## What The Check Catches
151
+
152
+ `forgecad check simready` is source-level validation. It does not need Isaac Sim, OpenUSD, `pxr`, MuJoCo, or NVIDIA validators installed.
153
+
154
+ It fails when:
155
+
156
+ - the returned model has no `withSimulation(...)` contract
157
+ - a physical part is missing `Sim.body(...)`
158
+ - mass, density, friction, restitution, torque, speed, damping, or world pose values are non-finite or invalid
159
+ - a collider is missing for profiles that require explicit collision intent
160
+ - `Sim.collider.none(...)` has no reason
161
+ - a contact surface names a missing connector
162
+ - a controller names an unknown or wrong-type joint
163
+ - a robot profile has no `rootPart`, an unknown `rootPart`, or a disconnected joint graph
164
+
165
+ It intentionally treats joint-connected bodies as connected. A robot with separated wheels, arms, or gripper fingers should pass when the articulated graph is valid.
166
+
167
+ ## Where Control Code Lives
168
+
169
+ ForgeCAD owns geometry, physical metadata, joints, drives, controllers, manifests, and starter simulator packages. It should not own a single blessed balance controller, reward function, or RL library.
170
+
171
+ For a runnable training loop, export MJCF and use the generated package scripts. The Scout Cam Rover example includes `gym_env.py`, `train_balance.py`, `train_sb3.py`, `play_balance.py`, and `live_policy_viewer.py`; see [Simulation Workflow](../simulation-workflow.md) for the full MuJoCo and Isaac Lab path.