Files
reducers/backend/input/assets/housings/gear_housing.py
T
2026-08-14 10:26:28 +08:00

191 lines
6.3 KiB
Python

from dataclasses import dataclass
from pathlib import Path
import math
import cadquery as cq
# All dimensions are millimetres. The main body is a revolved radial section.
INNER_BOTTOM_RADIUS = 60.0
INNER_STEP_RADIUS = 57.5
INNER_SHOULDER_RADIUS = 70.0
INNER_WALL_RADIUS = 79.0
BOTTOM_OUTER_RADIUS = 77.5
BODY_OUTER_RADIUS = 82.5
TOP_OUTER_RADIUS = 85.0
BASE_MIN_Z = -18.0
BASE_MAX_Z = 92.0
BASE_HEIGHT = BASE_MAX_Z - BASE_MIN_Z
@dataclass(frozen=True)
class HousingScale:
radial_scale: float = 1.0
axial_scale: float = 1.0
source: str = "default_template_scale"
def scaled_dimensions(scale: HousingScale) -> dict[str, float]:
return {
"inner_bottom_radius": INNER_BOTTOM_RADIUS * scale.radial_scale,
"inner_step_radius": INNER_STEP_RADIUS * scale.radial_scale,
"inner_shoulder_radius": INNER_SHOULDER_RADIUS * scale.radial_scale,
"inner_wall_radius": INNER_WALL_RADIUS * scale.radial_scale,
"bottom_outer_radius": BOTTOM_OUTER_RADIUS * scale.radial_scale,
"body_outer_radius": BODY_OUTER_RADIUS * scale.radial_scale,
"top_outer_radius": TOP_OUTER_RADIUS * scale.radial_scale,
"min_z": BASE_MIN_Z * scale.axial_scale,
"max_z": BASE_MAX_Z * scale.axial_scale,
"height": BASE_HEIGHT * scale.axial_scale,
"radial_scale": scale.radial_scale,
"axial_scale": scale.axial_scale,
"source": scale.source,
}
def scale_from_motor(
*,
motor_radial_diameter_mm: float,
motor_axis_length_mm: float,
radial_clearance_mm: float = 14.0,
reducer_axial_allowance_mm: float = 42.0,
min_radial_scale: float = 0.45,
max_radial_scale: float = 1.50,
min_axial_scale: float = 0.42,
max_axial_scale: float = 1.80,
) -> HousingScale:
target_inner_radius = motor_radial_diameter_mm / 2.0 + radial_clearance_mm
radial_scale = target_inner_radius / INNER_WALL_RADIUS
target_height = motor_axis_length_mm + reducer_axial_allowance_mm
axial_scale = target_height / BASE_HEIGHT
return HousingScale(
radial_scale=max(min_radial_scale, min(max_radial_scale, radial_scale)),
axial_scale=max(min_axial_scale, min(max_axial_scale, axial_scale)),
source="computed_from_motor_bbox",
)
def _polar_points(radius: float, start_degrees: float, count: int):
return [
(
radius * math.cos(math.radians(start_degrees + 360.0 * index / count)),
radius * math.sin(math.radians(start_degrees + 360.0 * index / count)),
)
for index in range(count)
]
def _radial_point(radius: float, angle_degrees: float, z: float) -> cq.Vector:
angle = math.radians(angle_degrees)
return cq.Vector(radius * math.cos(angle), radius * math.sin(angle), z)
def _radial_direction(angle_degrees: float) -> cq.Vector:
angle = math.radians(angle_degrees)
return cq.Vector(math.cos(angle), math.sin(angle), 0.0)
def build_body(scale: HousingScale = HousingScale()):
rd = scale.radial_scale
zd = scale.axial_scale
def r(value: float) -> float:
return value * rd
def z(value: float) -> float:
return value * zd
# Exact circular-arc midpoints for the two revolved outer blends.
lower_arc_mid = (r(78.8397459621556), z(-8.66025403784439))
upper_arc_mid = (r(83.1458565330652), z(71.9211556282713))
radial_section = (
cq.Workplane("XZ")
.moveTo(r(INNER_BOTTOM_RADIUS), z(-18.0))
.lineTo(r(BOTTOM_OUTER_RADIUS), z(-18.0))
.lineTo(r(BOTTOM_OUTER_RADIUS), z(-13.6602540378))
.threePointArc(lower_arc_mid, (r(BODY_OUTER_RADIUS), z(-5.0)))
.lineTo(r(BODY_OUTER_RADIUS), z(68.3856217223))
.threePointArc(upper_arc_mid, (r(TOP_OUTER_RADIUS), z(75.0)))
.lineTo(r(TOP_OUTER_RADIUS), z(92.0))
.lineTo(r(INNER_WALL_RADIUS), z(92.0))
.lineTo(r(INNER_WALL_RADIUS), z(0.0))
.lineTo(r(INNER_SHOULDER_RADIUS), z(0.0))
.lineTo(r(INNER_SHOULDER_RADIUS), z(-3.0))
.lineTo(r(INNER_STEP_RADIUS), z(-3.0))
.lineTo(r(INNER_STEP_RADIUS), z(-5.0))
.lineTo(r(INNER_BOTTOM_RADIUS), z(-5.0))
.close()
)
return radial_section.revolve(360.0, (0.0, 0.0), (0.0, 1.0))
def build(scale: HousingScale = HousingScale()):
rd = scale.radial_scale
zd = scale.axial_scale
def r(value: float) -> float:
return value * rd
def z(value: float) -> float:
return value * zd
part = build_body(scale)
# Ten Ø5 blind drilled holes, equally spaced on a Ø135 pitch circle.
drill_tip_height = z(1.5021515475689)
for x, y in _polar_points(r(67.5), 18.0, 10):
straight_hole = cq.Solid.makeCylinder(
r(2.5), z(10.0), cq.Vector(x, y, z(-18.0)), cq.Vector(0.0, 0.0, 1.0)
)
drill_tip = cq.Solid.makeCone(
r(2.5),
0.0,
drill_tip_height,
cq.Vector(x, y, z(-8.0)),
cq.Vector(0.0, 0.0, 1.0),
)
part = part.cut(straight_hole).cut(drill_tip)
# Four Ø2.5 blind holes opening at z=0, with 118-degree drill tips.
small_tip_height = z(0.75107577378445)
for x, y in _polar_points(r(71.875), 0.0, 4):
straight_hole = cq.Solid.makeCylinder(
r(1.25), z(4.24892422621555), cq.Vector(x, y, z(0.0)), cq.Vector(0.0, 0.0, -1.0)
)
drill_tip = cq.Solid.makeCone(
r(1.25),
0.0,
small_tip_height,
cq.Vector(x, y, z(-4.24892422621555)),
cq.Vector(0.0, 0.0, -1.0),
)
part = part.cut(straight_hole).cut(drill_tip)
# Eight radial Ø6.6 holes with 90-degree countersinks around the top rim.
for angle in range(0, 360, 45):
direction = _radial_direction(float(angle))
straight_hole = cq.Solid.makeCylinder(
r(3.3), r(3.9), _radial_point(r(78.0), float(angle), z(85.1)), direction
)
countersink = cq.Solid.makeCone(
r(3.3),
r(6.4),
r(3.1),
_radial_point(r(81.9), float(angle), z(85.1)),
direction,
)
part = part.cut(straight_hole).cut(countersink)
return part
def export_scaled(output: Path, scale: HousingScale) -> None:
cq.exporters.export(build(scale), str(output))
if __name__ == "__main__":
output = Path(__file__).resolve().with_name("gear_housing.step")
export_scaled(output, HousingScale())
print(output)