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,304 @@
|
|
|
1
|
+
// Upper body of the scout cam rover: printed shell (rounded cube + neck + vents
|
|
2
|
+
// + camera recess + corner plugs + servo bridge), camera bezel + black lens cowl,
|
|
3
|
+
// ESP32-CAM module, SG90 pan servo, and the 2x18650 battery holder.
|
|
4
|
+
// Shell-local frame: centered on XY, base z=0 (sits on the tub deck), front = -Y.
|
|
5
|
+
|
|
6
|
+
const wallP = param('wall_thickness', 2.4, { min: 2.0, max: 3.0, step: 0.1, unit: 'mm' });
|
|
7
|
+
const wheelODP = param('wheel_od', 110, { min: 96, max: 130, step: 1, unit: 'mm' });
|
|
8
|
+
|
|
9
|
+
const dimsMod = require('../lib/dims.js');
|
|
10
|
+
|
|
11
|
+
const COL_TEAL = '#2fb3b8';
|
|
12
|
+
const COL_BLACK = '#1f2023';
|
|
13
|
+
const COL_SCREW = '#3a3d42';
|
|
14
|
+
|
|
15
|
+
function tealMat(s) { return s.color(COL_TEAL).material({ metalness: 0.05, roughness: 0.45 }); }
|
|
16
|
+
|
|
17
|
+
// ----------------------------------------------------------------- shell -----
|
|
18
|
+
function buildShell(D) {
|
|
19
|
+
const B = D.body, N = D.neck, C = D.cam, V = D.vent, S = D.servo, BR = D.bridge;
|
|
20
|
+
const hw = B.w / 2, hd = B.d / 2;
|
|
21
|
+
const w = D.wall;
|
|
22
|
+
|
|
23
|
+
let sh = roundedRect(B.w, B.d, B.cornerR).extrude(B.h).shell(w, { openFaces: ['bottom'] });
|
|
24
|
+
|
|
25
|
+
// ---- neck + ring on top (one printed piece with the shell)
|
|
26
|
+
let neckSolid = cylinder(N.h + 0.3, N.od / 2).translate(0, 0, B.h - 0.3)
|
|
27
|
+
.add(cylinder(N.ringH, N.ringOD / 2).translate(0, 0, B.h + N.h - N.ringH));
|
|
28
|
+
neckSolid = neckSolid.subtract(cylinder(N.h + 1, N.boreD / 2).translate(0, 0, B.h - 0.1));
|
|
29
|
+
sh = sh.add(neckSolid);
|
|
30
|
+
// top-wall opening under the neck: bore minus a bridge bar (servo hangs from it)
|
|
31
|
+
const topHole = circle2d(N.boreD / 2 - 2).subtract(rect(N.boreD + 2, BR.w))
|
|
32
|
+
.add(circle2d(BR.holeD / 2)); // central pass-through for the servo tower/spline
|
|
33
|
+
sh = sh.subtract(topHole.extrude(w + 2).translate(0, 0, B.h - w - 1));
|
|
34
|
+
|
|
35
|
+
// ---- camera recess, through-hole, bezel pilots (front face, -Y) — one batched cut
|
|
36
|
+
const fw = -hd; // front outer face
|
|
37
|
+
const camCuts = [
|
|
38
|
+
cylinder(C.recessDepth + 0.1, C.recessD / 2).pointAlong([0, -1, 0]).translate(0, fw + C.recessDepth, C.zc),
|
|
39
|
+
cylinder(w + 2, C.holeD / 2).pointAlong([0, -1, 0]).translate(0, fw + w + 1, C.zc),
|
|
40
|
+
];
|
|
41
|
+
for (const pt of circularLayout(C.screwN, C.screwR, { startDeg: C.screwStartDeg })) {
|
|
42
|
+
camCuts.push(cylinder(6.2, C.pilotD / 2).pointAlong([0, 1, 0])
|
|
43
|
+
.translate(pt.x, fw + C.recessDepth - 0.1, C.zc + pt.y));
|
|
44
|
+
}
|
|
45
|
+
sh = sh.subtract(...camCuts);
|
|
46
|
+
|
|
47
|
+
// ---- ESP32-CAM slide rails on the inner front wall
|
|
48
|
+
const innerF = fw + w; // inner front wall face
|
|
49
|
+
const pcbFrontY = innerF + 2.7; // PCB front face plane (barrel tip flush with cowl face)
|
|
50
|
+
for (const sx of [-1, 1]) {
|
|
51
|
+
let rail = box(3.4, 6.4, 38).translate(sx * 15.3, innerF + 6.4 / 2 - 0.3, 24);
|
|
52
|
+
rail = rail.subtract(
|
|
53
|
+
box(2.2, 2.0, 40).translate(sx * (13.4 + 1.1), pcbFrontY + 1.0, 28)
|
|
54
|
+
);
|
|
55
|
+
sh = sh.add(rail);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ---- side vent bands: skewed recessed panel + 5 through slots (both faces)
|
|
59
|
+
const slope = V.skewDZ / V.bandW;
|
|
60
|
+
const bandPts = [
|
|
61
|
+
[V.yc - V.bandW / 2, V.zc - V.bandH / 2],
|
|
62
|
+
[V.yc + V.bandW / 2, V.zc - V.bandH / 2 + V.skewDZ],
|
|
63
|
+
[V.yc + V.bandW / 2, V.zc + V.bandH / 2],
|
|
64
|
+
[V.yc - V.bandW / 2, V.zc + V.bandH / 2 - V.skewDZ],
|
|
65
|
+
];
|
|
66
|
+
// rotate([1,1,1],120) maps sketch X -> world Y, sketch Y -> world Z, extrusion -> world X
|
|
67
|
+
const recessCut = polygon(bandPts).extrude(V.recessDepth + 0.4)
|
|
68
|
+
.rotate([1, 1, 1], 120).translate(hw - V.recessDepth, 0, 0);
|
|
69
|
+
let slotSk = null;
|
|
70
|
+
for (let i = 0; i < V.n; i++) {
|
|
71
|
+
const yi = V.yc + (i - (V.n - 1) / 2) * V.pitch;
|
|
72
|
+
const zi = V.zc + (yi - V.yc) * slope;
|
|
73
|
+
const one = slot(V.slotL, V.slotW).rotate(90).translate(yi, zi);
|
|
74
|
+
slotSk = slotSk ? slotSk.add(one) : one;
|
|
75
|
+
}
|
|
76
|
+
const slotCut = slotSk.extrude(w + 3).rotate([1, 1, 1], 120).translate(hw - w - 1.5, 0, 0);
|
|
77
|
+
sh = sh.subtract(recessCut, slotCut,
|
|
78
|
+
recessCut.mirrorThrough([0, 0, 0], [1, 0, 0]), slotCut.mirrorThrough([0, 0, 0], [1, 0, 0]));
|
|
79
|
+
|
|
80
|
+
// ---- corner plugs (drop into the tub posts) + bridge ribs to the side walls
|
|
81
|
+
// tub posts are at tub-centered (±26.6, ±27.6); shell is offset yOff toward the front
|
|
82
|
+
const T = D.tub;
|
|
83
|
+
const pcx = T.w / 2 - T.wall - T.postInset, pcy = T.d / 2 - T.wall - T.postInset;
|
|
84
|
+
for (const sx of [-1, 1]) for (const sy of [-1, 1]) {
|
|
85
|
+
const px = sx * pcx, py = sy * pcy - B.yOff;
|
|
86
|
+
let plug = box(B.plugSq, B.plugSq, B.plugL + 6).translate(px, py, -B.plugL);
|
|
87
|
+
// pilot for the horizontal shell screw (world z = tub.sideScrewZ)
|
|
88
|
+
const pz = T.sideScrewZ - T.h; // shell-local
|
|
89
|
+
plug = plug.subtract(cylinder(5.7, 0.85).pointAlong([-sx, 0, 0])
|
|
90
|
+
.translate(sx * (B.plugSq / 2 + pcx + 0.1), py, pz));
|
|
91
|
+
const ribLen = (B.w / 2 - w + 0.4) - (pcx + B.plugSq / 2);
|
|
92
|
+
const rib = box(ribLen, B.plugSq, 6)
|
|
93
|
+
.translate(sx * (pcx + B.plugSq / 2 + ribLen / 2), py, 0);
|
|
94
|
+
sh = sh.add(plug, rib);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ---- servo bosses hanging from the bridge bar
|
|
98
|
+
for (const hx of [S.bodyXc + S.tabHoleXa, S.bodyXc + S.tabHoleXb]) {
|
|
99
|
+
let boss = box(4, S.bossSq, B.h - w + 0.4 - S.tabPlaneZ)
|
|
100
|
+
.translate(hx, 0, S.tabPlaneZ);
|
|
101
|
+
boss = boss.subtract(cylinder(5.2, 0.85).translate(hx, 0, S.tabPlaneZ - 0.1));
|
|
102
|
+
sh = sh.add(boss);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---- battery-holder standoff bosses + pilots (rear inner wall is curved at
|
|
106
|
+
// the holder's corners, so the holder mounts 2.4mm off the wall on two bosses)
|
|
107
|
+
const bi = hd - w; // rear inner face at the wall centerline
|
|
108
|
+
const battBack = bi - 2.4;
|
|
109
|
+
for (const sx of [-1, 1]) {
|
|
110
|
+
sh = sh.add(cylinder(2.7, 3).pointAlong([0, -1, 0])
|
|
111
|
+
.translate(sx * D.batt.mountHoleSpan / 2, bi + 0.3, D.batt.zc));
|
|
112
|
+
}
|
|
113
|
+
sh = sh.subtract(
|
|
114
|
+
cylinder(4.5, 0.85).pointAlong([0, 1, 0]).translate(-D.batt.mountHoleSpan / 2, battBack - 0.1, D.batt.zc),
|
|
115
|
+
cylinder(4.5, 0.85).pointAlong([0, 1, 0]).translate(D.batt.mountHoleSpan / 2, battBack - 0.1, D.batt.zc),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const conns = {
|
|
119
|
+
base: connector('mount', { origin: [0, -B.yOff, 0], axis: [0, 0, -1], up: [0, 1, 0], kind: 'fixed' }),
|
|
120
|
+
cam_recess: connector('mount', { origin: [0, fw + C.recessDepth, C.zc], axis: [0, -1, 0], up: [0, 0, 1], kind: 'fixed' }),
|
|
121
|
+
cam_cowl: connector('mount', { origin: [0, fw + C.recessDepth, C.zc], axis: [0, -1, 0], up: [0, 0, 1], kind: 'fixed' }),
|
|
122
|
+
esp_cradle: connector('mount', { origin: [0, pcbFrontY, C.zc], axis: [0, -1, 0], up: [0, 0, 1], kind: 'fixed' }),
|
|
123
|
+
esp_optics: connector('mount', { origin: [0, pcbFrontY, C.zc], axis: [0, -1, 0], up: [0, 0, 1], kind: 'fixed' }),
|
|
124
|
+
servo_mount: connector('mount', { origin: [0, 0, S.tabPlaneZ], axis: [0, 0, -1], up: [0, 1, 0], kind: 'fixed' }),
|
|
125
|
+
servo_horn_mount: connector('mount', { origin: [0, 0, S.tabPlaneZ], axis: [0, 0, -1], up: [0, 1, 0], kind: 'fixed' }),
|
|
126
|
+
batt_mount: connector('mount', { origin: [0, battBack, D.batt.zc], axis: [0, -1, 0], up: [0, 0, 1], kind: 'fixed' }),
|
|
127
|
+
cell_a: connector('mount', { origin: [0, battBack, D.batt.zc], axis: [0, -1, 0], up: [0, 0, 1], kind: 'fixed' }),
|
|
128
|
+
cell_b: connector('mount', { origin: [0, battBack, D.batt.zc], axis: [0, -1, 0], up: [0, 0, 1], kind: 'fixed' }),
|
|
129
|
+
};
|
|
130
|
+
// bezel screws bear on the bezel front face (proud of the recess)
|
|
131
|
+
const bezelFaceY = fw + C.recessDepth - C.bezelT;
|
|
132
|
+
let bi2 = 0;
|
|
133
|
+
for (const pt of circularLayout(C.screwN, C.screwR, { startDeg: C.screwStartDeg })) {
|
|
134
|
+
conns[`bezel_screw_${bi2++}`] = connector('mount', {
|
|
135
|
+
origin: [pt.x, bezelFaceY, C.zc + pt.y], axis: [0, -1, 0], up: [0, 0, 1], kind: 'fixed',
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return tealMat(sh).withConnectors(conns);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ----------------------------------------------------------------- bezel -----
|
|
142
|
+
function buildBezel(D) {
|
|
143
|
+
const C = D.cam;
|
|
144
|
+
let bz = cylinder(C.bezelT, C.bezelOD / 2).subtract(cylinder(C.bezelT + 1, C.bezelID / 2).translate(0, 0, -0.5));
|
|
145
|
+
for (const pt of circularLayout(C.screwN, C.screwR, { startDeg: C.screwStartDeg })) {
|
|
146
|
+
bz = bz.subtract(cylinder(C.bezelT + 1, 1.0).translate(pt.x, pt.y, -0.5));
|
|
147
|
+
}
|
|
148
|
+
return tealMat(bz).withConnectors({
|
|
149
|
+
back: connector('mount', { origin: [0, 0, 0], axis: [0, 0, -1], up: [0, 1, 0], kind: 'fixed' }),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function buildCowl(D) {
|
|
154
|
+
const C = D.cam;
|
|
155
|
+
let cw = cylinder(C.cowlT, C.cowlD / 2, C.cowlD / 2 - 1.4);
|
|
156
|
+
cw = cw.subtract(cylinder(C.cowlT + 1, C.apertureD / 2).translate(0, 0, -0.5));
|
|
157
|
+
return cw.color(COL_BLACK).material({ metalness: 0.1, roughness: 0.35, clearcoat: 0.5 })
|
|
158
|
+
.withConnectors({
|
|
159
|
+
back: connector('mount', { origin: [0, 0, 0], axis: [0, 0, -1], up: [0, 1, 0], kind: 'fixed' }),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// -------------------------------------------------------------- ESP32-CAM ----
|
|
164
|
+
function buildEsp(D) {
|
|
165
|
+
const E = D.esp;
|
|
166
|
+
// local: z=0 at the PCB front face, lens barrel +Z. union() keeps only the
|
|
167
|
+
// first operand's color, so board and optics are separate shapes in a group.
|
|
168
|
+
const pcb = box(E.pcbW, E.pcbH, E.pcbT).translate(0, 0, -E.pcbT);
|
|
169
|
+
const shield = box(18, 17, E.compT).translate(0, -8, -E.pcbT - E.compT);
|
|
170
|
+
const camChip = box(9, 9, 1.4).translate(0, 0, -E.pcbT - 1.4);
|
|
171
|
+
const board = pcb.add(shield, camChip).color('#1a7a3a').material({ metalness: 0.3, roughness: 0.5 });
|
|
172
|
+
const barrel = cylinder(E.barrelL, E.barrelD / 2);
|
|
173
|
+
const lens = cylinder(1.3, 2.6).translate(0, 0, E.barrelL - 0.3); // welded into the barrel
|
|
174
|
+
const optics = barrel.add(lens).color(COL_BLACK)
|
|
175
|
+
.material({ metalness: 0.2, roughness: 0.35 });
|
|
176
|
+
const faceConn = () => connector('mount', { origin: [0, 0, 0], axis: [0, 0, -1], up: [0, 1, 0], kind: 'fixed' });
|
|
177
|
+
return {
|
|
178
|
+
board: board.withConnectors({ face: faceConn() }),
|
|
179
|
+
optics: optics.withConnectors({ face: faceConn() }),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ------------------------------------------------------------------ servo ----
|
|
184
|
+
function buildServo(D) {
|
|
185
|
+
const S = D.servo;
|
|
186
|
+
// local: z=0 tab top plane, spline axis at x=0, body hangs -Z
|
|
187
|
+
const body = box(S.bodyW, S.bodyD, S.bodyDown + S.bodyUp)
|
|
188
|
+
.translate(S.bodyXc, 0, -S.bodyDown);
|
|
189
|
+
const tabs = box(S.tabSpan, S.bodyD, S.tabT).translate(S.bodyXc, 0, -S.tabT);
|
|
190
|
+
const tower = cylinder(S.towerH, S.towerD / 2).translate(0, 0, S.bodyUp);
|
|
191
|
+
const spline = cylinder(S.splineH, S.splineD / 2).translate(0, 0, S.bodyUp + S.towerH);
|
|
192
|
+
const hornHub = cylinder(4.6, S.hornHubD / 2).translate(0, 0, S.bodyUp + S.towerH + S.splineH - 2.4);
|
|
193
|
+
const hornDisc = cylinder(S.hornT, S.hornD / 2).translate(0, 0, S.bodyUp + S.towerH + S.splineH + 2.2 - S.hornT);
|
|
194
|
+
// two M2 screws up through the tab holes into the boss pilots
|
|
195
|
+
let screws = null;
|
|
196
|
+
for (const hx of [S.bodyXc + S.tabHoleXa, S.bodyXc + S.tabHoleXb]) {
|
|
197
|
+
const sc = cylinder(1.5, 1.9).translate(hx, 0, -S.tabT - 1.5)
|
|
198
|
+
.add(cylinder(6.5, 0.8).translate(hx, 0, -S.tabT));
|
|
199
|
+
screws = screws ? screws.add(sc) : sc;
|
|
200
|
+
}
|
|
201
|
+
const blue = body.add(tabs, screws.color(COL_SCREW)).color('#2563b0')
|
|
202
|
+
.material({ metalness: 0.15, roughness: 0.55 });
|
|
203
|
+
const white = tower.add(spline, hornHub, hornDisc).color('#e8e6df')
|
|
204
|
+
.material({ metalness: 0.05, roughness: 0.5 });
|
|
205
|
+
const hornTopZ = S.bodyUp + S.towerH + S.splineH + 2.2;
|
|
206
|
+
const tabsConn = () => connector('mount', { origin: [0, 0, 0], axis: [0, 0, 1], up: [0, 1, 0], kind: 'fixed' });
|
|
207
|
+
return {
|
|
208
|
+
body: blue.withConnectors({ tabs: tabsConn() }),
|
|
209
|
+
horn: white.withConnectors({
|
|
210
|
+
tabs: tabsConn(),
|
|
211
|
+
spline: connector('axle', { origin: [0, 0, hornTopZ], axis: [0, 0, 1], up: [0, 1, 0], kind: 'revolute' }),
|
|
212
|
+
}),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ---------------------------------------------------------------- battery ----
|
|
217
|
+
function buildBatteryHolder(D) {
|
|
218
|
+
const BT = D.batt;
|
|
219
|
+
// local: z=0 back face (on the standoff bosses), +Z toward the interior
|
|
220
|
+
let holder = box(BT.w, BT.h, BT.d);
|
|
221
|
+
holder = holder.subtract(box(65, BT.h - 3.2, BT.d).translate(0, 0, 1.6));
|
|
222
|
+
return holder.color('#23262b').material({ metalness: 0.1, roughness: 0.6 }).withConnectors({
|
|
223
|
+
back: connector('mount', { origin: [0, 0, 0], axis: [0, 0, -1], up: [0, 1, 0], kind: 'fixed' }),
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function buildCell(D, sy) {
|
|
228
|
+
return cylinder(65, 9.2).pointAlong([1, 0, 0]).translate(-32.5, sy * 9.4, 1.6 + 9.2)
|
|
229
|
+
.color('#2e6647').material({ metalness: 0.5, roughness: 0.35 }).withConnectors({
|
|
230
|
+
back: connector('mount', { origin: [0, 0, 0], axis: [0, 0, -1], up: [0, 1, 0], kind: 'fixed' }),
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ------------------------------------------------------------------------------
|
|
235
|
+
const D = dimsMod.compute({ wall: wallP, wheelOD: wheelODP });
|
|
236
|
+
|
|
237
|
+
const make = {
|
|
238
|
+
buildShell, buildBezel, buildCowl, buildEsp,
|
|
239
|
+
buildServo, buildBatteryHolder, buildCell,
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
if (require.main === module) {
|
|
243
|
+
const sh = buildShell(D);
|
|
244
|
+
const bezel = buildBezel(D).matchTo(sh, 'back', 'cam_recess');
|
|
245
|
+
const cowl = buildCowl(D).matchTo(sh, 'back', 'cam_cowl');
|
|
246
|
+
const hw = require('./hardware.forge.js');
|
|
247
|
+
const bezelScrews = [];
|
|
248
|
+
for (let i = 0; i < 4; i++) {
|
|
249
|
+
bezelScrews.push({ name: `Bezel Screw ${i + 1}`, shape: hw.make.screwM2(6).matchTo(sh, 'seat', `bezel_screw_${i}`) });
|
|
250
|
+
}
|
|
251
|
+
const espParts = buildEsp(D);
|
|
252
|
+
const espBoard = espParts.board.matchTo(sh, 'face', 'esp_cradle');
|
|
253
|
+
const espOptics = espParts.optics.matchTo(sh, 'face', 'esp_optics');
|
|
254
|
+
const servoParts = buildServo(D);
|
|
255
|
+
const servoBody = servoParts.body.matchTo(sh, 'tabs', 'servo_mount');
|
|
256
|
+
const servoHorn = servoParts.horn.matchTo(sh, 'tabs', 'servo_horn_mount');
|
|
257
|
+
const holder = buildBatteryHolder(D).matchTo(sh, 'back', 'batt_mount');
|
|
258
|
+
const cellA = buildCell(D, -1).matchTo(sh, 'back', 'cell_a');
|
|
259
|
+
const cellB = buildCell(D, 1).matchTo(sh, 'back', 'cell_b');
|
|
260
|
+
|
|
261
|
+
verify.notColliding('bezel clear of shell', bezel, sh);
|
|
262
|
+
verify.notColliding('cowl clear of shell', cowl, sh);
|
|
263
|
+
verify.notColliding('cowl clear of bezel', cowl, bezel);
|
|
264
|
+
verify.notColliding('ESP32 board clear of shell', espBoard, sh);
|
|
265
|
+
verify.notColliding('ESP32 optics clear of shell', espOptics, sh);
|
|
266
|
+
verify.notColliding('ESP32 optics clear of cowl', espOptics, cowl);
|
|
267
|
+
verify.notColliding('servo body clear of shell', servoBody, sh);
|
|
268
|
+
verify.notColliding('servo horn clear of shell', servoHorn, sh);
|
|
269
|
+
verify.notColliding('battery holder clear of shell', holder, sh);
|
|
270
|
+
verify.notColliding('cell A clear of holder', cellA, holder);
|
|
271
|
+
verify.notColliding('cells clear of each other', cellA, cellB);
|
|
272
|
+
verify.clearanceBetween('bezel seats in recess', bezel, sh, -0.01, 0.1);
|
|
273
|
+
verify.clearanceBetween('ESP32 board held by rails', espBoard, sh, -0.01, 0.3);
|
|
274
|
+
verify.clearanceBetween('servo tabs seat on bosses', servoBody, sh, -0.01, 0.1);
|
|
275
|
+
verify.clearanceBetween('holder seats on rear wall', holder, sh, -0.01, 0.1);
|
|
276
|
+
|
|
277
|
+
scene({
|
|
278
|
+
camera: { position: [200, -230, 200], target: [0, 0, 55], fov: 40 },
|
|
279
|
+
environment: { preset: 'studio', intensity: 0.25, background: false },
|
|
280
|
+
lights: [
|
|
281
|
+
{ type: 'ambient', color: '#efe7dc', intensity: 0.18 },
|
|
282
|
+
{ type: 'directional', position: [140, -200, 260], color: '#ffe2bf', intensity: 2.6, castShadow: true },
|
|
283
|
+
{ type: 'directional', position: [-150, 120, 140], color: '#d4e6fb', intensity: 0.8 },
|
|
284
|
+
],
|
|
285
|
+
});
|
|
286
|
+
return {
|
|
287
|
+
preview: [
|
|
288
|
+
{ name: 'Body Shell', shape: sh },
|
|
289
|
+
{ name: 'Camera Bezel', shape: bezel },
|
|
290
|
+
{ name: 'Lens Cowl', shape: cowl },
|
|
291
|
+
...bezelScrews,
|
|
292
|
+
{ name: 'ESP32-CAM Board', shape: espBoard },
|
|
293
|
+
{ name: 'ESP32-CAM Optics', shape: espOptics },
|
|
294
|
+
{ name: 'Servo Body', shape: servoBody },
|
|
295
|
+
{ name: 'Servo Horn', shape: servoHorn },
|
|
296
|
+
{ name: 'Battery Holder', shape: holder },
|
|
297
|
+
{ name: '18650 Cell A', shape: cellA },
|
|
298
|
+
{ name: '18650 Cell B', shape: cellB },
|
|
299
|
+
],
|
|
300
|
+
make,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return { make };
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
// Chassis of the scout cam rover: printed tub + service door + motor pods,
|
|
2
|
+
// purchased N20 gearmotors, driver/IMU/buck boards, and a rocker switch.
|
|
3
|
+
// Tub-local frame: centered on XY, floor at z=0, axle along X at z=tub.axleZl.
|
|
4
|
+
// Connector recipe (probed): child parts are built +Z from a `z=0, axis [0,0,-1]`
|
|
5
|
+
// connector; the parent connector axis is the desired world direction of the child.
|
|
6
|
+
|
|
7
|
+
const wallP = param('wall_thickness', 2.4, { min: 2.0, max: 3.0, step: 0.1, unit: 'mm' });
|
|
8
|
+
const wheelODP = param('wheel_od', 110, { min: 96, max: 130, step: 1, unit: 'mm' });
|
|
9
|
+
|
|
10
|
+
const dimsMod = require('../lib/dims.js');
|
|
11
|
+
|
|
12
|
+
const COL_TEAL = '#2fb3b8';
|
|
13
|
+
const COL_BLACK = '#1f2023';
|
|
14
|
+
const COL_STEEL = '#b9bdc4';
|
|
15
|
+
const COL_SCREW = '#3a3d42';
|
|
16
|
+
|
|
17
|
+
function tealMat(s) { return s.color(COL_TEAL).material({ metalness: 0.05, roughness: 0.45 }); }
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------- tub --------
|
|
20
|
+
function buildTub(D) {
|
|
21
|
+
const T = D.tub, M = D.motor, dr = D.door, P = D.pod, B = D.boards, SW = D.sw;
|
|
22
|
+
const hw = T.w / 2, hd = T.d / 2;
|
|
23
|
+
const ihw = hw - T.wall, ihd = hd - T.wall;
|
|
24
|
+
const az = T.axleZl;
|
|
25
|
+
|
|
26
|
+
let tub = roundedRect(T.w, T.d, T.cornerR).extrude(T.h);
|
|
27
|
+
tub = tub.subtract(
|
|
28
|
+
roundedRect(2 * ihw, 2 * ihd, T.cornerR - 2).extrude(T.h - T.floorT + 2).translate(0, 0, T.floorT)
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
// corner posts that receive the body-shell plugs (welded 0.3mm into the walls).
|
|
32
|
+
// The outward diagonal corner is chamfered flat so the web to the rounded
|
|
33
|
+
// outer wall corner stays >= 1.4mm (a square corner leaves a 0.4mm sliver).
|
|
34
|
+
const pcx = hw - T.wall - T.postInset, pcy = hd - T.wall - T.postInset;
|
|
35
|
+
const ocx = hw - T.cornerR, ocy = hd - T.cornerR; // outer corner arc centers
|
|
36
|
+
for (const sx of [-1, 1]) for (const sy of [-1, 1]) {
|
|
37
|
+
let post = box(T.postSq + 0.6, T.postSq + 0.6, T.h - T.floorT)
|
|
38
|
+
.translate(sx * pcx, sy * pcy, T.floorT);
|
|
39
|
+
const diag = Math.SQRT1_2;
|
|
40
|
+
const cut = box(20, 20, T.h + 4).rotate([0, 0, 1], 45)
|
|
41
|
+
.translate(sx * (ocx + (T.cornerR - 1.4 + 10) * diag), sy * (ocy + (T.cornerR - 1.4 + 10) * diag), T.floorT - 2);
|
|
42
|
+
post = post.subtract(cut);
|
|
43
|
+
post = post.subtract(
|
|
44
|
+
box(D.body.socketSq, D.body.socketSq, D.body.socketDepth + 1)
|
|
45
|
+
.translate(sx * pcx, sy * pcy, T.h - D.body.socketDepth)
|
|
46
|
+
);
|
|
47
|
+
tub = tub.add(post);
|
|
48
|
+
}
|
|
49
|
+
// horizontal shell-screw clearance holes through the side walls into the sockets
|
|
50
|
+
for (const sx of [-1, 1]) for (const sy of [-1, 1]) {
|
|
51
|
+
tub = tub.subtract(
|
|
52
|
+
cylinder(8, 1.0).pointAlong([sx, 0, 0]).translate(sx * (hw - 7), sy * pcy, T.sideScrewZ)
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// raised border frame around the service door (reference look: inset panel in a frame)
|
|
57
|
+
const frameSk = roundedRect(dr.panelW + 12, dr.panelH + 12, 4)
|
|
58
|
+
.subtract(roundedRect(dr.panelW + 2 * dr.recessSlop + 4, dr.panelH + 2 * dr.recessSlop + 4, 3));
|
|
59
|
+
// welded 1.7 deep: the front face recedes up to 1.5mm where the frame spans
|
|
60
|
+
// the rounded wall corners, and a shallow weld leaves knife-edge slivers
|
|
61
|
+
tub = tub.add(
|
|
62
|
+
frameSk.extrude(1.4 + 1.7).rotate([1, 0, 0], 90).translate(0, -hd + 1.7, dr.zc)
|
|
63
|
+
.trimByPlane([0, 0, 1], 0.8) // keep the border inside the tub footprint
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// service-door recess + opening + screw bosses (front wall, -Y)
|
|
67
|
+
tub = tub.subtract(
|
|
68
|
+
box(dr.panelW + 2 * dr.recessSlop, dr.recess + 0.05, dr.panelH + 2 * dr.recessSlop)
|
|
69
|
+
.translate(0, -hd + (dr.recess + 0.05) / 2 - 0.05, dr.zc - (dr.panelH + 2 * dr.recessSlop) / 2)
|
|
70
|
+
);
|
|
71
|
+
tub = tub.subtract(
|
|
72
|
+
box(dr.openW, T.wall + dr.recess + 1, dr.openH)
|
|
73
|
+
.translate(0, -hd + (T.wall + dr.recess) / 2, dr.zc - dr.openH / 2)
|
|
74
|
+
);
|
|
75
|
+
for (const sx of [-1, 1]) for (const sz of [-1, 1]) {
|
|
76
|
+
const bz = dr.zc + sz * dr.screwDZ;
|
|
77
|
+
tub = tub.add(
|
|
78
|
+
cylinder(dr.bossLen + 0.3, dr.bossD / 2).pointAlong([0, 1, 0])
|
|
79
|
+
.translate(sx * dr.screwDX, -ihd - 0.3, bz)
|
|
80
|
+
);
|
|
81
|
+
tub = tub.subtract(
|
|
82
|
+
cylinder(7.2, dr.pilotD / 2).pointAlong([0, 1, 0])
|
|
83
|
+
.translate(sx * dr.screwDX, -hd + dr.recess - 0.1, bz)
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// motor wall slots (gearbox nose passes through, flush with outer face),
|
|
88
|
+
// plus the pod screw clearance holes — one batched subtract per side
|
|
89
|
+
for (const sx of [-1, 1]) {
|
|
90
|
+
const cuts = [
|
|
91
|
+
box(T.wall + 3, M.w + 0.6, M.h + 0.6).translate(sx * (hw - (T.wall + 3) / 2 + 1), 0, az - (M.h + 0.6) / 2),
|
|
92
|
+
];
|
|
93
|
+
for (const pt of circularLayout(P.screwN, P.screwR, { startDeg: 90 })) {
|
|
94
|
+
cuts.push(cylinder(6, P.wallClearD / 2).pointAlong([sx, 0, 0]).translate(sx * (hw - 5), pt.x, az + pt.y));
|
|
95
|
+
}
|
|
96
|
+
tub = tub.subtract(...cuts);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// motor saddles: base + side cheeks + rear stop rib with terminal notch
|
|
100
|
+
const sadY = M.w / 2 + 0.15; // cheek inner face
|
|
101
|
+
const motorBackX = hw - M.len;
|
|
102
|
+
for (const sx of [-1, 1]) {
|
|
103
|
+
const x0 = motorBackX - 0.2, x1 = ihw + 0.4; // welded into the wall
|
|
104
|
+
const sadLen = x1 - x0;
|
|
105
|
+
tub = tub.add(box(sadLen, 2 * sadY + 4.8, az - M.h / 2 - T.floorT + 0.2)
|
|
106
|
+
.translate(sx * (x0 + sadLen / 2), 0, T.floorT - 0.2));
|
|
107
|
+
for (const sy of [-1, 1]) {
|
|
108
|
+
tub = tub.add(box(sadLen, 2.4, M.h - 3)
|
|
109
|
+
.translate(sx * (x0 + sadLen / 2), sy * (sadY + 1.2), az - M.h / 2 - 0.2));
|
|
110
|
+
}
|
|
111
|
+
let rib = box(2.4, 2 * sadY + 4.8, az + M.h / 2 + 2.8 - T.floorT)
|
|
112
|
+
.translate(sx * (motorBackX - 1.2), 0, T.floorT - 0.2);
|
|
113
|
+
rib = rib.subtract(box(3, 9, M.h - 1).translate(sx * (motorBackX - 1.2), 0, az - M.h / 2 + 0.5));
|
|
114
|
+
tub = tub.add(rib);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// board standoff posts (4 per module, corner-inset 2.5mm)
|
|
118
|
+
for (const bd of [B.drv, B.buck, B.mpu]) {
|
|
119
|
+
for (const sx of [-1, 1]) for (const sy of [-1, 1]) {
|
|
120
|
+
tub = tub.add(
|
|
121
|
+
cylinder(B.standoffH + 0.3, 2).translate(bd.x + sx * (bd.w / 2 - 2.5), bd.y + sy * (bd.d / 2 - 2.5), T.floorT - 0.3)
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// rocker-switch opening (rear wall, +Y)
|
|
127
|
+
tub = tub.subtract(
|
|
128
|
+
box(SW.w + 0.4, T.wall + 1, SW.h + 0.4).translate(0, hd - (T.wall + 1) / 2 + 0.4, SW.zc - (SW.h + 0.4) / 2)
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
// connector map, incl. one bearing-plane connector per screw
|
|
132
|
+
const conns = {
|
|
133
|
+
deck: connector('mount', { origin: [0, 0, T.h], axis: [0, 0, 1], up: [0, 1, 0], kind: 'fixed' }),
|
|
134
|
+
door_frame: connector('mount', { origin: [0, -hd + dr.recess, dr.zc], axis: [0, -1, 0], up: [0, 0, 1], kind: 'fixed' }),
|
|
135
|
+
pod_seat_l: connector('mount', { origin: [-hw, 0, az], axis: [-1, 0, 0], up: [0, 0, 1], kind: 'fixed' }),
|
|
136
|
+
pod_seat_r: connector('mount', { origin: [hw, 0, az], axis: [1, 0, 0], up: [0, 0, 1], kind: 'fixed' }),
|
|
137
|
+
motor_seat_l: connector('mount', { origin: [-hw, 0, az], axis: [-1, 0, 0], up: [0, 0, 1], kind: 'fixed' }),
|
|
138
|
+
motor_seat_r: connector('mount', { origin: [hw, 0, az], axis: [1, 0, 0], up: [0, 0, 1], kind: 'fixed' }),
|
|
139
|
+
boards_drv: connector('mount', { origin: [0, 0, T.floorT], axis: [0, 0, 1], up: [0, 1, 0], kind: 'fixed' }),
|
|
140
|
+
boards_buck: connector('mount', { origin: [0, 0, T.floorT], axis: [0, 0, 1], up: [0, 1, 0], kind: 'fixed' }),
|
|
141
|
+
boards_mpu: connector('mount', { origin: [0, 0, T.floorT], axis: [0, 0, 1], up: [0, 1, 0], kind: 'fixed' }),
|
|
142
|
+
switch_seat: connector('mount', { origin: [0, hd, SW.zc], axis: [0, 1, 0], up: [0, 0, 1], kind: 'fixed' }),
|
|
143
|
+
};
|
|
144
|
+
// door screws bear on the panel face
|
|
145
|
+
let di = 0;
|
|
146
|
+
for (const sx of [-1, 1]) for (const sz of [-1, 1]) {
|
|
147
|
+
conns[`door_screw_${di++}`] = connector('mount', {
|
|
148
|
+
origin: [sx * dr.screwDX, -hd + dr.recess - dr.panelT, dr.zc + sz * dr.screwDZ],
|
|
149
|
+
axis: [0, -1, 0], up: [0, 0, 1], kind: 'fixed',
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
// pod screws bear on the inner wall face (heads inside the tub)
|
|
153
|
+
for (const side of [-1, 1]) {
|
|
154
|
+
let pi = 0;
|
|
155
|
+
for (const pt of circularLayout(P.screwN, P.screwR, { startDeg: 90 })) {
|
|
156
|
+
conns[`pod_screw_${side < 0 ? 'l' : 'r'}_${pi++}`] = connector('mount', {
|
|
157
|
+
origin: [side * ihw, pt.x, az + pt.y], axis: [-side, 0, 0], up: [0, 0, 1], kind: 'fixed',
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// shell screws bear on the outer side walls
|
|
162
|
+
let si = 0;
|
|
163
|
+
for (const sx of [-1, 1]) for (const sy of [-1, 1]) {
|
|
164
|
+
conns[`shell_screw_${si++}`] = connector('mount', {
|
|
165
|
+
origin: [sx * hw, sy * pcy, T.sideScrewZ], axis: [sx, 0, 0], up: [0, 0, 1], kind: 'fixed',
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return tealMat(tub).withConnectors(conns);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ------------------------------------------------------------- door ----------
|
|
172
|
+
function buildDoor(D) {
|
|
173
|
+
const dr = D.door;
|
|
174
|
+
let panel = roundedRect(dr.panelW, dr.panelH, 3).extrude(dr.panelT);
|
|
175
|
+
const plug = roundedRect(dr.openW - 2 * dr.slop, dr.openH - 2 * dr.slop, 2)
|
|
176
|
+
.extrude(dr.plugT).translate(0, 0, -dr.plugT);
|
|
177
|
+
let door = panel.add(plug);
|
|
178
|
+
for (const sx of [-1, 1]) for (const sy of [-1, 1]) {
|
|
179
|
+
door = door.subtract(cylinder(dr.plugT + dr.panelT + 1, 1.0)
|
|
180
|
+
.translate(sx * dr.screwDX, sy * dr.screwDZ, -dr.plugT - 0.5));
|
|
181
|
+
}
|
|
182
|
+
return tealMat(door).withConnectors({
|
|
183
|
+
back: connector('mount', { origin: [0, 0, 0], axis: [0, 0, -1], up: [0, 1, 0], kind: 'fixed' }),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ------------------------------------------------------------- pods ----------
|
|
188
|
+
function buildPod(D) {
|
|
189
|
+
const P = D.pod;
|
|
190
|
+
let pod = cylinder(P.len, P.od / 2).subtract(cylinder(P.len + 1, P.boreD / 2).translate(0, 0, -0.5));
|
|
191
|
+
for (const pt of circularLayout(P.screwN, P.screwR, { startDeg: 90 })) {
|
|
192
|
+
pod = pod.subtract(cylinder(P.pilotDepth + 0.1, P.pilotD / 2).translate(pt.x, pt.y, -0.1));
|
|
193
|
+
}
|
|
194
|
+
// cosmetic face groove
|
|
195
|
+
pod = pod.subtract(
|
|
196
|
+
cylinder(1.0, P.od / 2 - 2).subtract(cylinder(1.4, P.od / 2 - 3.4).translate(0, 0, -0.2))
|
|
197
|
+
.translate(0, 0, P.len - 1)
|
|
198
|
+
);
|
|
199
|
+
return pod.color(COL_BLACK).material({ metalness: 0.1, roughness: 0.6 }).withConnectors({
|
|
200
|
+
flange: connector('mount', { origin: [0, 0, 0], axis: [0, 0, -1], up: [0, 1, 0], kind: 'fixed' }),
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ------------------------------------------------------------ motor ----------
|
|
205
|
+
function buildMotor(D) {
|
|
206
|
+
const M = D.motor;
|
|
207
|
+
// single Shape (not a group) so collision/clearance verifies stay exact
|
|
208
|
+
const body = box(M.w, M.h, M.len).translate(0, 0, -M.len);
|
|
209
|
+
const nose = cylinder(M.noseBossL, M.noseBossD / 2);
|
|
210
|
+
// only a stub of the D-shaft: the exposed shaft is modeled on the wheel part,
|
|
211
|
+
// where it turns with the wheel (a static D-shaft in a turning D-bore would
|
|
212
|
+
// interpenetrate at every non-zero joint angle)
|
|
213
|
+
const stub = cylinder(1.3, M.shaftD / 2);
|
|
214
|
+
const terms = box(1.2, 3, 1.9).translate(3, 0, -M.len - 1.5)
|
|
215
|
+
.add(box(1.2, 3, 1.9).translate(-3, 0, -M.len - 1.5));
|
|
216
|
+
return body.add(nose, stub, terms)
|
|
217
|
+
.color(COL_STEEL).material({ metalness: 0.85, roughness: 0.35 })
|
|
218
|
+
.withConnectors({
|
|
219
|
+
nose: connector('mount', { origin: [0, 0, 0], axis: [0, 0, -1], up: [0, 1, 0], kind: 'fixed' }),
|
|
220
|
+
shaft_tip: connector('axle', { origin: [0, 0, 1.5], axis: [0, 0, 1], up: [0, 1, 0], kind: 'revolute' }),
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ----------------------------------------------------- electronics -----------
|
|
225
|
+
function buildBoards(D) {
|
|
226
|
+
const B = D.boards;
|
|
227
|
+
const mk = (bd, colPcb, chipW, chipD) => {
|
|
228
|
+
const pcb = box(bd.w, bd.d, bd.t).translate(bd.x, bd.y, B.standoffH);
|
|
229
|
+
const chip = box(chipW, chipD, 1.2).translate(bd.x, bd.y, B.standoffH + bd.t);
|
|
230
|
+
return pcb.add(chip).color(colPcb).material({ metalness: 0.1, roughness: 0.5 })
|
|
231
|
+
.withConnectors({
|
|
232
|
+
mount: connector('mount', { origin: [0, 0, 0], axis: [0, 0, -1], up: [0, 1, 0], kind: 'fixed' }),
|
|
233
|
+
});
|
|
234
|
+
};
|
|
235
|
+
// three separate purchased modules, each its own Shape (exact collision checks)
|
|
236
|
+
return {
|
|
237
|
+
drv: mk(B.drv, '#6b21a8', 6, 6),
|
|
238
|
+
buck: mk(B.buck, '#1e6fb8', 8, 7),
|
|
239
|
+
mpu: mk(B.mpu, '#1e40af', 5, 5),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function buildSwitch(D) {
|
|
244
|
+
const SW = D.sw;
|
|
245
|
+
// local frame: +Z = outward, mating plane (plate back on wall outer face) at z=0
|
|
246
|
+
const plateL = box(SW.plateW, SW.plateH, SW.plateT);
|
|
247
|
+
const bodyL = box(SW.w, SW.h, SW.t).translate(0, 0, -SW.t);
|
|
248
|
+
const rocker = box(8, 5, 1.6).translate(0, 0, SW.plateT);
|
|
249
|
+
let sw = plateL.add(bodyL).add(rocker);
|
|
250
|
+
return sw.color('#15161a').material({ metalness: 0.15, roughness: 0.45 }).withConnectors({
|
|
251
|
+
mount: connector('mount', { origin: [0, 0, 0], axis: [0, 0, -1], up: [0, 1, 0], kind: 'fixed' }),
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ------------------------------------------------------------------------------
|
|
256
|
+
const D = dimsMod.compute({ wall: wallP, wheelOD: wheelODP });
|
|
257
|
+
|
|
258
|
+
const make = { buildTub, buildDoor, buildPod, buildMotor, buildBoards, buildSwitch };
|
|
259
|
+
|
|
260
|
+
if (require.main === module) {
|
|
261
|
+
const hw = require('./hardware.forge.js');
|
|
262
|
+
const tub = buildTub(D);
|
|
263
|
+
const door = buildDoor(D).matchTo(tub, 'back', 'door_frame');
|
|
264
|
+
const screws = [];
|
|
265
|
+
for (let i = 0; i < 4; i++) screws.push({ name: `Door Screw ${i + 1}`, shape: hw.make.screwM2(8).matchTo(tub, 'seat', `door_screw_${i}`) });
|
|
266
|
+
for (const sd of ['l', 'r']) for (let i = 0; i < 3; i++) {
|
|
267
|
+
screws.push({ name: `Pod Screw ${sd.toUpperCase()} ${i + 1}`, shape: hw.make.screwM2(8).matchTo(tub, 'seat', `pod_screw_${sd}_${i}`) });
|
|
268
|
+
}
|
|
269
|
+
for (let i = 0; i < 4; i++) screws.push({ name: `Shell Screw ${i + 1}`, shape: hw.make.screwM2(8).matchTo(tub, 'seat', `shell_screw_${i}`) });
|
|
270
|
+
const podL = buildPod(D).matchTo(tub, 'flange', 'pod_seat_l');
|
|
271
|
+
const podR = buildPod(D).matchTo(tub, 'flange', 'pod_seat_r');
|
|
272
|
+
const motorL = buildMotor(D).matchTo(tub, 'nose', 'motor_seat_l');
|
|
273
|
+
const motorR = buildMotor(D).matchTo(tub, 'nose', 'motor_seat_r');
|
|
274
|
+
const mods = buildBoards(D);
|
|
275
|
+
const drv = mods.drv.matchTo(tub, 'mount', 'boards_drv');
|
|
276
|
+
const buck = mods.buck.matchTo(tub, 'mount', 'boards_buck');
|
|
277
|
+
const mpu = mods.mpu.matchTo(tub, 'mount', 'boards_mpu');
|
|
278
|
+
const rocker = buildSwitch(D).matchTo(tub, 'mount', 'switch_seat');
|
|
279
|
+
|
|
280
|
+
verify.notColliding('motor L clear of tub', motorL, tub);
|
|
281
|
+
verify.notColliding('motor R clear of tub', motorR, tub);
|
|
282
|
+
verify.notColliding('DRV8833 clear of tub', drv, tub);
|
|
283
|
+
verify.notColliding('buck clear of tub', buck, tub);
|
|
284
|
+
verify.notColliding('IMU clear of tub', mpu, tub);
|
|
285
|
+
verify.notColliding('door clear of tub', door, tub);
|
|
286
|
+
verify.notColliding('pod L clear of motor L', podL, motorL);
|
|
287
|
+
verify.notColliding('switch clear of tub', rocker, tub);
|
|
288
|
+
verify.clearanceBetween('door seats in recess', door, tub, -0.01, 0.1);
|
|
289
|
+
verify.clearanceBetween('motor L seats on saddle', motorL, tub, -0.01, 0.1);
|
|
290
|
+
verify.clearanceBetween('pod L seats on wall', podL, tub, -0.01, 0.1);
|
|
291
|
+
verify.clearanceBetween('IMU seats on standoffs', mpu, tub, -0.01, 0.1);
|
|
292
|
+
|
|
293
|
+
scene({
|
|
294
|
+
camera: { position: [180, -190, 150], target: [0, 0, 26], fov: 40 },
|
|
295
|
+
environment: { preset: 'studio', intensity: 0.25, background: false },
|
|
296
|
+
lights: [
|
|
297
|
+
{ type: 'ambient', color: '#efe7dc', intensity: 0.18 },
|
|
298
|
+
{ type: 'directional', position: [140, -160, 220], color: '#ffe2bf', intensity: 2.6, castShadow: true },
|
|
299
|
+
{ type: 'directional', position: [-130, 110, 110], color: '#d4e6fb', intensity: 0.8 },
|
|
300
|
+
],
|
|
301
|
+
});
|
|
302
|
+
return {
|
|
303
|
+
preview: [
|
|
304
|
+
{ name: 'Tub', shape: tub },
|
|
305
|
+
{ name: 'Door', shape: door },
|
|
306
|
+
...screws,
|
|
307
|
+
{ name: 'Pod L', shape: podL },
|
|
308
|
+
{ name: 'Pod R', shape: podR },
|
|
309
|
+
{ name: 'Motor L', shape: motorL },
|
|
310
|
+
{ name: 'Motor R', shape: motorR },
|
|
311
|
+
{ name: 'DRV8833', shape: drv },
|
|
312
|
+
{ name: 'MP1584 Buck', shape: buck },
|
|
313
|
+
{ name: 'MPU6050 IMU', shape: mpu },
|
|
314
|
+
{ name: 'Switch', shape: rocker },
|
|
315
|
+
],
|
|
316
|
+
make,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return { make };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Shared purchased hardware builders for the scout cam rover.
|
|
2
|
+
// screwM2(len): pan-head M2 self-tapping screw, modeled at thread minor diameter
|
|
3
|
+
// (1.6) so clearance holes (1.9-2.0) and pilots (1.7) never interpenetrate.
|
|
4
|
+
// Local frame: bearing plane (under-head) at z=0, head +Z, shank -Z.
|
|
5
|
+
// Connector 'seat': recipe frame — parent connector axis = world direction of the head side.
|
|
6
|
+
|
|
7
|
+
function screwM2(len) {
|
|
8
|
+
const head = cylinder(1.5, 1.9);
|
|
9
|
+
const shank = cylinder(len, 0.8).translate(0, 0, -len);
|
|
10
|
+
return head.add(shank)
|
|
11
|
+
.color('#3a3d42').material({ metalness: 0.8, roughness: 0.4 })
|
|
12
|
+
.withConnectors({
|
|
13
|
+
seat: connector('mount', { origin: [0, 0, 0], axis: [0, 0, -1], up: [0, 1, 0], kind: 'fixed' }),
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (require.main === module) {
|
|
18
|
+
return { preview: [{ name: 'M2x8', shape: screwM2(8) }], make: { screwM2 } };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return { make: { screwM2 } };
|