Files
cadSet/text-to-cad/models/fun/robotic_hand_end_effector.py
2026-07-22 19:38:36 +08:00

598 lines
20 KiB
Python

from math import cos, radians, sin, tau
from build123d import (
Align,
Box,
BuildPart,
BuildSketch,
Circle,
Color,
Compound,
Cone,
Cylinder,
Edge,
Location,
Locations,
Mode,
Plane,
Polygon,
RectangleRounded,
RegularPolygon,
Torus,
extrude,
fillet,
sweep,
)
# Units: millimeters.
# Origin: centered on the palm frame.
# XY: palm plane. +Z: back of hand/up. Fingers extend generally in +Y.
PALM_WIDTH = 70.0
PALM_LENGTH = 58.0
PALM_THICKNESS = 8.0
PALM_CORNER_RADIUS = 5.0
PALM_TOP_Z = PALM_THICKNESS
FINGER_LINK_WIDTH = 8.0
FINGER_LINK_THICKNESS = 6.0
FINGER_BASE_Z = PALM_TOP_Z + 4.5
FINGER_YAW_DEGREES = {
"index": 2.0,
"middle": 0.0,
"ring": -2.0,
"little": -4.0,
}
MAIN_FINGER_BASES = {
"index": (-24.0, 30.0, FINGER_BASE_Z),
"middle": (-8.0, 30.5, FINGER_BASE_Z + 0.5),
"ring": (8.0, 30.0, FINGER_BASE_Z),
"little": (24.0, 29.0, FINGER_BASE_Z - 0.5),
}
MAIN_LINK_LENGTHS = {
"index": (25.0, 20.0, 16.0),
"middle": (25.0, 20.0, 16.0),
"ring": (25.0, 20.0, 16.0),
"little": (22.0, 17.0, 14.0),
}
MAIN_FINGER_FLEX = {
"index": (15.0, 40.0, 60.0),
"middle": (13.0, 37.0, 56.0),
"ring": (16.0, 42.0, 62.0),
"little": (18.0, 45.0, 65.0),
}
THUMB_BASE = (-42.0, -5.0, FINGER_BASE_Z - 1.0)
THUMB_LINK_LENGTHS = (22.0, 18.0, 14.0)
THUMB_YAWS = (50.0, 36.0, 22.0)
THUMB_FLEX = (10.0, 30.0, 48.0)
DISPLAY_BASE_BOTTOM_Z = -64.0
DISPLAY_BASE_THICKNESS = 8.0
DISPLAY_BASE_TOP_Z = DISPLAY_BASE_BOTTOM_Z + DISPLAY_BASE_THICKNESS
WRIST_CENTER = (0.0, -45.0, -12.0)
WRIST_RING_OUTER_RADIUS = 27.0
WRIST_RING_INNER_RADIUS = 19.0
BRUSHED_ALUMINUM = Color(0.72, 0.74, 0.74, 1.0)
DARK_TITANIUM = Color(0.18, 0.20, 0.22, 1.0)
MATTE_BLACK = Color(0.01, 0.011, 0.012, 1.0)
DARK_GRAPHITE = Color(0.10, 0.105, 0.11, 1.0)
POLISHED_STEEL = Color(0.86, 0.88, 0.88, 1.0)
BLUE_ACCENT = Color(0.05, 0.33, 0.86, 1.0)
AMBER_ACCENT = Color(1.0, 0.55, 0.12, 1.0)
def _safe_fillet(edges, radius: float) -> None:
"""Apply a cosmetic fillet inside a BuildPart context when topology allows it."""
try:
fillet(edges, radius)
except Exception:
pass
def _set_metadata(part, label: str, color: Color):
part.label = label
part.color = color
return part
def _teardrop_points(center: tuple[float, float], width: float, height: float) -> list[tuple[float, float]]:
cx, cy = center
return [
(cx - width / 2.0, cy - height * 0.40),
(cx + width / 2.0, cy - height * 0.40),
(cx + width * 0.18, cy + height * 0.18),
(cx, cy + height * 0.52),
(cx - width * 0.18, cy + height * 0.18),
]
def _add_teardrop_sketch(center: tuple[float, float], width: float, height: float) -> None:
Polygon(_teardrop_points(center, width, height), align=None)
Circle(width * 0.28, align=Align.CENTER).locate(
Location((center[0], center[1] + height * 0.28, 0.0))
)
def _make_palm_frame():
openings = [
((-20.0, 0.0), 12.5, 27.0),
((0.0, 5.0), 13.5, 31.0),
((20.0, 0.0), 12.5, 27.0),
]
with BuildPart() as palm:
with BuildSketch(Plane.XY):
RectangleRounded(
PALM_WIDTH,
PALM_LENGTH,
PALM_CORNER_RADIUS,
align=(Align.CENTER, Align.CENTER),
)
extrude(amount=PALM_THICKNESS)
_safe_fillet(palm.edges(), 0.8)
for center, width, height in openings:
with BuildSketch(Plane.XY.offset(-0.8)):
_add_teardrop_sketch(center, width, height)
extrude(amount=PALM_THICKNESS + 1.6, mode=Mode.SUBTRACT)
return _set_metadata(palm.part, "skeletonized_palm_frame_70x58x8", BRUSHED_ALUMINUM)
def _make_palm_ribs():
ribs = []
openings = [
("left", (-20.0, 0.0), 12.5, 27.0),
("center", (0.0, 5.0), 13.5, 31.0),
("right", (20.0, 0.0), 12.5, 27.0),
]
for name, center, width, height in openings:
with BuildPart() as rib:
with BuildSketch(Plane.XY.offset(PALM_TOP_Z)):
_add_teardrop_sketch(center, width + 6.0, height + 6.0)
extrude(amount=2.0)
with BuildSketch(Plane.XY.offset(PALM_TOP_Z - 0.4)):
_add_teardrop_sketch(center, width + 0.6, height + 0.6)
extrude(amount=3.0, mode=Mode.SUBTRACT)
_safe_fillet(rib.edges(), 1.5)
ribs.append(_set_metadata(rib.part, f"raised_palm_rib_{name}_3mm_lip", BRUSHED_ALUMINUM))
return ribs
def _make_display_base():
with BuildPart() as base:
with BuildSketch(Plane.XY):
RectangleRounded(110.0, 80.0, 6.0, align=(Align.CENTER, Align.CENTER))
extrude(amount=DISPLAY_BASE_THICKNESS)
_safe_fillet(base.edges(), 3.0)
for x_pos in (-44.0, 44.0):
for y_pos in (-30.0, 30.0):
with Locations(Location((x_pos, y_pos, -0.6))):
Cylinder(
radius=2.35,
height=DISPLAY_BASE_THICKNESS + 1.2,
align=(Align.CENTER, Align.CENTER, Align.MIN),
mode=Mode.SUBTRACT,
)
with Locations(Location((x_pos, y_pos, DISPLAY_BASE_THICKNESS - 2.7))):
Cone(
bottom_radius=2.35,
top_radius=5.1,
height=2.9,
align=(Align.CENTER, Align.CENTER, Align.MIN),
mode=Mode.SUBTRACT,
)
part = base.part.moved(Location((0.0, -5.0, DISPLAY_BASE_BOTTOM_Z)))
return _set_metadata(part, "rounded_display_base_110x80x8_with_countersunk_holes", DARK_TITANIUM)
def _make_support_post():
height = WRIST_CENTER[2] - WRIST_RING_OUTER_RADIUS - DISPLAY_BASE_TOP_Z
with BuildPart() as post:
Cylinder(
radius=6.5,
height=height,
align=(Align.CENTER, Align.CENTER, Align.MIN),
)
_safe_fillet(post.edges(), 0.8)
part = post.part.moved(Location((0.0, -45.0, DISPLAY_BASE_TOP_Z)))
return _set_metadata(part, "vertical_support_post_to_wrist_gimbal", DARK_TITANIUM)
def _make_wrist_gimbal():
parts = []
with BuildPart() as wrist_cylinder:
Cylinder(
radius=21.0,
height=35.0,
align=(Align.CENTER, Align.CENTER, Align.CENTER),
)
_safe_fillet(wrist_cylinder.edges(), 0.5)
cylinder = wrist_cylinder.part.moved(Location(WRIST_CENTER, (90.0, 0.0, 0.0)))
parts.append(_set_metadata(cylinder, "wrist_short_cylinder_axis_y_od42_length35", DARK_TITANIUM))
with BuildPart() as ring:
Cylinder(
radius=WRIST_RING_OUTER_RADIUS,
height=6.0,
align=(Align.CENTER, Align.CENTER, Align.CENTER),
)
Cylinder(
radius=WRIST_RING_INNER_RADIUS,
height=7.0,
align=(Align.CENTER, Align.CENTER, Align.CENTER),
mode=Mode.SUBTRACT,
)
_safe_fillet(ring.edges(), 0.5)
roll_ring = ring.part.moved(Location(WRIST_CENTER, (90.0, 0.0, 0.0)))
parts.append(_set_metadata(roll_ring, "perpendicular_wrist_roll_ring_od54_id38_thick6", DARK_TITANIUM))
for side, y_offset in (("front", 3.9), ("rear", -3.9)):
for index in range(8):
angle = tau * index / 8.0
x_pos = WRIST_CENTER[0] + 23.7 * cos(angle)
z_pos = WRIST_CENTER[2] + 23.7 * sin(angle)
with BuildPart() as bolt:
Cylinder(radius=2.15, height=1.5, align=(Align.CENTER, Align.CENTER, Align.CENTER))
_safe_fillet(bolt.edges(), 0.5)
part = bolt.part.moved(Location((x_pos, WRIST_CENTER[1] + y_offset, z_pos), (90.0, 0.0, 0.0)))
parts.append(
_set_metadata(
part,
f"wrist_ring_{side}_polished_bolt_head_{index + 1:02d}",
POLISHED_STEEL,
)
)
return parts
def _make_servo_housing(label: str, center: tuple[float, float, float], accent_color: Color):
with BuildPart() as servo:
Box(12.0, 18.0, 8.0, align=(Align.CENTER, Align.CENTER, Align.CENTER))
_safe_fillet(servo.edges(), 1.2)
body = servo.part.moved(Location(center))
body = _set_metadata(body, f"dark_graphite_servo_housing_{label}_12x18x8", DARK_GRAPHITE)
with BuildPart() as panel:
Box(8.0, 2.8, 0.6, align=(Align.CENTER, Align.CENTER, Align.CENTER))
_safe_fillet(panel.edges(), 0.25)
accent = panel.part.moved(Location((center[0], center[1] + 5.5, center[2] + 4.25)))
accent = _set_metadata(accent, f"{label}_servo_blue_amber_accent_panel", accent_color)
screws = []
for sx in (-4.2, 4.2):
for sy in (-6.3, 6.3):
screws.append(
_make_screw_head(
f"{label}_servo_screw_{len(screws) + 1:02d}",
(center[0] + sx, center[1] + sy, center[2] + 4.55),
radius=1.05,
height=0.55,
axis="z",
)
)
return [body, accent, *screws]
def _make_screw_head(label: str, center: tuple[float, float, float], radius: float, height: float, axis: str):
with BuildPart() as screw:
Cylinder(radius=radius, height=height, align=(Align.CENTER, Align.CENTER, Align.CENTER))
_safe_fillet(screw.edges(), 0.35)
rotations = {
"x": (0.0, 90.0, 0.0),
"y": (90.0, 0.0, 0.0),
"z": (0.0, 0.0, 0.0),
}
part = screw.part.moved(Location(center, rotations[axis]))
return _set_metadata(part, f"polished_steel_{label}", POLISHED_STEEL)
def _make_palm_fasteners():
parts = []
locations = [
(-29.0, -22.0),
(29.0, -22.0),
(-29.0, 22.0),
(29.0, 22.0),
(-11.0, -23.0),
(11.0, -23.0),
(-11.0, 24.0),
(11.0, 24.0),
]
for index, (x_pos, y_pos) in enumerate(locations, start=1):
parts.append(
_make_screw_head(
f"palm_frame_socket_screw_{index:02d}",
(x_pos, y_pos, PALM_TOP_Z + 0.45),
radius=1.65,
height=0.9,
axis="z",
)
)
return parts
def _make_mechanical_link(label: str, length: float, start: tuple[float, float, float], flex: float, yaw: float):
with BuildPart() as link:
with BuildSketch(Plane.XY):
RectangleRounded(
FINGER_LINK_WIDTH,
length,
2.0,
align=(Align.CENTER, Align.MIN),
)
extrude(amount=FINGER_LINK_THICKNESS)
with Locations(Location((0.0, length * 0.50, FINGER_LINK_THICKNESS - 0.55))):
Box(
FINGER_LINK_WIDTH * 0.48,
max(5.0, length * 0.48),
1.3,
align=(Align.CENTER, Align.CENTER, Align.CENTER),
mode=Mode.SUBTRACT,
)
for x_pos in (-FINGER_LINK_WIDTH / 2.0 - 0.15, FINGER_LINK_WIDTH / 2.0 + 0.15):
with Locations(Location((x_pos, length * 0.52, FINGER_LINK_THICKNESS * 0.52))):
Box(
1.15,
max(5.0, length * 0.52),
3.3,
align=(Align.CENTER, Align.CENTER, Align.CENTER),
mode=Mode.SUBTRACT,
)
_safe_fillet(link.edges(), 1.0)
part = link.part.moved(Location((0.0, 0.0, -FINGER_LINK_THICKNESS / 2.0)))
part = part.moved(Location(start, (-flex, 0.0, yaw)))
return _set_metadata(part, f"brushed_aluminum_{label}_rounded_recessed_link", BRUSHED_ALUMINUM)
def _segment_end(start: tuple[float, float, float], length: float, flex: float, yaw: float) -> tuple[float, float, float]:
edge = Edge.make_line((0.0, 0.0, 0.0), (0.0, length, 0.0)).moved(
Location(start, (-flex, 0.0, yaw))
)
candidates = [tuple(vertex) for vertex in edge.vertices()]
return max(
candidates,
key=lambda point: (point[0] - start[0]) ** 2
+ (point[1] - start[1]) ** 2
+ (point[2] - start[2]) ** 2,
)
def _make_hinge_pin(label: str, center: tuple[float, float, float], yaw: float):
parts = []
with BuildPart() as pin:
Cylinder(radius=4.0, height=12.0, align=(Align.CENTER, Align.CENTER, Align.CENTER))
_safe_fillet(pin.edges(), 0.5)
body = pin.part.moved(Location(center, (0.0, 90.0, yaw)))
parts.append(_set_metadata(body, f"polished_steel_{label}_hinge_pin_d8", POLISHED_STEEL))
for side, x_local in (("left", -6.8), ("right", 6.8)):
x_pos = center[0] + x_local * cos(radians(yaw))
y_pos = center[1] + x_local * sin(radians(yaw))
with BuildPart() as cap:
Cylinder(radius=5.0, height=1.5, align=(Align.CENTER, Align.CENTER, Align.CENTER))
_safe_fillet(cap.edges(), 0.5)
cap_part = cap.part.moved(Location((x_pos, y_pos, center[2]), (0.0, 90.0, yaw)))
parts.append(_set_metadata(cap_part, f"polished_steel_{label}_{side}_circular_end_cap", POLISHED_STEEL))
screw_offset = 0.85 if side == "right" else -0.85
parts.append(
_make_screw_head(
f"{label}_{side}_visible_cap_screw",
(
center[0] + (x_local + screw_offset) * cos(radians(yaw)),
center[1] + (x_local + screw_offset) * sin(radians(yaw)),
center[2],
),
radius=1.35,
height=0.65,
axis="x",
)
)
return parts
def _make_fingertip_pad(label: str, start: tuple[float, float, float], flex: float, yaw: float):
pad_length = 7.2
with BuildPart() as pad:
with BuildSketch(Plane.XY):
RectangleRounded(9.5, pad_length, 3.3, align=(Align.CENTER, Align.MIN))
extrude(amount=7.2)
_safe_fillet(pad.edges(), 1.0)
part = pad.part.moved(Location((0.0, 0.0, -3.6)))
part = part.moved(Location(start, (-flex, 0.0, yaw)))
return _set_metadata(part, f"matte_black_rubber_{label}_rounded_fingertip_pad", MATTE_BLACK)
def _make_thumb_saddle():
with BuildPart() as saddle:
Box(13.0, 20.0, 9.0, align=(Align.CENTER, Align.CENTER, Align.CENTER))
_safe_fillet(saddle.edges(), 1.0)
part = saddle.part.moved(Location((-35.5, -4.0, PALM_TOP_Z + 1.5), (0.0, 0.0, -28.0)))
return _set_metadata(part, "brushed_aluminum_left_side_thumb_mount_saddle", BRUSHED_ALUMINUM)
def _make_fingers():
parts = []
for finger_name, base in MAIN_FINGER_BASES.items():
yaw = FINGER_YAW_DEGREES[finger_name]
lengths = MAIN_LINK_LENGTHS[finger_name]
flex_angles = MAIN_FINGER_FLEX[finger_name]
joint = base
for joint_index, (length, flex_angle) in enumerate(zip(lengths, flex_angles), start=1):
parts.extend(_make_hinge_pin(f"{finger_name}_joint_{joint_index}", joint, yaw))
parts.append(
_make_mechanical_link(
f"{finger_name}_phalange_{joint_index}_length_{length:.0f}mm",
length,
joint,
flex_angle,
yaw,
)
)
joint = _segment_end(joint, length, flex_angle, yaw)
parts.append(_make_fingertip_pad(finger_name, joint, flex_angles[-1], yaw))
parts.append(_make_thumb_saddle())
thumb_joint = THUMB_BASE
for joint_index, (length, flex_angle, yaw) in enumerate(
zip(THUMB_LINK_LENGTHS, THUMB_FLEX, THUMB_YAWS),
start=1,
):
parts.extend(_make_hinge_pin(f"thumb_joint_{joint_index}", thumb_joint, yaw))
parts.append(
_make_mechanical_link(
f"thumb_phalange_{joint_index}_length_{length:.0f}mm",
length,
thumb_joint,
flex_angle,
yaw,
)
)
thumb_joint = _segment_end(thumb_joint, length, flex_angle, yaw)
parts.append(_make_fingertip_pad("thumb", thumb_joint, THUMB_FLEX[-1], THUMB_YAWS[-1]))
return parts
def _tube_profile_plane(start: tuple[float, float, float], next_point: tuple[float, float, float]) -> Plane:
tangent = (
next_point[0] - start[0],
next_point[1] - start[1],
next_point[2] - start[2],
)
return Plane(origin=start, x_dir=(1.0, 0.0, 0.0), z_dir=tangent)
def _make_tendon_tube(label: str, points: list[tuple[float, float, float]]):
path = Edge.make_spline(points)
with BuildPart() as tube:
with BuildSketch(_tube_profile_plane(points[0], points[1])):
Circle(1.0)
sweep(path=path, is_frenet=True)
_safe_fillet(tube.edges(), 0.2)
return _set_metadata(tube.part, f"matte_black_tendon_tube_{label}_diameter_2mm", MATTE_BLACK)
def _make_servos_and_tendons():
parts = []
servo_centers = {
"thumb": (-24.0, -15.0, PALM_TOP_Z + 5.0),
"index": (-12.0, -13.0, PALM_TOP_Z + 5.0),
"middle": (0.0, -15.5, PALM_TOP_Z + 5.0),
"ring": (12.0, -13.0, PALM_TOP_Z + 5.0),
"little": (24.0, -15.0, PALM_TOP_Z + 5.0),
}
for index, (label, center) in enumerate(servo_centers.items()):
parts.extend(_make_servo_housing(label, center, BLUE_ACCENT if index % 2 else AMBER_ACCENT))
tendon_targets = {
"thumb": THUMB_BASE,
"index": MAIN_FINGER_BASES["index"],
"middle": MAIN_FINGER_BASES["middle"],
"ring": MAIN_FINGER_BASES["ring"],
"little": MAIN_FINGER_BASES["little"],
}
for label, start_center in servo_centers.items():
target = tendon_targets[label]
start = (start_center[0], start_center[1] + 9.0, start_center[2] + 3.7)
if label == "thumb":
points = [
start,
(-36.0, -11.0, PALM_TOP_Z + 14.0),
(-43.5, -3.0, PALM_TOP_Z + 8.0),
(target[0], target[1], target[2] + 1.0),
]
else:
points = [
start,
(start[0] * 0.75, 7.0, PALM_TOP_Z + 14.0),
(target[0] * 0.96, 22.0, PALM_TOP_Z + 8.5),
(target[0], target[1] - 1.5, target[2] + 0.6),
]
parts.append(_make_tendon_tube(label, points))
return parts
def _make_knuckle_yokes():
parts = []
for name, center in MAIN_FINGER_BASES.items():
with BuildPart() as yoke:
Box(12.0, 7.0, 7.0, align=(Align.CENTER, Align.CENTER, Align.CENTER))
Cylinder(
radius=4.25,
height=13.0,
align=(Align.CENTER, Align.CENTER, Align.CENTER),
rotation=(0.0, 90.0, FINGER_YAW_DEGREES[name]),
mode=Mode.SUBTRACT,
)
_safe_fillet(yoke.edges(), 0.8)
part = yoke.part.moved(Location((center[0], center[1] - 2.5, center[2] - 0.6)))
parts.append(_set_metadata(part, f"brushed_aluminum_{name}_base_knuckle_yoke", BRUSHED_ALUMINUM))
return parts
def _make_decorative_hex_fasteners():
parts = []
for index, (x_pos, y_pos, z_pos) in enumerate(
[
(-48.0, -36.0, DISPLAY_BASE_TOP_Z + 0.45),
(48.0, -36.0, DISPLAY_BASE_TOP_Z + 0.45),
(-48.0, 26.0, DISPLAY_BASE_TOP_Z + 0.45),
(48.0, 26.0, DISPLAY_BASE_TOP_Z + 0.45),
],
start=1,
):
with BuildPart() as fastener:
with BuildSketch(Plane.XY):
RegularPolygon(3.2, 6, rotation=30.0)
extrude(amount=0.9)
_safe_fillet(fastener.edges(), 0.25)
part = fastener.part.moved(Location((x_pos, y_pos, z_pos)))
parts.append(_set_metadata(part, f"polished_steel_display_base_hex_fastener_{index:02d}", POLISHED_STEEL))
return parts
def gen_step():
"""Return a labeled cybernetic robotic hand end-effector STEP assembly."""
parts = [
_make_display_base(),
_make_support_post(),
_make_palm_frame(),
*_make_palm_ribs(),
*_make_palm_fasteners(),
*_make_wrist_gimbal(),
*_make_knuckle_yokes(),
*_make_fingers(),
*_make_servos_and_tendons(),
*_make_decorative_hex_fasteners(),
]
assembly = Compound(
obj=parts,
children=parts,
label="articulated_cybernetic_robotic_hand_end_effector",
)
return assembly