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
@@ -51,7 +51,7 @@ const lensR = Param.number("Lens Radius", 6, { min: 3, max: 10, unit: "mm" });
51
51
  const lensGap = Param.number("Lens Gap", 3, { min: 1, max: 8, unit: "mm" });
52
52
 
53
53
  // Use Rectangle2D entity to derive lens positions from island bounds
54
- const camRect = Rectangle2D.fromCenterAndDimensions(point(camX, camY), camSize, camSize);
54
+ const camRect = Rectangle2D.fromCenterAndDimensions(new Point2D(camX, camY), camSize, camSize);
55
55
  const triRadius = camSize / 2 - lensR - lensGap;
56
56
 
57
57
  // Equilateral triangle: two lenses stacked on outer (left) edge,
@@ -0,0 +1,46 @@
1
+ # Robotics And SimReady Examples
2
+
3
+ These examples show the current ForgeCAD simulation contract: physical parts use `Sim.body(...)`, joints carry drive intent, and the returned assembly finishes with `withSimulation(...)`.
4
+
5
+ Run commands from the ForgeCAD repo root with the local CLI:
6
+
7
+ ```bash
8
+ node dist-cli/forgecad.js check simready examples/robotics/simready-diff-drive-rover.forge.js
9
+ node dist-cli/forgecad.js export mjcf examples/robotics/simready-diff-drive-rover.forge.js --output /tmp/forgecad-rover-mjcf
10
+ ```
11
+
12
+ ## Examples
13
+
14
+ | Example | File | What It Proves |
15
+ |---------|------|----------------|
16
+ | Diff-drive rover | `simready-diff-drive-rover.forge.js` | Robot-body profile, wheel contact surfaces, velocity drives, and diff-drive controller metadata |
17
+ | Parallel gripper | `simready-parallel-gripper.forge.js` | Prismatic joints, passive dynamics, and gripper contact-surface metadata |
18
+ | Asset crate | `simready-asset-crate.forge.js` | One-part asset assembly with physical material, collider, and grasp-ready contact metadata |
19
+ | Scout cam rover | `scout-cam-rover-simready/main.forge.js` | Larger copied robot with MJCF, SDF, URDF, MuJoCo smoke motion, starter Gym env, balance training, and push playback |
20
+
21
+ ## Minimum Workflow
22
+
23
+ ```bash
24
+ node dist-cli/forgecad.js run examples/robotics/simready-diff-drive-rover.forge.js
25
+ node dist-cli/forgecad.js check simready examples/robotics/simready-diff-drive-rover.forge.js
26
+ node dist-cli/forgecad.js export mjcf examples/robotics/simready-diff-drive-rover.forge.js --output /tmp/forgecad-rover-mjcf
27
+ ```
28
+
29
+ The MJCF export writes a runnable MuJoCo package with `scene.xml`, `manifest.json`, `simready-manifest.json`, and starter Python scripts.
30
+
31
+ ```bash
32
+ cd /tmp/forgecad-rover-mjcf
33
+ uv run --python 3.11 --with mujoco --with pillow python scripts/smoke_control.py --render-gif drive.gif
34
+ ```
35
+
36
+ The smaller examples are meant to be readable API references. The Scout example is the end-to-end simulator workflow.
37
+
38
+ ## Picking The Right Example
39
+
40
+ Use `simready-diff-drive-rover.forge.js` when you want the smallest robot that proves the full controller/export path.
41
+
42
+ Use `simready-parallel-gripper.forge.js` when you care about manipulators, fingertips, or grasp-readiness metadata.
43
+
44
+ Use `simready-asset-crate.forge.js` when the object is not a robot but still needs physical material, collision, and contact metadata.
45
+
46
+ Use `scout-cam-rover-simready/` when you want MuJoCo scripts, generated package structure, GIF output, and a realistic source-authored robot example.
@@ -0,0 +1,119 @@
1
+ # Scout Cam Rover SimReady Example
2
+
3
+ This is a copied, source-authored simulation version of the personal Scout Cam Rover model. The `.forge.js` file returns the normal assembly, and the same assembly carries the simulation contract through `withSimulation(...)`.
4
+
5
+ ## What Is Sim-Ready Here
6
+
7
+ - Every assembly part has `Sim.body(...)` metadata with mass, physical material, and collision intent.
8
+ - The TPU tires expose connector-based wheel contact surfaces through `Sim.contact.wheelSurface("tread")`.
9
+ - The two wheel joints have velocity drive intent through `Sim.drive.velocity(...)`.
10
+ - The root assembly uses `withSimulation(...)` with a `Sim.controller.diffDrive(...)` controller.
11
+ - SDF export emits a Gazebo Sim DiffDrive plugin that listens on `/model/scout_cam_rover/cmd_vel`.
12
+
13
+ ## Local Checks
14
+
15
+ Run from the ForgeCAD repo root:
16
+
17
+ ```bash
18
+ node dist-cli/forgecad.js run examples/robotics/scout-cam-rover-simready/main.forge.js
19
+ node dist-cli/forgecad.js check simready examples/robotics/scout-cam-rover-simready/main.forge.js
20
+ node dist-cli/forgecad.js export mjcf examples/robotics/scout-cam-rover-simready/main.forge.js --output /tmp/scout-cam-rover-mjcf
21
+ node dist-cli/forgecad.js export sdf examples/robotics/scout-cam-rover-simready/main.forge.js --output /tmp/scout-cam-rover-sdf
22
+ node dist-cli/forgecad.js export urdf examples/robotics/scout-cam-rover-simready/main.forge.js --output /tmp/scout-cam-rover-urdf
23
+ ```
24
+
25
+ The MJCF package is written to:
26
+
27
+ ```text
28
+ /tmp/scout-cam-rover-mjcf/scout_cam_rover.xml
29
+ /tmp/scout-cam-rover-mjcf/scene.xml
30
+ /tmp/scout-cam-rover-mjcf/scripts/gym_env.py
31
+ /tmp/scout-cam-rover-mjcf/scripts/smoke_control.py
32
+ /tmp/scout-cam-rover-mjcf/scripts/train_balance.py
33
+ /tmp/scout-cam-rover-mjcf/scripts/play_balance.py
34
+ /tmp/scout-cam-rover-mjcf/simready-manifest.json
35
+ ```
36
+
37
+ The SDF package is written to:
38
+
39
+ ```text
40
+ /tmp/scout-cam-rover-sdf/models/scout_cam_rover/model.sdf
41
+ /tmp/scout-cam-rover-sdf/simready-manifest.json
42
+ ```
43
+
44
+ The URDF package is written to:
45
+
46
+ ```text
47
+ /tmp/scout-cam-rover-urdf/scout_cam_rover.urdf
48
+ /tmp/scout-cam-rover-urdf/simready-manifest.json
49
+ ```
50
+
51
+ ## Moving It In A Simulator
52
+
53
+ The fastest local path is MuJoCo. The MJCF export includes a runnable scene and a package-local smoke controller that discovers the exported actuator names from `manifest.json`.
54
+
55
+ ```bash
56
+ cd /tmp/scout-cam-rover-mjcf
57
+ uv run --python 3.11 --with mujoco --with pillow python scripts/smoke_control.py --render-gif drive.gif
58
+ ```
59
+
60
+ That command compiles the generated `scene.xml`, commands the wheel velocity actuators, prints a motion summary, and writes `/tmp/scout-cam-rover-mjcf/drive.gif`.
61
+ The generated smoke controller calibrates the diff-drive motor signs inside MuJoCo before it commands semantic forward/turn/reverse motion, so mirrored wheel axes do not require hand-flipped script edits.
62
+
63
+ The MJCF package also includes a small Gymnasium control lab and starter balance trainer:
64
+
65
+ ```bash
66
+ cd /tmp/scout-cam-rover-mjcf
67
+ uv run --python 3.11 --with mujoco --with numpy --with gymnasium --with pillow python scripts/train_balance.py --render-gif balance.gif --fps 10
68
+ ```
69
+
70
+ That writes `policies/balance_linear.json` and `balance.gif`. The trainer is intentionally editable Python boilerplate: ForgeCAD wires the model, actuators, calibrated diff-drive semantics, and observation plumbing; the reward, policy class, curriculum, and RL library choice stay outside the CAD API.
71
+ The GIF duration is controlled by both the simulated `--seconds` value and the rendered `--fps`; 10 FPS maps to a clean 100 ms GIF frame delay in common viewers.
72
+
73
+ After training, push the policy with an external force pulse:
74
+
75
+ ```bash
76
+ cd /tmp/scout-cam-rover-mjcf
77
+ uv run --python 3.11 --with mujoco --with numpy --with gymnasium --with pillow python scripts/play_balance.py \
78
+ --policy policies/balance_linear.json \
79
+ --push-y 1 \
80
+ --push-at 2 \
81
+ --push-duration 0.15 \
82
+ --render-gif balance_push.gif \
83
+ --fps 10
84
+ ```
85
+
86
+ `play_balance.py` applies the pulse through MuJoCo's root-body external force array and prints the final tilt, survival time, frame count, and approximate GIF duration.
87
+
88
+ If your desktop supports MuJoCo's GLFW viewer, you can also open the generated scene:
89
+
90
+ ```bash
91
+ uv run --python 3.11 --with mujoco python -m mujoco.viewer --mjcf=/tmp/scout-cam-rover-mjcf/scene.xml
92
+ ```
93
+
94
+ Gazebo Sim is the SDF path, because ForgeCAD's SDF exporter already emits a DiffDrive plugin for the `Sim.controller.diffDrive(...)` metadata.
95
+
96
+ ```bash
97
+ export GZ_SIM_RESOURCE_PATH=/tmp/scout-cam-rover-sdf/models:${GZ_SIM_RESOURCE_PATH}
98
+ gz sim empty.sdf
99
+ gz service -s /world/empty/create \
100
+ --reqtype gz.msgs.EntityFactory \
101
+ --reptype gz.msgs.Boolean \
102
+ --timeout 300 \
103
+ --req 'sdf_filename: "/tmp/scout-cam-rover-sdf/models/scout_cam_rover/model.sdf", name: "scout_cam_rover"'
104
+ gz topic -t /model/scout_cam_rover/cmd_vel \
105
+ -m gz.msgs.Twist \
106
+ -p 'linear: {x: 0.15}, angular: {z: 0.0}'
107
+ ```
108
+
109
+ Those commands require Gazebo Sim to be installed on the host. This repo check does not execute Gazebo, Isaac Sim, MuJoCo, or any physics engine.
110
+
111
+ ## Important Physics Caveat
112
+
113
+ This robot is a two-wheel self-balancing rover. The exported DiffDrive controller can command the wheel joints, and the generated MuJoCo package includes a starter balance trainer, but the policy itself is simulator-side code. In a real physics project, the model usually needs one of these next:
114
+
115
+ - A quick demo stabilizer: add a caster, skid, or temporary roll/pitch constraint so basic drive motion is inspectable.
116
+ - A faithful robot controller: add IMU feedback and a balance PID/LQR loop that commands the wheel velocity or torque.
117
+ - A simulator integration layer: connect the exported model to ROS 2 control, Gazebo systems, Isaac controllers, PufferLib, MJX, or another MuJoCo actuator/controller stack.
118
+
119
+ Without that controller or stabilizer, the wheels can move but the body may fall over.
@@ -0,0 +1,140 @@
1
+ // Scout cam rover — shared dimension tree (pure data/math, no geometry).
2
+ // World frame: Z up, ground z=0, axle along X at z=AXLE_Z, front of robot faces -Y.
3
+ // All printed-part interfaces derive from these numbers; change here, not in parts.
4
+
5
+ function compute(p) {
6
+ const wall = p.wall; // printed shell wall thickness
7
+
8
+ // ---- drive line / wheels --------------------------------------------------
9
+ const tireOD = p.wheelOD; // outer diameter incl. TPU tire
10
+ const tireThk = 5; // radial
11
+ const tireW = 14;
12
+ const rimOD = tireOD - 2 * tireThk;
13
+ const rimID = rimOD - 12; // rim band 6 thick radially
14
+ const rimW = 14;
15
+ const axleZ = tireOD / 2;
16
+
17
+ // N20 micro gearmotor (purchased): body 12 wide x 10 tall, 24 long incl gearbox,
18
+ // D-shaft d3 x 9.5 with 2.5 flat.
19
+ const motor = {
20
+ w: 12, h: 10, len: 24, // gearbox front face = mating plane
21
+ shaftD: 3, shaftFlat: 2.5, shaftLen: 9.5,
22
+ noseBossD: 4, noseBossL: 1.2, // small ring around shaft exit
23
+ };
24
+
25
+ // ---- chassis tub ----------------------------------------------------------
26
+ const tub = {
27
+ w: 70, d: 72, h: 52, z0: axleZ - 26, // base z (world) — axle centered in tub height
28
+ cornerR: 6,
29
+ wall: wall,
30
+ floorT: 2.8,
31
+ postSq: 12, postInset: 6, // corner posts receiving the shell plugs
32
+ sideScrewZ: 47, // tub-local height of the 4 horizontal shell screws
33
+ };
34
+ tub.axleZl = axleZ - tub.z0; // axle height in tub-local Z
35
+
36
+ // motor pod (printed, black): hides shaft exit, wheel hub runs inside its bore
37
+ const pod = {
38
+ od: 34, len: 14, boreD: 24,
39
+ screwR: 14.0, screwN: 3, // M2 from inside tub through the wall into pod bosses
40
+ pilotD: 1.7, wallClearD: 1.9, pilotDepth: 6,
41
+ };
42
+
43
+ // wheel hub geometry (wheel-local: z=0 bore mouth face, +Z outward)
44
+ const hub = {
45
+ bossD: 20,
46
+ bossL: 14, // reaches from inside pod bore out to the spoke web
47
+ boreD: 3.2, boreFlat: 2.65, boreDepth: 9,
48
+ faceHoleN: 5, faceHoleD: 2.6, faceHoleR: 6,
49
+ };
50
+ const web = { t: 6, z0: hub.bossL }; // spoke web axial band (wheel-local)
51
+ const rim = { z0: web.z0 - (rimW - web.t) / 2 }; // rim band centered on web
52
+ // wheel-local Z of bore mouth maps to world |x| = tub.w/2 + 1.5 (mouth sits 1.5 outside the wall,
53
+ // inside the pod bore). Pod outer face at tub.w/2 + pod.len.
54
+ const wheelMouthX = tub.w / 2 + 1.5;
55
+
56
+ // battery: 2x18650 holder (purchased) mounted vertically on the body shell rear
57
+ // inner wall — high mass is correct for an inverted-pendulum self-balancer.
58
+ const batt = { w: 70, h: 42, d: 21, mountHoleSpan: 50, zc: 38 }; // zc = shell-local center
59
+
60
+ // service door in the tub front wall (exposes driver / IMU / wiring bay)
61
+ const door = {
62
+ openW: 44, openH: 22, zc: 19, // tub-local opening, center height
63
+ panelW: 54, panelH: 34, panelT: 2.0,
64
+ plugT: 2.2, slop: 0.3, // plug per-side clearance into opening
65
+ recess: 1.0, recessSlop: 0.4,
66
+ screwDX: 23.5, screwDZ: 13.5, // screw offsets from door center
67
+ pilotD: 1.7, screwHeadD: 3.8, bossD: 6, bossLen: 7,
68
+ };
69
+
70
+ // electronics in the tub bay (purchased modules on printed floor posts)
71
+ const boards = {
72
+ standoffH: 4,
73
+ drv: { w: 16, d: 18, t: 1.6, x: -12, y: -22.5 }, // DRV8833 dual H-bridge
74
+ buck: { w: 22, d: 17, t: 1.6, x: 7.6, y: -22.5 }, // MP1584 5V buck
75
+ mpu: { w: 16.5, d: 21.5, t: 1.6, x: 0, y: 3 }, // MPU6050 IMU (mid-bay)
76
+ };
77
+ const sw = { w: 13, h: 9, t: 9, plateW: 15, plateH: 11, plateT: 1.8, zc: 14 }; // rocker, rear wall
78
+
79
+ // ---- body shell (upper cube) ---------------------------------------------
80
+ const body = {
81
+ w: 84, d: 92, h: 88, cornerR: 13,
82
+ yOff: -4, // shell center sits 4mm toward the front of the tub center
83
+ topT: 3.0,
84
+ plugSq: 8, plugL: 9.7, socketSq: 8.3, socketDepth: 10, // 4 corner plugs into tub posts
85
+ plugInsetX: 10, plugInsetY: 10, // plug centers from tub inner corners
86
+ };
87
+ // neck + ring on top of shell (printed as part of shell)
88
+ const neck = { od: 44, h: 14, boreD: 36, ringOD: 50, ringH: 4 };
89
+
90
+ // camera (front face of shell, shell-local: centered XY, base z=0)
91
+ const cam = {
92
+ zc: 52, // lens center height above shell base
93
+ recessD: 44, recessDepth: 1.0,
94
+ holeD: 26,
95
+ screwR: 18.5, screwN: 4, pilotD: 1.7, screwStartDeg: 0,
96
+ bezelOD: 43.4, bezelID: 28, bezelT: 3.4, // proud of face by bezelT - recessDepth
97
+ cowlD: 27.8, cowlT: 3.0, apertureD: 9.5,
98
+ };
99
+
100
+ // ESP32-CAM (purchased): PCB 27 x 40.5 x 1.6, lens barrel d8 x 6.5 on front
101
+ const esp = { pcbW: 27, pcbH: 40.5, pcbT: 1.6, barrelD: 8, barrelL: 6.5, compT: 3.0 };
102
+
103
+ // side vents: skewed recessed panel + 5 vertical through slots, both side faces
104
+ const vent = {
105
+ n: 5, slotW: 4.5, slotL: 36, pitch: 11,
106
+ recessDepth: 0.8, skewDZ: 14, // rear edge of band this much higher than front
107
+ yc: 3, zc: 56, bandW: 58, bandH: 52, // band center (shell-local y/z) and size
108
+ };
109
+
110
+ // SG90 servo (purchased) hung under a bridge bar across the neck-bore opening.
111
+ // Servo-local frame: z=0 at the tab TOP plane (mates upward against the bosses);
112
+ // spline axis at local x=0.
113
+ const servo = {
114
+ bodyW: 23, bodyD: 12.4, bodyDown: 19, bodyUp: 4.5, // body extent below/above tab plane
115
+ bodyXc: -5.6, // body center offset so the spline sits at x=0
116
+ tabSpan: 32.4, tabT: 2.5, tabHoleXa: -13.75, tabHoleXb: 13.75, // relative to body center
117
+ towerH: 4.6, towerD: 11.8, splineD: 4.8, splineH: 3.2,
118
+ hornD: 20.6, hornT: 1.8, hornHubD: 7.4, // round horn pressed on the spline
119
+ tabPlaneZ: 78.2, // shell-local height of the tab plane
120
+ bossSq: 8, // printed bosses hanging from the bridge bar
121
+ };
122
+ const bridge = { w: 10, holeD: 13 }; // bar left across the neck-bore wall opening
123
+
124
+ // turret (printed black) rides the servo horn through the neck bore
125
+ const turret = {
126
+ baseD: 40, baseH: 5, lugN: 4, lugW: 5, lugT: 2, lugH: 2.4,
127
+ barrelD: 34, barrelH: 15, capD: 37, capH: 3,
128
+ colD: 14, colHubD: 25, colHubH: 5, colLen: 12.5, // column reaches the horn through the neck
129
+ socketD: 21.2, socketDepth: 2.0,
130
+ gapAboveNeck: 1.2, // running gap between turret base underside and neck ring top
131
+ };
132
+
133
+ return {
134
+ wall, axleZ, tireOD, tireThk, tireW, rimOD, rimID, rimW,
135
+ motor, tub, pod, hub, web, rim, wheelMouthX,
136
+ batt, door, boards, sw, body, neck, cam, esp, vent, servo, bridge, turret,
137
+ };
138
+ }
139
+
140
+ module.exports = { compute };
@@ -0,0 +1,343 @@
1
+ // Scout Cam Rover — a two-wheel self-balancing camera robot, replicated from a
2
+ // reference render. Fully home-buildable: FDM-printed teal PLA shells, black PLA
3
+ // pods/turret, TPU tires, and cheap purchased electronics (2x N20 gearmotors,
4
+ // ESP32-CAM behind the front bezel, SG90 pan servo under the top turret,
5
+ // DRV8833 + MPU6050 + MP1584, 2x18650 pack high in the body — an inverted
6
+ // pendulum balances better with mass up high; firmware: any ESP32 balance-bot
7
+ // sketch, e.g. B-robot style PID on the MPU6050).
8
+ //
9
+ // World frame: Z up, ground z=0, axle along X at z=55, front faces -Y.
10
+ // Motion joints: drive_left / drive_right (wheel spin, mirrored axes; MuJoCo
11
+ // calibration confirms forward roll is drive_left=+θ with drive_right=−θ)
12
+ // and turret_pan (SG90, ±90°).
13
+
14
+ const wallP = param('wall_thickness', 2.4, { min: 2.0, max: 3.0, step: 0.1, unit: 'mm' });
15
+ const wheelODP = param('wheel_od', 110, { min: 96, max: 130, step: 1, unit: 'mm' });
16
+ const auditP = Param.bool('Audit collisions (slow)', false);
17
+
18
+ const dimsMod = require('./lib/dims.js');
19
+ const D = dimsMod.compute({ wall: wallP, wheelOD: wheelODP });
20
+
21
+ const paramsDown = { wall_thickness: wallP, wheel_od: wheelODP };
22
+ const wheelMod = require('./parts/wheel.forge.js', paramsDown);
23
+ const chassisMod = require('./parts/chassis.forge.js', paramsDown);
24
+ const bodyMod = require('./parts/body.forge.js', paramsDown);
25
+ const turretMod = require('./parts/turret.forge.js', paramsDown);
26
+ const hwMod = require('./parts/hardware.forge.js');
27
+
28
+ const simMaterials = {
29
+ plaTeal: Sim.material('FDM PLA teal', {
30
+ densityKgM3: 1240,
31
+ staticFriction: 0.42,
32
+ dynamicFriction: 0.32,
33
+ restitution: 0.05,
34
+ }),
35
+ plaBlack: Sim.material('FDM PLA black', {
36
+ densityKgM3: 1240,
37
+ staticFriction: 0.4,
38
+ dynamicFriction: 0.3,
39
+ restitution: 0.05,
40
+ }),
41
+ tpu: Sim.material('TPU 95A tire', {
42
+ densityKgM3: 1200,
43
+ staticFriction: 1.0,
44
+ dynamicFriction: 0.85,
45
+ restitution: 0.18,
46
+ }),
47
+ steel: Sim.material('steel hardware', {
48
+ densityKgM3: 7850,
49
+ staticFriction: 0.35,
50
+ dynamicFriction: 0.25,
51
+ restitution: 0.04,
52
+ }),
53
+ electronics: Sim.material('electronics assembly', {
54
+ densityKgM3: 1500,
55
+ staticFriction: 0.45,
56
+ dynamicFriction: 0.32,
57
+ restitution: 0.04,
58
+ }),
59
+ battery: Sim.material('18650 lithium cell', {
60
+ densityKgM3: 2600,
61
+ staticFriction: 0.35,
62
+ dynamicFriction: 0.25,
63
+ restitution: 0.03,
64
+ }),
65
+ };
66
+
67
+ const simBody = (massKg, material, options = {}) => Sim.body({
68
+ massKg,
69
+ material,
70
+ collider: options.collider ?? Sim.collider.convexHull(),
71
+ contacts: options.contacts,
72
+ });
73
+
74
+ const wheelContact = { tread: Sim.contact.wheelSurface('tread') };
75
+ const wheelRadiusMm = D.tireOD / 2;
76
+ const wheelSeparationMm = 2 * (D.wheelMouthX + D.rim.z0 + D.tireW / 2);
77
+
78
+ // ---- build all parts in their local frames ----------------------------------
79
+ const tub = chassisMod.make.buildTub(D);
80
+ const door = chassisMod.make.buildDoor(D);
81
+ const podL = chassisMod.make.buildPod(D);
82
+ const podR = chassisMod.make.buildPod(D);
83
+ const motorL = chassisMod.make.buildMotor(D);
84
+ const motorR = chassisMod.make.buildMotor(D);
85
+ const mods = chassisMod.make.buildBoards(D);
86
+ const rocker = chassisMod.make.buildSwitch(D);
87
+
88
+ const shell = bodyMod.make.buildShell(D);
89
+ const bezel = bodyMod.make.buildBezel(D);
90
+ const cowl = bodyMod.make.buildCowl(D);
91
+ const espParts = bodyMod.make.buildEsp(D);
92
+ const servoParts = bodyMod.make.buildServo(D);
93
+ const battHolder = bodyMod.make.buildBatteryHolder(D);
94
+ const cellA = bodyMod.make.buildCell(D, -1);
95
+ const cellB = bodyMod.make.buildCell(D, 1);
96
+
97
+ const wheelL = wheelMod.make.buildWheel(D);
98
+ const wheelR = wheelMod.make.buildWheel(D);
99
+ const turret = turretMod.make.buildTurret(D);
100
+
101
+ // ---- assembly ----------------------------------------------------------------
102
+ let rover = assembly('Scout Cam Rover')
103
+ .addPart('Chassis Tub', tub, {
104
+ transform: Transform.translation(0, 0, D.tub.z0),
105
+ metadata: { material: 'PLA teal', process: 'FDM', qty: 1 },
106
+ sim: simBody(0.11, simMaterials.plaTeal),
107
+ })
108
+ .addPart('Service Door', door, { metadata: { material: 'PLA teal', process: 'FDM' }, sim: simBody(0.018, simMaterials.plaTeal) })
109
+ .addPart('Motor Pod L', podL, { metadata: { material: 'PLA black', process: 'FDM' }, sim: simBody(0.022, simMaterials.plaBlack) })
110
+ .addPart('Motor Pod R', podR, { metadata: { material: 'PLA black', process: 'FDM' }, sim: simBody(0.022, simMaterials.plaBlack) })
111
+ .addPart('Gearmotor L', motorL, { metadata: { material: 'purchased', notes: 'N20 6V 150RPM' }, sim: simBody(0.012, simMaterials.steel, { collider: Sim.collider.boundingBox() }) })
112
+ .addPart('Gearmotor R', motorR, { metadata: { material: 'purchased', notes: 'N20 6V 150RPM' }, sim: simBody(0.012, simMaterials.steel, { collider: Sim.collider.boundingBox() }) })
113
+ .addPart('DRV8833', mods.drv, { metadata: { material: 'purchased' }, sim: simBody(0.004, simMaterials.electronics, { collider: Sim.collider.boundingBox() }) })
114
+ .addPart('MP1584 Buck', mods.buck, { metadata: { material: 'purchased' }, sim: simBody(0.006, simMaterials.electronics, { collider: Sim.collider.boundingBox() }) })
115
+ .addPart('MPU6050', mods.mpu, { metadata: { material: 'purchased' }, sim: simBody(0.003, simMaterials.electronics, { collider: Sim.collider.boundingBox() }) })
116
+ .addPart('Rocker Switch', rocker, { metadata: { material: 'purchased' }, sim: simBody(0.006, simMaterials.electronics, { collider: Sim.collider.boundingBox() }) })
117
+ .addPart('Body Shell', shell, { metadata: { material: 'PLA teal', process: 'FDM' }, sim: simBody(0.16, simMaterials.plaTeal) })
118
+ .addPart('Camera Bezel', bezel, { metadata: { material: 'PLA teal', process: 'FDM' }, sim: simBody(0.012, simMaterials.plaTeal) })
119
+ .addPart('Lens Cowl', cowl, { metadata: { material: 'PLA black', process: 'FDM' }, sim: simBody(0.008, simMaterials.plaBlack) })
120
+ .addPart('ESP32-CAM Board', espParts.board, { metadata: { material: 'purchased' }, sim: simBody(0.008, simMaterials.electronics, { collider: Sim.collider.boundingBox() }) })
121
+ .addPart('ESP32-CAM Optics', espParts.optics, { metadata: { material: 'purchased' }, sim: simBody(0.004, simMaterials.electronics, { collider: Sim.collider.boundingBox() }) })
122
+ .addPart('Servo Body', servoParts.body, { metadata: { material: 'purchased', notes: 'SG90 9g' }, sim: simBody(0.009, simMaterials.electronics, { collider: Sim.collider.boundingBox() }) })
123
+ .addPart('Servo Horn', servoParts.horn, { metadata: { material: 'purchased', notes: 'SG90 round horn' }, sim: simBody(0.002, simMaterials.plaBlack) })
124
+ .addPart('Battery Holder', battHolder, { metadata: { material: 'purchased', notes: '2x18650 holder' }, sim: simBody(0.025, simMaterials.electronics, { collider: Sim.collider.boundingBox() }) })
125
+ .addPart('18650 Cell A', cellA, { metadata: { material: 'purchased' }, sim: simBody(0.045, simMaterials.battery, { collider: Sim.collider.boundingBox() }) })
126
+ .addPart('18650 Cell B', cellB, { metadata: { material: 'purchased' }, sim: simBody(0.045, simMaterials.battery, { collider: Sim.collider.boundingBox() }) })
127
+ .addPart('Wheel Rim L', wheelL.rim, { metadata: { material: 'PLA teal', process: 'FDM' }, sim: simBody(0.028, simMaterials.plaTeal) })
128
+ .addPart('Tire L', wheelL.tire, { metadata: { material: 'TPU 95A', process: 'FDM' }, sim: simBody(0.038, simMaterials.tpu, { contacts: wheelContact }) })
129
+ .addPart('Drive Shaft L', wheelL.shaft, { metadata: { material: 'purchased', notes: 'N20 output shaft' }, sim: simBody(0.002, simMaterials.steel, { collider: Sim.collider.boundingBox() }) })
130
+ .addPart('Wheel Rim R', wheelR.rim, { metadata: { material: 'PLA teal', process: 'FDM' }, sim: simBody(0.028, simMaterials.plaTeal) })
131
+ .addPart('Tire R', wheelR.tire, { metadata: { material: 'TPU 95A', process: 'FDM' }, sim: simBody(0.038, simMaterials.tpu, { contacts: wheelContact }) })
132
+ .addPart('Drive Shaft R', wheelR.shaft, { metadata: { material: 'purchased', notes: 'N20 output shaft' }, sim: simBody(0.002, simMaterials.steel, { collider: Sim.collider.boundingBox() }) })
133
+ .addPart('Turret', turret, { metadata: { material: 'PLA black', process: 'FDM' }, sim: simBody(0.024, simMaterials.plaBlack) });
134
+
135
+ // individual screws (separate purchased parts, one bearing connector each)
136
+ const screwSpecs = [];
137
+ for (let i = 0; i < 4; i++) screwSpecs.push([`Door Screw ${i + 1}`, 8, 'Chassis Tub', `door_screw_${i}`]);
138
+ for (const sd of ['l', 'r']) for (let i = 0; i < 3; i++) {
139
+ screwSpecs.push([`Pod Screw ${sd.toUpperCase()}${i + 1}`, 8, 'Chassis Tub', `pod_screw_${sd}_${i}`]);
140
+ }
141
+ for (let i = 0; i < 4; i++) screwSpecs.push([`Shell Screw ${i + 1}`, 8, 'Chassis Tub', `shell_screw_${i}`]);
142
+ for (let i = 0; i < 4; i++) screwSpecs.push([`Bezel Screw ${i + 1}`, 6, 'Body Shell', `bezel_screw_${i}`]);
143
+ for (const [name, len, parent, conn] of screwSpecs) {
144
+ rover = rover.addPart(name, hwMod.make.screwM2(len), {
145
+ metadata: { material: 'steel', notes: `M2x${len}` },
146
+ sim: simBody(len >= 8 ? 0.0005 : 0.00035, simMaterials.steel, { collider: Sim.collider.boundingBox() }),
147
+ });
148
+ rover = rover.connect(`${parent}.${conn}`, `${name}.seat`, { as: `${conn}_joint`, type: 'fixed' });
149
+ }
150
+
151
+ // fixed mounts (each interface is a connector pair)
152
+ rover = rover
153
+ .connect('Chassis Tub.door_frame', 'Service Door.back', { as: 'door_mount', type: 'fixed' })
154
+ .connect('Chassis Tub.pod_seat_l', 'Motor Pod L.flange', { as: 'pod_l_mount', type: 'fixed' })
155
+ .connect('Chassis Tub.pod_seat_r', 'Motor Pod R.flange', { as: 'pod_r_mount', type: 'fixed' })
156
+ .connect('Chassis Tub.motor_seat_l', 'Gearmotor L.nose', { as: 'motor_l_mount', type: 'fixed' })
157
+ .connect('Chassis Tub.motor_seat_r', 'Gearmotor R.nose', { as: 'motor_r_mount', type: 'fixed' })
158
+ .connect('Chassis Tub.boards_drv', 'DRV8833.mount', { as: 'drv_mount', type: 'fixed' })
159
+ .connect('Chassis Tub.boards_buck', 'MP1584 Buck.mount', { as: 'buck_mount', type: 'fixed' })
160
+ .connect('Chassis Tub.boards_mpu', 'MPU6050.mount', { as: 'mpu_mount', type: 'fixed' })
161
+ .connect('Chassis Tub.switch_seat', 'Rocker Switch.mount', { as: 'switch_mount', type: 'fixed' })
162
+ .connect('Chassis Tub.deck', 'Body Shell.base', { as: 'shell_mount', type: 'fixed' })
163
+ .connect('Body Shell.cam_recess', 'Camera Bezel.back', { as: 'bezel_mount', type: 'fixed' })
164
+ .connect('Body Shell.cam_cowl', 'Lens Cowl.back', { as: 'cowl_mount', type: 'fixed' })
165
+ .connect('Body Shell.esp_cradle', 'ESP32-CAM Board.face', { as: 'esp_mount', type: 'fixed' })
166
+ .connect('Body Shell.esp_optics', 'ESP32-CAM Optics.face', { as: 'esp_optics_mount', type: 'fixed' })
167
+ .connect('Body Shell.servo_mount', 'Servo Body.tabs', { as: 'servo_mount', type: 'fixed' })
168
+ .connect('Body Shell.servo_horn_mount', 'Servo Horn.tabs', { as: 'servo_horn_mount', type: 'fixed' })
169
+ .connect('Body Shell.batt_mount', 'Battery Holder.back', { as: 'batt_mount', type: 'fixed' })
170
+ .connect('Body Shell.cell_a', '18650 Cell A.back', { as: 'cell_a_mount', type: 'fixed' })
171
+ .connect('Body Shell.cell_b', '18650 Cell B.back', { as: 'cell_b_mount', type: 'fixed' });
172
+
173
+ // motion joints; tires and shafts are fixed children of the spinning rims
174
+ rover = rover
175
+ .connect('Gearmotor L.shaft_tip', 'Wheel Rim L.bore', {
176
+ as: 'drive_left', min: -720, max: 720, default: 0, unit: '°',
177
+ drive: Sim.drive.velocity({ maxTorqueNm: 0.08, maxSpeedRpm: 150, damping: 0.015, friction: 0.006 }),
178
+ })
179
+ .connect('Gearmotor R.shaft_tip', 'Wheel Rim R.bore', {
180
+ as: 'drive_right', min: -720, max: 720, default: 0, unit: '°',
181
+ drive: Sim.drive.velocity({ maxTorqueNm: 0.08, maxSpeedRpm: 150, damping: 0.015, friction: 0.006 }),
182
+ })
183
+ .connect('Wheel Rim L.tire_seat', 'Tire L.seat', { as: 'tire_l_fit', type: 'fixed' })
184
+ .connect('Wheel Rim L.shaft_seat', 'Drive Shaft L.seat', { as: 'shaft_l_fit', type: 'fixed' })
185
+ .connect('Wheel Rim R.tire_seat', 'Tire R.seat', { as: 'tire_r_fit', type: 'fixed' })
186
+ .connect('Wheel Rim R.shaft_seat', 'Drive Shaft R.seat', { as: 'shaft_r_fit', type: 'fixed' })
187
+ .connect('Servo Horn.spline', 'Turret.hub', {
188
+ as: 'turret_pan', min: -90, max: 90, default: 0, unit: '°',
189
+ drive: Sim.drive.velocity({ maxTorqueNm: 0.16, maxSpeedRpm: 60, damping: 0.04, friction: 0.01 }),
190
+ });
191
+
192
+ rover = rover.withSimulation({
193
+ rootPart: 'Chassis Tub',
194
+ profile: Sim.profile.robotBodyRunnable(),
195
+ controllers: [
196
+ Sim.controller.diffDrive({
197
+ leftJoints: ['drive_left'],
198
+ rightJoints: ['drive_right'],
199
+ wheelRadiusMm,
200
+ wheelSeparationMm,
201
+ }),
202
+ ],
203
+ });
204
+
205
+ // ---- animations ---------------------------------------------------------------
206
+ // forward roll = drive_left +θ, drive_right −θ (mirrored revolute axes)
207
+ rover = rover
208
+ .addAnimation('Drive & scan', {
209
+ duration: 9, loop: true, continuous: true, default: true,
210
+ keyframes: [
211
+ { values: { drive_left: 0, drive_right: 0, turret_pan: 0 } },
212
+ { values: { drive_left: 240, drive_right: -240, turret_pan: 55 } },
213
+ { values: { drive_left: 480, drive_right: -480, turret_pan: -55 } },
214
+ { values: { drive_left: 720, drive_right: -720, turret_pan: 0 } },
215
+ ],
216
+ })
217
+ .addAnimation('Spin in place', {
218
+ duration: 6, loop: true, continuous: true,
219
+ keyframes: [
220
+ { values: { drive_left: 0, drive_right: 0, turret_pan: 0 } },
221
+ { values: { drive_left: -360, drive_right: -360, turret_pan: 45 } },
222
+ { values: { drive_left: -720, drive_right: -720, turret_pan: -45 } },
223
+ { values: { drive_left: -720, drive_right: -720, turret_pan: 0 } },
224
+ ],
225
+ })
226
+ .addAnimation('Camera sweep', {
227
+ duration: 4, loop: true, continuous: true,
228
+ keyframes: [
229
+ { values: { turret_pan: 0 } },
230
+ { values: { turret_pan: 88 } },
231
+ { values: { turret_pan: -88 } },
232
+ { values: { turret_pan: 0 } },
233
+ ],
234
+ });
235
+
236
+ // ---- pose verification ---------------------------------------------------------
237
+ const poses = [
238
+ { drive_left: 0, drive_right: 0, turret_pan: 0 },
239
+ { drive_left: -45, drive_right: 45, turret_pan: 60 },
240
+ { drive_left: -137, drive_right: 290, turret_pan: -90 },
241
+ { drive_left: 720, drive_right: -720, turret_pan: 90 },
242
+ ];
243
+ for (const pose of poses) {
244
+ const s = rover.solve(pose);
245
+ const tag = `dl=${pose.drive_left} dr=${pose.drive_right} pan=${pose.turret_pan}`;
246
+ verify.that(`solve converges (${tag})`, () => s.warnings().length === 0,
247
+ `solver warnings: ${s.warnings().join('; ')}`);
248
+ verify.clearanceBetween(`wheel R clears pod R (${tag})`, s.getPart('Wheel Rim R'), s.getPart('Motor Pod R'), 0.4, 80);
249
+ verify.clearanceBetween(`wheel L clears tub (${tag})`, s.getPart('Wheel Rim L'), s.getPart('Chassis Tub'), 0.4, 80);
250
+ verify.clearanceBetween(`tire R clears body shell (${tag})`, s.getPart('Tire R'), s.getPart('Body Shell'), 2.0, 80);
251
+ verify.clearanceBetween(`turret clears shell neck (${tag})`, s.getPart('Turret'), s.getPart('Body Shell'), 0.4, 80);
252
+ verify.clearanceBetween(`turret clears servo body (${tag})`, s.getPart('Turret'), s.getPart('Servo Body'), 0.4, 80);
253
+ }
254
+
255
+ const sDefault = rover.solve({});
256
+ const gDefault = sDefault.toGroup();
257
+ verify.connectorDistance('wheel L bore on motor L shaft', gDefault, 'Wheel Rim L.bore', 'Gearmotor L.shaft_tip', 0, 0.01);
258
+ verify.connectorDistance('wheel R bore on motor R shaft', gDefault, 'Wheel Rim R.bore', 'Gearmotor R.shaft_tip', 0, 0.01);
259
+ verify.connectorDistance('turret hub on servo spline', gDefault, 'Turret.hub', 'Servo Horn.spline', 0, 0.01);
260
+ verify.connectorDistance('shell base on tub deck', gDefault, 'Body Shell.base', 'Chassis Tub.deck', 0, 0.01);
261
+
262
+ // ground contact: tire bottoms at z=0
263
+ const wheelBB = sDefault.getPart('Tire L').boundingBox();
264
+ verify.equal('left tire touches the ground', wheelBB.min[2], 0, 0.05);
265
+ verify.equal('wheel OD as designed', wheelBB.max[2] - wheelBB.min[2], wheelODP, 0.1);
266
+
267
+ // full pairwise audit (slow) — run with --param "Audit collisions (slow)=1"
268
+ if (auditP) {
269
+ for (const pose of poses) {
270
+ const s = rover.solve(pose);
271
+ const findings = s.collisionReport({ minOverlapVolume: 0.5 });
272
+ verify.that(`no collisions at dl=${pose.drive_left} pan=${pose.turret_pan}`,
273
+ () => findings.length === 0,
274
+ findings.map(f => `${f.partA ?? f.a} vs ${f.partB ?? f.b}: ${JSON.stringify(f)}`).join(' | ').slice(0, 400));
275
+ }
276
+ }
277
+
278
+ verify.physicalComponentCount('one jointed assembly', 1);
279
+
280
+ // ---- BOM ------------------------------------------------------------------------
281
+ bom(1, 'chassis tub', { material: 'PLA teal', process: 'FDM printed' });
282
+ bom(1, 'service door', { material: 'PLA teal', process: 'FDM printed' });
283
+ bom(1, 'body shell', { material: 'PLA teal', process: 'FDM printed' });
284
+ bom(1, 'camera bezel', { material: 'PLA teal', process: 'FDM printed' });
285
+ bom(2, 'wheel rim', { material: 'PLA teal', process: 'FDM printed' });
286
+ bom(2, 'motor pod', { material: 'PLA black', process: 'FDM printed' });
287
+ bom(1, 'lens cowl', { material: 'PLA black', process: 'FDM printed' });
288
+ bom(1, 'pan turret', { material: 'PLA black', process: 'FDM printed' });
289
+ bom(2, 'tire', { material: 'TPU 95A gray', process: 'FDM printed', notes: 'print ID 0.5mm under rim OD for stretch fit' });
290
+ bom(2, 'N20 micro gearmotor 6V 150RPM, 3mm D-shaft', { notes: '~$3 ea' });
291
+ bom(1, 'ESP32-CAM (AI-Thinker) + OV2640', { notes: 'camera + wifi + controller, ~$9' });
292
+ bom(1, 'SG90 micro servo + round horn', { notes: 'turret pan, ~$2.5' });
293
+ bom(1, 'DRV8833 dual H-bridge module', { notes: '~$2' });
294
+ bom(1, 'MPU6050 IMU module', { notes: 'balance sensing, ~$2' });
295
+ bom(1, 'MP1584EN 5V buck module', { notes: '~$1.5' });
296
+ bom(1, 'KCD11 mini rocker switch', { notes: '~$0.5' });
297
+ bom(1, '2x18650 battery holder (wire leads)', { notes: '~$1.5' });
298
+ bom(2, '18650 Li-ion cell, flat top', { notes: 'charge externally' });
299
+ bom(14, 'M2x8 self-tapping screw', { notes: 'door 4, pods 6, shell 4' });
300
+ bom(4, 'M2x6 self-tapping screw, bezel set');
301
+ bom(2, 'M2x6 screw, servo tabs' );
302
+ bom(2, 'M2x6 screw, battery holder');
303
+ bom(1, 'M2x10 screw, turret-to-horn retention');
304
+ bom(1, 'hookup wire + heat-shrink set', { notes: 'motor, battery, servo runs' });
305
+
306
+ // ---- dimension annotations -------------------------------------------------------
307
+ const trackOuter = D.wheelMouthX + D.rim.z0 + D.tireW;
308
+ dim([-trackOuter, 0, D.axleZ], [trackOuter, 0, D.axleZ], { label: 'Overall width', offset: 30 });
309
+ dim([0, -D.tireOD / 2, 0], [0, -D.tireOD / 2, D.tireOD], { label: 'Wheel OD', offset: 20 });
310
+ dim([0, 60, 0], [0, 60, D.tub.z0 + D.tub.h + D.body.h + D.neck.h + D.turret.baseH + D.turret.barrelH + D.turret.capH + D.turret.gapAboveNeck],
311
+ { label: 'Overall height', offset: 24 });
312
+ dim([-D.body.w / 2, -4, D.tub.z0 + D.tub.h + 2], [D.body.w / 2, -4, D.tub.z0 + D.tub.h + 2], { label: 'Body width', offset: 16 });
313
+ dim([-D.tub.w / 2, 0, D.tub.z0], [D.tub.w / 2, 0, D.tub.z0], { label: 'Tub width', offset: 12 });
314
+ dim([0, 0, 0], [0, 0, D.axleZ], { label: 'Axle height', offset: 18 });
315
+
316
+ // ---- scene -----------------------------------------------------------------------
317
+ scene({
318
+ background: { top: '#c3ccd7', bottom: '#566474' },
319
+ camera: { position: [320, -380, 300], target: [0, 0, 100], fov: 38 },
320
+ environment: { preset: 'studio', intensity: 0.2, background: false },
321
+ lights: [
322
+ { type: 'ambient', color: '#efe7dc', intensity: 0.16 },
323
+ { type: 'directional', position: [260, -320, 420], color: '#ffe2bf', intensity: 2.8, castShadow: true },
324
+ { type: 'directional', position: [-260, 210, 220], color: '#d4e6fb', intensity: 0.85 },
325
+ { type: 'hemisphere', skyColor: '#c7d3df', groundColor: '#495463', intensity: 0.15 },
326
+ ],
327
+ ground: { visible: true, color: '#3c4654', height: 0, receiveShadow: true },
328
+ postProcessing: {
329
+ bloom: { intensity: 0.04, threshold: 0.94, radius: 0.28 },
330
+ vignette: { darkness: 0.4, offset: 0.32 },
331
+ toneMappingExposure: 1.1,
332
+ },
333
+ views: {
334
+ hero: { camera: { position: [320, -380, 300], target: [0, 0, 100], up: [0, 0, 1], fov: 38 } },
335
+ front: { camera: { position: [0, -520, 130], target: [0, 0, 105], up: [0, 0, 1], fov: 32 } },
336
+ side: { camera: { position: [520, 0, 130], target: [0, 0, 105], up: [0, 0, 1], fov: 32 } },
337
+ rear: { camera: { position: [-300, 380, 280], target: [0, 0, 100], up: [0, 0, 1], fov: 38 } },
338
+ top: { camera: { position: [10, -40, 560], target: [0, 0, 80], up: [0, 1, 0], fov: 32 } },
339
+ underside: { camera: { position: [180, -260, -180], target: [0, 0, 70], up: [0, 0, 1], fov: 40 } },
340
+ },
341
+ });
342
+
343
+ return rover;