Files
cadSet/text-to-cad/models/robots/lyra/lyra_parts/digits.py
T
2026-07-22 19:38:36 +08:00

423 lines
14 KiB
Python

"""lyra digit links: finger phalanges and the thumb column.
Local frames (see lib.py):
- Finger phalanges: origin at the proximal joint center, +Z distal along
the segment, +Y palmar, flexion axis -X.
- Thumb segments: origin at the proximal joint center, +X distal along the
column, +Z palmar pad direction, flexion axis -Y (the thumb_base yaw
turret tang spins about +Z).
Each phalanx is a pearl composite shell TUBE over an exposed graphite
structural spine: graphite tang barrel -> center spine bar -> fork
crossbar -> clevis cheeks, one connected solid, with the spine visible at
both joint ends (the "visible precision joints" design language). The
soft-touch tactile strip rides the tube's palmar face.
Articulation-safety rules (hold for every joint at any angle in range):
- Full-width child geometry (the shell tube) starts beyond parent lobe
radius + 0.4 from the joint axis; only the circular tang barrel and the
narrow center spine (which never shares an x/y band with the parent
cheeks, and stays clear of the pin bore radius) sit closer.
- Fork crossbars sit >= 9 mm proximal of their own joint axis, outside the
+/-43 deg corner sweep of the child spine at full flexion.
Joint hardware ownership: the parent link owns clevis cheeks, aluminum rim
washers, and the pivot pin; the child owns the bored tang barrel.
"""
from __future__ import annotations
from build123d import Axis, Box, Cone, Pos, Rot, Sphere
from .chain import (
SEGMENT_MM,
THUMB_METACARPAL_LEN_MM,
THUMB_PROXIMAL_LEN_MM,
THUMB_BASE_LEN_MM,
)
from .lib import (
ALU_COLOR,
GRAPHITE_COLOR,
PAD_COLOR,
PEARL_COLOR,
part_compound,
pin,
safe_chamfer,
safe_fillet,
styled,
xcyl,
ycyl,
zcyl,
)
# Per-finger width scale (lengths come from chain.SEGMENT_MM).
FINGER_SCALE = {"index": 1.0, "middle": 1.03, "ring": 0.97, "pinky": 0.88}
# Knuckle hardware (mm at scale 1). Tang widths match the parent fork gap
# minus 0.6 clearance; bores give the parent-owned pin a 0.4 running fit.
PALM_LOBE_R = 8.0 # palm knuckle lobe radius (palm.py uses this too)
PROX_TANG_R, PROX_TANG_W, PROX_BORE_R = 7.2, 10.4, 3.0
PROX_FORK_GAP, PROX_CHEEK_T, PROX_LOBE_R = 9.6, 2.6, 6.5
PROX_PIN_R, PROX_RIM_R = 2.4, 4.4
MID_TANG_R, MID_TANG_W, MID_BORE_R = 6.4, 9.0, 2.8
MID_FORK_GAP, MID_CHEEK_T, MID_LOBE_R = 8.4, 2.4, 5.8
MID_PIN_R, MID_RIM_R = 2.1, 3.8
DIST_TANG_R, DIST_TANG_W, DIST_BORE_R = 5.6, 7.8, 2.5
MCP_PIN_R = 2.6 # palm-owned; phalanx bore must clear it
RIM_T = 1.2
def _bored_xbarrel(r, w, bore_r):
return xcyl(r, w) - xcyl(bore_r, w + 4.0)
def _bored_ybarrel(r, w, bore_r):
return ycyl(r, w) - ycyl(bore_r, w + 4.0)
def _finger_fork(z, gap, cheek_t, lobe_r, pin_r, rim_r, body_d):
"""Distal clevis of a finger phalanx: cheeks + lobes (one solid, bored),
rim washers, and the pivot pin."""
half = gap / 2.0
fork = None
for sx in (1.0, -1.0):
xc = sx * (half + cheek_t / 2.0)
plate = Pos(xc, 0.2, z - 6.0) * Box(cheek_t, body_d, 12.0)
lobe = Pos(xc, 0, z) * xcyl(lobe_r, cheek_t)
cheek = plate + lobe
fork = cheek if fork is None else fork + cheek
fork = safe_chamfer(
fork, [e for e in fork.edges().filter_by(Axis.X)], 0.8
)
fork -= Pos(0, 0, z) * xcyl(pin_r + 0.4, gap + 2 * cheek_t + 8.0)
rims = []
for sx in (1.0, -1.0):
xr = sx * (half + cheek_t + RIM_T / 2.0)
rim = Pos(xr, 0, z) * (
xcyl(rim_r, RIM_T) - xcyl(pin_r + 0.4, RIM_T + 2.0)
)
rims.append(rim)
pin_len = gap + 2 * cheek_t + 2 * RIM_T + 1.2
pivot = Pos(0, 0, z) * pin("x", pin_r, pin_len)
return fork, rims, pivot
def _finger_phalanx(
label_prefix,
pin_name,
s,
length,
tang_r,
tang_w,
bore_r,
fork_gap,
cheek_t,
lobe_r,
pin_r,
rim_r,
parent_lobe_r,
body_w,
body_d,
y_shift,
):
"""Pearl shell tube over an exposed graphite spine, plus fork hardware."""
tube_z0 = parent_lobe_r + 0.4
tube_z1 = length - 13.2 * s
bar_w = 6.5 * s
chan_w = bar_w + 0.5
# Graphite spine: tang barrel + center bar + fork crossbar + clevis.
core = _bored_xbarrel(tang_r, tang_w, bore_r)
bar = Pos(0, y_shift, (4.5 * s + length - 9.0 * s) / 2.0) * Box(
bar_w, bar_w, length - 9.0 * s - 4.5 * s
)
core += bar
crossbar = Pos(0, 0.2, length - 11.0 * s) * Box(
fork_gap + 2 * cheek_t, bar_w, 4.0 * s
)
core += crossbar
fork, rims, pivot = _finger_fork(
length, fork_gap, cheek_t, lobe_r, pin_r, rim_r, body_d
)
core += fork
# Pearl composite shell tube around the spine.
tube = Pos(0, y_shift, (tube_z0 + tube_z1) / 2.0) * Box(
body_w, body_d, tube_z1 - tube_z0
)
tube = safe_fillet(tube, tube.edges().filter_by(Axis.Z), min(2.6, body_w / 4.5))
tube -= Pos(0, y_shift, (tube_z0 + tube_z1) / 2.0) * Box(
chan_w, chan_w, tube_z1 - tube_z0 + 4.0
)
tube = safe_chamfer(
tube, [e for e in tube.edges().filter_by(Axis.X)], 0.7
)
# Soft-touch tactile strip on the palmar tube face.
pad = Pos(0, y_shift + body_d / 2.0 + 0.5, (tube_z0 + tube_z1) / 2.0) * Box(
body_w - 4.0 * s, 1.6, max((tube_z1 - tube_z0) * 0.72, 2.5)
)
pad = safe_fillet(pad, pad.edges().filter_by(Axis.Y), min(1.8 * s, 2.0))
solids = [
styled(core, f"{label_prefix}_spine", GRAPHITE_COLOR),
styled(tube, f"{label_prefix}_shell", PEARL_COLOR),
styled(pad, f"{label_prefix}_pad", PAD_COLOR),
styled(pivot, f"{pin_name}_pin", ALU_COLOR),
]
for i, rim in enumerate(rims):
solids.append(styled(rim, f"{pin_name}_rim_{i}", ALU_COLOR))
return solids
def build_finger_proximal(finger: str):
s = FINGER_SCALE[finger]
solids = _finger_phalanx(
f"{finger}_proximal", f"{finger}_pip", s, SEGMENT_MM[finger][0],
PROX_TANG_R * s, PROX_TANG_W * s, PROX_BORE_R * s,
PROX_FORK_GAP * s, PROX_CHEEK_T * s, PROX_LOBE_R * s,
PROX_PIN_R * s, PROX_RIM_R * s, PALM_LOBE_R * s,
13.4 * s, 11.4 * s, 0.7 * s,
)
return part_compound(f"{finger}_proximal", solids)
def build_finger_middle(finger: str):
s = FINGER_SCALE[finger]
solids = _finger_phalanx(
f"{finger}_middle", f"{finger}_dip", s, SEGMENT_MM[finger][1],
MID_TANG_R * s, MID_TANG_W * s, MID_BORE_R * s,
MID_FORK_GAP * s, MID_CHEEK_T * s, MID_LOBE_R * s,
MID_PIN_R * s, MID_RIM_R * s, PROX_LOBE_R * s,
12.2 * s, 10.6 * s, 0.6 * s,
)
return part_compound(f"{finger}_middle", solids)
def build_finger_distal(finger: str):
s = FINGER_SCALE[finger]
length = SEGMENT_MM[finger][2]
tip_r = 4.4 * s
cone_z0 = MID_LOBE_R * s + 0.4
tip_c = length - tip_r
# Tang + connection collar (graphite, one solid). The collar is
# center-band only, so it clears the parent pin bore and never shares
# an x band with the parent cheeks.
tang = _bored_xbarrel(DIST_TANG_R * s, DIST_TANG_W * s, DIST_BORE_R * s)
collar_r = 4.0 * s
collar_len = cone_z0 + 0.5 - 4.4
tang += Pos(0, 0.4 * s, 4.4 + collar_len / 2.0) * zcyl(collar_r, collar_len)
cone_h = tip_c - cone_z0
body = Pos(0, 0.4 * s, cone_z0 + cone_h / 2.0) * Cone(
bottom_radius=5.3 * s, top_radius=tip_r, height=cone_h
)
body += Pos(0, 0.4 * s, tip_c) * Sphere(tip_r)
# Seat the pearl body on the collar shoulder (face contact, no overlap).
body -= Pos(0, 0.4 * s, (cone_z0 + 0.5) / 2.0) * zcyl(collar_r + 0.05, cone_z0 + 0.5)
# Soft-touch fingertip pad: split a palmar cap off the body with a
# boolean sphere so the seam is face-to-face with no overlap.
# Palmar-only soft cap: the zone center sits palmar of the tip sphere so
# the apex and dorsal face stay pearl.
pad = None
try:
zone = Pos(0, 3.6 * s, tip_c + 0.2) * Sphere(4.7 * s)
cap = body & zone
if cap.volume > 1.0:
body = body - zone
pad = cap
except Exception:
pad = None
solids = [
styled(tang, f"{finger}_distal_tang", GRAPHITE_COLOR),
styled(body, f"{finger}_distal_body", PEARL_COLOR),
]
if pad is not None:
solids.append(styled(pad, f"{finger}_tip_pad", PAD_COLOR))
return part_compound(f"{finger}_distal", solids)
# ----------------------------------------------------------------- thumb
T_PROX_LOBE_R = 7.0 # thumb_base flex-fork lobe radius (palm relief uses it)
TB_TANG_R, TB_TANG_H, TB_BORE_R = 8.8, 9.5, 3.3
TM_TANG_R, TM_TANG_W, TM_BORE_R = 6.8, 9.8, 2.9
TM_FORK_GAP, TM_CHEEK_T, TM_LOBE_R = 9.8, 2.5, 6.4
TM_PIN_R, TM_RIM_R = 2.3, 4.2
TP_TANG_R, TP_TANG_W, TP_BORE_R = 6.2, 9.2, 2.7
TP_FORK_GAP, TP_CHEEK_T, TP_LOBE_R = 8.6, 2.4, 5.6
TP_PIN_R, TP_RIM_R = 2.1, 3.7
TD_TANG_R, TD_TANG_W, TD_BORE_R = 5.2, 8.0, 2.5
THUMB_TIP_R = 4.8
THUMB_TIP_TOTAL_MM = 24.0 # matches chain.THUMB_TIP_LEN_MM
def _thumb_fork(x, gap, cheek_t, lobe_r, pin_r, rim_r, body_d):
"""Distal clevis of a thumb segment: cheeks normal to Y."""
half = gap / 2.0
fork = None
for sy in (1.0, -1.0):
yc = sy * (half + cheek_t / 2.0)
plate = Pos(x - 6.0, yc, 0.2) * Box(12.0, cheek_t, body_d)
lobe = Pos(x, yc, 0) * ycyl(lobe_r, cheek_t)
cheek = plate + lobe
fork = cheek if fork is None else fork + cheek
fork = safe_chamfer(
fork, [e for e in fork.edges().filter_by(Axis.Y)], 0.8
)
fork -= Pos(x, 0, 0) * ycyl(pin_r + 0.4, gap + 2 * cheek_t + 8.0)
rims = []
for sy in (1.0, -1.0):
yr = sy * (half + cheek_t + RIM_T / 2.0)
rim = Pos(x, yr, 0) * (
ycyl(rim_r, RIM_T) - ycyl(pin_r + 0.4, RIM_T + 2.0)
)
rims.append(rim)
pin_len = gap + 2 * cheek_t + 2 * RIM_T + 1.2
pivot = Pos(x, 0, 0) * pin("y", pin_r, pin_len)
return fork, rims, pivot
def _thumb_phalanx(
label_prefix,
pin_name,
length,
tang_r,
tang_w,
bore_r,
fork_gap,
cheek_t,
lobe_r,
pin_r,
rim_r,
parent_lobe_r,
body_w,
body_d,
z_shift,
):
"""Thumb segment along +X: pearl tube over graphite spine, Y-axis forks."""
tube_x0 = parent_lobe_r + 0.4
tube_x1 = length - 13.2
bar_w = 6.5
chan_w = bar_w + 0.5
core = _bored_ybarrel(tang_r, tang_w, bore_r)
bar = Pos((4.5 + length - 9.0) / 2.0, 0, z_shift) * Box(
length - 9.0 - 4.5, bar_w, bar_w
)
core += bar
# Fork cheeks are stacked in Y, so the crossbar spans the Y width.
crossbar = Pos(length - 11.0, 0, 0.2) * Box(4.0, fork_gap + 2 * cheek_t, bar_w)
core += crossbar
fork, rims, pivot = _thumb_fork(
length, fork_gap, cheek_t, lobe_r, pin_r, rim_r, body_d
)
core += fork
tube = Pos((tube_x0 + tube_x1) / 2.0, 0, z_shift) * Box(
tube_x1 - tube_x0, body_w, body_d
)
tube = safe_fillet(tube, tube.edges().filter_by(Axis.X), min(2.6, body_w / 4.5))
tube -= Pos((tube_x0 + tube_x1) / 2.0, 0, z_shift) * Box(
tube_x1 - tube_x0 + 4.0, chan_w, chan_w
)
tube = safe_chamfer(
tube, [e for e in tube.edges().filter_by(Axis.Y)], 0.7
)
pad = Pos((tube_x0 + tube_x1) / 2.0, 0, z_shift + body_d / 2.0 + 0.5) * Box(
max((tube_x1 - tube_x0) * 0.72, 2.5), body_w - 4.0, 1.6
)
pad = safe_fillet(pad, pad.edges().filter_by(Axis.Z), 1.8)
solids = [
styled(core, f"{label_prefix}_spine", GRAPHITE_COLOR),
styled(tube, f"{label_prefix}_shell", PEARL_COLOR),
styled(pad, f"{label_prefix}_pad", PAD_COLOR),
styled(pivot, f"{pin_name}_pin", ALU_COLOR),
]
for i, rim in enumerate(rims):
solids.append(styled(rim, f"{pin_name}_rim_{i}", ALU_COLOR))
return solids
def build_thumb_base():
"""CMC turret tang (spins about +Z on the palm spindle) + flex clevis."""
disc = zcyl(TB_TANG_R, TB_TANG_H) - zcyl(TB_BORE_R, TB_TANG_H + 4.0)
disc = safe_chamfer(disc, disc.edges(), 0.8)
neck = Pos(8.6, 0, 0) * Box(8.6, 12.6, 9.0)
neck = safe_fillet(neck, neck.edges().filter_by(Axis.Z), 2.0)
core = disc + neck
fork, rims, pivot = _thumb_fork(
THUMB_BASE_LEN_MM, TM_TANG_W + 0.6, 2.6, T_PROX_LOBE_R, 2.5, 4.5, 11.0
)
core += fork
solids = [
styled(core, "thumb_base_link", GRAPHITE_COLOR),
styled(pivot, "thumb_cmc_flex_pin", ALU_COLOR),
]
for i, rim in enumerate(rims):
solids.append(styled(rim, f"thumb_cmc_flex_rim_{i}", ALU_COLOR))
return part_compound("thumb_base", solids)
def build_thumb_metacarpal():
solids = _thumb_phalanx(
"thumb_metacarpal", "thumb_mp", THUMB_METACARPAL_LEN_MM,
TM_TANG_R, TM_TANG_W, TM_BORE_R,
TM_FORK_GAP, TM_CHEEK_T, TM_LOBE_R, TM_PIN_R, TM_RIM_R,
T_PROX_LOBE_R, 12.6, 13.0, 0.5,
)
return part_compound("thumb_metacarpal", solids)
def build_thumb_proximal():
solids = _thumb_phalanx(
"thumb_proximal", "thumb_ip", THUMB_PROXIMAL_LEN_MM,
TP_TANG_R, TP_TANG_W, TP_BORE_R,
TP_FORK_GAP, TP_CHEEK_T, TP_LOBE_R, TP_PIN_R, TP_RIM_R,
TM_LOBE_R, 11.6, 11.8, 0.4,
)
return part_compound("thumb_proximal", solids)
def build_thumb_distal():
length = THUMB_TIP_TOTAL_MM
tip_r = THUMB_TIP_R
cone_x0 = TP_LOBE_R + 0.4
tip_c = length - tip_r
tang = _bored_ybarrel(TD_TANG_R, TD_TANG_W, TD_BORE_R)
collar_r = 4.0
collar_len = cone_x0 + 0.5 - 4.4
tang += Pos(4.4 + collar_len / 2.0, 0, 0.4) * xcyl(collar_r, collar_len)
cone_h = tip_c - cone_x0
body = Pos(cone_x0 + cone_h / 2.0, 0, 0.4) * Rot(0, 90, 0) * Cone(
bottom_radius=5.4, top_radius=tip_r, height=cone_h
)
body += Pos(tip_c, 0, 0.4) * Sphere(tip_r)
body -= Pos((cone_x0 + 0.5) / 2.0, 0, 0.4) * xcyl(collar_r + 0.05, cone_x0 + 0.5)
pad = None
try:
zone = Pos(tip_c + 0.2, 0, 3.4) * Sphere(5.0)
cap = body & zone
if cap.volume > 1.0:
body = body - zone
pad = cap
except Exception:
pad = None
solids = [
styled(tang, "thumb_distal_tang", GRAPHITE_COLOR),
styled(body, "thumb_distal_body", PEARL_COLOR),
]
if pad is not None:
solids.append(styled(pad, "thumb_tip_pad", PAD_COLOR))
return part_compound("thumb_distal", solids)