"""lyra palm: wrist flange, carpal core, palm slab, knuckle row, thumb turret. Local frame = robot root frame: origin at the wrist-flange mount face center, +Z distal, +Y palmar, +X radial. The palm owns all MCP clevis cheeks, knuckle pins, the thumb CMC turret pedestal/spindle, and the tactile palm pad. Articulation clearances baked into the shape: - Knuckle slots are open from y -14 to +15 so proximal phalanges sweep -15..+95 deg without touching the covers. - The palmar plate's top edge stays >= 8 mm proximal of each MCP axis, outside the swept envelope of the flexing proximal shaft. - A 15 mm radius relief is turned around the thumb turret so the yawing thumb base clevis never clips the radial palm corner. """ from __future__ import annotations from build123d import ( Axis, Box, Plane, Polyline, Pos, Rot, RectangleRounded, extrude, loft, make_face, ) from .chain import FINGERS, MCP_ORIGIN_MM, THUMB_CMC_ORIGIN_MM from .digits import ( FINGER_SCALE, MCP_PIN_R, PALM_LOBE_R, PROX_TANG_W, TB_TANG_R, TB_TANG_H, ) from .lib import ( ALU_COLOR, AMBER_COLOR, GRAPHITE_COLOR, PAD_COLOR, PEARL_COLOR, SENSOR_COLOR, part_compound, pin, safe_chamfer, safe_fillet, styled, xcyl, ycyl, zcyl, ) SLAB_HALF_Y = 9.5 # graphite core half-depth COVER_Y0, COVER_Y1 = -12.1, -9.5 # pearl dorsal cover PLATE_Y0, PLATE_Y1 = 9.5, 11.5 # pearl palmar plate MCP_PIN_LEN = 17.0 # Thumb turret (axis +Z through chain.THUMB_CMC_ORIGIN_MM). TURRET_R = 10.5 TURRET_RELIEF_R = 15.0 SPINDLE_R = 2.7 CX, CY, CZ = THUMB_CMC_ORIGIN_MM # Palm silhouette in the XZ plane (clockwise; +X radial, +Z distal). The # knuckle arch follows the MCP line (index 99 / middle 103 / ring 99 / # pinky 90); lobes added per knuckle crown above it. CORE_PTS = [ (-25.0, 33.0), (-33.0, 50.0), (-36.5, 72.0), (-36.5, 87.0), (-30.0, 90.0), (-19.0, 95.0), (-9.5, 99.0), (0.0, 101.0), (9.5, 103.0), (19.0, 101.0), (28.5, 99.0), (34.0, 97.5), (37.5, 95.5), (37.5, 70.0), (30.0, 46.0), (25.0, 33.0), ] COVER_PTS = [ (-23.5, 35.0), (-31.3, 51.0), (-34.8, 72.0), (-34.8, 85.6), (-29.0, 88.4), (-18.6, 93.0), (-9.5, 97.0), (0.0, 99.0), (9.5, 101.0), (18.6, 99.0), (28.5, 97.0), (33.0, 95.6), (35.8, 93.8), (35.8, 71.0), (28.6, 47.5), (23.4, 35.0), ] PLATE_PTS = [ (-23.5, 35.0), (-31.0, 51.0), (-34.5, 72.0), (-34.5, 80.0), (-28.6, 82.5), (-18.6, 86.0), (-9.5, 91.0), (0.0, 93.0), (9.5, 95.0), (19.0, 93.0), (28.5, 91.0), (35.5, 87.5), (35.5, 71.0), (28.6, 47.5), (23.4, 35.0), ] def _outline_solid(pts, y0, depth): pts3 = [(x, y0, z) for x, z in pts] face = make_face(Polyline(*pts3, close=True)) return extrude(face, amount=depth, dir=(0, 1, 0)) def _slot_gap(finger: str) -> float: return PROX_TANG_W * FINGER_SCALE[finger] + 0.7 def _knuckle_cuts(solid): """Open the four MCP clevis slots and pin bores in a palm solid.""" for finger in FINGERS: kx, _, kz = MCP_ORIGIN_MM[finger] gap = _slot_gap(finger) solid -= Pos(kx, 0.5, kz + 2.0) * Box(gap, 29.0, 26.0) solid -= Pos(kx, 0, kz) * xcyl(MCP_PIN_R + 0.4, 26.0) return solid def _thumb_relief(solid): """Turned clearance around the thumb turret for the yawing base clevis.""" disc_z0 = CZ - TB_TANG_H / 2.0 return solid - Pos(CX, CY, disc_z0 + 5.6) * zcyl(TURRET_RELIEF_R, 11.6) def _wrist_flange(): flange = Pos(0, 0, 3.0) * zcyl(23.0, 6.0) flange = safe_chamfer(flange, flange.edges(), 1.2) heads = [] for i in range(6): from math import cos, pi, sin a = 2.0 * pi * i / 6.0 bx, by = 16.5 * cos(a), 16.5 * sin(a) flange -= Pos(bx, by, 1.5) * zcyl(3.6, 3.0) flange -= Pos(bx, by, 3.0) * zcyl(2.3, 8.0) head = Pos(bx, by, 1.4) * zcyl(3.3, 2.2) head = safe_chamfer(head, head.edges(), 0.5) heads.append(styled(head, f"flange_bolt_{i}", GRAPHITE_COLOR)) return styled(flange, "wrist_flange", ALU_COLOR), heads def _carpal_core(): try: f1 = Plane.XY.offset(6.0) * RectangleRounded(40.0, 17.5, 7.0) f2 = Plane.XY.offset(33.0) * RectangleRounded(50.0, 19.0, 8.0) carpal = loft([f1, f2]) except Exception: carpal = Pos(0, 0, 19.5) * Box(46.0, 18.2, 27.0) carpal = safe_fillet(carpal, carpal.edges().filter_by(Axis.Z), 7.0) return styled(carpal, "carpal_core", GRAPHITE_COLOR) def _palm_core(): core = _outline_solid(CORE_PTS, -SLAB_HALF_Y, 2 * SLAB_HALF_Y) core = safe_fillet(core, core.edges().filter_by(Axis.Y), 2.2) # Knuckle lobe crowns (cut by the clevis slots into cheek arcs). for finger in FINGERS: kx, _, kz = MCP_ORIGIN_MM[finger] s = FINGER_SCALE[finger] core += Pos(kx, 0, kz) * xcyl(PALM_LOBE_R * s, _slot_gap(finger) + 7.0) # Thumb turret pedestal (bored for the spindle), merged into the # radial-palmar corner. disc_z0 = CZ - TB_TANG_H / 2.0 pedestal = Pos(CX, CY, disc_z0 - 3.4) * (zcyl(TURRET_R, 6.4) - zcyl(SPINDLE_R + 0.7, 9.0)) pedestal = safe_chamfer(pedestal, pedestal.edges(), 1.0) core += pedestal core = _knuckle_cuts(core) core = _thumb_relief(core) return styled(core, "palm_core", GRAPHITE_COLOR) def _dorsal_cover(): cover = _outline_solid(COVER_PTS, COVER_Y0, COVER_Y1 - COVER_Y0) cover = safe_fillet(cover, cover.edges().filter_by(Axis.Y), 2.0) try: outer = cover.faces().sort_by(Axis.Y)[0] cover = safe_chamfer(cover, outer.edges(), 1.0) except Exception: pass # Engraved tendon-channel lines running from the carpal exit to each # knuckle slot (clean covered cable routing, shown as shadow grooves). from math import atan2, degrees, hypot for finger in FINGERS: kx, _, kz = MCP_ORIGIN_MM[finger] ax, az = kx * 0.42, 40.0 bx, bz = kx, kz - 12.0 mx, mz = (ax + bx) / 2.0, (az + bz) / 2.0 ang = degrees(atan2(bx - ax, bz - az)) glen = hypot(bx - ax, bz - az) groove = Pos(mx, COVER_Y0 + 0.25, mz) * Rot(0, ang, 0) * Box(1.6, 1.0, glen) try: cover -= groove except Exception: pass cover = _knuckle_cuts(cover) cover = _thumb_relief(cover) return styled(cover, "dorsal_cover", PEARL_COLOR) def _palmar_plate(): plate = _outline_solid(PLATE_PTS, PLATE_Y0, PLATE_Y1 - PLATE_Y0) plate = safe_fillet(plate, plate.edges().filter_by(Axis.Y), 2.0) try: outer = plate.faces().sort_by(Axis.Y)[-1] plate = safe_chamfer(plate, outer.edges(), 0.8) except Exception: pass # Pocket for the tactile pad (pad sits 0.4 into the plate). plate -= Pos(0, PLATE_Y1, 65.0) * Box(46.4, 2.0, 30.4) plate = _knuckle_cuts(plate) plate = _thumb_relief(plate) return styled(plate, "palmar_plate", PEARL_COLOR) def _palm_pad(): pad = Pos(0, PLATE_Y1 - 0.4 + 0.85, 65.0) * Box(46.0, 1.7, 30.0) pad = safe_fillet(pad, pad.edges().filter_by(Axis.Y), 5.0) # Tactile zone split grooves (2 x 2 sensor array read as fine lines). for cut in ( Pos(0, PLATE_Y1 + 1.3, 65.0) * Box(1.2, 1.0, 30.5), Pos(0, PLATE_Y1 + 1.3, 65.0) * Box(46.5, 1.0, 1.2), ): try: pad -= cut except Exception: pass pad = _thumb_relief(pad) return styled(pad, "palm_pad", PAD_COLOR) def _palm_sensor(): ring = Pos(0, PLATE_Y1 + 0.3, 87.0) * (ycyl(5.0, 0.6) - ycyl(4.0, 3.0)) lens = Pos(0, PLATE_Y1 + 0.2, 87.0) * ycyl(3.9, 0.8) return [ styled(ring, "palm_sensor_ring", AMBER_COLOR), styled(lens, "palm_sensor_lens", SENSOR_COLOR), ] def _tensioners(): """Tendon tensioner dial row on the dorsal carpal face.""" out = [] for i, dx in enumerate((-12.0, -6.0, 0.0, 6.0, 12.0)): face_y = -(8.75 + 0.75 * (20.0 - 6.0) / 27.0) dial = Pos(dx, face_y - 1.1, 20.0) * ycyl(2.6, 2.4) dial = safe_chamfer(dial, dial.edges(), 0.5) out.append(styled(dial, f"tensioner_dial_{i}", ALU_COLOR)) dot = Pos(dx, face_y - 2.5, 20.0) * ycyl(1.0, 0.7) out.append(styled(dot, f"tensioner_dot_{i}", AMBER_COLOR)) return out def _knuckle_hardware(): """Palm-owned MCP pins plus rim washers on the outer index/pinky cheeks.""" out = [] for finger in FINGERS: kx, _, kz = MCP_ORIGIN_MM[finger] out.append( styled( Pos(kx, 0, kz) * pin("x", MCP_PIN_R, MCP_PIN_LEN), f"{finger}_mcp_pin", ALU_COLOR, ) ) for finger, xr in (("index", 38.2), ("pinky", -37.2)): kx, _, kz = MCP_ORIGIN_MM[finger] rim = Pos(xr, 0, kz) * (xcyl(4.6, 1.2) - xcyl(MCP_PIN_R + 0.4, 3.0)) out.append(styled(rim, f"{finger}_mcp_rim", ALU_COLOR)) return out def _turret_hardware(): """Bearing collar + spindle/cap for the thumb CMC turret (palm side).""" disc_z0 = CZ - TB_TANG_H / 2.0 disc_z1 = CZ + TB_TANG_H / 2.0 collar = Pos(CX, CY, disc_z0 - 0.95) * (zcyl(TURRET_R + 0.7, 1.5) - zcyl(TB_TANG_R + 0.8, 3.0)) collar = safe_chamfer(collar, collar.edges(), 0.4) spindle = Pos(CX, CY, (disc_z0 - 5.0 + disc_z1 + 0.4) / 2.0) * zcyl( SPINDLE_R, disc_z1 + 0.4 - (disc_z0 - 5.0) ) cap = Pos(CX, CY, disc_z1 + 0.4 + 0.8) * zcyl(4.8, 1.6) cap = safe_chamfer(cap, cap.edges(), 0.5) spindle += cap return [ styled(collar, "thumb_turret_collar", ALU_COLOR), styled(spindle, "thumb_turret_spindle", ALU_COLOR), ] def build_palm(): flange, bolt_heads = _wrist_flange() solids = [flange] solids += bolt_heads solids.append(_carpal_core()) solids.append(_palm_core()) solids.append(_dorsal_cover()) solids.append(_palmar_plate()) solids.append(_palm_pad()) solids += _palm_sensor() solids += _tensioners() solids += _knuckle_hardware() solids += _turret_hardware() return part_compound("palm", solids)