192 lines
6.3 KiB
Python
192 lines
6.3 KiB
Python
"""Shared helpers for the juno humanoid part modules.
|
|
|
|
Conventions (all part modules must follow):
|
|
- Units mm. Robot faces +X, +Y is robot-left, +Z is up.
|
|
- Every part builder returns an identity-location build123d Compound whose
|
|
children are closed, labeled, colored solids. Model geometry directly in the
|
|
part's local frame (joint/mate origins as documented per part).
|
|
- Limb segments: origin at the proximal joint center, segment extends -Z.
|
|
- Exposed actuator cans: cylinder axis along the joint axis, centered at the
|
|
joint center, built with `actuator_solids()`.
|
|
|
|
Verified joint-attach math (see probe_joint.py): for build123d 0.10,
|
|
`revolute_attach()` places the child so its joint plane coincides with the
|
|
parent joint plane rotated `angle_deg` about the shared axis.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
|
|
from build123d import (
|
|
Axis,
|
|
Box,
|
|
Color,
|
|
Compound,
|
|
Cylinder,
|
|
Location,
|
|
Plane,
|
|
Pos,
|
|
Rot,
|
|
Vector,
|
|
chamfer,
|
|
fillet,
|
|
)
|
|
|
|
# Palette: warm porcelain + graphite + aluminum, with a coral accent on
|
|
# small repeated functional details (hub caps, bumpers, vents). No logos.
|
|
SHELL = (0.82, 0.815, 0.79) # warm porcelain composite shells
|
|
STRUCT = (0.16, 0.17, 0.19) # graphite structural cores
|
|
ALU = (0.88, 0.90, 0.94) # bright machined-aluminum joint rims
|
|
VISOR = (0.02, 0.05, 0.11) # gloss midnight-blue sensor glass
|
|
RUBBER = (0.08, 0.08, 0.09) # sole / grip rubber
|
|
ACCENT = (0.93, 0.38, 0.16) # coral-orange accent details
|
|
EYE = (0.32, 0.90, 0.98) # bright cyan display "pixels" (Anki-style)
|
|
|
|
SHELL_COLOR = Color(*SHELL)
|
|
STRUCT_COLOR = Color(*STRUCT)
|
|
ALU_COLOR = Color(*ALU)
|
|
VISOR_COLOR = Color(*VISOR)
|
|
RUBBER_COLOR = Color(*RUBBER)
|
|
ACCENT_COLOR = Color(*ACCENT)
|
|
EYE_COLOR = Color(*EYE)
|
|
|
|
|
|
def styled(solid, label: str, color: Color):
|
|
solid.label = label
|
|
solid.color = color
|
|
return solid
|
|
|
|
|
|
def part_compound(label: str, children) -> Compound:
|
|
comp = Compound(children=list(children))
|
|
comp.label = label
|
|
return comp
|
|
|
|
|
|
def joint_plane(origin, axis_dir, x_ref) -> Location:
|
|
return Plane(
|
|
origin=Vector(origin), x_dir=Vector(x_ref), z_dir=Vector(axis_dir)
|
|
).location
|
|
|
|
|
|
def revolute_attach(
|
|
asm,
|
|
parent,
|
|
child,
|
|
name: str,
|
|
p_origin,
|
|
p_axis,
|
|
p_xref,
|
|
c_origin,
|
|
c_axis,
|
|
c_xref,
|
|
angle_deg: float,
|
|
):
|
|
"""Author a parent revolute frame + child rigid frame and connect them.
|
|
|
|
Joint origins/axes are given in each part's LOCAL modeling frame; the
|
|
helper transforms them by each part's current location, so parts that were
|
|
already repositioned by upstream connects keep correct joints (connect in
|
|
root-to-leaf order). At angle 0 the child's c_xref direction aligns with
|
|
the parent's p_xref direction about the shared axis.
|
|
"""
|
|
j_p_world = parent.location * joint_plane(p_origin, p_axis, p_xref)
|
|
z_tip = (j_p_world * Pos(0, 0, 1)).position
|
|
axis_world = Axis(j_p_world.position, z_tip - j_p_world.position)
|
|
f_parent = asm.revolute_frame(
|
|
parent, f"{name}_axis", axis_world, angular_range=(-360.0, 360.0)
|
|
)
|
|
# The joint's stored frame may use any x-reference convention; read the
|
|
# actual frame back and fold the constant Rz difference into the child
|
|
# mount so the child's c_xref aligns with p_xref at angle 0.
|
|
joint_obj = parent.joints[f"{name}_axis"]
|
|
a_eff_world = parent.location * joint_obj.relative_axis.location
|
|
delta = j_p_world.inverse() * a_eff_world # pure Rz about the joint axis
|
|
j_c = joint_plane(c_origin, c_axis, c_xref)
|
|
l_c_world = child.location * (j_c * delta)
|
|
f_child = asm.rigid_frame(child, f"{name}_mount", l_c_world)
|
|
asm.revolute(f_parent, f_child, angle=angle_deg, label=name)
|
|
|
|
|
|
def _axis_rot(axis_dir) -> Rot:
|
|
"""Rotation taking +Z to axis_dir (for placing cylinders along a joint axis)."""
|
|
d = Vector(axis_dir).normalized()
|
|
if abs(d.Z) > 0.999:
|
|
return Rot(0, 0, 0) if d.Z > 0 else Rot(180, 0, 0)
|
|
if abs(d.Y) > 0.999:
|
|
return Rot(-90, 0, 0) if d.Y > 0 else Rot(90, 0, 0)
|
|
if abs(d.X) > 0.999:
|
|
return Rot(0, 90, 0) if d.X > 0 else Rot(0, -90, 0)
|
|
raise ValueError("actuator axis must be a principal axis")
|
|
|
|
|
|
def actuator_solids(
|
|
name: str,
|
|
diameter: float,
|
|
length: float,
|
|
*,
|
|
center=(0.0, 0.0, 0.0),
|
|
axis=(0.0, 1.0, 0.0),
|
|
bolts: bool = True,
|
|
) -> list:
|
|
"""Exposed cylindrical actuator module centered on a joint.
|
|
|
|
Graphite can, recessed aluminum end caps, shallow rim grooves, and an
|
|
optional bolt circle on the +axis cap for larger sizes. Returns a list of
|
|
labeled, colored solids.
|
|
"""
|
|
r = diameter / 2.0
|
|
rot = _axis_rot(axis)
|
|
at = Pos(*center) * rot
|
|
solids = []
|
|
|
|
can_len = length * 0.78
|
|
can = at * Cylinder(radius=r, height=can_len)
|
|
can = fillet(can.edges(), min(2.2, r * 0.08))
|
|
solids.append(styled(can, f"{name}_can", STRUCT_COLOR))
|
|
|
|
cap_len = (length - can_len) / 2.0
|
|
rim_r = r * 0.92
|
|
for sign, tag in ((1.0, "out"), (-1.0, "in")):
|
|
cap = at * Pos(0, 0, sign * (can_len / 2.0 + cap_len / 2.0)) * Cylinder(
|
|
radius=rim_r, height=cap_len
|
|
)
|
|
cap = chamfer(cap.edges(), min(1.2, cap_len * 0.3))
|
|
solids.append(styled(cap, f"{name}_cap_{tag}", ALU_COLOR))
|
|
|
|
hub = at * Pos(0, 0, length / 2.0 + 1.0) * Cylinder(radius=r * 0.45, height=2.0)
|
|
solids.append(styled(hub, f"{name}_hub", ACCENT_COLOR))
|
|
|
|
if bolts and diameter >= 56.0:
|
|
n = 8 if diameter >= 80 else 6
|
|
bc_r = rim_r * 0.74
|
|
for i in range(n):
|
|
a = 2.0 * math.pi * i / n
|
|
bolt = at * Pos(
|
|
bc_r * math.cos(a), bc_r * math.sin(a), length / 2.0 + 0.6
|
|
) * Cylinder(radius=1.6, height=1.2)
|
|
solids.append(styled(bolt, f"{name}_bolt_{i}", STRUCT_COLOR))
|
|
return solids
|
|
|
|
|
|
def rounded_box(
|
|
label: str,
|
|
color: Color,
|
|
size,
|
|
*,
|
|
center=(0.0, 0.0, 0.0),
|
|
radius: float = 3.0,
|
|
axes: str = "z",
|
|
):
|
|
"""Box with filleted edges along the given axes ("z", "xz", "xyz"...)."""
|
|
sx, sy, sz = size
|
|
box = Pos(*center) * Box(sx, sy, sz)
|
|
edges = []
|
|
for ax_name, ax in (("x", Axis.X), ("y", Axis.Y), ("z", Axis.Z)):
|
|
if ax_name in axes:
|
|
edges.extend(box.edges().filter_by(ax))
|
|
if edges:
|
|
box = fillet(edges, min(radius, min(sx, sy, sz) / 2.0 - 0.4))
|
|
return styled(box, label, color)
|