forgecad 0.10.0 → 0.10.2
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.
- package/dist/assets/{AdminPage-DwYHz72L.js → AdminPage-CHY6ZN-p.js} +1 -1
- package/dist/assets/{BenchmarkPage-a9_f-1US.js → BenchmarkPage-BcRT5iGN.js} +1 -1
- package/dist/assets/{BlogPage-DodHpvmf.js → BlogPage-BssBbnb-.js} +1 -1
- package/dist/assets/{DocsPage-B5LePEuj.js → DocsPage-DsvdiRNK.js} +33 -2
- package/dist/assets/{EditorApp-QXsAISLR.js → EditorApp-Bfd3jbtC.js} +185 -44
- package/dist/assets/{EmbedViewer-DdEHGUMU.js → EmbedViewer-D5t8WamV.js} +3 -3
- package/dist/assets/{LandingPageProofDriven-yhhOodbf.js → LandingPageProofDriven-DbN7o-Be.js} +1 -1
- package/dist/assets/{LegalPage-5RbKRGYK.js → LegalPage-DNGrrY0p.js} +1 -1
- package/dist/assets/{PricingPage-E3Rma7aV.js → PricingPage-Nczr3pRz.js} +1 -1
- package/dist/assets/{SettingsPage-BJZcM97j.js → SettingsPage-DZlyu4d4.js} +1 -1
- package/dist/assets/{app-DSYrDg0V.js → app-C9ct2hRD.js} +1752 -474
- package/dist/assets/{app-CE3sYcV7.css → app-CjsbDlb7.css} +143 -0
- package/dist/assets/{scalar-sampling-budget-o90NSNmF.js → backendInit-ymjonyQp.js} +85756 -78750
- package/dist/assets/cli/{render-ZMHR9HkV.js → render-B_0lQwKU.js} +71 -193
- package/dist/assets/{constructionHistoryWorker-AwMMWSxg.js → constructionHistoryWorker-CZ42Dksy.js} +8058 -1225
- package/dist/assets/{evalWorker-DbNs7Dkp.js → evalWorker-C2pm8LHP.js} +23037 -15821
- package/dist/assets/{forgecad_geometry-Dgceylq9.js → forgecad_geometry-BlMtqluF.js} +120 -1
- package/dist/assets/{forgecad_geometry_bg-dD4RNQF1.wasm → forgecad_geometry_bg-BllP_WiL.wasm} +0 -0
- package/dist/assets/{inspectWorker-CZsCFtQT.js → inspectWorker-D5T5VbfK.js} +31375 -32603
- package/dist/assets/{jointPose-DO6mnXn_.js → jointPose-4r8ed8_5.js} +1 -1
- package/dist/assets/{manifold-BU-tJwQh.js → manifold-5PP1eGLN.js} +1 -1
- package/dist/assets/{manifold-fy2MV7K1.js → manifold-C4r6B-XY.js} +2 -2
- package/dist/assets/{manifold-BGlQBBH9.js → manifold-DjBkyIc8.js} +1 -1
- package/dist/assets/{reportWorker-DO6hcQbh.js → reportWorker-CwenM7wB.js} +46620 -44936
- package/dist/cli/render.html +1 -1
- package/dist/docs/index.html +2 -2
- package/dist/docs-raw/CLI.md +43 -16
- package/dist/docs-raw/generated/assembly.md +71 -6
- package/dist/docs-raw/generated/concepts.md +17 -3
- package/dist/docs-raw/generated/core.md +10 -3
- package/dist/docs-raw/generated/output.md +14 -43
- package/dist/docs-raw/generated/runtime-names.md +4 -4
- package/dist/docs-raw/generated/sdf.md +2 -2
- package/dist/docs-raw/guides/simready-quickstart.md +173 -0
- package/dist/docs-raw/simulation-workflow.md +273 -0
- package/dist/index.html +2 -2
- package/dist/sitemap.xml +25 -13
- package/dist-cli/{check-compiler-JTVBITCR.js → check-compiler-SP7FAL7R.js} +1 -1
- package/dist-cli/{check-query-propagation-3FFLSMVN.js → check-query-propagation-BRLSHP22.js} +1 -1
- package/dist-cli/{chunk-OAN5T4XD.js → chunk-RQQ42YCP.js} +51209 -43456
- package/dist-cli/forgecad.js +5783 -1691
- package/dist-cli/{forgecad_geometry-QOQIIP53.js → forgecad_geometry-7TVSNVUB.js} +119 -0
- package/dist-cli/forgecad_geometry_bg.wasm +0 -0
- package/dist-skill/CONTEXT.md +107 -68
- package/dist-skill/docs/API/core/concepts.md +2 -2
- package/dist-skill/docs/CLI.md +43 -16
- package/dist-skill/docs/generated/assembly.md +67 -6
- package/dist-skill/docs/generated/core.md +10 -3
- package/dist-skill/docs/generated/output.md +14 -43
- package/dist-skill/docs/generated/runtime-names.md +4 -4
- package/dist-skill/docs/generated/sdf.md +2 -2
- package/examples/api/gyroid-voronoi-blend.forge.js +1 -1
- package/examples/api/organic-noise-sculpture.forge.js +1 -1
- package/examples/api/sdf-circular-array-knurling.forge.js +1 -1
- package/examples/api/{sdf-custom-raymarch.forge.js → sdf-custom-field-mesh-preview.forge.js} +3 -4
- package/examples/api/sdf-materialize-tree.forge.js +2 -2
- package/examples/api/sdf-plain-return.forge.js +3 -2
- package/examples/api/sdf-shapes.forge.js +2 -2
- package/examples/api/sdf-surface-basket-weave.forge.js +2 -2
- package/examples/generative/twisted-lattice-tower.forge.js +1 -1
- package/examples/generative/voronoi-lampshade.forge.js +1 -1
- package/examples/robotics/README.md +46 -0
- package/examples/robotics/scout-cam-rover-simready/README.md +119 -0
- package/examples/robotics/scout-cam-rover-simready/lib/dims.js +140 -0
- package/examples/robotics/scout-cam-rover-simready/main.forge.js +343 -0
- package/examples/robotics/scout-cam-rover-simready/parts/body.forge.js +304 -0
- package/examples/robotics/scout-cam-rover-simready/parts/chassis.forge.js +320 -0
- package/examples/robotics/scout-cam-rover-simready/parts/hardware.forge.js +21 -0
- package/examples/robotics/scout-cam-rover-simready/parts/turret.forge.js +70 -0
- package/examples/robotics/scout-cam-rover-simready/parts/wheel.forge.js +116 -0
- package/examples/robotics/simready-asset-crate.forge.js +79 -0
- package/examples/robotics/simready-diff-drive-rover.forge.js +141 -0
- package/examples/robotics/simready-parallel-gripper.forge.js +102 -0
- package/package.json +2 -2
- package/dist/assets/manifold-CzYf_iub.js +0 -3023
|
@@ -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;
|