229 lines
7.2 KiB
Python
229 lines
7.2 KiB
Python
"""juno humanoid - arm segment builders (bicep, forearm).
|
|
|
|
Local frames (units mm, robot faces +X, +Y robot-left, +Z up):
|
|
- bicep: origin = shoulder-yaw joint center at the TOP of the bicep,
|
|
segment extends down -Z; elbow axis = Y through (0, 0, -156).
|
|
- forearm: origin = elbow joint center; wrist-roll seam at z = -150.
|
|
|
|
Styling: graphite structural core with ice-gray clamshell panels split
|
|
front/back (graphite reveal strips on the +-Y sides), machined-aluminum
|
|
joint rims, exposed cylindrical elbow actuator.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math # noqa: F401 (kept per module convention)
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
from build123d import (
|
|
Axis,
|
|
Box,
|
|
Circle,
|
|
Cylinder,
|
|
Ellipse,
|
|
Plane,
|
|
Pos,
|
|
RectangleRounded,
|
|
Rot,
|
|
chamfer,
|
|
fillet,
|
|
loft,
|
|
)
|
|
|
|
from .juno_lib import (
|
|
ALU_COLOR,
|
|
SHELL_COLOR,
|
|
STRUCT_COLOR,
|
|
actuator_solids,
|
|
part_compound,
|
|
styled,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------- helpers
|
|
|
|
|
|
def _loft(specs):
|
|
"""Loft elliptical sections; specs = [(z, half_x, half_y), ...]."""
|
|
return loft([Plane.XY.offset(z) * Ellipse(rx, ry) for z, rx, ry in specs])
|
|
|
|
|
|
def _try_fillet(solid, edges, radius):
|
|
try:
|
|
return fillet(edges, radius)
|
|
except Exception:
|
|
return solid
|
|
|
|
|
|
def _try_chamfer(solid, edges, length):
|
|
try:
|
|
return chamfer(edges, length)
|
|
except Exception:
|
|
return solid
|
|
|
|
|
|
def _ring_groove(solid, z, r_out, depth=0.9, width=1.5):
|
|
"""Shallow machined groove around a Z-axis ring at height z."""
|
|
try:
|
|
g = Pos(0, 0, z) * (
|
|
Cylinder(radius=r_out + 1.0, height=width)
|
|
- Cylinder(radius=r_out - depth, height=width)
|
|
)
|
|
return solid - g
|
|
except Exception:
|
|
return solid
|
|
|
|
|
|
def _y_cyl(y_center, z_center, radius, height):
|
|
"""Cylinder whose axis is Y, centered at (0, y_center, z_center)."""
|
|
return Pos(0, y_center, z_center) * Rot(-90, 0, 0) * Cylinder(
|
|
radius=radius, height=height
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------- bicep
|
|
|
|
|
|
def build_bicep(side: str):
|
|
s = 1.0 if side == "left" else -1.0
|
|
p = f"bicep_{side}"
|
|
solids = []
|
|
|
|
# -- shoulder-yaw rotation ring (LOCKED: dia 56, z 0..-10) --
|
|
ring = Pos(0, 0, -5.0) * Cylinder(radius=28.0, height=10.0)
|
|
ring = _try_chamfer(ring, ring.edges(), 0.8)
|
|
solids.append(styled(ring, f"{p}_yaw_ring", ALU_COLOR))
|
|
|
|
# -- upper-arm capsule: graphite core + clamshell panels (dia ~54 -> 46) --
|
|
outer_specs = [
|
|
(-10.0, 26.5, 26.5),
|
|
(-44.0, 27.4, 27.4),
|
|
(-78.0, 25.2, 25.2),
|
|
(-106.0, 23.0, 23.0),
|
|
]
|
|
core_specs = [(z, rx - 2.5, ry - 2.5) for z, rx, ry in outer_specs]
|
|
core = _loft(core_specs)
|
|
outer = _loft(outer_specs)
|
|
blank = outer - core
|
|
seam = 3.2 # graphite reveal half-width on each side
|
|
front = blank & (Pos(45.0 + seam, 0, -58.0) * Box(90, 90, 130))
|
|
rear = blank & (Pos(-45.0 - seam, 0, -58.0) * Box(90, 90, 130))
|
|
front = _try_fillet(front, front.edges(), 0.8)
|
|
rear = _try_fillet(rear, rear.edges(), 0.8)
|
|
solids.append(styled(core, f"{p}_core", STRUCT_COLOR))
|
|
solids.append(styled(front, f"{p}_shell_front", SHELL_COLOR))
|
|
solids.append(styled(rear, f"{p}_shell_rear", SHELL_COLOR))
|
|
|
|
# -- distal elbow fork (LOCKED: axis Y @ (0,0,-156), plates |y| 28..36,
|
|
# bosses dia 44 @ (0,+-32,-156), blends in by z -110) --
|
|
trans = loft(
|
|
[
|
|
Plane.XY.offset(-108.0) * Circle(22.9),
|
|
Plane.XY.offset(-120.0) * RectangleRounded(44.0, 54.0, 14.0),
|
|
Plane.XY.offset(-132.0) * RectangleRounded(50.0, 72.0, 10.0),
|
|
]
|
|
)
|
|
fork = trans
|
|
for ys in (1.0, -1.0):
|
|
slab = Pos(0, ys * 32.0, -144.0) * Box(50.0, 8.0, 24.0)
|
|
# soften plate edges on the primitives (post-boolean chamfers on this
|
|
# shape hang OCC, so detail is applied before fusing)
|
|
slab = _try_fillet(slab, slab.edges().filter_by(Axis.Y), 2.0)
|
|
tip = _y_cyl(ys * 32.0, -156.0, 25.0, 8.0)
|
|
tip = _try_chamfer(tip, tip.edges(), 1.0)
|
|
fork = fork + slab + tip
|
|
# clearance arch for the forearm elbow can (dia 62 + margin, |y|<=27.2)
|
|
fork = fork - _y_cyl(0.0, -156.0, 34.0, 54.4)
|
|
# through-bores for the aluminum joint bosses
|
|
for ys in (1.0, -1.0):
|
|
fork = fork - _y_cyl(ys * 32.0, -156.0, 22.05, 8.4)
|
|
solids.append(styled(fork, f"{p}_fork", STRUCT_COLOR))
|
|
|
|
# -- aluminum elbow bosses (LOCKED dia 44 at (0,+-32,-156)) --
|
|
for ys in (1.0, -1.0):
|
|
tag = "outboard" if ys * s > 0 else "inboard"
|
|
body = _y_cyl(ys * 32.0, -156.0, 21.9, 8.0)
|
|
lip = _y_cyl(ys * 37.0, -156.0, 16.0, 2.0)
|
|
boss = body + lip
|
|
boss = _try_chamfer(boss, boss.edges(), 0.8)
|
|
solids.append(styled(boss, f"{p}_elbow_boss_{tag}", ALU_COLOR))
|
|
|
|
return part_compound(p, solids)
|
|
|
|
|
|
# ---------------------------------------------------------------- forearm
|
|
|
|
|
|
def build_forearm(side: str):
|
|
s = 1.0 if side == "left" else -1.0
|
|
p = f"forearm_{side}"
|
|
solids = []
|
|
|
|
# -- exposed elbow actuator (LOCKED: dia 62, len 50, axis Y) --
|
|
# output hub faces outboard (+y on left, -y on right)
|
|
act = actuator_solids(f"{p}_elbow", 62.0, 50.0, center=(0, 0, 0), axis=(0, s, 0))
|
|
can, rest = act[0], act[1:]
|
|
solids.extend(rest)
|
|
|
|
# -- graphite chassis: yoke wrapping the can, tapering to a wrist flare --
|
|
core_specs = [
|
|
(-4.0, 24.0, 16.0),
|
|
(-26.0, 26.0, 18.0),
|
|
(-72.0, 19.0, 14.5),
|
|
(-112.0, 15.5, 12.5),
|
|
(-126.0, 15.2, 12.4),
|
|
]
|
|
flare = loft(
|
|
[
|
|
Plane.XY.offset(-126.0) * Ellipse(15.2, 12.4),
|
|
Plane.XY.offset(-140.0) * Ellipse(19.0, 19.0),
|
|
]
|
|
)
|
|
chassis = can + _loft(core_specs) + flare
|
|
solids.append(styled(chassis, f"{p}_chassis", STRUCT_COLOR))
|
|
|
|
# -- ice-gray clamshell panels over the chassis --
|
|
outer_specs = [
|
|
(-26.0, 28.5, 20.5),
|
|
(-72.0, 21.5, 17.0),
|
|
(-112.0, 18.0, 15.0),
|
|
(-124.0, 17.6, 14.7),
|
|
]
|
|
blank = _loft(outer_specs) - chassis
|
|
# wrap the panel tops around the actuator with a 2.5mm reveal
|
|
blank = blank - _y_cyl(0.0, 0.0, 33.5, 80.0)
|
|
seam = 3.0
|
|
front = blank & (Pos(45.0 + seam, 0, -76.0) * Box(90, 80, 110))
|
|
rear = blank & (Pos(-45.0 - seam, 0, -76.0) * Box(90, 80, 110))
|
|
front = _try_fillet(front, front.edges(), 0.8)
|
|
rear = _try_fillet(rear, rear.edges(), 0.8)
|
|
solids.append(styled(front, f"{p}_shell_front", SHELL_COLOR))
|
|
solids.append(styled(rear, f"{p}_shell_rear", SHELL_COLOR))
|
|
|
|
# -- wrist-roll collar (LOCKED: dia 44, z -140..-150) --
|
|
collar = Pos(0, 0, -145.0) * Cylinder(radius=22.0, height=10.0)
|
|
collar = _try_chamfer(collar, collar.edges(), 0.8)
|
|
solids.append(styled(collar, f"{p}_wrist_collar", ALU_COLOR))
|
|
|
|
return part_compound(p, solids)
|
|
|
|
|
|
# ---------------------------------------------------------------- demo
|
|
|
|
|
|
def gen_step():
|
|
layout = [
|
|
(build_bicep("left"), 0.0),
|
|
(build_forearm("left"), 220.0),
|
|
(build_bicep("right"), 440.0),
|
|
(build_forearm("right"), 660.0),
|
|
]
|
|
parts = []
|
|
for comp, x in layout:
|
|
comp.location = Pos(x, 0, 0)
|
|
parts.append(comp)
|
|
return part_compound("juno_arms", parts)
|