Files
cadSet/text-to-cad/models/robots/juno/juno_parts/juno_lib.py
T
2026-07-22 19:38:36 +08:00

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)