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)