593 lines
21 KiB
Python
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)
|