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

283 lines
8.2 KiB
Python

from math import cos, pi, sin, tau
from build123d import (
Align,
BuildPart,
BuildSketch,
Color,
Compound,
Cylinder,
Location,
Locations,
Mode,
Plane,
Polygon,
extrude,
)
# Units: millimeters.
# Origin: sun/ring center on the carrier axis.
# XY: gear plane. +Z: gear and pin axes.
GEAR_THICKNESS = 8.0
GEAR_BOTTOM_Z = 0.0
SUN_TEETH = 24
SUN_PITCH_DIAMETER = 48.0
SUN_ROOT_DIAMETER = 42.0
SUN_OUTSIDE_DIAMETER = 54.0
SUN_BORE_DIAMETER = 10.0
PLANET_TEETH = 18
PLANET_PITCH_DIAMETER = 36.0
PLANET_ROOT_DIAMETER = 31.0
PLANET_OUTSIDE_DIAMETER = 41.0
PLANET_CENTER_RADIUS = 42.0
PLANET_COUNT = 3
PLANET_BORE_DIAMETER = 6.8
EXTERNAL_TOOTH_ROOT_SPAN_FRACTION = 0.42
EXTERNAL_TOOTH_TIP_SPAN_FRACTION = 0.18
def _srgb_channel_to_linear(channel: int) -> float:
value = channel / 255.0
if value <= 0.04045:
return value / 12.92
return ((value + 0.055) / 1.055) ** 2.4
def _srgb_color(hex_color: str) -> Color:
value = hex_color.removeprefix("#")
if len(value) != 6:
raise ValueError(f"Expected #rrggbb color, got {hex_color!r}")
return Color(
_srgb_channel_to_linear(int(value[0:2], 16)),
_srgb_channel_to_linear(int(value[2:4], 16)),
_srgb_channel_to_linear(int(value[4:6], 16)),
1.0,
)
# Match the docs app's dark-mode hero render source colors.
PLANET_COLORS = (
_srgb_color("#61d4f6"),
_srgb_color("#a6ea90"),
_srgb_color("#ed9ee5"),
)
RING_TEETH = 60
RING_INTERNAL_PITCH_DIAMETER = 120.0
RING_INTERNAL_ROOT_DIAMETER = 126.0
RING_INTERNAL_TIP_DIAMETER = 115.0
RING_OUTSIDE_DIAMETER = 140.0
RING_TOOTH_ROOT_SPAN_FRACTION = 0.68
RING_TOOTH_TIP_SPAN_FRACTION = 0.34
RING_COLOR = _srgb_color("#c5adef")
SUN_COLOR = _srgb_color("#fdce76")
CARRIER_COLOR = _srgb_color("#bfcec8")
PIN_COLOR = _srgb_color("#818993")
CARRIER_DIAMETER = 105.0
CARRIER_BOTTOM_Z = -5.0
CARRIER_TOP_Z = -1.0
CARRIER_THICKNESS = CARRIER_TOP_Z - CARRIER_BOTTOM_Z
PIN_DIAMETER = 6.0
PIN_HEIGHT = 14.0
PIN_BOTTOM_Z = -5.0
PIN_CARRIER_CLEARANCE_DIAMETER = 6.4
def _polar_point(radius: float, angle: float) -> tuple[float, float]:
return (radius * cos(angle), radius * sin(angle))
def _trapezoid_tooth_profile(
*,
teeth: int,
root_radius: float,
tip_radius: float,
phase: float,
root_span_fraction: float = 0.72,
tip_span_fraction: float = 0.38,
) -> list[tuple[float, float]]:
"""Return a closed-profile point loop with straight-sided schematic teeth."""
pitch_angle = tau / teeth
points: list[tuple[float, float]] = []
for tooth_index in range(teeth):
center_angle = phase + tooth_index * pitch_angle
points.extend(
(
_polar_point(root_radius, center_angle - root_span_fraction * pitch_angle / 2.0),
_polar_point(tip_radius, center_angle - tip_span_fraction * pitch_angle / 2.0),
_polar_point(tip_radius, center_angle + tip_span_fraction * pitch_angle / 2.0),
_polar_point(root_radius, center_angle + root_span_fraction * pitch_angle / 2.0),
)
)
return points
def _make_external_gear(
*,
label: str,
teeth: int,
root_diameter: float,
outside_diameter: float,
phase: float,
center: tuple[float, float] = (0.0, 0.0),
bore_diameter: float | None = None,
color: Color | None = None,
):
with BuildPart() as gear:
with BuildSketch(Plane.XY):
Polygon(
_trapezoid_tooth_profile(
teeth=teeth,
root_radius=root_diameter / 2.0,
tip_radius=outside_diameter / 2.0,
phase=phase,
root_span_fraction=EXTERNAL_TOOTH_ROOT_SPAN_FRACTION,
tip_span_fraction=EXTERNAL_TOOTH_TIP_SPAN_FRACTION,
),
align=None,
)
extrude(amount=GEAR_THICKNESS)
if bore_diameter is not None:
with Locations(Location((0.0, 0.0, -0.5))):
Cylinder(
radius=bore_diameter / 2.0,
height=GEAR_THICKNESS + 1.0,
align=(Align.CENTER, Align.CENTER, Align.MIN),
mode=Mode.SUBTRACT,
)
part = gear.part.moved(Location((center[0], center[1], GEAR_BOTTOM_Z)))
part.label = label
part.color = color
return part
def _make_internal_ring_gear(*, label: str, phase: float, color: Color | None = None):
with BuildPart() as ring:
Cylinder(
radius=RING_OUTSIDE_DIAMETER / 2.0,
height=GEAR_THICKNESS,
align=(Align.CENTER, Align.CENTER, Align.MIN),
)
with BuildSketch(Plane.XY):
Polygon(
_trapezoid_tooth_profile(
teeth=RING_TEETH,
root_radius=RING_INTERNAL_ROOT_DIAMETER / 2.0,
tip_radius=RING_INTERNAL_TIP_DIAMETER / 2.0,
phase=phase,
root_span_fraction=RING_TOOTH_ROOT_SPAN_FRACTION,
tip_span_fraction=RING_TOOTH_TIP_SPAN_FRACTION,
),
align=None,
)
extrude(amount=GEAR_THICKNESS, mode=Mode.SUBTRACT)
part = ring.part
part.label = label
part.color = color
return part
def _make_carrier_plate():
with BuildPart() as carrier:
with Locations(Location((0.0, 0.0, CARRIER_BOTTOM_Z))):
Cylinder(
radius=CARRIER_DIAMETER / 2.0,
height=CARRIER_THICKNESS,
align=(Align.CENTER, Align.CENTER, Align.MIN),
)
for index in range(PLANET_COUNT):
center = _polar_point(PLANET_CENTER_RADIUS, tau * index / PLANET_COUNT)
with Locations(Location((center[0], center[1], CARRIER_BOTTOM_Z - 0.1))):
Cylinder(
radius=PIN_CARRIER_CLEARANCE_DIAMETER / 2.0,
height=CARRIER_THICKNESS + 0.2,
align=(Align.CENTER, Align.CENTER, Align.MIN),
mode=Mode.SUBTRACT,
)
part = carrier.part
part.label = "carrier_plate"
part.color = CARRIER_COLOR
return part
def _make_planet_pin(*, label: str, center: tuple[float, float]):
with BuildPart() as pin:
with Locations(Location((center[0], center[1], PIN_BOTTOM_Z))):
Cylinder(
radius=PIN_DIAMETER / 2.0,
height=PIN_HEIGHT,
align=(Align.CENTER, Align.CENTER, Align.MIN),
)
part = pin.part
part.label = label
part.color = PIN_COLOR
return part
def _planet_center(index: int) -> tuple[float, float]:
angle = tau * index / PLANET_COUNT
return _polar_point(PLANET_CENTER_RADIUS, angle)
def gen_step():
"""Return a labeled simplified planetary gear assembly in millimeters."""
sun_pitch_angle = tau / SUN_TEETH
ring_pitch_angle = tau / RING_TEETH
parts = [
_make_carrier_plate(),
_make_internal_ring_gear(
label="ring_gear_60_internal_teeth",
phase=-ring_pitch_angle / 2.0,
color=RING_COLOR,
),
_make_external_gear(
label="sun_gear_24_teeth",
teeth=SUN_TEETH,
root_diameter=SUN_ROOT_DIAMETER,
outside_diameter=SUN_OUTSIDE_DIAMETER,
phase=-sun_pitch_angle / 2.0,
bore_diameter=SUN_BORE_DIAMETER,
color=SUN_COLOR,
),
]
for index in range(PLANET_COUNT):
planet_angle = tau * index / PLANET_COUNT
center = _planet_center(index)
parts.append(
_make_external_gear(
label=f"planet_gear_{index + 1}_18_teeth",
teeth=PLANET_TEETH,
root_diameter=PLANET_ROOT_DIAMETER,
outside_diameter=PLANET_OUTSIDE_DIAMETER,
phase=planet_angle,
center=center,
bore_diameter=PLANET_BORE_DIAMETER,
color=PLANET_COLORS[index],
)
)
parts.append(_make_planet_pin(label=f"planet_pin_{index + 1}", center=center))
assembly = Compound(
obj=parts,
children=parts,
label="simplified_planetary_gear_assembly",
)
return assembly