Files
2026-07-22 13:48:46 +08:00

593 lines
21 KiB
Python

"""Bolt-aligned link, crank, pushrod, shank, and wheel parts for Example 18."""
from __future__ import annotations
import math
import simplecadapi as scad
from leg_common import (
make_bolt_circle_cutters_rsolidlist,
make_part_with_connectors_rpart,
make_rounded_bar_rsolid,
make_rounded_slot_cutter_rsolid,
)
from brackets import make_split_case_clamp_rsolid
from leg_dimensions import (
CASE_CLAMP_PINCH_AXIS_RADIUS,
CASE_CLAMP_PINCH_HALF_SPAN,
CASE_CLAMP_WIDTH,
DISTAL_CRANK_LENGTH,
DISTAL_CRANK_THICKNESS,
DISTAL_CRANK_WIDTH,
DISTAL_CRANK_Z,
DISTAL_PUSHROD_PIN,
KNEE_AXIS,
KNEE_BEARING_BORE_RADIUS,
KNEE_BEARING_OUTER_RADIUS,
KNEE_DRIVE_AXIS,
KNEE_PIVOT_Z,
OUTPUT_FLANGE_BOLT_ANGLES_DEGREES,
OUTPUT_FLANGE_BOLT_CIRCLE_RADIUS,
OUTPUT_FLANGE_BOLT_CLEARANCE_RADIUS,
OUTPUT_FLANGE_BOLT_COUNTERBORE_RADIUS,
OUTPUT_FLANGE_OUTER_RADIUS,
OUTPUT_FLANGE_REGISTER_INNER_RADIUS,
PIN_CLEARANCE_RADIUS,
PROXIMAL_PUSHROD_PIN,
PUSHROD_THICKNESS,
PUSHROD_WIDTH,
PUSHROD_Z,
REMOTE_CRANK_LENGTH,
REMOTE_CRANK_THICKNESS,
REMOTE_CRANK_WIDTH,
REMOTE_CRANK_Z,
ROD_PIN_AXIS_Z,
ROD_PIN_CLEARANCE_RADIUS,
ROOT_AXIS,
SHANK_KNEE_RADIUS,
SHANK_LENGTH,
SHANK_LINK_THICKNESS,
SHANK_LINK_Z,
SHANK_WEB_WIDTH,
SHANK_WHEEL_RADIUS,
SHANK_WINDOW_LENGTH,
SHANK_WINDOW_WIDTH,
UPPER_LINK_KNEE_RADIUS,
UPPER_LINK_ROOT_RADIUS,
UPPER_LINK_THICKNESS,
UPPER_LINK_WEB_WIDTH,
UPPER_LINK_WINDOW_LENGTH,
UPPER_LINK_WINDOW_WIDTH,
UPPER_LINK_Z,
WHEEL_AXIS,
WHEEL_HUB_PLATE_RADIUS,
WHEEL_HUB_PLATE_THICKNESS,
WHEEL_SPOKE_COUNT,
WHEEL_SPOKE_WIDTH,
WHEEL_TIRE_BORE_RADIUS,
WHEEL_TIRE_RADIUS,
WHEEL_TIRE_WIDTH,
WHEEL_CASE_CLAMP_Z,
)
def make_upper_link_plate_rpart(*, material: scad.Material) -> scad.Part:
"""Build the output-bolted upper link plate with knee bearing holes."""
plate = _make_axis_plate_base_rsolid(
start=ROOT_AXIS,
end=KNEE_AXIS,
z_center=UPPER_LINK_Z,
thickness=UPPER_LINK_THICKNESS,
start_radius=UPPER_LINK_ROOT_RADIUS,
end_radius=UPPER_LINK_KNEE_RADIUS,
web_width=UPPER_LINK_WEB_WIDTH,
tag="role.upper_link_plate_base",
)
z_min = UPPER_LINK_Z - UPPER_LINK_THICKNESS / 2.0
cutters = [
scad.make_cylinder_rsolid(
radius=OUTPUT_FLANGE_REGISTER_INNER_RADIUS + 0.05,
height=UPPER_LINK_THICKNESS + 2.0,
bottom_face_center=(ROOT_AXIS[0], ROOT_AXIS[1], z_min - 1.0),
axis=(0.0, 0.0, 1.0),
),
scad.make_cylinder_rsolid(
radius=KNEE_BEARING_BORE_RADIUS,
height=UPPER_LINK_THICKNESS + 2.0,
bottom_face_center=(KNEE_AXIS[0], KNEE_AXIS[1], z_min - 1.0),
axis=(0.0, 0.0, 1.0),
),
]
cutters.extend(
make_bolt_circle_cutters_rsolidlist(
center=ROOT_AXIS,
bolt_circle_radius=OUTPUT_FLANGE_BOLT_CIRCLE_RADIUS,
angles_degrees=OUTPUT_FLANGE_BOLT_ANGLES_DEGREES,
hole_radius=OUTPUT_FLANGE_BOLT_CLEARANCE_RADIUS,
z_min=z_min - 1.0,
height=UPPER_LINK_THICKNESS + 2.0,
counterbore_radius=OUTPUT_FLANGE_BOLT_COUNTERBORE_RADIUS,
counterbore_depth=3.0,
counterbore_from_top=True,
counterbore_face_z=z_min + UPPER_LINK_THICKNESS,
)
)
cutters.extend(
_make_link_window_cutters(
start=ROOT_AXIS,
end=KNEE_AXIS,
z_center=UPPER_LINK_Z,
thickness=UPPER_LINK_THICKNESS,
fractions=(0.40, 0.67),
length=UPPER_LINK_WINDOW_LENGTH,
width=UPPER_LINK_WINDOW_WIDTH,
tag_prefix="upper_link_window",
)
)
plate = scad.cut_rsolid(plate, cutters, skip_non_intersecting=False)
plate = scad.apply_tag(shape=plate, tag="role.upper_link_plate")
print(
f"upper_link_plate: output_holes={len(OUTPUT_FLANGE_BOLT_ANGLES_DEGREES)} "
f"output_pcd={OUTPUT_FLANGE_BOLT_CIRCLE_RADIUS * 2.0:.1f} "
f"knee_bushing_bore={KNEE_BEARING_BORE_RADIUS * 2.0:.1f} faces={len(plate.get_faces())} "
f"volume={plate.get_volume():.3f}"
)
return make_part_with_connectors_rpart(
part_id="upper_link_plate",
body=plate,
name="Upper link plate bolted to actuator output flange and knee bearing retainer",
material=material,
connectors=(
("output_axis", ROOT_AXIS, "z", "Actuator output flange datum"),
("knee_axis", (KNEE_AXIS[0], KNEE_AXIS[1], KNEE_PIVOT_Z), "z", "Knee bearing datum"),
*_output_bolt_connectors(center=ROOT_AXIS, face_z=z_min + UPPER_LINK_THICKNESS, axis="z"),
),
)
def make_proximal_crank_rpart(*, material: scad.Material) -> scad.Part:
"""Create the short crank on the independent knee-drive actuator output."""
crank = _make_axis_plate_base_rsolid(
start=KNEE_DRIVE_AXIS,
end=PROXIMAL_PUSHROD_PIN,
z_center=REMOTE_CRANK_Z,
thickness=REMOTE_CRANK_THICKNESS,
start_radius=OUTPUT_FLANGE_OUTER_RADIUS,
end_radius=REMOTE_CRANK_WIDTH / 2.0 + 2.0,
web_width=REMOTE_CRANK_WIDTH,
tag="role.proximal_output_crank_base",
)
z_min = REMOTE_CRANK_Z - REMOTE_CRANK_THICKNESS / 2.0
cutters = [
scad.make_cylinder_rsolid(
radius=OUTPUT_FLANGE_REGISTER_INNER_RADIUS + 0.05,
height=REMOTE_CRANK_THICKNESS + 2.0,
bottom_face_center=(KNEE_DRIVE_AXIS[0], KNEE_DRIVE_AXIS[1], z_min - 1.0),
axis=(0.0, 0.0, 1.0),
),
scad.make_cylinder_rsolid(
radius=ROD_PIN_CLEARANCE_RADIUS,
height=REMOTE_CRANK_THICKNESS + 2.0,
bottom_face_center=(PROXIMAL_PUSHROD_PIN[0], PROXIMAL_PUSHROD_PIN[1], z_min - 1.0),
axis=(0.0, 0.0, 1.0),
),
]
cutters.extend(
make_bolt_circle_cutters_rsolidlist(
center=KNEE_DRIVE_AXIS,
bolt_circle_radius=OUTPUT_FLANGE_BOLT_CIRCLE_RADIUS,
angles_degrees=OUTPUT_FLANGE_BOLT_ANGLES_DEGREES,
hole_radius=OUTPUT_FLANGE_BOLT_CLEARANCE_RADIUS,
z_min=z_min - 1.0,
height=REMOTE_CRANK_THICKNESS + 2.0,
counterbore_radius=OUTPUT_FLANGE_BOLT_COUNTERBORE_RADIUS,
counterbore_depth=3.0,
counterbore_from_top=True,
counterbore_face_z=z_min + REMOTE_CRANK_THICKNESS,
)
)
crank = scad.cut_rsolid(crank, cutters, skip_non_intersecting=False)
crank = scad.apply_tag(shape=crank, tag="role.proximal_output_crank")
print(
f"knee_drive_output_crank: length={REMOTE_CRANK_LENGTH:.1f} "
f"output_flange_holes={len(OUTPUT_FLANGE_BOLT_ANGLES_DEGREES)} "
f"faces={len(crank.get_faces())} volume={crank.get_volume():.3f}"
)
return make_part_with_connectors_rpart(
part_id="proximal_output_crank",
body=crank,
name="Knee-drive crank plate using the second actuator output flange holes",
material=material,
connectors=(
("output_axis", KNEE_DRIVE_AXIS, "z", "Knee-drive actuator output flange datum"),
("rod_pin", (PROXIMAL_PUSHROD_PIN[0], PROXIMAL_PUSHROD_PIN[1], ROD_PIN_AXIS_Z), "z", "Proximal pushrod pin datum"),
*_output_bolt_connectors(
center=KNEE_DRIVE_AXIS,
face_z=z_min + REMOTE_CRANK_THICKNESS,
axis="z",
),
),
)
def make_pushrod_rpart(*, material: scad.Material) -> scad.Part:
"""Create the flat pushrod with real pin-clearance holes."""
pushrod = make_rounded_bar_rsolid(
start=(PROXIMAL_PUSHROD_PIN[0], PROXIMAL_PUSHROD_PIN[1], PUSHROD_Z),
end=(DISTAL_PUSHROD_PIN[0], DISTAL_PUSHROD_PIN[1], PUSHROD_Z),
width=PUSHROD_WIDTH,
thickness=PUSHROD_THICKNESS,
end_hole_radius=ROD_PIN_CLEARANCE_RADIUS,
lightening_hole_radius=None,
lightening_count=0,
tag="role.knee_pushrod_plate",
)
print(
f"knee_pushrod: pin_distance={_xy_distance(PROXIMAL_PUSHROD_PIN, DISTAL_PUSHROD_PIN):.1f} "
f"pin_hole_diameter={ROD_PIN_CLEARANCE_RADIUS * 2.0:.1f}"
)
return make_part_with_connectors_rpart(
part_id="knee_pushrod",
body=pushrod,
name="Flat pushrod plate with matched clevis pin holes",
material=material,
connectors=(
("proximal_pin", (PROXIMAL_PUSHROD_PIN[0], PROXIMAL_PUSHROD_PIN[1], ROD_PIN_AXIS_Z), "z", "Proximal crank pin datum"),
("distal_pin", (DISTAL_PUSHROD_PIN[0], DISTAL_PUSHROD_PIN[1], ROD_PIN_AXIS_Z), "z", "Integral shank ear pin datum"),
),
)
def make_shank_link_rpart(*, material: scad.Material) -> scad.Part:
"""Build the lower shank plate with an integral pushrod extension ear."""
shank = _make_axis_plate_base_rsolid(
start=KNEE_AXIS,
end=WHEEL_AXIS,
z_center=SHANK_LINK_Z,
thickness=SHANK_LINK_THICKNESS,
start_radius=SHANK_KNEE_RADIUS,
end_radius=SHANK_WHEEL_RADIUS,
web_width=SHANK_WEB_WIDTH,
tag="role.shank_link_base",
)
z_min = SHANK_LINK_Z - SHANK_LINK_THICKNESS / 2.0
distal_z_min = DISTAL_CRANK_Z - DISTAL_CRANK_THICKNESS / 2.0
distal_z_top = distal_z_min + DISTAL_CRANK_THICKNESS
distal_drive_ear = _make_axis_plate_base_rsolid(
start=KNEE_AXIS,
end=DISTAL_PUSHROD_PIN,
z_center=DISTAL_CRANK_Z,
thickness=DISTAL_CRANK_THICKNESS,
start_radius=KNEE_BEARING_OUTER_RADIUS,
end_radius=DISTAL_CRANK_WIDTH / 2.0 + 2.0,
web_width=DISTAL_CRANK_WIDTH,
tag="role.shank_integral_pushrod_ear_base",
)
standoff_outer = scad.make_cylinder_rsolid(
radius=KNEE_BEARING_OUTER_RADIUS,
height=distal_z_top - z_min,
bottom_face_center=(KNEE_AXIS[0], KNEE_AXIS[1], z_min),
axis=(0.0, 0.0, 1.0),
)
standoff_inner = scad.make_cylinder_rsolid(
radius=KNEE_BEARING_BORE_RADIUS,
height=distal_z_top - z_min + 2.0,
bottom_face_center=(KNEE_AXIS[0], KNEE_AXIS[1], z_min - 1.0),
axis=(0.0, 0.0, 1.0),
)
knee_standoff = scad.cut_rsolid(standoff_outer, standoff_inner, skip_non_intersecting=False)
shank = scad.union_rsolid([shank, distal_drive_ear, knee_standoff], glue=False)
wheel_clamp = make_split_case_clamp_rsolid(
center=WHEEL_AXIS,
z_center=WHEEL_CASE_CLAMP_Z,
tag="role.wheel_actuator_split_clamp",
)
clamp_z_min = WHEEL_CASE_CLAMP_Z - CASE_CLAMP_WIDTH / 2.0
clamp_post_height = z_min + SHANK_LINK_THICKNESS - clamp_z_min
clamp_posts = [
scad.make_cylinder_rsolid(
radius=3.8,
height=clamp_post_height,
bottom_face_center=(WHEEL_AXIS[0], WHEEL_AXIS[1] + sign * 29.5, clamp_z_min),
axis=(0.0, 0.0, 1.0),
)
for sign in (-1.0, 1.0)
]
shank = scad.union_rsolid(shank, wheel_clamp, clamp_posts, glue=False)
integrated_knee_height = distal_z_top - z_min + 2.0
cutters = [
scad.make_cylinder_rsolid(
radius=PIN_CLEARANCE_RADIUS + 2.8,
height=integrated_knee_height,
bottom_face_center=(KNEE_AXIS[0], KNEE_AXIS[1], z_min - 1.0),
axis=(0.0, 0.0, 1.0),
),
scad.make_cylinder_rsolid(
radius=ROD_PIN_CLEARANCE_RADIUS,
height=DISTAL_CRANK_THICKNESS + 2.0,
bottom_face_center=(DISTAL_PUSHROD_PIN[0], DISTAL_PUSHROD_PIN[1], distal_z_min - 1.0),
axis=(0.0, 0.0, 1.0),
),
scad.make_cylinder_rsolid(
radius=19.5,
height=SHANK_LINK_THICKNESS + 2.0,
bottom_face_center=(WHEEL_AXIS[0], WHEEL_AXIS[1], z_min - 1.0),
axis=(0.0, 0.0, 1.0),
),
]
cutters.extend(
_make_link_window_cutters(
start=KNEE_AXIS,
end=WHEEL_AXIS,
z_center=SHANK_LINK_Z,
thickness=SHANK_LINK_THICKNESS,
fractions=(0.42, 0.66),
length=SHANK_WINDOW_LENGTH,
width=SHANK_WINDOW_WIDTH,
tag_prefix="shank_window",
)
)
shank = scad.cut_rsolid(shank, cutters, skip_non_intersecting=False)
shank = scad.apply_tag(shape=shank, tag="role.shank_wheel_plate")
print(
"shank_link: wheel_case_mount=split_clamp "
f"clamp_bore_d={50.3:.1f} "
f"integral_pushrod_ear={DISTAL_CRANK_LENGTH:.1f} "
f"length={SHANK_LENGTH:.1f} faces={len(shank.get_faces())} volume={shank.get_volume():.3f}"
)
return make_part_with_connectors_rpart(
part_id="shank_link",
body=shank,
name="Lower shank plate with integral pushrod ear and wheel actuator split clamp",
material=material,
connectors=(
("knee_axis", (KNEE_AXIS[0], KNEE_AXIS[1], KNEE_PIVOT_Z), "z", "Knee revolute datum"),
("rod_pin", (DISTAL_PUSHROD_PIN[0], DISTAL_PUSHROD_PIN[1], ROD_PIN_AXIS_Z), "z", "Integral shank pushrod pin datum"),
(
"wheel_case_axis",
(WHEEL_AXIS[0], WHEEL_AXIS[1], WHEEL_CASE_CLAMP_Z),
"z",
"Wheel hub actuator split-clamp datum",
),
(
"wheel_clamp_bolt_seat",
(
WHEEL_AXIS[0] + CASE_CLAMP_PINCH_AXIS_RADIUS,
WHEEL_AXIS[1] + CASE_CLAMP_PINCH_HALF_SPAN - 0.9,
WHEEL_CASE_CLAMP_Z,
),
"y",
"Wheel collar M4 bolt head seat",
),
),
)
def make_wheel_tire_rpart(*, material: scad.Material) -> scad.Part:
"""Create the rubber tire ring as a separate serviceable part."""
tire_center_z = -1.5
tire_outer = scad.make_cylinder_rsolid(
radius=WHEEL_TIRE_RADIUS,
height=WHEEL_TIRE_WIDTH,
bottom_face_center=(
WHEEL_AXIS[0],
WHEEL_AXIS[1],
tire_center_z - WHEEL_TIRE_WIDTH / 2.0,
),
axis=(0.0, 0.0, 1.0),
)
tire_bore = scad.make_cylinder_rsolid(
radius=WHEEL_TIRE_BORE_RADIUS,
height=WHEEL_TIRE_WIDTH + 2.0,
bottom_face_center=(
WHEEL_AXIS[0],
WHEEL_AXIS[1],
tire_center_z - WHEEL_TIRE_WIDTH / 2.0 - 1.0,
),
axis=(0.0, 0.0, 1.0),
)
tire_ring = scad.cut_rsolid(tire_outer, tire_bore, skip_non_intersecting=False)
tire_ring = scad.apply_tag(shape=tire_ring, tag="role.replaceable_rubber_tire")
print(
f"wheel_tire: tire_radius={WHEEL_TIRE_RADIUS:.1f} width={WHEEL_TIRE_WIDTH:.1f} "
f"faces={len(tire_ring.get_faces())} volume={tire_ring.get_volume():.3f}"
)
return make_part_with_connectors_rpart(
part_id="wheel_tire",
body=tire_ring,
name="Replaceable rubber wheel tire ring",
material=material,
connectors=(("hub_axis", WHEEL_AXIS, "z", "Wheel hub overmold datum"),),
)
def make_wheel_hub_rpart(*, material: scad.Material) -> scad.Part:
"""Create the rigid 7075 hub and spokes bolted to the actuator flange."""
hub_z = WHEEL_HUB_PLATE_THICKNESS / 2.0
hub_z_min = hub_z - WHEEL_HUB_PLATE_THICKNESS / 2.0
hub = scad.make_cylinder_rsolid(
radius=WHEEL_HUB_PLATE_RADIUS,
height=WHEEL_HUB_PLATE_THICKNESS,
bottom_face_center=(WHEEL_AXIS[0], WHEEL_AXIS[1], hub_z_min),
axis=(0.0, 0.0, 1.0),
)
spokes = [
_make_wheel_spoke_rsolid(
angle_degrees=22.5 + 360.0 * index / WHEEL_SPOKE_COUNT
)
for index in range(WHEEL_SPOKE_COUNT)
]
wheel = scad.union_rsolid([hub, spokes], glue=False)
cutters = [
scad.make_cylinder_rsolid(
radius=OUTPUT_FLANGE_REGISTER_INNER_RADIUS + 0.05,
height=WHEEL_HUB_PLATE_THICKNESS + 2.0,
bottom_face_center=(WHEEL_AXIS[0], WHEEL_AXIS[1], hub_z_min - 1.0),
axis=(0.0, 0.0, 1.0),
)
]
cutters.extend(
make_bolt_circle_cutters_rsolidlist(
center=WHEEL_AXIS,
bolt_circle_radius=OUTPUT_FLANGE_BOLT_CIRCLE_RADIUS,
angles_degrees=OUTPUT_FLANGE_BOLT_ANGLES_DEGREES,
hole_radius=OUTPUT_FLANGE_BOLT_CLEARANCE_RADIUS,
z_min=hub_z_min - 1.0,
height=WHEEL_HUB_PLATE_THICKNESS + 2.0,
counterbore_radius=OUTPUT_FLANGE_BOLT_COUNTERBORE_RADIUS,
counterbore_depth=3.0,
counterbore_from_top=True,
counterbore_face_z=hub_z_min + WHEEL_HUB_PLATE_THICKNESS,
)
)
wheel = scad.cut_rsolid(wheel, cutters, skip_non_intersecting=False)
wheel = scad.apply_tag(shape=wheel, tag="role.rigid_spoked_wheel_hub")
print(
f"wheel_hub: output_holes={len(OUTPUT_FLANGE_BOLT_ANGLES_DEGREES)} "
f"output_pcd={OUTPUT_FLANGE_BOLT_CIRCLE_RADIUS * 2.0:.1f} spokes={WHEEL_SPOKE_COUNT} "
f"faces={len(wheel.get_faces())} volume={wheel.get_volume():.3f}"
)
return make_part_with_connectors_rpart(
part_id="wheel_hub",
body=wheel,
name="7075 spoked wheel hub bolted to the actuator output flange",
material=material,
connectors=(
("wheel_axis", WHEEL_AXIS, "z", "Wheel spin datum"),
("tire_axis", WHEEL_AXIS, "z", "Replaceable tire datum"),
*_output_bolt_connectors(
center=WHEEL_AXIS,
face_z=hub_z_min + WHEEL_HUB_PLATE_THICKNESS,
axis="z",
),
),
)
def _make_axis_plate_base_rsolid(
*,
start: tuple[float, float, float],
end: tuple[float, float, float],
z_center: float,
thickness: float,
start_radius: float,
end_radius: float,
web_width: float,
tag: str,
) -> scad.Solid:
length = _xy_distance(start, end)
if length <= max(start_radius, end_radius):
raise ValueError("axis plate endpoints are too close")
z_min = z_center - thickness / 2.0
web = scad.make_box_rsolid(
width=length,
height=web_width,
depth=thickness,
bottom_face_center=(length / 2.0, 0.0, z_min),
)
start_boss = scad.make_cylinder_rsolid(
radius=start_radius,
height=thickness,
bottom_face_center=(0.0, 0.0, z_min),
axis=(0.0, 0.0, 1.0),
)
end_boss = scad.make_cylinder_rsolid(
radius=end_radius,
height=thickness,
bottom_face_center=(length, 0.0, z_min),
axis=(0.0, 0.0, 1.0),
)
plate = scad.union_rsolid([web, start_boss, end_boss], glue=False)
angle_degrees = math.degrees(math.atan2(end[1] - start[1], end[0] - start[0]))
plate = scad.rotate_shape(
shape=plate,
angle=angle_degrees,
axis=(0.0, 0.0, 1.0),
origin=(0.0, 0.0, z_center),
)
plate = scad.translate_shape(shape=plate, vector=(start[0], start[1], 0.0))
return scad.apply_tag(shape=plate, tag=tag)
def _make_link_window_cutters(
*,
start: tuple[float, float, float],
end: tuple[float, float, float],
z_center: float,
thickness: float,
fractions: tuple[float, ...],
length: float,
width: float,
tag_prefix: str,
) -> list[scad.Solid]:
angle_degrees = math.degrees(math.atan2(end[1] - start[1], end[0] - start[0]))
cutters = []
for index, fraction in enumerate(fractions, start=1):
cutters.append(
make_rounded_slot_cutter_rsolid(
center=(
start[0] + (end[0] - start[0]) * fraction,
start[1] + (end[1] - start[1]) * fraction,
z_center,
),
length=length,
width=width,
height=thickness + 2.0,
angle_degrees=angle_degrees,
tag=f"role.{tag_prefix}_{index}",
)
)
return cutters
def _make_wheel_spoke_rsolid(*, angle_degrees: float) -> scad.Solid:
hub_overlap_radius = WHEEL_HUB_PLATE_RADIUS - 1.5
rim_overlap_radius = WHEEL_TIRE_BORE_RADIUS + 1.5
length = rim_overlap_radius - hub_overlap_radius
radial_center = (hub_overlap_radius + rim_overlap_radius) / 2.0
z_center = WHEEL_HUB_PLATE_THICKNESS / 2.0
spoke = scad.make_box_rsolid(
width=length,
height=WHEEL_SPOKE_WIDTH,
depth=WHEEL_HUB_PLATE_THICKNESS,
bottom_face_center=(WHEEL_AXIS[0] + radial_center, WHEEL_AXIS[1], z_center - WHEEL_HUB_PLATE_THICKNESS / 2.0),
)
return scad.rotate_shape(
shape=spoke,
angle=angle_degrees,
axis=(0.0, 0.0, 1.0),
origin=WHEEL_AXIS,
)
def _xy_distance(a: tuple[float, float, float], b: tuple[float, float, float]) -> float:
return math.hypot(b[0] - a[0], b[1] - a[1])
def _output_bolt_connectors(
*, center: tuple[float, float, float], face_z: float, axis: str
) -> tuple[tuple[str, tuple[float, float, float], str, str], ...]:
connectors = []
for index, angle_degrees in enumerate(OUTPUT_FLANGE_BOLT_ANGLES_DEGREES, start=1):
angle = math.radians(angle_degrees)
connectors.append(
(
f"output_bolt_{index}_head_top",
(
center[0] + OUTPUT_FLANGE_BOLT_CIRCLE_RADIUS * math.cos(angle),
center[1] + OUTPUT_FLANGE_BOLT_CIRCLE_RADIUS * math.sin(angle),
face_z,
),
axis,
f"M3 output screw {index} head top",
)
)
return tuple(connectors)