339 lines
13 KiB
Python
339 lines
13 KiB
Python
from __future__ import annotations
|
|
|
|
import math
|
|
from collections.abc import Sequence
|
|
|
|
from build123d import (
|
|
Box,
|
|
Color,
|
|
Compound,
|
|
Cone,
|
|
Cylinder,
|
|
Location,
|
|
Shape,
|
|
Sphere,
|
|
Torus,
|
|
chamfer,
|
|
fillet,
|
|
scale,
|
|
)
|
|
from OCP.BRepBuilderAPI import BRepBuilderAPI_Transform
|
|
from OCP.BRepPrimAPI import BRepPrimAPI_MakeCylinder
|
|
from OCP.gp import gp_Ax2, gp_Dir, gp_Pnt, gp_Trsf
|
|
|
|
|
|
DISPLAY_NAME = "Pelican riding a bicycle"
|
|
|
|
# Units: millimeters.
|
|
# Origin: halfway between the wheel hubs on the bicycle center plane.
|
|
# X: rear-to-front bicycle direction, Y: wheel axle direction, +Z: up.
|
|
|
|
COLORS = {
|
|
"rubber_black": (0.003, 0.003, 0.003, 1.0),
|
|
"rim_silver": (0.70, 0.72, 0.70, 1.0),
|
|
"spoke_steel": (0.82, 0.84, 0.82, 1.0),
|
|
"frame_blue": (0.05, 0.28, 0.75, 1.0),
|
|
"seat_brown": (0.28, 0.12, 0.05, 1.0),
|
|
"pelican_white": (0.96, 0.95, 0.88, 1.0),
|
|
"wing_gray": (0.66, 0.68, 0.64, 1.0),
|
|
"feather_dark": (0.08, 0.08, 0.075, 1.0),
|
|
"bill_yellow": (1.00, 0.78, 0.26, 1.0),
|
|
"pouch_orange": (0.95, 0.45, 0.12, 1.0),
|
|
"leg_orange": (0.98, 0.50, 0.12, 1.0),
|
|
"eye_black": (0.0, 0.0, 0.0, 1.0),
|
|
}
|
|
|
|
Vector3 = tuple[float, float, float]
|
|
|
|
|
|
def _v_add(a: Vector3, b: Vector3) -> Vector3:
|
|
return (a[0] + b[0], a[1] + b[1], a[2] + b[2])
|
|
|
|
|
|
def _v_sub(a: Vector3, b: Vector3) -> Vector3:
|
|
return (a[0] - b[0], a[1] - b[1], a[2] - b[2])
|
|
|
|
|
|
def _v_mul(a: Vector3, scalar: float) -> Vector3:
|
|
return (a[0] * scalar, a[1] * scalar, a[2] * scalar)
|
|
|
|
|
|
def _v_dot(a: Vector3, b: Vector3) -> float:
|
|
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
|
|
|
|
|
|
def _v_cross(a: Vector3, b: Vector3) -> Vector3:
|
|
return (
|
|
a[1] * b[2] - a[2] * b[1],
|
|
a[2] * b[0] - a[0] * b[2],
|
|
a[0] * b[1] - a[1] * b[0],
|
|
)
|
|
|
|
|
|
def _v_len(a: Vector3) -> float:
|
|
return math.sqrt(_v_dot(a, a))
|
|
|
|
|
|
def _v_norm(a: Vector3) -> Vector3:
|
|
length = _v_len(a)
|
|
if length <= 1e-9:
|
|
raise ValueError("zero-length vector")
|
|
return (a[0] / length, a[1] / length, a[2] / length)
|
|
|
|
|
|
def _style(shape: Shape, label: str, color_name: str) -> Shape:
|
|
shape.label = label
|
|
shape.color = Color(*COLORS[color_name])
|
|
return shape
|
|
|
|
|
|
def _group(label: str, children: Sequence[Shape], color_name: str | None = None) -> Compound:
|
|
compound = Compound(obj=list(children), children=list(children), label=label)
|
|
if color_name is not None:
|
|
compound.color = Color(*COLORS[color_name])
|
|
return compound
|
|
|
|
|
|
def _softened(shape: Shape, radius: float) -> Shape:
|
|
try:
|
|
return fillet(shape.edges(), radius=radius)
|
|
except Exception:
|
|
try:
|
|
return chamfer(shape.edges(), length=radius * 0.45)
|
|
except Exception:
|
|
return shape
|
|
|
|
|
|
def _transformed(shape: Shape, center: Vector3, axis_x: Vector3, axis_y: Vector3, axis_z: Vector3) -> Shape:
|
|
x_dir = _v_norm(axis_x)
|
|
y_dir = _v_norm(axis_y)
|
|
z_dir = _v_norm(axis_z)
|
|
trsf = gp_Trsf()
|
|
trsf.SetValues(
|
|
x_dir[0],
|
|
y_dir[0],
|
|
z_dir[0],
|
|
center[0],
|
|
x_dir[1],
|
|
y_dir[1],
|
|
z_dir[1],
|
|
center[1],
|
|
x_dir[2],
|
|
y_dir[2],
|
|
z_dir[2],
|
|
center[2],
|
|
)
|
|
return Shape(obj=BRepBuilderAPI_Transform(shape.wrapped, trsf, True).Shape())
|
|
|
|
|
|
def _oriented_box(
|
|
label: str,
|
|
center: Vector3,
|
|
size_x: float,
|
|
size_y: float,
|
|
size_z: float,
|
|
axis_x: Vector3,
|
|
axis_y: Vector3,
|
|
axis_z: Vector3,
|
|
color_name: str,
|
|
*,
|
|
radius: float = 0.0,
|
|
) -> Shape:
|
|
shape = _transformed(Box(size_x, size_y, size_z), center, axis_x, axis_y, axis_z)
|
|
if radius > 0.0:
|
|
shape = _softened(shape, radius)
|
|
return _style(shape, label, color_name)
|
|
|
|
|
|
def _cylinder_between(label: str, p1: Vector3, p2: Vector3, radius: float, color_name: str) -> Shape:
|
|
axis = _v_sub(p2, p1)
|
|
length = _v_len(axis)
|
|
if length <= 1e-6:
|
|
return _style(Sphere(radius).moved(Location(p1)), label, color_name)
|
|
direction = _v_norm(axis)
|
|
wrapped = BRepPrimAPI_MakeCylinder(
|
|
gp_Ax2(gp_Pnt(*p1), gp_Dir(*direction)),
|
|
radius,
|
|
length,
|
|
).Shape()
|
|
return _style(Shape(obj=wrapped), label, color_name)
|
|
|
|
|
|
def _cylinder_y(label: str, center: Vector3, radius: float, length: float, color_name: str) -> Shape:
|
|
return _style(Cylinder(radius, length, rotation=(90, 0, 0)).moved(Location(center)), label, color_name)
|
|
|
|
|
|
def _cylinder_x(label: str, center: Vector3, radius: float, length: float, color_name: str) -> Shape:
|
|
return _style(Cylinder(radius, length, rotation=(0, 90, 0)).moved(Location(center)), label, color_name)
|
|
|
|
|
|
def _ellipsoid(label: str, center: Vector3, radii: Vector3, color_name: str) -> Shape:
|
|
shape = scale(Sphere(1.0), by=radii).moved(Location(center))
|
|
return _style(shape, label, color_name)
|
|
|
|
|
|
def _flat_cone_x(
|
|
label: str,
|
|
center: Vector3,
|
|
length: float,
|
|
base_radius: float,
|
|
tip_radius: float,
|
|
y_scale: float,
|
|
z_scale: float,
|
|
color_name: str,
|
|
) -> Shape:
|
|
cone = Cone(base_radius, tip_radius, length, rotation=(0, 90, 0))
|
|
shape = scale(cone, by=(1.0, y_scale, z_scale)).moved(Location(center))
|
|
return _style(shape, label, color_name)
|
|
|
|
|
|
def _make_wheel(label: str, center: Vector3) -> Compound:
|
|
cx, cy, cz = center
|
|
tire_major_radius = 40.0
|
|
tire_minor_radius = 4.0
|
|
rim_radius = 31.0
|
|
spoke_radius = 0.72
|
|
hub_radius = 4.2
|
|
spoke_count = 20
|
|
|
|
parts: list[Shape] = [
|
|
_style(
|
|
Torus(tire_major_radius, tire_minor_radius, rotation=(90, 0, 0)).moved(Location(center)),
|
|
f"{label}_continuous_black_tire",
|
|
"rubber_black",
|
|
),
|
|
_style(
|
|
Torus(rim_radius, 1.45, rotation=(90, 0, 0)).moved(Location(center)),
|
|
f"{label}_silver_rim",
|
|
"rim_silver",
|
|
),
|
|
_cylinder_y(f"{label}_hub_barrel", center, hub_radius, 14.0, "rim_silver"),
|
|
_cylinder_y(f"{label}_hub_axle_pin", center, 1.8, 24.0, "spoke_steel"),
|
|
]
|
|
|
|
for index in range(spoke_count):
|
|
theta = index * math.tau / spoke_count
|
|
rim_point = (cx + math.cos(theta) * (rim_radius - 1.8), cy, cz + math.sin(theta) * (rim_radius - 1.8))
|
|
opposite_offset = -0.9 if index % 2 == 0 else 0.9
|
|
hub_point = (cx, cy + opposite_offset, cz)
|
|
parts.append(_cylinder_between(f"{label}_radial_spoke_{index + 1:02d}", hub_point, rim_point, spoke_radius, "spoke_steel"))
|
|
|
|
return _group(label, parts)
|
|
|
|
|
|
def _make_bicycle() -> Compound:
|
|
rear = (-55.0, 0.0, 44.0)
|
|
front = (55.0, 0.0, 44.0)
|
|
bottom = (0.0, 0.0, 49.0)
|
|
seat_lug = (-15.0, 0.0, 86.0)
|
|
head_low = (43.0, 0.0, 66.0)
|
|
head_high = (38.0, 0.0, 88.0)
|
|
handle_stem = (57.0, 0.0, 99.0)
|
|
|
|
parts: list[Shape] = [
|
|
_make_wheel("rear_wheel", rear),
|
|
_make_wheel("front_wheel", front),
|
|
_cylinder_between("frame_seat_tube", bottom, seat_lug, 2.3, "frame_blue"),
|
|
_cylinder_between("frame_top_tube", seat_lug, head_high, 2.2, "frame_blue"),
|
|
_cylinder_between("frame_down_tube", bottom, head_low, 2.5, "frame_blue"),
|
|
_cylinder_between("frame_head_tube", head_low, head_high, 2.6, "frame_blue"),
|
|
_cylinder_between("frame_left_chain_stay", (bottom[0], -4.2, bottom[2]), (rear[0], -4.2, rear[2]), 1.7, "frame_blue"),
|
|
_cylinder_between("frame_right_chain_stay", (bottom[0], 4.2, bottom[2]), (rear[0], 4.2, rear[2]), 1.7, "frame_blue"),
|
|
_cylinder_between("frame_left_seat_stay", (seat_lug[0], -3.8, seat_lug[2]), (rear[0], -3.8, rear[2]), 1.55, "frame_blue"),
|
|
_cylinder_between("frame_right_seat_stay", (seat_lug[0], 3.8, seat_lug[2]), (rear[0], 3.8, rear[2]), 1.55, "frame_blue"),
|
|
_cylinder_between("front_left_fork_blade", (head_low[0], -4.0, head_low[2]), (front[0], -4.0, front[2]), 1.7, "frame_blue"),
|
|
_cylinder_between("front_right_fork_blade", (head_low[0], 4.0, head_low[2]), (front[0], 4.0, front[2]), 1.7, "frame_blue"),
|
|
_cylinder_between("handlebar_stem", head_high, handle_stem, 1.8, "frame_blue"),
|
|
_cylinder_y("handlebar_crossbar", (64.0, 0.0, 101.0), 1.6, 38.0, "frame_blue"),
|
|
_cylinder_y("left_black_grip", (64.0, -22.5, 101.0), 2.2, 8.0, "rubber_black"),
|
|
_cylinder_y("right_black_grip", (64.0, 22.5, 101.0), 2.2, 8.0, "rubber_black"),
|
|
_cylinder_between("seat_post", seat_lug, (-17.0, 0.0, 95.0), 1.75, "spoke_steel"),
|
|
_style(_softened(Box(24.0, 17.0, 5.0).moved(Location((-20.0, 0.0, 97.0))), 1.2), "brown_saddle", "seat_brown"),
|
|
_style(Torus(7.5, 0.9, rotation=(90, 0, 0)).moved(Location(bottom)), "crank_ring", "spoke_steel"),
|
|
_cylinder_y("bottom_bracket_axle", bottom, 3.3, 20.0, "rim_silver"),
|
|
]
|
|
|
|
pedal_front = (12.5, -11.0, 38.5)
|
|
pedal_back = (-12.5, 11.0, 60.5)
|
|
parts.extend(
|
|
[
|
|
_cylinder_between("front_crank_arm", bottom, pedal_front, 1.0, "spoke_steel"),
|
|
_cylinder_between("rear_crank_arm", bottom, pedal_back, 1.0, "spoke_steel"),
|
|
_style(_softened(Box(15.0, 4.0, 2.5).moved(Location(pedal_front)), 0.7), "front_pedal_flat", "rubber_black"),
|
|
_style(_softened(Box(15.0, 4.0, 2.5).moved(Location(pedal_back)), 0.7), "rear_pedal_flat", "rubber_black"),
|
|
]
|
|
)
|
|
|
|
return _group("bicycle", parts, "frame_blue")
|
|
|
|
|
|
def _make_pelican() -> Compound:
|
|
parts: list[Shape] = [
|
|
_ellipsoid("pelican_round_body", (-12.0, 0.0, 120.0), (24.0, 15.0, 29.0), "pelican_white"),
|
|
_ellipsoid("pelican_chest_belly_highlight", (1.0, 0.0, 112.0), (18.0, 13.0, 20.0), "pelican_white"),
|
|
_ellipsoid("left_folded_wing", (-22.0, -13.0, 120.0), (19.0, 3.2, 25.0), "wing_gray"),
|
|
_ellipsoid("right_folded_wing", (-22.0, 13.0, 120.0), (19.0, 3.2, 25.0), "wing_gray"),
|
|
_ellipsoid("left_dark_wing_tip", (-33.0, -15.7, 104.0), (12.0, 2.2, 10.0), "feather_dark"),
|
|
_ellipsoid("right_dark_wing_tip", (-33.0, 15.7, 104.0), (12.0, 2.2, 10.0), "feather_dark"),
|
|
_cylinder_between("curved_neck_lower_segment", (-1.0, 0.0, 139.0), (12.0, 0.0, 151.0), 5.0, "pelican_white"),
|
|
_cylinder_between("curved_neck_upper_segment", (12.0, 0.0, 151.0), (24.0, 0.0, 157.0), 4.4, "pelican_white"),
|
|
_ellipsoid("pelican_head", (28.0, 0.0, 160.0), (10.5, 8.1, 9.5), "pelican_white"),
|
|
_flat_cone_x("long_upper_bill", (52.0, 0.0, 160.5), 48.0, 5.2, 1.1, 1.45, 0.55, "bill_yellow"),
|
|
_ellipsoid("rounded_lower_bill_pouch", (49.0, 0.0, 151.5), (23.0, 7.0, 7.2), "pouch_orange"),
|
|
_cylinder_between("dark_bill_seam_left", (31.0, -7.3, 156.2), (70.0, -2.7, 157.0), 0.45, "feather_dark"),
|
|
_cylinder_between("dark_bill_seam_right", (31.0, 7.3, 156.2), (70.0, 2.7, 157.0), 0.45, "feather_dark"),
|
|
_ellipsoid("left_eye", (31.5, -7.4, 162.8), (1.45, 0.75, 1.45), "eye_black"),
|
|
_ellipsoid("right_eye", (31.5, 7.4, 162.8), (1.45, 0.75, 1.45), "eye_black"),
|
|
_ellipsoid("soft_head_crest", (19.0, 0.0, 168.0), (4.0, 4.0, 5.0), "pelican_white"),
|
|
]
|
|
|
|
tail_roots = [(-34.0, -4.5, 108.0), (-38.0, 0.0, 110.0), (-34.0, 4.5, 108.0)]
|
|
for index, center in enumerate(tail_roots, start=1):
|
|
parts.append(_ellipsoid(f"short_tail_feather_{index}", center, (11.0, 2.0, 5.2), "feather_dark"))
|
|
|
|
# Bent orange legs and webbed feet make the bird visibly seated on the cycle.
|
|
left_hip = (-8.0, -5.7, 96.0)
|
|
left_knee = (2.5, -7.5, 74.0)
|
|
left_pedal = (12.5, -11.0, 38.5)
|
|
right_hip = (-13.0, 5.7, 96.0)
|
|
right_knee = (-19.0, 8.4, 74.0)
|
|
right_pedal = (-12.5, 11.0, 60.5)
|
|
parts.extend(
|
|
[
|
|
_cylinder_between("left_upper_leg", left_hip, left_knee, 1.8, "leg_orange"),
|
|
_cylinder_between("left_lower_leg", left_knee, left_pedal, 1.6, "leg_orange"),
|
|
_cylinder_between("right_upper_leg", right_hip, right_knee, 1.8, "leg_orange"),
|
|
_cylinder_between("right_lower_leg", right_knee, right_pedal, 1.6, "leg_orange"),
|
|
_ellipsoid("left_webbed_foot_on_forward_pedal", _v_add(left_pedal, (1.5, 0.0, 2.0)), (8.5, 3.5, 2.2), "leg_orange"),
|
|
_ellipsoid("right_webbed_foot_on_rear_pedal", _v_add(right_pedal, (-1.5, 0.0, 2.0)), (8.5, 3.5, 2.2), "leg_orange"),
|
|
]
|
|
)
|
|
|
|
feather_line_specs = [
|
|
("left_wing_feather_line_1", (-17.0, -16.2, 136.0), (-33.0, -16.2, 112.0)),
|
|
("left_wing_feather_line_2", (-11.0, -16.4, 126.0), (-34.0, -16.4, 101.0)),
|
|
("right_wing_feather_line_1", (-17.0, 16.2, 136.0), (-33.0, 16.2, 112.0)),
|
|
("right_wing_feather_line_2", (-11.0, 16.4, 126.0), (-34.0, 16.4, 101.0)),
|
|
]
|
|
for label, p1, p2 in feather_line_specs:
|
|
parts.append(_cylinder_between(label, p1, p2, 0.55, "feather_dark"))
|
|
|
|
return _group("pelican", parts, "pelican_white")
|
|
|
|
|
|
def _make_ground_shadow() -> Compound:
|
|
# Thin reference pads give the model a stable visual base without merging
|
|
# into the wheels.
|
|
front_pad = _style(_softened(Box(92.0, 28.0, 1.0).moved(Location((55.0, 0.0, -2.0))), 1.0), "front_wheel_ground_shadow", "rubber_black")
|
|
rear_pad = _style(_softened(Box(92.0, 28.0, 1.0).moved(Location((-55.0, 0.0, -2.0))), 1.0), "rear_wheel_ground_shadow", "rubber_black")
|
|
return _group("subtle_ground_shadow", [front_pad, rear_pad])
|
|
|
|
|
|
def gen_step() -> Compound:
|
|
bicycle = _make_bicycle()
|
|
pelican = _make_pelican()
|
|
ground = _make_ground_shadow()
|
|
assembly = _group("pelican_riding_bicycle", [bicycle, pelican, ground])
|
|
assembly.label = "pelican_riding_bicycle"
|
|
return assembly
|