first commit
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
"""Safe reuse boundary for the integrated Example 20 joint actuator."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import simplecadapi as scad
|
||||
|
||||
|
||||
EXAMPLES_DIR = Path(__file__).resolve().parents[1]
|
||||
if str(EXAMPLES_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(EXAMPLES_DIR))
|
||||
|
||||
_assembly = importlib.import_module("20_integrated_bldc_joint_actuator.assembly")
|
||||
_dimensions = importlib.import_module("20_integrated_bldc_joint_actuator.dimensions")
|
||||
_materials = importlib.import_module("20_integrated_bldc_joint_actuator.materials")
|
||||
|
||||
ACTUATOR_CASE_CLAMP_Z = _dimensions.OUTPUT_CASE_CLAMP_CENTER_Z
|
||||
ACTUATOR_OUTPUT_FACE_Z = _dimensions.OUTPUT_FLANGE_TOP_Z
|
||||
ACTUATOR_PACKAGE_RADIUS = _dimensions.PACKAGE_RADIUS
|
||||
ACTUATOR_PACKAGE_BOTTOM_Z = _dimensions.PACKAGE_STRUCTURAL_BOTTOM_Z
|
||||
ACTUATOR_PACKAGE_TOP_Z = _dimensions.PACKAGE_TOP_Z
|
||||
OUTPUT_BOLT_ANGLES_DEGREES = _dimensions.OUTPUT_LINK_BOLT_ANGLES_DEGREES
|
||||
OUTPUT_BOLT_CIRCLE_RADIUS = _dimensions.OUTPUT_LINK_HOLE_PCD / 2.0
|
||||
OUTPUT_BOLT_COUNT = _dimensions.OUTPUT_LINK_BOLT_COUNT
|
||||
OUTPUT_TAP_RADIUS = _dimensions.OUTPUT_LINK_TAP_RADIUS
|
||||
OUTPUT_REGISTER_HEIGHT = _dimensions.OUTPUT_REGISTER_HEIGHT
|
||||
OUTPUT_REGISTER_RADIUS = _dimensions.OUTPUT_REGISTER_RADIUS
|
||||
|
||||
|
||||
def make_actuator_materials_rdict() -> dict[str, scad.Material]:
|
||||
"""Create the externally supplied material set used by Example 20."""
|
||||
|
||||
return _materials.make_actuator_materials_rdict()
|
||||
|
||||
|
||||
def make_joint_actuator_rassembly(
|
||||
*, materials: dict[str, scad.Material]
|
||||
) -> scad.Assembly:
|
||||
"""Build the complete actuator as a two-body kinematic subassembly."""
|
||||
|
||||
_dimensions.validate_design_dimensions()
|
||||
component_specs = _assembly.make_integrated_bldc_joint_actuator_components_rtuple(
|
||||
materials=materials
|
||||
)
|
||||
fixed_body = scad.make_assembly_rassembly(
|
||||
assembly_id="integrated_50mm_bldc_joint_actuator_fixed_body",
|
||||
name="Rigid actuator housing, motor, electronics, and reducer internals",
|
||||
)
|
||||
output_carrier = next(
|
||||
component for component in component_specs if component[0] == "output_carrier"
|
||||
)
|
||||
for component_id, item, source_placement, name in component_specs:
|
||||
if component_id == "output_carrier":
|
||||
continue
|
||||
fixed_body = scad.add_component_rassembly(
|
||||
assembly=fixed_body,
|
||||
item=item,
|
||||
component_id=component_id,
|
||||
placement=source_placement,
|
||||
name=name,
|
||||
)
|
||||
|
||||
for connector_id, source_component_id, source_connector_id, name in (
|
||||
("case_clamp_axis", "reducer_housing", "case_clamp_axis", "External split-clamp datum"),
|
||||
("case_mount_axis", "output_bearing_cap", "case_mount_axis", "Fixed actuator case datum"),
|
||||
("output_support_axis", "reducer_housing", "stage2_carrier_axis", "Output carrier bearing axis"),
|
||||
("phase_terminal_access", "controller", "phase_access", "Rear phase-terminal service datum"),
|
||||
("power_can_terminal_access", "controller", "power_can_access", "Rear power/CAN service datum"),
|
||||
):
|
||||
fixed_body = scad.forward_connector_rassembly(
|
||||
assembly=fixed_body,
|
||||
connector_id=connector_id,
|
||||
source_component_id=source_component_id,
|
||||
source_connector_id=source_connector_id,
|
||||
name=name,
|
||||
)
|
||||
|
||||
actuator = scad.make_assembly_rassembly(
|
||||
assembly_id="leg_joint_actuator",
|
||||
name="50 mm integrated BLDC actuator with one external output degree of freedom",
|
||||
)
|
||||
actuator = scad.add_component_rassembly(
|
||||
assembly=actuator,
|
||||
item=fixed_body,
|
||||
component_id="fixed_body",
|
||||
placement=scad.identity_placement_rplacement(),
|
||||
name="Rigid actuator body",
|
||||
)
|
||||
actuator = scad.add_component_rassembly(
|
||||
assembly=actuator,
|
||||
item=output_carrier[1],
|
||||
component_id="output_carrier",
|
||||
placement=output_carrier[2],
|
||||
name=output_carrier[3],
|
||||
)
|
||||
actuator = scad.ground_component_rassembly(
|
||||
assembly=actuator,
|
||||
component_id="fixed_body",
|
||||
)
|
||||
actuator = scad.add_revolute_constraint_rassembly(
|
||||
assembly=actuator,
|
||||
constraint_id="output_revolute",
|
||||
connector_a=scad.make_connector_ref_rconnectorref(
|
||||
component_id="fixed_body",
|
||||
connector_id="output_support_axis",
|
||||
),
|
||||
connector_b=scad.make_connector_ref_rconnectorref(
|
||||
component_id="output_carrier",
|
||||
connector_id="carrier_axis",
|
||||
),
|
||||
name="Actuator output carrier rotation",
|
||||
)
|
||||
actuator = scad.solve_assembly_constraints_rassembly(
|
||||
assembly=actuator,
|
||||
strict=True,
|
||||
)
|
||||
for connector_id, source_component_id, source_connector_id, name in (
|
||||
("case_clamp_axis", "fixed_body", "case_clamp_axis", "External split-clamp datum"),
|
||||
("case_mount_axis", "fixed_body", "case_mount_axis", "Fixed actuator case datum"),
|
||||
("output_link_axis", "output_carrier", "output_link_axis", "Rotating six-hole output flange"),
|
||||
("phase_terminal_access", "fixed_body", "phase_terminal_access", "Rear phase-terminal service datum"),
|
||||
("power_can_terminal_access", "fixed_body", "power_can_terminal_access", "Rear power/CAN service datum"),
|
||||
):
|
||||
actuator = scad.forward_connector_rassembly(
|
||||
assembly=actuator,
|
||||
connector_id=connector_id,
|
||||
source_component_id=source_component_id,
|
||||
source_connector_id=source_connector_id,
|
||||
name=name,
|
||||
)
|
||||
print(
|
||||
"leg_joint_actuator: "
|
||||
f"diameter={ACTUATOR_PACKAGE_RADIUS * 2.0:.1f} "
|
||||
f"length={ACTUATOR_PACKAGE_TOP_Z - ACTUATOR_PACKAGE_BOTTOM_Z:.1f} "
|
||||
f"output_pcd={OUTPUT_BOLT_CIRCLE_RADIUS * 2.0:.1f} "
|
||||
f"components={len(actuator.component_ids())} revolutes=1 "
|
||||
f"connectors={','.join(actuator.connector_ids())}"
|
||||
)
|
||||
return actuator
|
||||
@@ -0,0 +1,194 @@
|
||||
"""Split-clamp actuator mounts for the leg-wheel example."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import simplecadapi as scad
|
||||
|
||||
from leg_common import make_part_with_connectors_rpart
|
||||
from leg_dimensions import (
|
||||
BODY_STANDOFF_THICKNESS,
|
||||
BODY_STANDOFF_Z,
|
||||
CASE_CLAMP_INNER_RADIUS,
|
||||
CASE_CLAMP_OUTER_RADIUS,
|
||||
CASE_CLAMP_PINCH_AXIS_RADIUS,
|
||||
CASE_CLAMP_PINCH_HALF_SPAN,
|
||||
CASE_CLAMP_PINCH_HOLE_RADIUS,
|
||||
CASE_CLAMP_SLIT_WIDTH,
|
||||
CASE_CLAMP_WIDTH,
|
||||
KNEE_CASE_CLAMP_Z,
|
||||
ROOT_AXIS,
|
||||
THIGH_CASE_CLAMP_Z,
|
||||
)
|
||||
|
||||
|
||||
def make_split_case_clamp_rsolid(
|
||||
*, center: tuple[float, float, float], z_center: float, tag: str
|
||||
) -> scad.Solid:
|
||||
"""Create one machinable C-clamp with coaxial pinch-bolt ears."""
|
||||
|
||||
z_min = z_center - CASE_CLAMP_WIDTH / 2.0
|
||||
outer = scad.make_cylinder_rsolid(
|
||||
radius=CASE_CLAMP_OUTER_RADIUS,
|
||||
height=CASE_CLAMP_WIDTH,
|
||||
bottom_face_center=(center[0], center[1], z_min),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
bore = scad.make_cylinder_rsolid(
|
||||
radius=CASE_CLAMP_INNER_RADIUS,
|
||||
height=CASE_CLAMP_WIDTH + 2.0,
|
||||
bottom_face_center=(center[0], center[1], z_min - 1.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
clamp = scad.cut_rsolid(outer, bore, skip_non_intersecting=False)
|
||||
|
||||
ear_center_x = center[0] + CASE_CLAMP_PINCH_AXIS_RADIUS - 2.5
|
||||
ear_center_y = CASE_CLAMP_SLIT_WIDTH / 2.0 + 2.5
|
||||
ears = [
|
||||
scad.make_box_rsolid(
|
||||
width=8.0,
|
||||
height=5.0,
|
||||
depth=CASE_CLAMP_WIDTH,
|
||||
bottom_face_center=(ear_center_x, center[1] + sign * ear_center_y, z_min),
|
||||
)
|
||||
for sign in (-1.0, 1.0)
|
||||
]
|
||||
clamp = scad.union_rsolid(clamp, ears, glue=False)
|
||||
slit = scad.make_box_rsolid(
|
||||
width=15.0,
|
||||
height=CASE_CLAMP_SLIT_WIDTH,
|
||||
depth=CASE_CLAMP_WIDTH + 2.0,
|
||||
bottom_face_center=(center[0] + 31.0, center[1], z_min - 1.0),
|
||||
)
|
||||
pinch_hole = scad.make_cylinder_rsolid(
|
||||
radius=CASE_CLAMP_PINCH_HOLE_RADIUS,
|
||||
height=CASE_CLAMP_PINCH_HALF_SPAN * 2.0,
|
||||
bottom_face_center=(
|
||||
center[0] + CASE_CLAMP_PINCH_AXIS_RADIUS,
|
||||
center[1] - CASE_CLAMP_PINCH_HALF_SPAN,
|
||||
z_center,
|
||||
),
|
||||
axis=(0.0, 1.0, 0.0),
|
||||
)
|
||||
clamp = scad.cut_rsolid(
|
||||
clamp,
|
||||
slit,
|
||||
pinch_hole,
|
||||
skip_non_intersecting=False,
|
||||
)
|
||||
clamp = scad.apply_tag(shape=clamp, tag=tag)
|
||||
print(
|
||||
f"{tag}: bore_d={CASE_CLAMP_INNER_RADIUS * 2.0:.2f} "
|
||||
f"pinch_hole_d={CASE_CLAMP_PINCH_HOLE_RADIUS * 2.0:.2f} "
|
||||
f"faces={len(clamp.get_faces())} volume={clamp.get_volume():.3f}"
|
||||
)
|
||||
return clamp
|
||||
|
||||
|
||||
def make_body_mount_plate_rpart(*, material: scad.Material) -> scad.Part:
|
||||
"""Build the two-collar body mount for coaxial tandem root actuators."""
|
||||
|
||||
lower = make_split_case_clamp_rsolid(
|
||||
center=ROOT_AXIS,
|
||||
z_center=THIGH_CASE_CLAMP_Z,
|
||||
tag="role.thigh_actuator_split_clamp",
|
||||
)
|
||||
upper = make_split_case_clamp_rsolid(
|
||||
center=ROOT_AXIS,
|
||||
z_center=KNEE_CASE_CLAMP_Z,
|
||||
tag="role.knee_drive_split_clamp",
|
||||
)
|
||||
lower_z = THIGH_CASE_CLAMP_Z - CASE_CLAMP_WIDTH / 2.0
|
||||
upper_z = KNEE_CASE_CLAMP_Z + CASE_CLAMP_WIDTH / 2.0
|
||||
post_height = upper_z - lower_z
|
||||
post_offset_x = 35.0
|
||||
posts = [
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=3.8,
|
||||
height=post_height,
|
||||
bottom_face_center=(ROOT_AXIS[0] + sign * post_offset_x, ROOT_AXIS[1], lower_z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
for sign in (-1.0, 1.0)
|
||||
]
|
||||
collar_bridges = [
|
||||
scad.make_box_rsolid(
|
||||
width=8.0,
|
||||
height=7.6,
|
||||
depth=CASE_CLAMP_WIDTH,
|
||||
bottom_face_center=(
|
||||
ROOT_AXIS[0] + sign * 33.0,
|
||||
ROOT_AXIS[1],
|
||||
clamp_z - CASE_CLAMP_WIDTH / 2.0,
|
||||
),
|
||||
)
|
||||
for sign in (-1.0, 1.0)
|
||||
for clamp_z in (THIGH_CASE_CLAMP_Z, KNEE_CASE_CLAMP_Z)
|
||||
]
|
||||
lug_z_min = BODY_STANDOFF_Z - BODY_STANDOFF_THICKNESS / 2.0
|
||||
lugs = [
|
||||
scad.make_box_rsolid(
|
||||
width=24.0,
|
||||
height=18.0,
|
||||
depth=BODY_STANDOFF_THICKNESS,
|
||||
bottom_face_center=(ROOT_AXIS[0] + sign * 41.0, ROOT_AXIS[1], lug_z_min),
|
||||
)
|
||||
for sign in (-1.0, 1.0)
|
||||
]
|
||||
mount = scad.union_rsolid(lower, upper, posts, collar_bridges, lugs, glue=False)
|
||||
torso_holes = [
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=2.3,
|
||||
height=BODY_STANDOFF_THICKNESS + 2.0,
|
||||
bottom_face_center=(ROOT_AXIS[0] + sign * 45.0, ROOT_AXIS[1], lug_z_min - 1.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
for sign in (-1.0, 1.0)
|
||||
]
|
||||
mount = scad.cut_rsolid(mount, torso_holes, skip_non_intersecting=False)
|
||||
mount = scad.apply_tag(shape=mount, tag="role.body_mount_plate")
|
||||
print(
|
||||
"body_mount_plate: root_actuators=2 tandem=true "
|
||||
f"clamp_z=({THIGH_CASE_CLAMP_Z:.1f},{KNEE_CASE_CLAMP_Z:.1f}) "
|
||||
f"torso_holes=2 faces={len(mount.get_faces())} volume={mount.get_volume():.3f}"
|
||||
)
|
||||
return make_part_with_connectors_rpart(
|
||||
part_id="body_mount_plate",
|
||||
body=mount,
|
||||
name="Coaxial tandem root actuator split-clamp and body lug bracket",
|
||||
material=material,
|
||||
connectors=(
|
||||
("case_axis", (ROOT_AXIS[0], ROOT_AXIS[1], THIGH_CASE_CLAMP_Z), "z", "Thigh actuator clamp datum"),
|
||||
(
|
||||
"knee_drive_case_axis",
|
||||
(ROOT_AXIS[0], ROOT_AXIS[1], KNEE_CASE_CLAMP_Z),
|
||||
"z",
|
||||
"Knee-drive actuator clamp datum opposite the crank",
|
||||
),
|
||||
(
|
||||
"thigh_clamp_bolt_seat",
|
||||
(
|
||||
ROOT_AXIS[0] + CASE_CLAMP_PINCH_AXIS_RADIUS,
|
||||
ROOT_AXIS[1] + CASE_CLAMP_PINCH_HALF_SPAN - 0.9,
|
||||
THIGH_CASE_CLAMP_Z,
|
||||
),
|
||||
"y",
|
||||
"Thigh collar M4 bolt head seat",
|
||||
),
|
||||
(
|
||||
"knee_clamp_bolt_seat",
|
||||
(
|
||||
ROOT_AXIS[0] + CASE_CLAMP_PINCH_AXIS_RADIUS,
|
||||
ROOT_AXIS[1] + CASE_CLAMP_PINCH_HALF_SPAN - 0.9,
|
||||
KNEE_CASE_CLAMP_Z,
|
||||
),
|
||||
"y",
|
||||
"Knee-drive collar M4 bolt head seat",
|
||||
),
|
||||
(
|
||||
"body_frame_axis",
|
||||
(ROOT_AXIS[0], ROOT_AXIS[1], BODY_STANDOFF_Z),
|
||||
"z",
|
||||
"Body frame datum",
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,151 @@
|
||||
"""Static external-envelope collision probe for the rebuilt Example 18."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import io
|
||||
import sys
|
||||
import time
|
||||
|
||||
import simplecadapi as scad
|
||||
|
||||
from actuator import make_actuator_materials_rdict
|
||||
from leg_assembly import make_leg_wheel_robot_dog_leg_rassembly
|
||||
from leg_materials import make_leg_materials_rdict
|
||||
|
||||
|
||||
ACTUATOR_IDS = (
|
||||
"thigh_actuator",
|
||||
"knee_drive_actuator",
|
||||
"wheel_hub_actuator",
|
||||
)
|
||||
EXTERNAL_ACTUATOR_LEAVES = (
|
||||
("reducer_housing",),
|
||||
("motor_shell",),
|
||||
("rear_electronics_cover",),
|
||||
("output_bearing_cap",),
|
||||
("output_carrier",),
|
||||
("controller", "three_phase_terminal"),
|
||||
("controller", "power_can_terminal"),
|
||||
)
|
||||
TOP_LEVEL_EXTERNALS = (
|
||||
"body_mount_plate",
|
||||
"upper_link_plate",
|
||||
"proximal_output_crank",
|
||||
"knee_pushrod",
|
||||
"shank_link",
|
||||
"wheel_hub",
|
||||
"wheel_tire",
|
||||
"knee_bushing",
|
||||
"knee_axle",
|
||||
"thigh_clamp_bolt",
|
||||
"knee_drive_clamp_bolt",
|
||||
"wheel_clamp_bolt",
|
||||
"proximal_linkage_pin",
|
||||
"distal_linkage_pin",
|
||||
)
|
||||
|
||||
|
||||
def _leg_level_component_paths() -> tuple[tuple[str, ...], ...]:
|
||||
paths: list[tuple[str, ...]] = []
|
||||
for actuator_id in ACTUATOR_IDS:
|
||||
for leaf in EXTERNAL_ACTUATOR_LEAVES:
|
||||
if leaf == ("output_carrier",):
|
||||
paths.append((actuator_id, *leaf))
|
||||
else:
|
||||
paths.append((actuator_id, "fixed_body", *leaf))
|
||||
paths.extend((component_id,) for component_id in TOP_LEVEL_EXTERNALS)
|
||||
for interface in ("thigh", "knee_drive", "wheel"):
|
||||
paths.extend((f"{interface}_output_screw_{index}",) for index in range(1, 7))
|
||||
return tuple(paths)
|
||||
|
||||
|
||||
def _intentional_mating_pairs() -> tuple[scad.verifier.ComponentPair, ...]:
|
||||
pairs = [
|
||||
scad.verifier.ComponentPair("wheel_hub", "wheel_tire"),
|
||||
scad.verifier.ComponentPair("body_mount_plate", "thigh_clamp_bolt"),
|
||||
scad.verifier.ComponentPair("body_mount_plate", "knee_drive_clamp_bolt"),
|
||||
scad.verifier.ComponentPair("shank_link", "wheel_clamp_bolt"),
|
||||
]
|
||||
for actuator_id in ACTUATOR_IDS:
|
||||
pairs.append(
|
||||
scad.verifier.ComponentPair(
|
||||
(actuator_id, "fixed_body", "reducer_housing"),
|
||||
(actuator_id, "fixed_body", "output_bearing_cap"),
|
||||
)
|
||||
)
|
||||
for interface, actuator_id, driven_component_id in (
|
||||
("thigh", "thigh_actuator", "upper_link_plate"),
|
||||
("knee_drive", "knee_drive_actuator", "proximal_output_crank"),
|
||||
("wheel", "wheel_hub_actuator", "wheel_hub"),
|
||||
):
|
||||
pairs.append(
|
||||
scad.verifier.ComponentPair(
|
||||
(actuator_id, "output_carrier"),
|
||||
driven_component_id,
|
||||
)
|
||||
)
|
||||
for index in range(1, 7):
|
||||
pairs.append(
|
||||
scad.verifier.ComponentPair(
|
||||
f"{interface}_output_screw_{index}",
|
||||
(actuator_id, "output_carrier"),
|
||||
)
|
||||
)
|
||||
return tuple(pairs)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
sys.setrecursionlimit(40000)
|
||||
build_log = io.StringIO()
|
||||
start = time.perf_counter()
|
||||
with contextlib.redirect_stdout(build_log):
|
||||
assembly = make_leg_wheel_robot_dog_leg_rassembly(
|
||||
actuator_materials=make_actuator_materials_rdict(),
|
||||
leg_materials=make_leg_materials_rdict(),
|
||||
)
|
||||
build_seconds = time.perf_counter() - start
|
||||
|
||||
config = scad.verifier.CollisionCheckConfig(
|
||||
max_allowed_penetration=0.08,
|
||||
max_contacts_per_pair=32,
|
||||
scope=scad.verifier.CollisionScope(
|
||||
component_paths=_leg_level_component_paths(),
|
||||
exclude_pairs=_intentional_mating_pairs(),
|
||||
),
|
||||
)
|
||||
start = time.perf_counter()
|
||||
report = scad.verifier.check_collision_rcollisionreport(
|
||||
assembly=assembly,
|
||||
config=config,
|
||||
)
|
||||
check_seconds = time.perf_counter() - start
|
||||
|
||||
print(f"assembly {assembly.assembly_id}")
|
||||
print(f"build_log_lines {len(build_log.getvalue().splitlines())}")
|
||||
print(f"build_seconds {build_seconds:.3f}")
|
||||
print(f"check_seconds {check_seconds:.3f}")
|
||||
print(f"completed {report.completed}")
|
||||
print(f"passed {report.passed}")
|
||||
print(f"checked_pair_count {report.checked_pair_count}")
|
||||
print(f"failed_pair_count {report.failed_pair_count}")
|
||||
print(f"warning_count {len(report.warnings)}")
|
||||
for warning in report.warnings:
|
||||
path = "/".join(warning.component_path or ())
|
||||
print(f"warning {path} {warning.code} {warning.message}")
|
||||
for failure in sorted(
|
||||
report.failures,
|
||||
key=lambda item: item.penetration_depth,
|
||||
reverse=True,
|
||||
)[:25]:
|
||||
print(
|
||||
"failure",
|
||||
"/".join(failure.component_a),
|
||||
"/".join(failure.component_b),
|
||||
f"depth={failure.penetration_depth:.3f}",
|
||||
f"allowed={failure.allowed_penetration:.3f}",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,164 @@
|
||||
"""Modeled bolts, threaded fasteners, and joint bushings for Example 18."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import simplecadapi as scad
|
||||
|
||||
from leg_common import make_part_with_connectors_rpart
|
||||
from leg_dimensions import KNEE_AXIS
|
||||
|
||||
|
||||
def make_socket_head_screw_rpart(
|
||||
*,
|
||||
part_id: str,
|
||||
shank_radius: float,
|
||||
shank_length: float,
|
||||
head_radius: float,
|
||||
head_height: float,
|
||||
material: scad.Material,
|
||||
) -> scad.Part:
|
||||
"""Create a socket-head screw with its seat plane at local Z=0."""
|
||||
|
||||
shank = scad.make_cylinder_rsolid(
|
||||
radius=shank_radius,
|
||||
height=shank_length,
|
||||
bottom_face_center=(0.0, 0.0, -head_height - shank_length),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
head = scad.make_cylinder_rsolid(
|
||||
radius=head_radius,
|
||||
height=head_height,
|
||||
bottom_face_center=(0.0, 0.0, -head_height),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
screw = scad.union_rsolid(shank, head, glue=False)
|
||||
screw = scad.apply_tag(shape=screw, tag="role.socket_head_screw")
|
||||
return make_part_with_connectors_rpart(
|
||||
part_id=part_id,
|
||||
body=screw,
|
||||
name=f"Socket-head screw {shank_radius * 2.0:.1f} x {shank_length:.1f} mm",
|
||||
material=material,
|
||||
connectors=(("head_top_axis", (0.0, 0.0, 0.0), "z", "Flush screw head top plane"),),
|
||||
)
|
||||
|
||||
|
||||
def make_clamp_bolt_stack_rpart(*, material: scad.Material) -> scad.Part:
|
||||
"""Create an M4 bolt plus flange-nut stack for a split collar."""
|
||||
|
||||
shank_length = 12.2
|
||||
shank = scad.make_cylinder_rsolid(
|
||||
radius=2.0,
|
||||
height=shank_length,
|
||||
bottom_face_center=(0.0, 0.0, -shank_length),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
head = scad.make_cylinder_rsolid(
|
||||
radius=3.6,
|
||||
height=3.2,
|
||||
bottom_face_center=(0.0, 0.0, -0.05),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
nut = scad.make_cylinder_rsolid(
|
||||
radius=3.8,
|
||||
height=3.2,
|
||||
bottom_face_center=(0.0, 0.0, -shank_length - 3.15),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
stack = scad.union_rsolid(shank, head, nut, glue=False)
|
||||
stack = scad.apply_tag(shape=stack, tag="role.clamp_bolt_and_nut")
|
||||
return make_part_with_connectors_rpart(
|
||||
part_id="m4_split_clamp_bolt_stack",
|
||||
body=stack,
|
||||
name="M4 split-clamp socket bolt and flange nut",
|
||||
material=material,
|
||||
connectors=(("seat_axis", (0.0, 0.0, 0.0), "z", "Clamp bolt head seat"),),
|
||||
)
|
||||
|
||||
|
||||
def make_knee_bushing_rpart(*, material: scad.Material) -> scad.Part:
|
||||
"""Create a continuous bronze knee sleeve and axial spacer."""
|
||||
|
||||
outer = scad.make_cylinder_rsolid(
|
||||
radius=5.95,
|
||||
height=13.0,
|
||||
bottom_face_center=(KNEE_AXIS[0], KNEE_AXIS[1], 0.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
bore = scad.make_cylinder_rsolid(
|
||||
radius=3.2,
|
||||
height=15.0,
|
||||
bottom_face_center=(KNEE_AXIS[0], KNEE_AXIS[1], -1.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
sleeve = scad.cut_rsolid(outer, bore, skip_non_intersecting=False)
|
||||
sleeve = scad.apply_tag(shape=sleeve, tag="role.knee_bearing_bushing")
|
||||
return make_part_with_connectors_rpart(
|
||||
part_id="knee_bronze_bushing",
|
||||
body=sleeve,
|
||||
name="12 mm OD bronze knee bushing and spacer",
|
||||
material=material,
|
||||
connectors=(
|
||||
("knee_axis", (KNEE_AXIS[0], KNEE_AXIS[1], 6.5), "z", "Knee revolute axis"),
|
||||
("bolt_head_top_axis", (KNEE_AXIS[0], KNEE_AXIS[1], 16.0), "z", "Shoulder bolt head top plane"),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def make_knee_shoulder_bolt_stack_rpart(*, material: scad.Material) -> scad.Part:
|
||||
"""Create the knee shoulder axle, socket head, and retained nut."""
|
||||
|
||||
shaft = scad.make_cylinder_rsolid(
|
||||
radius=3.0,
|
||||
height=13.0,
|
||||
bottom_face_center=(KNEE_AXIS[0], KNEE_AXIS[1], 0.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
head = scad.make_cylinder_rsolid(
|
||||
radius=5.5,
|
||||
height=3.0,
|
||||
bottom_face_center=(KNEE_AXIS[0], KNEE_AXIS[1], 13.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
nut = scad.make_cylinder_rsolid(
|
||||
radius=5.5,
|
||||
height=4.0,
|
||||
bottom_face_center=(KNEE_AXIS[0], KNEE_AXIS[1], -4.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
axle = scad.union_rsolid(shaft, head, nut, glue=False)
|
||||
axle = scad.apply_tag(shape=axle, tag="role.knee_shoulder_axle")
|
||||
return make_part_with_connectors_rpart(
|
||||
part_id="knee_shoulder_bolt_stack",
|
||||
body=axle,
|
||||
name="M6 knee shoulder axle with socket head and retained nut",
|
||||
material=material,
|
||||
connectors=(("knee_axis", (KNEE_AXIS[0], KNEE_AXIS[1], 6.5), "z", "Knee axle axis"),),
|
||||
)
|
||||
def make_linkage_pin_stack_rpart(*, material: scad.Material) -> scad.Part:
|
||||
"""Create a retained M4 shoulder pin spanning crank, gap, and pushrod."""
|
||||
|
||||
span = 9.3
|
||||
shaft = scad.make_cylinder_rsolid(
|
||||
radius=2.0,
|
||||
height=span,
|
||||
bottom_face_center=(0.0, 0.0, -span / 2.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
retainers = [
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=3.5,
|
||||
height=1.2,
|
||||
bottom_face_center=(0.0, 0.0, sign * span / 2.0 - (1.2 if sign < 0.0 else 0.0)),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
for sign in (-1.0, 1.0)
|
||||
]
|
||||
pin = scad.union_rsolid(shaft, retainers, glue=False)
|
||||
pin = scad.apply_tag(shape=pin, tag="role.retained_linkage_pin")
|
||||
return make_part_with_connectors_rpart(
|
||||
part_id="m4_linkage_shoulder_pin_stack",
|
||||
body=pin,
|
||||
name="M4 retained linkage shoulder pin",
|
||||
material=material,
|
||||
connectors=(("pin_axis", (0.0, 0.0, 0.0), "z", "Linkage pin axis"),),
|
||||
)
|
||||
@@ -0,0 +1,254 @@
|
||||
"""Top-level bolt-aligned leg-wheel robot dog leg assembly."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import simplecadapi as scad
|
||||
|
||||
from actuator import make_joint_actuator_rassembly
|
||||
from brackets import make_body_mount_plate_rpart
|
||||
from hardware import (
|
||||
make_clamp_bolt_stack_rpart,
|
||||
make_knee_bushing_rpart,
|
||||
make_knee_shoulder_bolt_stack_rpart,
|
||||
make_linkage_pin_stack_rpart,
|
||||
make_socket_head_screw_rpart,
|
||||
)
|
||||
from leg_common import connector_ref, make_actuator_target_rplacement
|
||||
from leg_dimensions import (
|
||||
ACTUATOR_OUTPUT_CONNECTOR_Z,
|
||||
DISTAL_PUSHROD_PIN,
|
||||
KNEE_DRIVE_AXIS,
|
||||
PROXIMAL_PUSHROD_PIN,
|
||||
OUTPUT_FLANGE_SCREW_SHANK_RADIUS,
|
||||
ROD_PIN_AXIS_Z,
|
||||
ROOT_AXIS,
|
||||
WHEEL_AXIS,
|
||||
)
|
||||
from links import (
|
||||
make_proximal_crank_rpart,
|
||||
make_pushrod_rpart,
|
||||
make_shank_link_rpart,
|
||||
make_upper_link_plate_rpart,
|
||||
make_wheel_hub_rpart,
|
||||
make_wheel_tire_rpart,
|
||||
)
|
||||
|
||||
|
||||
def make_leg_wheel_robot_dog_leg_rassembly(
|
||||
*,
|
||||
actuator_materials: dict[str, scad.Material],
|
||||
leg_materials: dict[str, scad.Material],
|
||||
) -> scad.Assembly:
|
||||
"""Build the posed planar leg-wheel assembly with explicit bolt interfaces."""
|
||||
|
||||
actuator = make_joint_actuator_rassembly(materials=actuator_materials)
|
||||
body_mount = make_body_mount_plate_rpart(material=leg_materials["bracket"])
|
||||
upper_link = make_upper_link_plate_rpart(material=leg_materials["link"])
|
||||
proximal_crank = make_proximal_crank_rpart(material=leg_materials["linkage"])
|
||||
pushrod = make_pushrod_rpart(material=leg_materials["linkage"])
|
||||
shank_link = make_shank_link_rpart(material=leg_materials["link"])
|
||||
wheel_hub = make_wheel_hub_rpart(material=leg_materials["wheel_hub"])
|
||||
wheel_tire = make_wheel_tire_rpart(material=leg_materials["tire"])
|
||||
output_screw = make_socket_head_screw_rpart(
|
||||
part_id="m3x5_output_socket_head_screw",
|
||||
shank_radius=OUTPUT_FLANGE_SCREW_SHANK_RADIUS,
|
||||
shank_length=5.0,
|
||||
head_radius=2.85,
|
||||
head_height=3.0,
|
||||
material=leg_materials["fastener"],
|
||||
)
|
||||
clamp_bolt = make_clamp_bolt_stack_rpart(material=leg_materials["fastener"])
|
||||
linkage_pin = make_linkage_pin_stack_rpart(material=leg_materials["fastener"])
|
||||
knee_bushing = make_knee_bushing_rpart(material=leg_materials["bushing"])
|
||||
knee_axle = make_knee_shoulder_bolt_stack_rpart(material=leg_materials["fastener"])
|
||||
|
||||
leg = scad.make_assembly_rassembly(
|
||||
assembly_id="leg_wheel_robot_dog_leg",
|
||||
name="Planar leg-wheel module with bolt-aligned actuator, knee, and wheel interfaces",
|
||||
)
|
||||
for component_id, target, axis, name in (
|
||||
("thigh_actuator", ROOT_AXIS, "z", "Body-fixed thigh reducer actuator"),
|
||||
("knee_drive_actuator", KNEE_DRIVE_AXIS, "z", "Body-fixed knee-drive actuator opposite the crank"),
|
||||
("wheel_hub_actuator", WHEEL_AXIS, "z", "Distal wheel hub reducer actuator"),
|
||||
):
|
||||
leg = scad.add_component_rassembly(
|
||||
assembly=leg,
|
||||
item=actuator,
|
||||
component_id=component_id,
|
||||
placement=make_actuator_target_rplacement(
|
||||
output_axis_origin=target,
|
||||
output_axis_local_z=ACTUATOR_OUTPUT_CONNECTOR_Z,
|
||||
axis=axis,
|
||||
),
|
||||
name=name,
|
||||
)
|
||||
|
||||
for component_id, item, name in (
|
||||
("body_mount_plate", body_mount, "Body-fixed hip stack bracket for thigh and knee-drive cases"),
|
||||
("upper_link_plate", upper_link, "Output-bolted upper link plate"),
|
||||
("proximal_output_crank", proximal_crank, "Output-bolted knee-drive crank"),
|
||||
("knee_pushrod", pushrod, "Pinned pushrod between crank and shank"),
|
||||
("shank_link", shank_link, "Lower shank with integral pushrod ear and wheel hub case mount"),
|
||||
("wheel_hub", wheel_hub, "Rigid wheel hub bolted to actuator output"),
|
||||
("wheel_tire", wheel_tire, "Replaceable rubber tire fitted to rigid wheel hub"),
|
||||
("knee_bushing", knee_bushing, "Bronze knee pivot sleeve"),
|
||||
("knee_axle", knee_axle, "Retained knee shoulder axle"),
|
||||
):
|
||||
leg = scad.add_component_rassembly(
|
||||
assembly=leg,
|
||||
item=item,
|
||||
component_id=component_id,
|
||||
placement=scad.identity_placement_rplacement(),
|
||||
name=name,
|
||||
)
|
||||
|
||||
for component_id, item, placement, name in (
|
||||
(
|
||||
"thigh_clamp_bolt",
|
||||
clamp_bolt,
|
||||
scad.identity_placement_rplacement(),
|
||||
"M4 thigh actuator split-clamp bolt",
|
||||
),
|
||||
(
|
||||
"knee_drive_clamp_bolt",
|
||||
clamp_bolt,
|
||||
scad.identity_placement_rplacement(),
|
||||
"M4 knee-drive actuator split-clamp bolt",
|
||||
),
|
||||
(
|
||||
"wheel_clamp_bolt",
|
||||
clamp_bolt,
|
||||
scad.identity_placement_rplacement(),
|
||||
"M4 wheel actuator split-clamp bolt",
|
||||
),
|
||||
(
|
||||
"proximal_linkage_pin",
|
||||
linkage_pin,
|
||||
scad.make_placement_rplacement(
|
||||
origin=(PROXIMAL_PUSHROD_PIN[0], PROXIMAL_PUSHROD_PIN[1], ROD_PIN_AXIS_Z)
|
||||
),
|
||||
"Retained proximal linkage shoulder pin",
|
||||
),
|
||||
(
|
||||
"distal_linkage_pin",
|
||||
linkage_pin,
|
||||
scad.make_placement_rplacement(
|
||||
origin=(DISTAL_PUSHROD_PIN[0], DISTAL_PUSHROD_PIN[1], ROD_PIN_AXIS_Z)
|
||||
),
|
||||
"Retained distal linkage shoulder pin",
|
||||
),
|
||||
):
|
||||
leg = scad.add_component_rassembly(
|
||||
assembly=leg,
|
||||
item=item,
|
||||
component_id=component_id,
|
||||
placement=placement,
|
||||
name=name,
|
||||
)
|
||||
|
||||
for interface, plate_component in (
|
||||
("thigh", "upper_link_plate"),
|
||||
("knee_drive", "proximal_output_crank"),
|
||||
("wheel", "wheel_hub"),
|
||||
):
|
||||
for index in range(1, 7):
|
||||
leg = scad.add_component_rassembly(
|
||||
assembly=leg,
|
||||
item=output_screw,
|
||||
component_id=f"{interface}_output_screw_{index}",
|
||||
placement=scad.identity_placement_rplacement(),
|
||||
name=f"{interface.replace('_', ' ')} output M3 screw {index}",
|
||||
)
|
||||
|
||||
leg = _add_leg_constraints_rassembly(assembly=leg)
|
||||
leg = scad.solve_assembly_constraints_rassembly(assembly=leg, strict=True)
|
||||
_ground_constraint_report(assembly=leg)
|
||||
print(
|
||||
"leg_components: actuators=3 structural=7 hardware=25 components="
|
||||
f"{len(leg.component_ids())} constraints={len(leg.constraint_ids())}"
|
||||
)
|
||||
return leg
|
||||
|
||||
|
||||
def _add_leg_constraints_rassembly(*, assembly: scad.Assembly) -> scad.Assembly:
|
||||
assembly = scad.ground_component_rassembly(assembly=assembly, component_id="body_mount_plate")
|
||||
|
||||
fixed_pairs = (
|
||||
("hip_stack_to_thigh_case", "body_mount_plate", "case_axis", "thigh_actuator", "case_clamp_axis"),
|
||||
("hip_stack_to_knee_drive_case", "body_mount_plate", "knee_drive_case_axis", "knee_drive_actuator", "case_clamp_axis"),
|
||||
("shank_clamped_to_wheel_case", "shank_link", "wheel_case_axis", "wheel_hub_actuator", "case_clamp_axis"),
|
||||
("thigh_output_bolted_to_link", "thigh_actuator", "output_link_axis", "upper_link_plate", "output_axis"),
|
||||
("knee_output_bolted_to_crank", "knee_drive_actuator", "output_link_axis", "proximal_output_crank", "output_axis"),
|
||||
("wheel_output_bolted_to_hub", "wheel_hub_actuator", "output_link_axis", "wheel_hub", "wheel_axis"),
|
||||
("wheel_tire_bonded_to_hub", "wheel_hub", "tire_axis", "wheel_tire", "hub_axis"),
|
||||
("knee_bushing_pressed_in_upper_link", "upper_link_plate", "knee_axis", "knee_bushing", "knee_axis"),
|
||||
("knee_axle_locked_to_bushing", "knee_bushing", "knee_axis", "knee_axle", "knee_axis"),
|
||||
("thigh_clamp_bolt_seated", "body_mount_plate", "thigh_clamp_bolt_seat", "thigh_clamp_bolt", "seat_axis"),
|
||||
("knee_clamp_bolt_seated", "body_mount_plate", "knee_clamp_bolt_seat", "knee_drive_clamp_bolt", "seat_axis"),
|
||||
("wheel_clamp_bolt_seated", "shank_link", "wheel_clamp_bolt_seat", "wheel_clamp_bolt", "seat_axis"),
|
||||
("proximal_pin_locked_to_crank", "proximal_output_crank", "rod_pin", "proximal_linkage_pin", "pin_axis"),
|
||||
("distal_pin_locked_to_shank", "shank_link", "rod_pin", "distal_linkage_pin", "pin_axis"),
|
||||
)
|
||||
for constraint_id, a_component, a_connector, b_component, b_connector in fixed_pairs:
|
||||
assembly = scad.add_fixed_constraint_rassembly(
|
||||
assembly=assembly,
|
||||
constraint_id=constraint_id,
|
||||
connector_a=connector_ref(component_id=a_component, connector_id=a_connector),
|
||||
connector_b=connector_ref(component_id=b_component, connector_id=b_connector),
|
||||
name=constraint_id.replace("_", " "),
|
||||
)
|
||||
|
||||
revolutes = (
|
||||
("proximal_pushrod_on_shoulder_pin", "proximal_linkage_pin", "pin_axis", "knee_pushrod", "proximal_pin", None),
|
||||
("distal_pushrod_on_shoulder_pin", "distal_linkage_pin", "pin_axis", "knee_pushrod", "distal_pin", None),
|
||||
("shank_rotates_on_knee_bushing", "knee_bushing", "knee_axis", "shank_link", "knee_axis", None),
|
||||
)
|
||||
for constraint_id, a_component, a_connector, b_component, b_connector, drive_angle in revolutes:
|
||||
assembly = scad.add_revolute_constraint_rassembly(
|
||||
assembly=assembly,
|
||||
constraint_id=constraint_id,
|
||||
connector_a=connector_ref(component_id=a_component, connector_id=a_connector),
|
||||
connector_b=connector_ref(component_id=b_component, connector_id=b_connector),
|
||||
drive_angle_degrees=drive_angle,
|
||||
angle_limit=None,
|
||||
name=constraint_id.replace("_", " "),
|
||||
)
|
||||
|
||||
for interface, plate_component in (
|
||||
("thigh", "upper_link_plate"),
|
||||
("knee_drive", "proximal_output_crank"),
|
||||
("wheel", "wheel_hub"),
|
||||
):
|
||||
for index in range(1, 7):
|
||||
assembly = scad.add_fixed_constraint_rassembly(
|
||||
assembly=assembly,
|
||||
constraint_id=f"{interface}_output_screw_{index}_seated",
|
||||
connector_a=connector_ref(
|
||||
component_id=plate_component,
|
||||
connector_id=f"output_bolt_{index}_head_top",
|
||||
),
|
||||
connector_b=connector_ref(
|
||||
component_id=f"{interface}_output_screw_{index}",
|
||||
connector_id="head_top_axis",
|
||||
),
|
||||
name=f"{interface.replace('_', ' ')} output screw {index} coaxial",
|
||||
)
|
||||
|
||||
print(
|
||||
f"leg_constraints_added: fixed={len(fixed_pairs) + 18} "
|
||||
f"revolute={len(revolutes)} actuator_output_mounts=fixed bolt_aligned=21"
|
||||
)
|
||||
return assembly
|
||||
|
||||
|
||||
def _ground_constraint_report(*, assembly: scad.Assembly) -> None:
|
||||
report = scad.inspect_assembly_constraints_rconstraintreport(assembly=assembly)
|
||||
print(
|
||||
f"leg_constraints: solved={report.solved} grounded={len(report.grounded_component_ids)} "
|
||||
f"solved_components={len(report.solved_component_ids)} unsolved={len(report.unsolved_component_ids)}"
|
||||
)
|
||||
for residual in report.residuals:
|
||||
print(
|
||||
f"leg_constraint_{residual.constraint_id}: translation={residual.translation_error:.6g} "
|
||||
f"angle={residual.angular_error_degrees:.6g} ok={residual.within_tolerance}"
|
||||
)
|
||||
@@ -0,0 +1,343 @@
|
||||
"""Shared geometry helpers for Example 18."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from collections.abc import Iterable
|
||||
|
||||
import simplecadapi as scad
|
||||
from simplecadapi import ql
|
||||
|
||||
|
||||
Point3 = tuple[float, float, float]
|
||||
|
||||
|
||||
def bolt_circle_points(
|
||||
*,
|
||||
center: Point3,
|
||||
radius: float,
|
||||
angles_degrees: Iterable[float],
|
||||
) -> tuple[Point3, ...]:
|
||||
"""Return XY bolt-center points on a named bolt-circle datum."""
|
||||
|
||||
points = []
|
||||
for angle_degrees in angles_degrees:
|
||||
angle = math.radians(angle_degrees)
|
||||
points.append(
|
||||
(
|
||||
center[0] + radius * math.cos(angle),
|
||||
center[1] + radius * math.sin(angle),
|
||||
center[2],
|
||||
)
|
||||
)
|
||||
return tuple(points)
|
||||
|
||||
|
||||
def make_bolt_circle_cutters_rsolidlist(
|
||||
*,
|
||||
center: Point3,
|
||||
bolt_circle_radius: float,
|
||||
angles_degrees: Iterable[float],
|
||||
hole_radius: float,
|
||||
z_min: float,
|
||||
height: float,
|
||||
counterbore_radius: float | None = None,
|
||||
counterbore_depth: float = 0.0,
|
||||
counterbore_from_top: bool = True,
|
||||
counterbore_face_z: float | None = None,
|
||||
) -> list[scad.Solid]:
|
||||
"""Build through-hole and optional counterbore cutters for a bolt circle."""
|
||||
|
||||
cutters: list[scad.Solid] = []
|
||||
for point in bolt_circle_points(
|
||||
center=center,
|
||||
radius=bolt_circle_radius,
|
||||
angles_degrees=angles_degrees,
|
||||
):
|
||||
cutters.append(
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=hole_radius,
|
||||
height=height,
|
||||
bottom_face_center=(point[0], point[1], z_min),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
)
|
||||
if counterbore_radius is not None and counterbore_depth > 0.0:
|
||||
face_z = counterbore_face_z
|
||||
if face_z is None:
|
||||
face_z = z_min + height if counterbore_from_top else z_min
|
||||
counterbore_z = face_z - counterbore_depth if counterbore_from_top else face_z - 0.2
|
||||
cutters.append(
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=counterbore_radius,
|
||||
height=counterbore_depth + 0.2,
|
||||
bottom_face_center=(point[0], point[1], counterbore_z),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
)
|
||||
return cutters
|
||||
|
||||
|
||||
def make_rounded_slot_cutter_rsolid(
|
||||
*,
|
||||
center: Point3,
|
||||
length: float,
|
||||
width: float,
|
||||
height: float,
|
||||
angle_degrees: float,
|
||||
tag: str,
|
||||
) -> scad.Solid:
|
||||
"""Build a capsule-shaped cutter for a lightening pocket."""
|
||||
|
||||
if length <= width:
|
||||
raise ValueError("rounded slot length must exceed width")
|
||||
radius = width / 2.0
|
||||
straight = length - width
|
||||
z_min = center[2] - height / 2.0
|
||||
bridge = scad.make_box_rsolid(
|
||||
width=straight,
|
||||
height=width,
|
||||
depth=height,
|
||||
bottom_face_center=(0.0, 0.0, z_min),
|
||||
)
|
||||
left = scad.make_cylinder_rsolid(
|
||||
radius=radius,
|
||||
height=height,
|
||||
bottom_face_center=(-straight / 2.0, 0.0, z_min),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
right = scad.make_cylinder_rsolid(
|
||||
radius=radius,
|
||||
height=height,
|
||||
bottom_face_center=(straight / 2.0, 0.0, z_min),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
cutter = scad.union_rsolid([bridge, left, right], glue=False)
|
||||
cutter = scad.rotate_shape(
|
||||
shape=cutter,
|
||||
angle=angle_degrees,
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
origin=(0.0, 0.0, center[2]),
|
||||
)
|
||||
cutter = scad.translate_shape(shape=cutter, vector=(center[0], center[1], 0.0))
|
||||
return scad.apply_tag(shape=cutter, tag=tag)
|
||||
|
||||
|
||||
def make_axis_placement_rplacement(
|
||||
*,
|
||||
origin: Point3,
|
||||
axis: str = "z",
|
||||
) -> scad.Placement:
|
||||
"""Create a placement whose local Z axis is the requested world axis."""
|
||||
|
||||
if axis == "z":
|
||||
return scad.make_placement_rplacement(
|
||||
origin=origin,
|
||||
x_axis=(1.0, 0.0, 0.0),
|
||||
y_axis=(0.0, 1.0, 0.0),
|
||||
)
|
||||
if axis == "y":
|
||||
return scad.make_placement_rplacement(
|
||||
origin=origin,
|
||||
x_axis=(1.0, 0.0, 0.0),
|
||||
y_axis=(0.0, 0.0, -1.0),
|
||||
)
|
||||
if axis == "-z":
|
||||
return scad.make_placement_rplacement(
|
||||
origin=origin,
|
||||
x_axis=(1.0, 0.0, 0.0),
|
||||
y_axis=(0.0, -1.0, 0.0),
|
||||
)
|
||||
if axis == "x":
|
||||
return scad.make_placement_rplacement(
|
||||
origin=origin,
|
||||
x_axis=(0.0, 1.0, 0.0),
|
||||
y_axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
raise ValueError(f"unsupported axis {axis!r}")
|
||||
|
||||
|
||||
def make_actuator_target_rplacement(
|
||||
*,
|
||||
output_axis_origin: Point3,
|
||||
output_axis_local_z: float,
|
||||
axis: str,
|
||||
) -> scad.Placement:
|
||||
"""Place an actuator so its forwarded output/case axis lands on a world point."""
|
||||
|
||||
if axis == "z":
|
||||
origin = (
|
||||
output_axis_origin[0],
|
||||
output_axis_origin[1],
|
||||
output_axis_origin[2] - output_axis_local_z,
|
||||
)
|
||||
elif axis == "y":
|
||||
origin = (
|
||||
output_axis_origin[0],
|
||||
output_axis_origin[1] - output_axis_local_z,
|
||||
output_axis_origin[2],
|
||||
)
|
||||
elif axis == "-z":
|
||||
origin = (
|
||||
output_axis_origin[0],
|
||||
output_axis_origin[1],
|
||||
output_axis_origin[2] + output_axis_local_z,
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"unsupported axis {axis!r}")
|
||||
return make_axis_placement_rplacement(origin=origin, axis=axis)
|
||||
|
||||
|
||||
def add_datum_connector_rpart(
|
||||
*,
|
||||
part: scad.Part,
|
||||
connector_id: str,
|
||||
origin: Point3,
|
||||
axis: str = "z",
|
||||
name: str | None = None,
|
||||
) -> scad.Part:
|
||||
"""Attach a topology-free connector datum to a part."""
|
||||
|
||||
connector = scad.make_placement_connector_rconnector(
|
||||
connector_id=connector_id,
|
||||
placement=make_axis_placement_rplacement(origin=origin, axis=axis),
|
||||
name=name,
|
||||
)
|
||||
return scad.add_connector_rpart(part=part, connector=connector)
|
||||
|
||||
|
||||
def make_part_with_connectors_rpart(
|
||||
*,
|
||||
part_id: str,
|
||||
body: scad.Solid,
|
||||
name: str,
|
||||
material: scad.Material,
|
||||
connectors: Iterable[tuple[str, Point3, str, str | None]],
|
||||
) -> scad.Part:
|
||||
"""Wrap a solid as a materialized Part and add placement connectors."""
|
||||
|
||||
part = scad.make_part_rpart(part_id=part_id, body=body, name=name)
|
||||
part = scad.assign_material_rpart(part=part, material=material)
|
||||
for connector_id, origin, axis, connector_name in connectors:
|
||||
part = add_datum_connector_rpart(
|
||||
part=part,
|
||||
connector_id=connector_id,
|
||||
origin=origin,
|
||||
axis=axis,
|
||||
name=connector_name,
|
||||
)
|
||||
print(f"part_{part_id}: connectors={len(part.connectors)} volume={body.get_volume():.3f}")
|
||||
return part
|
||||
|
||||
|
||||
def make_rounded_bar_rsolid(
|
||||
*,
|
||||
start: Point3,
|
||||
end: Point3,
|
||||
width: float,
|
||||
thickness: float,
|
||||
end_hole_radius: float,
|
||||
lightening_hole_radius: float | None = None,
|
||||
lightening_count: int = 0,
|
||||
tag: str,
|
||||
) -> scad.Solid:
|
||||
"""Build a planar rounded-end plate between two XY points at constant Z."""
|
||||
|
||||
dx = end[0] - start[0]
|
||||
dy = end[1] - start[1]
|
||||
length = math.hypot(dx, dy)
|
||||
if length <= width:
|
||||
raise ValueError("rounded bar length must exceed width")
|
||||
z_center = start[2]
|
||||
z_min = z_center - thickness / 2.0
|
||||
radius = width / 2.0
|
||||
bridge = scad.make_box_rsolid(
|
||||
width=length,
|
||||
height=width,
|
||||
depth=thickness,
|
||||
bottom_face_center=(length / 2.0, 0.0, z_min),
|
||||
)
|
||||
left = scad.make_cylinder_rsolid(
|
||||
radius=radius,
|
||||
height=thickness,
|
||||
bottom_face_center=(0.0, 0.0, z_min),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
right = scad.make_cylinder_rsolid(
|
||||
radius=radius,
|
||||
height=thickness,
|
||||
bottom_face_center=(length, 0.0, z_min),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
body = scad.union_rsolid([bridge, left, right], glue=False)
|
||||
cutters = [
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=end_hole_radius,
|
||||
height=thickness + 2.0,
|
||||
bottom_face_center=(0.0, 0.0, z_min - 1.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
),
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=end_hole_radius,
|
||||
height=thickness + 2.0,
|
||||
bottom_face_center=(length, 0.0, z_min - 1.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
),
|
||||
]
|
||||
if lightening_hole_radius is not None and lightening_count > 0:
|
||||
for index in range(lightening_count):
|
||||
fraction = (index + 1.0) / (lightening_count + 1.0)
|
||||
cutters.append(
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=lightening_hole_radius,
|
||||
height=thickness + 2.0,
|
||||
bottom_face_center=(length * fraction, 0.0, z_min - 1.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
)
|
||||
body = scad.cut_rsolid(body, cutters, skip_non_intersecting=False)
|
||||
body = scad.rotate_shape(
|
||||
shape=body,
|
||||
angle=math.degrees(math.atan2(dy, dx)),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
origin=(0.0, 0.0, z_center),
|
||||
)
|
||||
body = scad.translate_shape(shape=body, vector=(start[0], start[1], 0.0))
|
||||
body = scad.apply_tag(shape=body, tag=tag)
|
||||
_ground_solid(label=tag, solid=body)
|
||||
return body
|
||||
|
||||
|
||||
def _ground_solid(*, label: str, solid: scad.Solid) -> None:
|
||||
faces = ql.select(items=solid.get_faces()).all()
|
||||
print(f"{label}: faces={len(faces)} volume={solid.get_volume():.3f}")
|
||||
|
||||
|
||||
def _axis_vector(*, axis: str) -> Point3:
|
||||
if axis == "z":
|
||||
return (0.0, 0.0, 1.0)
|
||||
if axis == "y":
|
||||
return (0.0, 1.0, 0.0)
|
||||
if axis == "x":
|
||||
return (1.0, 0.0, 0.0)
|
||||
if axis == "-z":
|
||||
return (0.0, 0.0, -1.0)
|
||||
raise ValueError(f"unsupported axis {axis!r}")
|
||||
|
||||
|
||||
def ground_compound(*, label: str, compound: scad.Compound) -> None:
|
||||
"""Print compact grounding facts for an assembly preview."""
|
||||
|
||||
solids = ql.select(items=compound.get_solids()).all()
|
||||
face_count = sum(len(ql.select(items=solid.get_faces()).all()) for solid in solids)
|
||||
print(
|
||||
f"{label}: solids={len(solids)} faces={face_count} "
|
||||
f"volume={compound.get_volume():.3f}"
|
||||
)
|
||||
|
||||
|
||||
def connector_ref(*, component_id: str, connector_id: str) -> scad.ConnectorRef:
|
||||
return scad.make_connector_ref_rconnectorref(
|
||||
component_id=component_id,
|
||||
connector_id=connector_id,
|
||||
)
|
||||
@@ -0,0 +1,173 @@
|
||||
"""Design constants for the bolt-aligned planar leg-wheel example."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
from actuator import (
|
||||
ACTUATOR_CASE_CLAMP_Z,
|
||||
ACTUATOR_OUTPUT_FACE_Z,
|
||||
ACTUATOR_PACKAGE_RADIUS,
|
||||
OUTPUT_BOLT_ANGLES_DEGREES,
|
||||
OUTPUT_BOLT_CIRCLE_RADIUS,
|
||||
OUTPUT_BOLT_COUNT,
|
||||
OUTPUT_TAP_RADIUS,
|
||||
OUTPUT_REGISTER_HEIGHT,
|
||||
OUTPUT_REGISTER_RADIUS,
|
||||
)
|
||||
|
||||
|
||||
# This example intentionally drops the hip-abduction package for now. The model
|
||||
# is one planar leg module like the reference sketch: one body-fixed thigh
|
||||
# actuator drives the upper link, a second body-fixed coaxial knee-drive actuator
|
||||
# drives a crank/pushrod, and a third hub actuator drives the wheel.
|
||||
|
||||
ROOT_AXIS = (0.0, 0.0, 0.0)
|
||||
KNEE_AXIS = (42.0, -126.0, 0.0)
|
||||
WHEEL_AXIS = (-78.0, -220.0, 0.0)
|
||||
KNEE_PIVOT_Z = 6.5
|
||||
# The knee-drive actuator is mirrored about its unchanged split-clamp plane, so
|
||||
# its output flange and red crank sit on the side opposite the actuator body.
|
||||
KNEE_DRIVE_OUTPUT_Z = 85.6
|
||||
ROD_PIN_AXIS_Z = 85.95
|
||||
|
||||
THIGH_VECTOR_X = KNEE_AXIS[0] - ROOT_AXIS[0]
|
||||
THIGH_VECTOR_Y = KNEE_AXIS[1] - ROOT_AXIS[1]
|
||||
THIGH_LENGTH = math.hypot(THIGH_VECTOR_X, THIGH_VECTOR_Y)
|
||||
THIGH_UNIT_X = THIGH_VECTOR_X / THIGH_LENGTH
|
||||
THIGH_UNIT_Y = THIGH_VECTOR_Y / THIGH_LENGTH
|
||||
THIGH_NORMAL_X = -THIGH_UNIT_Y
|
||||
THIGH_NORMAL_Y = THIGH_UNIT_X
|
||||
|
||||
SHANK_VECTOR_X = WHEEL_AXIS[0] - KNEE_AXIS[0]
|
||||
SHANK_VECTOR_Y = WHEEL_AXIS[1] - KNEE_AXIS[1]
|
||||
SHANK_LENGTH = math.hypot(SHANK_VECTOR_X, SHANK_VECTOR_Y)
|
||||
SHANK_UNIT_X = SHANK_VECTOR_X / SHANK_LENGTH
|
||||
SHANK_UNIT_Y = SHANK_VECTOR_Y / SHANK_LENGTH
|
||||
SHANK_NORMAL_X = -SHANK_UNIT_Y
|
||||
SHANK_NORMAL_Y = SHANK_UNIT_X
|
||||
|
||||
REMOTE_CRANK_LENGTH = 46.0
|
||||
DISTAL_CRANK_LENGTH = REMOTE_CRANK_LENGTH
|
||||
KNEE_DRIVE_AXIS = (
|
||||
ROOT_AXIS[0],
|
||||
ROOT_AXIS[1],
|
||||
KNEE_DRIVE_OUTPUT_Z,
|
||||
)
|
||||
PROXIMAL_PUSHROD_PIN = (
|
||||
ROOT_AXIS[0] + THIGH_NORMAL_X * REMOTE_CRANK_LENGTH,
|
||||
ROOT_AXIS[1] + THIGH_NORMAL_Y * REMOTE_CRANK_LENGTH,
|
||||
0.0,
|
||||
)
|
||||
DISTAL_PUSHROD_PIN = (
|
||||
KNEE_AXIS[0] + THIGH_NORMAL_X * DISTAL_CRANK_LENGTH,
|
||||
KNEE_AXIS[1] + THIGH_NORMAL_Y * DISTAL_CRANK_LENGTH,
|
||||
0.0,
|
||||
)
|
||||
|
||||
# Shared Example 20 external interfaces. The actuator uses M3 tapped output
|
||||
# holes; the leg-side plates use ISO-style clearance holes and socket-head
|
||||
# counterbores at exactly the same centers.
|
||||
ACTUATOR_OUTPUT_CONNECTOR_Z = ACTUATOR_OUTPUT_FACE_Z
|
||||
ACTUATOR_CASE_OUTER_RADIUS = ACTUATOR_PACKAGE_RADIUS
|
||||
OUTPUT_FLANGE_OUTER_RADIUS = 22.4
|
||||
OUTPUT_FLANGE_REGISTER_INNER_RADIUS = OUTPUT_REGISTER_RADIUS
|
||||
OUTPUT_FLANGE_BOLT_CIRCLE_RADIUS = OUTPUT_BOLT_CIRCLE_RADIUS
|
||||
OUTPUT_FLANGE_BOLT_CLEARANCE_RADIUS = 1.65
|
||||
OUTPUT_FLANGE_BOLT_COUNTERBORE_RADIUS = 3.0
|
||||
OUTPUT_FLANGE_SCREW_SHANK_RADIUS = 1.5
|
||||
OUTPUT_FLANGE_BOLT_COUNT = OUTPUT_BOLT_COUNT
|
||||
OUTPUT_FLANGE_BOLT_ANGLES_DEGREES = OUTPUT_BOLT_ANGLES_DEGREES
|
||||
|
||||
CASE_CLAMP_INNER_RADIUS = ACTUATOR_CASE_OUTER_RADIUS + 0.15
|
||||
CASE_CLAMP_OUTER_RADIUS = 30.5
|
||||
CASE_CLAMP_WIDTH = 8.0
|
||||
CASE_CLAMP_SLIT_WIDTH = 2.2
|
||||
CASE_CLAMP_PINCH_HOLE_RADIUS = 2.15
|
||||
CASE_CLAMP_BOLT_SHANK_RADIUS = 2.0
|
||||
CASE_CLAMP_PINCH_AXIS_RADIUS = 32.0
|
||||
CASE_CLAMP_PINCH_HALF_SPAN = 7.0
|
||||
THIGH_CASE_CLAMP_Z = ROOT_AXIS[2] - (ACTUATOR_OUTPUT_FACE_Z - ACTUATOR_CASE_CLAMP_Z)
|
||||
KNEE_CASE_CLAMP_Z = KNEE_DRIVE_OUTPUT_Z - (
|
||||
ACTUATOR_OUTPUT_FACE_Z - ACTUATOR_CASE_CLAMP_Z
|
||||
)
|
||||
WHEEL_CASE_CLAMP_Z = WHEEL_AXIS[2] - (
|
||||
ACTUATOR_OUTPUT_FACE_Z - ACTUATOR_CASE_CLAMP_Z
|
||||
)
|
||||
|
||||
PIN_CLEARANCE_RADIUS = 3.2
|
||||
ROD_PIN_CLEARANCE_RADIUS = 2.2
|
||||
LIGHTENING_CORNER_RADIUS = 4.0
|
||||
|
||||
UPPER_LINK_Z = 2.5
|
||||
UPPER_LINK_THICKNESS = 5.0
|
||||
UPPER_LINK_ROOT_RADIUS = 30.5
|
||||
UPPER_LINK_KNEE_RADIUS = 17.0
|
||||
UPPER_LINK_WEB_WIDTH = 28.0
|
||||
UPPER_LINK_WINDOW_LENGTH = 28.0
|
||||
UPPER_LINK_WINDOW_WIDTH = 14.0
|
||||
|
||||
KNEE_BEARING_OUTER_RADIUS = 14.0
|
||||
KNEE_BEARING_BORE_RADIUS = 6.0
|
||||
SHANK_LINK_Z = 10.5
|
||||
SHANK_LINK_THICKNESS = 5.0
|
||||
SHANK_KNEE_RADIUS = 18.0
|
||||
SHANK_WHEEL_RADIUS = 31.5
|
||||
SHANK_WEB_WIDTH = 24.0
|
||||
SHANK_WINDOW_LENGTH = 25.0
|
||||
SHANK_WINDOW_WIDTH = 12.0
|
||||
|
||||
REMOTE_CRANK_Z = 88.1
|
||||
REMOTE_CRANK_THICKNESS = 5.0
|
||||
REMOTE_CRANK_WIDTH = 12.0
|
||||
DISTAL_CRANK_Z = 88.1
|
||||
DISTAL_CRANK_THICKNESS = 5.0
|
||||
DISTAL_CRANK_WIDTH = 12.0
|
||||
PUSHROD_Z = 83.1
|
||||
PUSHROD_THICKNESS = 3.6
|
||||
PUSHROD_WIDTH = 9.0
|
||||
|
||||
WHEEL_TIRE_RADIUS = 48.0
|
||||
WHEEL_TIRE_WIDTH = 18.0
|
||||
WHEEL_TIRE_BORE_RADIUS = 34.0
|
||||
WHEEL_HUB_PLATE_RADIUS = 24.5
|
||||
WHEEL_HUB_PLATE_THICKNESS = 5.0
|
||||
WHEEL_SPOKE_WIDTH = 5.5
|
||||
WHEEL_SPOKE_COUNT = 8
|
||||
|
||||
BODY_STANDOFF_Z = 22.0
|
||||
BODY_STANDOFF_THICKNESS = 8.0
|
||||
KNEE_STACK_CLAMP_Z = KNEE_CASE_CLAMP_Z
|
||||
KNEE_STACK_CLAMP_THICKNESS = CASE_CLAMP_WIDTH
|
||||
|
||||
assert len(OUTPUT_FLANGE_BOLT_ANGLES_DEGREES) == OUTPUT_FLANGE_BOLT_COUNT
|
||||
assert OUTPUT_FLANGE_BOLT_CIRCLE_RADIUS == 17.0
|
||||
assert OUTPUT_FLANGE_REGISTER_INNER_RADIUS == 7.98
|
||||
assert OUTPUT_REGISTER_HEIGHT == 1.5
|
||||
assert CASE_CLAMP_INNER_RADIUS > ACTUATOR_CASE_OUTER_RADIUS
|
||||
assert OUTPUT_FLANGE_BOLT_CIRCLE_RADIUS + OUTPUT_FLANGE_BOLT_COUNTERBORE_RADIUS < 21.0
|
||||
|
||||
|
||||
def validate_leg_interface_dimensions() -> None:
|
||||
"""Validate shared actuator/leg hole, register, and clamp clearances."""
|
||||
|
||||
generated_angles = tuple(
|
||||
OUTPUT_FLANGE_BOLT_ANGLES_DEGREES[0]
|
||||
+ 360.0 * index / OUTPUT_FLANGE_BOLT_COUNT
|
||||
for index in range(OUTPUT_FLANGE_BOLT_COUNT)
|
||||
)
|
||||
assert generated_angles == OUTPUT_FLANGE_BOLT_ANGLES_DEGREES
|
||||
assert OUTPUT_TAP_RADIUS < OUTPUT_FLANGE_SCREW_SHANK_RADIUS
|
||||
assert OUTPUT_FLANGE_SCREW_SHANK_RADIUS < OUTPUT_FLANGE_BOLT_CLEARANCE_RADIUS
|
||||
assert OUTPUT_FLANGE_BOLT_COUNTERBORE_RADIUS > OUTPUT_FLANGE_SCREW_SHANK_RADIUS
|
||||
assert CASE_CLAMP_BOLT_SHANK_RADIUS < CASE_CLAMP_PINCH_HOLE_RADIUS
|
||||
assert abs(CASE_CLAMP_INNER_RADIUS - ACTUATOR_CASE_OUTER_RADIUS - 0.15) < 1.0e-9
|
||||
print(
|
||||
"leg_interface_dimensions: "
|
||||
f"output_holes={OUTPUT_FLANGE_BOLT_COUNT} "
|
||||
f"pcd={OUTPUT_FLANGE_BOLT_CIRCLE_RADIUS * 2.0:.1f} "
|
||||
f"tap_d={OUTPUT_TAP_RADIUS * 2.0:.2f} "
|
||||
f"screw_d={OUTPUT_FLANGE_SCREW_SHANK_RADIUS * 2.0:.2f} "
|
||||
f"clearance_d={OUTPUT_FLANGE_BOLT_CLEARANCE_RADIUS * 2.0:.2f} "
|
||||
f"pilot_d={OUTPUT_FLANGE_REGISTER_INNER_RADIUS * 2.0:.2f}"
|
||||
)
|
||||
@@ -0,0 +1,61 @@
|
||||
"""Materials for the leg-wheel robot dog leg example."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import simplecadapi as scad
|
||||
|
||||
|
||||
def make_leg_materials_rdict() -> dict[str, scad.Material]:
|
||||
"""Create reusable material definitions for the leg assembly."""
|
||||
|
||||
return {
|
||||
"link": scad.make_material_rmaterial(
|
||||
material_id="leg_link_turquoise_7075",
|
||||
name="Turquoise anodized 7075 link plates",
|
||||
density=2.81e-6,
|
||||
density_unit="kg/mm^3",
|
||||
color=(0.02, 0.67, 0.78),
|
||||
),
|
||||
"linkage": scad.make_material_rmaterial(
|
||||
material_id="leg_linkage_orange_ti",
|
||||
name="Orange anodized titanium linkage hardware",
|
||||
density=4.43e-6,
|
||||
density_unit="kg/mm^3",
|
||||
color=(0.95, 0.25, 0.06),
|
||||
),
|
||||
"bracket": scad.make_material_rmaterial(
|
||||
material_id="leg_bracket_violet_7075",
|
||||
name="Violet anodized 7075 actuator clamps",
|
||||
density=2.81e-6,
|
||||
density_unit="kg/mm^3",
|
||||
color=(0.48, 0.19, 0.76),
|
||||
),
|
||||
"wheel_hub": scad.make_material_rmaterial(
|
||||
material_id="leg_wheel_hub_gold_7075",
|
||||
name="Gold anodized 7075 wheel hub",
|
||||
density=2.81e-6,
|
||||
density_unit="kg/mm^3",
|
||||
color=(0.94, 0.62, 0.05),
|
||||
),
|
||||
"fastener": scad.make_material_rmaterial(
|
||||
material_id="leg_fastener_black_12_9_steel",
|
||||
name="Black oxide class 12.9 fastener steel",
|
||||
density=7.85e-6,
|
||||
density_unit="kg/mm^3",
|
||||
color=(0.08, 0.10, 0.13),
|
||||
),
|
||||
"bushing": scad.make_material_rmaterial(
|
||||
material_id="leg_bushing_bronze",
|
||||
name="Oil-impregnated bearing bronze",
|
||||
density=8.80e-6,
|
||||
density_unit="kg/mm^3",
|
||||
color=(0.63, 0.34, 0.12),
|
||||
),
|
||||
"tire": scad.make_material_rmaterial(
|
||||
material_id="leg_wheel_rubber",
|
||||
name="Dark rubber tire",
|
||||
density=1.15e-6,
|
||||
density_unit="kg/mm^3",
|
||||
color=(0.025, 0.03, 0.035),
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,592 @@
|
||||
"""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)
|
||||
@@ -0,0 +1,90 @@
|
||||
"""Build, validate, and export the leg-wheel robot dog leg example."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import simplecadapi as scad
|
||||
|
||||
from leg_assembly import make_leg_wheel_robot_dog_leg_rassembly
|
||||
from actuator import make_actuator_materials_rdict
|
||||
from leg_common import ground_compound
|
||||
from leg_materials import make_leg_materials_rdict
|
||||
from leg_dimensions import validate_leg_interface_dimensions
|
||||
|
||||
|
||||
# Example 16's reducer graph is intentionally deep because of herringbone gears.
|
||||
sys.setrecursionlimit(40000)
|
||||
|
||||
OUT_DIR = Path("examples/out/leg_wheel_robot_dog_leg")
|
||||
|
||||
|
||||
def _build_leg_wheel_robot_dog_leg():
|
||||
validate_leg_interface_dimensions()
|
||||
actuator_materials = make_actuator_materials_rdict()
|
||||
leg_materials = make_leg_materials_rdict()
|
||||
with scad.GraphSession(graph_id="leg_wheel_robot_dog_leg") as session:
|
||||
assembly = make_leg_wheel_robot_dog_leg_rassembly(
|
||||
actuator_materials=actuator_materials,
|
||||
leg_materials=leg_materials,
|
||||
)
|
||||
preview = scad.make_compound_from_assembly_rcompound(assembly=assembly)
|
||||
ground_compound(label="leg_preview", compound=preview)
|
||||
leaf_ops = [node.op for node in session.graph.leaf_nodes()]
|
||||
print(f"leg_graph_results: leaves={len(leaf_ops)} ops={','.join(leaf_ops)}")
|
||||
if leaf_ops != ["make_compound_from_assembly_rcompound"]:
|
||||
raise RuntimeError("Leg graph contains detached source results")
|
||||
session_json = scad.export_session_json(session=session)
|
||||
model_json = scad.export_model_json(session=session)
|
||||
return assembly, preview, model_json, session_json
|
||||
|
||||
|
||||
def main() -> None:
|
||||
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
model_path = OUT_DIR / "leg_wheel_robot_dog_leg.model.json"
|
||||
session_path = OUT_DIR / "leg_wheel_robot_dog_leg.session.json"
|
||||
step_path = OUT_DIR / "leg_wheel_robot_dog_leg.step"
|
||||
fcstd_path = OUT_DIR / "leg_wheel_robot_dog_leg.FCStd"
|
||||
if fcstd_path.exists():
|
||||
fcstd_path.unlink()
|
||||
|
||||
assembly, preview, model_json, session_json = _build_leg_wheel_robot_dog_leg()
|
||||
model_path.write_text(model_json, encoding="utf-8")
|
||||
session_path.write_text(session_json, encoding="utf-8")
|
||||
scad.export_step(shapes=preview, filename=str(step_path))
|
||||
|
||||
imported = scad.import_model_json(json_str=model_json)
|
||||
replayed = scad.replay_model_json(json_str=model_json)
|
||||
payload = json.loads(model_json)
|
||||
|
||||
fcstd_status = "not attempted"
|
||||
try:
|
||||
scad.translator.freecad_translator.translate_model_json_to_fcstd(
|
||||
json_str=model_json,
|
||||
output_path=str(fcstd_path.resolve()),
|
||||
document_name="LegWheelRobotDogLeg",
|
||||
freecad_cmd=None,
|
||||
)
|
||||
fcstd_status = f"{fcstd_path} ({fcstd_path.stat().st_size} bytes)"
|
||||
except Exception as exc: # pragma: no cover - depends on local FreeCAD install
|
||||
fcstd_status = f"skipped ({exc.__class__.__name__}: {exc})"
|
||||
|
||||
print(f"assembly={assembly.assembly_id}")
|
||||
print("components=" + ",".join(assembly.component_ids()))
|
||||
print("constraints=" + ",".join(assembly.constraint_ids()))
|
||||
print(f"preview_solids={len(preview.get_solids())}")
|
||||
print(f"preview_volume={preview.get_volume():.3f}")
|
||||
print(f"imported_keys={','.join(sorted(imported.keys()))}")
|
||||
print(f"replay_outputs={len(replayed)}")
|
||||
print("replay_types=" + ",".join(type(item).__name__ for item in replayed))
|
||||
print(f"graph_nodes={len(payload['graph']['nodes'])}")
|
||||
print(f"model={model_path}")
|
||||
print(f"session={session_path}")
|
||||
print(f"step={step_path}")
|
||||
print(f"fcstd={fcstd_status}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user