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

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