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

@@ -0,0 +1,109 @@
"""Export the OpenSCAD robot as meshes for the Unity simulation.
python export_unity.py (from hardware/openscad, needs OpenSCAD)
Writes Hexapod_Simulation_Unity/Assets/HexapodSim/Resources/OpenScadModel/*.bytes,
loaded by Assets/HexapodSim/OpenScadModel.cs. Every file is one rig segment
in the robot frame (mm, +X forward, +Y left, +Z up), relative to the
segment's pivot, in the rest pose that matches the rig's zero rotations
(coxa 0, femur 0, tibia -90):
body Z = 0 on the joint plane (13.5 mm above the bottom plate)
leg_<side>_* left = as leg 4 (mount +90), right = its mirror (mount -90)
File format (little endian): b"HXM1", int32 groups, then per group
float32 r, g, b, a, int32 triangles, triangles * 9 float32 (x, y, z).
Triangles wind counter-clockwise seen from outside (right-handed).
"""
import os, re, struct, subprocess, sys, tempfile
HERE = os.path.dirname(os.path.abspath(__file__)).replace("\\", "/")
OUT = os.path.normpath(os.path.join(HERE, "..", "..", "Hexapod_Simulation_Unity", "Assets",
"HexapodSim", "Resources", "OpenScadModel"))
OPENSCAD = os.environ.get("OPENSCAD", r"C:/Program Files/OpenSCAD/openscad.exe")
HEADER = f"use <{HERE}/body.scad>\nuse <{HERE}/leg.scad>\n"
JOINT_PLANE_Z = 13.5
def hexcol(h, a=1.0):
h = h.lstrip("#")
return [int(h[i:i + 2], 16) / 255 for i in (0, 2, 4)] + [a]
# segment -> list of (colour, OpenSCAD geometry in the segment's frame)
BODY = [
("#3a3a3a", f"translate([0, 0, -{JOINT_PLANE_Z}]) fusion_to_robot() bottom_half();"),
("#555555", f"translate([0, 0, -{JOINT_PLANE_Z}]) fusion_to_robot() top_half();"),
("#2a2a2a", f"translate([0, 0, -{JOINT_PLANE_Z}]) fusion_to_robot() body_servos_uv();"),
("#2b6b3a", f"translate([0, 0, -{JOINT_PLANE_Z}]) pcb_in_body();"),
]
LEFT = "rotate([0, 0, 90]) " # leg frame -> robot frame for the left legs (mount +90)
LEG = {
"coxa": [
("#d9822b", LEFT + "{ coxa_bracket_upper(); coxa_bracket_lower(); }"),
("#bdbdbd", LEFT + "translate([55, 0, 0]) along_y(28.7) horn_adapter();"),
("#5a3a1e", LEFT + "coxa_cables();"),
],
"femur": [
("#4a7fbf", LEFT + "{ link_channel(); link_lid(); }"),
("#2a2a2a", LEFT + "link_servos();"),
("#5a3a1e", LEFT + "femur_cables();"),
],
"tibia": [
("#5fa35f", LEFT + "{ tibia_bearing_half(); tibia_horn_half(); }"),
("#bdbdbd", LEFT + "along_y(28.7) horn_adapter();"),
("#1c1c1c", LEFT + "foot_sock();"),
],
}
def render(geometry, tmp):
scad = os.path.join(tmp, "part.scad"); stl = os.path.join(tmp, "part.stl")
with open(scad, "w") as f:
f.write(HEADER + geometry + "\n")
r = subprocess.run([OPENSCAD, "-o", stl, scad], capture_output=True, text=True)
if r.returncode != 0 or not os.path.exists(stl):
sys.exit(f"OpenSCAD failed on: {geometry}\n{r.stderr}")
tris = []
num = r"([-+0-9.eE]+)"
for m in re.finditer(r"outer loop\s+vertex\s+%s\s+%s\s+%s\s+vertex\s+%s\s+%s\s+%s\s+vertex\s+%s\s+%s\s+%s" % ((num,) * 9),
open(stl).read()):
tris.append([float(v) for v in m.groups()])
os.remove(stl)
return tris
def mirror_y(tris):
# (x, y, z) -> (x, -y, z) and swap two vertices to keep outward winding
out = []
for t in tris:
a, b, c = t[0:3], t[3:6], t[6:9]
a, b, c = [a[0], -a[1], a[2]], [b[0], -b[1], b[2]], [c[0], -c[1], c[2]]
out.append(a + c + b)
return out
def write(name, groups):
path = os.path.join(OUT, name + ".bytes")
with open(path, "wb") as f:
f.write(b"HXM1" + struct.pack("<i", len(groups)))
for col, tris in groups:
f.write(struct.pack("<4f", *col) + struct.pack("<i", len(tris)))
for t in tris:
f.write(struct.pack("<9f", *t))
print(f" {name}: {sum(len(t) for _, t in groups)} triangles")
def main():
os.makedirs(OUT, exist_ok=True)
with tempfile.TemporaryDirectory() as tmp:
print("body")
write("body", [(hexcol(c), render(g, tmp)) for c, g in BODY])
for seg, parts in LEG.items():
print("leg", seg)
groups = [(hexcol(c), render(g, tmp)) for c, g in parts]
write(f"leg_left_{seg}", groups)
write(f"leg_right_{seg}", [(c, mirror_y(t)) for c, t in groups])
if __name__ == "__main__":
main()