"""juno humanoid -- leg segments: thigh, shin, foot. Local frames (units mm, robot faces +X, +Y is robot-left, +Z up): - thigh: origin at hip-pitch joint center; knee axis = Y through (0,0,-245). - shin: origin at knee joint center; ankle-pitch axis = Y through (0,0,-245). - foot: origin at ankle-roll joint center, 26 mm above the sole bottom. Builders return identity-location Compounds of labeled, colored solids. Left/right mirror about the XZ plane. """ from __future__ import annotations import sys import traceback from pathlib import Path from build123d import ( Axis, Box, Compound, Cylinder, GeomType, Plane, Pos, RectangleRounded, Rot, chamfer, fillet, loft, mirror, ) from .juno_lib import ( ACCENT_COLOR, ALU_COLOR, RUBBER_COLOR, SHELL_COLOR, STRUCT_COLOR, actuator_solids, part_compound, rounded_box, styled, ) # --------------------------------------------------------------- helpers def _solo(shape): """Largest solid of a boolean result (keeps each child manifold).""" sols = shape.solids() if len(sols) == 1: return sols[0] return max(sols, key=lambda s: s.volume) def _solids_over(shape, min_volume=800.0): """All solids of a boolean result above a volume floor, biggest first.""" sols = sorted(shape.solids(), key=lambda s: s.volume, reverse=True) return [s for s in sols if s.volume > min_volume] or sols[:1] def _try_fillet(shape, edges, radius): try: edges = list(edges) if edges: return _solo(fillet(edges, radius)) except Exception: pass return shape def _try_chamfer(shape, edges, length): try: edges = list(edges) if edges: return _solo(chamfer(edges, length)) except Exception: pass return shape def _cyl_y(radius, length, center=(0.0, 0.0, 0.0)): return Pos(*center) * Rot(-90, 0, 0) * Cylinder(radius, length) def _cyl_x(radius, length, center=(0.0, 0.0, 0.0)): return Pos(*center) * Rot(0, 90, 0) * Cylinder(radius, length) def _loft_z(specs): """Loft rounded-rectangle sections normal to Z. specs: iterable of (z, x_size, y_size, x_center, y_center, corner_r). """ faces = [ Pos(xc, yc, z) * RectangleRounded(w, d, r) for (z, w, d, xc, yc, r) in specs ] return _solo(loft(faces)) def _loft_x(specs): """Loft rounded-rectangle sections normal to X. specs: iterable of (x, y_size, z_size, z_center, corner_r). """ faces = [ Plane.YZ.offset(x) * Pos(0, zc, 0) * RectangleRounded(wy, hz, r) for (x, wy, hz, zc, r) in specs ] return _solo(loft(faces)) def _rim_chamfer(shape, radius, length): """Chamfer circular edges of a given radius (collar rims etc.).""" try: edges = [ e for e in shape.edges().filter_by(GeomType.CIRCLE) if abs(e.radius - radius) < 0.6 ] return _try_chamfer(shape, edges, length) except Exception: return shape def _yoke_fillet(shape, z_lo, z_hi, radius): """Fillet vertical (Z) edges whose centers lie in a z band.""" try: edges = [ e for e in shape.edges().filter_by(Axis.Z) if z_lo < e.center().Z < z_hi and e.length > 20 ] return _try_fillet(shape, edges, radius) except Exception: return shape def _mirror_children(children): out = [] for c in children: m = mirror(c, about=Plane.XZ) if m.volume < 0: # reversed orientation from the mirror try: m = m.fix() except Exception: pass out.append(styled(m, c.label, c.color)) return out def _fallback(label, size): return [styled(Box(*size), f"{label}_fallback", STRUCT_COLOR)] # ----------------------------------------------------------------- thigh KNEE_Z = -290.0 # stretched +45 per proportion audit (G1-like leg ratio) def _thigh_children(): ch = [] # Exposed hip-pitch actuator (locked interface). ch += actuator_solids("hip_pitch", 92, 72, center=(0, 0, 0), axis=(0, 1, 0)) # Graphite structural core: quad-like taper + full hip collar ring, # bored for the actuator can so faces stay clear. core = _loft_z( [ (-30, 92, 72, 2, 0, 18), (-100, 98, 86, 8, 0, 22), (-185, 84, 76, 6, 0, 18), (-225, 60, 68, 2, 0, 14), ] ) core = _solo(core + _cyl_y(55, 34)) # hip collar band |y|<=17 core = _solo(core - _cyl_y(46.5, 130)) # can clearance bore core = _rim_chamfer(core, 55.0, 2.0) ch.append(styled(core, "thigh_core", STRUCT_COLOR)) # Ice-gray fairing wrapping front + outer (robot-left) faces, split into # two shells by a panel reveal that exposes the graphite core. shell = _loft_z( [ (-58, 94, 78, 11, 3, 18), (-105, 99, 88, 12, 4, 22), (-185, 84, 80, 11, 3, 18), (-221, 62, 70, 8, 2, 14), ] ) shell = shell - core shell = shell - (Pos(0, 0, -140.5) * Box(320, 320, 3)) # panel reveal panels = _solids_over(shell)[:2] tags = ("upper", "lower") for sol, tag in zip(panels, tags): sol = _try_chamfer(sol, sol.edges(), 0.5) ch.append(styled(sol, f"thigh_fairing_{tag}", SHELL_COLOR)) # Knee fork (locked): yoke + twin plates, inner faces y=+-38, outer # y=+-48, spanning z -225..-324, bored dia 56 on the knee axis. fork = Pos(0, 0, -241) * Box(58, 96, 32) for s in (1.0, -1.0): fork = fork + Pos(0, s * 43, -273) * Box(60, 10, 34) fork = fork + _cyl_y(34, 10, (0, s * 43, KNEE_Z)) fork = _solo(fork - _cyl_y(28, 120, (0, 0, KNEE_Z))) # relief arch so the shin's knee can (dia 92, |y|<=33) swings clear fork = _solo(fork - _cyl_y(46.8, 67.6, (0, 0, KNEE_Z))) fork = _yoke_fillet(fork, -258, -224, 8.0) fork = _try_chamfer(fork, fork.edges(), 0.8) ch.append(styled(fork, "knee_fork", STRUCT_COLOR)) # Aluminum bearing bosses dia 56 at (0,+-43,-245), through the plates. for s, tag in ((1.0, "out"), (-1.0, "in")): boss = _cyl_y(28, 10, (0, s * 43, KNEE_Z)) + _cyl_y( 30.5, 1.8, (0, s * 48.9, KNEE_Z) ) boss = _solo(boss) boss = _try_chamfer(boss, boss.edges(), 0.6) ch.append(styled(boss, f"knee_boss_{tag}", ALU_COLOR)) return ch def build_thigh(side="left"): label = f"thigh_{side}" try: ch = _thigh_children() except Exception: traceback.print_exc() ch = _fallback(label, (90, 90, 260)) if side == "right": ch = _mirror_children(ch) return part_compound(label, ch) # ------------------------------------------------------------------ shin ANKLE_Z = -290.0 # stretched +45 per proportion audit (G1-like leg ratio) def _shin_children(): ch = [] # Exposed knee actuator (locked interface; dia 92 per torque hierarchy). ch += actuator_solids("knee", 92, 66, center=(0, 0, 0), axis=(0, 1, 0)) # Graphite calf core: full upper rear (calf), slim near ankle. core = _loft_z( [ (-26, 82, 60, 0, 0, 16), (-95, 84, 72, -6, 0, 18), (-185, 62, 58, -2, 0, 14), (-225, 44, 50, 0, 0, 11), ] ) core = _solo(core + _cyl_y(53, 30)) # knee collar band |y|<=15 core = _solo(core - _cyl_y(46.5, 120)) # can clearance bore core = _rim_chamfer(core, 53.0, 2.0) ch.append(styled(core, "shin_core", STRUCT_COLOR)) # Front shin-guard ridge (ice-gray), split by a panel reveal. guard = _loft_z( [ (-55, 78, 64, 7, 0, 16), (-100, 78, 64, 6, 0, 16), (-185, 64, 50, 6, 0, 12), (-223, 46, 40, 4, 0, 10), ] ) guard = guard - core guard = guard - (Pos(0, 0, -145.5) * Box(320, 320, 3)) panels = _solids_over(guard)[:2] for sol, tag in zip(panels, ("upper", "lower")): sol = _try_chamfer(sol, sol.edges(), 0.5) ch.append(styled(sol, f"shin_guard_{tag}", SHELL_COLOR)) # Ankle fork (locked): plates inner y=+-26, outer y=+-34, bored dia 40 # on the ankle-pitch axis through (0,0,-290). fork = Pos(0, 0, -241) * Box(46, 68, 28) for s in (1.0, -1.0): fork = fork + Pos(0, s * 30, -270.5) * Box(54, 8, 39) fork = fork + _cyl_y(27, 8, (0, s * 30, ANKLE_Z)) fork = _solo(fork - _cyl_y(20, 100, (0, 0, ANKLE_Z))) fork = _yoke_fillet(fork, -256, -226, 7.0) fork = _try_chamfer(fork, fork.edges(), 0.8) ch.append(styled(fork, "ankle_fork", STRUCT_COLOR)) # Aluminum bearing bosses dia 40 at (0,+-30,-245). for s, tag in ((1.0, "out"), (-1.0, "in")): boss = _cyl_y(20, 8, (0, s * 30, ANKLE_Z)) + _cyl_y( 22, 1.8, (0, s * 34.9, ANKLE_Z) ) boss = _solo(boss) boss = _try_chamfer(boss, boss.edges(), 0.6) ch.append(styled(boss, f"ankle_boss_{tag}", ALU_COLOR)) return ch def build_shin(side="left"): label = f"shin_{side}" try: ch = _shin_children() except Exception: traceback.print_exc() ch = _fallback(label, (80, 80, 260)) if side == "right": ch = _mirror_children(ch) return part_compound(label, ch) # ------------------------------------------------------------------ foot def _foot_children(): ch = [] # Clearance for the ankle-roll can (dia 44, |x|<21 keep-clear r<24). roll_clear = _cyl_x(24.5, 43) # Rubber sole plate z -26..-19, x -68..112, width 70, toe-up + heel kick. sole = rounded_box( "sole_blank", RUBBER_COLOR, (180, 70, 7), center=(22, 0, -22.5), radius=16, axes="z", ) sole = sole - roll_clear # toe-up wedge over the last ~30 mm (cut line (82,-26) -> (113,-20.4)) sole = sole - (Pos(96.5, 0, -29.46) * Rot(0, -10.3, 0) * Box(44, 96, 12)) # small heel kick (cut line (-58,-26) -> (-69,-22.5)) sole = sole - (Pos(-63, 0, -29.66) * Rot(0, 17.65, 0) * Box(24, 96, 10)) for gx in (-54.0, -36.0, -18.0, 36.0, 54.0, 72.0): # tread grooves sole = sole - (Pos(gx, 0, -25.8) * Box(4.5, 100, 4.5)) ch.append(styled(_solo(sole), "sole", RUBBER_COLOR)) # Sneaker-like wedge upper (ice-gray), lofted along X. Heights raised # ~+6 mm per the visual review (feet read too thin in profile). upper = _loft_x( [ (-62, 54, 27, -5.5, 6), (-30, 62, 32, -3.0, 7), (30, 62, 30, -4.0, 7), (72, 56, 22, -8.0, 5), (104, 38, 10, -14.0, 2.4), ] ) recess = rounded_box( "recess_cut", SHELL_COLOR, (66, 64, 20), center=(0, 0, 2), radius=9, axes="z", ) upper = upper - recess # saddle recess down to z=-8 upper = _solo(upper - roll_clear) ch.append(styled(upper, "foot_upper", SHELL_COLOR)) # Top saddle (locked): twin plates normal to X at x 22..32 / -32..-22, # domed around the roll axis, tied by side rails outside the keep-clear. saddle = None for s in (1.0, -1.0): plate = Pos(27 * s, 0, -1) * Box(10, 62, 14) plate = plate + _cyl_x(19, 10, (27 * s, 0, 0)) saddle = plate if saddle is None else saddle + plate saddle = saddle + Pos(0, s * 27.75, -2) * Box(64, 6.5, 12) saddle = saddle & (Pos(0, 0, 4) * Box(80, 90, 24)) # clip to z -8..16 saddle = _solo(saddle) saddle = _try_chamfer(saddle, saddle.edges(), 0.8) ch.append(styled(saddle, "roll_saddle", STRUCT_COLOR)) # Aluminum bearing bosses dia 34, coaxial with X through the origin. for s, tag in ((1.0, "out"), (-1.0, "in")): boss = _cyl_x(17, 5.5, (34.75 * s, 0, 0)) + _cyl_x( 9, 2, (38.5 * s, 0, 0) ) boss = _solo(boss) boss = _try_chamfer(boss, boss.edges(), 0.6) ch.append(styled(boss, f"roll_boss_{tag}", ALU_COLOR)) # Graphite toe bumper wrapping the toe box. toe = rounded_box( "toe_blank", STRUCT_COLOR, (13, 50, 10), center=(105.5, 0, -14), radius=5, axes="z", ) toe = _solo(toe - upper) ch.append(styled(toe, "toe_bumper", ACCENT_COLOR)) # Graphite heel bumper. heel = rounded_box( "heel_blank", STRUCT_COLOR, (10, 52, 11), center=(-63, 0, -13.5), radius=4, axes="z", ) heel = _solo(heel - upper) ch.append(styled(heel, "heel_bumper", ACCENT_COLOR)) return ch def build_foot(side="left"): label = f"foot_{side}" try: ch = _foot_children() except Exception: traceback.print_exc() ch = _fallback(label, (180, 70, 32)) if side == "right": ch = _mirror_children(ch) return part_compound(label, ch) # ------------------------------------------------------------------ demo def gen_step(): cols = [] x = 0.0 for side in ("left", "right"): for builder in (build_thigh, build_shin, build_foot): comp = builder(side) comp.location = Pos(x, 0, 0) cols.append(comp) x += 260.0 demo = Compound(children=cols) demo.label = "juno_legs_demo" return demo