"""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)