381 lines
11 KiB
Python
381 lines
11 KiB
Python
"""juno humanoid: dexterous 5-digit hand (4 fingers + opposed thumb).
|
|
|
|
Local frame: origin = wrist-pitch joint center, fingers extend -Z, thumb on
|
|
the +X (forward) edge. RIGHT hand palm normal = +Y; LEFT is the XZ-mirror.
|
|
|
|
Locked interfaces:
|
|
- Clevis cheek plates normal to Y at y in [22,30] / [-30,-22], z 0..-44,
|
|
x -20..+24, aluminum boss disc dia 34 coaxial with Y on each outer face.
|
|
- Keep |y| < 21 clear for r < 26 around the Y axis near the origin
|
|
(wrist-pitch actuator dia 48 x len 40 lives there; owned by the forearm).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
import math # noqa: F401 (kept for tuning)
|
|
|
|
from build123d import (
|
|
Axis,
|
|
Box,
|
|
Cone,
|
|
Cylinder,
|
|
Plane,
|
|
Polyline,
|
|
Pos,
|
|
Rot,
|
|
Sphere,
|
|
chamfer,
|
|
extrude,
|
|
fillet,
|
|
make_face,
|
|
mirror,
|
|
)
|
|
|
|
from .juno_lib import (
|
|
ACCENT_COLOR,
|
|
ALU_COLOR,
|
|
RUBBER_COLOR,
|
|
SHELL_COLOR,
|
|
STRUCT_COLOR,
|
|
VISOR_COLOR,
|
|
part_compound,
|
|
styled,
|
|
)
|
|
|
|
# ---------------------------------------------------------------- helpers
|
|
|
|
|
|
def _safe_chamfer(part, edges, length, length2=None):
|
|
try:
|
|
edges = list(edges)
|
|
if not edges:
|
|
return part
|
|
if length2 is not None:
|
|
return chamfer(edges, length, length2)
|
|
return chamfer(edges, length)
|
|
except Exception:
|
|
return part
|
|
|
|
|
|
def _safe_fillet(part, edges, radius):
|
|
try:
|
|
edges = list(edges)
|
|
if not edges:
|
|
return part
|
|
return fillet(edges, radius)
|
|
except Exception:
|
|
return part
|
|
|
|
|
|
def _xcyl(r, h):
|
|
"""Cylinder along +X centered at origin."""
|
|
return Rot(0, 90, 0) * Cylinder(radius=r, height=h)
|
|
|
|
|
|
def _ycyl(r, h):
|
|
"""Cylinder along +Y centered at origin."""
|
|
return Rot(-90, 0, 0) * Cylinder(radius=r, height=h)
|
|
|
|
|
|
# ---------------------------------------------------------------- wrist clevis
|
|
|
|
CHEEK_X0, CHEEK_X1 = -20.0, 24.0
|
|
CHEEK_Z0, CHEEK_Z1 = 0.0, -44.0
|
|
LOBE_R = 24.0
|
|
|
|
|
|
def _wrist_clevis():
|
|
"""Twin cheek plates + pivot lobes + lower cross bridge (one solid)."""
|
|
cheeks = []
|
|
for ys in (1.0, -1.0):
|
|
yc = ys * 26.0
|
|
plate = Pos(
|
|
(CHEEK_X0 + CHEEK_X1) / 2.0, yc, (CHEEK_Z0 + CHEEK_Z1) / 2.0
|
|
) * Box(CHEEK_X1 - CHEEK_X0, 8.0, CHEEK_Z0 - CHEEK_Z1)
|
|
lobe = Pos(0, yc, 0) * _ycyl(LOBE_R, 8.0)
|
|
cheek = plate + lobe
|
|
# bevel the face outlines (machined plate look)
|
|
face_edges = []
|
|
for f in cheek.faces().filter_by(Axis.Y):
|
|
face_edges.extend(f.edges())
|
|
cheek = _safe_chamfer(cheek, face_edges, 1.2)
|
|
cheeks.append(cheek)
|
|
|
|
bridge = Pos(2.0, 0.0, -37.0) * Box(44.0, 44.0, 14.0)
|
|
bridge = _safe_chamfer(bridge, bridge.edges().filter_by(Axis.X), 2.0)
|
|
bridge = _safe_chamfer(bridge, bridge.edges().filter_by(Axis.Y), 2.0)
|
|
|
|
clevis = cheeks[0] + cheeks[1] + bridge
|
|
|
|
# shallow turned ring groove on each outer face, around the boss disc
|
|
for ys in (1.0, -1.0):
|
|
try:
|
|
ring = (Pos(0, ys * 30.0, 0) * _ycyl(21.0, 1.6)) - (
|
|
Pos(0, ys * 30.0, 0) * _ycyl(19.0, 3.0)
|
|
)
|
|
clevis -= ring
|
|
except Exception:
|
|
pass
|
|
return styled(clevis, "wrist_clevis", STRUCT_COLOR)
|
|
|
|
|
|
def _cheek_bolts():
|
|
out = []
|
|
i = 0
|
|
for ys in (1.0, -1.0):
|
|
for bx in (-13.0, 17.0):
|
|
bolt = Pos(bx, ys * 30.7, -37.0) * _ycyl(2.2, 1.4)
|
|
bolt = _safe_chamfer(bolt, bolt.edges(), 0.5)
|
|
out.append(styled(bolt, f"cheek_bolt_{i}", ALU_COLOR))
|
|
i += 1
|
|
return out
|
|
|
|
|
|
def _boss_discs():
|
|
out = []
|
|
for ys, tag in ((1.0, "l"), (-1.0, "r")):
|
|
boss = Pos(0, ys * 31.6, 0) * _ycyl(17.0, 3.2)
|
|
boss = _safe_chamfer(boss, boss.edges(), 0.8)
|
|
out.append(styled(boss, f"wrist_boss_{tag}", ALU_COLOR))
|
|
return out
|
|
|
|
|
|
# ---------------------------------------------------------------- palm
|
|
|
|
KNUCKLE_Z = -112.0
|
|
KNUCKLE_Y = -2.0
|
|
KNUCKLE_XS = (-27.0, -9.0, 9.0, 27.0)
|
|
|
|
# thumb CMC frame (root on the +X palm edge)
|
|
THUMB_ROOT = (39.5, 2.0, -60.0)
|
|
T_CMC = Pos(*THUMB_ROOT) * Rot(0, -28, 0) * Rot(0, 0, 38) # hinge axis = local X
|
|
T_SEG1 = T_CMC * Rot(12, 0, 0) # CMC curl
|
|
T_SEG2 = T_SEG1 * Pos(0, 0, -32.0) * Rot(30, 0, 0) # IP curl
|
|
|
|
|
|
def _palm_outline_face(pts, y):
|
|
pts3 = [(x, y, z) for x, z in pts]
|
|
wire = Polyline(*pts3, close=True)
|
|
return make_face(wire)
|
|
|
|
|
|
def _palm_core():
|
|
pts = [
|
|
(-20.0, -44.0),
|
|
(24.0, -44.0),
|
|
(44.0, -58.0),
|
|
(44.0, -96.0),
|
|
(36.0, -108.0),
|
|
(-36.0, -108.0),
|
|
]
|
|
core = extrude(_palm_outline_face(pts, -16.0), amount=34.0, dir=(0, 1, 0))
|
|
core = _safe_fillet(core, core.edges().filter_by(Axis.Y), 4.0)
|
|
|
|
# raked palm-side bottom edge over the knuckle line
|
|
rake = [
|
|
e
|
|
for e in core.edges()
|
|
if e.center().Y > 16.5 and e.center().Z < -107.0
|
|
]
|
|
core = _safe_chamfer(core, rake, 5.0)
|
|
|
|
# knuckle scallops (clearance arches over the proximal barrels)
|
|
for kx in KNUCKLE_XS:
|
|
core -= Pos(kx, KNUCKLE_Y, KNUCKLE_Z) * _xcyl(8.0, 15.5)
|
|
|
|
# thumb CMC pocket + diagonal thumb relief groove
|
|
core -= T_CMC * _xcyl(9.5, 30.0)
|
|
core -= T_SEG1 * Pos(0, 0, -14.0) * Rot(0, 90, 0) * Cylinder(
|
|
radius=11.2, height=60.0
|
|
)
|
|
|
|
# palm sensor recess
|
|
core -= Pos(2.0, 17.4, -80.0) * _ycyl(8.3, 3.2)
|
|
|
|
return styled(core, "palm_core", STRUCT_COLOR)
|
|
|
|
|
|
def _dorsal_cover():
|
|
pts = [
|
|
(-18.0, -46.0),
|
|
(22.0, -46.0),
|
|
(42.0, -59.0),
|
|
(42.0, -95.0),
|
|
(34.5, -106.0),
|
|
(-34.0, -106.0),
|
|
]
|
|
cover = extrude(_palm_outline_face(pts, -20.0), amount=4.0, dir=(0, 1, 0))
|
|
cover = _safe_fillet(cover, cover.edges().filter_by(Axis.Y), 4.0)
|
|
try:
|
|
outer = cover.faces().sort_by(Axis.Y)[0]
|
|
cover = _safe_chamfer(cover, outer.edges(), 1.2)
|
|
except Exception:
|
|
pass
|
|
# shallow panel-line grooves on the outer face
|
|
for gz in (-64.0, -69.0):
|
|
try:
|
|
cover -= Pos(2.0, -20.0, gz) * Box(52.0, 1.2, 1.5)
|
|
except Exception:
|
|
pass
|
|
return styled(cover, "dorsal_cover", SHELL_COLOR)
|
|
|
|
|
|
def _palm_sensor():
|
|
ring_raw = Cylinder(radius=8.1, height=1.2) - Cylinder(radius=6.3, height=4.0)
|
|
ring = Pos(2.0, 16.5, -80.0) * Rot(-90, 0, 0) * ring_raw
|
|
lens = Pos(2.0, 16.7, -80.0) * _ycyl(6.1, 1.6)
|
|
return [
|
|
styled(ring, "palm_sensor_ring", ALU_COLOR),
|
|
styled(lens, "palm_sensor_lens", VISOR_COLOR),
|
|
]
|
|
|
|
|
|
# ---------------------------------------------------------------- digits
|
|
|
|
|
|
def _link(length, width, r_a, r_b, shaft_w, shaft_d, slot_w, bore_r):
|
|
"""Phalanx link: barrel at z=0, shaft, forked barrel at z=-length."""
|
|
barrel_a = _xcyl(r_a, width)
|
|
shaft_h = length - 7.0
|
|
shaft = Pos(0, 0, -(4.0 + shaft_h / 2.0)) * Box(shaft_w, shaft_d, shaft_h)
|
|
barrel_b = Pos(0, 0, -length) * _xcyl(r_b, width)
|
|
link = barrel_a + shaft + barrel_b
|
|
link = _safe_chamfer(link, link.edges().filter_by(Axis.Z), 1.2)
|
|
# fork slot for the next segment
|
|
link -= Pos(0, 0, -(length + 0.5)) * Box(slot_w, 40.0, 15.0)
|
|
# pin bores
|
|
link -= _xcyl(bore_r, width + 8.0)
|
|
link -= Pos(0, 0, -length) * _xcyl(bore_r, width + 8.0)
|
|
return link
|
|
|
|
|
|
def _tip_segment(r_barrel, w_barrel, r_top, r_tip, tip_z, cone_top_z, pad_c, pad_r, bore_r):
|
|
"""Distal segment: hinge barrel + tapered body + rounded tip.
|
|
|
|
Returns (body, pad) where pad is a rubber fingertip cap split off the
|
|
body by a boolean intersection (face-to-face, no overlap), or None.
|
|
"""
|
|
barrel = _xcyl(r_barrel, w_barrel)
|
|
cone_h = cone_top_z - tip_z
|
|
cone = Pos(0, 0, (cone_top_z + tip_z) / 2.0) * Cone(
|
|
bottom_radius=r_tip, top_radius=r_top, height=cone_h
|
|
)
|
|
tip = Pos(0, 0, tip_z) * Sphere(r_tip)
|
|
body = barrel + cone + tip
|
|
body -= _xcyl(bore_r, w_barrel + 8.0)
|
|
pad = None
|
|
try:
|
|
zone = Pos(*pad_c) * Sphere(pad_r)
|
|
cap = body & zone
|
|
if cap.volume > 1.0:
|
|
body = body - zone
|
|
pad = cap
|
|
except Exception:
|
|
pad = None
|
|
return body, pad
|
|
|
|
|
|
def _pin(r, h):
|
|
pin = _xcyl(r, h)
|
|
return _safe_chamfer(pin, pin.edges(), 0.7)
|
|
|
|
|
|
def _finger(idx, kx, curl1=21.0, curl2=25.0):
|
|
t1 = Pos(kx, KNUCKLE_Y, KNUCKLE_Z) * Rot(curl1, 0, 0)
|
|
t2 = t1 * Pos(0, 0, -36.0) * Rot(curl2, 0, 0)
|
|
|
|
prox = _link(
|
|
length=36.0, width=15.0, r_a=7.5, r_b=7.0,
|
|
shaft_w=13.0, shaft_d=13.5, slot_w=9.5, bore_r=3.9,
|
|
)
|
|
body, pad = _tip_segment(
|
|
r_barrel=6.6, w_barrel=9.0, r_top=6.2, r_tip=4.6,
|
|
tip_z=-25.0, cone_top_z=-4.0,
|
|
pad_c=(0, 1.9, -24.5), pad_r=5.5, bore_r=3.9,
|
|
)
|
|
|
|
solids = [
|
|
styled(t1 * prox, f"finger{idx}_proximal", STRUCT_COLOR),
|
|
styled(t2 * body, f"finger{idx}_distal", STRUCT_COLOR),
|
|
styled(Pos(kx, KNUCKLE_Y, KNUCKLE_Z) * _pin(3.7, 17.5),
|
|
f"finger{idx}_pin_mcp", ALU_COLOR),
|
|
styled(t1 * Pos(0, 0, -36.0) * _pin(3.7, 17.5),
|
|
f"finger{idx}_pin_pip", ALU_COLOR),
|
|
]
|
|
if pad is not None:
|
|
solids.append(styled(t2 * pad, f"finger{idx}_tip_pad", ACCENT_COLOR))
|
|
return solids
|
|
|
|
|
|
def _thumb():
|
|
seg1 = _link(
|
|
length=32.0, width=15.0, r_a=8.5, r_b=7.5,
|
|
shaft_w=13.0, shaft_d=13.0, slot_w=9.7, bore_r=4.2,
|
|
)
|
|
body, pad = _tip_segment(
|
|
r_barrel=6.7, w_barrel=9.2, r_top=6.2, r_tip=4.9,
|
|
tip_z=-21.5, cone_top_z=-5.0,
|
|
pad_c=(0, 1.8, -21.0), pad_r=5.7, bore_r=3.9,
|
|
)
|
|
solids = [
|
|
styled(T_SEG1 * seg1, "thumb_proximal", STRUCT_COLOR),
|
|
styled(T_SEG2 * body, "thumb_distal", STRUCT_COLOR),
|
|
styled(T_CMC * _pin(4.0, 17.0), "thumb_pin_cmc", ALU_COLOR),
|
|
styled(T_SEG1 * Pos(0, 0, -32.0) * _pin(3.7, 16.0),
|
|
"thumb_pin_ip", ALU_COLOR),
|
|
]
|
|
if pad is not None:
|
|
solids.append(styled(T_SEG2 * pad, "thumb_tip_pad", ACCENT_COLOR))
|
|
return solids
|
|
|
|
|
|
# ---------------------------------------------------------------- assembly
|
|
|
|
|
|
def _right_solids():
|
|
solids = [_wrist_clevis()]
|
|
solids += _boss_discs()
|
|
solids += _cheek_bolts()
|
|
solids.append(_palm_core())
|
|
solids.append(_dorsal_cover())
|
|
solids += _palm_sensor()
|
|
for i, kx in enumerate(KNUCKLE_XS):
|
|
solids += _finger(i, kx)
|
|
solids += _thumb()
|
|
return solids
|
|
|
|
|
|
def _fallback_solids():
|
|
blk = Pos(4, 0, -100) * Box(80, 36, 200)
|
|
return [styled(blk, "hand_fallback", STRUCT_COLOR)]
|
|
|
|
|
|
def build_hand(side: str):
|
|
try:
|
|
solids = _right_solids()
|
|
if side == "left":
|
|
mirrored = []
|
|
for s in solids:
|
|
lbl, col = s.label, s.color
|
|
m = mirror(s, about=Plane.XZ)
|
|
mirrored.append(styled(m, lbl, col))
|
|
solids = mirrored
|
|
for s in solids:
|
|
s.label = f"{side}_{s.label}"
|
|
except Exception:
|
|
solids = _fallback_solids()
|
|
return part_compound(f"hand_{side}", solids)
|
|
|
|
|
|
def gen_step():
|
|
right = build_hand("right")
|
|
left = build_hand("left")
|
|
right.location = Pos(0, -125, 0)
|
|
left.location = Pos(0, 125, 0)
|
|
return part_compound("hand_demo", [right, left])
|