2026: OpenSCAD robot and remote, physical joint limits, model in Unity

hardware/openscad/ rebuilds the broken Fusion models as OpenSCAD:
body (top/bottom plate, standoffs, battery bay, switch), legs (coxa
bracket, femur link, tibia, horn adapters, TPU socks, servo cables),
the whole robot, and the remote (shell, lid, Nextion 3.5", JH-D400X-R4
sticks, 18650 UPS board, IMU, Teensy 4.0, USB-C breakout). Every part
was checked by slicing against the original STEP solids; the original's
bugs are fixed and listed in PLANNING.md.

The robot now uses a hexagon leg layout (corner legs 60 deg round from
the mid legs; the corner coxa brackets need turning on their splines).
Physical limits from a collision sweep of the model are in Config.h:
per-leg coxa limits and LEG_CLEARANCE_MM 56 (a leg is 55.2 mm wide at
the joints); the sim API exports them (version 7). sim_smoke passes.

Unity loads the model (export_unity.py -> Resources/OpenScadModel,
OpenScadModel.cs builds a new rig in place of the old scene's robot).
The PCB model's mounting holes now use the exact EasyEDA grid. README
shows renders of both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-11-14 12:14:32 +01:00
1 parent b1c94e4149
commit 91b5b99fd5
42 files changed
+1560 -20

No files matched your search

+245
View File
@@ -0,0 +1,245 @@
// Handheld remote: bottom shell + lid, rebuilt from the Fusion 360 model
// fusion360/remote/remote v40.f3d (the `bottom` and `top` solids in
// "remote v40.stp"; remote v4 is an early flat stick plate).
//
// Written in the original's top-view coordinates (u, v) = (Fusion x,
// -Fusion z), so every number can be checked against the Fusion file.
// Z = 0 on the rim of the bottom shell (the lid's top face sits flush).
// Text on the lid reads upright with +v up.
//
// Deviations from the original (its bugs):
// - Lid moved by (-0.406, -0.21, +0.365) mm: it was off-centre and sat
// 0.365 mm into the bottom's ledge, its posts overlapping the bosses.
// - The display bezel's stepped facets (an imported mesh) are a clean
// rectangular window plus its 0.45 mm lip; small clip notches omitted.
// - Body12 (an "ON-OFF" bracket line that was never cut into the lid) left out.
/* [Shell] */
CORNERS = [[-66.5, -58.883], [66.5, -58.883], [66.5, 41.117], [-66.5, 41.117]];
R_OUT = 37.5; // outside of the bottom shell
R_WALL = 32.5; // inside of the walls (5 mm)
R_LEDGE = 35.5; // rebate the lid drops into
R_LID = 35.0; // lid outline (0.5 mm clearance in the rebate)
FLOOR_Z = [-45, -42];
LEDGE_Z = -5;
LID_Z = [-5, 0];
/* [Fixing] */
BOSSES = [[-30, 40], [30, 40], [-80.387, -72.711], [82.73, -73]];
BOSS_D = 10;
BOSS_TOP = -35;
BOSS_HOLE_D = 6; // through the floor, 10 mm deep (magnets?)
POST_HOLE_D = 3; // blind, from the lid post's end
/* [Sticks, switch, display] */
STICKS = [[-66.5, 41.117], [66.5, 41.117]];
STICK_HOLE_D = 40;
STICK_SCREW = 16.25; // 4 x d2 (M2.5 self-tapping) on the joystick's 32.5 mm square (original: 33)
SWITCH_SLOT = [[-4, 33.975], [4, 49.975]]; // on/off slide switch, mounted on top (same as the robot's)
SWITCH_HOLES = [[0, 32.475], [0, 51.475]];
PILL = [[0, 5], [0, 15]]; // centres of the round ends
WINDOW = [[-37.94, -66.964], [37.96, -16.064]];
WINDOW_LIP = [0.45, -1.05, -0.45]; // inset, z range
DISPLAY_POCKET = [[-49.0, -73.0], [52.258, -10.0]];
DISPLAY_POCKET_Z = -2; // pocket from the lid's underside up to here
DISPLAY_POSTS = [[-44.939, -65.772], [49.261, -65.772], [-44.939, -17.272], [49.261, -17.272]];
TEXT = [["ON", [0.7, 26.29]], ["OFF", [0.15, 57.61]]];
ENGRAVE_D = 1;
/* [Rendering] */
SHOW_BOTTOM = true;
SHOW_LID = true;
SHOW_DISPLAY = true;
SHOW_STICKS = true;
SHOW_BATTERY = true;
EXPLODE = 0;
/* [Hidden] */
$fa = 2; $fs = 0.3;
BIG = 300;
module lx(h) linear_extrude(h, convexity = 10) children();
module rounded_2d(r) hull() for (c = CORNERS) translate(c) circle(r = r);
module stadium_2d(a, b, r) hull() { translate(a) circle(r = r); translate(b) circle(r = r); }
// ---------- bottom shell --------------------------------------------------
module remote_bottom() render(convexity = 10) difference() {
union() {
difference() {
translate([0, 0, FLOOR_Z[0]]) lx(-FLOOR_Z[0]) rounded_2d(R_OUT);
translate([0, 0, FLOOR_Z[1]]) lx(BIG) rounded_2d(R_WALL);
translate([0, 0, LEDGE_Z]) lx(BIG) rounded_2d(R_LEDGE);
}
for (b = BOSSES) translate([b[0], b[1], FLOOR_Z[1] - 1]) cylinder(d = BOSS_D, h = BOSS_TOP - FLOOR_Z[1] + 1);
// blocks rising from the floor
translate([-10.5, 29, FLOOR_Z[1] - 1]) cube([21, 9, -17 - FLOOR_Z[1] + 1]);
translate([-4, 44, FLOOR_Z[1] - 1]) cube([8, 24, -17 - FLOOR_Z[1] + 1]);
for (m = [-1, 1]) translate([m * 27.9, -15, FLOOR_Z[1] - 1]) cylinder(d = 5, h = 4);
translate([88, -10, FLOOR_Z[1] - 1]) cube([12, 14, -28 - FLOOR_Z[1] + 1]); // port board block
// post up through the lid's slot, with a rounded lanyard loop on top
translate([0, 0, FLOOR_Z[1] - 1]) lx(-FLOOR_Z[1] + 1) stadium_2d(PILL[0], PILL[1], 4.75);
difference() {
intersection() {
hull() for (p = PILL) translate([p[0], p[1], 2.75]) sphere(r = 4.75);
translate([-10, 0, 0]) cube([20, 30, 10]);
}
hull() for (p = PILL) translate([-10, p[1], 2.75]) rotate([0, 90, 0]) cylinder(r = 2.75, h = 20);
}
}
for (b = BOSSES) translate([b[0], b[1], FLOOR_Z[0] - 1]) cylinder(d = BOSS_HOLE_D, h = BOSS_TOP - FLOOR_Z[0] + 2);
for (m = [-1, 1]) translate([m * 6.562, 35, FLOOR_Z[1]]) cylinder(d = 3, h = 30);
for (m = [-1, 1]) translate([m * 27.9, -15, FLOOR_Z[1]]) cylinder(d = 3, h = 10);
for (m = [-1, 1]) translate([0, 0, FLOOR_Z[0] - 1])
lx(5) stadium_2d([m * 23.9, -17], [m * 23.9, -13], 1);
translate([98, -10, -35]) cube([10, 14, 15]); // side port opening
for (v = [-7.5, 1.5]) translate([91, v, -37]) cylinder(d = 3, h = 10);
}
// ---------- lid -----------------------------------------------------------
module lid_plate_2d() difference() {
rounded_2d(R_LID);
for (s = STICKS) translate(s) {
circle(d = STICK_HOLE_D);
for (du = [-1, 1], dv = [-1, 1]) translate([du, dv] * STICK_SCREW) circle(d = 2);
}
stadium_2d(PILL[0], PILL[1], 5);
translate(SWITCH_SLOT[0]) square(SWITCH_SLOT[1] - SWITCH_SLOT[0]);
for (h = SWITCH_HOLES) translate(h) circle(d = 2);
translate(WINDOW[0]) offset(r = 0.5) offset(delta = -0.5) square(WINDOW[1] - WINDOW[0]);
}
module remote_lid() render(convexity = 10) difference() {
union() {
translate([0, 0, LID_Z[0]]) lx(LID_Z[1] - LID_Z[0]) lid_plate_2d();
for (b = BOSSES) translate([b[0], b[1], BOSS_TOP]) cylinder(d = BOSS_D, h = LID_Z[0] - BOSS_TOP + 1);
// window lip, left over from the original's display bezel
translate([0, 0, WINDOW_LIP[1]]) lx(WINDOW_LIP[2] - WINDOW_LIP[1]) difference() {
translate(WINDOW[0]) square(WINDOW[1] - WINDOW[0]);
translate(WINDOW[0] + [1, 1] * WINDOW_LIP[0]) square(WINDOW[1] - WINDOW[0] - [2, 2] * WINDOW_LIP[0]);
}
}
translate([0, 0, LID_Z[0] - 1]) lx(DISPLAY_POCKET_Z - LID_Z[0] + 1) difference() {
translate(DISPLAY_POCKET[0]) square(DISPLAY_POCKET[1] - DISPLAY_POCKET[0]);
for (p = DISPLAY_POSTS) translate(p) circle(d = 6);
}
for (p = DISPLAY_POSTS) translate([p[0], p[1], LID_Z[0] - 1]) cylinder(d = 2.5, h = 4);
for (b = BOSSES) translate([b[0], b[1], BOSS_TOP - 1]) cylinder(d = POST_HOLE_D, h = LID_Z[0] - BOSS_TOP + 1);
for (t = TEXT) translate([t[1][0], t[1][1], LID_Z[1] - ENGRAVE_D]) lx(ENGRAVE_D + 1)
text(t[0], size = 5.8, halign = "center", valign = "center", font = "Liberation Sans");
}
// ---------- reference parts -----------------------------------------------
// Nextion NX4832T035 (3.5" Basic): 100.5 x 54.94 mm board, M3 holes 3.15 /
// 3.22 mm from its edges = the lid's 94.2 x 48.5 post grid; LCD module
// approximate, its visible area (73.44 x 48.96) under the window.
module nextion_3_5() {
centre = (DISPLAY_POSTS[0] + DISPLAY_POSTS[3]) / 2;
translate([centre[0], centre[1], DISPLAY_POCKET_Z - 4.5 - 1.6]) {
color("#1e5a2a") difference() {
translate([-100.5/2, -54.94/2, 0]) cube([100.5, 54.94, 1.6]);
for (p = DISPLAY_POSTS) translate([p[0] - centre[0], p[1] - centre[1], -1]) cylinder(d = 3.2, h = 4);
}
}
win = (WINDOW[0] + WINDOW[1]) / 2;
color("#202020") translate([win[0] - 84/2, win[1] - 54/2, DISPLAY_POCKET_Z - 4.5]) cube([84, 54, 4.5]);
color("#3050a0") translate([win[0] - 73.44/2, win[1] - 48.96/2, DISPLAY_POCKET_Z - 0.2]) cube([73.44, 48.96, 0.2]);
}
// JH-D400X-R4 4-axis joystick (X/Y +-25 deg, twist +-45 deg, button),
// from its dimension drawing. Mounted with its dust cover on the lid's top
// face; the body hangs through the d40 opening. Local frame: stick axis on
// Z, Z = 0 on the dust cover's underside. Pots on +X and +Y.
JOY_COVER_D = 53.5;
JOY_COVER_T = 3; // not dimensioned; drawing proportions
JOY_BOX = 40.4; // control box, square
JOY_BOX_H = 25.5;
JOY_BELOW = 42; // box bottom to dust cover top
JOY_ABOVE = 52.5; // dust cover top to button top
JOY_POT_D = 16.5;
JOY_POT_L = (49.6 - 40.4);
module jh_d400x() {
top = JOY_COVER_T;
color("#202020") {
difference() {
cylinder(d = JOY_COVER_D, h = JOY_COVER_T);
for (du = [-1, 1], dv = [-1, 1]) translate([du * 16.25, dv * 16.25, -1]) cylinder(d = 2.7, h = 5);
}
translate([0, 0, top]) cylinder(d1 = 39.5, d2 = 30, h = 3); // dust ring / boot
translate([0, 0, top - JOY_BELOW]) translate([-JOY_BOX/2, -JOY_BOX/2, 0]) cube([JOY_BOX, JOY_BOX, JOY_BOX_H]);
translate([0, 0, top - JOY_BELOW + JOY_BOX_H]) cylinder(d = 34, h = JOY_BELOW - JOY_BOX_H - JOY_COVER_T + 0.01);
for (a = [0, 90]) rotate([0, 0, a])
translate([JOY_BOX/2, 0, top - JOY_BELOW + JOY_BOX_H/2]) rotate([0, 90, 0]) cylinder(d = JOY_POT_D, h = JOY_POT_L);
// handle: neck, knurled body, top with the button
translate([0, 0, top + 6]) cylinder(d = 6, h = 12);
translate([0, 0, top + 16]) cylinder(d = 22, h = 8);
translate([0, 0, top + 24]) cylinder(d = 35, h = 22);
translate([0, 0, top + 46]) cylinder(d1 = 35, d2 = 26, h = 5);
}
color("#c0c0c0") translate([0, 0, top + 51]) cylinder(d = 8, h = JOY_ABOVE - 51);
}
// 18650 UPS / boost board (5/9/12 V, Type-C charging), "SMD battery box"
// version: 89 x 22 mm board, holder on top, 22.4 mm overall, d3 holes.
// Placed on the bottom's two d5 standoffs at (+-27.9, -15); the board's real
// hole spacing isn't confirmed. The floor slots beside them take a strap.
UPS_CENTRE = [0, -15];
UPS_Z = -39; // standoff tops
module ups_18650() translate([UPS_CENTRE[0], UPS_CENTRE[1], UPS_Z]) {
color("#202020") difference() {
translate([-89/2, -22/2, 0]) cube([89, 22, 1.6]);
for (m = [-1, 1]) translate([m * 27.9, 0, -1]) cylinder(d = 3, h = 4);
}
color("#151515") translate([-77/2, -21/2, 1.6]) difference() {
cube([77, 21, 22.4 - 1.6]);
translate([3, 1.5, 3]) cube([71, 18, 30]);
}
color("#4060c0") translate([-65/2, 0, 1.6 + 3 + 18.5/2]) rotate([0, 90, 0]) cylinder(d = 18.5, h = 65);
color("#b0b0b0") translate([89/2 - 7, -4.5, 1.6]) cube([7.5, 9, 3.2]); // Type-C
}
// Small USB-C breakout (owner: glued in the holder at the side port), on the
// port block (u 88..99, v -10..4, top Z -28) with the receptacle facing the
// opening (v -10..4, Z -35..-20). Board size approximate.
module usb_c_breakout() translate([88, -10, -28]) {
color("#1a3a8a") cube([11, 14, 1.6]);
color("#c0c0c0") translate([11 - 7.3, 7 - 8.9/2, 1.6]) cube([7.5, 8.9, 3.2]);
}
// IMU (owner: on the big standoff in the middle), assumed a GY-521 / MPU6050
// like the robot's: 21.2 x 15.6 mm board on the 21 x 9 mm block (top Z -17),
// its hole end over the block's two d3 holes at v 35, board towards the centre.
module imu_gy521() translate([0, 35 + 3.5 - 15.6, -17]) {
color("#1f4fa0") translate([-21.2/2, 0, 0]) cube([21.2, 15.6, 1.6]);
color("#222") translate([-2, 7, 1.6]) cube([4, 4, 1]);
}
// Teensy 4.0 (owner; no mounting holes), assumed on the hole-less 8 x 24 mm
// block (u -4..4, v 44..68, top Z -17). Across the block by default; lengthwise
// it would end 0.1 mm from the shell wall.
TEENSY_LENGTHWISE = false;
module teensy_4_0() translate([0, 56, -17]) rotate([0, 0, TEENSY_LENGTHWISE ? 90 : 0]) {
color("#1e5a2a") translate([-35.6/2, -17.8/2, 0]) cube([35.6, 17.8, 1.6]);
color("#222") translate([-3, -5, 1.6]) cube([10, 10, 1.2]); // i.MX RT1062
color("#c0c0c0") translate([-35.6/2 - 1, -7.5/2, 1.6]) cube([5.7, 7.5, 2.9]); // micro-USB
}
module remote_assembly() {
if (SHOW_BOTTOM) color("#3a3a3a") remote_bottom();
if (SHOW_LID) translate([0, 0, EXPLODE]) color("#5a5a5a") remote_lid();
if (SHOW_DISPLAY) translate([0, 0, EXPLODE]) nextion_3_5();
if (SHOW_BATTERY) ups_18650();
if (SHOW_BATTERY) usb_c_breakout();
if (SHOW_BATTERY) imu_gy521();
if (SHOW_BATTERY) teensy_4_0();
// pots towards the nearest side wall and the near edge, where there's room
if (SHOW_STICKS) for (s = STICKS) translate([s[0], s[1], LID_Z[1] + EXPLODE])
mirror([s[0] < 0 ? 1 : 0, 0, 0]) jh_d400x();
}
remote_assembly();