191 lines
6.3 KiB
Python
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)
|